From ff71b7097789e18fef4549b7b2b6b74c57114ed3 Mon Sep 17 00:00:00 2001 From: Ian Lin <128665735+ianlin-bbpos@users.noreply.github.com> Date: Wed, 20 Mar 2024 04:20:05 +0800 Subject: [PATCH] Support collect inputs (#633) * support collect inputs. * fix lint and update jest snapshot. * revert wildcard imports & lint. * optimize input definitions & typo. --------- Co-authored-by: lam161006 --- .../DiscoveryMethod.kt | 2 +- .../com/stripeterminalreactnative/Errors.kt | 9 +- .../com/stripeterminalreactnative/Mappers.kt | 76 ++++- .../ReactExtensions.kt | 13 +- .../StripeTerminalReactNativeModule.kt | 312 +++++++++++++++--- .../StripeTerminalReactNativePackage.kt | 1 - .../TerminalApplicationDelegate.kt | 1 - .../callback/NoOpCallback.kt | 1 - .../callback/RNCollectInputResultCallback.kt | 27 ++ .../callback/RNLocationListCallback.kt | 12 +- .../callback/RNPaymentIntentCallback.kt | 10 +- .../callback/RNPaymentMethodCallback.kt | 10 +- .../callback/RNRefundCallback.kt | 10 +- .../callback/RNSetupIntentCallback.kt | 10 +- .../ktx/TerminalExtensions.kt | 35 +- .../listener/RNBluetoothReaderListener.kt | 11 +- .../listener/RNDiscoveryListener.kt | 2 +- .../listener/RNOfflineListener.kt | 15 +- .../listener/RNReaderReconnectionListener.kt | 17 +- .../listener/RNTerminalListener.kt | 11 +- .../listener/RNUsbReaderListener.kt | 13 +- dev-app/src/App.tsx | 12 +- dev-app/src/screens/CollectInputsScreen.tsx | 157 +++++++++ dev-app/src/screens/HomeScreen.tsx | 9 + dev-app/src/screens/ReaderSettingsScreen.tsx | 2 +- ios/Mappers.swift | 159 +++++---- ios/StripeTerminalReactNative.m | 12 +- ios/StripeTerminalReactNative.swift | 158 +++++++++ src/StripeTerminalSdk.tsx | 6 + .../__snapshots__/functions.test.ts.snap | 2 + .../__snapshots__/index.test.tsx.snap | 13 + src/functions.ts | 32 ++ .../useStripeTerminal.test.tsx.snap | 2 + src/hooks/useStripeTerminal.tsx | 36 ++ src/types/index.ts | 58 ++++ 35 files changed, 1062 insertions(+), 194 deletions(-) create mode 100644 android/src/main/java/com/stripeterminalreactnative/callback/RNCollectInputResultCallback.kt create mode 100644 dev-app/src/screens/CollectInputsScreen.tsx diff --git a/android/src/main/java/com/stripeterminalreactnative/DiscoveryMethod.kt b/android/src/main/java/com/stripeterminalreactnative/DiscoveryMethod.kt index 213ec70a..fbb1e71a 100644 --- a/android/src/main/java/com/stripeterminalreactnative/DiscoveryMethod.kt +++ b/android/src/main/java/com/stripeterminalreactnative/DiscoveryMethod.kt @@ -5,5 +5,5 @@ enum class DiscoveryMethod { INTERNET, LOCAL_MOBILE, HANDOFF, - USB, + USB } diff --git a/android/src/main/java/com/stripeterminalreactnative/Errors.kt b/android/src/main/java/com/stripeterminalreactnative/Errors.kt index f8514e3d..d4ace370 100644 --- a/android/src/main/java/com/stripeterminalreactnative/Errors.kt +++ b/android/src/main/java/com/stripeterminalreactnative/Errors.kt @@ -11,9 +11,12 @@ import kotlin.jvm.Throws internal fun createError(throwable: Throwable): ReadableMap = nativeMapOf { putError(throwable) } internal fun WritableMap.putError(throwable: Throwable): ReadableMap = apply { - putMap("error", nativeMapOf { - putErrorContents(throwable) - }) + putMap( + "error", + nativeMapOf { + putErrorContents(throwable) + } + ) } private fun WritableMap.putErrorContents(throwable: Throwable?) { diff --git a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt index c614e24d..b9670b79 100644 --- a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt +++ b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt @@ -5,17 +5,21 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray +import com.stripe.stripeterminal.external.CollectInputs import com.stripe.stripeterminal.external.models.Address import com.stripe.stripeterminal.external.models.CardDetails import com.stripe.stripeterminal.external.models.CardPresentDetails import com.stripe.stripeterminal.external.models.CartLineItem import com.stripe.stripeterminal.external.models.Charge +import com.stripe.stripeterminal.external.models.CollectInputsResult import com.stripe.stripeterminal.external.models.ConnectionStatus import com.stripe.stripeterminal.external.models.DeviceType import com.stripe.stripeterminal.external.models.DisconnectReason +import com.stripe.stripeterminal.external.models.EmailResult import com.stripe.stripeterminal.external.models.Location import com.stripe.stripeterminal.external.models.LocationStatus import com.stripe.stripeterminal.external.models.NetworkStatus +import com.stripe.stripeterminal.external.models.NumericResult import com.stripe.stripeterminal.external.models.OfflineStatus import com.stripe.stripeterminal.external.models.PaymentIntent import com.stripe.stripeterminal.external.models.PaymentIntentStatus @@ -23,17 +27,19 @@ import com.stripe.stripeterminal.external.models.PaymentMethod import com.stripe.stripeterminal.external.models.PaymentMethodDetails import com.stripe.stripeterminal.external.models.PaymentMethodType import com.stripe.stripeterminal.external.models.PaymentStatus +import com.stripe.stripeterminal.external.models.PhoneResult import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.ReaderAccessibility 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.ReaderInputOptions.ReaderInputOption -import com.stripe.stripeterminal.external.models.ReaderAccessibility import com.stripe.stripeterminal.external.models.ReaderSettings import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate import com.stripe.stripeterminal.external.models.ReaderTextToSpeechStatus import com.stripe.stripeterminal.external.models.ReceiptDetails import com.stripe.stripeterminal.external.models.Refund +import com.stripe.stripeterminal.external.models.SelectionResult import com.stripe.stripeterminal.external.models.SetupAttempt import com.stripe.stripeterminal.external.models.SetupAttemptStatus import com.stripe.stripeterminal.external.models.SetupIntent @@ -41,7 +47,9 @@ import com.stripe.stripeterminal.external.models.SetupIntentCardPresentDetails import com.stripe.stripeterminal.external.models.SetupIntentPaymentMethodDetails import com.stripe.stripeterminal.external.models.SetupIntentStatus import com.stripe.stripeterminal.external.models.SetupIntentUsage +import com.stripe.stripeterminal.external.models.SignatureResult import com.stripe.stripeterminal.external.models.SimulateReaderUpdate +import com.stripe.stripeterminal.external.models.TextResult import com.stripe.stripeterminal.external.models.Wallet import com.stripe.stripeterminal.log.LogLevel @@ -425,11 +433,14 @@ internal fun mapFromRefund(refund: Refund): ReadableMap = nativeMapOf { putString("failureBalanceTransaction", refund.failureBalanceTransaction) putString("failureReason", refund.failureReason) putString("id", refund.id) - putMap("metadata", nativeMapOf { - refund.metadata?.map { - putString(it.key, it.value) + putMap( + "metadata", + nativeMapOf { + refund.metadata?.map { + putString(it.key, it.value) + } } - }) + ) putString("paymentIntentId", refund.paymentIntentId) putMap("paymentMethodDetails", mapFromPaymentMethodDetails(refund.paymentMethodDetails)) putString("reason", refund.reason) @@ -500,7 +511,10 @@ private fun mapFromCardPresentDetails(cardPresentDetails: CardPresentDetails?): putString("network", cardPresentDetails?.network) putString("description", cardPresentDetails?.description) putMap("wallet", mapFromWallet(cardPresentDetails?.wallet)) - putArray("preferredLocales", convertListToReadableArray(cardPresentDetails?.preferredLocales)) + putArray( + "preferredLocales", + convertListToReadableArray(cardPresentDetails?.preferredLocales) + ) } internal fun mapFromWallet(wallet: Wallet?): ReadableMap = @@ -509,7 +523,9 @@ internal fun mapFromWallet(wallet: Wallet?): ReadableMap = } private fun convertListToReadableArray(list: List?): ReadableArray? { - return list?.let { WritableNativeArray().apply { for (item in list) { pushString(item) } } } ?: null + return list?.let { + WritableNativeArray().apply { for (item in list) { pushString(item) } } + } ?: null } fun mapFromReceiptDetails(receiptDetails: ReceiptDetails?): ReadableMap = @@ -598,3 +614,49 @@ internal fun mapFromReaderSettings(settings: ReaderSettings): ReadableMap { } } } + +@OptIn(CollectInputs::class) +fun mapFromCollectInputsResults(results: List): ReadableArray { + return nativeArrayOf { + results.forEach { + when (it) { + is EmailResult -> pushMap( + nativeMapOf { + putBoolean("skipped", it.skipped) + putString("email", it.email) + } + ) + is NumericResult -> pushMap( + nativeMapOf { + putBoolean("skipped", it.skipped) + putString("numericString", it.numericString) + } + ) + is PhoneResult -> pushMap( + nativeMapOf { + putBoolean("skipped", it.skipped) + putString("phone", it.phone) + } + ) + is SelectionResult -> pushMap( + nativeMapOf { + putBoolean("skipped", it.skipped) + putString("selection", it.selection) + } + ) + is SignatureResult -> pushMap( + nativeMapOf { + putBoolean("skipped", it.skipped) + putString("signatureSvg", it.signatureSvg) + } + ) + is TextResult -> pushMap( + nativeMapOf { + putBoolean("skipped", it.skipped) + putString("text", it.text) + } + ) + } + } + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/ReactExtensions.kt b/android/src/main/java/com/stripeterminalreactnative/ReactExtensions.kt index d85398a5..ae4cc52f 100644 --- a/android/src/main/java/com/stripeterminalreactnative/ReactExtensions.kt +++ b/android/src/main/java/com/stripeterminalreactnative/ReactExtensions.kt @@ -8,13 +8,16 @@ internal object ReactExtensions { fun ReactApplicationContext.sendEvent( eventName: String, - resultBuilder: (WritableMap.() -> Unit)? = null, + resultBuilder: (WritableMap.() -> Unit)? = null ) { getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - .emit(eventName, resultBuilder?.let { - nativeMapOf { - it() + .emit( + eventName, + resultBuilder?.let { + nativeMapOf { + it() + } } - }) + ) } } diff --git a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt index c17f47ae..390c2946 100644 --- a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt +++ b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt @@ -1,5 +1,5 @@ package com.stripeterminalreactnative -import android.util.Log + import android.annotation.SuppressLint import android.app.Application import android.content.ComponentCallbacks2 @@ -12,12 +12,53 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil import com.stripe.stripeterminal.Terminal import com.stripe.stripeterminal.TerminalApplicationDelegate.onCreate +import com.stripe.stripeterminal.external.CollectInputs import com.stripe.stripeterminal.external.OfflineMode import com.stripe.stripeterminal.external.callable.Cancelable import com.stripe.stripeterminal.external.callable.ReaderListenable -import com.stripe.stripeterminal.external.models.* -import com.stripeterminalreactnative.callback.* +import com.stripe.stripeterminal.external.models.CaptureMethod +import com.stripe.stripeterminal.external.models.CardPresentParameters +import com.stripe.stripeterminal.external.models.CardPresentRoutingOptionParameters +import com.stripe.stripeterminal.external.models.Cart +import com.stripe.stripeterminal.external.models.CollectConfiguration +import com.stripe.stripeterminal.external.models.CollectInputsParameters +import com.stripe.stripeterminal.external.models.CreateConfiguration +import com.stripe.stripeterminal.external.models.DiscoveryConfiguration +import com.stripe.stripeterminal.external.models.EmailInput +import com.stripe.stripeterminal.external.models.Input +import com.stripe.stripeterminal.external.models.ListLocationsParameters +import com.stripe.stripeterminal.external.models.NumericInput +import com.stripe.stripeterminal.external.models.OfflineBehavior +import com.stripe.stripeterminal.external.models.PaymentIntent +import com.stripe.stripeterminal.external.models.PaymentIntentParameters +import com.stripe.stripeterminal.external.models.PaymentMethodOptionsParameters +import com.stripe.stripeterminal.external.models.PaymentMethodType +import com.stripe.stripeterminal.external.models.PhoneInput +import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.ReaderSettingsParameters +import com.stripe.stripeterminal.external.models.RefundConfiguration +import com.stripe.stripeterminal.external.models.RefundParameters +import com.stripe.stripeterminal.external.models.RoutingPriority +import com.stripe.stripeterminal.external.models.SelectionButton +import com.stripe.stripeterminal.external.models.SelectionButtonStyle +import com.stripe.stripeterminal.external.models.SelectionInput +import com.stripe.stripeterminal.external.models.SetupIntent +import com.stripe.stripeterminal.external.models.SetupIntentCancellationParameters +import com.stripe.stripeterminal.external.models.SetupIntentConfiguration +import com.stripe.stripeterminal.external.models.SetupIntentParameters +import com.stripe.stripeterminal.external.models.SignatureInput +import com.stripe.stripeterminal.external.models.SimulatedCard +import com.stripe.stripeterminal.external.models.SimulatorConfiguration +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripe.stripeterminal.external.models.TextInput +import com.stripe.stripeterminal.external.models.TippingConfiguration import com.stripeterminalreactnative.callback.NoOpCallback +import com.stripeterminalreactnative.callback.RNCollectInputResultCallback +import com.stripeterminalreactnative.callback.RNLocationListCallback +import com.stripeterminalreactnative.callback.RNPaymentIntentCallback +import com.stripeterminalreactnative.callback.RNReadSettingsCallback +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 @@ -32,7 +73,6 @@ import kotlinx.coroutines.launch import java.util.UUID import kotlin.collections.HashMap - class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private var discoveredReadersList: List = listOf() @@ -42,6 +82,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : private var collectSetupIntentCancelable: Cancelable? = null private var installUpdateCancelable: Cancelable? = null private var cancelReaderConnectionCancellable: Cancelable? = null + private var collectInputsCancelable: Cancelable? = null private var paymentIntents: HashMap = HashMap() private var setupIntents: HashMap = HashMap() @@ -60,7 +101,8 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : override fun onTrimMemory(level: Int) {} override fun onLowMemory() {} override fun onConfigurationChanged(p0: Configuration) {} - }) + } + ) } override fun getConstants(): MutableMap = @@ -80,7 +122,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : mapToLogLevel(params.getString("logLevel")), tokenProvider, RNTerminalListener(context), - RNOfflineListener(context), + RNOfflineListener(context) ) NativeTypeFactory.writableNativeMap() } else { @@ -134,7 +176,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : fun setConnectionToken(params: ReadableMap, promise: Promise) { tokenProvider.setConnectionToken( token = params.getString("token"), - error = params.getString("error"), + error = params.getString("error") ) promise.resolve(null) } @@ -162,12 +204,22 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : } discoverCancelable = terminal.discoverReaders( - config = when(discoveryMethod) { - DiscoveryMethod.BLUETOOTH_SCAN -> DiscoveryConfiguration.BluetoothDiscoveryConfiguration(0, getBoolean(params, "simulated")) - DiscoveryMethod.INTERNET -> DiscoveryConfiguration.InternetDiscoveryConfiguration(isSimulated = getBoolean(params, "simulated")) - DiscoveryMethod.USB -> DiscoveryConfiguration.UsbDiscoveryConfiguration(0, getBoolean(params, "simulated")) + config = when (discoveryMethod) { + DiscoveryMethod.BLUETOOTH_SCAN -> DiscoveryConfiguration.BluetoothDiscoveryConfiguration( + 0, + getBoolean(params, "simulated") + ) + DiscoveryMethod.INTERNET -> DiscoveryConfiguration.InternetDiscoveryConfiguration( + isSimulated = getBoolean(params, "simulated") + ) + DiscoveryMethod.USB -> DiscoveryConfiguration.UsbDiscoveryConfiguration( + 0, + getBoolean(params, "simulated") + ) DiscoveryMethod.HANDOFF -> DiscoveryConfiguration.HandoffDiscoveryConfiguration() - DiscoveryMethod.LOCAL_MOBILE -> DiscoveryConfiguration.LocalMobileDiscoveryConfiguration(getBoolean(params, "simulated")) }, + DiscoveryMethod.LOCAL_MOBILE -> DiscoveryConfiguration.LocalMobileDiscoveryConfiguration( + getBoolean(params, "simulated") + ) }, listener, listener ) @@ -195,9 +247,11 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val serialNumber = reader.getString("serialNumber") - val selectedReader = requireParam(discoveredReadersList.find { - it.serialNumber == serialNumber - }) { + val selectedReader = requireParam( + discoveredReadersList.find { + it.serialNumber == serialNumber + } + ) { "Could not find a reader with serialNumber $serialNumber" } @@ -205,8 +259,10 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : params.getString("locationId") ?: selectedReader.location?.id.orEmpty() val autoReconnectOnUnexpectedDisconnect = if (discoveryMethod == DiscoveryMethod.BLUETOOTH_SCAN || discoveryMethod == DiscoveryMethod.USB || discoveryMethod == DiscoveryMethod.LOCAL_MOBILE) { - getBoolean(params,"autoReconnectOnUnexpectedDisconnect") - } else false + getBoolean(params, "autoReconnectOnUnexpectedDisconnect") + } else { + false + } val reconnectionListener = RNReaderReconnectionListener(context) { cancelReaderConnectionCancellable = it @@ -313,8 +369,11 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val offlineBehavior = params.getString("offlineBehavior") val paymentMethodTypes = paymentMethods?.toArrayList()?.mapNotNull { - if (it is String) PaymentMethodType.valueOf(it.uppercase()) - else null + if (it is String) { + PaymentMethodType.valueOf(it.uppercase()) + } else { + null + } } val intentParams = paymentMethodTypes?.let { @@ -399,9 +458,13 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val uuid = UUID.randomUUID().toString() - terminal.createPaymentIntent(intentParams.build(), RNPaymentIntentCallback(promise, uuid) { pi -> - paymentIntents[uuid] = pi - }, CreateConfiguration(offlineBehaviorParam)) + terminal.createPaymentIntent( + intentParams.build(), + RNPaymentIntentCallback(promise, uuid) { pi -> + paymentIntents[uuid] = pi + }, + CreateConfiguration(offlineBehaviorParam) + ) } @OptIn(OfflineMode::class) @@ -435,7 +498,9 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : configBuilder.updatePaymentIntent(getBoolean(params, "updatePaymentIntent")) } if (params.hasKey("enableCustomerCancellation")) { - configBuilder.setEnableCustomerCancellation(getBoolean(params, "enableCustomerCancellation")) + configBuilder.setEnableCustomerCancellation( + getBoolean(params, "enableCustomerCancellation") + ) } val config = configBuilder.build() @@ -453,14 +518,19 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @Suppress("unused") fun retrievePaymentIntent(clientSecret: String, promise: Promise) { val uuid = UUID.randomUUID().toString() - terminal.retrievePaymentIntent(clientSecret, RNPaymentIntentCallback(promise, uuid) { pi -> - paymentIntents[uuid] = pi - }) + terminal.retrievePaymentIntent( + clientSecret, + RNPaymentIntentCallback(promise, uuid) { pi -> + paymentIntents[uuid] = pi + } + ) } @ReactMethod @Suppress("unused") - fun confirmPaymentIntent(paymentIntent: ReadableMap, promise: Promise) = withExceptionResolver(promise) { + fun confirmPaymentIntent(paymentIntent: ReadableMap, promise: Promise) = withExceptionResolver( + promise + ) { val uuid = requireParam(paymentIntent.getString("sdkUuid")) { "The PaymentIntent is missing sdkUuid field. This method requires you to use the PaymentIntent that was returned from either createPaymentIntent or retrievePaymentIntent." } @@ -468,9 +538,12 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : "No PaymentIntent was found with the sdkUuid $uuid. The PaymentIntent provided must be re-retrieved with retrievePaymentIntent or a new PaymentIntent must be created with createPaymentIntent." } - terminal.confirmPaymentIntent(paymentIntent, RNPaymentIntentCallback(promise, uuid) { - paymentIntents.clear() - }) + terminal.confirmPaymentIntent( + paymentIntent, + RNPaymentIntentCallback(promise, uuid) { + paymentIntents.clear() + } + ) } @ReactMethod @@ -492,18 +565,24 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : } ?: SetupIntentParameters.NULL val uuid = UUID.randomUUID().toString() - terminal.createSetupIntent(intentParams, RNSetupIntentCallback(promise, uuid) { - setupIntents[uuid] = it - }) + terminal.createSetupIntent( + intentParams, + RNSetupIntentCallback(promise, uuid) { + setupIntents[uuid] = it + } + ) } @ReactMethod @Suppress("unused") fun retrieveSetupIntent(clientSecret: String, promise: Promise) { val uuid = UUID.randomUUID().toString() - terminal.retrieveSetupIntent(clientSecret, RNSetupIntentCallback(promise, uuid) { - setupIntents[uuid] = it - }) + terminal.retrieveSetupIntent( + clientSecret, + RNSetupIntentCallback(promise, uuid) { + setupIntents[uuid] = it + } + ) } @OptIn(OfflineMode::class) @@ -518,9 +597,12 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : "No PaymentIntent was found with the sdkUuid $uuid. The PaymentIntent provided must be re-retrieved with retrievePaymentIntent or a new PaymentIntent must be created with createPaymentIntent." } - terminal.cancelPaymentIntent(paymentIntent, RNPaymentIntentCallback(promise, uuid) { - paymentIntents[uuid] = null - }) + terminal.cancelPaymentIntent( + paymentIntent, + RNPaymentIntentCallback(promise, uuid) { + paymentIntents[uuid] = null + } + ) } @ReactMethod @@ -577,7 +659,9 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : } val cartLineItems = - mapToCartLineItems(params.getArray("lineItems") ?: NativeTypeFactory.writableNativeArray()) + mapToCartLineItems( + params.getArray("lineItems") ?: NativeTypeFactory.writableNativeArray() + ) val cart = Cart.Builder( currency = currency, @@ -602,9 +686,13 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val params = SetupIntentCancellationParameters.Builder().build() - terminal.cancelSetupIntent(setupIntent, params, RNSetupIntentCallback(promise, uuid) { - setupIntents[setupIntent.id] = null - }) + terminal.cancelSetupIntent( + setupIntent, + params, + RNSetupIntentCallback(promise, uuid) { + setupIntents[setupIntent.id] = null + } + ) } @ReactMethod @@ -618,9 +706,12 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : "No SetupIntent was found with the sdkUuid $uuid. The SetupIntent provided must be re-retrieved with retrieveSetupIntent or a new SetupIntent must be created with createSetupIntent." } - terminal.confirmSetupIntent(setupIntent, RNSetupIntentCallback(promise, uuid) { - setupIntents[it.id] = null - }) + terminal.confirmSetupIntent( + setupIntent, + RNSetupIntentCallback(promise, uuid) { + setupIntents[it.id] = null + } + ) } @ReactMethod @@ -652,7 +743,11 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val reverseTransfer = params.getBoolean("reverseTransfer") var intentParamsBuild = if (!paymentIntentId.isNullOrBlank()) { - RefundParameters.Builder(RefundParameters.Id.PaymentIntent(paymentIntentId), amount, currency) + RefundParameters.Builder( + RefundParameters.Id.PaymentIntent(paymentIntentId), + amount, + currency + ) } else { RefundParameters.Builder(RefundParameters.Id.Charge(chargeId!!), amount, currency) } @@ -665,7 +760,9 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : collectRefundPaymentMethodCancelable = terminal.collectRefundPaymentMethod( intentParams, - RefundConfiguration.Builder().setEnableCustomerCancellation(enableCustomerCancellation).build(), + RefundConfiguration.Builder().setEnableCustomerCancellation( + enableCustomerCancellation + ).build(), NoOpCallback(promise) ) } @@ -694,7 +791,6 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod @Suppress("unused") fun getReaderSettings(promise: Promise) { - Log.e("ianTest", "getReaderSettings") terminal.getReaderSettings(RNReadSettingsCallback(promise)) } @@ -710,6 +806,122 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : terminal.setReaderSettings(readerSettingsParameters, RNReadSettingsCallback(promise)) } + @OptIn(CollectInputs::class) + @ReactMethod + @Suppress("unused") + fun collectInputs(params: ReadableMap, promise: Promise) = withExceptionResolver(promise) { + val collectInputs = requireParam(params.getArray("collectInputs")) { + "You must provide a collectInputs" + } + val listInput = ArrayList() + for (i in 0 until collectInputs.size()) { + val collectInput = collectInputs.getMap(i) + when (collectInput.getString("inputType")) { + "TEXT" -> { + collectInput.let { + listInput.add( + TextInput.Builder(it.getString("title") ?: "") + .setDescription(it.getString("description") ?: "") + .setRequired(it.getBoolean("required")) + .setSkipButtonText(it.getString("skipButtonText")) + .setSubmitButtonText(it.getString("submitButtonText")) + .build() + ) + } + } + "NUMERIC" -> { + collectInput.let { + listInput.add( + NumericInput.Builder(it.getString("title") ?: "") + .setDescription(it.getString("description")) + .setRequired(it.getBoolean("required")) + .setSkipButtonText(it.getString("skipButtonText")) + .setSubmitButtonText(it.getString("submitButtonText")) + .build() + ) + } + } + "EMAIL" -> { + collectInput.let { + listInput.add( + EmailInput.Builder(it.getString("title") ?: "") + .setDescription(it.getString("description")) + .setRequired(it.getBoolean("required")) + .setSkipButtonText(it.getString("skipButtonText")) + .setSubmitButtonText(it.getString("submitButtonText")) + .build() + ) + } + } + "PHONE" -> { + collectInput.let { + listInput.add( + PhoneInput.Builder(it.getString("title") ?: "") + .setDescription(it.getString("description")) + .setRequired(it.getBoolean("required")) + .setSkipButtonText(it.getString("skipButtonText")) + .setSubmitButtonText(it.getString("submitButtonText")) + .build() + ) + } + } + "SIGNATURE" -> { + collectInput.let { + listInput.add( + SignatureInput.Builder(it.getString("title") ?: "") + .setDescription(it.getString("description")) + .setRequired(it.getBoolean("required")) + .setSkipButtonText(it.getString("skipButtonText")) + .setSubmitButtonText(it.getString("submitButtonText")) + .build() + ) + } + } + "SELECTION" -> { + collectInput.let { + val selectionButtons = it.getArray("selectionButtons") + val listSelectionButtons = ArrayList() + selectionButtons?.let { array -> + for (i in 0 until array.size()) { + val button = array.getMap(i) + listSelectionButtons.add( + SelectionButton( + if (button.getString("style") == "PRIMARY") { + SelectionButtonStyle.PRIMARY + } else { + SelectionButtonStyle.SECONDARY + }, + button.getString("text") ?: "" + ) + ) + } + } + listInput.add( + SelectionInput.Builder(it.getString("title") ?: "") + .setDescription(it.getString("description") ?: "") + .setRequired(it.getBoolean("required")) + .setSkipButtonText(it.getString("skipButtonText") ?: "") + .setSelectionButtons(listSelectionButtons) + .build() + ) + } + } + } + } + + val collectInputsParameters = CollectInputsParameters(listInput) + collectInputsCancelable = terminal.collectInputs( + collectInputsParameters, + RNCollectInputResultCallback(promise) + ) + } + + @ReactMethod + @Suppress("unused") + fun cancelCollectInputs(promise: Promise) { + cancelOperation(promise, collectInputsCancelable, "collectInputs") + } + @ReactMethod fun addListener(eventName: String?) { // Set up any upstream listeners or background tasks as necessary @@ -734,6 +946,6 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : } private fun busyMessage(command: String, busyBy: String): String { - return "Could not execute $command because the SDK is busy with another command: $busyBy." + return "Could not execute $command because the SDK is busy with another command: $busyBy." } } diff --git a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativePackage.kt b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativePackage.kt index b2d3c231..f376634e 100644 --- a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativePackage.kt +++ b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativePackage.kt @@ -5,7 +5,6 @@ import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ViewManager - class StripeTerminalReactNativePackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List { return listOf(StripeTerminalReactNativeModule(reactContext)) diff --git a/android/src/main/java/com/stripeterminalreactnative/TerminalApplicationDelegate.kt b/android/src/main/java/com/stripeterminalreactnative/TerminalApplicationDelegate.kt index cbfbbb06..c559c4f9 100644 --- a/android/src/main/java/com/stripeterminalreactnative/TerminalApplicationDelegate.kt +++ b/android/src/main/java/com/stripeterminalreactnative/TerminalApplicationDelegate.kt @@ -1,7 +1,6 @@ package com.stripeterminalreactnative import android.app.Application -import com.facebook.react.bridge.UiThreadUtil import com.stripe.stripeterminal.TerminalApplicationDelegate object TerminalApplicationDelegate { diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/NoOpCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/NoOpCallback.kt index 298552b4..9371fa7e 100644 --- a/android/src/main/java/com/stripeterminalreactnative/callback/NoOpCallback.kt +++ b/android/src/main/java/com/stripeterminalreactnative/callback/NoOpCallback.kt @@ -1,7 +1,6 @@ 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.NativeTypeFactory diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectInputResultCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectInputResultCallback.kt new file mode 100644 index 00000000..01f7d571 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectInputResultCallback.kt @@ -0,0 +1,27 @@ +package com.stripeterminalreactnative.callback + +import com.facebook.react.bridge.Promise +import com.stripe.stripeterminal.external.CollectInputs +import com.stripe.stripeterminal.external.callable.CollectInputsResultCallback +import com.stripe.stripeterminal.external.models.CollectInputsResult +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripeterminalreactnative.createError +import com.stripeterminalreactnative.mapFromCollectInputsResults +import com.stripeterminalreactnative.nativeMapOf + +@OptIn(CollectInputs::class) +class RNCollectInputResultCallback( + private val promise: Promise +) : CollectInputsResultCallback { + override fun onFailure(e: TerminalException) { + promise.resolve(createError(e)) + } + + override fun onSuccess(results: List) { + promise.resolve( + nativeMapOf { + putArray("collectInputResults", mapFromCollectInputsResults(results)) + } + ) + } +} diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNLocationListCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNLocationListCallback.kt index df8dbf4c..7cde5dff 100644 --- a/android/src/main/java/com/stripeterminalreactnative/callback/RNLocationListCallback.kt +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNLocationListCallback.kt @@ -9,13 +9,15 @@ import com.stripeterminalreactnative.mapFromListLocations import com.stripeterminalreactnative.nativeMapOf class RNLocationListCallback( - private val promise: Promise, + private val promise: Promise ) : LocationListCallback { override fun onSuccess(locations: List, hasMore: Boolean) { - promise.resolve(nativeMapOf { - putArray("locations", mapFromListLocations(locations)) - putBoolean("hasMore", hasMore) - }) + promise.resolve( + nativeMapOf { + putArray("locations", mapFromListLocations(locations)) + putBoolean("hasMore", hasMore) + } + ) } override fun onFailure(e: TerminalException) { diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentIntentCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentIntentCallback.kt index caaf06b8..99618c68 100644 --- a/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentIntentCallback.kt +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentIntentCallback.kt @@ -12,13 +12,15 @@ class RNPaymentIntentCallback( private val promise: Promise, private val uuid: String, private val onPaymentIntentSuccess: (PaymentIntent) -> Unit = {} - ): PaymentIntentCallback { +) : PaymentIntentCallback { override fun onSuccess(paymentIntent: PaymentIntent) { onPaymentIntentSuccess(paymentIntent) - promise.resolve(nativeMapOf { - putMap("paymentIntent", mapFromPaymentIntent(paymentIntent, uuid)) - }) + promise.resolve( + nativeMapOf { + putMap("paymentIntent", mapFromPaymentIntent(paymentIntent, uuid)) + } + ) } override fun onFailure(e: TerminalException) { diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentMethodCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentMethodCallback.kt index 6d1f8fbd..c995d03d 100644 --- a/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentMethodCallback.kt +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNPaymentMethodCallback.kt @@ -9,12 +9,14 @@ import com.stripeterminalreactnative.mapFromPaymentMethod import com.stripeterminalreactnative.nativeMapOf class RNPaymentMethodCallback( - private val promise: Promise, + private val promise: Promise ) : PaymentMethodCallback { override fun onSuccess(paymentMethod: PaymentMethod) { - promise.resolve(nativeMapOf { - putMap("paymentMethod", mapFromPaymentMethod(paymentMethod)) - }) + promise.resolve( + nativeMapOf { + putMap("paymentMethod", mapFromPaymentMethod(paymentMethod)) + } + ) } override fun onFailure(e: TerminalException) { diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNRefundCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNRefundCallback.kt index 54c2917a..b9070be1 100644 --- a/android/src/main/java/com/stripeterminalreactnative/callback/RNRefundCallback.kt +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNRefundCallback.kt @@ -9,12 +9,14 @@ import com.stripeterminalreactnative.mapFromRefund import com.stripeterminalreactnative.nativeMapOf class RNRefundCallback( - private val promise: Promise, + private val promise: Promise ) : RefundCallback { override fun onSuccess(refund: Refund) { - promise.resolve(nativeMapOf { - putMap("refund", mapFromRefund(refund)) - }) + promise.resolve( + nativeMapOf { + putMap("refund", mapFromRefund(refund)) + } + ) } override fun onFailure(e: TerminalException) { diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNSetupIntentCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNSetupIntentCallback.kt index 9049e12c..9cbe3923 100644 --- a/android/src/main/java/com/stripeterminalreactnative/callback/RNSetupIntentCallback.kt +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNSetupIntentCallback.kt @@ -12,13 +12,15 @@ class RNSetupIntentCallback( private val promise: Promise, private val uuid: String, private val onSetupIntentSuccess: (SetupIntent) -> Unit = {} - ): SetupIntentCallback { +) : SetupIntentCallback { override fun onSuccess(setupIntent: SetupIntent) { onSetupIntentSuccess(setupIntent) - promise.resolve(nativeMapOf { - putMap("setupIntent", mapFromSetupIntent(setupIntent, uuid)) - }) + promise.resolve( + nativeMapOf { + putMap("setupIntent", mapFromSetupIntent(setupIntent, uuid)) + } + ) } override fun onFailure(e: TerminalException) { diff --git a/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt b/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt index d810dd38..bbbb28d8 100644 --- a/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt +++ b/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt @@ -26,7 +26,7 @@ import kotlin.coroutines.resumeWithException suspend fun Terminal.connectBluetoothReader( reader: Reader, config: BluetoothConnectionConfiguration, - listener: ReaderListener = object : ReaderListener {}, + listener: ReaderListener = object : ReaderListener {} ): Reader { return readerCallbackCoroutine { connectBluetoothReader(reader, config, listener, it) @@ -39,7 +39,7 @@ suspend fun Terminal.connectBluetoothReader( suspend fun Terminal.connectHandoffReader( reader: Reader, config: HandoffConnectionConfiguration, - listener: HandoffReaderListener = object : HandoffReaderListener {}, + listener: HandoffReaderListener = object : HandoffReaderListener {} ): Reader { return readerCallbackCoroutine { connectHandoffReader(reader, config, listener, it) @@ -72,7 +72,7 @@ suspend fun Terminal.connectLocalMobileReader( suspend fun Terminal.connectUsbReader( reader: Reader, config: UsbConnectionConfiguration, - listener: ReaderListener = object : ReaderListener {}, + listener: ReaderListener = object : ReaderListener {} ): Reader { return readerCallbackCoroutine { connectUsbReader(reader, config, listener, it) @@ -102,22 +102,39 @@ suspend fun Terminal.connectReader( reconnectionListener: ReaderReconnectionListener ): Reader = when (discoveryMethod) { DiscoveryMethod.BLUETOOTH_SCAN -> { - val connConfig = BluetoothConnectionConfiguration(locationId, autoReconnectOnUnexpectedDisconnect, reconnectionListener) + val connConfig = BluetoothConnectionConfiguration( + locationId, + autoReconnectOnUnexpectedDisconnect, + reconnectionListener + ) if (listener is ReaderListener) { connectBluetoothReader(reader, connConfig, listener) } else { connectBluetoothReader(reader, connConfig) } } - DiscoveryMethod.LOCAL_MOBILE -> connectLocalMobileReader(reader, LocalMobileConnectionConfiguration(locationId, autoReconnectOnUnexpectedDisconnect, reconnectionListener)) + DiscoveryMethod.LOCAL_MOBILE -> connectLocalMobileReader( + reader, + LocalMobileConnectionConfiguration( + locationId, + autoReconnectOnUnexpectedDisconnect, + reconnectionListener + ) + ) DiscoveryMethod.INTERNET -> connectInternetReader(reader, InternetConnectionConfiguration()) DiscoveryMethod.HANDOFF -> { - if (listener is HandoffReaderListener) + if (listener is HandoffReaderListener) { connectHandoffReader(reader, HandoffConnectionConfiguration(), listener) - else connectHandoffReader(reader, HandoffConnectionConfiguration()) + } else { + connectHandoffReader(reader, HandoffConnectionConfiguration()) + } } DiscoveryMethod.USB -> { - val connConfig = UsbConnectionConfiguration(locationId, autoReconnectOnUnexpectedDisconnect, reconnectionListener) + val connConfig = UsbConnectionConfiguration( + locationId, + autoReconnectOnUnexpectedDisconnect, + reconnectionListener + ) if (listener is ReaderListener) { connectUsbReader(reader, connConfig, listener) } else { @@ -125,6 +142,6 @@ suspend fun Terminal.connectReader( } } else -> { - throw IllegalArgumentException("Unsupported discovery method: ${discoveryMethod}") + throw IllegalArgumentException("Unsupported discovery method: $discoveryMethod") } } diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt index f3c1bd00..6422aeb6 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt @@ -25,7 +25,7 @@ import com.stripeterminalreactnative.putError class RNBluetoothReaderListener( private val context: ReactApplicationContext, - private val onStartInstallingUpdate: (cancelable: Cancelable?) -> Unit, + private val onStartInstallingUpdate: (cancelable: Cancelable?) -> Unit ) : ReaderListener { override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { context.sendEvent(REPORT_AVAILABLE_UPDATE.listenerName) { @@ -45,9 +45,12 @@ class RNBluetoothReaderListener( override fun onReportReaderSoftwareUpdateProgress(progress: Float) { context.sendEvent(REPORT_UPDATE_PROGRESS.listenerName) { - putMap("result", nativeMapOf { - putString("progress", progress.toString()) - }) + putMap( + "result", + nativeMapOf { + putString("progress", progress.toString()) + } + ) } } diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNDiscoveryListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNDiscoveryListener.kt index 0dca85ef..b0723d09 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNDiscoveryListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNDiscoveryListener.kt @@ -17,7 +17,7 @@ internal class RNDiscoveryListener( private val context: ReactApplicationContext, promise: Promise, private val onDiscoveredReaders: (readers: List) -> Unit, - private val onComplete: () -> Unit, + private val onComplete: () -> Unit ) : DiscoveryListener, Callback { // Our no-op callback handles resolving the promise. diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNOfflineListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNOfflineListener.kt index 07f15191..75053cb1 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNOfflineListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNOfflineListener.kt @@ -15,8 +15,8 @@ import com.stripeterminalreactnative.nativeMapOf @OptIn(OfflineMode::class) class RNOfflineListener( - private val context: ReactApplicationContext, -): OfflineListener { + private val context: ReactApplicationContext +) : OfflineListener { override fun onOfflineStatusChange(offlineStatus: OfflineStatus) { context.sendEvent(ReactNativeConstants.CHANGE_OFFLINE_STATUS.listenerName) { putMap("result", mapFromOfflineStatus(offlineStatus)) @@ -27,10 +27,13 @@ class RNOfflineListener( context.sendEvent(ReactNativeConstants.FORWARD_PAYMENT_INTENT.listenerName) { putMap("result", mapFromPaymentIntent(paymentIntent, "")) if (e != null) { - putMap("error", nativeMapOf { - putString("code", e.errorCode.toString()) - putString("message", e.errorMessage) - }) + putMap( + "error", + nativeMapOf { + putString("code", e.errorCode.toString()) + putString("message", e.errorMessage) + } + ) } } } diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListener.kt index b1721b41..14d4e32b 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListener.kt @@ -6,21 +6,26 @@ import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.TerminalException.TerminalErrorCode import com.stripeterminalreactnative.ReactExtensions.sendEvent -import com.stripeterminalreactnative.ReactNativeConstants.* +import com.stripeterminalreactnative.ReactNativeConstants.READER_RECONNECT_FAIL +import com.stripeterminalreactnative.ReactNativeConstants.READER_RECONNECT_SUCCEED +import com.stripeterminalreactnative.ReactNativeConstants.START_READER_RECONNECT import com.stripeterminalreactnative.mapFromReader import com.stripeterminalreactnative.nativeMapOf class RNReaderReconnectionListener( private val context: ReactApplicationContext, - private val onReaderReconnectStarted: (cancelable: Cancelable?) -> Unit, + private val onReaderReconnectStarted: (cancelable: Cancelable?) -> Unit ) : ReaderReconnectionListener { override fun onReaderReconnectFailed(reader: Reader) { context.sendEvent(READER_RECONNECT_FAIL.listenerName) { - putMap("error", nativeMapOf { - putString("code", TerminalErrorCode.UNEXPECTED_SDK_ERROR.toString()) - putString("message", "Reader reconnect fail") - }) + putMap( + "error", + nativeMapOf { + putString("code", TerminalErrorCode.UNEXPECTED_SDK_ERROR.toString()) + putString("message", "Reader reconnect fail") + } + ) } } diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt index 1c4b2c1d..fc729bc9 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt @@ -17,10 +17,13 @@ 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") - }) + putMap( + "error", + nativeMapOf { + putString("code", TerminalErrorCode.UNEXPECTED_SDK_ERROR.toString()) + putString("message", "Reader has been disconnected unexpectedly") + } + ) } } diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt index 00daa13a..37d070ea 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt @@ -25,8 +25,8 @@ import com.stripeterminalreactnative.putError class RNUsbReaderListener( private val context: ReactApplicationContext, - private val onStartInstallingUpdate: (cancelable: Cancelable?) -> Unit, -): ReaderListener { + private val onStartInstallingUpdate: (cancelable: Cancelable?) -> Unit +) : ReaderListener { override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { context.sendEvent(REPORT_AVAILABLE_UPDATE.listenerName) { putMap("result", mapFromReaderSoftwareUpdate(update)) @@ -45,9 +45,12 @@ class RNUsbReaderListener( override fun onReportReaderSoftwareUpdateProgress(progress: Float) { context.sendEvent(REPORT_UPDATE_PROGRESS.listenerName) { - putMap("result", nativeMapOf { - putString("progress", progress.toString()) - }) + putMap( + "result", + nativeMapOf { + putString("progress", progress.toString()) + } + ) } } diff --git a/dev-app/src/App.tsx b/dev-app/src/App.tsx index d8790042..1b534dbf 100644 --- a/dev-app/src/App.tsx +++ b/dev-app/src/App.tsx @@ -27,6 +27,9 @@ import MerchantSelectScreen from './screens/MerchantSelectScreen'; import LogListScreen from './screens/LogListScreen'; import LogScreen from './screens/LogScreen'; import RegisterInternetReaderScreen from './screens/RegisterInternetReaderScreen'; +import DatabaseScreen from './screens/DatabaseScreen'; +import ReaderSettingsScreen from './screens/ReaderSettingsScreen'; +import CollectInputsScreen from './screens/CollectInputsScreen'; import { Reader, Location, @@ -36,8 +39,6 @@ import { import { Alert, LogBox } from 'react-native'; import { AppContext } from './AppContext'; -import DatabaseScreen from './screens/DatabaseScreen'; -import ReaderSettingsScreen from './screens/ReaderSettingsScreen'; export type RouteParamList = { UpdateReader: { @@ -282,6 +283,13 @@ export default function App() { }} component={SetupIntentScreen} /> + ({ diff --git a/dev-app/src/screens/CollectInputsScreen.tsx b/dev-app/src/screens/CollectInputsScreen.tsx new file mode 100644 index 00000000..620119e0 --- /dev/null +++ b/dev-app/src/screens/CollectInputsScreen.tsx @@ -0,0 +1,157 @@ +import React, { useContext } from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import List from '../components/List'; +import ListItem from '../components/ListItem'; +import { + CollectInputsParameters, + SelectionButtonStyle, + useStripeTerminal, +} from '@stripe/stripe-terminal-react-native'; +import { colors } from '../colors'; +import { LogContext } from '../components/LogContext'; +import { useNavigation } from '@react-navigation/native'; + +export default function CollectInputsScreen() { + const { collectInputs, cancelCollectInputs } = useStripeTerminal(); + const { addLogs, clearLogs, setCancel } = useContext(LogContext); + const navigation = useNavigation(); + + const _collectInputs = async (params: CollectInputsParameters) => { + clearLogs(); + setCancel({ + label: 'Cancel CollectInput', + isDisabled: false, + action: cancelCollectInputs, + }); + navigation.navigate('LogListScreen'); + addLogs({ + name: 'Collect Inputs', + events: [ + { + name: 'Initiate', + description: 'terminal.collectInputs', + onBack: cancelCollectInputs, + }, + ], + }); + + const response = await collectInputs(params); + + if (response.error) { + addLogs({ + name: 'Collect Inputs', + events: [ + { + name: 'Failed', + description: 'terminal.collectInputs', + metadata: { + errorCode: response.error?.code, + errorMessage: response.error?.message, + }, + }, + ], + }); + return; + } + + addLogs({ + name: 'Collect Inputs', + events: [ + { + name: 'Succeeded', + description: 'terminal.collectInputs', + metadata: { + COLLECTINPUTS: JSON.stringify(response.collectInputResults), + }, + }, + ], + }); + }; + + return ( + + + { + _collectInputs({ + collectInputs: [ + { + inputType: 'SIGNATURE', + title: 'Please sign', + required: false, + description: + 'Please sign if you agree to the terms and conditions', + submitButtonText: 'submit signature', + }, + { + inputType: 'SELECTION', + title: 'Choose an option', + required: false, + description: 'Were you happy with customer service?', + selectionButtons: [ + { style: SelectionButtonStyle.PRIMARY, text: 'Yes' }, + { style: SelectionButtonStyle.SECONDARY, text: 'No' }, + ], + }, + ], + }); + }} + /> + { + _collectInputs({ + collectInputs: [ + { + inputType: 'TEXT', + title: 'Enter your name', + required: false, + description: "We'll need your name to look up your account", + submitButtonText: 'Done', + }, + { + inputType: 'NUMERIC', + title: 'Enter your zip code', + required: false, + description: '', + submitButtonText: 'Done', + }, + { + inputType: 'EMAIL', + title: 'Enter your email address', + required: false, + description: + "We'll send you updates on your order and occasional deals", + submitButtonText: 'Done', + }, + { + inputType: 'PHONE', + title: 'Enter your phone number', + required: false, + description: "We'll text you when your order is ready", + submitButtonText: 'Done', + }, + ], + }); + }} + /> + + + ); +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: colors.light_gray, + flex: 1, + paddingVertical: 22, + }, +}); diff --git a/dev-app/src/screens/HomeScreen.tsx b/dev-app/src/screens/HomeScreen.tsx index 3ca80df2..a21e7d9c 100644 --- a/dev-app/src/screens/HomeScreen.tsx +++ b/dev-app/src/screens/HomeScreen.tsx @@ -170,6 +170,15 @@ export default function HomeScreen() { }); }} /> + { + navigation.navigate('CollectInputsScreen', { + simulated, + discoveryMethod, + }); + }} + /> [NSDictionary] { var readersList: [NSDictionary] = [] - + for reader in readers { let result = mapFromReader(reader) readersList.append(result) } - + return readersList } - + class func mapFromReader(_ reader: Reader) -> NSDictionary { let result: NSDictionary = [ "label": reader.label ?? NSNull(), @@ -32,7 +32,7 @@ class Mappers { ] return result } - + class func mapFromLocationStatus(_ status: LocationStatus) -> String { switch status { case LocationStatus.notSet: return "notSet" @@ -41,7 +41,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromReaderNetworkStatus(_ status: ReaderNetworkStatus) -> String { switch status { case ReaderNetworkStatus.offline: return "offline" @@ -49,7 +49,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromBatteryStatus(_ status: BatteryStatus) -> String { switch status { case BatteryStatus.critical: return "critical" @@ -59,7 +59,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromDeviceType(_ type: DeviceType) -> String { switch type { case DeviceType.chipper1X: return "chipper1X" @@ -75,12 +75,12 @@ class Mappers { default: return "unknown" } } - + class func mapToCartLineItem(_ cartLineItem: NSDictionary) -> CartLineItem? { guard let displayName = cartLineItem["displayName"] as? String else { return nil } guard let quantity = cartLineItem["quantity"] as? NSNumber else { return nil } guard let amount = cartLineItem["amount"] as? NSNumber else { return nil } - + do { let lineItem = try CartLineItemBuilder(displayName: displayName) .setQuantity(Int(truncating: quantity)) @@ -92,10 +92,10 @@ class Mappers { return nil } } - + class func mapToCartLineItems(_ cartLineItems: NSArray) -> [CartLineItem] { var items = [CartLineItem]() - + cartLineItems.forEach { if let item = $0 as? NSDictionary { if let lineItem = Mappers.mapToCartLineItem(item) { @@ -105,8 +105,8 @@ class Mappers { } return items } - - + + class func mapToDiscoveryMethod(_ discoveryMethod: String?) -> DiscoveryMethod { if let method = discoveryMethod { switch method { @@ -119,7 +119,7 @@ class Mappers { } return DiscoveryMethod.internet } - + class func mapToDiscoveryConfiguration(_ discoveryMethod: String?, simulated: Bool) throws-> DiscoveryConfiguration { switch discoveryMethod { case "bluetoothScan": @@ -135,8 +135,8 @@ class Mappers { return try BluetoothScanDiscoveryConfigurationBuilder().setSimulated(simulated).build() } } - - + + class func mapFromPaymentIntent(_ paymentIntent: PaymentIntent, uuid: String) -> NSDictionary { let result: NSDictionary = [ "amount": paymentIntent.amount, @@ -150,7 +150,7 @@ class Mappers { ] return result } - + class func mapFromSetupIntent(_ setupIntent: SetupIntent, uuid: String) -> NSDictionary { let result: NSDictionary = [ "id": setupIntent.stripeId, @@ -162,7 +162,7 @@ class Mappers { ] return result } - + class func mapFromSetupIntentUsage(_ usage: SetupIntentUsage) -> String { switch usage { case SetupIntentUsage.offSession: return "offSession" @@ -170,7 +170,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromSetupAttempt(_ attempt: SetupAttempt?) -> NSDictionary? { guard let unwrappedAttempt = attempt else { return nil @@ -188,7 +188,7 @@ class Mappers { ] return result } - + class func mapFromSetupAttemptPaymentMethodDetails(_ details: SetupAttemptPaymentMethodDetails?) -> NSDictionary? { guard let unwrappedDetails = details else { return nil @@ -200,7 +200,7 @@ class Mappers { ] return result } - + class func mapFromSetupAttemptCardPresentDetails(_ details: SetupAttemptCardPresentDetails?) -> NSDictionary? { guard let unwrappedDetails = details else { return nil @@ -211,8 +211,8 @@ class Mappers { ] return result } - - + + class func mapFromSetupIntentStatus(_ status: SetupIntentStatus) -> String { switch status { case SetupIntentStatus.canceled: return "canceled" @@ -224,7 +224,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromPaymentIntentStatus(_ status: PaymentIntentStatus) -> String { switch status { case PaymentIntentStatus.canceled: return "canceled" @@ -236,7 +236,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromChargeStatus(_ status: ChargeStatus) -> String { switch status { case ChargeStatus.failed: return "failed" @@ -245,7 +245,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromReaderDisplayMessage(_ displayMessage: ReaderDisplayMessage) -> String { switch displayMessage { case ReaderDisplayMessage.insertCard: return "insertCard" @@ -260,11 +260,11 @@ class Mappers { default: return "unknown" } } - + class func mapFromReaderInputOptions(_ inputOptions: ReaderInputOptions) -> NSMutableArray { let array = inputOptions.rawValue.bitComponents() let mappedOptions: NSMutableArray = [] - + array.forEach { item in switch item { case 0: return @@ -274,21 +274,21 @@ class Mappers { default: return } } - + return mappedOptions } - + class func mapFromCharges(_ charges: [Charge]) -> [NSDictionary] { var list: [NSDictionary] = [] - + for charge in charges { let result = mapFromCharge(charge) list.append(result) } - + return list } - + class func mapFromReaderSoftwareUpdate(_ update: ReaderSoftwareUpdate?) -> [AnyHashable:Any?]? { guard let unwrappedUpdate = update else { return nil @@ -300,7 +300,7 @@ class Mappers { ] return result } - + class func mapFromUpdateTimeEstimate(_ time: UpdateTimeEstimate) -> String { switch time { case UpdateTimeEstimate.estimate1To2Minutes: return "estimate1To2Minutes" @@ -310,21 +310,21 @@ class Mappers { default: return "unknown" } } - - + + class func mapFromLocationsList(_ locations: [Location]) -> [NSDictionary] { var list: [NSDictionary] = [] - + for location in locations { let result = mapFromLocation(location) if let result = result { list.append(result) } } - + return list } - + class func mapFromLocation(_ location: Location?) -> NSDictionary? { guard let unwrappedLocation = location else { return nil @@ -337,7 +337,7 @@ class Mappers { ] return result } - + class func mapFromAddress(_ address: Address?) -> NSDictionary? { if let address = address { let result: NSDictionary = [ @@ -353,13 +353,13 @@ class Mappers { return nil } } - + class func mapFromCharge(_ charge: Charge) -> NSDictionary { var paymentMethodDetailsMap: NSDictionary? if let paymentMethodDetails = charge.paymentMethodDetails { paymentMethodDetailsMap = mapFromPaymentMethodDetails(paymentMethodDetails) } - + let result: NSDictionary = [ "amount": charge.amount, "description": charge.stripeDescription ?? NSNull(), @@ -371,7 +371,7 @@ class Mappers { ] return result } - + class func convertDateToUnixTimestamp(date: Date?) -> String? { if let date = date { let value = date.timeIntervalSince1970 * 1000.0 @@ -379,7 +379,7 @@ class Mappers { } return nil } - + class func mapToSimulateReaderUpdate(_ update: String) -> SimulateReaderUpdate { switch update { case "available": return SimulateReaderUpdate.available @@ -390,7 +390,7 @@ class Mappers { default: return SimulateReaderUpdate.none } } - + class func mapFromCardPresent(_ cardPresent: CardPresentDetails) -> NSDictionary { var receiptDetailsMap: NSDictionary? if let receiptDetails = cardPresent.receipt { @@ -420,14 +420,14 @@ class Mappers { ] return result } - + class func mapFromCardPresentDetailsWallet(_ wallet: SCPWallet) -> NSDictionary { let result: NSDictionary = [ "type": wallet.type ] return result } - + class func mapFromCardPresentDetailsFunding(_ type: CardFundingType) -> String { switch type { case CardFundingType.debit: return "debit" @@ -436,7 +436,7 @@ class Mappers { default: return "other" } } - + class func mapFromCardPresentDetailsBrand(_ type: CardBrand) -> String { switch type { case CardBrand.visa: return "visa" @@ -451,7 +451,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromReceiptDetails(_ receiptDetails: ReceiptDetails) -> NSDictionary { let result: NSDictionary = [ "accountType": receiptDetails.accountType, @@ -465,7 +465,7 @@ class Mappers { ] return result } - + class func mapFromPaymentMethodDetailsType(_ type: PaymentMethodType) -> String { switch type { case PaymentMethodType.card: return "card" @@ -474,7 +474,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromPaymentMethodDetails(_ paymentMethodDetails: PaymentMethodDetails) -> NSDictionary { var cardPresentMapped: NSDictionary? if let cardPresent = paymentMethodDetails.cardPresent{ @@ -484,7 +484,7 @@ class Mappers { if let interacPresent = paymentMethodDetails.interacPresent{ interacPresentMapped = mapFromCardPresent(interacPresent) } - + let result: NSDictionary = [ "type": mapFromPaymentMethodDetailsType(paymentMethodDetails.type), "cardPresentDetails": cardPresentMapped ?? NSNull(), @@ -492,7 +492,7 @@ class Mappers { ] return result } - + class func mapFromRefund(_ refund: Refund) -> NSDictionary { var paymentMethodDetailsMapped: NSDictionary? if let paymentMethodDetails = refund.paymentMethodDetails{ @@ -512,7 +512,7 @@ class Mappers { ] return result } - + class func mapFromRefundStatus(_ type: RefundStatus) -> String { switch type { case RefundStatus.failed: return "failed" @@ -522,7 +522,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromCardDetails(_ cardDetails: CardDetails) -> NSDictionary { let result: NSDictionary = [ "brand": cardDetails.brand, @@ -534,7 +534,7 @@ class Mappers { ] return result } - + class func mapFromPaymentMethod(_ paymentMethod: PaymentMethod) -> NSDictionary { let result: NSDictionary = [ "id": paymentMethod.stripeId, @@ -545,7 +545,7 @@ class Mappers { ] return result } - + class func mapFromPaymentStatus(_ paymentStatus: PaymentStatus) -> String { switch paymentStatus { case PaymentStatus.notReady: return "notReady" @@ -555,7 +555,7 @@ class Mappers { default: return "unknown" } } - + class func mapFromConnectionStatus(_ connectionStatus: ConnectionStatus) -> String { switch connectionStatus { case ConnectionStatus.connected: return "connected" @@ -564,7 +564,7 @@ class Mappers { default: return "unknown" } } - + class func mapToLogLevel(_ logLevel: String?) -> LogLevel { switch logLevel { case "none": return LogLevel.none @@ -572,7 +572,7 @@ class Mappers { default: return LogLevel.none } } - + class func mapFromNetworkStatus(_ status: NetworkStatus) -> String { switch status { case NetworkStatus.online: return "online" @@ -581,14 +581,14 @@ class Mappers { default: return "unknown" } } - + class func mapFromOfflineStatus(_ offlineStatus: OfflineStatus) -> NSDictionary { let sdkDict: NSDictionary = [ "networkStatus": Mappers.mapFromNetworkStatus(offlineStatus.sdk.networkStatus), "offlinePaymentsCount": offlineStatus.sdk.paymentsCount ?? 0, "offlinePaymentAmountsByCurrency": offlineStatus.sdk.paymentAmountsByCurrency ] - + var readerDict: NSDictionary = [:] if let reader = offlineStatus.reader { readerDict = [ @@ -597,7 +597,7 @@ class Mappers { "offlinePaymentAmountsByCurrency": reader.paymentAmountsByCurrency ] } - + return(["sdk": sdkDict, "reader": readerDict]) } @@ -638,6 +638,39 @@ class Mappers { default: return "unknown" } } + + class func mapFromCollectInputs(_ results: [CollectInputsResult]) -> NSDictionary { + var collectInputResults: [String : Any] = [:] + for result in results { + if result is EmailResult { + let result = result as! EmailResult + var emailResult: NSDictionary = ["skipped": result.skipped, "email": result.email ?? ""] + collectInputResults["emailResult"] = emailResult + } else if result is PhoneResult { + let result = result as! PhoneResult + var phoneResult: NSDictionary = ["skipped": result.skipped, "phone": result.phone ?? ""] + collectInputResults["phoneResult"] = phoneResult + } else if result is TextResult { + let result = result as! TextResult + var textResult: NSDictionary = ["skipped": result.skipped, "text": result.text ?? ""] + collectInputResults["textResult"] = textResult + } else if result is NumericResult { + let result = result as! NumericResult + var numericResult: NSDictionary = ["skipped": result.skipped, "numericString": result.numericString ?? ""] + collectInputResults["numericResult"] = numericResult + } else if result is SignatureResult { + let result = result as! SignatureResult + var signatureResult: NSDictionary = ["skipped": result.skipped, "signatureSvg": result.signatureSvg ?? ""] + collectInputResults["signatureResult"] = signatureResult + } else if result is SelectionResult { + let result = result as! SelectionResult + var selectionResult: NSDictionary = ["skipped": result.skipped, "selection": result.selection ?? ""] + collectInputResults["selectionResult"] = selectionResult + } + } + + return (["collectInputResults": collectInputResults]) + } } extension UInt { diff --git a/ios/StripeTerminalReactNative.m b/ios/StripeTerminalReactNative.m index 66409883..01f83ce8 100644 --- a/ios/StripeTerminalReactNative.m +++ b/ios/StripeTerminalReactNative.m @@ -185,17 +185,18 @@ @interface RCT_EXTERN_MODULE(StripeTerminalReactNative, RCTEventEmitter) ) RCT_EXTERN_METHOD( - cancelReadReusableCard:(RCTPromiseResolveBlock)resolve + getLoggingToken:(RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) RCT_EXTERN_METHOD( - getLoggingToken:(RCTPromiseResolveBlock)resolve + getOfflineStatus:(RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) RCT_EXTERN_METHOD( - getOfflineStatus:(RCTPromiseResolveBlock)resolve + collectInputs:(NSDictionary *)params + resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) @@ -210,4 +211,9 @@ @interface RCT_EXTERN_MODULE(StripeTerminalReactNative, RCTEventEmitter) rejecter: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + cancelCollectInputs:(RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) + @end diff --git a/ios/StripeTerminalReactNative.swift b/ios/StripeTerminalReactNative.swift index 0400b335..bd42ac18 100644 --- a/ios/StripeTerminalReactNative.swift +++ b/ios/StripeTerminalReactNative.swift @@ -52,6 +52,7 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe var installUpdateCancelable: Cancelable? = nil var readReusableCardCancelable: Cancelable? = nil var cancelReaderConnectionCancellable: Cancelable? = nil + var collectInputsCancellable: Cancelable? = nil var loggingToken: String? = nil func terminal(_ terminal: Terminal, didUpdateDiscoveredReaders readers: [Reader]) { @@ -947,6 +948,163 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe resolve(result) } + + @objc(collectInputs:resolver:rejecter:) + func collectInputs(_ params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + let invalidParams = Errors.validateRequiredParameters(params: params, requiredParams: ["collectInputs"]) + + guard invalidParams == nil else { + resolve(Errors.createError(code: CommonErrorType.InvalidRequiredParameter, message: "You must provide \(invalidParams!) parameters.")) + return + } + + let collectInputsParameters: CollectInputsParameters + + var inputs: [Input] = [] + let collectInputs = params["collectInputs"] as? [NSDictionary] + if let collectInputs = collectInputs { + for collectInput in collectInputs { + let inputType = collectInput["inputType"] as? String ?? "" + switch (inputType) { + case "EMAIL": + do { + let input = try EmailInputBuilder(title: collectInput["title"] as! String) + .setRequired(collectInput["required"] as? Bool ?? false) + .setStripeDescription(collectInput["description"] as? String ?? "") + .setSkipButtonText(collectInput["skipButtonText"] as? String ?? "") + .setSubmitButtonText(collectInput["submitButtonText"] as? String ?? "") + .build() + inputs.append(input) + } catch { + resolve(Errors.createError(nsError: error as NSError)) + return + } + break + case "NUMERIC": + do { + let input = try NumericInputBuilder(title: collectInput["title"] as! String) + .setRequired(collectInput["required"] as? Bool ?? false) + .setStripeDescription(collectInput["description"] as? String ?? "") + .setSkipButtonText(collectInput["skipButtonText"] as? String ?? "") + .setSubmitButtonText(collectInput["submitButtonText"] as? String ?? "") + .build() + inputs.append(input) + } catch { + resolve(Errors.createError(nsError: error as NSError)) + return + } + break + case "PHONE": + do { + let input = try PhoneInputBuilder(title: collectInput["title"] as! String) + .setRequired(collectInput["required"] as? Bool ?? false) + .setStripeDescription(collectInput["description"] as? String ?? "") + .setSkipButtonText(collectInput["skipButtonText"] as? String ?? "") + .setSubmitButtonText(collectInput["submitButtonText"] as? String ?? "") + .build() + inputs.append(input) + } catch { + resolve(Errors.createError(nsError: error as NSError)) + return + } + break + case "TEXT": + do { + let input = try TextInputBuilder(title: collectInput["title"] as! String) + .setRequired(collectInput["required"] as? Bool ?? false) + .setStripeDescription(collectInput["description"] as? String ?? "") + .setSkipButtonText(collectInput["skipButtonText"] as? String ?? "") + .setSubmitButtonText(collectInput["submitButtonText"] as? String ?? "") + .build() + inputs.append(input) + } catch { + resolve(Errors.createError(nsError: error as NSError)) + return + } + break + case "SELECTION": + var selectionButtons: [SelectionButton] = [] + let selections = collectInput["selectionButtons"] as? [NSDictionary] + if let selections = selections { + for it in selections { + do { + let style = it["style"] as! String + let text = it["text"] as! String + let button = try SelectionButtonBuilder(style: (style == "PRIMARY") ? .primary : .secondary, + text: text).build() + selectionButtons.append(button) + } catch { + resolve(Errors.createError(nsError: error as NSError)) + return + } + } + } + do { + let input = try SelectionInputBuilder(title: collectInput["title"] as! String) + .setRequired(collectInput["required"] as? Bool ?? false) + .setStripeDescription(collectInput["description"] as? String ?? "") + .setSkipButtonText(collectInput["skipButtonText"] as? String ?? "") + .setSelectionButtons(selectionButtons) + .build() + inputs.append(input) + } catch { + resolve(Errors.createError(nsError: error as NSError)) + return + } + break + case "SIGNATURE": + do { + let input = try SignatureInputBuilder(title: collectInput["title"] as! String) + .setRequired(collectInput["required"] as? Bool ?? false) + .setStripeDescription(collectInput["description"] as? String ?? "") + .setSkipButtonText(collectInput["skipButtonText"] as? String ?? "") + .setSubmitButtonText(collectInput["submitButtonText"] as? String ?? "") + .build() + inputs.append(input) + } catch { + resolve(Errors.createError(nsError: error as NSError)) + return + } + break + default: break + } + } + } + + do { + collectInputsParameters = try CollectInputsParametersBuilder(inputs: inputs).build() + } catch { + resolve(Errors.createError(nsError: error as NSError)) + return + } + + DispatchQueue.main.async { + self.collectInputsCancellable = Terminal.shared.collectInputs(collectInputsParameters) { collectInputResults, error in + if let error = error as NSError? { + resolve(Errors.createError(nsError: error)) + } else { + resolve(Mappers.mapFromCollectInputs(collectInputResults ?? [])) + } + } + } + } + + @objc(cancelCollectInputs:rejecter:) + func cancelCollectInputs(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + guard let cancelable = collectInputsCancellable else { + resolve(Errors.createError(code: ErrorCode.cancelFailedAlreadyCompleted, message: "collectInputsCancellable could not be canceled because the command has already been canceled or has completed.")) + return + } + cancelable.cancel() { error in + if let error = error as NSError? { + resolve(Errors.createError(nsError: error)) + } + else { + resolve([:]) + } + self.collectInputsCancellable = nil + } + } @objc(getReaderSettings:rejecter:) func getReaderSettings(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { diff --git a/src/StripeTerminalSdk.tsx b/src/StripeTerminalSdk.tsx index c8a4efa1..03542c8c 100644 --- a/src/StripeTerminalSdk.tsx +++ b/src/StripeTerminalSdk.tsx @@ -31,6 +31,8 @@ import type { PaymentIntent, SetupIntent, OfflineStatus, + CollectInputsParameters, + CollectInputsResults, } from './types'; const { StripeTerminalReactNative } = NativeModules; @@ -147,6 +149,10 @@ export interface StripeTerminalSdkType { setReaderSettings( params: Reader.ReaderSettingsParameters ): Promise; + collectInputs(params: CollectInputsParameters): Promise; + cancelCollectInputs(): Promise<{ + error?: StripeError; + }>; } export default StripeTerminalReactNative as StripeTerminalSdkType; diff --git a/src/__tests__/__snapshots__/functions.test.ts.snap b/src/__tests__/__snapshots__/functions.test.ts.snap index b9ba86b8..047aa2e0 100644 --- a/src/__tests__/__snapshots__/functions.test.ts.snap +++ b/src/__tests__/__snapshots__/functions.test.ts.snap @@ -2,6 +2,7 @@ exports[`functions.test.ts Functions snapshot ensure there are no unexpected changes to the functions exports 1`] = ` Object { + "cancelCollectInputs": [Function], "cancelCollectPaymentMethod": [Function], "cancelCollectRefundPaymentMethod": [Function], "cancelCollectSetupIntent": [Function], @@ -11,6 +12,7 @@ Object { "cancelSetupIntent": [Function], "clearCachedCredentials": [Function], "clearReaderDisplay": [Function], + "collectInputs": [Function], "collectPaymentMethod": [Function], "collectRefundPaymentMethod": [Function], "collectSetupIntentPaymentMethod": [Function], diff --git a/src/__tests__/__snapshots__/index.test.tsx.snap b/src/__tests__/__snapshots__/index.test.tsx.snap index bcd3bcdb..6768ddcb 100644 --- a/src/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/__tests__/__snapshots__/index.test.tsx.snap @@ -37,12 +37,25 @@ Object { }, "START_INSTALLING_UPDATE": "START_INSTALLING_UPDATE", "START_READER_RECONNECT": undefined, + "SelectionButtonStyle": Object { + "PRIMARY": "PRIMARY", + "SECONDARY": "CanSECONDARYceled", + }, "SetupIntent": Object { "Android": Object {}, "IOS": Object {}, }, "StripeTerminalProvider": [Function], "StripeTerminalProviderProps": undefined, + "ToggleResult": Object { + "DISABLED": "DISABLED", + "ENABLED": "ENABLED", + "SKIPPED": "SKIPPED", + }, + "ToggleValue": Object { + "DISABLED": "DISABLED", + "ENABLED": "ENABLED", + }, "UPDATE_DISCOVERED_READERS": "UPDATE_DISCOVERED_READERS", "UseStripeTerminalProps": undefined, "WithStripeTerminalProps": undefined, diff --git a/src/functions.ts b/src/functions.ts index 747462ee..a6dfe16d 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -32,6 +32,8 @@ import type { PaymentIntent, SetupIntent, OfflineStatus, + CollectInputsParameters, + CollectInputsResults, } from './types'; export async function initialize( @@ -799,3 +801,33 @@ export async function setReaderSettings( } }, 'setReaderSettings')(); } + +export async function collectInputs( + params: CollectInputsParameters +): Promise { + return Logger.traceSdkMethod(async () => { + try { + const response = await StripeTerminalSdk.collectInputs(params); + return response; + } catch (error) { + return { + error: error as any, + }; + } + }, 'collectInputs')(); +} + +export async function cancelCollectInputs(): Promise<{ + error?: StripeError; +}> { + return Logger.traceSdkMethod(async () => { + try { + await StripeTerminalSdk.cancelCollectInputs(); + return {}; + } catch (error) { + return { + error: error as any, + }; + } + }, 'cancelCollectInputs')(); +} diff --git a/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap b/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap index 79e6f770..137f33aa 100644 --- a/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap +++ b/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap @@ -3,6 +3,7 @@ exports[`useStripeTerminal.test.tsx Public API snapshot ensure there are no unexpected changes to the hook exports 1`] = ` Object { "current": Object { + "cancelCollectInputs": [Function], "cancelCollectPaymentMethod": [Function], "cancelCollectRefundPaymentMethod": [Function], "cancelCollectSetupIntent": [Function], @@ -12,6 +13,7 @@ Object { "cancelSetupIntent": [Function], "clearCachedCredentials": [Function], "clearReaderDisplay": [Function], + "collectInputs": [Function], "collectPaymentMethod": [Function], "collectRefundPaymentMethod": [Function], "collectSetupIntentPaymentMethod": [Function], diff --git a/src/hooks/useStripeTerminal.tsx b/src/hooks/useStripeTerminal.tsx index 11473c4e..d3e63a41 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -21,6 +21,7 @@ import type { PaymentIntent, SetupIntent, OfflineStatus, + CollectInputsParameters, } from '../types'; import { discoverReaders, @@ -58,6 +59,8 @@ import { getOfflineStatus, getReaderSettings, setReaderSettings, + collectInputs, + cancelCollectInputs, } from '../functions'; import { StripeTerminalContext } from '../components/StripeTerminalContext'; import { useListener } from './useListener'; @@ -895,6 +898,37 @@ export function useStripeTerminal(props?: Props) { [_isInitialized] ); + const _collectInputs = useCallback( + async (params: CollectInputsParameters) => { + if (!_isInitialized()) { + console.error(NOT_INITIALIZED_ERROR_MESSAGE); + throw Error(NOT_INITIALIZED_ERROR_MESSAGE); + } + setLoading(true); + + const response = await collectInputs(params); + + setLoading(false); + + return response; + }, + [_isInitialized, setLoading] + ); + + const _cancelCollectInputs = useCallback(async () => { + if (!_isInitialized()) { + console.error(NOT_INITIALIZED_ERROR_MESSAGE); + throw Error(NOT_INITIALIZED_ERROR_MESSAGE); + } + setLoading(true); + + const response = await cancelCollectInputs(); + + setLoading(false); + + return response; + }, [_isInitialized, setLoading]); + return { initialize: _initialize, discoverReaders: _discoverReaders, @@ -932,6 +966,8 @@ export function useStripeTerminal(props?: Props) { getOfflineStatus: _getOfflineStatus, getReaderSettings: _getReaderSettings, setReaderSettings: _setReaderSettings, + collectInputs: _collectInputs, + cancelCollectInputs: _cancelCollectInputs, emitter: emitter, discoveredReaders, connectedReader, diff --git a/src/types/index.ts b/src/types/index.ts index 1c8d838f..dcac6fbf 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -388,3 +388,61 @@ export type PaymentMethodResultType = paymentMethod: undefined; error: StripeError; }; + +export type CollectInputsParameters = { + collectInputs: Input[]; +}; + +export type CollectInputsResults = { + collectInputResults?: CollectInputResult[]; + error: StripeError; +}; + +export type Input = { + inputType: 'EMAIL' | 'NUMERIC' | 'PHONE' | 'TEXT' | 'SELECTION' | 'SIGNATURE'; + description?: string; + required: boolean; + skipButtonText?: string; + submitButtonText?: string; + title: string; + toggles?: Toggle[]; + selectionButtons?: SelectionButton[]; +}; + +export type CollectInputResult = { + skipped: boolean; + email?: string; + numericString?: string; + phone?: string; + selection?: string; + signatureSvg?: string; + text?: string; + toggles?: ToggleResult[]; +}; + +export type Toggle = { + title: string; + description: string; + defaultValue: ToggleValue; +}; + +export enum ToggleValue { + ENABLED = 'ENABLED', + DISABLED = 'DISABLED', +} + +export enum ToggleResult { + ENABLED = 'ENABLED', + DISABLED = 'DISABLED', + SKIPPED = 'SKIPPED', +} + +export type SelectionButton = { + style: SelectionButtonStyle; + text: string; +}; + +export enum SelectionButtonStyle { + PRIMARY = 'PRIMARY', + SECONDARY = 'CanSECONDARYceled', +}