Skip to main content

Security Best Practices

API Key Management

Critical: Never expose your API key in client-side code. Always call the Doshi API from your backend.
// ✅ Correct - Backend handles API key
// backend/api/auth.ts
app.post('/generate-doshi-token', async (req, res) => {
  const response = await fetch('https://api.doshi.app/client/auth/token', {
    headers: {
      'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`
    }
  });
  // ...
});

// ❌ Never do this - API key exposed
// frontend/app.tsx
const response = await fetch('https://api.doshi.app/client/auth/token', {
  headers: {
    'Authorization': 'Bearer sk_live_abc123...' // EXPOSED!
  }
});

Environment Variables

Store your API key securely:
# .env
DOSHI_API_KEY=your_api_key_here
DOSHI_API_URL=https://api.doshi.app
// config.ts
export const config = {
  doshiApiKey: process.env.DOSHI_API_KEY,
  doshiApiUrl: process.env.DOSHI_API_URL,
};

// Never commit .env files
// Add to .gitignore:
// .env
// .env.local

Always Use HTTPS

// ✅ Correct
const webviewUrl = "https://embed.v2.doshi.app";
const apiUrl = "https://api.doshi.app";

// ❌ Never use HTTP in production
const insecureUrl = "http://embed.v2.doshi.app";

Implement Proper Origin Checks

// For postMessage
window.addEventListener("message", (event) => {
  // ✅ Always verify origin in production
  if (event.origin !== 'https://embed.v2.doshi.app') {
    console.warn('Rejected message from:', event.origin);
    return;
  }
  
  // Process message
});

Never Use Wildcards in Production

// ❌ Development/testing only
webview.contentWindow.postMessage(data, "*");

// ✅ Production - always specify exact origin
webview.contentWindow.postMessage(data, "https://embed.v2.doshi.app");

Validate All Data

function validateAuthData(data: any): data is AuthData {
  if (!data || typeof data !== 'object') {
    throw new Error('Invalid data format');
  }

  if (!data.token || typeof data.token !== 'string') {
    throw new Error('Missing or invalid token');
  }

  if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
    throw new Error('Invalid email format');
  }

  return true;
}

// Use in your code
try {
  validateAuthData(authData);
  // Proceed with authentication
} catch (error) {
  console.error('Validation failed:', error);
  // Handle error
}

Performance Best Practices

Minimize API Calls

Cache the nonce token temporarily if you need to render multiple iframes for the same user session
// Token cache with expiration
class TokenCache {
  private cache = new Map<string, { token: string; expires: number }>();
  private readonly TTL = 5 * 60 * 1000; // 5 minutes

  async getToken(userId: string): Promise<string> {
    const cached = this.cache.get(userId);
    
    if (cached && cached.expires > Date.now()) {
      return cached.token;
    }

    // Fetch new token
    const token = await this.fetchNewToken(userId);
    
    this.cache.set(userId, {
      token,
      expires: Date.now() + this.TTL
    });

    return token;
  }

  private async fetchNewToken(userId: string): Promise<string> {
    // Call your backend
    const response = await fetch('/api/generate-doshi-token', {
      method: 'POST',
      body: JSON.stringify({ userId })
    });
    const data = await response.json();
    return data.token;
  }
}

const tokenCache = new TokenCache();

Optimize Message Size

// ✅ Send all data in a single message
const authData = {
  token: user.token,
  email: user.email,
  segment: user.segment,
  branchId: user.branchId,
  type: "AUTH"
};
webview.contentWindow.postMessage(JSON.stringify(authData), origin);

// ❌ Avoid multiple sequential messages
webview.contentWindow.postMessage(JSON.stringify({ token }), origin);
webview.contentWindow.postMessage(JSON.stringify({ email }), origin);
webview.contentWindow.postMessage(JSON.stringify({ segment }), origin);

Implement Proper Cleanup

useEffect(() => {
  const handleMessage = (event: MessageEvent) => {
    // Handle message
  };

  window.addEventListener("message", handleMessage);
  
  // ✅ Always cleanup
  return () => {
    window.removeEventListener("message", handleMessage);
  };
}, []);

