From 2d03a3b720ebb9312670971122aa1ae2146d8acd Mon Sep 17 00:00:00 2001 From: dhenry-stripe <83039776+dhenry-stripe@users.noreply.github.com> Date: Thu, 24 Mar 2022 13:00:03 -0400 Subject: [PATCH] StripeTerminalReactNativeModule reduced scope (#202) * separated callbacks and listeners from RN module * cancelOperation added * ReactNativeConstants imports * extracting reader.serialNumber --- android/build.gradle | 1 + .../com/stripeterminalreactnative/Errors.kt | 28 +- .../ReactExtensions.kt | 18 + .../StripeTerminalReactNativeModule.kt | 786 ++++-------------- .../callback/NoOpCallback.kt | 17 + .../callback/RNLocationListCallback.kt | 27 + .../callback/RNPaymentIntentCallback.kt | 26 + .../callback/RNPaymentMethodCallback.kt | 23 + .../callback/RNRefundCallback.kt | 23 + .../callback/RNSetupIntentCallback.kt | 26 + .../ktx/TerminalExtensions.kt | 140 ++++ .../listener/RNBluetoothReaderListener.kt | 75 ++ .../listener/RNDiscoveryListener.kt | 37 + .../listener/RNHandoffReaderListener.kt | 17 + .../listener/RNTerminalListener.kt | 38 + .../listener/RNUsbReaderListener.kt | 75 ++ 16 files changed, 705 insertions(+), 652 deletions(-) create mode 100644 android/src/main/java/com/stripeterminalreactnative/ReactExtensions.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/callback/NoOpCallback.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/callback/RNLocationListCallback.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentIntentCallback.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentMethodCallback.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/callback/RNRefundCallback.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/callback/RNSetupIntentCallback.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/listener/RNDiscoveryListener.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/listener/RNHandoffReaderListener.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt create mode 100644 android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt diff --git a/android/build.gradle b/android/build.gradle index e89e30f8..0074591c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -127,4 +127,5 @@ dependencies { implementation 'com.stripe:stripeterminal:2.7.1' implementation 'com.google.code.gson:gson:2.3.1' implementation 'com.squareup.okhttp3:okhttp:4.9.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' } diff --git a/android/src/main/java/com/stripeterminalreactnative/Errors.kt b/android/src/main/java/com/stripeterminalreactnative/Errors.kt index 0ca4d934..22dca831 100644 --- a/android/src/main/java/com/stripeterminalreactnative/Errors.kt +++ b/android/src/main/java/com/stripeterminalreactnative/Errors.kt @@ -2,20 +2,38 @@ package com.stripeterminalreactnative import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableNativeMap import com.stripe.stripeterminal.external.models.TerminalException +import com.stripe.stripeterminal.external.models.TerminalException.TerminalErrorCode +import kotlinx.coroutines.CancellationException import kotlin.jvm.Throws -internal fun createError(exception: TerminalException): ReadableMap = nativeMapOf { +internal fun createError(throwable: Throwable): ReadableMap = nativeMapOf { putMap("error", nativeMapOf { - putString("message", exception.errorMessage) - putString("code", exception.errorCode.toString()) + writeError(throwable) }) } +private fun WritableNativeMap.writeError(throwable: Throwable?) { + when (throwable) { + is TerminalException -> { + putString("message", throwable.errorMessage) + putString("code", throwable.errorCode.toString()) + } + is CancellationException -> { + writeError(throwable.cause) + } + else -> { + putString("message", throwable?.message ?: "Unknown error") + putString("code", TerminalErrorCode.UNEXPECTED_SDK_ERROR.toString()) + } + } +} + @Throws(TerminalException::class) internal fun requireCancelable(cancelable: T?, lazyMessage: () -> String): T { return cancelable ?: throw TerminalException( - TerminalException.TerminalErrorCode.CANCEL_FAILED, + TerminalErrorCode.CANCEL_FAILED, lazyMessage() ) } @@ -23,7 +41,7 @@ internal fun requireCancelable(cancelable: T?, lazyMessage: () -> String): T @Throws(TerminalException::class) internal fun requireParam(input: T?, lazyMessage: () -> String): T { return input ?: throw TerminalException( - TerminalException.TerminalErrorCode.INVALID_REQUIRED_PARAMETER, + TerminalErrorCode.INVALID_REQUIRED_PARAMETER, lazyMessage() ) } diff --git a/android/src/main/java/com/stripeterminalreactnative/ReactExtensions.kt b/android/src/main/java/com/stripeterminalreactnative/ReactExtensions.kt new file mode 100644 index 00000000..e289b11b --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/ReactExtensions.kt @@ -0,0 +1,18 @@ +package com.stripeterminalreactnative + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.modules.core.DeviceEventManagerModule + +internal object ReactExtensions { + + fun ReactApplicationContext.sendEvent( + eventName: String, + resultBuilder: WritableNativeMap.() -> Unit + ) { + getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(eventName, nativeMapOf { + resultBuilder() + }) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt index aa3718ee..d908a1b9 100644 --- a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt +++ b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt @@ -15,55 +15,38 @@ 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 -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 -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.callable.ReaderListenable import com.stripe.stripeterminal.external.models.Cart -import com.stripe.stripeterminal.external.models.ConnectionConfiguration -import com.stripe.stripeterminal.external.models.ConnectionStatus import com.stripe.stripeterminal.external.models.DiscoveryConfiguration +import com.stripe.stripeterminal.external.models.DiscoveryMethod import com.stripe.stripeterminal.external.models.ListLocationsParameters -import com.stripe.stripeterminal.external.models.Location import com.stripe.stripeterminal.external.models.PaymentIntent import com.stripe.stripeterminal.external.models.PaymentIntentParameters -import com.stripe.stripeterminal.external.models.PaymentMethod -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 import com.stripe.stripeterminal.external.models.RefundParameters import com.stripe.stripeterminal.external.models.SetupIntent import com.stripe.stripeterminal.external.models.SetupIntentCancellationParameters import com.stripe.stripeterminal.external.models.SetupIntentParameters import com.stripe.stripeterminal.external.models.SimulatorConfiguration -import com.stripe.stripeterminal.external.models.TerminalException -import com.stripeterminalreactnative.ReactNativeConstants.CHANGE_CONNECTION_STATUS -import com.stripeterminalreactnative.ReactNativeConstants.CHANGE_PAYMENT_STATUS import com.stripeterminalreactnative.ReactNativeConstants.FETCH_TOKEN_PROVIDER -import com.stripeterminalreactnative.ReactNativeConstants.FINISH_DISCOVERING_READERS -import com.stripeterminalreactnative.ReactNativeConstants.FINISH_INSTALLING_UPDATE -import com.stripeterminalreactnative.ReactNativeConstants.REPORT_AVAILABLE_UPDATE -import com.stripeterminalreactnative.ReactNativeConstants.REPORT_UNEXPECTED_READER_DISCONNECT -import com.stripeterminalreactnative.ReactNativeConstants.REPORT_UPDATE_PROGRESS -import com.stripeterminalreactnative.ReactNativeConstants.REQUEST_READER_DISPLAY_MESSAGE -import com.stripeterminalreactnative.ReactNativeConstants.REQUEST_READER_INPUT -import com.stripeterminalreactnative.ReactNativeConstants.START_INSTALLING_UPDATE -import com.stripeterminalreactnative.ReactNativeConstants.UPDATE_DISCOVERED_READERS +import com.stripeterminalreactnative.callback.NoOpCallback +import com.stripeterminalreactnative.callback.RNLocationListCallback +import com.stripeterminalreactnative.callback.RNPaymentIntentCallback +import com.stripeterminalreactnative.callback.RNPaymentMethodCallback +import com.stripeterminalreactnative.callback.RNRefundCallback +import com.stripeterminalreactnative.callback.RNSetupIntentCallback +import com.stripeterminalreactnative.ktx.connectReader +import com.stripeterminalreactnative.listener.RNBluetoothReaderListener +import com.stripeterminalreactnative.listener.RNDiscoveryListener +import com.stripeterminalreactnative.listener.RNHandoffReaderListener +import com.stripeterminalreactnative.listener.RNTerminalListener +import com.stripeterminalreactnative.listener.RNUsbReaderListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @@ -81,19 +64,22 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : private val terminal: Terminal get() = Terminal.getInstance() + private val context: ReactApplicationContext + get() = reactApplicationContext + init { TokenProvider.tokenProviderCallback = object : TokenProviderCallback { override fun invoke() { - reactApplicationContext + context .getJSModule(RCTDeviceEventEmitter::class.java) .emit(FETCH_TOKEN_PROVIDER.listenerName, null) } } - reactApplicationContext.registerComponentCallbacks( + context.registerComponentCallbacks( object : ComponentCallbacks2 { override fun onTrimMemory(level: Int) { - onTrimMemory(reactApplicationContext.applicationContext as Application, level) + onTrimMemory(context.applicationContext as Application, level) } override fun onLowMemory() {} @@ -111,85 +97,36 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod @Suppress("unused") fun initialize(params: ReadableMap, promise: Promise) { - UiThreadUtil.runOnUiThread { - onCreate(reactApplicationContext.applicationContext as Application) - } - - val listener: TerminalListener = object : TerminalListener { - override fun onUnexpectedReaderDisconnect(reader: Reader) { - val error = createError( - TerminalException( - TerminalException.TerminalErrorCode.UNEXPECTED_SDK_ERROR, - "Reader has been disconnected unexpectedly" - ) - ) - sendEvent(REPORT_UNEXPECTED_READER_DISCONNECT.listenerName, error) - } - - override fun onConnectionStatusChange(status: ConnectionStatus) { - sendEvent(CHANGE_CONNECTION_STATUS.listenerName) { - putString("result", mapFromConnectionStatus(status)) - } - } - - override fun onPaymentStatusChange(status: PaymentStatus) { - sendEvent(CHANGE_PAYMENT_STATUS.listenerName) { - putString("result", mapFromPaymentStatus(status)) - } - } - } - val result = WritableNativeMap() - - if (!Terminal.isInitialized()) { - val logLevel = mapToLogLevel(params.getString("logLevel")) + UiThreadUtil.runOnUiThread { onCreate(context.applicationContext as Application) } + val result = if (!Terminal.isInitialized()) { Terminal.initTerminal( - this.reactApplicationContext.applicationContext, - logLevel, + this.context.applicationContext, + mapToLogLevel(params.getString("logLevel")), TokenProvider.Companion, - listener + RNTerminalListener(context) ) + WritableNativeMap() } else { - terminal.connectedReader?.let { - result.putMap("reader", mapFromReader(it)) + nativeMapOf { + terminal.connectedReader?.let { + putMap("reader", mapFromReader(it)) + } } } - promise.resolve(result) } @ReactMethod @Suppress("unused") - fun cancelCollectPaymentMethod(promise: Promise) = withExceptionResolver(promise) { - val cancelable = requireCancelable(collectPaymentMethodCancelable) { - "collectPaymentMethod could not be canceled because the command has already been canceled or has completed." - } - cancelable.cancel(object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + fun cancelCollectPaymentMethod(promise: Promise) { + cancelOperation(promise, collectPaymentMethodCancelable, "collectPaymentMethod") } @ReactMethod @Suppress("unused") - fun cancelCollectSetupIntent(promise: Promise) = withExceptionResolver(promise) { - val cancelable = requireCancelable(collectSetupIntentCancelable) { - "collectSetupIntent could not be canceled because the command has already been canceled or has completed." - } - cancelable.cancel(object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + fun cancelCollectSetupIntent(promise: Promise) { + cancelOperation(promise, collectSetupIntentCancelable, "collectSetupIntent") } @ReactMethod @@ -203,9 +140,10 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod @Suppress("unused") fun setConnectionToken(params: ReadableMap, promise: Promise) { - val token = params.getString("token") - val error = params.getString("error") - TokenProvider.setConnectionToken(token, error) + TokenProvider.setConnectionToken( + token = params.getString("token"), + error = params.getString("error"), + ) promise.resolve(null) } @@ -219,383 +157,97 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : "Unknown discoveryMethod: $discoveryMethodParam" } - val simulated = getBoolean(params, "simulated") - val config = DiscoveryConfiguration(0, discoveryMethod, simulated) + val listener = RNDiscoveryListener(context) { discoveredReadersList = it } discoverCancelable = terminal.discoverReaders( - config, - object : DiscoveryListener { - override fun onUpdateDiscoveredReaders(readers: List) { - discoveredReadersList = readers - sendEvent(UPDATE_DISCOVERED_READERS.listenerName) { - putArray("readers", mapFromReaders(readers)) - } - } - }, - object : Callback { - override fun onSuccess() { - sendEvent(FINISH_DISCOVERING_READERS.listenerName) { - putMap("result", WritableNativeMap()) - } - } - - override fun onFailure(e: TerminalException) { - sendEvent(FINISH_DISCOVERING_READERS.listenerName) { - putMap("result", createError(e)) - } - } - } + DiscoveryConfiguration(0, discoveryMethod, getBoolean(params, "simulated")), + listener, + listener ) } @ReactMethod @Suppress("unused") - fun cancelDiscovering(promise: Promise) = withExceptionResolver(promise) { - val cancelable = requireCancelable(discoverCancelable) { - "discoverReaders could not be canceled because the command has already been canceled or has completed." - } - cancelable.cancel(object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + fun cancelDiscovering(promise: Promise) { + cancelOperation(promise, discoverCancelable, "discoverReaders") } - @ReactMethod - fun connectLocalMobileReader(params: ReadableMap, promise: Promise) { - val readerId = requireParam(params.getString("readerId")) { - "You must provide readerId" + private fun connectReader( + params: ReadableMap, + promise: Promise, + discoveryMethod: DiscoveryMethod, + listener: ReaderListenable? = null + ) = withExceptionResolver(promise) { + val reader = requireParam(params.getMap("reader")) { + "You must provide a reader" } + val serialNumber = reader.getString("serialNumber") + val selectedReader = requireParam(discoveredReadersList.find { - it.serialNumber == readerId + it.serialNumber == serialNumber }) { - "Could not find reader with id $readerId" + "Could not find a reader with serialNumber $serialNumber" } 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)) - } - } - ) + CoroutineScope(Dispatchers.IO).launch { + val connectedReader = + terminal.connectReader(discoveryMethod, selectedReader, locationId, listener) + promise.resolve(nativeMapOf { + putMap("reader", mapFromReader(connectedReader)) + }) + } } @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) - } - } - ) + @Suppress("unused") + fun connectBluetoothReader(params: ReadableMap, promise: Promise) { + val listener = RNBluetoothReaderListener(context) { + installUpdateCancelable = it } + connectReader(params, promise, DiscoveryMethod.BLUETOOTH_SCAN, listener) + } @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)) - } - } - ) - } + @Suppress("unused") + fun connectEmbeddedReader(params: ReadableMap, promise: Promise) { + connectReader(params, promise, DiscoveryMethod.EMBEDDED) + } @ReactMethod @Suppress("unused") - fun connectBluetoothReader(params: ReadableMap, promise: Promise) = - withExceptionResolver(promise) { - val reader = requireParam(params.getMap("reader")) { - "You must provide a reader object" - } - val readerId = reader.getString("serialNumber") as String - - val selectedReader = requireParam(discoveredReadersList.find { - it.serialNumber == readerId - }) { - "Could not find reader with id $readerId" - } - - val locationId = - requireParam(params.getString("locationId") ?: selectedReader.location?.id) { - "You must provide a locationId" - } - - val listener: BluetoothReaderListener = object : BluetoothReaderListener { - 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)) - } - } - } - - terminal.connectBluetoothReader( - selectedReader, - ConnectionConfiguration.BluetoothConnectionConfiguration(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)) - } - } - ) - } + fun connectHandoffReader(params: ReadableMap, promise: Promise) { + val listener = RNHandoffReaderListener(context) + connectReader(params, promise, DiscoveryMethod.HANDOFF, listener) + } @ReactMethod @Suppress("unused") - fun connectInternetReader(params: ReadableMap, promise: Promise) = - withExceptionResolver(promise) { - val reader = requireParam(params.getMap("reader")) { - "You must provide a reader object" - } - val readerId = reader.getString("serialNumber") as String - - val selectedReader = requireParam(discoveredReadersList.find { - it.serialNumber == readerId - }) { - "Could not find reader with id $readerId" - } - - val connectionConfig = ConnectionConfiguration.InternetConnectionConfiguration( - failIfInUse = getBoolean(params, "failIfInUse") - ) - - terminal.connectInternetReader( - selectedReader, - connectionConfig, - object : ReaderCallback { - override fun onSuccess(reader: Reader) { - promise.resolve(nativeMapOf { - putMap("reader", mapFromReader(reader)) - }) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - } - ) - } + fun connectInternetReader(params: ReadableMap, promise: Promise) { + connectReader(params, promise, DiscoveryMethod.INTERNET) + } - @OptIn(UsbConnectivity::class) @ReactMethod @Suppress("unused") - fun connectUsbReader(params: ReadableMap, promise: Promise) = withExceptionResolver(promise) { - val reader = requireParam(params.getMap("reader")) { - "You must provide a reader object" - } - val readerId = reader.getString("serialNumber") - - val selectedReader = requireParam( - discoveredReadersList.find { it.serialNumber == readerId } - ) { - "Could not find reader with id $readerId" - } - - val locationId = requireParam( - params.getString("locationId") ?: selectedReader.location?.id - ) { - "You must provide a locationId" - } - - 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)) - } - } + fun connectLocalMobileReader(params: ReadableMap, promise: Promise) { + connectReader(params, promise, DiscoveryMethod.LOCAL_MOBILE) + } - override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) { - sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) { - putString("result", mapFromReaderDisplayMessage(message)) - } - } + @ReactMethod + @Suppress("unused") + fun connectUsbReader(params: ReadableMap, promise: Promise) { + val listener = RNUsbReaderListener(context) { + installUpdateCancelable = it } - - terminal.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)) - } - } - ) + connectReader(params, promise, DiscoveryMethod.USB, listener) } @ReactMethod @Suppress("unused") fun disconnectReader(promise: Promise) { - terminal.disconnectReader(object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + terminal.disconnectReader(NoOpCallback(promise)) } @ReactMethod @@ -613,15 +265,8 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : intentParams.setSetupFutureUsage(it) } - terminal.createPaymentIntent(intentParams.build(), object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - paymentIntents[paymentIntent.id] = paymentIntent - onPaymentIntentCallback(paymentIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } + terminal.createPaymentIntent(intentParams.build(), RNPaymentIntentCallback(promise) { + paymentIntents[it.id] = it }) } @@ -632,35 +277,20 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val paymentIntent = requireParam(paymentIntents[paymentIntentId]) { "There is no associated paymentIntent with id $paymentIntentId" } - collectPaymentMethodCancelable = terminal - .collectPaymentMethod(paymentIntent, object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - paymentIntents[paymentIntent.id] = paymentIntent - onPaymentIntentCallback(paymentIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + collectPaymentMethodCancelable = terminal.collectPaymentMethod( + paymentIntent, + RNPaymentIntentCallback(promise) { paymentIntents[it.id] = it } + ) } @ReactMethod @Suppress("unused") fun retrievePaymentIntent(clientSecret: String, promise: Promise) { - terminal.retrievePaymentIntent(clientSecret, object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - paymentIntents[paymentIntent.id] = paymentIntent - onPaymentIntentCallback(paymentIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } + terminal.retrievePaymentIntent(clientSecret, RNPaymentIntentCallback(promise) { + paymentIntents[it.id] = it }) } - @ReactMethod @Suppress("unused") fun processPayment(paymentIntentId: String, promise: Promise) = withExceptionResolver(promise) { @@ -668,38 +298,20 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : "There is no associated paymentIntent with id $paymentIntentId" } - terminal.processPayment(paymentIntent, object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - paymentIntents[paymentIntent.id] = paymentIntent - onPaymentIntentCallback(paymentIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } + terminal.processPayment(paymentIntent, RNPaymentIntentCallback(promise) { + paymentIntents[it.id] = it }) } @ReactMethod @Suppress("unused") fun getLocations(params: ReadableMap, promise: Promise) { - val listParameters = ListLocationsParameters.Builder() - listParameters.endingBefore = params.getString("endingBefore") - listParameters.startingAfter = params.getString("startingAfter") - listParameters.limit = getInt(params, "endingBefore") - - terminal.listLocations(listParameters.build(), object : LocationListCallback { - override fun onSuccess(locations: List, hasMore: Boolean) { - promise.resolve(nativeMapOf { - putArray("locations", mapFromListLocations(locations)) - putBoolean("hasMore", hasMore) - }) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + val listParameters = ListLocationsParameters.Builder().apply { + endingBefore = params.getString("endingBefore") + startingAfter = params.getString("startingAfter") + limit = getInt(params, "endingBefore") + } + terminal.listLocations(listParameters.build(), RNLocationListCallback(promise)) } @ReactMethod @@ -709,30 +321,16 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : SetupIntentParameters.Builder().setCustomer(customerId).build() } ?: SetupIntentParameters.NULL - terminal.createSetupIntent(intentParams, object : SetupIntentCallback { - override fun onSuccess(setupIntent: SetupIntent) { - setupIntents[setupIntent.id] = setupIntent - onSetupIntentCallback(setupIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } + terminal.createSetupIntent(intentParams, RNSetupIntentCallback(promise) { + setupIntents[it.id] = it }) } @ReactMethod @Suppress("unused") fun retrieveSetupIntent(clientSecret: String, promise: Promise) { - terminal.retrieveSetupIntent(clientSecret, object : SetupIntentCallback { - override fun onSuccess(setupIntent: SetupIntent) { - setupIntents[setupIntent.id] = setupIntent - onSetupIntentCallback(setupIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } + terminal.retrieveSetupIntent(clientSecret, RNSetupIntentCallback(promise) { + setupIntents[it.id] = it }) } @@ -743,32 +341,15 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val paymentIntent = requireParam(paymentIntents[paymentIntentId]) { "There is no associated paymentIntent with id $paymentIntentId" } - terminal.cancelPaymentIntent(paymentIntent, object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - onPaymentIntentCallback(paymentIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } + terminal.cancelPaymentIntent(paymentIntent, RNPaymentIntentCallback(promise) { + paymentIntents[it.id] = null }) } @ReactMethod @Suppress("unused") - fun cancelReadReusableCard(promise: Promise) = withExceptionResolver(promise) { - val cancelable = requireCancelable(readReusableCardCancelable) { - "readReusableCard could not be canceled because the command has already been canceled or has completed." - } - cancelable.cancel(object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + fun cancelReadReusableCard(promise: Promise) { + cancelOperation(promise, readReusableCardCancelable, "readReusableCard") } @ReactMethod @@ -784,15 +365,8 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : collectSetupIntentCancelable = terminal.collectSetupIntentPaymentMethod( setupIntent, customerConsentCollected, - object : SetupIntentCallback { - override fun onSuccess(setupIntent: SetupIntent) { - onSetupIntentCallback(setupIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + RNSetupIntentCallback(promise) { setupIntents[it.id] = it } + ) } @ReactMethod @@ -805,15 +379,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod @Suppress("unused") fun cancelInstallingUpdate(promise: Promise) { - installUpdateCancelable?.cancel(object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + cancelOperation(promise, installUpdateCancelable, "installUpdate") } @ReactMethod @@ -839,15 +405,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : lineItems = cartLineItems ).build() - terminal.setReaderDisplay(cart, object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + terminal.setReaderDisplay(cart, NoOpCallback(promise)) } @ReactMethod @@ -860,15 +418,8 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val params = SetupIntentCancellationParameters.Builder().build() - terminal.cancelSetupIntent(setupIntent, params, object : SetupIntentCallback { - override fun onSuccess(setupIntent: SetupIntent) { - setupIntents[setupIntent.id] = null - onSetupIntentCallback(setupIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } + terminal.cancelSetupIntent(setupIntent, params, RNSetupIntentCallback(promise) { + setupIntents[setupIntent.id] = null }) } @@ -880,30 +431,15 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : "There is no associated setupIntent with id $setupIntentId" } - terminal.confirmSetupIntent(setupIntent, object : SetupIntentCallback { - override fun onSuccess(setupIntent: SetupIntent) { - setupIntents[setupIntent.id] = null - onSetupIntentCallback(setupIntent, promise) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } + terminal.confirmSetupIntent(setupIntent, RNSetupIntentCallback(promise) { + setupIntents[it.id] = null }) } @ReactMethod @Suppress("unused") fun clearReaderDisplay(promise: Promise) { - terminal.clearReaderDisplay(object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + terminal.clearReaderDisplay(NoOpCallback(promise)) } @ReactMethod @@ -921,16 +457,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : } val intentParams = RefundParameters.Builder(chargeId, amount, currency).build() - - terminal.collectRefundPaymentMethod(intentParams, object : Callback { - override fun onSuccess() { - promise.resolve(WritableNativeMap()) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + terminal.collectRefundPaymentMethod(intentParams, NoOpCallback(promise)) } @ReactMethod @@ -943,17 +470,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod @Suppress("unused") fun processRefund(promise: Promise) { - terminal.processRefund(object : RefundCallback { - override fun onSuccess(refund: Refund) { - promise.resolve(nativeMapOf { - putMap("refund", mapFromRefund(refund)) - }) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + terminal.processRefund(RNRefundCallback(promise)) } @ReactMethod @@ -964,42 +481,17 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : } ?: ReadReusableCardParameters.NULL readReusableCardCancelable = terminal - .readReusableCard(reusableCardParams, object : PaymentMethodCallback { - override fun onSuccess(paymentMethod: PaymentMethod) { - promise.resolve(nativeMapOf { - putMap("paymentMethod", mapFromPaymentMethod(paymentMethod)) - }) - } - - override fun onFailure(e: TerminalException) { - promise.resolve(createError(e)) - } - }) + .readReusableCard(reusableCardParams, RNPaymentMethodCallback(promise)) } - private fun sendEvent(eventName: String, result: ReadableMap) { - reactApplicationContext - .getJSModule(RCTDeviceEventEmitter::class.java) - .emit(eventName, result) - } - - private fun sendEvent(eventName: String, resultBuilder: WritableNativeMap.() -> Unit) { - reactApplicationContext - .getJSModule(RCTDeviceEventEmitter::class.java) - .emit(eventName, nativeMapOf { - resultBuilder() - }) - } - - private fun onPaymentIntentCallback(paymentIntent: PaymentIntent, promise: Promise) { - promise.resolve(nativeMapOf { - putMap("paymentIntent", mapFromPaymentIntent(paymentIntent)) - }) - } - - private fun onSetupIntentCallback(setupIntent: SetupIntent, promise: Promise) { - promise.resolve(nativeMapOf { - putMap("setupIntent", mapFromSetupIntent(setupIntent)) - }) + private fun cancelOperation( + promise: Promise, + cancelable: Cancelable?, + operationName: String, + ) = withExceptionResolver(promise) { + val toCancel = requireCancelable(cancelable) { + "$operationName could not be canceled because it has already been canceled or has completed." + } + toCancel.cancel(NoOpCallback(promise)) } } diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/NoOpCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/NoOpCallback.kt new file mode 100644 index 00000000..b1588377 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/callback/NoOpCallback.kt @@ -0,0 +1,17 @@ +package com.stripeterminalreactnative.callback + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.WritableNativeMap +import com.stripe.stripeterminal.external.callable.Callback +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.createError + +internal class NoOpCallback(private val promise: Promise) : Callback { + override fun onSuccess() { + promise.resolve(WritableNativeMap()) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNLocationListCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNLocationListCallback.kt new file mode 100644 index 00000000..b681e3e4 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNLocationListCallback.kt @@ -0,0 +1,27 @@ +package com.stripeterminalreactnative.callback + +import com.facebook.react.bridge.Promise +import com.stripe.stripeterminal.external.callable.LocationListCallback +import com.stripe.stripeterminal.external.callable.RefundCallback +import com.stripe.stripeterminal.external.models.Location +import com.stripe.stripeterminal.external.models.Refund +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.createError +import com.stripeterminalreactnative.mapFromListLocations +import com.stripeterminalreactnative.mapFromRefund +import com.stripeterminalreactnative.nativeMapOf + +class RNLocationListCallback( + private val promise: Promise, +) : LocationListCallback { + override fun onSuccess(locations: List, hasMore: Boolean) { + promise.resolve(nativeMapOf { + putArray("locations", mapFromListLocations(locations)) + putBoolean("hasMore", hasMore) + }) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentIntentCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentIntentCallback.kt new file mode 100644 index 00000000..7113effa --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentIntentCallback.kt @@ -0,0 +1,26 @@ +package com.stripeterminalreactnative.callback + +import com.facebook.react.bridge.Promise +import com.stripe.stripeterminal.external.callable.PaymentIntentCallback +import com.stripe.stripeterminal.external.models.PaymentIntent +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.createError +import com.stripeterminalreactnative.mapFromPaymentIntent +import com.stripeterminalreactnative.nativeMapOf + +class RNPaymentIntentCallback( + private val promise: Promise, + private val onPaymentIntentSuccess: (PaymentIntent) -> Unit = {} + ): PaymentIntentCallback { + + override fun onSuccess(paymentIntent: PaymentIntent) { + onPaymentIntentSuccess(paymentIntent) + promise.resolve(nativeMapOf { + putMap("paymentIntent", mapFromPaymentIntent(paymentIntent)) + }) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentMethodCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentMethodCallback.kt new file mode 100644 index 00000000..6d1f8fbd --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentMethodCallback.kt @@ -0,0 +1,23 @@ +package com.stripeterminalreactnative.callback + +import com.facebook.react.bridge.Promise +import com.stripe.stripeterminal.external.callable.PaymentMethodCallback +import com.stripe.stripeterminal.external.models.PaymentMethod +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.createError +import com.stripeterminalreactnative.mapFromPaymentMethod +import com.stripeterminalreactnative.nativeMapOf + +class RNPaymentMethodCallback( + private val promise: Promise, +) : PaymentMethodCallback { + override fun onSuccess(paymentMethod: PaymentMethod) { + promise.resolve(nativeMapOf { + putMap("paymentMethod", mapFromPaymentMethod(paymentMethod)) + }) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNRefundCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNRefundCallback.kt new file mode 100644 index 00000000..54c2917a --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNRefundCallback.kt @@ -0,0 +1,23 @@ +package com.stripeterminalreactnative.callback + +import com.facebook.react.bridge.Promise +import com.stripe.stripeterminal.external.callable.RefundCallback +import com.stripe.stripeterminal.external.models.Refund +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.createError +import com.stripeterminalreactnative.mapFromRefund +import com.stripeterminalreactnative.nativeMapOf + +class RNRefundCallback( + private val promise: Promise, +) : RefundCallback { + override fun onSuccess(refund: Refund) { + promise.resolve(nativeMapOf { + putMap("refund", mapFromRefund(refund)) + }) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNSetupIntentCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNSetupIntentCallback.kt new file mode 100644 index 00000000..5346c900 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNSetupIntentCallback.kt @@ -0,0 +1,26 @@ +package com.stripeterminalreactnative.callback + +import com.facebook.react.bridge.Promise +import com.stripe.stripeterminal.external.callable.SetupIntentCallback +import com.stripe.stripeterminal.external.models.SetupIntent +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.createError +import com.stripeterminalreactnative.mapFromSetupIntent +import com.stripeterminalreactnative.nativeMapOf + +class RNSetupIntentCallback( + private val promise: Promise, + private val onSetupIntentSuccess: (SetupIntent) -> Unit = {} + ): SetupIntentCallback { + + override fun onSuccess(setupIntent: SetupIntent) { + onSetupIntentSuccess(setupIntent) + promise.resolve(nativeMapOf { + putMap("setupIntent", mapFromSetupIntent(setupIntent)) + }) + } + + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt b/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt new file mode 100644 index 00000000..6e6a7637 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt @@ -0,0 +1,140 @@ +package com.stripeterminalreactnative.ktx + +import com.stripe.stripeterminal.Terminal +import com.stripe.stripeterminal.external.UsbConnectivity +import com.stripe.stripeterminal.external.callable.BluetoothReaderListener +import com.stripe.stripeterminal.external.callable.HandoffReaderListener +import com.stripe.stripeterminal.external.callable.ReaderCallback +import com.stripe.stripeterminal.external.callable.ReaderListenable +import com.stripe.stripeterminal.external.callable.UsbReaderListener +import com.stripe.stripeterminal.external.models.ConnectionConfiguration.BluetoothConnectionConfiguration +import com.stripe.stripeterminal.external.models.ConnectionConfiguration.EmbeddedConnectionConfiguration +import com.stripe.stripeterminal.external.models.ConnectionConfiguration.HandoffConnectionConfiguration +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.DiscoveryMethod +import com.stripe.stripeterminal.external.models.DiscoveryMethod.BLUETOOTH_SCAN +import com.stripe.stripeterminal.external.models.DiscoveryMethod.EMBEDDED +import com.stripe.stripeterminal.external.models.DiscoveryMethod.HANDOFF +import com.stripe.stripeterminal.external.models.DiscoveryMethod.INTERNET +import com.stripe.stripeterminal.external.models.DiscoveryMethod.LOCAL_MOBILE +import com.stripe.stripeterminal.external.models.DiscoveryMethod.USB +import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.TerminalException +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +// TODO (dhenry): replace this with terminalsdk:ktx when module is publicly available + +/** + * @see [Terminal.connectBluetoothReader] + */ +suspend fun Terminal.connectBluetoothReader( + reader: Reader, + config: BluetoothConnectionConfiguration, + listener: BluetoothReaderListener = object : BluetoothReaderListener {}, +): Reader { + return readerCallbackCoroutine { + connectBluetoothReader(reader, config, listener, it) + } +} + +/** + * @see [Terminal.connectEmbeddedReader] + */ +suspend fun Terminal.connectEmbeddedReader( + reader: Reader, + config: EmbeddedConnectionConfiguration +): Reader { + return readerCallbackCoroutine { connectEmbeddedReader(reader, config, it) } +} + +/** + * @see [Terminal.connectHandoffReader] + */ +suspend fun Terminal.connectHandoffReader( + reader: Reader, + config: HandoffConnectionConfiguration, + listener: HandoffReaderListener = object : HandoffReaderListener {}, +): Reader { + return readerCallbackCoroutine { + connectHandoffReader(reader, config, listener, it) + } +} + +/** + * @see [Terminal.connectInternetReader] + */ +suspend fun Terminal.connectInternetReader( + reader: Reader, + config: InternetConnectionConfiguration +): Reader { + return readerCallbackCoroutine { connectInternetReader(reader, config, it) } +} + +/** + * @see [Terminal.connectLocalMobileReader] + */ +suspend fun Terminal.connectLocalMobileReader( + reader: Reader, + config: LocalMobileConnectionConfiguration +): Reader { + return readerCallbackCoroutine { connectLocalMobileReader(reader, config, it) } +} + +/** + * @see [Terminal.connectUsbReader] + */ +@UsbConnectivity +suspend fun Terminal.connectUsbReader( + reader: Reader, + config: UsbConnectionConfiguration, + listener: UsbReaderListener = object : UsbReaderListener {}, +): Reader { + return readerCallbackCoroutine { + connectUsbReader(reader, config, listener, it) + } +} + +private suspend inline fun readerCallbackCoroutine(crossinline block: (ReaderCallback) -> Unit): Reader { + return suspendCancellableCoroutine { continuation -> + block(object : ReaderCallback { + override fun onSuccess(reader: Reader) { + continuation.resume(reader) + } + + override fun onFailure(e: TerminalException) { + continuation.resumeWithException(e) + } + }) + } +} + +@OptIn(UsbConnectivity::class) +suspend fun Terminal.connectReader( + discoveryMethod: DiscoveryMethod, + reader: Reader, + locationId: String, + listener: ReaderListenable? = null, +): Reader = when (discoveryMethod) { + BLUETOOTH_SCAN -> { + if (listener is BluetoothReaderListener) + connectBluetoothReader(reader, BluetoothConnectionConfiguration(locationId), listener) + else connectBluetoothReader(reader, BluetoothConnectionConfiguration(locationId)) + } + LOCAL_MOBILE -> connectLocalMobileReader(reader, LocalMobileConnectionConfiguration(locationId)) + INTERNET -> connectInternetReader(reader, InternetConnectionConfiguration()) + HANDOFF -> { + if (listener is HandoffReaderListener) + connectHandoffReader(reader, HandoffConnectionConfiguration(locationId), listener) + else connectHandoffReader(reader, HandoffConnectionConfiguration(locationId)) + } + EMBEDDED -> connectEmbeddedReader(reader, EmbeddedConnectionConfiguration(Unit)) + USB -> { + if (listener is UsbReaderListener) + connectUsbReader(reader, UsbConnectionConfiguration(locationId), listener) + else connectUsbReader(reader, UsbConnectionConfiguration(locationId)) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt new file mode 100644 index 00000000..47db16b5 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt @@ -0,0 +1,75 @@ +package com.stripeterminalreactnative.listener + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableNativeMap +import com.stripe.stripeterminal.external.callable.BluetoothReaderListener +import com.stripe.stripeterminal.external.callable.Cancelable +import com.stripe.stripeterminal.external.models.ReaderDisplayMessage +import com.stripe.stripeterminal.external.models.ReaderInputOptions +import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.ReactExtensions.sendEvent +import com.stripeterminalreactnative.ReactNativeConstants.FINISH_INSTALLING_UPDATE +import com.stripeterminalreactnative.ReactNativeConstants.REPORT_AVAILABLE_UPDATE +import com.stripeterminalreactnative.ReactNativeConstants.REPORT_UPDATE_PROGRESS +import com.stripeterminalreactnative.ReactNativeConstants.REQUEST_READER_DISPLAY_MESSAGE +import com.stripeterminalreactnative.ReactNativeConstants.REQUEST_READER_INPUT +import com.stripeterminalreactnative.ReactNativeConstants.START_INSTALLING_UPDATE +import com.stripeterminalreactnative.mapFromReaderDisplayMessage +import com.stripeterminalreactnative.mapFromReaderInputOptions +import com.stripeterminalreactnative.mapFromReaderSoftwareUpdate +import com.stripeterminalreactnative.nativeMapOf + +class RNBluetoothReaderListener( + private val context: ReactApplicationContext, + private val onStartInstallingUpdate: (cancelable: Cancelable?) -> Unit, +) : BluetoothReaderListener { + override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { + context.sendEvent(REPORT_AVAILABLE_UPDATE.listenerName) { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } + } + + override fun onStartInstallingUpdate( + update: ReaderSoftwareUpdate, + cancelable: Cancelable? + ) { + onStartInstallingUpdate(cancelable) + context.sendEvent(START_INSTALLING_UPDATE.listenerName) { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } + } + + override fun onReportReaderSoftwareUpdateProgress(progress: Float) { + context.sendEvent(REPORT_UPDATE_PROGRESS.listenerName) { + putMap("result", nativeMapOf { + putString("progress", progress.toString()) + }) + } + } + + override fun onFinishInstallingUpdate( + update: ReaderSoftwareUpdate?, + e: TerminalException? + ) { + context.sendEvent(FINISH_INSTALLING_UPDATE.listenerName) { + update?.let { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } ?: run { + putMap("result", WritableNativeMap()) + } + } + } + + override fun onRequestReaderInput(options: ReaderInputOptions) { + context.sendEvent(REQUEST_READER_INPUT.listenerName) { + putArray("result", mapFromReaderInputOptions(options)) + } + } + + override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) { + context.sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) { + putString("result", mapFromReaderDisplayMessage(message)) + } + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNDiscoveryListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNDiscoveryListener.kt new file mode 100644 index 00000000..9146efa4 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNDiscoveryListener.kt @@ -0,0 +1,37 @@ +package com.stripeterminalreactnative.listener + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableNativeMap +import com.stripe.stripeterminal.external.callable.Callback +import com.stripe.stripeterminal.external.callable.DiscoveryListener +import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.ReactExtensions.sendEvent +import com.stripeterminalreactnative.ReactNativeConstants +import com.stripeterminalreactnative.createError +import com.stripeterminalreactnative.mapFromReaders + +class RNDiscoveryListener( + private val context: ReactApplicationContext, + private val onDiscoveredReaders: (readers: List) -> Unit, +) : DiscoveryListener, Callback { + + override fun onUpdateDiscoveredReaders(readers: List) { + onDiscoveredReaders(readers) + context.sendEvent(ReactNativeConstants.UPDATE_DISCOVERED_READERS.listenerName) { + putArray("readers", mapFromReaders(readers)) + } + } + + override fun onSuccess() { + context.sendEvent(ReactNativeConstants.FINISH_DISCOVERING_READERS.listenerName) { + putMap("result", WritableNativeMap()) + } + } + + override fun onFailure(e: TerminalException) { + context.sendEvent(ReactNativeConstants.FINISH_DISCOVERING_READERS.listenerName) { + putMap("result", createError(e)) + } + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNHandoffReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNHandoffReaderListener.kt new file mode 100644 index 00000000..91971f42 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNHandoffReaderListener.kt @@ -0,0 +1,17 @@ +package com.stripeterminalreactnative.listener + +import com.facebook.react.bridge.ReactApplicationContext +import com.stripe.stripeterminal.external.callable.HandoffReaderListener +import com.stripe.stripeterminal.external.models.ReaderEvent +import com.stripeterminalreactnative.ReactExtensions.sendEvent +import com.stripeterminalreactnative.ReactNativeConstants.REQUEST_READER_INPUT +import com.stripeterminalreactnative.mapFromReaderEvent + +class RNHandoffReaderListener(private val context: ReactApplicationContext) : + HandoffReaderListener { + override fun onReportReaderEvent(event: ReaderEvent) { + context.sendEvent(REQUEST_READER_INPUT.listenerName) { + putString("event", mapFromReaderEvent(event)) + } + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt new file mode 100644 index 00000000..1c4b2c1d --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt @@ -0,0 +1,38 @@ +package com.stripeterminalreactnative.listener + +import com.facebook.react.bridge.ReactApplicationContext +import com.stripe.stripeterminal.external.callable.TerminalListener +import com.stripe.stripeterminal.external.models.ConnectionStatus +import com.stripe.stripeterminal.external.models.PaymentStatus +import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.TerminalException.TerminalErrorCode +import com.stripeterminalreactnative.ReactExtensions.sendEvent +import com.stripeterminalreactnative.ReactNativeConstants.CHANGE_CONNECTION_STATUS +import com.stripeterminalreactnative.ReactNativeConstants.CHANGE_PAYMENT_STATUS +import com.stripeterminalreactnative.ReactNativeConstants.REPORT_UNEXPECTED_READER_DISCONNECT +import com.stripeterminalreactnative.mapFromConnectionStatus +import com.stripeterminalreactnative.mapFromPaymentStatus +import com.stripeterminalreactnative.nativeMapOf + +class RNTerminalListener(private val context: ReactApplicationContext) : TerminalListener { + override fun onUnexpectedReaderDisconnect(reader: Reader) { + context.sendEvent(REPORT_UNEXPECTED_READER_DISCONNECT.listenerName) { + putMap("error", nativeMapOf { + putString("code", TerminalErrorCode.UNEXPECTED_SDK_ERROR.toString()) + putString("message", "Reader has been disconnected unexpectedly") + }) + } + } + + override fun onConnectionStatusChange(status: ConnectionStatus) { + context.sendEvent(CHANGE_CONNECTION_STATUS.listenerName) { + putString("result", mapFromConnectionStatus(status)) + } + } + + override fun onPaymentStatusChange(status: PaymentStatus) { + context.sendEvent(CHANGE_PAYMENT_STATUS.listenerName) { + putString("result", mapFromPaymentStatus(status)) + } + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt new file mode 100644 index 00000000..3e9f0d70 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt @@ -0,0 +1,75 @@ +package com.stripeterminalreactnative.listener + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableNativeMap +import com.stripe.stripeterminal.external.callable.Cancelable +import com.stripe.stripeterminal.external.callable.UsbReaderListener +import com.stripe.stripeterminal.external.models.ReaderDisplayMessage +import com.stripe.stripeterminal.external.models.ReaderInputOptions +import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.ReactExtensions.sendEvent +import com.stripeterminalreactnative.ReactNativeConstants.FINISH_INSTALLING_UPDATE +import com.stripeterminalreactnative.ReactNativeConstants.REPORT_AVAILABLE_UPDATE +import com.stripeterminalreactnative.ReactNativeConstants.REPORT_UPDATE_PROGRESS +import com.stripeterminalreactnative.ReactNativeConstants.REQUEST_READER_DISPLAY_MESSAGE +import com.stripeterminalreactnative.ReactNativeConstants.REQUEST_READER_INPUT +import com.stripeterminalreactnative.ReactNativeConstants.START_INSTALLING_UPDATE +import com.stripeterminalreactnative.mapFromReaderDisplayMessage +import com.stripeterminalreactnative.mapFromReaderInputOptions +import com.stripeterminalreactnative.mapFromReaderSoftwareUpdate +import com.stripeterminalreactnative.nativeMapOf + +class RNUsbReaderListener( + private val context: ReactApplicationContext, + private val onStartInstallingUpdate: (cancelable: Cancelable?) -> Unit, +): UsbReaderListener { + override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { + context.sendEvent(REPORT_AVAILABLE_UPDATE.listenerName) { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } + } + + override fun onStartInstallingUpdate( + update: ReaderSoftwareUpdate, + cancelable: Cancelable? + ) { + onStartInstallingUpdate(cancelable) + context.sendEvent(START_INSTALLING_UPDATE.listenerName) { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } + } + + override fun onReportReaderSoftwareUpdateProgress(progress: Float) { + context.sendEvent(REPORT_UPDATE_PROGRESS.listenerName) { + putMap("result", nativeMapOf { + putString("progress", progress.toString()) + }) + } + } + + override fun onFinishInstallingUpdate( + update: ReaderSoftwareUpdate?, + e: TerminalException? + ) { + context.sendEvent(FINISH_INSTALLING_UPDATE.listenerName) { + update?.let { + putMap("result", mapFromReaderSoftwareUpdate(update)) + } ?: run { + putMap("result", WritableNativeMap()) + } + } + } + + override fun onRequestReaderInput(options: ReaderInputOptions) { + context.sendEvent(REQUEST_READER_INPUT.listenerName) { + putArray("result", mapFromReaderInputOptions(options)) + } + } + + override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) { + context.sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) { + putString("result", mapFromReaderDisplayMessage(message)) + } + } +}