UNPKG

react-native-adapty

Version:
310 lines (270 loc) 9.35 kB
import { AdaptyIOSPresentationStyle, AdaptyUiDialogActionType, AdaptyUiDialogConfig, AdaptyUiView, CreatePaywallViewParamsInput, DEFAULT_EVENT_HANDLERS, EventHandlers, } from './types'; import { ViewEmitter } from './view-emitter'; import { AdaptyPaywall } from '@/types'; import { LogContext, LogScope } from '@/logger'; import { AdaptyPaywallCoder } from '@/coders/adapty-paywall'; import { AdaptyUICreatePaywallViewParamsCoder } from '@/coders'; import { MethodName } from '@/types/bridge'; import { $bridge } from '@/bridge'; import { AdaptyError } from '@/adapty-error'; import { AdaptyType } from '@/coders/parse'; import { Req } from '@/types/schema'; import { AdaptyUiDialogConfigCoder } from '@/coders/adapty-ui-dialog-config'; export const DEFAULT_PARAMS: CreatePaywallViewParamsInput = { prefetchProducts: true, loadTimeoutMs: 5000, }; /** * Provides methods to control created paywall view * @public */ export class ViewController { /** * Intended way to create a ViewController instance. * It prepares a native controller to be presented * and creates reference between native controller and JS instance * @internal */ static async create( paywall: AdaptyPaywall, params: CreatePaywallViewParamsInput, ): Promise<ViewController> { const ctx = new LogContext(); const log = ctx.call({ methodName: 'createPaywallView' }); log.start({ paywall, params }); const view = new ViewController(); const paywallCoder = new AdaptyPaywallCoder(); const paramsCoder = new AdaptyUICreatePaywallViewParamsCoder(); const methodKey = 'adapty_ui_create_paywall_view'; // Set default values for required parameters const paramsWithDefaults: CreatePaywallViewParamsInput = { ...DEFAULT_PARAMS, ...params, }; const data: Req['AdaptyUICreatePaywallView.Request'] = { method: methodKey, paywall: paywallCoder.encode(paywall), ...paramsCoder.encode(paramsWithDefaults), }; const body = JSON.stringify(data); const result = await view.handle<AdaptyUiView>( methodKey, body, 'AdaptyUiView', ctx, log, ); view.id = result.id; view.setEventHandlers(DEFAULT_EVENT_HANDLERS); return view; } private id: string | null; // reference to a native view. UUID private viewEmitter: ViewEmitter | null = null; /** * Since constructors in JS cannot be async, it is not * preferred to create ViewControllers in direct way. * Consider using @link{ViewController.create} instead * * @remarks * Creating ViewController this way does not let you * to make native create request and set _id. * It is intended to avoid usage * * @internal */ private constructor() { this.id = null; } private async handle<T>( method: MethodName, params: string, resultType: AdaptyType, ctx: LogContext, log: LogScope, ): Promise<T> { try { const result = await $bridge.request(method, params, resultType, ctx); log.success(result); return result as T; } catch (error) { /* * Success because error was handled validly * It is a developer task to define which errors must be logged */ log.success({ error }); throw error; } } /** * Presents a paywall view as a modal * * @param {Object} options - Presentation options * @param {AdaptyIOSPresentationStyle} [options.iosPresentationStyle] - iOS presentation style. * Available options: 'full_screen' (default) or 'page_sheet'. * Only affects iOS platform. * * @remarks * Calling `present` upon already visible paywall view * would result in an error * * @throws {AdaptyError} */ public async present( options: { iosPresentationStyle?: AdaptyIOSPresentationStyle } = {}, ): Promise<void> { const ctx = new LogContext(); const log = ctx.call({ methodName: 'present' }); log.start({ _id: this.id, iosPresentationStyle: options.iosPresentationStyle, }); if (this.id === null) { log.failed({ error: 'no _id' }); throw this.errNoViewReference(); } const methodKey = 'adapty_ui_present_paywall_view'; const body = JSON.stringify({ method: methodKey, id: this.id, ios_presentation_style: options.iosPresentationStyle ?? 'full_screen', } satisfies Req['AdaptyUIPresentPaywallView.Request']); const result = await this.handle<void>(methodKey, body, 'Void', ctx, log); return result; } /** * Dismisses a paywall view * * @throws {AdaptyError} */ public async dismiss(): Promise<void> { const ctx = new LogContext(); const log = ctx.call({ methodName: 'dismiss' }); log.start({ _id: this.id }); if (this.id === null) { log.failed({ error: 'no id' }); throw this.errNoViewReference(); } const methodKey = 'adapty_ui_dismiss_paywall_view'; const body = JSON.stringify({ method: methodKey, id: this.id, destroy: false, } satisfies Req['AdaptyUIDismissPaywallView.Request']); await this.handle<void>(methodKey, body, 'Void', ctx, log); if (this.viewEmitter) { this.viewEmitter.removeAllListeners(); } } /** * Presents an alert dialog * * @param {AdaptyUiDialogConfig} config - A config for showing the dialog. * * @remarks * Use this method instead of RN alert dialogs when paywall view is presented. * On Android, built-in RN alerts appear behind the paywall view, making them invisible to users. * This method ensures proper dialog presentation above the paywall on all platforms. * * If you provide two actions in the config, be sure `primaryAction` cancels the operation * and leaves things unchanged. * * @returns {Promise<AdaptyUiDialogActionType>} A Promise that resolves to the {@link AdaptyUiDialogActionType} object * * @throws {AdaptyError} */ public async showDialog( config: AdaptyUiDialogConfig, ): Promise<AdaptyUiDialogActionType> { const ctx = new LogContext(); const log = ctx.call({ methodName: 'showDialog' }); log.start({ _id: this.id }); if (this.id === null) { log.failed({ error: 'no id' }); throw this.errNoViewReference(); } const coder = new AdaptyUiDialogConfigCoder(); const methodKey = 'adapty_ui_show_dialog'; const body = JSON.stringify({ method: methodKey, id: this.id, configuration: coder.encode(config), } satisfies Req['AdaptyUIShowDialog.Request']); return await this.handle<AdaptyUiDialogActionType>( methodKey, body, 'Void', ctx, log, ); } private onRequestClose = async (): Promise<void> => { try { await this.dismiss(); } catch (error) { // Log error but don't re-throw to avoid breaking event handling const ctx = new LogContext(); const log = ctx.call({ methodName: 'onRequestClose' }); log.failed({ error, message: 'Failed to dismiss paywall' }); } }; /** * Sets event handlers for paywall view events * * @see {@link https://adapty.io/docs/react-native-handling-events-1 | [DOC] Handling View Events} * * @remarks * Each event type can have only one handler — new handlers replace existing ones. * Default handlers are set during view creation: {@link DEFAULT_EVENT_HANDLERS} * - `onCloseButtonPress` - closes paywall (returns `true`) * - `onAndroidSystemBack` - closes paywall (returns `true`) * - `onRestoreCompleted` - closes paywall (returns `true`) * - `onPurchaseCompleted` - closes paywall on success (returns `purchaseResult.type !== 'user_cancelled'`) * - `onUrlPress` - opens URL and keeps paywall open (returns `false`) * * If you want to override these listeners, we strongly recommend to return the same value as the default implementation * from your custom listener to retain default behavior. * * **Important**: Calling this method multiple times will override only the handlers you provide, * keeping previously set handlers intact. * * @param {Partial<EventHandlers>} [eventHandlers] - set of event handling callbacks * @returns {() => void} unsubscribe - function to unsubscribe all listeners */ public setEventHandlers( eventHandlers: Partial<EventHandlers> = {}, ): () => void { const ctx = new LogContext(); const log = ctx.call({ methodName: 'setEventHandlers' }); log.start({ _id: this.id }); if (this.id === null) { throw this.errNoViewReference(); } // Create viewEmitter on first call if (!this.viewEmitter) { this.viewEmitter = new ViewEmitter(this.id); } // Register only provided handlers (they will replace existing ones for same events) Object.keys(eventHandlers).forEach(eventStr => { const event = eventStr as keyof EventHandlers; if (!eventHandlers.hasOwnProperty(event)) { return; } const handler = eventHandlers[ event ] as EventHandlers[keyof EventHandlers]; this.viewEmitter!.addListener(event, handler, this.onRequestClose); }); return () => this.viewEmitter?.removeAllListeners(); } private errNoViewReference(): AdaptyError { throw new Error('View reference not found'); } }