UNPKG

@adapty/react-native-ui

Version:

Extension to the Adapty SDK that allows you to easily add purchase screens to your application.

246 lines (203 loc) 7.09 kB
import { AdaptyError, AdaptyPaywall } from 'react-native-adapty'; import { LogContext, LogScope } from 'react-native-adapty/dist/logger'; import { AdaptyPaywallCoder } from 'react-native-adapty/dist/coders/adapty-paywall'; import { $bridge, ParamMap } from './bridge'; import { ViewEmitter } from './view-emitter'; import { CreatePaywallViewParamsInput, DEFAULT_EVENT_HANDLERS, EventHandlers, } from './types'; import { MethodName } from './types/bridge'; /** * 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 */ static async create( paywall: AdaptyPaywall, params: CreatePaywallViewParamsInput, ): Promise<ViewController> { const ctx = new LogContext(); const log = ctx.call({ methodName: 'createController' }); log.start({ paywall, params }); const view = new ViewController(); const body = new ParamMap(); const coder = new AdaptyPaywallCoder(); body.set('paywall', JSON.stringify(coder.encode(paywall))); body.set('prefetch_products', params.prefetchProducts ?? true); if (params.customTags) { body.set('custom_tags', JSON.stringify(params.customTags)); } if (params.timerInfo) { const convertTimerInfo = (timerInfo: Record<string, Date>): Record<string, string> => { const formatDate = (date: Date): string => { const pad = (num: number): string => num < 10 ? `0${num}` : num.toString(); const year = date.getFullYear(); const month = pad(date.getMonth() + 1); const day = pad(date.getDate()); const hours = pad(date.getHours()); const minutes = pad(date.getMinutes()); const seconds = pad(date.getSeconds()); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; }; const result: Record<string, string> = {}; for (const key in timerInfo) { if (timerInfo.hasOwnProperty(key)) { const date = timerInfo[key]; if (date instanceof Date) { result[key] = formatDate(date); } } } return result; } body.set('timer_info', JSON.stringify(convertTimerInfo(params.timerInfo))); } const result = await view.handle<string>('create_view', body, ctx, log); view.id = result; return view; } private id: string | null; // reference to a native view. UUID private unsubscribeAllListeners: null | (() => void) = 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: ParamMap, ctx: LogContext, log: LogScope, ): Promise<T> { try { const result = await $bridge.request(method, params, 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 full-screen modal * * @remarks * Calling `present` upon already visible paywall view * would result in an error * * @throws {AdaptyError} */ public async present(): Promise<void> { const ctx = new LogContext(); const log = ctx.call({ methodName: 'present' }); log.start({ _id: this.id }); if (this.id === null) { log.failed({ error: 'no _id' }); throw this.errNoViewReference(); } const body = new ParamMap(); body.set('view_id', this.id); const result = await this.handle<void>('present_view', body, 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 body = new ParamMap(); body.set('view_id', this.id); await this.handle<void>('dismiss_view', body, ctx, log); if (this.unsubscribeAllListeners) { this.unsubscribeAllListeners(); } } /** * Creates a set of specific view event listeners * * @see {@link https://docs.adapty.io/docs/react-native-handling-events | [DOC] Handling View Events} * * @remarks * It registers only requested set of event handlers. * Your config is assigned into three event listeners {@link DEFAULT_EVENT_HANDLERS}, * that handle default closing behavior. * - `onCloseButtonPress` * - `onAndroidSystemBack` * - `onRestoreCompleted` * - `onPurchaseCompleted` * * If you want to override these listeners, we strongly recommend to return `true` * from your custom listener to retain default closing behavior. * * @param {Partial<EventHandlers> | undefined} [eventHandlers] - set of event handling callbacks * @returns {() => void} unsubscribe - function to unsubscribe all listeners */ public registerEventHandlers( eventHandlers: Partial<EventHandlers> = DEFAULT_EVENT_HANDLERS, ): () => void { const ctx = new LogContext(); const log = ctx.call({ methodName: 'registerEventHandlers' }); log.start({ _id: this.id }); if (this.id === null) { throw this.errNoViewReference(); } const finalEventHandlers: Partial<EventHandlers> = { ...DEFAULT_EVENT_HANDLERS, ...eventHandlers, }; // DIY way to tell TS that original arg should not be used const deprecateVar = (_target: unknown): _target is never => true; if (!deprecateVar(eventHandlers)) { return () => {}; } const viewEmitter = new ViewEmitter(this.id); Object.keys(finalEventHandlers).forEach(eventStr => { const event = eventStr as keyof EventHandlers; if (!finalEventHandlers.hasOwnProperty(event)) { return; } const handler = finalEventHandlers[ event ] as EventHandlers[keyof EventHandlers]; viewEmitter.addListener(event, handler, () => this.dismiss()); }); const unsubscribe = () => viewEmitter.removeAllListeners(); // expose to class to be able to unsubscribe on dismiss this.unsubscribeAllListeners = unsubscribe; return unsubscribe; } private errNoViewReference(): AdaptyError { // TODO: Make a separate error type once AdaptyError is refactored throw new Error('View reference not found'); } }