Lazy Load the Iframe

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

const LazyWebview: React.FC = () => {
  const [shouldLoad, setShouldLoad] = useState(false);

  useEffect(() => {
    // Load iframe only when needed
    const timer = setTimeout(() => {
      setShouldLoad(true);
    }, 1000);

    return () => clearTimeout(timer);
  }, []);

  if (!shouldLoad) {
    return <div>Preparing...</div>;
  }

  return <WebviewComponent />;
};

Preconnect to Doshi Domain

<!-- Add to <head> for faster iframe loading -->
<link rel="preconnect" href="https://embed.doshi.app">
<link rel="dns-prefetch" href="https://embed.doshi.app">

Mobile App Integration

When embedding Doshi Frontend in a mobile app (iOS/Android WebView), there are special considerations:

Disable Zoom

Zooming should be disabled on mobile to prevent layout issues and ensure consistent user experience.
For Web (HTML Meta Tag):
<!-- Add to your <head> tag -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
For React:
// Add to your main HTML file or use react-helmet
import { Helmet } from 'react-helmet';

function App() {
  return (
    <>
      <Helmet>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
      </Helmet>
      <DoshiEmbed />
    </>
  );
}
For iOS (Swift/UIWebView):
// Disable pinch to zoom
webView.scrollView.isScrollEnabled = false
webView.scrollView.bounces = false

// For WKWebView
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.scrollView.isScrollEnabled = false

// Inject viewport meta tag
let viewportScript = """
var meta = document.createElement('meta');
meta.setAttribute('name', 'viewport');
meta.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
document.getElementsByTagName('head')[0].appendChild(meta);
"""
let userScript = WKUserScript(source: viewportScript, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(userScript)
For Android (WebView):
// In your Activity or Fragment
webView.settings.apply {
    setSupportZoom(false)
    builtInZoomControls = false
    displayZoomControls = false
}

// Load HTML with viewport meta tag
val htmlContent = """
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
    <iframe src="https://embed.doshi.app?token=YOUR_TOKEN" 
            style="width:100%; height:100vh; border:none;">
    </iframe>
</body>
</html>
"""
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)

Handle Keyboard in Mobile Apps

When the keyboard opens in mobile apps, it can squeeze the iframe height and make content not visible. Handle this in your app environment.
The Doshi Frontend is designed to work within mobile WebViews, but your app needs to handle keyboard behavior correctly. iOS (Swift) - Adjust WebView when Keyboard Opens:
class ViewController: UIViewController {
    var webView: WKWebView!
    var webViewBottomConstraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Setup WebView with constraints
        webView = WKWebView()
        view.addSubview(webView)
        webView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        
        webViewBottomConstraint = webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        webViewBottomConstraint.isActive = true
        
        // Observe keyboard notifications
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    
    @objc func keyboardWillShow(_ notification: Notification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
            return
        }
        
        // Adjust WebView bottom constraint
        webViewBottomConstraint.constant = -keyboardFrame.height
        
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
    
    @objc func keyboardWillHide(_ notification: Notification) {
        // Reset WebView bottom constraint
        webViewBottomConstraint.constant = 0
        
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}
Android (Kotlin) - Adjust WebView when Keyboard Opens:
class WebViewActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Set window to adjust resize when keyboard appears
        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
        
        setContentView(R.layout.activity_webview)
        
        webView = findViewById(R.id.webView)
        webView.settings.apply {
            javaScriptEnabled = true
            domStorageEnabled = true
        }
        
