diff --git a/demo/angular/src/app/terminal/terminal.page.html b/demo/angular/src/app/terminal/terminal.page.html index c656f9be5..8b60598c3 100644 --- a/demo/angular/src/app/terminal/terminal.page.html +++ b/demo/angular/src/app/terminal/terminal.page.html @@ -37,13 +37,13 @@ Device Update - Happy Path simulateReaderUpdate.UpdateAvailable simulateReaderUpdate.Required - this.helper.updateItem(this.eventItems, 'collectPaymentMethod', true), - ) - .catch(async (e) => { - await this.helper.updateItem( - this.eventItems, - 'collectPaymentMethod', - false, - ); - throw e; - }); + if (readerType === TerminalConnectTypes.Internet) { + await StripeTerminal.setReaderDisplay({ + currency: 'usd', + tax: 0, + total: 1000, + lineItems: [{ + displayName: 'winecode', + quantity: 2, + amount: 500 + }] as CartLineItem[], + }) + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + await StripeTerminal.clearReaderDisplay(); + } + + if (type === 'cancelPath') { + // During Collect, cancel the payment + StripeTerminal.collectPaymentMethod({ paymentIntent }) + .catch(async (e) => { + await this.helper.updateItem( + this.eventItems, + 'collectPaymentMethod', + false, + ); + throw e; + }); + await this.helper.updateItem(this.eventItems, 'collectPaymentMethod', true); await new Promise((resolve) => setTimeout(resolve, 2000)); await StripeTerminal.cancelCollectPaymentMethod().catch(async (e) => { await this.helper.updateItem( @@ -174,6 +194,18 @@ export class TerminalPage { true, ); } else { + await StripeTerminal.collectPaymentMethod({ paymentIntent }) + .then(() => + this.helper.updateItem(this.eventItems, 'collectPaymentMethod', true), + ) + .catch(async (e) => { + await this.helper.updateItem( + this.eventItems, + 'collectPaymentMethod', + false, + ); + throw e; + }); await StripeTerminal.confirmPaymentIntent(); await this.helper.updateItem( this.eventItems, @@ -186,28 +218,102 @@ export class TerminalPage { this.listenerHandlers.forEach((handler) => handler.remove()); } - async checkUpdateDevice(readerType: TerminalConnectTypes = TerminalConnectTypes.Bluetooth, simulateReaderUpdate: SimulateReaderUpdate) { - await this.prepareTerminalEvents(structuredClone(checkUpdateDeviceItems)); - - switch (simulateReaderUpdate) { - case SimulateReaderUpdate.UpdateAvailable: - await StripeTerminal.setSimulatorConfiguration({ update: SimulateReaderUpdate.UpdateAvailable }) - .then(() => this.helper.updateItem(this.eventItems, 'setSimulatorConfiguration:UPDATE_AVAILABLE', true)); - break; - case SimulateReaderUpdate.LowBattery: - await StripeTerminal.setSimulatorConfiguration({ update: SimulateReaderUpdate.LowBattery }) - .then(() => this.helper.updateItem(this.eventItems, 'setSimulatorConfiguration:LOW_BATTERY', true)); - break; - case SimulateReaderUpdate.LowBatterySucceedConnect: - await StripeTerminal.setSimulatorConfiguration({ update: SimulateReaderUpdate.LowBatterySucceedConnect }) - .then(() => this.helper.updateItem(this.eventItems, 'setSimulatorConfiguration:LOW_BATTERY_SUCCEED_CONNECT', true)); - break; - case SimulateReaderUpdate.Required: - await StripeTerminal.setSimulatorConfiguration({ update: SimulateReaderUpdate.Required }) - .then(() => this.helper.updateItem(this.eventItems, 'setSimulatorConfiguration:REQUIRED', true)); - break; + async checkUpdateDeviceUpdate(readerType: TerminalConnectTypes = TerminalConnectTypes.Bluetooth) { + await this.prepareTerminalEvents(structuredClone(updateDeviceUpdateItems)); + await StripeTerminal.setSimulatorConfiguration({ update: SimulateReaderUpdate.UpdateAvailable }) + .then(() => this.helper.updateItem(this.eventItems, 'setSimulatorConfiguration:UPDATE_AVAILABLE', true)); + + const result = await StripeTerminal.discoverReaders({ + type: readerType, + locationId: + [TerminalConnectTypes.Usb].includes(readerType) + ? 'tml_Ff37mAmk1XdBYT' // Auckland, New Zealand + : 'tml_FOUOdQVIxvVdvN', // San Francisco, CA 94110 + }).catch((e) => { + this.helper.updateItem(this.eventItems, 'discoverReaders', false); + throw e; + }); + + await this.helper.updateItem( + this.eventItems, + 'discoverReaders', + result.readers.length > 0, + ); + + const selectedReader = + result.readers.length === 1 + ? result.readers[0] + : await this.alertFilterReaders(result.readers); + console.log(selectedReader); + if (!selectedReader) { + alert('No reader selected'); + return; } + await StripeTerminal.connectReader({ + reader: selectedReader, + }).catch((e) => { + alert(e); + this.helper.updateItem(this.eventItems, 'connectReader', false); + throw e; + }); + await this.helper.updateItem(this.eventItems, 'connectReader', true); + + await StripeTerminal.installAvailableUpdate() + .then(() => this.helper.updateItem(this.eventItems, 'installAvailableUpdate', true)); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + await StripeTerminal.cancelInstallUpdate() + .then(() => this.helper.updateItem(this.eventItems, 'cancelInstallUpdate', true)); + + // await new Promise((resolve) => setTimeout(resolve, 5000)); + + const { paymentIntent } = await firstValueFrom( + this.http.post<{ + paymentIntent: string; + }>(environment.api + 'connection/intent', {}), + ).catch(async (e) => { + await this.helper.updateItem( + this.eventItems, + 'HttpClientPaymentIntent', + false, + ); + throw e; + }); + await this.helper.updateItem( + this.eventItems, + 'HttpClientPaymentIntent', + true, + ); + + await StripeTerminal.collectPaymentMethod({ paymentIntent }) + .then(() => + this.helper.updateItem(this.eventItems, 'collectPaymentMethod', true), + ) + .catch(async (e) => { + await this.helper.updateItem( + this.eventItems, + 'collectPaymentMethod', + false, + ); + throw e; + }); + + await StripeTerminal.disconnectReader().catch((e) => { + this.helper.updateItem(this.eventItems, 'disconnectReader', false); + throw e; + }); + await this.helper.updateItem(this.eventItems, 'disconnectReader', true); + + this.listenerHandlers.forEach((handler) => handler.remove()); + } + + async checkUpdateDeviceRequired(readerType: TerminalConnectTypes = TerminalConnectTypes.Bluetooth) { + await this.prepareTerminalEvents(structuredClone(updateDeviceRequiredItems)); + await StripeTerminal.setSimulatorConfiguration({ update: SimulateReaderUpdate.Required }) + .then(() => this.helper.updateItem(this.eventItems, 'setSimulatorConfiguration:REQUIRED', true)); + const result = await StripeTerminal.discoverReaders({ type: readerType, locationId: @@ -347,12 +453,14 @@ export class TerminalPage { const alert = await this.alertCtrl.create({ header: `Select a reader`, message: `Select a reader to connect to.`, + backdropDismiss: false, inputs: readers.map((reader, index) => ({ name: 'serialNumber', type: 'radio', - label: reader.serialNumber, + label: reader.deviceType, value: reader.serialNumber, checked: index === 0, + disabled: reader.status === 'OFFLINE' })), buttons: [ { diff --git a/demo/angular/src/app/terminal/updateDeviceRequiredItems.ts b/demo/angular/src/app/terminal/updateDeviceRequiredItems.ts new file mode 100644 index 000000000..d379a682f --- /dev/null +++ b/demo/angular/src/app/terminal/updateDeviceRequiredItems.ts @@ -0,0 +1,54 @@ +import {ITestItems} from '../shared/interfaces'; +import {TerminalEventsEnum} from '@capacitor-community/stripe-terminal'; + +export const updateDeviceRequiredItems: ITestItems[] = [ + { + type: 'method', + name: 'initialize', + }, + { + type: 'event', + name: TerminalEventsEnum.Loaded, + }, + + { + type: 'method', + name: 'setSimulatorConfiguration:REQUIRED', + }, + { + type: 'method', + name: 'discoverReaders', + }, + { + type: 'event', + name: TerminalEventsEnum.DiscoveredReaders, + }, + { + type: 'method', + name: 'connectReader', + }, + { + type: 'event', + name: TerminalEventsEnum.ConnectedReader, + }, + { + type: 'event', + name: TerminalEventsEnum.StartInstallingUpdate, + }, + { + type: 'event', + name: TerminalEventsEnum.ReaderSoftwareUpdateProgress, + }, + { + type: 'event', + name: TerminalEventsEnum.FinishInstallingUpdate, + }, + { + type: 'method', + name: 'disconnectReader', + }, + { + type: 'event', + name: TerminalEventsEnum.DisconnectedReader, + }, +]; diff --git a/demo/angular/src/app/terminal/checkUpdateDeviceItems.ts b/demo/angular/src/app/terminal/updateDeviceUpdateItems.ts similarity index 85% rename from demo/angular/src/app/terminal/checkUpdateDeviceItems.ts rename to demo/angular/src/app/terminal/updateDeviceUpdateItems.ts index c6d9a3c7d..51252cc34 100644 --- a/demo/angular/src/app/terminal/checkUpdateDeviceItems.ts +++ b/demo/angular/src/app/terminal/updateDeviceUpdateItems.ts @@ -1,7 +1,7 @@ import {ITestItems} from '../shared/interfaces'; import {TerminalEventsEnum} from '@capacitor-community/stripe-terminal'; -export const checkUpdateDeviceItems: ITestItems[] = [ +export const updateDeviceUpdateItems: ITestItems[] = [ { type: 'method', name: 'initialize', @@ -10,47 +10,46 @@ export const checkUpdateDeviceItems: ITestItems[] = [ type: 'event', name: TerminalEventsEnum.Loaded, }, - { type: 'method', - name: 'setSimulatorConfiguration:UPDATE_AVAILABLE', + name: 'discoverReaders', }, { type: 'event', - name: TerminalEventsEnum.ReportAvailableUpdate, + name: TerminalEventsEnum.DiscoveredReaders, }, { type: 'method', - name: 'setSimulatorConfiguration:REQUIRED', + name: 'connectReader', }, { type: 'event', - name: TerminalEventsEnum.StartInstallingUpdate, + name: TerminalEventsEnum.ConnectedReader, }, + { - type: 'event', - name: TerminalEventsEnum.ReaderSoftwareUpdateProgress, + type: 'method', + name: 'setSimulatorConfiguration:UPDATE_AVAILABLE', }, { type: 'event', - name: TerminalEventsEnum.FinishInstallingUpdate, + name: TerminalEventsEnum.ReportAvailableUpdate, }, - { type: 'method', - name: 'discoverReaders', + name: 'installAvailableUpdate', }, { type: 'event', - name: TerminalEventsEnum.DiscoveredReaders, + name: TerminalEventsEnum.StartInstallingUpdate, }, { - type: 'method', - name: 'connectReader', + type: 'event', + name: TerminalEventsEnum.ReaderSoftwareUpdateProgress, }, { - type: 'event', - name: TerminalEventsEnum.ConnectedReader, + type: 'method', + name: 'cancelInstallUpdate', }, { type: 'method', diff --git a/packages/identity/package-lock.json b/packages/identity/package-lock.json index c3325aba8..9cacd84b4 100644 --- a/packages/identity/package-lock.json +++ b/packages/identity/package-lock.json @@ -1,12 +1,12 @@ { "name": "@capacitor-community/stripe-identity", - "version": "6.0.2", + "version": "6.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@capacitor-community/stripe-identity", - "version": "6.0.2", + "version": "6.1.0", "license": "MIT", "dependencies": { "@stripe/stripe-js": "^2.1.11" diff --git a/packages/identity/package.json b/packages/identity/package.json index 5f17bcc86..56964dbfb 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor-community/stripe-identity", - "version": "6.0.2", + "version": "6.1.0", "engines": { "node": ">=18.0.0" }, diff --git a/packages/payment/package-lock.json b/packages/payment/package-lock.json index c998bdaf4..5614e28f3 100644 --- a/packages/payment/package-lock.json +++ b/packages/payment/package-lock.json @@ -1,12 +1,12 @@ { "name": "@capacitor-community/stripe", - "version": "6.0.2", + "version": "6.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@capacitor-community/stripe", - "version": "6.0.2", + "version": "6.1.0", "license": "MIT", "devDependencies": { "@capacitor/android": "^6.0.0", diff --git a/packages/payment/package.json b/packages/payment/package.json index 55ec8f2f8..8feeac06d 100644 --- a/packages/payment/package.json +++ b/packages/payment/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor-community/stripe", - "version": "6.0.2", + "version": "6.1.0", "engines": { "node": ">=18.0.0" }, diff --git a/packages/terminal/README.md b/packages/terminal/README.md index 29466bb57..77f7f867d 100644 --- a/packages/terminal/README.md +++ b/packages/terminal/README.md @@ -1,13 +1,8 @@ # @capacitor-community/stripe-terminal -Stripe SDK bindings for Capacitor Applications. __This plugin is still in beta.__ +Stripe SDK bindings for Capacitor Applications. __This plugin is still in rc(pre-release) version.__ We have confirmed that it works well in the demo project. Please refer to https://github.com/capacitor-community/stripe/tree/main/demo/angular for the implementation. -- [x] Tap To Pay -- [x] Internet -- [x] Bluetooth -- [x] USB - ## Install ```bash @@ -57,7 +52,9 @@ And update minSdkVersion to 26 And compileSdkVersion to 34 in your `android/app/ ## Usage -### use native http client for getting a token +### Simple collect payment + +#### Use plugin client ```typescript (async ()=> { @@ -81,7 +78,7 @@ And update minSdkVersion to 26 And compileSdkVersion to 34 in your `android/app/ }); ``` -### set string token +#### set string token ```typescript (async ()=> { @@ -109,6 +106,95 @@ And update minSdkVersion to 26 And compileSdkVersion to 34 in your `android/app/ }); ```` +### Listen device update + +The device will **if necessary** automatically start updating itself. It is important to handle them as needed so as not to disrupt business operations. + +```ts +(async ()=> { + StripeTerminal.addListener(TerminalEventsEnum.ReportAvailableUpdate, async ({ update }) => { + if (window.confirm("Will you update the device?")) { + await StripeTerminal.installAvailableUpdate(); + } + }); + StripeTerminal.addListener(TerminalEventsEnum.StartInstallingUpdate, async ({ update }) => { + console.log(update); + if (window.confirm("Will you interrupt the update?")) { + StripeTerminal.cancelInstallUpdate(); + } + }); + StripeTerminal.addListener(TerminalEventsEnum.ReaderSoftwareUpdateProgress, async ({ progress }) => { + // be able to use this value to create a progress bar. + }); + StripeTerminal.addListener(TerminalEventsEnum.FinishInstallingUpdate, async ({ update }) => { + console.log(update); + }); +}); +``` + +### Get terminal processing information + +For devices without leader screen, processing information must be retrieved and displayed on the mobile device. Get it with a listener. + +```ts +/** + * Listen battery level. If the battery level is low, you can notify the user to charge the device. + */ +StripeTerminal.addListener(TerminalEventsEnum.BatteryLevel, async ({ level, charging, status }) => { + console.log(level, charging, status); +}); + +/** + * Listen reader event. You can get the reader's status and display it on the mobile device. + */ +StripeTerminal.addListener(TerminalEventsEnum.ReaderEvent, async ({ event }) => { + console.log(event); +}); + +/** + * Listen display message. You can get the message to be displayed on the mobile device. + */ +StripeTerminal.addListener(TerminalEventsEnum.RequestDisplayMessage, async ({ messageType, message }) => { + console.log(messageType, message); +}); + +/** + * Listen reader input. You can get the message what can be used for payment. + */ +StripeTerminal.addListener(TerminalEventsEnum.RequestReaderInput, async ({ options, message }) => { + console.log(options, message); +}); +``` + +### More details on the leader screen + +The contents of the payment can be shown on the display. This requires a leader screen on the device. +This should be run before `collectPaymentMethod`. + +```ts +await StripeTerminal.setReaderDisplay({ + currency: 'usd', + tax: 0, + total: 1000, + lineItems: [{ + displayName: 'winecode', + quantity: 2, + amount: 500 + }] as CartLineItem[], +}) + +// Of course, erasure is also possible. +await StripeTerminal.clearReaderDisplay(); +``` + +### Simulate reader status changes for testing + +To implement updates, etc., we are facilitating an API to change the state of the simulator. This should be done before discoverReaders. + +```ts +await StripeTerminal.setSimulatorConfiguration({ update: SimulateReaderUpdate.UpdateAvailable }) +``` + ## API @@ -124,6 +210,12 @@ And update minSdkVersion to 26 And compileSdkVersion to 34 in your `android/app/ * [`collectPaymentMethod(...)`](#collectpaymentmethod) * [`cancelCollectPaymentMethod()`](#cancelcollectpaymentmethod) * [`confirmPaymentIntent()`](#confirmpaymentintent) +* [`installAvailableUpdate()`](#installavailableupdate) +* [`cancelInstallUpdate()`](#cancelinstallupdate) +* [`setReaderDisplay(...)`](#setreaderdisplay) +* [`clearReaderDisplay()`](#clearreaderdisplay) +* [`rebootReader()`](#rebootreader) +* [`cancelReaderReconnection()`](#cancelreaderreconnection) * [`addListener(TerminalEventsEnum.Loaded, ...)`](#addlistenerterminaleventsenumloaded) * [`addListener(TerminalEventsEnum.RequestedConnectionToken, ...)`](#addlistenerterminaleventsenumrequestedconnectiontoken) * [`addListener(TerminalEventsEnum.DiscoveredReaders, ...)`](#addlistenerterminaleventsenumdiscoveredreaders) @@ -144,6 +236,9 @@ And update minSdkVersion to 26 And compileSdkVersion to 34 in your `android/app/ * [`addListener(TerminalEventsEnum.RequestDisplayMessage, ...)`](#addlistenerterminaleventsenumrequestdisplaymessage) * [`addListener(TerminalEventsEnum.RequestReaderInput, ...)`](#addlistenerterminaleventsenumrequestreaderinput) * [`addListener(TerminalEventsEnum.PaymentStatusChange, ...)`](#addlistenerterminaleventsenumpaymentstatuschange) +* [`addListener(TerminalEventsEnum.ReaderReconnectStarted, ...)`](#addlistenerterminaleventsenumreaderreconnectstarted) +* [`addListener(TerminalEventsEnum.ReaderReconnectSucceeded, ...)`](#addlistenerterminaleventsenumreaderreconnectsucceeded) +* [`addListener(TerminalEventsEnum.ReaderReconnectFailed, ...)`](#addlistenerterminaleventsenumreaderreconnectfailed) * [Interfaces](#interfaces) * [Type Aliases](#type-aliases) * [Enums](#enums) @@ -212,12 +307,12 @@ setSimulatorConfiguration(options: { update?: SimulateReaderUpdate; simulatedCar ### connectReader(...) ```typescript -connectReader(options: { reader: ReaderInterface; }) => Promise +connectReader(options: { reader: ReaderInterface; autoReconnectOnUnexpectedDisconnect?: boolean; merchantDisplayName?: string; onBehalfOf?: string; }) => Promise ``` -| Param | Type | -| ------------- | ------------------------------------------------------------------------ | -| **`options`** | { reader: ReaderInterface; } | +| Param | Type | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`options`** | { reader: ReaderInterface; autoReconnectOnUnexpectedDisconnect?: boolean; merchantDisplayName?: string; onBehalfOf?: string; } | -------------------- @@ -282,6 +377,64 @@ confirmPaymentIntent() => Promise -------------------- +### installAvailableUpdate() + +```typescript +installAvailableUpdate() => Promise +``` + +-------------------- + + +### cancelInstallUpdate() + +```typescript +cancelInstallUpdate() => Promise +``` + +-------------------- + + +### setReaderDisplay(...) + +```typescript +setReaderDisplay(options: Cart) => Promise +``` + +| Param | Type | +| ------------- | ------------------------------------- | +| **`options`** | Cart | + +-------------------- + + +### clearReaderDisplay() + +```typescript +clearReaderDisplay() => Promise +``` + +-------------------- + + +### rebootReader() + +```typescript +rebootReader() => Promise +``` + +-------------------- + + +### cancelReaderReconnection() + +```typescript +cancelReaderReconnection() => Promise +``` + +-------------------- + + ### addListener(TerminalEventsEnum.Loaded, ...) ```typescript @@ -700,6 +853,54 @@ addListener(eventName: TerminalEventsEnum.PaymentStatusChange, listenerFunc: ({ -------------------- +### addListener(TerminalEventsEnum.ReaderReconnectStarted, ...) + +```typescript +addListener(eventName: TerminalEventsEnum.ReaderReconnectStarted, listenerFunc: ({ reader, reason, }: { reader: ReaderInterface; reason: string; }) => void) => Promise +``` + +| Param | Type | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| **`eventName`** | TerminalEventsEnum.ReaderReconnectStarted | +| **`listenerFunc`** | ({ reader, reason, }: { reader: ReaderInterface; reason: string; }) => void | + +**Returns:** Promise<PluginListenerHandle> + +-------------------- + + +### addListener(TerminalEventsEnum.ReaderReconnectSucceeded, ...) + +```typescript +addListener(eventName: TerminalEventsEnum.ReaderReconnectSucceeded, listenerFunc: ({ reader }: { reader: ReaderInterface; }) => void) => Promise +``` + +| Param | Type | +| ------------------ | ------------------------------------------------------------------------------------------------- | +| **`eventName`** | TerminalEventsEnum.ReaderReconnectSucceeded | +| **`listenerFunc`** | ({ reader }: { reader: ReaderInterface; }) => void | + +**Returns:** Promise<PluginListenerHandle> + +-------------------- + + +### addListener(TerminalEventsEnum.ReaderReconnectFailed, ...) + +```typescript +addListener(eventName: TerminalEventsEnum.ReaderReconnectFailed, listenerFunc: ({ reader }: { reader: ReaderInterface; }) => void) => Promise +``` + +| Param | Type | +| ------------------ | ------------------------------------------------------------------------------------------------- | +| **`eventName`** | TerminalEventsEnum.ReaderReconnectFailed | +| **`listenerFunc`** | ({ reader }: { reader: ReaderInterface; }) => void | + +**Returns:** Promise<PluginListenerHandle> + +-------------------- + + ### Interfaces @@ -715,17 +916,91 @@ addListener(eventName: TerminalEventsEnum.PaymentStatusChange, listenerFunc: ({ #### ReaderInterface -{ index: number; serialNumber: string; } +{ /** * The unique serial number is primary identifier inner plugin. */ serialNumber: string; label: string; batteryLevel: number; batteryStatus: BatteryStatus; simulated: boolean; id: number; availableUpdate: ReaderSoftwareUpdateInterface; locationId: string; ipAddress: string; status: NetworkStatus; location: LocationInterface; locationStatus: LocationStatus; deviceType: DeviceType; deviceSoftwareVersion: string; /** * iOS Only properties. These properties are not available on Android. */ isCharging: number; /** * Android Only properties. These properties are not available on iOS. */ baseUrl: string; bootloaderVersion: string; configVersion: string; emvKeyProfileId: string; firmwareVersion: string; hardwareVersion: string; macKeyProfileId: string; pinKeyProfileId: string; trackKeyProfileId: string; settingsVersion: string; pinKeysetId: string; /** * @deprecated This property has been deprecated and should use the `serialNumber` property. */ index?: number; } #### ReaderSoftwareUpdateInterface -{ version: string; settingsVersion: string; requiredAt: number; timeEstimate: UpdateTimeEstimate; } +{ deviceSoftwareVersion: string; estimatedUpdateTime: UpdateTimeEstimate; requiredAt: number; } + + +#### LocationInterface + +{ id: string; displayName: string; address: { city: string; country: string; postalCode: string; line1: string; line2: string; state: string; }; ipAddress: string; } + + +#### Cart + +{ currency: string; tax: number; total: number; lineItems: CartLineItem[]; } + + +#### CartLineItem + +{ displayName: string; quantity: number; amount: number; } ### Enums +#### BatteryStatus + +| Members | Value | +| -------------- | ----------------------- | +| **`Unknown`** | 'UNKNOWN' | +| **`Critical`** | 'CRITICAL' | +| **`Low`** | 'LOW' | +| **`Nominal`** | 'NOMINAL' | + + +#### UpdateTimeEstimate + +| Members | Value | +| -------------------------- | -------------------------------------- | +| **`LessThanOneMinute`** | 'LESS_THAN_ONE_MINUTE' | +| **`OneToTwoMinutes`** | 'ONE_TO_TWO_MINUTES' | +| **`TwoToFiveMinutes`** | 'TWO_TO_FIVE_MINUTES' | +| **`FiveToFifteenMinutes`** | 'FIVE_TO_FIFTEEN_MINUTES' | + + +#### NetworkStatus + +| Members | Value | +| ------------- | ---------------------- | +| **`Unknown`** | 'UNKNOWN' | +| **`Online`** | 'ONLINE' | +| **`Offline`** | 'OFFLINE' | + + +#### LocationStatus + +| Members | Value | +| ------------- | ---------------------- | +| **`NotSet`** | 'NOT_SET' | +| **`Set`** | 'SET' | +| **`Unknown`** | 'UNKNOWN' | + + +#### DeviceType + +| Members | Value | +| ---------------------- | ------------------------------- | +| **`cotsDevice`** | 'cotsDevice' | +| **`wisePad3s`** | 'wisePad3s' | +| **`appleBuiltIn`** | 'appleBuiltIn' | +| **`chipper1X`** | 'chipper1X' | +| **`chipper2X`** | 'chipper2X' | +| **`etna`** | 'etna' | +| **`stripeM2`** | 'stripeM2' | +| **`stripeS700`** | 'stripeS700' | +| **`stripeS700DevKit`** | 'stripeS700Devkit' | +| **`verifoneP400`** | 'verifoneP400' | +| **`wiseCube`** | 'wiseCube' | +| **`wisePad3`** | 'wisePad3' | +| **`wisePosE`** | 'wisePosE' | +| **`wisePosEDevKit`** | 'wisePosEDevkit' | +| **`unknown`** | 'unknown' | + + #### TerminalConnectTypes | Members | Value | @@ -808,6 +1083,9 @@ addListener(eventName: TerminalEventsEnum.PaymentStatusChange, listenerFunc: ({ | **`RequestDisplayMessage`** | 'terminalRequestDisplayMessage' | | **`RequestReaderInput`** | 'terminalRequestReaderInput' | | **`PaymentStatusChange`** | 'terminalPaymentStatusChange' | +| **`ReaderReconnectStarted`** | 'terminalReaderReconnectStarted' | +| **`ReaderReconnectSucceeded`** | 'terminalReaderReconnectSucceeded' | +| **`ReaderReconnectFailed`** | 'terminalReaderReconnectFailed' | #### DisconnectReason @@ -833,26 +1111,6 @@ addListener(eventName: TerminalEventsEnum.PaymentStatusChange, listenerFunc: ({ | **`Connected`** | 'CONNECTED' | -#### UpdateTimeEstimate - -| Members | Value | -| -------------------------- | -------------------------------------- | -| **`LessThanOneMinute`** | 'LESS_THAN_ONE_MINUTE' | -| **`OneToTwoMinutes`** | 'ONE_TO_TWO_MINUTES' | -| **`TwoToFiveMinutes`** | 'TWO_TO_FIVE_MINUTES' | -| **`FiveToFifteenMinutes`** | 'FIVE_TO_FIFTEEN_MINUTES' | - - -#### BatteryStatus - -| Members | Value | -| -------------- | ----------------------- | -| **`Unknown`** | 'UNKNOWN' | -| **`Critical`** | 'CRITICAL' | -| **`Low`** | 'LOW' | -| **`Nominal`** | 'NOMINAL' | - - #### ReaderEvent | Members | Value | diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.java index a0252d006..e7e9a84ab 100644 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.java +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.java @@ -14,6 +14,7 @@ import com.getcapacitor.JSArray; import com.getcapacitor.JSObject; import com.getcapacitor.PluginCall; +import com.getcapacitor.community.stripe.terminal.helper.TerminalMappers; import com.getcapacitor.community.stripe.terminal.models.Executor; import com.google.android.gms.common.util.BiConsumer; import com.stripe.stripeterminal.Terminal; @@ -28,14 +29,19 @@ import com.stripe.stripeterminal.external.callable.TerminalListener; import com.stripe.stripeterminal.external.models.BatteryStatus; import com.stripe.stripeterminal.external.models.CardPresentDetails; +import com.stripe.stripeterminal.external.models.Cart; +import com.stripe.stripeterminal.external.models.CartLineItem; import com.stripe.stripeterminal.external.models.CollectConfiguration; import com.stripe.stripeterminal.external.models.ConnectionConfiguration.BluetoothConnectionConfiguration; import com.stripe.stripeterminal.external.models.ConnectionConfiguration.InternetConnectionConfiguration; import com.stripe.stripeterminal.external.models.ConnectionConfiguration.LocalMobileConnectionConfiguration; import com.stripe.stripeterminal.external.models.ConnectionConfiguration.UsbConnectionConfiguration; import com.stripe.stripeterminal.external.models.ConnectionStatus; +import com.stripe.stripeterminal.external.models.DeviceType; import com.stripe.stripeterminal.external.models.DisconnectReason; import com.stripe.stripeterminal.external.models.DiscoveryConfiguration; +import com.stripe.stripeterminal.external.models.Location; +import com.stripe.stripeterminal.external.models.LocationStatus; import com.stripe.stripeterminal.external.models.PaymentIntent; import com.stripe.stripeterminal.external.models.PaymentMethod; import com.stripe.stripeterminal.external.models.PaymentStatus; @@ -54,13 +60,17 @@ import java.util.List; import java.util.Objects; import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; public class StripeTerminal extends Executor { private TokenProvider tokenProvider; private Cancelable discoveryCancelable; private Cancelable collectCancelable; - private List readers; + private Cancelable installUpdateCancelable; + private Cancelable cancelReaderConnectionCancellable; + private List discoveredReadersList; private String locationId; private PluginCall collectCall; private PluginCall confirmPaymentIntentCall; @@ -69,6 +79,8 @@ public class StripeTerminal extends Executor { private TerminalConnectTypes terminalConnectType; private PaymentIntent paymentIntentInstance; + private final TerminalMappers terminalMappers = new TerminalMappers(); + public StripeTerminal( Supplier contextSupplier, Supplier activitySupplier, @@ -77,7 +89,7 @@ public StripeTerminal( ) { super(contextSupplier, activitySupplier, notifyListenersFunction, pluginLogTag, "StripeTerminalExecutor"); this.contextSupplier = contextSupplier; - this.readers = new ArrayList<>(); + this.discoveredReadersList = new ArrayList<>(); } public void initialize(final PluginCall call) throws TerminalException { @@ -189,11 +201,11 @@ public void onDiscoverReaders(final PluginCall call) { final DiscoveryListener discoveryListener = readers -> { // 検索したReaderの一覧をListenerで渡す Log.d(logTag, String.valueOf(readers.get(0).getSerialNumber())); - this.readers = readers; + this.discoveredReadersList = readers; JSArray readersJSObject = new JSArray(); int i = 0; - for (Reader reader : this.readers) { + for (Reader reader : this.discoveredReadersList) { readersJSObject.put(convertReaderInterface(reader).put("index", String.valueOf(i))); } this.notifyListeners(TerminalEnumEvent.DiscoveredReaders.getWebEventName(), new JSObject().put("readers", readersJSObject)); @@ -268,81 +280,125 @@ public void onFailure(@NonNull TerminalException ex) { private void connectLocalMobileReader(final PluginCall call) { JSObject reader = call.getObject("reader"); + String serialNumber = reader.getString("serialNumber"); + + Reader foundReader = + this.discoveredReadersList.stream().filter(device -> serialNumber.equals(device.getSerialNumber())).findFirst().orElse(null); - if (reader.getInteger("index") == null) { + if (serialNumber == null || foundReader == null) { call.reject("The reader value is not set correctly."); return; } + Boolean autoReconnectOnUnexpectedDisconnect = call.getBoolean("autoReconnectOnUnexpectedDisconnect", false); + LocalMobileConnectionConfiguration config = new LocalMobileConnectionConfiguration( this.locationId, - true, - this.localMobileReaderReconnectionListener + autoReconnectOnUnexpectedDisconnect, + this.readerReconnectionListener ); - Terminal.getInstance().connectLocalMobileReader(this.readers.get(reader.getInteger("index")), config, this.readerCallback(call)); + Terminal.getInstance().connectLocalMobileReader(foundReader, config, this.readerCallback(call)); } - ReaderReconnectionListener localMobileReaderReconnectionListener = new ReaderReconnectionListener() { + ReaderReconnectionListener readerReconnectionListener = new ReaderReconnectionListener() { @Override - public void onReaderReconnectStarted(@NonNull Reader reader, @NonNull Cancelable cancelReconnect) { - // 1. Notified at the start of a reconnection attempt - // Use cancelable to stop reconnection at any time + public void onReaderReconnectStarted(@NonNull Reader reader, @NonNull Cancelable cancelable, @NonNull DisconnectReason reason) { + cancelReaderConnectionCancellable = cancelable; + notifyListeners( + TerminalEnumEvent.ReaderReconnectStarted.getWebEventName(), + new JSObject().put("reason", reason.toString()).put("reader", convertReaderInterface(reader)) + ); } @Override public void onReaderReconnectSucceeded(@NonNull Reader reader) { - // 2. Notified when reader reconnection succeeds - // App is now connected + notifyListeners( + TerminalEnumEvent.ReaderReconnectSucceeded.getWebEventName(), + new JSObject().put("reader", convertReaderInterface(reader)) + ); } @Override public void onReaderReconnectFailed(@NonNull Reader reader) { - // 3. Notified when reader reconnection fails - // App is now disconnected + notifyListeners( + TerminalEnumEvent.ReaderReconnectFailed.getWebEventName(), + new JSObject().put("reader", convertReaderInterface(reader)) + ); } }; private void connectInternetReader(final PluginCall call) { JSObject reader = call.getObject("reader"); + String serialNumber = reader.getString("serialNumber"); + + Reader foundReader = + this.discoveredReadersList.stream().filter(device -> serialNumber.equals(device.getSerialNumber())).findFirst().orElse(null); + + if (serialNumber == null || foundReader == null) { + call.reject("The reader value is not set correctly."); + return; + } + InternetConnectionConfiguration config = new InternetConnectionConfiguration(); - Terminal.getInstance().connectInternetReader(this.readers.get(reader.getInteger("index")), config, this.readerCallback(call)); + Terminal.getInstance().connectInternetReader(foundReader, config, this.readerCallback(call)); } private void connectUsbReader(final PluginCall call) { JSObject reader = call.getObject("reader"); + String serialNumber = reader.getString("serialNumber"); + + Reader foundReader = + this.discoveredReadersList.stream().filter(device -> serialNumber.equals(device.getSerialNumber())).findFirst().orElse(null); + + if (serialNumber == null || foundReader == null) { + call.reject("The reader value is not set correctly."); + return; + } + UsbConnectionConfiguration config = new UsbConnectionConfiguration(this.locationId); - Terminal - .getInstance() - .connectUsbReader(this.readers.get(reader.getInteger("index")), config, this.readerListener(), this.readerCallback(call)); + Terminal.getInstance().connectUsbReader(foundReader, config, this.readerListener(), this.readerCallback(call)); } private void connectBluetoothReader(final PluginCall call) { JSObject reader = call.getObject("reader"); - BluetoothConnectionConfiguration config = new BluetoothConnectionConfiguration(this.locationId); - Terminal - .getInstance() - .connectBluetoothReader(this.readers.get(reader.getInteger("index")), config, this.readerListener(), this.readerCallback(call)); + String serialNumber = reader.getString("serialNumber"); + + Reader foundReader = + this.discoveredReadersList.stream().filter(device -> serialNumber.equals(device.getSerialNumber())).findFirst().orElse(null); + + if (serialNumber == null || foundReader == null) { + call.reject("The reader value is not set correctly."); + return; + } + Boolean autoReconnectOnUnexpectedDisconnect = call.getBoolean("autoReconnectOnUnexpectedDisconnect", false); + + BluetoothConnectionConfiguration config = new BluetoothConnectionConfiguration( + this.locationId, + autoReconnectOnUnexpectedDisconnect, + this.readerReconnectionListener + ); + Terminal.getInstance().connectBluetoothReader(foundReader, config, this.readerListener(), this.readerCallback(call)); } public void cancelDiscoverReaders(final PluginCall call) { - if (discoveryCancelable != null) { - discoveryCancelable.cancel( - new Callback() { - @Override - public void onSuccess() { - notifyListeners(TerminalEnumEvent.CancelDiscoveredReaders.getWebEventName(), emptyObject); - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException ex) { - call.reject(ex.getLocalizedMessage(), ex); - } - } - ); - } else { + if (discoveryCancelable == null || discoveryCancelable.isCompleted()) { call.resolve(); + return; } + discoveryCancelable.cancel( + new Callback() { + @Override + public void onSuccess() { + notifyListeners(TerminalEnumEvent.CancelDiscoveredReaders.getWebEventName(), emptyObject); + call.resolve(); + } + + @Override + public void onFailure(@NonNull TerminalException ex) { + call.reject(ex.getLocalizedMessage(), ex); + } + } + ); } public void collectPaymentMethod(final PluginCall call) { @@ -364,7 +420,6 @@ public void cancelCollectPaymentMethod(final PluginCall call) { new Callback() { @Override public void onSuccess() { - collectCancelable = null; notifyListeners(TerminalEnumEvent.Canceled.getWebEventName(), emptyObject); call.resolve(); } @@ -394,7 +449,6 @@ public void onFailure(@NonNull TerminalException ex) { private final PaymentIntentCallback collectPaymentMethodCallback = new PaymentIntentCallback() { @Override public void onSuccess(PaymentIntent paymentIntent) { - collectCancelable = null; paymentIntentInstance = paymentIntent; notifyListeners(TerminalEnumEvent.CollectedPaymentIntent.getWebEventName(), emptyObject); @@ -424,7 +478,6 @@ public void onSuccess(PaymentIntent paymentIntent) { @Override public void onFailure(@NonNull TerminalException ex) { - collectCancelable = null; notifyListeners(TerminalEnumEvent.Failed.getWebEventName(), emptyObject); collectCall.reject(ex.getLocalizedMessage(), ex); } @@ -440,6 +493,149 @@ public void confirmPaymentIntent(final PluginCall call) { Terminal.getInstance().confirmPaymentIntent(this.paymentIntentInstance, confirmPaymentMethodCallback); } + public void installAvailableUpdate(final PluginCall call) { + Terminal.getInstance().installAvailableUpdate(); + call.resolve(emptyObject); + } + + public void cancelInstallUpdate(final PluginCall call) { + if (this.installUpdateCancelable == null || this.installUpdateCancelable.isCompleted()) { + call.resolve(); + return; + } + + this.installUpdateCancelable.cancel( + new Callback() { + @Override + public void onSuccess() { + call.resolve(); + } + + @Override + public void onFailure(@NonNull TerminalException e) { + call.reject(e.getLocalizedMessage()); + } + } + ); + } + + public void setReaderDisplay(final PluginCall call) { + String currency = call.getString("currency", null); + if (currency == null) { + call.reject("You must provide a currency value"); + return; + } + + int tax = call.getInt("tax", 0); + int total = call.getInt("total", 0); + if (total == 0) { + call.reject("You must provide a total value"); + return; + } + + JSArray lineItems = call.getArray("lineItems"); + List lineItemsList; + try { + lineItemsList = lineItems.toList(); + } catch (JSONException e) { + call.reject(e.getLocalizedMessage()); + return; + } + + List cartLineItems = new ArrayList<>(); + for (JSONObject item : lineItemsList) { + try { + JSObject itemObj = JSObject.fromJSONObject(item); + cartLineItems.add( + new CartLineItem( + Objects.requireNonNull(itemObj.getString("displayName")), + Objects.requireNonNull(itemObj.getInteger("quantity")), + Objects.requireNonNull(itemObj.getInteger("amount")) + ) + ); + } catch (JSONException e) { + call.reject(e.getLocalizedMessage()); + return; + } + } + + Cart cart = new Cart.Builder(currency, tax, total, cartLineItems).build(); + + Terminal + .getInstance() + .setReaderDisplay( + cart, + new Callback() { + @Override + public void onSuccess() { + call.resolve(); + } + + @Override + public void onFailure(@NonNull TerminalException e) { + call.reject(e.getErrorMessage()); + } + } + ); + } + + public void clearReaderDisplay(final PluginCall call) { + Terminal + .getInstance() + .clearReaderDisplay( + new Callback() { + @Override + public void onSuccess() { + call.resolve(); + } + + @Override + public void onFailure(@NonNull TerminalException e) { + call.reject(e.getErrorMessage()); + } + } + ); + } + + public void rebootReader(final PluginCall call) { + Terminal + .getInstance() + .rebootReader( + new Callback() { + @Override + public void onSuccess() { + paymentIntentInstance = null; + call.resolve(emptyObject); + } + + @Override + public void onFailure(@NonNull TerminalException e) { + call.reject(e.getLocalizedMessage()); + } + } + ); + } + + public void cancelReaderReconnection(final PluginCall call) { + if (cancelReaderConnectionCancellable == null || cancelReaderConnectionCancellable.isCompleted()) { + call.resolve(); + return; + } + cancelReaderConnectionCancellable.cancel( + new Callback() { + @Override + public void onSuccess() { + call.resolve(); + } + + @Override + public void onFailure(@NonNull TerminalException ex) { + call.reject(ex.getLocalizedMessage(), ex); + } + } + ); + } + private final PaymentIntentCallback confirmPaymentMethodCallback = new PaymentIntentCallback() { @Override public void onSuccess(PaymentIntent paymentIntent) { @@ -476,6 +672,7 @@ private ReaderListener readerListener() { @Override public void onStartInstallingUpdate(@NotNull ReaderSoftwareUpdate update, @NotNull Cancelable cancelable) { // Show UI communicating that a required update has started installing + installUpdateCancelable = cancelable; notifyListeners( TerminalEnumEvent.StartInstallingUpdate.getWebEventName(), new JSObject().put("update", convertReaderSoftwareUpdate(update)) @@ -558,14 +755,35 @@ public void onDisconnect(@NotNull DisconnectReason reason) { } private JSObject convertReaderInterface(Reader reader) { - return new JSObject().put("serialNumber", reader.getSerialNumber()); + return new JSObject() + .put("label", reader.getLabel()) + .put("serialNumber", reader.getSerialNumber()) + .put("id", reader.getId()) + .put("locationId", reader.getLocation() != null ? reader.getLocation().getId() : null) + .put("deviceSoftwareVersion", reader.getDeviceSwVersion$external_publish()) + .put("simulated", reader.isSimulated()) + .put("serialNumber", reader.getSerialNumber()) + .put("ipAddress", reader.getIpAddress()) + .put("baseUrl", reader.getBaseUrl()) + .put("bootloaderVersion", reader.getBootloaderVersion()) + .put("configVersion", reader.getConfigVersion()) + .put("emvKeyProfileId", reader.getEmvKeyProfileId()) + .put("firmwareVersion", reader.getFirmwareVersion()) + .put("hardwareVersion", reader.getHardwareVersion()) + .put("macKeyProfileId", reader.getMacKeyProfileId()) + .put("pinKeyProfileId", reader.getPinKeyProfileId()) + .put("trackKeyProfileId", reader.getTrackKeyProfileId()) + .put("settingsVersion", reader.getSettingsVersion()) + .put("pinKeysetId", reader.getPinKeysetId()) + .put("deviceType", terminalMappers.mapFromDeviceType(reader.getDeviceType())) + .put("status", terminalMappers.mapFromNetworkStatus(reader.getNetworkStatus())) + .put("locationStatus", terminalMappers.mapFromLocationStatus(reader.getLocationStatus())) + .put("batteryLevel", reader.getBatteryLevel() != null ? reader.getBatteryLevel().doubleValue() : null) + .put("availableUpdate", terminalMappers.mapFromReaderSoftwareUpdate(reader.getAvailableUpdate())) + .put("location", terminalMappers.mapFromLocation(reader.getLocation())); } private JSObject convertReaderSoftwareUpdate(ReaderSoftwareUpdate update) { - return new JSObject() - .put("version", update.getVersion()) - .put("settingsVersion", update.getSettingsVersion()) - .put("requiredAt", update.getRequiredAt().getTime()) - .put("timeEstimate", update.getTimeEstimate().toString()); + return terminalMappers.mapFromReaderSoftwareUpdate(update); } } diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.java index e52cd7a97..ba11ce81b 100644 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.java +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.java @@ -160,4 +160,34 @@ public void cancelCollectPaymentMethod(final PluginCall call) { public void confirmPaymentIntent(PluginCall call) { this.implementation.confirmPaymentIntent(call); } + + @PluginMethod + public void installAvailableUpdate(PluginCall call) { + this.implementation.installAvailableUpdate(call); + } + + @PluginMethod + public void cancelInstallUpdate(PluginCall call) { + this.implementation.cancelInstallUpdate(call); + } + + @PluginMethod + public void setReaderDisplay(PluginCall call) { + this.implementation.setReaderDisplay(call); + } + + @PluginMethod + public void clearReaderDisplay(PluginCall call) { + this.implementation.clearReaderDisplay(call); + } + + @PluginMethod + public void rebootReader(PluginCall call) { + this.implementation.rebootReader(call); + } + + @PluginMethod + public void cancelReaderReconnection(PluginCall call) { + this.implementation.cancelReaderReconnection(call); + } } diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TerminalEvent.kt b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TerminalEvent.kt index 4bbf84605..ed3839a1f 100644 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TerminalEvent.kt +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TerminalEvent.kt @@ -22,4 +22,7 @@ enum class TerminalEnumEvent(val webEventName: String) { RequestDisplayMessage("terminalRequestDisplayMessage"), RequestReaderInput("terminalRequestReaderInput"), PaymentStatusChange("terminalPaymentStatusChange"), + ReaderReconnectStarted("terminalReaderReconnectStarted"), + ReaderReconnectSucceeded("terminalReaderReconnectSucceeded"), + ReaderReconnectFailed("terminalReaderReconnectFailed"), } diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.java new file mode 100644 index 000000000..38623649c --- /dev/null +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.java @@ -0,0 +1,96 @@ +package com.getcapacitor.community.stripe.terminal.helper; + +import com.getcapacitor.JSObject; +import com.stripe.stripeterminal.external.models.DeviceType; +import com.stripe.stripeterminal.external.models.Location; +import com.stripe.stripeterminal.external.models.LocationStatus; +import com.stripe.stripeterminal.external.models.Reader; +import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate; + +public class TerminalMappers { + + public JSObject mapFromLocation(Location location) { + if (location == null) { + return new JSObject(); + } + + JSObject address = new JSObject(); + + if (location.getAddress() != null) { + address + .put("country", location.getAddress().getCountry()) + .put("city", location.getAddress().getCity()) + .put("postalCode", location.getAddress().getPostalCode()) + .put("line1", location.getAddress().getLine1()) + .put("line2", location.getAddress().getLine2()) + .put("state", location.getAddress().getState()); + } + + return new JSObject() + .put("id", location.getId()) + .put("displayName", location.getDisplayName()) + .put("address", address) + .put("livemode", location.getLivemode()); + } + + public JSObject mapFromReaderSoftwareUpdate(ReaderSoftwareUpdate update) { + if (update == null) { + return new JSObject(); + } + + return new JSObject() + .put("deviceSoftwareVersion", update.getVersion()) + .put("estimatedUpdateTime", update.getTimeEstimate().toString()) + .put("requiredAt", update.getRequiredAt().getTime()); + } + + public String mapFromLocationStatus(LocationStatus status) { + if (status == null) { + return "UNKNOWN"; + } + + return switch (status) { + case NOT_SET -> "NOT_SET"; + case SET -> "SET"; + case UNKNOWN -> "UNKNOWN"; + default -> "UNKNOWN"; + }; + } + + public String mapFromNetworkStatus(Reader.NetworkStatus status) { + if (status == null) { + return "unknown"; + } + + return switch (status) { + case OFFLINE -> "OFFLINE"; + case ONLINE -> "ONLINE"; + default -> "UNKNOWN"; + }; + } + + public String mapFromDeviceType(DeviceType type) { + return switch (type) { + case CHIPPER_1X -> "chipper1X"; + case CHIPPER_2X -> "chipper2X"; + case COTS_DEVICE -> "cotsDevice"; + case ETNA -> "etna"; + case STRIPE_M2 -> "stripeM2"; + case STRIPE_S700 -> "stripeS700"; + case STRIPE_S700_DEVKIT -> "stripeS700Devkit"; + // React Native has this model. deprecated? + // case STRIPE_S710: + // return "stripeS710"; + // case STRIPE_S710_DEVKIT: + // return "stripeS710Devkit"; + case UNKNOWN -> "unknown"; + case VERIFONE_P400 -> "verifoneP400"; + case WISECUBE -> "wiseCube"; + case WISEPAD_3 -> "wisePad3"; + case WISEPAD_3S -> "wisePad3s"; + case WISEPOS_E -> "wisePosE"; + case WISEPOS_E_DEVKIT -> "wisePosEDevkit"; + default -> throw new IllegalArgumentException("Unknown DeviceType: " + type); + }; + } +} diff --git a/packages/terminal/ios/Plugin/StripeTerminal.swift b/packages/terminal/ios/Plugin/StripeTerminal.swift index 86aa9e69b..136d31ad2 100644 --- a/packages/terminal/ios/Plugin/StripeTerminal.swift +++ b/packages/terminal/ios/Plugin/StripeTerminal.swift @@ -2,20 +2,24 @@ import Foundation import Capacitor import StripeTerminal -public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDelegate, BluetoothReaderDelegate, TerminalDelegate { +public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDelegate, BluetoothReaderDelegate, TerminalDelegate, ReconnectionDelegate { weak var plugin: StripeTerminalPlugin? private let apiClient = APIClient() + var discoverCancelable: Cancelable? + var collectCancelable: Cancelable? + var installUpdateCancelable: Cancelable? + var cancelReaderConnectionCancellable: Cancelable? + var discoverCall: CAPPluginCall? var locationId: String? var isTest: Bool? - var collectCancelable: Cancelable? var type: DiscoveryMethod? var isInitialize: Bool = false var paymentIntent: PaymentIntent? - var readers: [Reader]? + var discoveredReadersList: [Reader]? @objc public func initialize(_ call: CAPPluginCall) { self.isTest = call.getBool("isTest", true) @@ -61,38 +65,21 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg call.reject(error.localizedDescription) self.discoverCall = nil } else { + // This call is passed to discoverCall. So not resolve. } } } - func cancelDiscoverReaders(_ call: CAPPluginCall) { - - if let cancelable = self.discoverCancelable { - cancelable.cancel { error in - if let error = error { - call.reject(error.localizedDescription) - } else { - self.collectCancelable = nil - call.resolve() - } - } - return - } - - call.resolve() - } - public func terminal(_ terminal: Terminal, didUpdateDiscoveredReaders readers: [Reader]) { var readersJSObject: JSArray = [] var i = 0 for reader in readers { readersJSObject.append([ - "index": i, - "serialNumber": reader.serialNumber - ]) + "index": i + ].merging(self.convertReaderInterface(reader: reader)) { (_, new) in new }) i += 1 } - self.readers = readers + self.discoveredReadersList = readers self.plugin?.notifyListeners(TerminalEvents.DiscoveredReaders.rawValue, data: ["readers": readersJSObject]) self.discoverCall?.resolve([ @@ -113,9 +100,7 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg public func getConnectedReader(_ call: CAPPluginCall) { if let reader = Terminal.shared.connectedReader { - call.resolve(["reader": [ - "serialNumber": reader.serialNumber - ]]) + call.resolve(["reader": self.convertReaderInterface(reader: reader)]) } else { call.resolve(["reader": nil]) } @@ -140,11 +125,25 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg } private func connectLocalMobileReader(_ call: CAPPluginCall) { - let connectionConfig = try! LocalMobileConnectionConfigurationBuilder.init(locationId: self.locationId!).build() + let autoReconnectOnUnexpectedDisconnect = call.getBool("autoReconnectOnUnexpectedDisconnect", false) + let merchantDisplayName: String? = call.getString("merchantDisplayName") + let onBehalfOf: String? = call.getString("onBehalfOf") let reader: JSObject = call.getObject("reader")! - let index: Int = reader["index"] as! Int + let serialNumber: String = reader["serialNumber"] as! String - Terminal.shared.connectLocalMobileReader(self.readers![index], delegate: self, connectionConfig: connectionConfig) { reader, error in + let connectionConfig = try! LocalMobileConnectionConfigurationBuilder.init(locationId: self.locationId!) + .setMerchantDisplayName(merchantDisplayName ?? nil) + .setOnBehalfOf(onBehalfOf ?? nil) + .setAutoReconnectOnUnexpectedDisconnect(autoReconnectOnUnexpectedDisconnect) + .setAutoReconnectionDelegate(autoReconnectOnUnexpectedDisconnect ? self : nil) + .build() + + guard let foundReader = self.discoveredReadersList?.first(where: { $0.serialNumber == serialNumber }) else { + call.reject("reader is not match from descovered readers.") + return + } + + Terminal.shared.connectLocalMobileReader(foundReader, delegate: self, connectionConfig: connectionConfig) { reader, error in if let reader = reader { self.plugin?.notifyListeners(TerminalEvents.ConnectedReader.rawValue, data: [:]) call.resolve() @@ -155,13 +154,19 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg } private func connectInternetReader(_ call: CAPPluginCall) { + let reader: JSObject = call.getObject("reader")! + let serialNumber: String = reader["serialNumber"] as! String + + guard let foundReader = self.discoveredReadersList?.first(where: { $0.serialNumber == serialNumber }) else { + call.reject("reader is not match from descovered readers.") + return + } + let config = try! InternetConnectionConfigurationBuilder() .setFailIfInUse(true) .build() - let reader: JSObject = call.getObject("reader")! - let index: Int = reader["index"] as! Int - Terminal.shared.connectInternetReader(self.readers![index], connectionConfig: config) { reader, error in + Terminal.shared.connectInternetReader(foundReader, connectionConfig: config) { reader, error in if let reader = reader { self.plugin?.notifyListeners(TerminalEvents.ConnectedReader.rawValue, data: [:]) call.resolve() @@ -172,11 +177,24 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg } private func connectBluetoothReader(_ call: CAPPluginCall) { - let config = try! BluetoothConnectionConfigurationBuilder(locationId: self.locationId!).build() let reader: JSObject = call.getObject("reader")! - let index: Int = reader["index"] as! Int + let serialNumber: String = reader["serialNumber"] as! String + + guard let foundReader = self.discoveredReadersList?.first(where: { $0.serialNumber == serialNumber }) else { + call.reject("reader is not match from descovered readers.") + return + } + + let autoReconnectOnUnexpectedDisconnect = call.getBool("autoReconnectOnUnexpectedDisconnect", false) + let merchantDisplayName: String? = call.getString("merchantDisplayName") + let onBehalfOf: String? = call.getString("onBehalfOf") - Terminal.shared.connectBluetoothReader(self.readers![index], delegate: self, connectionConfig: config) { reader, error in + let config = try! BluetoothConnectionConfigurationBuilder(locationId: self.locationId!) + .setAutoReconnectOnUnexpectedDisconnect(autoReconnectOnUnexpectedDisconnect) + .setAutoReconnectionDelegate(autoReconnectOnUnexpectedDisconnect ? self : nil) + .build() + + Terminal.shared.connectBluetoothReader(foundReader, delegate: self, connectionConfig: config) { reader, error in if let reader = reader { self.plugin?.notifyListeners(TerminalEvents.ConnectedReader.rawValue, data: [:]) call.resolve() @@ -190,6 +208,7 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg Terminal.shared.retrievePaymentIntent(clientSecret: call.getString("paymentIntent")!) { retrieveResult, retrieveError in if let error = retrieveError { print("retrievePaymentIntent failed: \(error)") + call.reject(error.localizedDescription) } else if let paymentIntent = retrieveResult { self.collectCancelable = Terminal.shared.collectPaymentMethod(paymentIntent) { collectResult, collectError in if let error = collectError { @@ -205,23 +224,6 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg } } - public func cancelCollectPaymentMethod(_ call: CAPPluginCall) { - if let cancelable = self.collectCancelable { - cancelable.cancel { error in - if let error = error { - call.reject(error.localizedDescription) - } else { - self.plugin?.notifyListeners(TerminalEvents.Canceled.rawValue, data: [:]) - self.collectCancelable = nil - self.paymentIntent = nil - call.resolve() - } - } - return - } - call.resolve() - } - public func confirmPaymentIntent(_ call: CAPPluginCall) { if let paymentIntent = self.paymentIntent { Terminal.shared.confirmPaymentIntent(paymentIntent) { confirmResult, confirmError in @@ -250,6 +252,151 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg call.resolve([:]) } + public func installAvailableUpdate(_ call: CAPPluginCall) { + Terminal.shared.installAvailableUpdate() + call.resolve([:]) + } + + public func setReaderDisplay(_ call: CAPPluginCall) { + guard let currency = call.getString("currency") else { + call.reject("You must provide a currency value") + return + } + guard let tax = call.getInt("tax") as? NSNumber else { + call.reject("You must provide a tax value") + return + } + guard let total = call.getInt("total") as? NSNumber else { + call.reject("You must provide a total value") + return + } + + let cartBuilder = CartBuilder(currency: currency) + .setTax(Int(truncating: tax)) + .setTotal(Int(truncating: total)) + + let cartLineItems = TerminalMappers.mapToCartLineItems(call.getArray("lineItems") ?? JSArray()) + + cartBuilder.setLineItems(cartLineItems) + + let cart: Cart + do { + cart = try cartBuilder.build() + } catch { + call.reject(error.localizedDescription) + return + } + + Terminal.shared.setReaderDisplay(cart) { error in + if let error = error as NSError? { + call.reject(error.localizedDescription) + } else { + call.resolve([:]) + } + } + + } + + public func clearReaderDisplay(_ call: CAPPluginCall) { + Terminal.shared.clearReaderDisplay { error in + if let error = error as NSError? { + call.reject(error.localizedDescription) + } else { + call.resolve([:]) + } + } + } + + public func rebootReader(_ call: CAPPluginCall) { + Terminal.shared.rebootReader { error in + if let error = error as NSError? { + call.reject(error.localizedDescription) + } else { + self.paymentIntent = nil + call.resolve([:]) + } + } + } + + /** + * Cancelable + */ + public func cancelInstallUpdate(_ call: CAPPluginCall) { + if let cancelable = self.installUpdateCancelable { + if cancelable.completed { + call.resolve() + return + } + cancelable.cancel { error in + if let error = error as NSError? { + call.reject(error.localizedDescription) + } else { + call.resolve([:]) + } + } + return + } + call.resolve([:]) + } + + public func cancelCollectPaymentMethod(_ call: CAPPluginCall) { + if let cancelable = self.collectCancelable { + if cancelable.completed { + call.resolve() + return + } + cancelable.cancel { error in + if let error = error { + call.reject(error.localizedDescription) + } else { + self.plugin?.notifyListeners(TerminalEvents.Canceled.rawValue, data: [:]) + self.paymentIntent = nil + call.resolve() + } + } + return + } + call.resolve() + } + + func cancelDiscoverReaders(_ call: CAPPluginCall) { + if let cancelable = self.discoverCancelable { + if cancelable.completed { + call.resolve() + return + } + cancelable.cancel { error in + if let error = error { + call.reject(error.localizedDescription) + } else { + call.resolve() + } + } + return + } + + call.resolve() + } + + public func cancelReaderReconnection(_ call: CAPPluginCall) { + if let cancelable = self.cancelReaderConnectionCancellable { + if cancelable.completed { + call.resolve() + return + } + cancelable.cancel { error in + if let error = error as NSError? { + call.reject(error.localizedDescription) + } else { + call.resolve([:]) + } + } + return + } + + call.resolve() + } + /* * Terminal */ @@ -270,7 +417,10 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg */ public func localMobileReader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) { - self.plugin?.notifyListeners(TerminalEvents.StartInstallingUpdate.rawValue, data: self.convertReaderSoftwareUpdate(update: update)) + self.installUpdateCancelable = cancelable + self.plugin?.notifyListeners(TerminalEvents.StartInstallingUpdate.rawValue, data: [ + "update": self.convertReaderSoftwareUpdate(update: update) + ]) } public func localMobileReader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) { @@ -307,11 +457,16 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg */ public func reader(_: Reader, didReportAvailableUpdate update: ReaderSoftwareUpdate) { - self.plugin?.notifyListeners(TerminalEvents.ReportAvailableUpdate.rawValue, data: self.convertReaderSoftwareUpdate(update: update)) + self.plugin?.notifyListeners(TerminalEvents.ReportAvailableUpdate.rawValue, data: [ + "update": self.convertReaderSoftwareUpdate(update: update) + ]) } public func reader(_: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) { - self.plugin?.notifyListeners(TerminalEvents.StartInstallingUpdate.rawValue, data: self.convertReaderSoftwareUpdate(update: update)) + self.installUpdateCancelable = cancelable + self.plugin?.notifyListeners(TerminalEvents.StartInstallingUpdate.rawValue, data: [ + "update": self.convertReaderSoftwareUpdate(update: update) + ]) } public func reader(_: Reader, didReportReaderSoftwareUpdateProgress progress: Float) { @@ -363,19 +518,49 @@ public class StripeTerminal: NSObject, DiscoveryDelegate, LocalMobileReaderDeleg ]) } - private func convertReaderInterface(reader: Reader) -> [String: String] { - return ["serialNumber": reader.serialNumber] + /* + * Reconnection + */ + public func reader(_ reader: Reader, didStartReconnect cancelable: Cancelable, disconnectReason: DisconnectReason) { + self.cancelReaderConnectionCancellable = cancelable + self.plugin?.notifyListeners(TerminalEvents.ReaderReconnectStarted.rawValue, data: ["reader": self.convertReaderInterface(reader: reader), "reason": disconnectReason.rawValue]) + } + + public func readerDidSucceedReconnect(_ reader: Reader) { + self.plugin?.notifyListeners(TerminalEvents.ReaderReconnectSucceeded.rawValue, data: ["reader": self.convertReaderInterface(reader: reader)]) + } + + public func readerDidFailReconnect(_ reader: Reader) { + self.plugin?.notifyListeners(TerminalEvents.ReaderReconnectFailed.rawValue, data: ["reader": self.convertReaderInterface(reader: reader)]) } - private func convertReaderSoftwareUpdate(update: ReaderSoftwareUpdate) -> [String: String] { + /* + * Private + */ + private func convertReaderInterface(reader: Reader) -> JSObject { return [ - "version": update.deviceSoftwareVersion, - "settingsVersion": update.deviceSoftwareVersion, - "requiredAt": update.requiredAt.description, - "timeEstimate": TerminalMappers.mapFromUpdateTimeEstimate(update.estimatedUpdateTime) + "label": reader.label ?? NSNull(), + "batteryLevel": (reader.batteryLevel ?? 0).intValue, + "batteryStatus": TerminalMappers.mapFromBatteryStatus(reader.batteryStatus), + "simulated": reader.simulated, + "serialNumber": reader.serialNumber, + "isCharging": (reader.isCharging ?? 0).intValue, + "id": reader.stripeId ?? NSNull(), + "availableUpdate": TerminalMappers.mapFromReaderSoftwareUpdate(reader.availableUpdate), + "locationId": reader.locationId ?? NSNull(), + "ipAddress": reader.ipAddress ?? NSNull(), + "status": TerminalMappers.mapFromReaderNetworkStatus(reader.status), + "location": TerminalMappers.mapFromLocation(reader.location), + "locationStatus": TerminalMappers.mapFromLocationStatus(reader.locationStatus), + "deviceType": TerminalMappers.mapFromDeviceType(reader.deviceType), + "deviceSoftwareVersion": reader.deviceSoftwareVersion ?? NSNull() ] } + private func convertReaderSoftwareUpdate(update: ReaderSoftwareUpdate) -> JSObject { + return TerminalMappers.mapFromReaderSoftwareUpdate(update) + } + } class APIClient: ConnectionTokenProvider { diff --git a/packages/terminal/ios/Plugin/StripeTerminalPlugin.m b/packages/terminal/ios/Plugin/StripeTerminalPlugin.m index cd0a88768..e456baed0 100644 --- a/packages/terminal/ios/Plugin/StripeTerminalPlugin.m +++ b/packages/terminal/ios/Plugin/StripeTerminalPlugin.m @@ -15,4 +15,10 @@ CAP_PLUGIN_METHOD(cancelCollectPaymentMethod, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(confirmPaymentIntent, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setSimulatorConfiguration, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(installAvailableUpdate, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(cancelInstallUpdate, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(setReaderDisplay, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(clearReaderDisplay, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(rebootReader, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(cancelReaderReconnection, CAPPluginReturnPromise); ) diff --git a/packages/terminal/ios/Plugin/StripeTerminalPlugin.swift b/packages/terminal/ios/Plugin/StripeTerminalPlugin.swift index 32822010b..b5637a38d 100644 --- a/packages/terminal/ios/Plugin/StripeTerminalPlugin.swift +++ b/packages/terminal/ios/Plugin/StripeTerminalPlugin.swift @@ -3,10 +3,6 @@ import StripeTerminal import Capacitor import PassKit -/** - * Please read the Capacitor iOS Plugin Development Guide - * here: https://capacitorjs.com/docs/plugins/ios - */ @objc(StripeTerminalPlugin) public class StripeTerminalPlugin: CAPPlugin { private let implementation = StripeTerminal() @@ -64,4 +60,29 @@ public class StripeTerminalPlugin: CAPPlugin { @objc func confirmPaymentIntent(_ call: CAPPluginCall) { self.implementation.confirmPaymentIntent(call) } + + @objc func installAvailableUpdate(_ call: CAPPluginCall) { + self.implementation.installAvailableUpdate(call) + } + + @objc func cancelInstallUpdate(_ call: CAPPluginCall) { + self.implementation.cancelInstallUpdate(call) + } + + @objc func setReaderDisplay(_ call: CAPPluginCall) { + self.implementation.setReaderDisplay(call) + } + + @objc func clearReaderDisplay(_ call: CAPPluginCall) { + self.implementation.clearReaderDisplay(call) + } + + @objc func rebootReader(_ call: CAPPluginCall) { + self.implementation.rebootReader(call) + } + + @objc func cancelReaderReconnection(_ call: CAPPluginCall) { + self.implementation.cancelReaderReconnection(call) + } + } diff --git a/packages/terminal/ios/Plugin/TerminalEvents.swift b/packages/terminal/ios/Plugin/TerminalEvents.swift index 2450762c6..a97f76359 100644 --- a/packages/terminal/ios/Plugin/TerminalEvents.swift +++ b/packages/terminal/ios/Plugin/TerminalEvents.swift @@ -19,4 +19,7 @@ public enum TerminalEvents: String { case RequestDisplayMessage = "terminalRequestDisplayMessage" case RequestReaderInput = "terminalRequestReaderInput" case PaymentStatusChange = "terminalPaymentStatusChange" + case ReaderReconnectStarted = "terminalReaderReconnectStarted" + case ReaderReconnectSucceeded = "terminalReaderReconnectSucceeded" + case ReaderReconnectFailed = "terminalReaderReconnectFailed" } diff --git a/packages/terminal/ios/Plugin/TerminalMappers.swift b/packages/terminal/ios/Plugin/TerminalMappers.swift index a63ed32e0..af942fd32 100644 --- a/packages/terminal/ios/Plugin/TerminalMappers.swift +++ b/packages/terminal/ios/Plugin/TerminalMappers.swift @@ -2,6 +2,133 @@ import StripeTerminal import Capacitor class TerminalMappers { + class func mapFromDeviceType(_ type: DeviceType) -> String { + switch type { + case DeviceType.appleBuiltIn: return "appleBuiltIn" + case DeviceType.chipper1X: return "chipper1X" + case DeviceType.chipper2X: return "chipper2X" + case DeviceType.etna: return "etna" + case DeviceType.stripeM2: return "stripeM2" + case DeviceType.stripeS700: return "stripeS700" + case DeviceType.stripeS700DevKit: return "stripeS700Devkit" + case DeviceType.verifoneP400: return "verifoneP400" + case DeviceType.wiseCube: return "wiseCube" + case DeviceType.wisePad3: return "wisePad3" + case DeviceType.wisePosE: return "wisePosE" + case DeviceType.wisePosEDevKit: return "wisePosEDevkit" + default: return "unknown" + } + } + + class func mapFromAddress(_ address: Address?) -> JSObject { + if let address = address { + let result: JSObject = [ + "city": address.city ?? NSNull(), + "country": address.country ?? NSNull(), + "postalCode": address.postalCode ?? NSNull(), + "line1": address.line1 ?? NSNull(), + "line2": address.line2 ?? NSNull(), + "state": address.state ?? NSNull() + ] + return result + } else { + return JSObject() + } + } + + class func mapFromLocation(_ location: Location?) -> JSObject { + guard let unwrappedLocation = location else { + return [:] + } + let result: JSObject = [ + "displayName": unwrappedLocation.displayName ?? NSNull(), + "id": unwrappedLocation.stripeId, + "livemode": unwrappedLocation.livemode, + "address": mapFromAddress(unwrappedLocation.address) + ] + return result + } + + class func mapFromLocationStatus(_ status: LocationStatus) -> String { + switch status { + case LocationStatus.notSet: return "NOT_SET" + case LocationStatus.set: return "SET" + case LocationStatus.unknown: return "UNKNOWN" + default: return "UNKNOWN" + } + } + + class func mapFromLocationsList(_ locations: [Location]) -> JSArray { + var list: JSArray = [] + + for location in locations { + let result = mapFromLocation(location) + if result.count != 0 { + list.append(result) + } + } + + return list + } + + class func mapFromReaderNetworkStatus(_ status: ReaderNetworkStatus) -> String { + switch status { + case ReaderNetworkStatus.offline: return "OFFLINE" + case ReaderNetworkStatus.online: return "ONLINE" + default: return "UNKNOWN" + } + } + + class func convertDateToUnixTimestamp(date: Date?) -> String { + if let date = date { + let value = date.timeIntervalSince1970 * 1000.0 + return String(format: "%.0f", value) + } + return "" + } + + class func mapFromReaderSoftwareUpdate(_ update: ReaderSoftwareUpdate?) -> JSObject { + guard let unwrappedUpdate = update else { + return JSObject() + } + let result: JSObject = [ + "deviceSoftwareVersion": unwrappedUpdate.deviceSoftwareVersion, + "estimatedUpdateTime": mapFromUpdateTimeEstimate(unwrappedUpdate.estimatedUpdateTime), + "requiredAt": convertDateToUnixTimestamp(date: unwrappedUpdate.requiredAt) + ] + return result + } + + class func mapToCartLineItem(_ cartLineItem: NSDictionary) -> CartLineItem? { + guard let displayName = cartLineItem["displayName"] as? String else { return nil } + guard let quantity = cartLineItem["quantity"] as? NSNumber else { return nil } + guard let amount = cartLineItem["amount"] as? NSNumber else { return nil } + + do { + let lineItem = try CartLineItemBuilder(displayName: displayName) + .setQuantity(Int(truncating: quantity)) + .setAmount(Int(truncating: amount)) + .build() + return lineItem + } catch { + print("Error wihle building CartLineItem, error:\(error)") + return nil + } + } + + class func mapToCartLineItems(_ cartLineItems: JSArray) -> [CartLineItem] { + var items = [CartLineItem]() + + cartLineItems.forEach { + if let item = $0 as? NSDictionary { + if let lineItem = TerminalMappers.mapToCartLineItem(item) { + items.append(lineItem) + } + } + } + return items + } + class func mapToSimulateReaderUpdate(_ update: String) -> SimulateReaderUpdate { switch update { case "UPDATE_AVAILABLE": return SimulateReaderUpdate.available diff --git a/packages/terminal/package-lock.json b/packages/terminal/package-lock.json index 79236838c..0a3479dac 100644 --- a/packages/terminal/package-lock.json +++ b/packages/terminal/package-lock.json @@ -1,12 +1,12 @@ { "name": "@capacitor-community/stripe-terminal", - "version": "6.0.2", + "version": "6.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@capacitor-community/stripe-terminal", - "version": "6.0.2", + "version": "6.1.0", "license": "MIT", "devDependencies": { "@capacitor/android": "^6.0.0", diff --git a/packages/terminal/package.json b/packages/terminal/package.json index 22494ab95..cc530418e 100644 --- a/packages/terminal/package.json +++ b/packages/terminal/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor-community/stripe-terminal", - "version": "6.0.2", + "version": "6.1.0", "engines": { "node": ">=18.0.0" }, diff --git a/packages/terminal/src/definitions.ts b/packages/terminal/src/definitions.ts index 695679419..a1db543ee 100644 --- a/packages/terminal/src/definitions.ts +++ b/packages/terminal/src/definitions.ts @@ -13,18 +13,87 @@ import type { PaymentStatus, DisconnectReason, ConnectionStatus, + NetworkStatus, + LocationStatus, + DeviceType, } from './stripe.enum'; export type ReaderInterface = { - index: number; + /** + * The unique serial number is primary identifier inner plugin. + */ serialNumber: string; + + label: string; + batteryLevel: number; + batteryStatus: BatteryStatus; + simulated: boolean; + id: number; + availableUpdate: ReaderSoftwareUpdateInterface; + locationId: string; + ipAddress: string; + status: NetworkStatus; + location: LocationInterface; + locationStatus: LocationStatus; + deviceType: DeviceType; + deviceSoftwareVersion: string; + + /** + * iOS Only properties. These properties are not available on Android. + */ + isCharging: number; + + /** + * Android Only properties. These properties are not available on iOS. + */ + baseUrl: string; + bootloaderVersion: string; + configVersion: string; + emvKeyProfileId: string; + firmwareVersion: string; + hardwareVersion: string; + macKeyProfileId: string; + pinKeyProfileId: string; + trackKeyProfileId: string; + settingsVersion: string; + pinKeysetId: string; + + /** + * @deprecated This property has been deprecated and should use the `serialNumber` property. + */ + index?: number; +}; +export type LocationInterface = { + id: string; + displayName: string; + address: { + city: string; + country: string; + postalCode: string; + line1: string; + line2: string; + state: string; + }; + ipAddress: string; }; export type ReaderSoftwareUpdateInterface = { - version: string; - settingsVersion: string; + deviceSoftwareVersion: string; + estimatedUpdateTime: UpdateTimeEstimate; requiredAt: number; - timeEstimate: UpdateTimeEstimate; +}; + +export type CartLineItem = { + displayName: string; + quantity: number; + amount: number; +}; + +export type Cart = { + currency: string; + tax: number; + total: number; + lineItems: CartLineItem[]; }; export * from './events.enum'; @@ -49,13 +118,37 @@ export interface StripeTerminalPlugin { simulatedCard?: SimulatedCardType; simulatedTipAmount?: number; }): Promise; - connectReader(options: { reader: ReaderInterface }): Promise; + + /** + * @param options.autoReconnectOnUnexpectedDisconnect If true, the SDK will automatically attempt to reconnect to the reader. default is false. + */ + connectReader(options: { + reader: ReaderInterface; + autoReconnectOnUnexpectedDisconnect?: boolean; + + /** + * iOS and LocalMobileReader only. Android needs to be set to PaymentIntent only. + */ + merchantDisplayName?: string; + + /** + * iOS and LocalMobileReader only. Android needs to be set to PaymentIntent only. + * The Stripe account ID for which these funds are intended. + */ + onBehalfOf?: string; + }): Promise; getConnectedReader(): Promise<{ reader: ReaderInterface | null }>; disconnectReader(): Promise; cancelDiscoverReaders(): Promise; collectPaymentMethod(options: { paymentIntent: string }): Promise; cancelCollectPaymentMethod(): Promise; confirmPaymentIntent(): Promise; + installAvailableUpdate(): Promise; + cancelInstallUpdate(): Promise; + setReaderDisplay(options: Cart): Promise; + clearReaderDisplay(): Promise; + rebootReader(): Promise; + cancelReaderReconnection(): Promise; addListener( eventName: TerminalEventsEnum.Loaded, @@ -305,6 +398,27 @@ export interface StripeTerminalPlugin { listenerFunc: ({ status }: { status: PaymentStatus }) => void, ): Promise; + addListener( + eventName: TerminalEventsEnum.ReaderReconnectStarted, + listenerFunc: ({ + reader, + reason, + }: { + reader: ReaderInterface; + reason: string; + }) => void, + ): Promise; + + addListener( + eventName: TerminalEventsEnum.ReaderReconnectSucceeded, + listenerFunc: ({ reader }: { reader: ReaderInterface }) => void, + ): Promise; + + addListener( + eventName: TerminalEventsEnum.ReaderReconnectFailed, + listenerFunc: ({ reader }: { reader: ReaderInterface }) => void, + ): Promise; + /** * @deprecated * This method has been deprecated and replaced by the `collectPaymentMethod`. diff --git a/packages/terminal/src/events.enum.ts b/packages/terminal/src/events.enum.ts index 54d32a191..51725eb26 100644 --- a/packages/terminal/src/events.enum.ts +++ b/packages/terminal/src/events.enum.ts @@ -20,6 +20,9 @@ export enum TerminalEventsEnum { RequestDisplayMessage = 'terminalRequestDisplayMessage', RequestReaderInput = 'terminalRequestReaderInput', PaymentStatusChange = 'terminalPaymentStatusChange', + ReaderReconnectStarted = 'terminalReaderReconnectStarted', + ReaderReconnectSucceeded = 'terminalReaderReconnectSucceeded', + ReaderReconnectFailed = 'terminalReaderReconnectFailed', } export type TerminalResultInterface = diff --git a/packages/terminal/src/stripe.enum.ts b/packages/terminal/src/stripe.enum.ts index b4e0f246b..5adba9db8 100644 --- a/packages/terminal/src/stripe.enum.ts +++ b/packages/terminal/src/stripe.enum.ts @@ -6,6 +6,49 @@ export enum TerminalConnectTypes { TapToPay = 'tap-to-pay', } +/** + * Note: Don't need to use this enum. It's just for reference. + */ +export enum DeviceType { + cotsDevice = 'cotsDevice', + wisePad3s = 'wisePad3s', + appleBuiltIn = 'appleBuiltIn', + chipper1X = 'chipper1X', + chipper2X = 'chipper2X', + etna = 'etna', + stripeM2 = 'stripeM2', + stripeS700 = 'stripeS700', + stripeS700DevKit = 'stripeS700Devkit', + verifoneP400 = 'verifoneP400', + wiseCube = 'wiseCube', + wisePad3 = 'wisePad3', + wisePosE = 'wisePosE', + wisePosEDevKit = 'wisePosEDevkit', + unknown = 'unknown', +} + +/** + * This group is useful for pick image. + * Reference: https://github.com/stripe/stripe-terminal-ios/blob/fc571ab441b14639243a11d19d8f62bbe93feea5/Example/Example/ReaderHeaderView.swift#L95-L113 + */ +export enum DeviceGroup { + stripeM2 = 'stripe_m2', + chipper1X = 'chipper', + chipper2X = 'chipper', + wiseCube = 'chipper', + verifoneP400 = 'verifone', + wisePad3s = 'wisepad', + wisePad3 = 'wisepad', + wisePosEDevKit = 'wisepose', + etna = 'wisepose', + wisePosE = 'wisepose', + stripeS700DevKit = 's700', + stripeS700 = 's700', + appleBuiltIn = 'apple', // unknown change to apple + cotsDevice = 'unknown', + unknown = 'unknown', +} + export enum UpdateTimeEstimate { LessThanOneMinute = 'LESS_THAN_ONE_MINUTE', OneToTwoMinutes = 'ONE_TO_TWO_MINUTES', @@ -60,6 +103,18 @@ export enum BatteryStatus { Nominal = 'NOMINAL', } +export enum LocationStatus { + NotSet = 'NOT_SET', + Set = 'SET', + Unknown = 'UNKNOWN', +} + +export enum NetworkStatus { + Unknown = 'UNKNOWN', + Online = 'ONLINE', + Offline = 'OFFLINE', +} + export enum ReaderEvent { Unknown = 'UNKNOWN', CardInserted = 'CARD_INSERTED', diff --git a/packages/terminal/src/web.ts b/packages/terminal/src/web.ts index d4ba0fff8..31602c861 100644 --- a/packages/terminal/src/web.ts +++ b/packages/terminal/src/web.ts @@ -6,6 +6,7 @@ import type { ReaderInterface, SimulateReaderUpdate, SimulatedCardType, + Cart, } from './definitions'; import { TerminalEventsEnum } from './events.enum'; @@ -86,6 +87,26 @@ export class StripeTerminalWeb this.notifyListeners(TerminalEventsEnum.ConfirmedPaymentIntent, null); } + async installAvailableUpdate(): Promise { + console.log('installAvailableUpdate'); + } + async cancelInstallUpdate(): Promise { + console.log('cancelInstallUpdate'); + } + async setReaderDisplay(options: Cart): Promise { + console.log('setReaderDisplay', options); + } + async clearReaderDisplay(): Promise { + console.log('clearReaderDisplay'); + } + async rebootReader(): Promise { + console.log('rebootReader'); + } + + async cancelReaderReconnection(): Promise { + console.log('cancelReaderReconnection'); + } + collect = 'deprecated'; cancelCollect = 'deprecated'; }