Skip to main content

Overview

The postMessage API provides secure, real-time cross-origin communication between your parent window and the embedded webview. This is the recommended method for handling sensitive authentication data.

How It Works

1

Generate Nonce Token

Your backend calls the Doshi API to generate a nonce token
2

Webview Sends PING

The embedded webview sends a PING message to signal it’s ready
3

Parent Receives PING

Parent window listens for the PING and prepares authentication data
4

Parent Sends AUTH

Parent sends the nonce token and user data back to the webview
5

Webview Processes AUTH

Webview receives and processes the authentication, handling 2FA if needed

TypeScript Interfaces

interface AuthData {
  token: string;              // Required - nonce token from API
  email?: string;             // Optional - user's email
  segment?: string;           // Optional - for multiple paths
  branchId?: string;          // Optional - branch identifier
  
  // 2FA Parameters (optional, required when is2FaEnabled is true)
  is2FaEnabled?: boolean;     // Enable 2FA flow in iframe
  dob?: string;               // Date of birth (YYYY-MM-DD)
  organizationId?: string;    // Organization ID
  partnerUserId?: string;     // Partner user ID
  firstName?: string;         // First name
  lastName?: string;          // Last name
}

interface WebviewMessage {
  type: string;
  [key: string]: any;
}

React Implementation

Complete Example with API Call

import React, { useEffect, useState } from "react";

interface AuthData {
  token: string;
  email?: string;
  segment?: string;
  branchId?: string;
  is2FaEnabled?: boolean;
  dob?: string;
  organizationId?: string;
  partnerUserId?: string;
  firstName?: string;
  lastName?: string;
}

interface WebviewMessage {
  type: string;
  [key: string]: any;
}

interface Props {
  userEmail: string;
  segment?: string;
  branchId?: string;
  // 2FA props
  is2FaEnabled?: boolean;
  dob?: string;
  organizationId?: string;
  partnerUserId?: string;
  firstName?: string;
  lastName?: string;
}

const WebviewComponent: React.FC<Props> = ({ 
  userEmail,
  segment,
  branchId,
  is2FaEnabled = false,
  dob,
  organizationId,
  partnerUserId,
  firstName,
  lastName
}) => {
  const [authToken, setAuthToken] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // Step 1: Get nonce token from your backend
  useEffect(() => {
    async function fetchToken() {
      try {
        // Call YOUR backend which securely calls Doshi API
        const response = await fetch('/api/generate-doshi-token', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            email: userEmail,
            branchId
          })
        });

        if (!response.ok) {
          throw new Error('Failed to generate token');
        }

        const data = await response.json();
        setAuthToken(data.token);
      } catch (error) {
        console.error('Failed to fetch auth token:', error);
        setError('Authentication failed. Please try again.');
      } finally {
        setIsLoading(false);
      }
    }

    fetchToken();
  }, [userEmail, branchId]);

  // Step 2: Setup postMessage listener
  useEffect(() => {
    if (!authToken) return;

    const handleMessage = (event: MessageEvent) => {
      // ✅ Verify origin in production
      if (event.origin !== 'https://embed.doshi.app') {
        console.warn('Rejected message from:', event.origin);
        return;
      }
      
      try {
        const data: WebviewMessage =
          typeof event.data === "string" ? JSON.parse(event.data) : event.data;

        // When we receive PING from webview, send the auth data
        if (data.type === "PING") {
          handleSendAuth();
        }
      } catch (error) {
        console.error("Error processing message:", error);
      }
    };

    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
  }, [authToken]);

  const handleSendAuth = () => {
    if (!authToken) return;

    // Build auth data object
    const authData: AuthData = {
      token: authToken,
      type: "AUTH"
    };

    // Add optional fields
    if (userEmail) authData.email = userEmail;
    if (segment) authData.segment = segment;
    if (branchId) authData.branchId = branchId;

    // Add 2FA fields if enabled
    if (is2FaEnabled) {
      authData.is2FaEnabled = true;
      if (dob) authData.dob = dob;
      if (organizationId) authData.organizationId = organizationId;
      if (partnerUserId) authData.partnerUserId = partnerUserId;
      if (firstName) authData.firstName = firstName;
      if (lastName) authData.lastName = lastName;
    }

    // Send to iframe
    const webview = document.querySelector("iframe");
    if (webview && webview.contentWindow) {
      webview.contentWindow.postMessage(
        JSON.stringify(authData),
        "https://embed.doshi.app" // ⚠️ Never use "*" in production
      );
    }
  };

  if (isLoading) {
    return (
      <div className="flex items-center justify-center h-screen">
        <div className="text-center">
          <div className="spinner mb-4"></div>
          <p>Loading...</p>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="flex items-center justify-center h-screen">
        <div className="text-center text-red-600">
          <p>{error}</p>
          <button 
            onClick={() => window.location.reload()}
            className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
          >
            Retry
          </button>
        </div>
      </div>
    );
  }

  if (!authToken) {
    return <div>Failed to authenticate</div>;
  }

  return (
    <div className="webview-container h-screen w-full">
      <iframe
        src="https://embed.doshi.app"
        className="h-full w-full"
        frameBorder="0"
        allowFullScreen
      />
    </div>
  );
};