        // Optional: Listen to keyboard visibility
        val rootView = findViewById<View>(android.R.id.content)
        rootView.viewTreeObserver.addOnGlobalLayoutListener {
            val heightDiff = rootView.rootView.height - rootView.height
            if (heightDiff > 200) {
                // Keyboard is visible
                onKeyboardVisible(heightDiff)
            } else {
                // Keyboard is hidden
                onKeyboardHidden()
            }
        }
    }
    
    private fun onKeyboardVisible(keyboardHeight: Int) {
        // Adjust WebView or scroll to focused input
        webView.evaluateJavascript("""
            (function() {
                var activeElement = document.activeElement;
                if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
                    activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
                }
            })();
        """, null)
    }
    
    private fun onKeyboardHidden() {
        // Reset any adjustments if needed
    }
}
React Native (if applicable):
import { KeyboardAvoidingView, Platform } from 'react-native';
import { WebView } from 'react-native-webview';

function DoshiWebView() {
  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      style={{ flex: 1 }}
    >
      <WebView
        source={{ uri: 'https://embed.doshi.app?token=YOUR_TOKEN' }}
        style={{ flex: 1 }}
        scrollEnabled={false}
        scalesPageToFit={false}
      />
    </KeyboardAvoidingView>
  );
}
In Manifest (Android):
<!-- In AndroidManifest.xml -->
<activity
    android:name=".WebViewActivity"
    android:windowSoftInputMode="adjustResize">
</activity>

Best Practices for Mobile

Always disable zoom for consistent experience
Handle keyboard visibility in your app wrapper
Use adjustResize on Android to resize layout when keyboard appears
Use constraint-based layout on iOS to adjust for keyboard
Test on various screen sizes and keyboard types
Ensure input fields remain visible when keyboard is open
Doshi Frontend cannot handle external link clicks that open in new windows/popups due to security restrictions.

Why Popups Are Blocked

For security reasons, Doshi Frontend does not support opening links in popups or new windows from within the iframe. This prevents:
  • Phishing attacks
  • Unauthorized redirects
  • Cross-site scripting vulnerabilities
When users click on external links within Doshi Frontend, the iframe will send a postMessage to your parent application. You must handle these messages and open links in your environment. Listen for Link Click Messages:
window.addEventListener('message', (event) => {
  // Verify origin
  if (event.origin !== 'https://embed.doshi.app') return;
  
  try {
    const data = typeof event.data === 'string' 
      ? JSON.parse(event.data) 
      : event.data;
    
    // Handle external link clicks
    if (data.type === 'EXTERNAL_LINK') {
      const { url, title } = data;
      
      // Open in same tab
      window.location.href = url;
      
      // Or open in new tab
      // window.open(url, '_blank');
      
      // Or handle in your app's navigation
      // navigateToExternal(url);
    }
  } catch (error) {
    console.error('Error handling message:', error);
  }
});
iOS (Swift) - Handle Link Callbacks:
// Add WKScriptMessageHandler
extension ViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, 
                              didReceive message: WKScriptMessage) {
        if message.name == "linkHandler" {
            if let body = message.body as? [String: Any],
               let url = body["url"] as? String {
                // Handle the link
                if let linkUrl = URL(string: url) {
                    // Option 1: Open in Safari
                    UIApplication.shared.open(linkUrl)
                    
                    // Option 2: Open in SFSafariViewController
                    let safariVC = SFSafariViewController(url: linkUrl)
                    present(safariVC, animated: true)
                }
            }
        }
    }
}

// Setup message handler
override func viewDidLoad() {
    super.viewDidLoad()
    
    let contentController = webView.configuration.userContentController
    contentController.add(self, name: "linkHandler")
    
    // Inject message handler script
    let script = """
    window.addEventListener('message', function(event) {
        if (event.data.type === 'EXTERNAL_LINK') {
            window.webkit.messageHandlers.linkHandler.postMessage({
                url: event.data.url,
                title: event.data.title
            });
        }
    });
    """
    
    let userScript = WKUserScript(
        source: script,
        injectionTime: .atDocumentEnd,
        forMainFrameOnly: false
    )
    contentController.addUserScript(userScript)
}
Android (Kotlin) - Handle Link Callbacks:
webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
        val url = request?.url.toString()
        
        // If it's an external link, handle it in your app
        if (url.startsWith("http") && !url.contains("embed.doshi.app")) {
            // Option 1: Open in external browser
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            startActivity(intent)
            return true
            
            // Option 2: Open in Chrome Custom Tabs
            val builder = CustomTabsIntent.Builder()
            val customTabsIntent = builder.build()
            customTabsIntent.launchUrl(this@WebViewActivity, Uri.parse(url))
            return true
        }
        
        return false
    }
}

