UNPKG

react-native-adapty

Version:
215 lines (197 loc) 6.2 kB
import type { EventHandlers } from './types'; import { $bridge } from '@/bridge'; import { EmitterSubscription } from 'react-native'; type EventName = keyof EventHandlers; // Emitting view ID is passed in JSON["_view_id"] // So that no all visible views would emit this event // Must be in every callback response in the form of UUID string // const KEY_VIEW = 'view_id'; /** * @remarks * View emitter wraps NativeEventEmitter * and provides several modifications: * - Synthetic type restrictions to avoid misspelling * - Safe data deserialization with SDK decoders * - Logging emitting and deserialization processes * - Filters out events for other views by _id * * @internal */ export class ViewEmitter { private viewId: string; private eventListeners: Map<string, EmitterSubscription> = new Map(); private handlers: Map< string, Array<{ handler: EventHandlers[keyof EventHandlers]; config: (typeof HANDLER_TO_EVENT_CONFIG)[keyof typeof HANDLER_TO_EVENT_CONFIG]; onRequestClose: () => Promise<void>; }> > = new Map(); constructor(viewId: string) { this.viewId = viewId; } public addListener( event: EventName, callback: EventHandlers[EventName], onRequestClose: () => Promise<void>, ): EmitterSubscription { const viewId = this.viewId; const config = HANDLER_TO_EVENT_CONFIG[event]; if (!config) { throw new Error(`No event config found for handler: ${event}`); } const handlersForEvent = this.handlers.get(config.nativeEvent) ?? []; handlersForEvent.push({ handler: callback, config, onRequestClose, }); this.handlers.set(config.nativeEvent, handlersForEvent); if (!this.eventListeners.has(config.nativeEvent)) { const handlers = this.handlers; // Capture the reference const subscription = $bridge.addEventListener( config.nativeEvent, function (arg) { const eventViewId = this.rawValue['view']?.['id'] ?? null; if (viewId !== eventViewId) { return; } const eventHandlers = handlers.get(config.nativeEvent) ?? []; for (const { handler, config, onRequestClose } of eventHandlers) { if ( config.propertyMap && (arg as any)['action']?.type !== config.propertyMap['action'] ) { continue; } const callbackArgs = extractCallbackArgs( config.handlerName, arg as Record<string, any>, ); const cb = handler as (...args: typeof callbackArgs) => boolean; const shouldClose = cb.apply(null, callbackArgs); if (shouldClose) { onRequestClose(); } } }, ); this.eventListeners.set(config.nativeEvent, subscription); } return this.eventListeners.get(config.nativeEvent)!; } public removeAllListeners() { this.eventListeners.forEach(subscription => subscription.remove()); this.eventListeners.clear(); this.handlers.clear(); $bridge.removeAllEventListeners(); } } type UiEventMapping = { [nativeEventId: string]: { handlerName: keyof EventHandlers; propertyMap?: { [key: string]: string; }; }[]; }; const UI_EVENT_MAPPINGS: UiEventMapping = { paywall_view_did_perform_action: [ { handlerName: 'onCloseButtonPress', propertyMap: { action: 'close', }, }, { handlerName: 'onAndroidSystemBack', propertyMap: { action: 'system_back', }, }, { handlerName: 'onUrlPress', propertyMap: { action: 'open_url', }, }, { handlerName: 'onCustomAction', propertyMap: { action: 'custom', }, }, ], paywall_view_did_select_product: [{ handlerName: 'onProductSelected' }], paywall_view_did_start_purchase: [{ handlerName: 'onPurchaseStarted' }], paywall_view_did_finish_purchase: [{ handlerName: 'onPurchaseCompleted' }], paywall_view_did_fail_purchase: [{ handlerName: 'onPurchaseFailed' }], paywall_view_did_start_restore: [{ handlerName: 'onRestoreStarted' }], paywall_view_did_appear: [{ handlerName: 'onPaywallShown' }], paywall_view_did_disappear: [{ handlerName: 'onPaywallClosed' }], paywall_view_did_finish_web_payment_navigation: [ { handlerName: 'onWebPaymentNavigationFinished' }, ], paywall_view_did_finish_restore: [{ handlerName: 'onRestoreCompleted' }], paywall_view_did_fail_restore: [{ handlerName: 'onRestoreFailed' }], paywall_view_did_fail_rendering: [{ handlerName: 'onRenderingFailed' }], paywall_view_did_fail_loading_products: [ { handlerName: 'onLoadingProductsFailed' }, ], }; const HANDLER_TO_EVENT_CONFIG: Record< EventName, { nativeEvent: string; propertyMap?: { [key: string]: string }; handlerName: EventName; } > = Object.entries(UI_EVENT_MAPPINGS).reduce( (acc, [nativeEvent, mappings]) => { mappings.forEach(({ handlerName, propertyMap }) => { acc[handlerName] = { nativeEvent, propertyMap, handlerName, }; }); return acc; }, {} as Record< EventName, { nativeEvent: string; propertyMap?: { [key: string]: string }; handlerName: EventName; } >, ); function extractCallbackArgs( handlerName: EventName, eventArg: Record<string, any>, ) { switch (handlerName) { case 'onProductSelected': return [eventArg['product_id']]; case 'onPurchaseStarted': return [eventArg['product']]; case 'onPurchaseCompleted': return [eventArg['purchased_result'], eventArg['product']]; case 'onPurchaseFailed': return [eventArg['error'], eventArg['product']]; case 'onRestoreCompleted': return [eventArg['profile']]; case 'onRestoreFailed': case 'onRenderingFailed': case 'onLoadingProductsFailed': return [eventArg['error']]; case 'onCustomAction': case 'onUrlPress': return [eventArg['action'].value]; case 'onWebPaymentNavigationFinished': return [eventArg['product'], eventArg['error']]; default: return []; } }