diff --git a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt index 3371e0bf..da9baa86 100644 --- a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt +++ b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt @@ -199,6 +199,13 @@ internal fun mapFromReaderInputOptions(options: ReaderInputOptions): WritableArr return mappedOptions } +internal fun mapFromReaderEvent(event: ReaderEvent): String { + return when (event) { + ReaderEvent.CARD_INSERTED -> "cardInserted" + ReaderEvent.CARD_REMOVED -> "cardRemoved" + } +} + internal fun mapFromReaderDisplayMessage(message: ReaderDisplayMessage): String { return when (message) { ReaderDisplayMessage.CHECK_MOBILE_DEVICE -> "checkMobileDevice" diff --git a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt index 3db65f80..aa3718ee 100644 --- a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt +++ b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt @@ -20,6 +20,7 @@ import com.stripe.stripeterminal.external.callable.BluetoothReaderListener import com.stripe.stripeterminal.external.callable.Callback import com.stripe.stripeterminal.external.callable.Cancelable import com.stripe.stripeterminal.external.callable.DiscoveryListener +import com.stripe.stripeterminal.external.callable.HandoffReaderListener import com.stripe.stripeterminal.external.callable.LocationListCallback import com.stripe.stripeterminal.external.callable.PaymentIntentCallback import com.stripe.stripeterminal.external.callable.PaymentMethodCallback @@ -41,6 +42,7 @@ import com.stripe.stripeterminal.external.models.PaymentStatus import com.stripe.stripeterminal.external.models.ReadReusableCardParameters import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.ReaderDisplayMessage +import com.stripe.stripeterminal.external.models.ReaderEvent import com.stripe.stripeterminal.external.models.ReaderInputOptions import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate import com.stripe.stripeterminal.external.models.Refund @@ -263,6 +265,110 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : }) } + @ReactMethod + fun connectLocalMobileReader(params: ReadableMap, promise: Promise) { + val readerId = requireParam(params.getString("readerId")) { + "You must provide readerId" + } + + val selectedReader = requireParam(discoveredReadersList.find { + it.serialNumber == readerId + }) { + "Could not find reader with id $readerId" + } + + val locationId = params.getString("locationId") ?: selectedReader.location?.id.orEmpty() + + Terminal.getInstance().connectLocalMobileReader( + selectedReader, + ConnectionConfiguration.LocalMobileConnectionConfiguration(locationId), + object : ReaderCallback { + override fun onSuccess(reader: Reader) { + promise.resolve(nativeMapOf { + putMap("reader", mapFromReader(reader)) + }) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } + } + ) + } + + @ReactMethod + fun connectEmbeddedReader(params: ReadableMap, promise: Promise) = + withExceptionResolver(promise) { + val readerId = requireParam(params.getString("readerId")) { + "You must provide readerId" + } + + val selectedReader = requireParam(discoveredReadersList.find { + it.serialNumber == readerId + }) { + "Could not find reader with id $readerId" + } + + val locationId = params.getString("locationId") ?: selectedReader.location?.id.orEmpty() + + Terminal.getInstance().connectEmbeddedReader( + selectedReader, + ConnectionConfiguration.EmbeddedConnectionConfiguration(locationId), + object : ReaderCallback { + override fun onSuccess(reader: Reader) { + promise.resolve(nativeMapOf { + putMap("reader", mapFromReader(reader)) + }) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(e) + } + } + ) + } + + @ReactMethod + fun connectHandoffReader(params: ReadableMap, promise: Promise) = + withExceptionResolver(promise) { + val readerId = requireParam(params.getString("readerId")) { + "You must provide readerId" + } + + val selectedReader = requireParam(discoveredReadersList.find { + it.serialNumber == readerId + }) { + "Could not find reader with id $readerId" + } + + val locationId = params.getString("locationId") ?: selectedReader.location?.id.orEmpty() + + val listener: HandoffReaderListener = object : HandoffReaderListener { + override fun onReportReaderEvent(event: ReaderEvent) { + sendEvent("didRequestReaderInput", nativeMapOf { + putString("event", mapFromReaderEvent(event)) + }) + } + } + + Terminal.getInstance().connectHandoffReader( + selectedReader, + ConnectionConfiguration.HandoffConnectionConfiguration(locationId), + listener, + object : ReaderCallback { + override fun onSuccess(reader: Reader) { + promise.resolve(nativeMapOf { + putMap("reader", mapFromReader(reader)) + }) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } + } + ) + } + @ReactMethod @Suppress("unused") fun connectBluetoothReader(params: ReadableMap, promise: Promise) = @@ -283,10 +389,6 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : "You must provide a locationId" } - val connectionConfig = ConnectionConfiguration.BluetoothConnectionConfiguration( - locationId - ) - val listener: BluetoothReaderListener = object : BluetoothReaderListener { override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { sendEvent(REPORT_AVAILABLE_UPDATE.listenerName) { @@ -340,7 +442,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : terminal.connectBluetoothReader( selectedReader, - connectionConfig, + ConnectionConfiguration.BluetoothConnectionConfiguration(locationId), listener, object : ReaderCallback { override fun onSuccess(reader: Reader) { @@ -461,9 +563,6 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) { putString("result", mapFromReaderDisplayMessage(message)) } - sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) { - putString("result", mapFromReaderDisplayMessage(message)) - } } } diff --git a/e2e/app.e2e.js b/e2e/app.e2e.js index 95c25e6c..e131b1d0 100644 --- a/e2e/app.e2e.js +++ b/e2e/app.e2e.js @@ -50,6 +50,9 @@ describe('Payments', () => { }); it('Change discovery method to bluetooth proximity', async () => { + if (device.getPlatform() !== 'ios') { + return; + } await changeDiscoveryMethod('Bluetooth Proximity'); }); @@ -57,6 +60,27 @@ describe('Payments', () => { await changeDiscoveryMethod('Internet'); }); + it('Change discovery method to Embedded', async () => { + if (device.getPlatform() !== 'android') { + return; + } + await changeDiscoveryMethod('Embedded'); + }); + + it('Change discovery method to LocalMobile', async () => { + if (device.getPlatform() !== 'android') { + return; + } + await changeDiscoveryMethod('Local mobile'); + }); + + it('Change discovery method to Handoff', async () => { + if (device.getPlatform() !== 'android') { + return; + } + await changeDiscoveryMethod('Handoff'); + }); + // temporary skipped due to bug in stripe-termina-ios that connects the device despite an error. // it('Required update impossible due to low battery', async () => { diff --git a/example/src/screens/DiscoverReadersScreen.tsx b/example/src/screens/DiscoverReadersScreen.tsx index f1ebbd51..7b527802 100644 --- a/example/src/screens/DiscoverReadersScreen.tsx +++ b/example/src/screens/DiscoverReadersScreen.tsx @@ -49,6 +49,9 @@ export default function DiscoverReadersScreen() { connectInternetReader, connectUsbReader, simulateReaderUpdate, + connectEmbeddedReader, + connectLocalMobileReader, + connectHandoffReader, } = useStripeTerminal({ onFinishDiscoveringReaders: (finishError) => { if (finishError) { @@ -154,6 +157,15 @@ export default function DiscoverReadersScreen() { ) { const result = await handleConnectBluetoothReader(reader); error = result.error; + } else if (discoveryMethod === 'localMobile') { + const result = await handleConnectLocalMobileReader(reader); + error = result.error; + } else if (discoveryMethod === 'handoff') { + const result = await handleConnectHandoffReader(reader); + error = result.error; + } else if (discoveryMethod === 'embedded') { + const result = await handleConnectEmbeddedReader(reader); + error = result.error; } else if (discoveryMethod === 'usb') { const result = await handleConnectUsbReader(reader); error = result.error; @@ -166,6 +178,54 @@ export default function DiscoverReadersScreen() { } }; + const handleConnectEmbeddedReader = async (reader: Reader.Type) => { + setConnectingReader(reader); + + const { reader: connectedReader, error } = await connectEmbeddedReader({ + reader, + locationId: selectedLocation?.id, + }); + + if (error) { + console.log('connectEmbeddedReader error:', error); + } else { + console.log('Reader connected successfully', connectedReader); + } + return { error }; + }; + + const handleConnectHandoffReader = async (reader: Reader.Type) => { + setConnectingReader(reader); + + const { reader: connectedReader, error } = await connectHandoffReader({ + reader, + locationId: selectedLocation?.id, + }); + + if (error) { + console.log('connectHandoffReader error:', error); + } else { + console.log('Reader connected successfully', connectedReader); + } + return { error }; + }; + + const handleConnectLocalMobileReader = async (reader: Reader.Type) => { + setConnectingReader(reader); + + const { reader: connectedReader, error } = await connectLocalMobileReader({ + reader, + locationId: selectedLocation?.id, + }); + + if (error) { + console.log('connectLocalMobileReader error:', error); + } else { + console.log('Reader connected successfully', connectedReader); + } + return { error }; + }; + const handleConnectBluetoothReader = async (reader: Reader.Type) => { setConnectingReader(reader); diff --git a/example/src/screens/DiscoveryMethodScreen.tsx b/example/src/screens/DiscoveryMethodScreen.tsx index 3b1f99a2..bfbee63c 100644 --- a/example/src/screens/DiscoveryMethodScreen.tsx +++ b/example/src/screens/DiscoveryMethodScreen.tsx @@ -1,6 +1,6 @@ import { RouteProp, useNavigation, useRoute } from '@react-navigation/core'; import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { Platform, StyleSheet, Text, View } from 'react-native'; import type { Reader } from 'stripe-terminal-react-native'; import { colors } from '../colors'; import ListItem from '../components/ListItem'; @@ -28,14 +28,7 @@ export default function DiscoveryMethodScreen() { Discover a reader by scanning for Bluetooth or Bluetooth LE devices. - onSelect('bluetoothProximity')} - title="Bluetooth Proximity" - /> - - Discover a reader by holding it next to the iOS device (only supported - for the BBPOS Chipper 2X BT). - + { 'Note: the Stripe Terminal SDK can discover supported readers automatically - you should not connect to the reader in the iOS Settings > Bluetooth page.' @@ -51,6 +44,28 @@ export default function DiscoveryMethodScreen() { Discover a reader connected to this device via USB. + + {Platform.OS === 'android' ? ( + <> + onSelect('embedded')} title="Embedded" /> + onSelect('handoff')} title="Handoff" /> + onSelect('localMobile')} + title="Local mobile" + /> + + ) : ( + <> + onSelect('bluetoothProximity')} + title="Bluetooth Proximity" + /> + + Discover a reader by holding it next to the iOS device (only + supported for the BBPOS Chipper 2X BT). + + + )} ); } diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index 394241d7..28591d5c 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -153,6 +153,12 @@ function mapFromDiscoveryMethod(method: Reader.DiscoveryMethod) { return 'Bluetooth Proximity'; case 'internet': return 'Internet'; + case 'embedded': + return 'Embedded'; + case 'handoff': + return 'Handoff'; + case 'localMobile': + return 'Local mobile'; case 'usb': return 'USB'; default: diff --git a/src/StripeTerminalSdk.tsx b/src/StripeTerminalSdk.tsx index 9c9a652e..84d53269 100644 --- a/src/StripeTerminalSdk.tsx +++ b/src/StripeTerminalSdk.tsx @@ -5,11 +5,9 @@ import type { DiscoverReadersParams, DiscoverReadersResultType, CancelDiscoveringResultType, - ConnectBluetoothReaderResultType, ConnectBluetoothReaderParams, DisconnectReaderResultType, Reader, - ConnectInternetResultType, ConnectInternetReaderParams, ConnectUsbReaderResultType, ConnectUsbReaderParams, @@ -28,6 +26,10 @@ import type { ReadReusableCardParamsType, PaymentMethodResultType, SetConnectionTokenParams, + ConnectHandoffParams, + ConnectEmbeddedParams, + ConnectLocalMobileParams, + ConnectReaderResultType, } from './types'; const { StripeTerminalReactNative } = NativeModules; @@ -49,11 +51,20 @@ type StripeTerminalSdkType = { // Connect to reader via bluetooth connectBluetoothReader( params: ConnectBluetoothReaderParams - ): Promise; + ): Promise; // Connect to reader via internet connectInternetReader( params: ConnectInternetReaderParams - ): Promise; + ): Promise; + connectHandoffReader( + params: ConnectHandoffParams + ): Promise; + connectEmbeddedReader( + params: ConnectEmbeddedParams + ): Promise; + connectLocalMobileReader( + params: ConnectLocalMobileParams + ): Promise; // Connect to reader via USB connectUsbReader( params: ConnectUsbReaderParams diff --git a/src/functions.ts b/src/functions.ts index b282ac12..de45910d 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -6,10 +6,8 @@ import type { DiscoverReadersResultType, ConnectBluetoothReaderParams, CancelDiscoveringResultType, - ConnectBluetoothReaderResultType, DisconnectReaderResultType, ConnectInternetReaderParams, - ConnectInternetResultType, ConnectUsbReaderParams, ConnectUsbReaderResultType, CreatePaymentIntentParams, @@ -27,6 +25,10 @@ import type { PaymentMethodResultType, ReadReusableCardParamsType, ProcessRefundResultType, + ConnectLocalMobileParams, + ConnectReaderResultType, + ConnectHandoffParams, + ConnectEmbeddedParams, } from './types'; export async function initialize( @@ -96,7 +98,7 @@ export async function cancelDiscovering(): Promise export async function connectBluetoothReader( params: ConnectBluetoothReaderParams -): Promise { +): Promise { try { const { error, reader } = await StripeTerminalSdk.connectBluetoothReader( params @@ -119,9 +121,84 @@ export async function connectBluetoothReader( } } +export async function connectHandoffReader( + params: ConnectHandoffParams +): Promise { + try { + const { error, reader } = await StripeTerminalSdk.connectHandoffReader( + params + ); + + if (error) { + return { + error, + reader: undefined, + }; + } + return { + reader: reader!, + error: undefined, + }; + } catch (error) { + return { + error: error as any, + }; + } +} + +export async function connectEmbeddedReader( + params: ConnectEmbeddedParams +): Promise { + try { + const { error, reader } = await StripeTerminalSdk.connectEmbeddedReader( + params + ); + + if (error) { + return { + error, + reader: undefined, + }; + } + return { + reader: reader!, + error: undefined, + }; + } catch (error) { + return { + error: error as any, + }; + } +} + +export async function connectLocalMobileReader( + params: ConnectLocalMobileParams +): Promise { + try { + const { error, reader } = await StripeTerminalSdk.connectLocalMobileReader( + params + ); + + if (error) { + return { + error, + reader: undefined, + }; + } + return { + reader: reader!, + error: undefined, + }; + } catch (error) { + return { + error: error as any, + }; + } +} + export async function connectInternetReader( params: ConnectInternetReaderParams -): Promise { +): Promise { try { const { error, reader } = await StripeTerminalSdk.connectInternetReader( params diff --git a/src/hooks/useStripeTerminal.tsx b/src/hooks/useStripeTerminal.tsx index 597d439c..09fff13c 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -13,6 +13,8 @@ import type { RefundParams, ReadReusableCardParamsType, InitParams, + ConnectEmbeddedParams, + ConnectLocalMobileParams, UserCallbacks, } from '../types'; import { @@ -45,6 +47,9 @@ import { cancelCollectPaymentMethod, cancelCollectSetupIntent, cancelReadReusableCard, + connectEmbeddedReader, + connectHandoffReader, + connectLocalMobileReader, } from '../functions'; import { StripeTerminalContext } from '../components/StripeTerminalContext'; import { useListener } from './useListener'; @@ -211,6 +216,54 @@ export function useStripeTerminal(props?: Props) { [setConnectedReader, setLoading] ); + const _connectEmbeddedReader = useCallback( + async (params: ConnectEmbeddedParams) => { + setLoading(true); + + const response = await connectEmbeddedReader(params); + + if (response.reader) { + setConnectedReader(response.reader); + } + setLoading(false); + + return response; + }, + [setConnectedReader, setLoading] + ); + + const _connectLocalMobileReader = useCallback( + async (params: ConnectLocalMobileParams) => { + setLoading(true); + + const response = await connectLocalMobileReader(params); + + if (response.reader) { + setConnectedReader(response.reader); + } + setLoading(false); + + return response; + }, + [setConnectedReader, setLoading] + ); + + const _connectHandoffReader = useCallback( + async (params: ConnectEmbeddedParams) => { + setLoading(true); + + const response = await connectHandoffReader(params); + + if (response.reader) { + setConnectedReader(response.reader); + } + setLoading(false); + + return response; + }, + [setConnectedReader, setLoading] + ); + const _disconnectReader = useCallback(async () => { setLoading(true); @@ -528,6 +581,9 @@ export function useStripeTerminal(props?: Props) { cancelCollectPaymentMethod: _cancelCollectPaymentMethod, cancelCollectSetupIntent: _cancelCollectSetupIntent, cancelReadReusableCard: _cancelReadReusableCard, + connectEmbeddedReader: _connectEmbeddedReader, + connectHandoffReader: _connectHandoffReader, + connectLocalMobileReader: _connectLocalMobileReader, emitter: emitter, discoveredReaders, connectedReader, diff --git a/src/types/Reader.ts b/src/types/Reader.ts index d5887389..6f69f6bd 100644 --- a/src/types/Reader.ts +++ b/src/types/Reader.ts @@ -1,6 +1,8 @@ import type { Location, LocationStatus } from './'; export namespace Reader { + export type DiscoveryMethod = IOS.DiscoveryMethod | Android.DiscoveryMethod; + export type Type = IOS.Type & Android.Type & { id: string; @@ -23,6 +25,11 @@ export namespace Reader { batteryStatus: BatteryStatus; isCharging?: number; }; + + export type DiscoveryMethod = + | 'bluetoothProximity' + | 'bluetoothScan' + | 'internet'; } export namespace Android { @@ -39,6 +46,14 @@ export namespace Reader { settingsVersion?: string; pinKeysetId?: string; }; + + export type DiscoveryMethod = + | 'bluetoothScan' + | 'internet' + | 'embedded' + | 'localMobile' + | 'handoff' + | 'usb'; } export type BatteryStatus = 'critical' | 'low' | 'nominal' | 'unknown'; @@ -57,15 +72,6 @@ export namespace Reader { | 'estimate5To15Minutes' | 'estimateLessThan1Minute'; - export type DiscoveryMethod = - | 'bluetoothProximity' - | 'bluetoothScan' - | 'internet' - | 'embedded' - | 'localMobile' - | 'handoff' - | 'usb'; - export type SimulateUpdateType = | 'random' | 'available' diff --git a/src/types/index.ts b/src/types/index.ts index af9e0f45..b0259087 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -42,6 +42,21 @@ export type ConnectUsbReaderParams = { locationId?: string; }; +export type ConnectLocalMobileParams = { + reader: Reader.Type; + locationId?: string; +}; + +export type ConnectHandoffParams = { + reader: Reader.Type; + locationId?: string; +}; + +export type ConnectEmbeddedParams = { + reader: Reader.Type; + locationId?: string; +}; + export type LineItem = { displayName: string; quantity: number; @@ -88,14 +103,7 @@ export type CancelDiscoveringResultType = Promise<{ error?: StripeError; }>; -export type ConnectBluetoothReaderResultType = - | { - reader: Reader.Type; - error?: undefined; - } - | { reader?: undefined; error: StripeError }; - -export type ConnectInternetResultType = +export type ConnectReaderResultType = | { reader: Reader.Type; error?: undefined;