// Add JavaScript interface for postMessage
webView.addJavascriptInterface(object {
    @JavascriptInterface
    fun handleExternalLink(url: String, title: String) {
        runOnUiThread {
            // Handle the link
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            startActivity(intent)
        }
    }
}, "AndroidInterface")

// Inject message listener
webView.evaluateJavascript("""
    window.addEventListener('message', function(event) {
        if (event.data.type === 'EXTERNAL_LINK') {
            AndroidInterface.handleExternalLink(event.data.url, event.data.title);
        }
    });
""", null)
React - Handle Link Callbacks:
import React, { useEffect } from 'react';

function DoshiEmbed() {
  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.origin !== 'https://embed.doshi.app') return;
      
      try {
        const data = typeof event.data === 'string'
          ? JSON.parse(event.data)
          : event.data;
        
        if (data.type === 'EXTERNAL_LINK') {
          // Option 1: Navigate in same window
          window.location.href = data.url;
          
          // Option 2: Open in new tab
          // window.open(data.url, '_blank', 'noopener,noreferrer');
          
          // Option 3: Use React Router (if applicable)
          // navigate(`/external?url=${encodeURIComponent(data.url)}`);
          
          // Option 4: Show confirmation dialog
          // if (confirm(`Open ${data.title || 'link'}?`)) {
          //   window.open(data.url, '_blank');
          // }
        }
      } catch (error) {
        console.error('Error handling link message:', error);
      }
    };
    
    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);
  
  return (
    <iframe
      src="https://embed.doshi.app"
      className="w-full h-screen"
      frameBorder="0"
      allowFullScreen
    />
  );
}

Message Format

When a user clicks an external link, Doshi Frontend sends:
{
  type: 'EXTERNAL_LINK',
  url: 'https://example.com/article',
  title: 'Article Title', // Optional
  timestamp: 1234567890
}

Security Considerations

Always verify event.origin before processing messages
Validate URLs before opening (check for malicious protocols)
Use noopener,noreferrer when opening links in new tabs
Consider showing user confirmation for external links
Log external link clicks for security auditing

URL Validation Example

function isValidUrl(url: string): boolean {
  try {
    const urlObj = new URL(url);
    
    // Only allow http and https protocols
    if (!['http:', 'https:'].includes(urlObj.protocol)) {
      return false;
    }
    
    // Optional: Whitelist or blacklist domains
    const blockedDomains = ['malicious-site.com', 'phishing-site.com'];
    if (blockedDomains.some(domain => urlObj.hostname.includes(domain))) {
      return false;
    }
    
    return true;
  } catch {
    return false;
  }
}

// Use in message handler
if (data.type === 'EXTERNAL_LINK') {
  if (isValidUrl(data.url)) {
    window.open(data.url, '_blank', 'noopener,noreferrer');
  } else {
    console.warn('Invalid or blocked URL:', data.url);
  }
}

Error Handling Best Practices

Comprehensive Error Handling

const handleAuthentication = async () => {
  try {
    // Attempt to get token
    const token = await fetchToken();
    
    if (!token) {
      throw new Error('No token received');
    }

    // Attempt to send to iframe
    await sendAuthToIframe(token);
    
  } catch (error) {
    // Log error
    console.error('Authentication error:', error);
    
    // Send to monitoring service
    if (window.analytics) {
      window.analytics.track('Authentication Failed', {
        error: error.message,
        timestamp: new Date().toISOString()
      });
    }
    
    // Show user-friendly message
    setError('Unable to authenticate. Please try again.');
    
    // Optionally retry
    if (retryCount < MAX_RETRIES) {
      setTimeout(() => {
        setRetryCount(retryCount + 1);
        handleAuthentication();
      }, RETRY_DELAY);
    }
  }
};

User Feedback

