All files / src/utils modal-monitoring.ts

96.2% Statements 152/158
92.3% Branches 24/26
100% Functions 3/3
96.2% Lines 152/158

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 1591x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 21x 21x 2x 2x 2x 19x 19x 19x 19x 21x 1x 1x 1x 18x 18x 18x 18x 18x 17x 16x 21x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x       7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 11x 11x 11x 11x 11x 10x 9x 21x 8x 8x 8x 8x 8x 8x 8x 8x 8x       8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 21x 30x 30x 30x 30x 30x 30x 5x 5x 1x 1x 1x 1x 4x 4x 5x 5x 30x 30x 30x 30x 1x 1x 1x 1x 1x 59x 24x 24x 24x 59x 23x 23x 23x 59x  
/**
 * Modal iframe monitoring utilities
 */
 
import type { ThreeDSAuthResult } from '../types';
import { SDKError } from '../types';
import { CF_3DS_AUTH_COMPLETE, CF_AUTH_ERROR } from '../constants';
 
export interface MonitoringCallbacks {
  onComplete?: (result: ThreeDSAuthResult) => void;
  onError?: (error: Error) => void;
  onClose?: (result: ThreeDSAuthResult) => void;
  isStillActive: () => boolean;
  log?: (message: string, ...args: unknown[]) => void;
}
 
export interface MonitoringHandlers {
  messageHandler: ((event: MessageEvent) => void) | null;
  timeoutId: NodeJS.Timeout | null;
}
 
/**
 * Sets up postMessage listener for iframe communication
 */
export function setupIframeMonitoring(
  iframeElement: HTMLIFrameElement,
  timeout: number,
  callbacks: MonitoringCallbacks
): MonitoringHandlers {
  const handlers: MonitoringHandlers = {
    messageHandler: null,
    timeoutId: null,
  };
 
  callbacks.log?.('Setting up postMessage listener for iframe communication');
 
  // Set up postMessage listener
  handlers.messageHandler = (event: MessageEvent): void => {
    // Check if this modal is still active before processing messages
    if (!callbacks.isStillActive()) {
      callbacks.log?.('Received message but modal is no longer active, ignoring');
      return;
    }
 
    // Verify message is from our iframe (security check)
    // For cross-origin 3DS providers, we can't always verify the source
    // In production, you might want to add origin validation
    if (event.source !== iframeElement.contentWindow) {
      callbacks.log?.('Received message from unknown source, ignoring');
      return;
    }
 
    // Check if message is an error message
    if (
      event.data &&
      typeof event.data === 'object' &&
      'type' in event.data &&
      (event.data as { type: string }).type === CF_AUTH_ERROR
    ) {
      const errorData = event.data as {
        type: string;
        respMsg?: string;
        description?: string;
        data?: unknown;
      };
      callbacks.log?.('Received auth error message from iframe', errorData);
 
      // Double-check we're still active before processing
      if (!callbacks.isStillActive()) {
        callbacks.log?.('Modal no longer active, ignoring error message');
        return;
      }
 
      // Clean up
      cleanupMonitoring(handlers);
 
      // Create error and call onError callback
      const errorMessage =
        errorData.respMsg || errorData.description || 'Authentication error occurred';
      const error = new SDKError(errorMessage, 'AUTH_ERROR', errorData.data);
      callbacks.onError?.(error);
 
      // Close modal with error result
      callbacks.onClose?.({
        success: false,
        error: errorMessage,
        data: errorData as Record<string, unknown>,
      });
      return;
    }
 
    // Check if message is a 3DS completion message
    if (
      event.data &&
      typeof event.data === 'object' &&
      'type' in event.data &&
      (event.data as { type: string }).type === CF_3DS_AUTH_COMPLETE
    ) {
      const completionData = event.data as {
        type: string;
        statusCode?: number;
        [key: string]: unknown;
      };
      callbacks.log?.('Received 3DS completion message from iframe', completionData);
 
      // Double-check we're still active before processing
      if (!callbacks.isStillActive()) {
        callbacks.log?.('Modal no longer active, ignoring completion message');
        return;
      }
 
      // Remove type property for data
      const { type: _, ...resultData } = completionData;
      // Clean up
      cleanupMonitoring(handlers);
 
      // Close modal with result
      const success = completionData.statusCode == 200 || completionData.statusCode == 201;
      callbacks.onClose?.({
        success: success,
        data: resultData as Record<string, unknown>,
      });
    }
  };
 
  // Add event listener
  window.addEventListener('message', handlers.messageHandler);
 
  // Set up timeout fallback
  handlers.timeoutId = setTimeout(() => {
    // Check if this modal is still active before processing timeout
    if (!callbacks.isStillActive()) {
      callbacks.log?.('Timeout reached but modal is no longer active, ignoring');
      cleanupMonitoring(handlers);
      return;
    }
 
    callbacks.log?.('Timeout reached, closing modal');
    cleanupMonitoring(handlers);
    callbacks.onClose?.({ success: false, error: 'Authentication timeout' });
  }, timeout);
 
  return handlers;
}
 
/**
 * Cleans up monitoring resources
 */
export function cleanupMonitoring(handlers: MonitoringHandlers): void {
  if (handlers.messageHandler) {
    window.removeEventListener('message', handlers.messageHandler);
    handlers.messageHandler = null;
  }
  if (handlers.timeoutId) {
    clearTimeout(handlers.timeoutId);
    handlers.timeoutId = null;
  }
}