@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
text/typescript
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');
}
}