const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState<string | null>(null);

// Show appropriate UI based on status
return (
  <div>
    {status === 'loading' && (
      <div className="loading-state">
        <Spinner />
        <p>Authenticating...</p>
      </div>
    )}
    
    {status === 'error' && (
      <div className="error-state">
        <ErrorIcon />
        <p>{errorMessage}</p>
        <button onClick={retry}>Retry</button>
      </div>
    )}
    
    {status === 'success' && (
      <iframe src={webviewUrl} />
    )}
  </div>
);

Timeout Handling

const AUTH_TIMEOUT = 10000; // 10 seconds

const authenticateWithTimeout = () => {
  return Promise.race([
    authenticate(),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Authentication timeout')), AUTH_TIMEOUT)
    )
  ]);
};

try {
  await authenticateWithTimeout();
} catch (error) {
  if (error.message === 'Authentication timeout') {
    setError('Authentication is taking longer than expected. Please check your connection.');
  }
}

Method Selection Guide

When to Use postMessage

Handling sensitive authentication data
Need for real-time, bidirectional communication
Production environments
Multiple authentication steps
Maximum security requirements
Complex 2FA flows
// Use postMessage for production
const ProductionWebview = ({ userData }) => {
  // postMessage implementation
  // More secure, no data in URLs
};

When to Use Query Parameters

Simple, one-time authentication
Rapid prototyping and development
Need for easier debugging
Simpler implementation requirements
Quick demos or proof of concepts
// Use query params for development/testing
const DevelopmentWebview = ({ userData }) => {
  const url = `https://embed.doshi.app?token=${token}`;
  return <iframe src={url} />;
};

Code Organization

Separation of Concerns

// services/doshi-auth.service.ts
export class DoshiAuthService {
  private apiUrl: string;
  private embedUrl: string;

  constructor(config: Config) {
    this.apiUrl = config.apiUrl;
    this.embedUrl = config.embedUrl;
  }

  async getAuthToken(userData: UserData): Promise<string> {
    const response = await fetch(`${this.apiUrl}/generate-token`, {
      method: 'POST',
      body: JSON.stringify(userData)
    });
    const data = await response.json();
    return data.token;
  }

  buildEmbedUrl(token: string, params: AuthParams): string {
    const urlParams = new URLSearchParams({ token, ...params });
    return `${this.embedUrl}?${urlParams.toString()}`;
  }
}

// components/DoshiEmbed.tsx
export const DoshiEmbed: React.FC<Props> = ({ userData }) => {
  const authService = useDoshiAuthService();
  // Component logic
};

Configuration Management

// config/doshi.config.ts
export const doshiConfig = {
  development: {
    apiUrl: 'https://sandbox.api.doshi.app',
    embedUrl: 'https://staging-embed.doshi.app',
    enableDebugLogs: true,
    strictOriginCheck: false
  },
  production: {
    apiUrl: 'https://production-doshi-api-8kq2.encr.app',
    embedUrl: 'https://embed.doshi.app',
    enableDebugLogs: false,
    strictOriginCheck: true
  }
};

export const getConfig = () => {
  const env = process.env.NODE_ENV || 'development';
  return doshiConfig[env];
};

Styling Best Practices

Responsive Container

.webview-container {
  /* Full viewport height */
  width: 100%;
  height: 100vh;
  
  /* Prevent overflow */
  overflow: hidden;
  
  /* Optional: Add subtle border */
  border: 1px solid #e5e7eb;
  border-radius: 8px;
}

/* Mobile responsive */
@media (max-width: 768px) {
  .webview-container {
    /* Adjust for mobile viewport */
    height: calc(100vh - 60px); /* Account for mobile browser UI */
  }
}

/* Tablet */
@media (min-width: 769px) and (max-width: 1024px) {
  .webview-container {
    height: calc(100vh - 80px);
  }
}

Loading States

const LoadingOverlay: React.FC = () => (
  <div className="absolute inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50">
    <div className="text-center">
      <div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-solid border-blue-600 border-r-transparent"></div>
      <p className="mt-4 text-gray-700">Loading Doshi...</p>
    </div>
  </div>
);

