🔄 Communication Flow

The modal uses the browser's postMessage API for secure communication. Messages flow in both directions:

Parent → Modal
Configuration & control
Modal → Parent
Events & results

📤 Messages to Modal (Parent → Modal)

⚙️ api-config Parent → Modal

Send authentication and configuration data to the modal. This is the primary way to pass sensitive data securely.

JavaScript
iframe.contentWindow.postMessage({
  type: 'api-config',
  
  // Authentication
  jwtToken: 'your-jwt-token',
  
  // Item data (for ITEM purchases)
  playerEmail: 'player@example.com',
  playerLevel: 42,
  playerCountry: 'US',
  itemPrice: 29.99,
  itemId: 123,
  itemName: 'Premium Sword',
  
  // NFT data (for NFT purchases)
  walletAddress: '0x1234...abcd',
  collectionAddress: '0xNFT...collection',
  tokenId: '1',
  tokenPrice: '0.05',
  tokenName: 'Epic NFT',
  userEmail: 'user@example.com',
  chainId: '1',
  tokenAddress: '0xToken...',
  
  // Optional
  paymentMode: 'BNPL' // or 'UPFRONT'
}, '*');
🎨 modal-config Parent → Modal

Override the modal's appearance. Use this for custom branding when not using backend configuration.

JavaScript
iframe.contentWindow.postMessage({
  type: 'modal-config',
  
  // Colors
  primaryColor: '#6366f1',
  secondaryColor: '#8b5cf6',
  backgroundColor: '#ffffff',
  textColor: '#0f172a',
  accentColor: '#10b981',
  errorColor: '#ef4444',
  
  // Typography
  fontFamily: 'Inter, system-ui, sans-serif',
  fontUrl: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap',
  
  // Branding
  logoUrl: 'https://your-domain.com/logo.png',
  modalTitle: 'Complete Your Purchase',
  modalSubtitle: 'Choose your preferred payment method',
  
  // Border radius
  borderRadius: '1.25rem',
  buttonBorderRadius: '0.75rem',
  
  // Payment availability
  availableBnpl: true,
  availableUpfront: true
}, '*');
🔄 reset-modal Parent → Modal

Reset the modal to its initial state. Useful when reopening the modal for a new purchase.

JavaScript
iframe.contentWindow.postMessage({
  type: 'reset-modal'
}, '*');

📥 Messages from Modal (Modal → Parent)

payment-success Modal → Parent

Sent when a card payment is completed successfully via Stripe.

JavaScript
{
  type: 'payment-success',
  paymentIntentId: 'pi_1234567890',
  firstPaymentAmount: '9.99',      // BNPL first payment
  weeklyPaymentAmount: '5.00',     // BNPL weekly amount
  totalAmount: '29.99'             // Total item price
}
🔗 crypto-payment-success Modal → Parent

Sent when an NFT is successfully minted via crypto payment.

JavaScript
{
  type: 'crypto-payment-success',
  transactionHash: '0xabc...123',
  tokenId: '42'
}
payment-cancelled Modal → Parent

Sent when the user cancels the payment process.

JavaScript
{
  type: 'payment-cancelled'
}
⚠️ crypto-payment-error Modal → Parent

Sent when a crypto payment fails.

JavaScript
{
  type: 'crypto-payment-error',
  error: 'User rejected the transaction'
}
👆 modal-choice Modal → Parent

Sent when the user makes a selection (useful for analytics).

JavaScript
{
  type: 'modal-choice',
  choice: 'PayCard',              // or 'PayCrypto'
  productType: 'Item',            // or 'NFT'
  paymentType: 'BNPL',            // or 'UPFRONT'
  timestamp: 1703980800000,
  
  // Item data (when applicable)
  itemPrice: 29.99,
  itemId: 123,
  itemName: 'Premium Sword',
  playerEmail: 'player@example.com',
  playerLevel: 42,
  playerCountry: 'US'
}
🚪 modal-close Modal → Parent

Sent when the user clicks the Close button after viewing the success screen. Use this to close the modal overlay in your application.

JavaScript
{
  type: 'modal-close'
}
💡 Recommended Pattern

Handle payment-success to store the payment data, then use modal-close to actually close the modal overlay and redirect/update your UI.

👂 Listening for Messages

Here's a complete example of handling all modal messages:

JavaScript
// Store payment data to use when modal closes
let pendingPaymentData = null;

window.addEventListener('message', (event) => {
  // IMPORTANT: Ignore Stripe's internal messages
  if (event.origin === 'https://js.stripe.com') {
    return;
  }
  
  // Optional: Verify the origin matches your modal URL
  const MODAL_ORIGIN = 'https://modal.dev.merso.io';
  // if (event.origin !== MODAL_ORIGIN) return;
  
  const { type, ...data } = event.data || {};
  
  switch (type) {
    case 'payment-success':
      handlePaymentSuccess(data);
      break;
      
    case 'crypto-payment-success':
      handleCryptoSuccess(data);
      break;
      
    case 'payment-cancelled':
      handlePaymentCancelled();
      break;
      
    case 'crypto-payment-error':
      handleCryptoError(data.error);
      break;
      
    case 'modal-choice':
      trackAnalytics('modal_choice', data);
      break;
      
    case 'modal-close':
      handleModalClose();
      break;
      
    default:
      // Unknown message type - ignore
      break;
  }
});

function handlePaymentSuccess({ paymentIntentId, totalAmount, firstPaymentAmount, weeklyPaymentAmount }) {
  console.log('Payment successful!', paymentIntentId);
  // Store payment data - don't close modal yet (user sees success screen)
  pendingPaymentData = { paymentIntentId, totalAmount, firstPaymentAmount, weeklyPaymentAmount };
}

function handleCryptoSuccess({ transactionHash, tokenId }) {
  console.log('NFT minted!', transactionHash);
  // Store payment data - don't close modal yet (user sees success screen)
  pendingPaymentData = { transactionHash, tokenId };
}

function handleModalClose() {
  // User clicked Close after seeing success screen
  console.log('Modal closed by user');
  closeModal();
  
  if (pendingPaymentData) {
    showSuccessNotification('Purchase completed!');
    refreshUserInventory();
    pendingPaymentData = null;
  }
}

function handlePaymentCancelled() {
  console.log('Payment cancelled by user');
  closeModal();
}

function handleCryptoError(error) {
  console.error('Crypto payment failed:', error);
  showErrorNotification(error);
}
⚠️ Important: Stripe Messages

Stripe Elements uses postMessage internally. You'll see messages from https://js.stripe.com - these should be ignored as shown above.