diff --git a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt index 85140477..b176aff3 100644 --- a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt +++ b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt @@ -35,6 +35,12 @@ fun putIntOrNull(mapTarget: WritableMap, key: String, value: Int?) { } } +internal fun nativeMapOf(block: WritableNativeMap.() -> Unit): ReadableMap { + return WritableNativeMap().apply { + block() + } +} + internal fun mapFromReaders(readers: List): WritableArray = readers.collectToWritableArray { mapFromReader(it) } @@ -101,6 +107,7 @@ internal fun mapToDiscoveryMethod(method: String?): DiscoveryMethod? { "embedded" -> DiscoveryMethod.EMBEDDED "localMobile" -> DiscoveryMethod.LOCAL_MOBILE "handoff" -> DiscoveryMethod.HANDOFF + "usb" -> DiscoveryMethod.USB else -> null } } diff --git a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt index fd0721fb..548b4b38 100644 --- a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt +++ b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt @@ -15,6 +15,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEm import com.stripe.stripeterminal.Terminal import com.stripe.stripeterminal.TerminalApplicationDelegate.onCreate import com.stripe.stripeterminal.TerminalApplicationDelegate.onTrimMemory +import com.stripe.stripeterminal.external.UsbConnectivity import com.stripe.stripeterminal.external.callable.BluetoothReaderListener import com.stripe.stripeterminal.external.callable.Callback import com.stripe.stripeterminal.external.callable.Cancelable @@ -26,6 +27,7 @@ import com.stripe.stripeterminal.external.callable.ReaderCallback import com.stripe.stripeterminal.external.callable.RefundCallback import com.stripe.stripeterminal.external.callable.SetupIntentCallback import com.stripe.stripeterminal.external.callable.TerminalListener +import com.stripe.stripeterminal.external.callable.UsbReaderListener import com.stripe.stripeterminal.external.models.Cart import com.stripe.stripeterminal.external.models.ConnectionConfiguration import com.stripe.stripeterminal.external.models.ConnectionStatus @@ -488,6 +490,121 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : ) } + @OptIn(UsbConnectivity::class) + @ReactMethod + @Suppress("unused") + fun connectUsbReader(params: ReadableMap, promise: Promise) { + val reader = getMapOr(params, "reader") ?: run { + promise.resolve( + createError( + TerminalException( + TerminalException.TerminalErrorCode.INVALID_REQUIRED_PARAMETER, + "You must provide a reader object" + ) + ) + ) + return + } + val readerId = getStringOr(reader, "serialNumber") + + val selectedReader = discoveredReadersList.find { + it.serialNumber == readerId + } ?: run { + promise.resolve( + createError( + TerminalException( + TerminalException.TerminalErrorCode.INVALID_REQUIRED_PARAMETER, + "Could not find reader with id $readerId" + ) + ) + ) + return + } + + val locationId = getStringOr(params, "locationId") ?: selectedReader.location?.id ?: run { + promise.resolve( + createError( + TerminalException( + TerminalException.TerminalErrorCode.INVALID_REQUIRED_PARAMETER, + "You must provide a locationId" + ) + ) + ) + return + } + + val listener: UsbReaderListener = object : UsbReaderListener { + override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { + sendEvent(REPORT_AVAILABLE_UPDATE.listenerName) { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } + } + + override fun onStartInstallingUpdate( + update: ReaderSoftwareUpdate, + cancelable: Cancelable? + ) { + installUpdateCancelable = cancelable + sendEvent(START_INSTALLING_UPDATE.listenerName) { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } + } + + override fun onReportReaderSoftwareUpdateProgress(progress: Float) { + sendEvent(REPORT_UPDATE_PROGRESS.listenerName) { + putMap("result", nativeMapOf { + putString("progress", progress.toString()) + }) + } + } + + override fun onFinishInstallingUpdate( + update: ReaderSoftwareUpdate?, + e: TerminalException? + ) { + sendEvent(FINISH_INSTALLING_UPDATE.listenerName) { + update?.let { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } ?: run { + putMap("result", WritableNativeMap()) + } + } + } + + override fun onRequestReaderInput(options: ReaderInputOptions) { + sendEvent(REQUEST_READER_INPUT.listenerName) { + putArray("result", mapFromReaderInputOptions(options)) + } + } + + override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) { + sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) { + putString("result", mapFromReaderDisplayMessage(message)) + } + sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) { + putString("result", mapFromReaderDisplayMessage(message)) + } + } + } + + Terminal.getInstance().connectUsbReader( + selectedReader, + ConnectionConfiguration.UsbConnectionConfiguration(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 disconnectReader(promise: Promise) { @@ -962,6 +1079,14 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : .emit(eventName, result) } + private fun sendEvent(eventName: String, resultBuilder: WritableNativeMap.() -> Unit) { + reactApplicationContext + .getJSModule(RCTDeviceEventEmitter::class.java) + .emit(eventName, WritableNativeMap().apply { + resultBuilder() + }) + } + private fun onPaymentIntentCallback(paymentIntent: PaymentIntent, promise: Promise) { val pi = mapFromPaymentIntent(paymentIntent) val result = WritableNativeMap().apply { diff --git a/example/src/screens/DiscoverReadersScreen.tsx b/example/src/screens/DiscoverReadersScreen.tsx index c55e3d1c..f1ebbd51 100644 --- a/example/src/screens/DiscoverReadersScreen.tsx +++ b/example/src/screens/DiscoverReadersScreen.tsx @@ -47,6 +47,7 @@ export default function DiscoverReadersScreen() { connectBluetoothReader, discoveredReaders, connectInternetReader, + connectUsbReader, simulateReaderUpdate, } = useStripeTerminal({ onFinishDiscoveringReaders: (finishError) => { @@ -153,6 +154,9 @@ export default function DiscoverReadersScreen() { ) { const result = await handleConnectBluetoothReader(reader); error = result.error; + } else if (discoveryMethod === 'usb') { + const result = await handleConnectUsbReader(reader); + error = result.error; } if (error) { setConnectingReader(undefined); @@ -194,6 +198,22 @@ export default function DiscoverReadersScreen() { return { error }; }; + const handleConnectUsbReader = async (reader: Reader.Type) => { + setConnectingReader(reader); + + const { reader: connectedReader, error } = await connectUsbReader({ + reader, + locationId: selectedLocation?.id || reader?.location?.id, + }); + + if (error) { + console.log('connectUsbReader error:', error); + } else { + console.log('Reader connected successfully', connectedReader); + } + return { error }; + }; + const handleChangeUpdatePlan = async (plan: Reader.SimulateUpdateType) => { await simulateReaderUpdate(plan); setSelectedUpdatePlan(plan); diff --git a/example/src/screens/DiscoveryMethodScreen.tsx b/example/src/screens/DiscoveryMethodScreen.tsx index d745dc94..3b1f99a2 100644 --- a/example/src/screens/DiscoveryMethodScreen.tsx +++ b/example/src/screens/DiscoveryMethodScreen.tsx @@ -23,10 +23,10 @@ export default function DiscoveryMethodScreen() { onSelect('bluetoothScan')} - title="Bluetooth scan" + title="Bluetooth Scan" /> - Discover a reader by scanning for Bluetooth LE devices. + Discover a reader by scanning for Bluetooth or Bluetooth LE devices. onSelect('bluetoothProximity')} @@ -47,6 +47,10 @@ export default function DiscoveryMethodScreen() { Discovers readers that have been registered to your account via the Stripe API or Dashboard. + onSelect('usb')} /> + + Discover a reader connected to this device via USB. + ); } diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index fe81ea9b..394241d7 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -153,6 +153,8 @@ function mapFromDiscoveryMethod(method: Reader.DiscoveryMethod) { return 'Bluetooth Proximity'; case 'internet': return 'Internet'; + case 'usb': + return 'USB'; default: return ''; } diff --git a/src/StripeTerminalSdk.tsx b/src/StripeTerminalSdk.tsx index 96adf09a..9c9a652e 100644 --- a/src/StripeTerminalSdk.tsx +++ b/src/StripeTerminalSdk.tsx @@ -11,6 +11,8 @@ import type { Reader, ConnectInternetResultType, ConnectInternetReaderParams, + ConnectUsbReaderResultType, + ConnectUsbReaderParams, CreatePaymentIntentParams, CollectSetupIntentPaymentMethodParams, PaymentIntentResultType, @@ -52,6 +54,10 @@ type StripeTerminalSdkType = { connectInternetReader( params: ConnectInternetReaderParams ): Promise; + // Connect to reader via USB + connectUsbReader( + params: ConnectUsbReaderParams + ): Promise; // Disconnect reader disconnectReader(): Promise; // Create a payment intent diff --git a/src/functions.ts b/src/functions.ts index 6ca2314b..b282ac12 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -10,6 +10,8 @@ import type { DisconnectReaderResultType, ConnectInternetReaderParams, ConnectInternetResultType, + ConnectUsbReaderParams, + ConnectUsbReaderResultType, CreatePaymentIntentParams, CollectSetupIntentPaymentMethodParams, PaymentIntentResultType, @@ -142,6 +144,29 @@ export async function connectInternetReader( } } +export async function connectUsbReader( + params: ConnectUsbReaderParams +): Promise { + try { + const { error, reader } = await StripeTerminalSdk.connectUsbReader(params); + + if (error) { + return { + error, + reader: undefined, + }; + } + return { + reader: reader!, + error: undefined, + }; + } catch (error) { + return { + error: error as any, + }; + } +} + export async function disconnectReader(): Promise { try { const { error } = await StripeTerminalSdk.disconnectReader(); diff --git a/src/hooks/useStripeTerminal.tsx b/src/hooks/useStripeTerminal.tsx index 023d01fd..9aedf9c4 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -6,6 +6,7 @@ import type { ConnectInternetReaderParams, CreatePaymentIntentParams, ConnectBluetoothReaderParams, + ConnectUsbReaderParams, GetLocationsParams, Cart, CreateSetupIntentParams, @@ -21,6 +22,7 @@ import { connectBluetoothReader, disconnectReader, connectInternetReader, + connectUsbReader, createPaymentIntent, collectPaymentMethod, retrievePaymentIntent, @@ -343,6 +345,22 @@ export function useStripeTerminal(props?: Props) { [setConnectedReader, setLoading] ); + const _connectUsbReader = useCallback( + async (params: ConnectUsbReaderParams) => { + setLoading(true); + + const response = await connectUsbReader(params); + + if (response.reader && !response.error) { + setConnectedReader(response.reader); + } + setLoading(false); + + return response; + }, + [setConnectedReader, setLoading] + ); + const _disconnectReader = useCallback(async () => { setLoading(true); @@ -636,6 +654,7 @@ export function useStripeTerminal(props?: Props) { connectBluetoothReader: _connectBluetoothReader, disconnectReader: _disconnectReader, connectInternetReader: _connectInternetReader, + connectUsbReader: _connectUsbReader, createPaymentIntent: _createPaymentIntent, collectPaymentMethod: _collectPaymentMethod, retrievePaymentIntent: _retrievePaymentIntent, diff --git a/src/types/Reader.ts b/src/types/Reader.ts index cf3d9c5d..d5887389 100644 --- a/src/types/Reader.ts +++ b/src/types/Reader.ts @@ -63,7 +63,8 @@ export namespace Reader { | 'internet' | 'embedded' | 'localMobile' - | 'handoff'; + | 'handoff' + | 'usb'; export type SimulateUpdateType = | 'random' diff --git a/src/types/index.ts b/src/types/index.ts index 7971acdd..6673c63a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -37,6 +37,11 @@ export type ConnectBluetoothReaderParams = { locationId?: string; }; +export type ConnectUsbReaderParams = { + reader: Reader.Type; + locationId?: string; +}; + export type LineItem = { displayName: string; quantity: number; @@ -97,6 +102,13 @@ export type ConnectInternetResultType = } | { reader?: undefined; error: StripeError }; +export type ConnectUsbReaderResultType = + | { + reader: Reader.Type; + error?: undefined; + } + | { reader?: undefined; error: StripeError }; + export type DisconnectReaderResultType = { error: StripeError; };