// Usage
<div className="relative webview-container">
  {isLoading && <LoadingOverlay />}
  <iframe src={url} onLoad={() => setIsLoading(false)} />
</div>

Full-Screen Support

.webview-container iframe {
  width: 100%;
  height: 100%;
  border: none;
  display: block;
}

/* Allow fullscreen */
.webview-container iframe:fullscreen {
  width: 100vw;
  height: 100vh;
}

Testing Best Practices

Environment-Specific Testing

describe('Doshi Authentication', () => {
  beforeEach(() => {
    // Use sandbox environment for testing
    process.env.DOSHI_API_URL = 'https://sandbox.api.doshi.app';
  });

  it('should generate auth token', async () => {
    const token = await generateAuthToken({
      email: '[email protected]'
    });
    
    expect(token).toBeDefined();
    expect(typeof token).toBe('string');
  });

  it('should handle authentication errors', async () => {
    // Test error handling
    await expect(
      generateAuthToken({ email: 'invalid' })
    ).rejects.toThrow();
  });
});

Mock API Responses

// __mocks__/doshi-api.ts
export const mockDoshiApi = {
  generateToken: jest.fn().mockResolvedValue({
    token: 'mock_token_123',
    userId: 'user_123',
    isNewUser: false
  }),
  
  mockError: jest.fn().mockRejectedValue(
    new Error('API Error')
  )
};

Integration Testing

import { render, screen, waitFor } from '@testing-library/react';

test('displays webview after successful authentication', async () => {
  render(<DoshiEmbed userEmail="[email protected]" />);
  
  // Should show loading initially
  expect(screen.getByText('Loading...')).toBeInTheDocument();
  
  // Should show iframe after auth
  await waitFor(() => {
    expect(screen.getByTitle('Doshi Embed')).toBeInTheDocument();
  });
});

Monitoring and Logging

Track Key Events

// Track authentication flow
const trackAuthEvent = (event: string, metadata?: any) => {
  if (window.analytics) {
    window.analytics.track(event, {
      ...metadata,
      timestamp: new Date().toISOString(),
      environment: process.env.NODE_ENV
    });
  }
};

// Usage
trackAuthEvent('Auth Token Requested', { userId: user.id });
trackAuthEvent('Auth Token Received', { userId: user.id });
trackAuthEvent('Iframe Loaded', { userId: user.id });

Error Monitoring

// Send errors to monitoring service
const logError = (error: Error, context?: any) => {
  console.error('Doshi Auth Error:', error);
  
  // Send to error tracking service (e.g., Sentry)
  if (window.Sentry) {
    window.Sentry.captureException(error, {
      extra: context
    });
  }
};

// Usage
try {
  await authenticate();
} catch (error) {
  logError(error, {
    userId: user.id,
    action: 'authentication'
  });
}

Performance Monitoring

// Measure authentication performance
const measureAuthPerformance = async () => {
  const startTime = performance.now();
  
  try {
    await authenticate();
    const endTime = performance.now();
    const duration = endTime - startTime;
    
    // Log performance metric
    console.log(`Authentication took ${duration}ms`);
    
    // Send to analytics
    if (window.analytics) {
      window.analytics.track('Auth Performance', {
        duration,
        status: 'success'
      });
    }
  } catch (error) {
    const endTime = performance.now();
    const duration = endTime - startTime;
    
    if (window.analytics) {
      window.analytics.track('Auth Performance', {
        duration,
        status: 'error'
      });
    }
  }
};

Production Checklist

API key stored securely in environment variables
API calls made from backend only
HTTPS used for all URLs
Origin verification enabled for postMessage
No ”*” wildcards in production
Comprehensive error handling implemented
User feedback for all states (loading, error, success)
Timeout handling for slow connections
Proper cleanup of event listeners
Error logging and monitoring configured
Performance tracking in place
Tested across multiple browsers
Tested on mobile devices
Responsive design implemented
Accessibility considerations addressed

Next Steps