import { Linking } from 'react-native'; import { NamiEventEmitter, PAYWALL_ACTION_EVENT, NamiPaywallAction, namiBuySKU, PaywallManagerEvents, hasCapability, Capabilities, logger, storageService, } from '@namiml/sdk-core'; import type { NamiSKU, NamiPaywallComponentChange, } from '@namiml/sdk-core'; import type { PaywallContextValue } from '../context/PaywallContext'; import { setVideoMuted, setVideoPlaying } from './videoControls'; export const ACTION_CLOSE_PAYWALL = 'namiClosePaywall'; export const ACTION_RESTORE_PURCHASES = 'namiRestorePurchases'; export const ACTION_SIGN_IN = 'namiSignIn'; export const ACTION_DEEP_LINK = 'namiDeeplink'; export const ACTION_BUY_SKU = 'namiBuySKU'; export const ACTION_NAVIGATE_TO_SCREEN = 'namiNavigateToScreen'; export const ACTION_SET_STATE = 'setState'; export const ACTION_SELECT_SKU = 'namiSelectSKU'; export const ACTION_PURCHASE_SELECTED = 'namiPurchaseSelectedSKU'; export const ACTION_RELOAD_PRODUCTS = 'namiReloadProducts'; export const ACTION_PLAY_VIDEO = 'namiPlayVideo'; export const ACTION_PAUSE_VIDEO = 'namiPauseVideo'; export const ACTION_MUTE_VIDEO = 'namiMuteVideo'; export const ACTION_UNMUTE_VIDEO = 'namiUnmuteVideo'; type NamiTapAction = { function: string; parameters?: Record; }; type ProductOffer = { offer_ref_id?: string; offer_type?: string; price?: number; }; type ProductDetailsWithOffers = { offers?: ProductOffer[]; }; type ActionSku = NamiSKU & { id?: string; skuId?: string; sku_ref_id?: string; promoId?: string; productDetails?: ProductDetailsWithOffers; product_details?: ProductDetailsWithOffers; }; type SetStateActionParams = { formId?: string; value?: string; }; interface HandleActionParams { onTap?: NamiTapAction; sku?: NamiSKU; componentChange?: NamiPaywallComponentChange; ctx: PaywallContextValue; onClose?: () => void; } export function handleAction({ onTap, sku, componentChange, ctx, onClose }: HandleActionParams): void { if (!onTap?.function) return; const eventData = ctx.getPaywallActionEventData(); switch (onTap.function) { case ACTION_CLOSE_PAYWALL: emitAction(NamiPaywallAction.CLOSE_PAYWALL, { ...eventData, componentChange }); closePaywallHandler(onClose); break; case ACTION_RESTORE_PURCHASES: emitAction(NamiPaywallAction.RESTORE_PURCHASES, { ...eventData, componentChange }); restorePurchase(); break; case ACTION_SIGN_IN: emitAction(NamiPaywallAction.SIGN_IN, { ...eventData, componentChange }); signIn(); break; case ACTION_DEEP_LINK: { const url = asOptionalString(onTap.parameters?.url); emitAction(NamiPaywallAction.DEEPLINK, { ...eventData, deeplinkUrl: url, componentChange }); deepLink(url); break; } case ACTION_BUY_SKU: { if (!sku) break; ctx.setPurchaseInProgress(true); const nextSku = buySKU(ctx, onTap, sku); emitAction(NamiPaywallAction.BUY_SKU, { ...eventData, sku: nextSku, componentChange }); break; } case ACTION_SELECT_SKU: { if (!sku) break; const skuId = getSkuIdentifier(sku as ActionSku); if (!skuId) break; const products = { ...ctx.state.selectedProducts, [ctx.state.currentGroupId]: skuId }; ctx.setSelectedProducts(products); emitAction(NamiPaywallAction.SELECT_SKU, { ...eventData, sku, componentChange }); break; } case ACTION_NAVIGATE_TO_SCREEN: { const screen = asOptionalString(onTap.parameters?.screen); if (screen) { ctx.setCurrentPage(screen); } emitAction(NamiPaywallAction.PAGE_CHANGE, { ...eventData, componentChange }); break; } case ACTION_SET_STATE: { const params = onTap.parameters as SetStateActionParams | undefined; if (params?.formId) { ctx.setCurrentFormId(params.formId, params.value); } else { ctx.setCurrentGroupData(ctx.state.currentGroupId, (ctx.state as any).currentGroupName ?? ''); } emitAction(NamiPaywallAction.TOGGLE_CHANGE, { ...eventData, componentChange }); break; } case ACTION_PURCHASE_SELECTED: { if (sku) { ctx.setPurchase(true, sku); } emitAction(NamiPaywallAction.PURCHASE_SELECTED_SKU, { ...eventData, sku, componentChange }); break; } case ACTION_RELOAD_PRODUCTS: emitAction(NamiPaywallAction.UNKNOWN, eventData); break; case ACTION_PLAY_VIDEO: setVideoPlaying(true); emitAction(NamiPaywallAction.UNKNOWN, { ...eventData, componentChange }); break; case ACTION_PAUSE_VIDEO: setVideoPlaying(false); emitAction(NamiPaywallAction.UNKNOWN, { ...eventData, componentChange }); break; case ACTION_MUTE_VIDEO: setVideoMuted(true); emitAction(NamiPaywallAction.UNKNOWN, { ...eventData, componentChange }); break; case ACTION_UNMUTE_VIDEO: setVideoMuted(false); emitAction(NamiPaywallAction.UNKNOWN, { ...eventData, componentChange }); break; default: break; } } function emitAction(action: NamiPaywallAction, data: Record): void { NamiEventEmitter.getInstance().emit(PAYWALL_ACTION_EVENT, { ...data, action }); } export function shouldHoist(onTap?: Pick): boolean { if (!onTap) return false; return [ ACTION_SIGN_IN, ACTION_DEEP_LINK, ACTION_RESTORE_PURCHASES, ACTION_CLOSE_PAYWALL, ACTION_BUY_SKU, ACTION_SELECT_SKU, ].includes(onTap.function); } function closePaywallHandler(onClose?: () => void): void { storageService.clearLaunchId(); if (!invokeListener(PaywallManagerEvents.Close)) { onClose?.(); } } function restorePurchase(): void { const thirdPartyTransCap = hasCapability(Capabilities.THIRD_PARTY_TRANSACTIONS); if (!invokeListener(PaywallManagerEvents.Restore) && !thirdPartyTransCap) { logger.warn('NamiPaywallManager.registerRestoreHandler is not registered, so restore uses the default Expo behavior.'); } } function signIn(): void { invokeListener(PaywallManagerEvents.SignIn); } function deepLink(url?: string): void { if (!url) { logger.debug('Paywall deep link action invoked, but no url present.'); return; } if (!invokeListener(PaywallManagerEvents.DeeplinkAction, url)) { Linking.canOpenURL(url).then((canOpen: boolean) => { if (canOpen) { Linking.openURL(url); } }).catch(() => {}); } } function buySKU( ctx: PaywallContextValue, onTap: NamiTapAction, sku: NamiSKU, ): NamiSKU { const nextSku = { ...sku } as ActionSku; const promoId = onTap.parameters?.promo; if (promoId) { nextSku.promoId = String(promoId); } else { const product = nextSku.productDetails ?? nextSku.product_details; const trialOffer = getFreeTrialOffer(product); if (trialOffer?.offer_ref_id) { nextSku.promoId = String(trialOffer.offer_ref_id); } else { const firstOffer = product?.offers?.[0]; if (firstOffer?.offer_ref_id) { nextSku.promoId = String(firstOffer.offer_ref_id); } } } handleClickedSKU(ctx, nextSku); return nextSku; } function handleClickedSKU(ctx: PaywallContextValue, sku: NamiSKU): void { if (hasCapability(Capabilities.THIRD_PARTY_TRANSACTIONS)) { storageService.setPurchaseImpression(); if (!invokeListener(PaywallManagerEvents.BuySku, sku)) { logger.warn( 'In order for Nami to hand off the purchase, you must register a NamiBuySkuHandler with NamiPaywallManager.registerBuySkuHandler.', ); } return; } namiBuySKU(getSkuIdentifier(sku as ActionSku) ?? ''); } function invokeListener(eventName: string, ...args: any[]): boolean { const emitter = NamiEventEmitter.getInstance(); const hasListeners = emitter.listenerCount(eventName) > 0; if (hasListeners) { emitter.emit(eventName, ...args); } return hasListeners; } function getFreeTrialOffer(product?: ProductDetailsWithOffers): ProductOffer | undefined { if (!product?.offers?.length) { return undefined; } return product.offers.find((offer) => offer.offer_type === 'promo' && offer.price === 0); } function getSkuIdentifier(sku?: ActionSku): string | undefined { return sku?.skuId ?? sku?.sku_ref_id ?? sku?.id; } function asOptionalString(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 ? value : undefined; }