import { Observable } from 'rxjs'; import { NativeModules, NativeEventEmitter, DeviceEventEmitter, Platform, } from 'react-native'; import { StripeTerminalConfig, DiscoveryMethod, Reader, ReaderSoftwareUpdate, ConnectionStatus, ReaderInputOptions, ReaderDisplayMessage, PaymentIntent, } from './utils'; const { MadStripeSdk } = NativeModules; /** * The Android connection status enum is different from iOS, this maps Android to iOS * @ignore */ const AndroidConnectionStatusMap = { 0: ConnectionStatus.NotConnected, 1: ConnectionStatus.Connecting, 2: ConnectionStatus.Connected, }; export class StripeTerminalSDK { private _fetchConnectionToken: () => Promise = () => Promise.reject('You must initialize StripeTerminalSDK first.'); private _onUnexpectedReaderDisconnect: () => void = () => Promise.reject('You must initialize StripeTerminalSDK first.'); static listener: NativeEventEmitter; public isInitialized = false; private static isDiscovering: boolean = false; constructor(options: StripeTerminalConfig) { StripeTerminalSDK.listener = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.ReactNativeEventEmitter) : DeviceEventEmitter; this._fetchConnectionToken = options.fetchConnectionToken; } private async init() { StripeTerminalSDK.listener.addListener('requestConnectionToken', () => { this._fetchConnectionToken() .then((token) => { if (token) { console.log(token); MadStripeSdk.setConnectionToken(token, null); } else { throw new Error( 'User-supplied `fetchConnectionToken` resolved successfully, but no token was returned.' ); } }) .catch((err) => MadStripeSdk.setConnectionToken( null, err.message || 'Error in user-supplied `fetchConnectionToken`.' ) ); }); StripeTerminalSDK.listener.addListener( 'didReportUnexpectedReaderDisconnect', () => { this._onUnexpectedReaderDisconnect(); } ); await MadStripeSdk.initTerminal(); this.isInitialized = true; } private _listenerToObservable( name: | 'didRequestReaderDisplayMessage' | 'didRequestReaderInput' | 'didReportAvailableUpdate' | 'didStartInstallingUpdate' | 'didReportReaderSoftwareUpdateProgress' | 'didFinishInstallingUpdate', transformFunc?: (data: any) => any ): Observable { return new Observable((subscriber) => { StripeTerminalSDK.listener.addListener(name, (data: any) => { if (transformFunc) { return subscriber.next(transformFunc(data)); } return subscriber.next(data); }); return { unsubscribe: () => { StripeTerminalSDK.listener.removeListener(name, () => {}); }, }; }); } public didStartInstallingUpdate(): Observable { return this._listenerToObservable( 'didStartInstallingUpdate', (data: { update: ReaderSoftwareUpdate }) => { return data?.update ?? null; } ); } public didReportAvailableUpdate(): Observable { return this._listenerToObservable( 'didReportAvailableUpdate', (data: { update: ReaderSoftwareUpdate }) => { return data?.update ?? null; } ); } public didReportReaderSoftwareUpdateProgress(): Observable { return this._listenerToObservable( 'didReportReaderSoftwareUpdateProgress', (data: any) => { return parseFloat(data.progress); } ); } public didFinishInstallingUpdate(): Observable<{ update?: ReaderSoftwareUpdate; error?: string; }> { return this._listenerToObservable( 'didFinishInstallingUpdate', (data: { update?: ReaderSoftwareUpdate; error?: string }) => { return data ?? null; } ); } public didRequestReaderInput(): Observable { return this._listenerToObservable('didRequestReaderInput', (data: any) => { if (data.isAndroid) { return data.value; } return parseFloat(data.value); }); } public didRequestReaderDisplayMessage(): Observable { return this._listenerToObservable( 'didRequestReaderDisplayMessage', (data: any) => { return parseFloat(data.value); } ); } public static async createPaymentIntent( amount: number, currency: string ): Promise { this.ensureInitialized(); const data = await MadStripeSdk.createPaymentIntent(amount, currency); return data?.intent ?? null; } public static async collectPaymentMethod(): Promise { this.ensureInitialized(); const data = await MadStripeSdk.collectPaymentMethod(); return data?.intent ?? null; } public static async create( options: StripeTerminalConfig ): Promise { const terminal = new StripeTerminalSDK(options); await terminal.init(); return terminal; } public static isInitialized(): Promise { return MadStripeSdk.isInitialized(); } public static discoverReaders( discoveryMethod: DiscoveryMethod, simulated: boolean ): Observable { this.ensureInitialized(); return new Observable((subscriber) => { this.listener.addListener( 'readerDiscoveryCompletion', (event: { readers?: Reader[] }) => { try { const readers = event!.readers!.map((reader: Reader) => { return reader; }) || []; subscriber.next(readers); } catch (error) { console.log(error); } } ); // start discovery this.isDiscovering = true; MadStripeSdk.discoverReaders(discoveryMethod, simulated) .then(() => { this.isDiscovering = false; subscriber.complete(); }) .catch((err: any) => { this.isDiscovering = false; subscriber.error(err); }); return { unsubscribe: () => { this.cancelDiscoverReaders(); }, }; }); } private static ensureInitialized() { if (!this.isInitialized) { throw new Error( 'StripeTerminalSDK must be initialized before you can use any methods.' ); } } public static async cancelDiscoverReaders(): Promise { try { if (!this.isDiscovering) { return; } this.listener.removeListener('readerDiscoveryCompletion', () => {}); await MadStripeSdk.cancelDiscoverReaders(); this.isDiscovering = false; } catch (err) { console.log(err); } } public static async connectBluetoothReader( serialNumber: string, locationId: string ): Promise { this.ensureInitialized(); const data = await MadStripeSdk.connectBluetoothReader( serialNumber, locationId ); return data?.reader ?? null; } public static async connectInternetReader( serialNumber: String, failIfInUse?: boolean ): Promise { this.ensureInitialized(); const data = await MadStripeSdk.connectInternetReader( serialNumber, failIfInUse ); return data?.reader ?? null; } public static async getConnectedReader(): Promise { this.ensureInitialized(); const data = await MadStripeSdk.getConnectedReader(); return data ?? null; } public static async disconnectReader(): Promise { this.ensureInitialized(); return await MadStripeSdk.disconnectReader(); } public static connectionStatus(): Observable { this.ensureInitialized(); return new Observable((subscriber) => { let hasSentEvent = false; // get current value MadStripeSdk.getConnectionStatus() .then((status: any) => { // only send the initial value if the event listener hasn't already if (!hasSentEvent) { subscriber.next(this.translateConnectionStatus(status)); } }) .catch((err: any) => { subscriber.error(err); }); // then listen for changes this.listener.addListener('didChangeConnectionStatus', (status: any) => { hasSentEvent = true; subscriber.next(this.translateConnectionStatus(status)); }); return { unsubscribe: () => { this.listener.removeListener('didChangeConnectionStatus', () => {}); }, }; }); } public static async clearCachedCredentials(): Promise { this.ensureInitialized(); return await MadStripeSdk.clearCachedCredentials(); } public static async cancelCollectPaymentMethod(): Promise { this.ensureInitialized(); return await MadStripeSdk.cancelCollectPaymentMethod(); } public static async processPayment(): Promise { this.ensureInitialized(); const data = await MadStripeSdk.confirmPaymentIntent(); return data?.intent ?? null; } public static async installAvailableUpdate(): Promise { this.ensureInitialized(); return await MadStripeSdk.installAvailableUpdate(); } private static translateConnectionStatus(data: { status: ConnectionStatus; isAndroid?: boolean; }): ConnectionStatus { let status: ConnectionStatus = data.status; if (data.isAndroid) { // the connection status on android is different than on iOS so we have to translate it status = AndroidConnectionStatusMap[data.status]; } return status; } }