UNPKG

@getpassage/react-native

Version:

Passage React Native SDK for mobile authentication

395 lines (394 loc) 18.9 kB
import React, { createContext, useState, useCallback, useRef } from "react"; import { Alert } from "react-native"; import { WebViewModal } from "./WebViewModal"; import { RemoteControlManager } from "./remote-control"; import { logger } from "./logger"; import { analytics, ANALYTICS_EVENTS } from "./analytics"; import { extractDebugFlag, extractRecordFlag } from "./jwt-utils"; import { DEFAULT_WEB_BASE_URL, DEFAULT_API_BASE_URL, DEFAULT_SOCKET_URL, DEFAULT_SOCKET_NAMESPACE, CONNECT_PATH, } from "./config"; export const PassageContext = createContext(null); export const PassageProvider = ({ children, config = {}, }) => { const [isOpen, setIsOpen] = useState(false); const [webViewUrl, setWebViewUrl] = useState(""); const [presentationStyle, setPresentationStyle] = useState("modal"); const [isConnected, setIsConnected] = useState(false); const [intentToken, setIntentToken] = useState(null); const [sessionData, setSessionData] = useState(null); const [automationUserAgent, setAutomationUserAgent] = useState(undefined); const [automationUrl, setAutomationUrl] = useState(undefined); const [marginBottom, setMarginBottom] = useState(undefined); // Initialize logger and analytics with debug mode React.useEffect(() => { var _a; logger.setDebugMode((_a = config.debug) !== null && _a !== void 0 ? _a : false); logger.debug("[PASSAGE PROVIDER] Initialized with config:", config); // Configure analytics (enabled by default) analytics.configure({ enabled: config.analytics !== false }); }, [config.debug, config.analytics]); // Store callbacks in refs to avoid re-renders const onConnectionCompleteRef = useRef(undefined); const onConnectionErrorRef = useRef(undefined); const onDataCompleteRef = useRef(undefined); const onPromptCompleteRef = useRef(undefined); const onExitRef = useRef(undefined); const onWebviewChangeRef = useRef(undefined); const remoteControl = RemoteControlManager.getInstance(); // Check JWT flags and enable debug mode if needed const checkJWTForDebugMode = useCallback((token) => { try { const hasDebugFlag = extractDebugFlag(token); const hasRecordFlag = extractRecordFlag(token); if (hasDebugFlag) { logger.debug("[PASSAGE PROVIDER] Debug flag found in JWT, enabling debug mode"); logger.setDebugMode(true); } else if (hasRecordFlag) { logger.debug("[PASSAGE PROVIDER] Record flag found in JWT, enabling debug mode"); logger.setDebugMode(true); } } catch (error) { logger.debug("[PASSAGE PROVIDER] Error checking JWT flags:", error); } }, []); // Handle image capture const handleImageCaptured = useCallback((imageUri) => { remoteControl.setCurrentImage(imageUri); }, []); // Generate intent token const generateIntentToken = useCallback(async (publishableKey, prompts = []) => { try { const apiUrl = config.apiUrl || DEFAULT_API_BASE_URL; const payload = { publishableKey, prompts, }; logger.debug("[PASSAGE PROVIDER] Generating intent token with payload:", { publishableKey, promptsCount: prompts.length, prompts: prompts.map((p) => ({ identifier: p.identifier, prompt: p.prompt, integrationid: p.integrationid, forceRefresh: p.forceRefresh, })), }); const response = await fetch(`${apiUrl}/intent-token`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Failed to generate intent token: ${response.status}`); } const data = await response.json(); logger.debug("[PASSAGE PROVIDER] Intent token generated successfully"); return data.intentToken; } catch (error) { logger.error("[PASSAGE PROVIDER] Failed to generate intent token:", error); throw error; } }, [config.apiUrl]); const initialize = useCallback(async (options) => { var _a, _b, _c; logger.debug("[PASSAGE PROVIDER] Initializing with options:", { publishableKey: options.publishableKey, hasPrompts: !!((_a = options.prompts) === null || _a === void 0 ? void 0 : _a.length), promptsCount: ((_b = options.prompts) === null || _b === void 0 ? void 0 : _b.length) || 0, }); try { // Generate intent token const token = await generateIntentToken(options.publishableKey, options.prompts || []); setIntentToken(token); logger.updateIntentToken(token); analytics.updateIntentToken(token); // Check for debug flag in JWT and enable debug mode if present checkJWTForDebugMode(token); logger.debug("[PASSAGE PROVIDER] Initialization complete with session tracking"); } catch (error) { logger.error("[PASSAGE PROVIDER] Initialization failed:", error); (_c = options.onError) === null || _c === void 0 ? void 0 : _c.call(options, { error: error instanceof Error ? error.message : "Initialization failed", data: error, }); } }, [generateIntentToken]); const buildUrlFromOptions = (token, sdkSession) => { const webUrl = config.webUrl || DEFAULT_WEB_BASE_URL; const url = new URL(`${webUrl}${CONNECT_PATH}`); url.searchParams.append("intentToken", token); if (sdkSession) { url.searchParams.append("sdkSession", sdkSession); } const urlString = url.toString(); logger.debug("[PASSAGE PROVIDER] Built URL from options:", { webUrl, intentToken: token, sdkSession, finalUrl: urlString, }); return urlString; }; const open = useCallback(async (options = {}) => { var _a, _b, _c, _d; // Use provided intent token or the stored one const token = options.intentToken || intentToken; if (!token) { const error = "No intent token available. Initialize first or provide intentToken in options"; logger.error("[PASSAGE PROVIDER]", error); (_a = options.onConnectionError) === null || _a === void 0 ? void 0 : _a.call(options, { error }); return; } // Generate SDK session if not provided const sdkSession = options.sdkSession || `sdk-session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; logger.debug("[PASSAGE PROVIDER] Opening passage with options:", { intentToken: token, sdkSession, hasPrompts: !!((_b = options.prompts) === null || _b === void 0 ? void 0 : _b.length), presentationStyle: options.presentationStyle, marginBottom: options.marginBottom, }); // Update logger and analytics with intent token for session tracking logger.updateIntentToken(token); analytics.updateIntentToken(token); // Check for debug flag in JWT and enable debug mode if present checkJWTForDebugMode(token); // Track SDK open request analytics.track(ANALYTICS_EVENTS.SDK_OPEN_REQUEST, { intentToken: token, sdkSession, hasPrompts: !!((_c = options.prompts) === null || _c === void 0 ? void 0 : _c.length), presentationStyle: options.presentationStyle, }); const url = buildUrlFromOptions(token, sdkSession); // Store callbacks onConnectionCompleteRef.current = options.onConnectionComplete; onConnectionErrorRef.current = options.onConnectionError; onDataCompleteRef.current = options.onDataComplete; onPromptCompleteRef.current = options.onPromptComplete; onExitRef.current = options.onExit; onWebviewChangeRef.current = options.onWebviewChange; const apiUrl = config.apiUrl || DEFAULT_API_BASE_URL; const socketUrl = config.socketUrl || DEFAULT_SOCKET_URL; const socketNamespace = config.socketNamespace || DEFAULT_SOCKET_NAMESPACE; logger.debug("[PASSAGE PROVIDER] Using connection config:", { apiUrl, socketUrl, socketNamespace, }); // Set configuration callback before connecting to fix race condition remoteControl.setConfigurationCallback((config) => { logger.debug("[PASSAGE PROVIDER] Configuration updated:", config); if (config.userAgent && config.userAgent.trim() !== "") { setAutomationUserAgent(config.userAgent); } else { setAutomationUserAgent(undefined); } if (config.integrationUrl) { setAutomationUrl(config.integrationUrl); } }); // Connect remote control try { await remoteControl.connect(socketUrl, socketNamespace, token, { onSuccess: (data) => { var _a, _b, _c; logger.debug("[PASSAGE PROVIDER] Connection successful (done command):", { hasData: !!data.data, hasSessionInfo: !!data.sessionInfo, cookieCount: ((_b = (_a = data.sessionInfo) === null || _a === void 0 ? void 0 : _a.cookies) === null || _b === void 0 ? void 0 : _b.length) || 0, }); setIsConnected(true); // Store session data const sessionDataResult = { data: data.data, prompts: [], // Will be populated by prompt processing }; setSessionData(sessionDataResult); (_c = onConnectionCompleteRef.current) === null || _c === void 0 ? void 0 : _c.call(onConnectionCompleteRef, data); }, onError: (error) => { var _a; logger.debug("[PASSAGE PROVIDER] Connection error:", error); // Show alert for invalid session if ((error === null || error === void 0 ? void 0 : error.error) === "Session is not valid anymore") { logger.debug("[PASSAGE PROVIDER] Session invalid, showing alert"); Alert.alert("Session Invalid", "Session is not valid anymore", [ { text: "OK", onPress: () => { } }, ]); } (_a = onConnectionErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onConnectionErrorRef, error); }, onDataComplete: (data) => { var _a; logger.debug("[PASSAGE PROVIDER] Data complete event received:", data); // Update session data and call onDataComplete const sessionDataResult = { data: data, prompts: [], // Will be populated by prompt processing }; setSessionData(sessionDataResult); (_a = onDataCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onDataCompleteRef, sessionDataResult); }, onPromptComplete: (prompt) => { var _a; logger.debug("[PASSAGE PROVIDER] Prompt complete event received:", prompt); // Call onPromptComplete callback (_a = onPromptCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onPromptCompleteRef, prompt); }, }, apiUrl, config.webUrl || DEFAULT_WEB_BASE_URL); // Only open modal if connection succeeded logger.debug("[PASSAGE PROVIDER] Opening WebView modal with URL:", url); setWebViewUrl(url); setPresentationStyle(options.presentationStyle || "modal"); setMarginBottom(options.marginBottom); setIsOpen(true); // Track modal opened analytics.track(ANALYTICS_EVENTS.SDK_MODAL_OPENED, { presentationStyle: options.presentationStyle || "modal", timestamp: new Date().toISOString(), }); // Track SDK open success analytics.track(ANALYTICS_EVENTS.SDK_OPEN_SUCCESS); } catch (error) { // Handle any unexpected errors logger.error("[PASSAGE PROVIDER] Failed to open Passage:", error); // Track SDK open error analytics.track(ANALYTICS_EVENTS.SDK_OPEN_ERROR, { error: error instanceof Error ? error.message : String(error), }); (_d = onConnectionErrorRef.current) === null || _d === void 0 ? void 0 : _d.call(onConnectionErrorRef, { error: error instanceof Error ? error.message : "Failed to open Passage", data: error, }); } }, [config, intentToken, buildUrlFromOptions]); const close = useCallback(async () => { logger.debug("[PASSAGE PROVIDER] Closing modal", { isConnected, hasExitCallback: !!onExitRef.current, }); setIsOpen(false); // If not connected yet, this is a manual exit if (!isConnected && onExitRef.current) { logger.debug("[PASSAGE PROVIDER] User manually closed modal before connection"); onExitRef.current("manual_close"); } // Track modal closed analytics.track(ANALYTICS_EVENTS.SDK_MODAL_CLOSED, { timestamp: new Date().toISOString(), }); setIsConnected(false); setWebViewUrl(""); setAutomationUserAgent(undefined); setAutomationUrl(undefined); setMarginBottom(undefined); await remoteControl.emitModalExit(); remoteControl.disconnect(); remoteControl.setConfigurationCallback(null); // Clear all callbacks onConnectionCompleteRef.current = undefined; onConnectionErrorRef.current = undefined; onDataCompleteRef.current = undefined; onPromptCompleteRef.current = undefined; onExitRef.current = undefined; onWebviewChangeRef.current = undefined; // Clear logger and analytics intent token logger.updateIntentToken(null); analytics.updateIntentToken(null); logger.debug("[PASSAGE PROVIDER] Modal closed and state cleared"); }, [isConnected]); const getData = useCallback(async () => { if (sessionData) { logger.debug("[PASSAGE PROVIDER] Returning cached session data"); return sessionData; } // If no cached data, return empty result logger.debug("[PASSAGE PROVIDER] No session data available"); return { data: null, prompts: [], }; }, [sessionData]); const connect = useCallback(async (options) => { var _a; logger.debug("[PASSAGE PROVIDER] Connecting in headless mode:", { intentToken: options.intentToken, hasMessageCallback: !!options.onMessage, hasErrorCallback: !!options.onError, hasCloseCallback: !!options.onClose, }); const socketUrl = config.socketUrl || DEFAULT_SOCKET_URL; const socketNamespace = config.socketNamespace || DEFAULT_SOCKET_NAMESPACE; const apiUrl = config.apiUrl || DEFAULT_API_BASE_URL; logger.debug("[PASSAGE PROVIDER] Headless connection config:", { socketUrl, socketNamespace, apiUrl, }); try { await remoteControl.connectHeadless(socketUrl, socketNamespace, options.intentToken, { onMessage: options.onMessage, onError: options.onError, onClose: options.onClose, }, apiUrl, config.webUrl || DEFAULT_WEB_BASE_URL); setIsConnected(true); logger.debug("[PASSAGE PROVIDER] Headless connection established"); } catch (error) { logger.error("[PASSAGE PROVIDER] Failed to connect to Passage:", error); (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, { error: error instanceof Error ? error.message : "Failed to connect", data: error, }); } }, [config]); const disconnect = useCallback(() => { logger.debug("[PASSAGE PROVIDER] Disconnecting headless connection"); remoteControl.disconnect(); setIsConnected(false); logger.debug("[PASSAGE PROVIDER] Headless connection disconnected"); }, []); const handleSuccess = useCallback((data) => { // This is handled internally by remote control now close(); }, [close]); const handleError = useCallback((error) => { // This is handled internally by remote control now close(); }, [close]); const completeRecording = useCallback(async (data) => { logger.debug("[PASSAGE PROVIDER] Completing recording with data:", data); await remoteControl.completeRecording(data); }, []); const captureRecordingData = useCallback(async (data) => { logger.debug("[PASSAGE PROVIDER] Capturing recording data with data:", data); await remoteControl.captureRecordingData(data); }, []); const setOnWebviewChange = useCallback((callback) => { logger.debug("[PASSAGE PROVIDER] Setting webview change callback"); onWebviewChangeRef.current = callback; }, []); const contextValue = { initialize, open, close, getData, connect, disconnect, completeRecording, captureRecordingData, onWebviewChange: setOnWebviewChange, }; return (<PassageContext.Provider value={contextValue}> {children} <WebViewModal visible={isOpen} url={webViewUrl} automationUrl={automationUrl} presentationStyle={presentationStyle} automationUserAgent={automationUserAgent} marginBottom={marginBottom} onClose={close} onSuccess={handleSuccess} onError={handleError} onImageCaptured={handleImageCaptured} onWebviewChange={onWebviewChangeRef.current}/> </PassageContext.Provider>); };