import { NativeModules, NativeEventEmitter, EmitterSubscription, Platform, DeviceEventEmitter, } from 'react-native'; import NativeComplyCubeModule from './NativeComplyCubeModule'; import type { DesignTokensInput } from './DesignTokens'; const NativeIOS = NativeModules.ComplyCubeRNSDK; // iOS legacy module const NativeAndroid = NativeModules.ComplyCubeModule; // Android legacy module const NativeTurbo = NativeComplyCubeModule; // Codegen/Turbo module (if available) const Native = Platform.OS === 'ios' ? NativeIOS || NativeTurbo || NativeAndroid : NativeTurbo || NativeAndroid || NativeIOS; const EventSource = Platform.OS === 'ios' ? NativeIOS || Native : Native; if (!Native) { throw new Error('[ComplyCube] Native module not found. Did you add the package on both iOS & Android?'); } export type StartOptions = { clientID: string; clientToken: string; language?: string; lookAndFeel?: Record; designTokens?: DesignTokensInput; stages: any[]; // keep wide for compatibility // future flags (e.g., nfcEnabled) can live here cross-platform }; const LEGACY_SUCCESS = 'ComplyCubeSuccess'; const LEGACY_ERROR = 'ComplyCubeError'; const LEGACY_CANCEL = 'ComplyCubeCancel'; const TELEMETRY_NEW = 'ComplyCubeEvent'; const TELEMETRY_LEGACY = 'ComplyCubeCustomEvent'; const emitter = Platform.OS === 'ios' ? new NativeEventEmitter(EventSource as any) : DeviceEventEmitter; const debugLog = (...args: any[]) => { if (__DEV__ && Platform.OS === 'ios') { // eslint-disable-next-line no-console console.log('[ComplyCube]', ...args); } }; const emit = (type: string, payload: any) => { try { debugLog('emit', { type, payload }); emitter.emit(type, payload); } catch {} }; async function maybeCall(method: string, ...args: any[]) { if (typeof (Native as any)[method] !== 'function') { throw new Error(`[ComplyCube] Native method missing: ${method}`); } return (Native as any)[method](...args); } export async function start(options: StartOptions): Promise { const out = await startSafe(options); if (out.status === 'success') return out.result; const err: any = new Error(out.message); err.code = out.status === 'cancelled' ? 'E_CANCELLED' : 'E_SDK'; err.details = out.details; throw err; } // Types for clear call-sites export type StartOutcome = | { status: 'success'; result: string } | { status: 'cancelled'; message: string; details?: any } | { status: 'error'; message: string; details?: any }; export async function startSafe(options: StartOptions): Promise { // Prefer new native API if present if (typeof (Native as any).start === 'function') { debugLog('startSafe: invoking native start', { platform: Platform.OS, hasNativeStart: typeof (Native as any).start === 'function', workflowTemplateId: (options as any)?.workflowTemplateId, stageCount: Array.isArray((options as any)?.stages) ? (options as any).stages.length : 0, }); try { const result = await (Native as any).start(options); debugLog('startSafe: native resolved', result); return { status: 'success', result }; } catch (err: any) { const code = err?.code ?? ''; const message = err?.message || String(err); const details = err; debugLog('startSafe: native rejected', { code, message, details }); // Heuristic: map to cancelled vs error const isCancelled = code === 'E_CANCELLED' || code === 'Canceled' || code === 'USER_CANCELED' || err?.userCancelled === true || /cancel/i.test(message); const payload = { message, details }; // Emit for anyone listening emit(isCancelled ? LEGACY_CANCEL : LEGACY_ERROR, payload); // Resolve instead of throw return isCancelled ? { status: 'cancelled', message, details } : { status: 'error', message, details }; } } // Legacy bridge path — convert rejects to resolves return new Promise((resolve) => { let subs: EmitterSubscription[] = []; const cleanup = () => { subs.forEach(s => { try { s.remove(); } catch {} }); subs = []; }; const once = (fn: (...args: T) => void) => { let called = false; return (...args: T) => { if (called) return; called = true; cleanup(); fn(...args); }; }; const onSuccess = once((payload: any) => { debugLog('legacy onSuccess', payload); resolve({ status: 'success', result: payload }); }); const onCancel = once((payload: any) => { debugLog('legacy onCancel', payload); const message = payload?.message || 'User cancelled'; const outcome: StartOutcome = { status: 'cancelled', message, details: payload }; try { emit(LEGACY_CANCEL, payload); } finally { resolve(outcome); } }); const onError = once((payload: any) => { debugLog('legacy onError', payload); const message = payload?.message || payload?.description || 'Unknown error'; const outcome: StartOutcome = { status: 'error', message, details: payload }; try { emit(LEGACY_ERROR, payload); } finally { resolve(outcome); } }); try { debugLog('legacy: subscribing to native events'); subs.push(emitter.addListener(LEGACY_SUCCESS, onSuccess)); subs.push(emitter.addListener(LEGACY_CANCEL, onCancel)); subs.push(emitter.addListener(LEGACY_ERROR, onError)); debugLog('legacy: invoking native start'); maybeCall('start', options).catch((e: any) => { const message = e?.message || String(e); debugLog('legacy: native start rejected', message); onError({ message }); }); } catch (e: any) { const message = e?.message || String(e); debugLog('legacy: setup failed', message); cleanup(); const payload = { message }; emit(LEGACY_ERROR, payload); resolve({ status: 'error', message, details: payload }); } }); } export function subscribe(listener: (message: string) => void): () => void { const toText = (raw: any) => { if (typeof raw === 'string') return raw; try { return JSON.stringify(raw); } catch { return String(raw ?? ''); } }; const subs: Array<{ remove: () => void }> = []; const handler = (raw: any) => { debugLog('subscribe:event', raw); try { listener(toText(raw)); } catch (e) { console.error('[SDK] CustomEvents handler error:', e); } }; // Native-originated telemetry: subs.push(emitter.addListener(TELEMETRY_NEW, handler)); subs.push(emitter.addListener(TELEMETRY_LEGACY, handler)); subs.push(emitter.addListener(LEGACY_SUCCESS, handler)); subs.push(emitter.addListener(LEGACY_CANCEL, handler)); subs.push(emitter.addListener(LEGACY_ERROR, handler)); return () => { subs.forEach(s => { try { s.remove(); } catch {} }); }; } export default { start, startSafe, subscribe };