@getpassage/react-native
Version:
Passage React Native SDK for mobile authentication
395 lines (394 loc) • 18.9 kB
JavaScript
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>);
};