export default WebviewComponent;

Usage Example

import WebviewComponent from './WebviewComponent';

function App() {
  return (
    <div className="app">
      <h1>My Application</h1>
      <WebviewComponent 
        userEmail="[email protected]"
        segment="premium"
        branchId="branch_789"
      />
    </div>
  );
}

// With 2FA enabled
function AppWith2FA() {
  return (
    <div className="app">
      <h1>My Application</h1>
      <WebviewComponent 
        userEmail="[email protected]"
        segment="premium"
        branchId="branch_789"
        is2FaEnabled={true}
        dob="1990-01-15"
        organizationId="org_123"
        partnerUserId="partner_123"
        firstName="John"
        lastName="Doe"
      />
    </div>
  );
}

Vanilla JavaScript Implementation

Parent Window

// Configuration
const config = {
  doshiEmbedUrl: 'https://embed.doshi.app',
  backendUrl: '/api/generate-doshi-token'
};

// User data
const userData = {
  email: "[email protected]",
  segment: "premium",
  branchId: "branch_789"
};

// 2FA data (optional)
const twoFactorData = {
  is2FaEnabled: true,
  dob: "1990-01-15",
  organizationId: "org_123",
  partnerUserId: "partner_123",
  firstName: "John",
  lastName: "Doe"
};

let authToken = null;

// Step 1: Get nonce token from your backend
async function fetchAuthToken() {
  try {
    const response = await fetch(config.backendUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        email: userData.email,
        branchId: userData.branchId
      })
    });

    if (!response.ok) {
      throw new Error('Failed to generate token');
    }

    const data = await response.json();
    authToken = data.token;
    
    // Create iframe after token is ready
    createWebview();
  } catch (error) {
    console.error('Failed to fetch auth token:', error);
    showError('Authentication failed. Please try again.');
  }
}

// Step 2: Create and add webview to the page
function createWebview() {
  const webview = document.createElement("iframe");
  webview.src = config.doshiEmbedUrl;
  webview.className = "h-full w-full";
  webview.frameBorder = "0";
  webview.allowFullscreen = true;

  document.querySelector("#doshi-container").appendChild(webview);
  
  // Setup message listener
  setupMessageListener();
}

// Step 3: Handle messages from webview
function setupMessageListener() {
  window.addEventListener("message", handleMessage);
  
  // Cleanup on page unload
  window.addEventListener("beforeunload", () => {
    window.removeEventListener("message", handleMessage);
  });
}

function handleMessage(event) {
  // ✅ Verify origin in production
  if (event.origin !== config.doshiEmbedUrl) {
    console.warn('Rejected message from:', event.origin);
    return;
  }
  
  try {
    const data = typeof event.data === "string" 
      ? JSON.parse(event.data) 
      : event.data;

    // When we receive PING from webview, send the auth data
    if (data.type === "PING") {
      sendAuthData();
    } else if (data.type === "AUTH_SUCCESS") {
      console.log('Authentication successful');
      onAuthSuccess(data);
    } else if (data.type === "AUTH_ERROR") {
      console.error('Authentication failed:', data.error);
      onAuthError(data.error);
    }
  } catch (error) {
    console.error("Error processing message:", error);
  }
}

// Step 4: Send auth data to webview
function sendAuthData() {
  if (!authToken) {
    console.error('No auth token available');
    return;
  }

  // Build auth data
  const authData = {
    token: authToken,
    email: userData.email,
    segment: userData.segment,
    branchId: userData.branchId,
    type: "AUTH"
  };

  // Add 2FA data if enabled
  if (twoFactorData.is2FaEnabled) {
    Object.assign(authData, twoFactorData);
  }

  // Send to webview
  const webview = document.querySelector("iframe");
  if (webview && webview.contentWindow) {
    webview.contentWindow.postMessage(
      JSON.stringify(authData),
      config.doshiEmbedUrl // ⚠️ Never use "*" in production
    );
  }
}

// Callback functions
function onAuthSuccess(data) {
  console.log('User authenticated:', data);
  // Hide loading indicator, update UI, etc.
}

function onAuthError(error) {
  console.error('Authentication error:', error);
  showError('Authentication failed. Please try again.');
}

function showError(message) {
  const container = document.querySelector("#doshi-container");
  container.innerHTML = `
    <div class="error-message">
      <p>${message}</p>
      <button onclick="window.location.reload()">Retry</button>
    </div>
  `;
}

// Initialize
fetchAuthToken();

HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Doshi Integration</title>
  <style>
    #doshi-container {
      width: 100%;
      height: 100vh;
      position: relative;
    }
    
    .error-message {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      text-align: center;
    }
    
    .error-message button {
      margin-top: 1rem;
      padding: 0.5rem 1rem;
      background-color: #3b82f6;
      color: white;
      border: none;
      border-radius: 0.375rem;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div id="doshi-container"></div>
  <script src="doshi-integration.js"></script>
</body>
</html>

Backend Implementation

Your backend should handle the API key securely:
// backend/routes/auth.ts
import express from 'express';

const router = express.Router();

router.post('/generate-doshi-token', async (req, res) => {
  try {
    // Verify user is authenticated in your system
    if (!req.session?.userId) {
      return res.status(401).json({ error: 'Unauthorized' });
    }

    const { email, branchId } = req.body;

    // Call Doshi API with your API key
    const response = await fetch(
      'https://production-doshi-api-8kq2.encr.app/client/auth/token',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          email,
          partnerUserId: req.session.userId,
          branchId
        })
      }
    );

    if (!response.ok) {
      throw new Error('Doshi API request failed');
    }

    const data = await response.json();
    
    // Log for security monitoring
    console.log(`Generated token for user ${req.session.userId}`);
    
    res.json({ token: data.token });
  } catch (error) {
    console.error('Error generating Doshi token:', error);
    res.status(500).json({ error: 'Failed to generate authentication token' });
  }
});

export default router;

Child Window (Webview) Implementation

This section is for reference only. The Doshi iframe already implements this functionality.

Sending PING

// Inside the webview, send PING to parent when ready
window.parent.postMessage(
  JSON.stringify({ type: "PING" }),
  "*" // ⚠️ In production: specify parent origin if known
);

Receiving AUTH Data

// Listen for AUTH response from parent
window.addEventListener("message", (event) => {
  // ✅ Verify origin in production
  // if (event.origin !== 'https://parent-domain.com') return;
  
  try {
    const data = typeof event.data === "string" 
      ? JSON.parse(event.data) 
      : event.data;
    
    if (data.type === "AUTH") {
      // Process authentication data
      const { 
        token, 
        email, 
        segment, 
        branchId,
        is2FaEnabled,
        dob,
        organizationId,
        partnerUserId,
        firstName,
        lastName
      } = data;
      
      // Authenticate user with token
      await authenticateUser(token);
      
      // If 2FA is enabled, show OTP screen
      if (is2FaEnabled) {
        await handle2FAFlow({
          dob,
          organizationId,
          partnerUserId,
          firstName,
          lastName
        });
      }
      
      // Notify parent of success
      window.parent.postMessage(
        JSON.stringify({ 
          type: "AUTH_SUCCESS",
          userId: user.id 
        }),
        event.origin
      );
    }
  } catch (error) {
    console.error("Error processing auth message:", error);
    
    // Notify parent of error
    window.parent.postMessage(
      JSON.stringify({ 
        type: "AUTH_ERROR",
        error: error.message 
      }),
      event.origin
    );
  }
});

Advanced: Retry Logic

Add retry logic for more robust authentication:
const WebviewWithRetry: React.FC<Props> = ({ userEmail }) => {
  const [retryCount, setRetryCount] = useState(0);
  const MAX_RETRIES = 3;
  const RETRY_DELAY = 2000; // 2 seconds

  const handleSendAuth = async () => {
    try {
      const authData = {
        token: authToken,
        email: userEmail,
        type: "AUTH"
      };

      const webview = document.querySelector("iframe");
      if (webview && webview.contentWindow) {
        webview.contentWindow.postMessage(
          JSON.stringify(authData),
          "https://embed.doshi.app"
        );
      } else {
        throw new Error("Webview not found");
      }
    } catch (error) {
      console.error("Error sending auth:", error);
      
      if (retryCount < MAX_RETRIES) {
        console.log(`Retrying... (${retryCount + 1}/${MAX_RETRIES})`);
        setTimeout(() => {
          setRetryCount(retryCount + 1);
          handleSendAuth();
        }, RETRY_DELAY);
      } else {
        setError('Failed to authenticate after multiple attempts');
      }
    }
  };

  // ... rest of component
};

Message Types

type
string
required
The message type identifier

Testing postMessage Flow

// Test the authentication flow
function testPostMessageFlow() {
  console.log('Testing postMessage flow...');
  
  // Simulate PING from iframe
  window.postMessage(
    JSON.stringify({ type: "PING" }),
    window.location.origin
  );
  
  // Listen for AUTH response
  window.addEventListener("message", (event) => {
    const data = JSON.parse(event.data);
    console.log("Received message:", data);
    
    if (data.type === "AUTH") {
      console.log("✅ AUTH message received");
      console.log("Token:", data.token);
      console.log("Email:", data.email);
    }
  });
}

Common Issues

Solutions:
  1. Check that origin validation matches exactly
  2. Verify iframe has loaded before sending messages
  3. Ensure JSON.stringify is used when sending data
  4. Check browser console for errors
Solutions:
  1. Ensure backend API call completed successfully
  2. Check network tab for API response
  3. Verify error handling in token fetch logic
  4. Check that API key is valid
Solutions:
  1. Verify is2FaEnabled is set to true
  2. Ensure all required 2FA fields are provided
  3. Check that organization has 2FA enabled
  4. Verify user phone number is registered

Next Steps