diff --git a/android/build.gradle b/android/build.gradle index f5209277..53e937bd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -28,7 +28,8 @@ def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['StripeTerminalReactNative_' + name]).toInteger() } -def terminalAndroidSdkVersion = '3.10.1' + +def terminalAndroidSdkVersion = '4.0.0' def reactNativeSdkVersion = getVersionFromNpm() android { @@ -140,7 +141,7 @@ dependencies { api 'com.facebook.react:react-android:+' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.stripe:stripeterminal-core:$terminalAndroidSdkVersion" - implementation "com.stripe:stripeterminal-localmobile:$terminalAndroidSdkVersion" + implementation "com.stripe:stripeterminal-taptopay:$terminalAndroidSdkVersion" implementation "com.stripe:stripeterminal-handoffclient:$terminalAndroidSdkVersion" implementation 'com.google.code.gson:gson:2.3.1' implementation 'com.squareup.okhttp3:okhttp:4.9.1' diff --git a/android/src/main/java/com/stripeterminalreactnative/DiscoveryMethod.kt b/android/src/main/java/com/stripeterminalreactnative/DiscoveryMethod.kt index fbb1e71a..db8e4729 100644 --- a/android/src/main/java/com/stripeterminalreactnative/DiscoveryMethod.kt +++ b/android/src/main/java/com/stripeterminalreactnative/DiscoveryMethod.kt @@ -3,7 +3,7 @@ package com.stripeterminalreactnative enum class DiscoveryMethod { BLUETOOTH_SCAN, INTERNET, - LOCAL_MOBILE, + TAP_TO_PAY, HANDOFF, USB } diff --git a/android/src/main/java/com/stripeterminalreactnative/Errors.kt b/android/src/main/java/com/stripeterminalreactnative/Errors.kt index d299547d..93c4bb4f 100644 --- a/android/src/main/java/com/stripeterminalreactnative/Errors.kt +++ b/android/src/main/java/com/stripeterminalreactnative/Errors.kt @@ -3,10 +3,9 @@ package com.stripeterminalreactnative import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap +import com.stripe.stripeterminal.external.models.TerminalErrorCode 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(throwable: Throwable): ReadableMap = nativeMapOf { putError(throwable) } diff --git a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt index 7c2586a5..84549ed0 100644 --- a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt +++ b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt @@ -9,6 +9,7 @@ import com.facebook.react.bridge.WritableNativeArray import com.stripe.stripeterminal.external.CollectInputs import com.stripe.stripeterminal.external.OfflineMode import com.stripe.stripeterminal.external.models.Address +import com.stripe.stripeterminal.external.models.AllowRedisplay import com.stripe.stripeterminal.external.models.AmountDetails import com.stripe.stripeterminal.external.models.BatteryStatus import com.stripe.stripeterminal.external.models.CardDetails @@ -16,13 +17,12 @@ 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.CollectDataType -import com.stripe.stripeterminal.external.models.CollectedData import com.stripe.stripeterminal.external.models.CollectInputsResult +import com.stripe.stripeterminal.external.models.CollectedData 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.LocalMobileUxConfiguration import com.stripe.stripeterminal.external.models.Location import com.stripe.stripeterminal.external.models.LocationStatus import com.stripe.stripeterminal.external.models.NetworkStatus @@ -60,6 +60,7 @@ 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.TapToPayUxConfiguration import com.stripe.stripeterminal.external.models.TextResult import com.stripe.stripeterminal.external.models.ToggleResult import com.stripe.stripeterminal.external.models.Wallet @@ -72,6 +73,9 @@ internal fun getInt(map: ReadableMap, key: String): Int? = internal fun getBoolean(map: ReadableMap?, key: String): Boolean = if (map?.hasKey(key) == true) map.getBoolean(key) else false +internal fun getBoolean(map: ReadableMap?, key: String, defaultValue: Boolean): Boolean = + if (map?.hasKey(key) == true) map.getBoolean(key) else defaultValue + internal fun putDoubleOrNull(mapTarget: WritableMap, key: String, value: Double?) { value?.let { mapTarget.putDouble(key, it) @@ -143,7 +147,6 @@ internal fun mapFromDeviceType(type: DeviceType): String { return when (type) { DeviceType.CHIPPER_1X -> "chipper1X" DeviceType.CHIPPER_2X -> "chipper2X" - DeviceType.COTS_DEVICE -> "cotsDevice" DeviceType.ETNA -> "etna" DeviceType.STRIPE_M2 -> "stripeM2" DeviceType.STRIPE_S700 -> "stripeS700" @@ -157,6 +160,7 @@ internal fun mapFromDeviceType(type: DeviceType): String { DeviceType.WISEPAD_3S -> "wisePad3s" DeviceType.WISEPOS_E -> "wisePosE" DeviceType.WISEPOS_E_DEVKIT -> "wisePosEDevkit" + DeviceType.TAP_TO_PAY_DEVICE -> "tapToPay" } } @@ -164,7 +168,6 @@ internal fun mapToDeviceType(type: String): DeviceType? { return when (type) { "chipper1X" -> DeviceType.CHIPPER_1X "chipper2X" -> DeviceType.CHIPPER_2X - "cotsDevice" -> DeviceType.COTS_DEVICE "etna" -> DeviceType.ETNA "stripeM2" -> DeviceType.STRIPE_M2 "stripeS700" -> DeviceType.STRIPE_S700 @@ -177,6 +180,7 @@ internal fun mapToDeviceType(type: String): DeviceType? { "wisePad3s" -> DeviceType.WISEPAD_3S "wisePosE" -> DeviceType.WISEPOS_E "wisePosEDevkit" -> DeviceType.WISEPOS_E_DEVKIT + "tapToPay" -> DeviceType.TAP_TO_PAY_DEVICE else -> null } } @@ -193,13 +197,21 @@ internal fun mapToDiscoveryMethod(method: String?): DiscoveryMethod? { return when (method) { "bluetoothScan" -> DiscoveryMethod.BLUETOOTH_SCAN "internet" -> DiscoveryMethod.INTERNET - "localMobile" -> DiscoveryMethod.LOCAL_MOBILE + "tapToPay" -> DiscoveryMethod.TAP_TO_PAY "handoff" -> DiscoveryMethod.HANDOFF "usb" -> DiscoveryMethod.USB else -> null } } +internal fun mapToAllowRedisplay(method: String?): AllowRedisplay { + return when (method) { + "always" -> AllowRedisplay.ALWAYS + "limited" -> AllowRedisplay.LIMITED + else -> AllowRedisplay.UNSPECIFIED + } +} + @OptIn(OfflineMode::class) internal fun mapFromPaymentIntent(paymentIntent: PaymentIntent, uuid: String): ReadableMap = nativeMapOf { @@ -449,6 +461,7 @@ internal fun mapFromConnectionStatus(status: ConnectionStatus): String { ConnectionStatus.CONNECTED -> "connected" ConnectionStatus.NOT_CONNECTED -> "notConnected" ConnectionStatus.CONNECTING -> "connecting" + ConnectionStatus.DISCOVERING -> "discovering" } } @@ -492,18 +505,18 @@ internal fun mapFromReaderSoftwareUpdate(update: ReaderSoftwareUpdate?): Writabl putString("deviceSoftwareVersion", it.version) putString( "estimatedUpdateTime", - mapFromUpdateTimeEstimate(it.timeEstimate) + mapFromUpdateTimeEstimate(it.durationEstimate) ) - putString("requiredAt", it.requiredAt.time.toString()) + putString("requiredAt", it.requiredAtMs.toString()) } } -internal fun mapFromUpdateTimeEstimate(time: ReaderSoftwareUpdate.UpdateTimeEstimate): String { +internal fun mapFromUpdateTimeEstimate(time: ReaderSoftwareUpdate.UpdateDurationEstimate): String { return when (time) { - ReaderSoftwareUpdate.UpdateTimeEstimate.FIVE_TO_FIFTEEN_MINUTES -> "estimate5To15Minutes" - ReaderSoftwareUpdate.UpdateTimeEstimate.LESS_THAN_ONE_MINUTE -> "estimateLessThan1Minute" - ReaderSoftwareUpdate.UpdateTimeEstimate.ONE_TO_TWO_MINUTES -> "estimate1To2Minutes" - ReaderSoftwareUpdate.UpdateTimeEstimate.TWO_TO_FIVE_MINUTES -> "estimate2To5Minutes" + ReaderSoftwareUpdate.UpdateDurationEstimate.FIVE_TO_FIFTEEN_MINUTES -> "estimate5To15Minutes" + ReaderSoftwareUpdate.UpdateDurationEstimate.LESS_THAN_ONE_MINUTE -> "estimateLessThan1Minute" + ReaderSoftwareUpdate.UpdateDurationEstimate.ONE_TO_TWO_MINUTES -> "estimate1To2Minutes" + ReaderSoftwareUpdate.UpdateDurationEstimate.TWO_TO_FIVE_MINUTES -> "estimate2To5Minutes" } } @@ -659,7 +672,7 @@ private fun mapFromWechatPayDetails(wechatPayDetails: WechatPayDetails?): Readab private fun mapFromOfflineDetails(offlineDetails: OfflineDetails?): ReadableMap? = offlineDetails?.let { nativeMapOf { - putString("storedAt", offlineDetails.storedAt.toString()) + putString("storedAtMs", offlineDetails.storedAtMs.toString()) putBoolean("requiresUpload", offlineDetails.requiresUpload) putMap( "cardPresentDetails", @@ -963,23 +976,23 @@ fun mapFromCollectDataType(type: String): CollectDataType? { } } -fun mapToTapZoneIndicator(indicator: String?): LocalMobileUxConfiguration.TapZoneIndicator { +fun mapToTapZoneIndicator(indicator: String?): TapToPayUxConfiguration.TapZoneIndicator { return when (indicator) { - "default" -> LocalMobileUxConfiguration.TapZoneIndicator.DEFAULT - "above" -> LocalMobileUxConfiguration.TapZoneIndicator.ABOVE - "below" -> LocalMobileUxConfiguration.TapZoneIndicator.BELOW - "front" -> LocalMobileUxConfiguration.TapZoneIndicator.FRONT - "behind" -> LocalMobileUxConfiguration.TapZoneIndicator.BEHIND - else -> LocalMobileUxConfiguration.TapZoneIndicator.DEFAULT + "default" -> TapToPayUxConfiguration.TapZoneIndicator.DEFAULT + "above" -> TapToPayUxConfiguration.TapZoneIndicator.ABOVE + "below" -> TapToPayUxConfiguration.TapZoneIndicator.BELOW + "front" -> TapToPayUxConfiguration.TapZoneIndicator.FRONT + "behind" -> TapToPayUxConfiguration.TapZoneIndicator.BEHIND + else -> TapToPayUxConfiguration.TapZoneIndicator.DEFAULT } } -fun mapToDarkMode(mode: String?): LocalMobileUxConfiguration.DarkMode { +fun mapToDarkMode(mode: String?): TapToPayUxConfiguration.DarkMode { return when (mode) { - "dark" -> LocalMobileUxConfiguration.DarkMode.DARK - "light" -> LocalMobileUxConfiguration.DarkMode.LIGHT - "system" -> LocalMobileUxConfiguration.DarkMode.SYSTEM - else -> LocalMobileUxConfiguration.DarkMode.LIGHT + "dark" -> TapToPayUxConfiguration.DarkMode.DARK + "light" -> TapToPayUxConfiguration.DarkMode.LIGHT + "system" -> TapToPayUxConfiguration.DarkMode.SYSTEM + else -> TapToPayUxConfiguration.DarkMode.LIGHT } } diff --git a/android/src/main/java/com/stripeterminalreactnative/ReactNativeConstants.kt b/android/src/main/java/com/stripeterminalreactnative/ReactNativeConstants.kt index 4fd7fe6d..49e129cb 100644 --- a/android/src/main/java/com/stripeterminalreactnative/ReactNativeConstants.kt +++ b/android/src/main/java/com/stripeterminalreactnative/ReactNativeConstants.kt @@ -9,7 +9,6 @@ enum class ReactNativeConstants(val listenerName: String) { REQUEST_READER_DISPLAY_MESSAGE("didRequestReaderDisplayMessage"), REQUEST_READER_INPUT("didRequestReaderInput"), REPORT_AVAILABLE_UPDATE("didReportAvailableUpdate"), - REPORT_UNEXPECTED_READER_DISCONNECT("didReportUnexpectedReaderDisconnect"), REPORT_UPDATE_PROGRESS("didReportReaderSoftwareUpdateProgress"), START_INSTALLING_UPDATE("didStartInstallingUpdate"), UPDATE_DISCOVERED_READERS("didUpdateDiscoveredReaders"), diff --git a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt index 5d926aa6..336829d7 100644 --- a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt +++ b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt @@ -18,7 +18,6 @@ import com.stripe.stripeterminal.external.CollectData 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.CaptureMethod import com.stripe.stripeterminal.external.models.CardPresentParameters import com.stripe.stripeterminal.external.models.CardPresentRoutingOptionParameters @@ -27,12 +26,12 @@ import com.stripe.stripeterminal.external.models.CollectConfiguration import com.stripe.stripeterminal.external.models.CollectDataConfiguration import com.stripe.stripeterminal.external.models.CollectInputsParameters import com.stripe.stripeterminal.external.models.ConfirmConfiguration +import com.stripe.stripeterminal.external.models.ConnectionConfiguration 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.LocalMobileUxConfiguration import com.stripe.stripeterminal.external.models.NumericInput import com.stripe.stripeterminal.external.models.OfflineBehavior import com.stripe.stripeterminal.external.models.PaymentIntent @@ -55,32 +54,35 @@ 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.TapToPayUxConfiguration +import com.stripe.stripeterminal.external.models.TerminalErrorCode import com.stripe.stripeterminal.external.models.TerminalException import com.stripe.stripeterminal.external.models.TextInput import com.stripe.stripeterminal.external.models.TippingConfiguration import com.stripe.stripeterminal.external.models.Toggle import com.stripe.stripeterminal.external.models.ToggleValue import com.stripeterminalreactnative.callback.NoOpCallback -import com.stripeterminalreactnative.callback.RNCollectedDataCallback import com.stripeterminalreactnative.callback.RNCollectInputResultCallback +import com.stripeterminalreactnative.callback.RNCollectedDataCallback 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 import com.stripeterminalreactnative.listener.RNHandoffReaderListener +import com.stripeterminalreactnative.listener.RNInternetReaderListener +import com.stripeterminalreactnative.listener.RNMobileReaderListener import com.stripeterminalreactnative.listener.RNOfflineListener +import com.stripeterminalreactnative.listener.RNReaderDisconnectListener import com.stripeterminalreactnative.listener.RNReaderReconnectionListener +import com.stripeterminalreactnative.listener.RNTapToPayReaderListener import com.stripeterminalreactnative.listener.RNTerminalListener -import com.stripeterminalreactnative.listener.RNUsbReaderListener import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.UUID -import kotlin.collections.HashMap class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { @@ -221,18 +223,22 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : getInt(params, "timeout") ?: 0, getBoolean(params, "simulated") ) + DiscoveryMethod.INTERNET -> DiscoveryConfiguration.InternetDiscoveryConfiguration( isSimulated = getBoolean(params, "simulated"), location = locationId ) + DiscoveryMethod.USB -> DiscoveryConfiguration.UsbDiscoveryConfiguration( getInt(params, "timeout") ?: 0, getBoolean(params, "simulated") ) + DiscoveryMethod.HANDOFF -> DiscoveryConfiguration.HandoffDiscoveryConfiguration() - DiscoveryMethod.LOCAL_MOBILE -> DiscoveryConfiguration.LocalMobileDiscoveryConfiguration( + DiscoveryMethod.TAP_TO_PAY -> DiscoveryConfiguration.TapToPayDiscoveryConfiguration( getBoolean(params, "simulated") - ) }, + ) + }, listener, listener ) @@ -246,11 +252,74 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : } } - private fun connectReader( + private fun getConnectionConfig( + discoveryMethod: DiscoveryMethod, + locationId: String, + autoReconnectOnUnexpectedDisconnect: Boolean + ): ConnectionConfiguration { + val disconnectListener = RNReaderDisconnectListener(context) + return when (discoveryMethod) { + DiscoveryMethod.BLUETOOTH_SCAN -> { + val reconnectionListener = RNReaderReconnectionListener(context) { + cancelReaderConnectionCancellable = it + } + val listener = + RNMobileReaderListener(context, reconnectionListener, disconnectListener) { + installUpdateCancelable = it + } + ConnectionConfiguration.BluetoothConnectionConfiguration( + locationId, + autoReconnectOnUnexpectedDisconnect, + listener + ) + } + + DiscoveryMethod.TAP_TO_PAY -> { + val reconnectionListener = RNReaderReconnectionListener(context) { + cancelReaderConnectionCancellable = it + } + val listener = RNTapToPayReaderListener(disconnectListener, reconnectionListener) + ConnectionConfiguration.TapToPayConnectionConfiguration( + locationId, + autoReconnectOnUnexpectedDisconnect, + listener + ) + } + + DiscoveryMethod.INTERNET -> { + val listener = RNInternetReaderListener(disconnectListener) + ConnectionConfiguration.InternetConnectionConfiguration( + internetReaderListener = listener + ) + } + + DiscoveryMethod.HANDOFF -> { + ConnectionConfiguration.HandoffConnectionConfiguration( + RNHandoffReaderListener(context, disconnectListener) + ) + } + + DiscoveryMethod.USB -> { + val reconnectionListener = RNReaderReconnectionListener(context) { + cancelReaderConnectionCancellable = it + } + val listener = + RNMobileReaderListener(context, reconnectionListener, disconnectListener) { + installUpdateCancelable = it + } + ConnectionConfiguration.UsbConnectionConfiguration( + locationId, + autoReconnectOnUnexpectedDisconnect, + listener + ) + } + } + } + + private fun innerConnectReader( params: ReadableMap, - promise: Promise, discoveryMethod: DiscoveryMethod, - listener: ReaderListenable? = null + promise: Promise ) { CoroutineScope(Dispatchers.IO).launch { withSuspendExceptionResolver(promise) { @@ -271,24 +340,25 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val locationId = 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 - } + val autoReconnectOnUnexpectedDisconnect = + when (discoveryMethod) { + DiscoveryMethod.TAP_TO_PAY -> { + getBoolean(params, "autoReconnectOnUnexpectedDisconnect", true) + } + DiscoveryMethod.BLUETOOTH_SCAN, DiscoveryMethod.USB -> { + getBoolean(params, "autoReconnectOnUnexpectedDisconnect") + } + else -> { + false + } + } - val reconnectionListener = RNReaderReconnectionListener(context) { - cancelReaderConnectionCancellable = it - } - val connectedReader = - terminal.connectReader( - discoveryMethod, - selectedReader, - locationId, - autoReconnectOnUnexpectedDisconnect, - listener, - reconnectionListener - ) + val connConfig = getConnectionConfig( + discoveryMethod, + locationId, + autoReconnectOnUnexpectedDisconnect + ) + val connectedReader = terminal.connectReader(selectedReader, connConfig) promise.resolve( nativeMapOf { putMap("reader", mapFromReader(connectedReader)) @@ -300,39 +370,10 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod @Suppress("unused") - fun connectBluetoothReader(params: ReadableMap, promise: Promise) { - val listener = RNBluetoothReaderListener(context) { - installUpdateCancelable = it - } - connectReader(params, promise, DiscoveryMethod.BLUETOOTH_SCAN, listener) - } - - @ReactMethod - @Suppress("unused") - 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) { - connectReader(params, promise, DiscoveryMethod.INTERNET) - } - - @ReactMethod - @Suppress("unused") - fun connectLocalMobileReader(params: ReadableMap, promise: Promise) { - connectReader(params, promise, DiscoveryMethod.LOCAL_MOBILE) - } - - @ReactMethod - @Suppress("unused") - fun connectUsbReader(params: ReadableMap, promise: Promise) { - val listener = RNUsbReaderListener(context) { - installUpdateCancelable = it - } - connectReader(params, promise, DiscoveryMethod.USB, listener) + fun connectReader(params: ReadableMap, discoveryMethod: String, promise: Promise) { + mapToDiscoveryMethod(discoveryMethod)?.let { + innerConnectReader(params, it, promise) + } ?: promise.resolve(createError(RuntimeException("Unknown discovery method: $discoveryMethod"))) } @ReactMethod @@ -525,6 +566,10 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : if (params.hasKey("surchargeNotice")) { configBuilder.setSurchargeNotice(params.getString("surchargeNotice")) } + if (params.hasKey("allowRedisplay")) { + configBuilder.setAllowRedisplay(mapToAllowRedisplay(params.getString("allowRedisplay"))) + } + val config = configBuilder.build() collectPaymentMethodCancelable = terminal.collectPaymentMethod( @@ -654,13 +699,12 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : val setupIntent = requireParam(setupIntents[uuid]) { "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." } - - val customerConsentCollected = getBoolean(params, "customerConsentCollected") + val allowRedisplay = mapToAllowRedisplay(params.getString("allowRedisplay")) val enableCustomerCancellation = getBoolean(params, "enableCustomerCancellation") collectSetupIntentCancelable = terminal.collectSetupIntentPaymentMethod( setupIntent, - customerConsentCollected, + allowRedisplay, SetupIntentConfiguration.Builder() .setEnableCustomerCancellation(enableCustomerCancellation) .build(), @@ -729,7 +773,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : setupIntent, params, RNSetupIntentCallback(promise, uuid) { - setupIntents[setupIntent.id] = null + setupIntents[setupIntent.id.orEmpty()] = null } ) } @@ -751,7 +795,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : terminal.confirmSetupIntent( setupIntent, RNSetupIntentCallback(promise, uuid) { - setupIntents[it.id] = null + setupIntents[it.id.orEmpty()] = null } ) } @@ -771,7 +815,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : if (chargeId.isNullOrBlank() == paymentIntentId.isNullOrBlank()) { throw TerminalException( - TerminalException.TerminalErrorCode.INVALID_REQUIRED_PARAMETER, + TerminalErrorCode.INVALID_REQUIRED_PARAMETER, "You must provide either a charge ID or a payment intent ID." ) } @@ -936,6 +980,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : ) } } + "numeric" -> { collectInput.let { var toggles = ArrayList() @@ -952,6 +997,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : ) } } + "email" -> { collectInput.let { var toggles = ArrayList() @@ -968,6 +1014,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : ) } } + "phone" -> { collectInput.let { var toggles = ArrayList() @@ -984,6 +1031,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : ) } } + "signature" -> { collectInput.let { var toggles = ArrayList() @@ -1000,6 +1048,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : ) } } + "selection" -> { collectInput.let { var toggles = ArrayList() @@ -1072,17 +1121,22 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : getInt(params, "timeout") ?: 0, getBoolean(params, "simulated") ) + DiscoveryMethod.INTERNET -> DiscoveryConfiguration.InternetDiscoveryConfiguration( isSimulated = getBoolean(params, "simulated") ) + DiscoveryMethod.USB -> DiscoveryConfiguration.UsbDiscoveryConfiguration( getInt(params, "timeout") ?: 0, getBoolean(params, "simulated") ) + DiscoveryMethod.HANDOFF -> DiscoveryConfiguration.HandoffDiscoveryConfiguration() - DiscoveryMethod.LOCAL_MOBILE -> DiscoveryConfiguration.LocalMobileDiscoveryConfiguration( + DiscoveryMethod.TAP_TO_PAY -> DiscoveryConfiguration.TapToPayDiscoveryConfiguration( getBoolean(params, "simulated") - ) } + ) + + } ) promise.resolve(mapFromReaderSupportResult(readerSupportResult)) @@ -1090,41 +1144,45 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod @Suppress("unused") - fun setLocalMobileUxConfiguration(params: ReadableMap, promise: Promise) = withExceptionResolver(promise) { - val localMobileUxConfigurationBuilder = LocalMobileUxConfiguration.Builder() + fun setTapToPayUxConfiguration(params: ReadableMap, promise: Promise) = withExceptionResolver(promise) { + val tapToPayUxConfigurationBuilder = TapToPayUxConfiguration.Builder() - var tapZone: LocalMobileUxConfiguration.TapZone? = null + var tapZone: TapToPayUxConfiguration.TapZone? = null val tapZoneParam = params.getMap("tapZone") tapZoneParam?.let { - val tapZoneIndicator = mapToTapZoneIndicator(tapZoneParam.getString("tapZoneIndicator")) + val tapZoneIndicator = + mapToTapZoneIndicator(tapZoneParam.getString("tapZoneIndicator")) val tapZonePosition = tapZoneParam.getMap("tapZonePosition")?.let { - LocalMobileUxConfiguration.TapZonePosition.Manual( + TapToPayUxConfiguration.TapZonePosition.Manual( it.getDouble("xBias").toFloat(), - it.getDouble("yBias").toFloat()) - } ?: LocalMobileUxConfiguration.TapZonePosition.Default + it.getDouble("yBias").toFloat() + ) + } ?: TapToPayUxConfiguration.TapZonePosition.Default - tapZone = LocalMobileUxConfiguration.TapZone.Manual.Builder() + tapZone = TapToPayUxConfiguration.TapZone.Manual.Builder() .indicator(tapZoneIndicator) .position(tapZonePosition) .build() } - localMobileUxConfigurationBuilder.tapZone(tapZone?:LocalMobileUxConfiguration.TapZone.Default) + tapToPayUxConfigurationBuilder.tapZone( + tapZone ?: TapToPayUxConfiguration.TapZone.Default + ) val colorsParam = params.getMap("colors") colorsParam?.let { - val colorSchemeBuilder = LocalMobileUxConfiguration.ColorScheme.Builder() + val colorSchemeBuilder = TapToPayUxConfiguration.ColorScheme.Builder() colorSchemeBuilder.apply { - primary(it.getString("primary").toLocalMobileColor()) - success(it.getString("success").toLocalMobileColor()) - error(it.getString("error").toLocalMobileColor()) + primary(it.getString("primary").toTapToPayColor()) + success(it.getString("success").toTapToPayColor()) + error(it.getString("error").toTapToPayColor()) } - localMobileUxConfigurationBuilder.colors(colorSchemeBuilder.build()) + tapToPayUxConfigurationBuilder.colors(colorSchemeBuilder.build()) } - localMobileUxConfigurationBuilder.darkMode(mapToDarkMode(params.getString("darkMode"))) + tapToPayUxConfigurationBuilder.darkMode(mapToDarkMode(params.getString("darkMode"))) - terminal.setLocalMobileUxConfiguration(localMobileUxConfigurationBuilder.build()) + terminal.setTapToPayUxConfiguration(tapToPayUxConfigurationBuilder.build()) promise.resolve(NativeTypeFactory.writableNativeMap()) } @@ -1134,10 +1192,10 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : promise.resolve(BuildConfig.SDK_VERSION_NAME) } - private fun String?.toLocalMobileColor(): LocalMobileUxConfiguration.Color { + private fun String?.toTapToPayColor(): TapToPayUxConfiguration.Color { return this - ?.let { LocalMobileUxConfiguration.Color.Value(hexToArgb(it)) } - ?: LocalMobileUxConfiguration.Color.Default + ?.let { TapToPayUxConfiguration.Color.Value(hexToArgb(it)) } + ?: TapToPayUxConfiguration.Color.Default } @ReactMethod diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectInputResultCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectInputResultCallback.kt index a2d46698..01f7d571 100644 --- a/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectInputResultCallback.kt +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectInputResultCallback.kt @@ -3,7 +3,8 @@ 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.* +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 diff --git a/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectedDataCallback.kt b/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectedDataCallback.kt index 2ce67a5f..dcac8ac3 100644 --- a/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectedDataCallback.kt +++ b/android/src/main/java/com/stripeterminalreactnative/callback/RNCollectedDataCallback.kt @@ -1,6 +1,5 @@ package com.stripeterminalreactnative.callback -import android.util.Log import com.facebook.react.bridge.Promise import com.stripe.stripeterminal.external.CollectData import com.stripe.stripeterminal.external.callable.CollectedDataCallback @@ -25,4 +24,4 @@ class RNCollectedDataCallback( override fun onFailure(e: TerminalException) { promise.resolve(createError(e)) } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt b/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt index bbbb28d8..93165f74 100644 --- a/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt +++ b/android/src/main/java/com/stripeterminalreactnative/ktx/TerminalExtensions.kt @@ -1,19 +1,10 @@ package com.stripeterminalreactnative.ktx import com.stripe.stripeterminal.Terminal -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.ReaderListener -import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener -import com.stripe.stripeterminal.external.models.ConnectionConfiguration.BluetoothConnectionConfiguration -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.ConnectionConfiguration import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.TerminalException -import com.stripeterminalreactnative.DiscoveryMethod import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -21,61 +12,14 @@ import kotlin.coroutines.resumeWithException // TODO (dhenry): replace this with terminalsdk:ktx when module is publicly available /** - * @see [Terminal.connectBluetoothReader] + * @see [Terminal.connectReader] */ -suspend fun Terminal.connectBluetoothReader( - reader: Reader, - config: BluetoothConnectionConfiguration, - listener: ReaderListener = object : ReaderListener {} -): Reader { - return readerCallbackCoroutine { - connectBluetoothReader(reader, config, listener, 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] - */ -suspend fun Terminal.connectUsbReader( +suspend fun Terminal.connectReader( reader: Reader, - config: UsbConnectionConfiguration, - listener: ReaderListener = object : ReaderListener {} + config: ConnectionConfiguration ): Reader { return readerCallbackCoroutine { - connectUsbReader(reader, config, listener, it) + connectReader(reader, config, it) } } @@ -91,57 +35,4 @@ private suspend inline fun readerCallbackCoroutine(crossinline block: (ReaderCal } }) } -} - -suspend fun Terminal.connectReader( - discoveryMethod: DiscoveryMethod, - reader: Reader, - locationId: String, - autoReconnectOnUnexpectedDisconnect: Boolean = false, - listener: ReaderListenable? = null, - reconnectionListener: ReaderReconnectionListener -): Reader = when (discoveryMethod) { - DiscoveryMethod.BLUETOOTH_SCAN -> { - 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.INTERNET -> connectInternetReader(reader, InternetConnectionConfiguration()) - DiscoveryMethod.HANDOFF -> { - if (listener is HandoffReaderListener) { - connectHandoffReader(reader, HandoffConnectionConfiguration(), listener) - } else { - connectHandoffReader(reader, HandoffConnectionConfiguration()) - } - } - DiscoveryMethod.USB -> { - val connConfig = UsbConnectionConfiguration( - locationId, - autoReconnectOnUnexpectedDisconnect, - reconnectionListener - ) - if (listener is ReaderListener) { - connectUsbReader(reader, connConfig, listener) - } else { - connectUsbReader(reader, connConfig) - } - } - else -> { - throw IllegalArgumentException("Unsupported discovery method: $discoveryMethod") - } -} +} \ No newline at end of file diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt deleted file mode 100644 index 0f6cb844..00000000 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListener.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.stripeterminalreactnative.listener - -import com.facebook.react.bridge.ReactApplicationContext -import com.stripe.stripeterminal.external.callable.Cancelable -import com.stripe.stripeterminal.external.callable.ReaderListener -import com.stripe.stripeterminal.external.models.BatteryStatus -import com.stripe.stripeterminal.external.models.DisconnectReason -import com.stripe.stripeterminal.external.models.ReaderEvent -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.DISCONNECT -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.mapFromReaderDisconnectReason -import com.stripeterminalreactnative.ReactNativeConstants.UPDATE_BATTERY_LEVEL -import com.stripeterminalreactnative.ReactNativeConstants.REPORT_LOW_BATTERY_WARNING -import com.stripeterminalreactnative.ReactNativeConstants.REPORT_READER_EVENT -import com.stripeterminalreactnative.mapFromBatteryStatus -import com.stripeterminalreactnative.mapFromReaderDisplayMessage -import com.stripeterminalreactnative.mapFromReaderEvent -import com.stripeterminalreactnative.mapFromReaderInputOptions -import com.stripeterminalreactnative.mapFromReaderSoftwareUpdate -import com.stripeterminalreactnative.nativeMapOf -import com.stripeterminalreactnative.putError -import com.stripeterminalreactnative.putDoubleOrNull - -class RNBluetoothReaderListener( - private val context: ReactApplicationContext, - private val onStartInstallingUpdate: (cancelable: Cancelable?) -> Unit -) : ReaderListener { - 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) { - val result = update?.let { mapFromReaderSoftwareUpdate(update) } ?: nativeMapOf() - e?.let { - result.putError(e) - } - putMap("result", result) - } - } - - 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)) - } - } - - override fun onDisconnect(reason: DisconnectReason) { - context.sendEvent(DISCONNECT.listenerName) { - putString("reason", mapFromReaderDisconnectReason(reason)) - } - } - - override fun onBatteryLevelUpdate( - batteryLevel: Float, - batteryStatus: BatteryStatus, - isCharging: Boolean - ) { - context.sendEvent(UPDATE_BATTERY_LEVEL.listenerName) { - putMap( - "result", - nativeMapOf { - putDoubleOrNull(this,"batteryLevel", batteryLevel.toDouble()) - putString("batteryStatus", mapFromBatteryStatus(batteryStatus)) - putBoolean("isCharging", isCharging) - } - ) - } - } - - override fun onReportLowBatteryWarning() { - context.sendEvent(REPORT_LOW_BATTERY_WARNING.listenerName) { - putString("result", "LOW BATTERY") - } - } - - override fun onReportReaderEvent(event: ReaderEvent) { - context.sendEvent(REPORT_READER_EVENT.listenerName) { - putString("result", mapFromReaderEvent(event)) - } - } -} diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNHandoffReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNHandoffReaderListener.kt index 91971f42..32fdde9e 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNHandoffReaderListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNHandoffReaderListener.kt @@ -2,13 +2,16 @@ package com.stripeterminalreactnative.listener import com.facebook.react.bridge.ReactApplicationContext import com.stripe.stripeterminal.external.callable.HandoffReaderListener +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener 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 { +class RNHandoffReaderListener( + private val context: ReactApplicationContext, + private val readerDisconnectListener: ReaderDisconnectListener +) : HandoffReaderListener, ReaderDisconnectListener by readerDisconnectListener { 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/RNInternetReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNInternetReaderListener.kt new file mode 100644 index 00000000..17d8ee02 --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNInternetReaderListener.kt @@ -0,0 +1,7 @@ +package com.stripeterminalreactnative.listener + +import com.stripe.stripeterminal.external.callable.InternetReaderListener +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener + +class RNInternetReaderListener(private val readerDisconnectListener: ReaderDisconnectListener) : + InternetReaderListener, ReaderDisconnectListener by readerDisconnectListener diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNMobileReaderListener.kt similarity index 62% rename from android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt rename to android/src/main/java/com/stripeterminalreactnative/listener/RNMobileReaderListener.kt index 65955fbd..8501893a 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNUsbReaderListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNMobileReaderListener.kt @@ -2,41 +2,35 @@ package com.stripeterminalreactnative.listener import com.facebook.react.bridge.ReactApplicationContext import com.stripe.stripeterminal.external.callable.Cancelable -import com.stripe.stripeterminal.external.callable.ReaderListener +import com.stripe.stripeterminal.external.callable.MobileReaderListener +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener +import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener import com.stripe.stripeterminal.external.models.BatteryStatus -import com.stripe.stripeterminal.external.models.DisconnectReason -import com.stripe.stripeterminal.external.models.ReaderEvent 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.TerminalException import com.stripeterminalreactnative.ReactExtensions.sendEvent -import com.stripeterminalreactnative.ReactNativeConstants.DISCONNECT -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.ReactNativeConstants.UPDATE_BATTERY_LEVEL -import com.stripeterminalreactnative.ReactNativeConstants.REPORT_LOW_BATTERY_WARNING -import com.stripeterminalreactnative.ReactNativeConstants.REPORT_READER_EVENT +import com.stripeterminalreactnative.ReactNativeConstants import com.stripeterminalreactnative.mapFromBatteryStatus -import com.stripeterminalreactnative.mapFromReaderDisconnectReason import com.stripeterminalreactnative.mapFromReaderDisplayMessage import com.stripeterminalreactnative.mapFromReaderEvent import com.stripeterminalreactnative.mapFromReaderInputOptions import com.stripeterminalreactnative.mapFromReaderSoftwareUpdate import com.stripeterminalreactnative.nativeMapOf -import com.stripeterminalreactnative.putError import com.stripeterminalreactnative.putDoubleOrNull +import com.stripeterminalreactnative.putError -class RNUsbReaderListener( +class RNMobileReaderListener( private val context: ReactApplicationContext, + private val readerReconnectionListener: ReaderReconnectionListener, + private val readerDisconnectListener: ReaderDisconnectListener, private val onStartInstallingUpdate: (cancelable: Cancelable?) -> Unit -) : ReaderListener { +) : MobileReaderListener, ReaderDisconnectListener by readerDisconnectListener, + ReaderReconnectionListener by readerReconnectionListener { override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { - context.sendEvent(REPORT_AVAILABLE_UPDATE.listenerName) { + context.sendEvent(ReactNativeConstants.REPORT_AVAILABLE_UPDATE.listenerName) { putMap("result", mapFromReaderSoftwareUpdate(update)) } } @@ -46,13 +40,13 @@ class RNUsbReaderListener( cancelable: Cancelable? ) { onStartInstallingUpdate(cancelable) - context.sendEvent(START_INSTALLING_UPDATE.listenerName) { + context.sendEvent(ReactNativeConstants.START_INSTALLING_UPDATE.listenerName) { putMap("result", mapFromReaderSoftwareUpdate(update)) } } override fun onReportReaderSoftwareUpdateProgress(progress: Float) { - context.sendEvent(REPORT_UPDATE_PROGRESS.listenerName) { + context.sendEvent(ReactNativeConstants.REPORT_UPDATE_PROGRESS.listenerName) { putMap( "result", nativeMapOf { @@ -66,7 +60,7 @@ class RNUsbReaderListener( update: ReaderSoftwareUpdate?, e: TerminalException? ) { - context.sendEvent(FINISH_INSTALLING_UPDATE.listenerName) { + context.sendEvent(ReactNativeConstants.FINISH_INSTALLING_UPDATE.listenerName) { val result = update?.let { mapFromReaderSoftwareUpdate(update) } ?: nativeMapOf() e?.let { result.putError(e) @@ -76,33 +70,27 @@ class RNUsbReaderListener( } override fun onRequestReaderInput(options: ReaderInputOptions) { - context.sendEvent(REQUEST_READER_INPUT.listenerName) { + context.sendEvent(ReactNativeConstants.REQUEST_READER_INPUT.listenerName) { putArray("result", mapFromReaderInputOptions(options)) } } override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) { - context.sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) { + context.sendEvent(ReactNativeConstants.REQUEST_READER_DISPLAY_MESSAGE.listenerName) { putString("result", mapFromReaderDisplayMessage(message)) } } - override fun onDisconnect(reason: DisconnectReason) { - context.sendEvent(DISCONNECT.listenerName) { - putString("reason", mapFromReaderDisconnectReason(reason)) - } - } - override fun onBatteryLevelUpdate( batteryLevel: Float, batteryStatus: BatteryStatus, isCharging: Boolean ) { - context.sendEvent(UPDATE_BATTERY_LEVEL.listenerName) { + context.sendEvent(ReactNativeConstants.UPDATE_BATTERY_LEVEL.listenerName) { putMap( "result", nativeMapOf { - putDoubleOrNull(this,"batteryLevel", batteryLevel.toDouble()) + putDoubleOrNull(this, "batteryLevel", batteryLevel.toDouble()) putString("batteryStatus", mapFromBatteryStatus(batteryStatus)) putBoolean("isCharging", isCharging) } @@ -111,13 +99,13 @@ class RNUsbReaderListener( } override fun onReportLowBatteryWarning() { - context.sendEvent(REPORT_LOW_BATTERY_WARNING.listenerName) { + context.sendEvent(ReactNativeConstants.REPORT_LOW_BATTERY_WARNING.listenerName) { putString("result", "LOW BATTERY") } } override fun onReportReaderEvent(event: ReaderEvent) { - context.sendEvent(REPORT_READER_EVENT.listenerName) { + context.sendEvent(ReactNativeConstants.REPORT_READER_EVENT.listenerName) { putString("result", mapFromReaderEvent(event)) } } diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderDisconnectListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderDisconnectListener.kt new file mode 100644 index 00000000..3883776b --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderDisconnectListener.kt @@ -0,0 +1,17 @@ +package com.stripeterminalreactnative.listener + +import com.facebook.react.bridge.ReactApplicationContext +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener +import com.stripe.stripeterminal.external.models.DisconnectReason +import com.stripeterminalreactnative.ReactExtensions.sendEvent +import com.stripeterminalreactnative.ReactNativeConstants.DISCONNECT +import com.stripeterminalreactnative.mapFromReaderDisconnectReason + +class RNReaderDisconnectListener(private val context: ReactApplicationContext) : ReaderDisconnectListener { + override fun onDisconnect(reason: DisconnectReason) { + super.onDisconnect(reason) + context.sendEvent(DISCONNECT.listenerName) { + putString("reason", mapFromReaderDisconnectReason(reason)) + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListener.kt index 14d4e32b..a89b669a 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListener.kt @@ -3,13 +3,13 @@ package com.stripeterminalreactnative.listener import com.facebook.react.bridge.ReactApplicationContext import com.stripe.stripeterminal.external.callable.Cancelable import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener +import com.stripe.stripeterminal.external.models.DisconnectReason import com.stripe.stripeterminal.external.models.Reader -import com.stripe.stripeterminal.external.models.TerminalException.TerminalErrorCode +import com.stripe.stripeterminal.external.models.TerminalErrorCode import com.stripeterminalreactnative.ReactExtensions.sendEvent -import com.stripeterminalreactnative.ReactNativeConstants.READER_RECONNECT_FAIL -import com.stripeterminalreactnative.ReactNativeConstants.READER_RECONNECT_SUCCEED -import com.stripeterminalreactnative.ReactNativeConstants.START_READER_RECONNECT +import com.stripeterminalreactnative.ReactNativeConstants import com.stripeterminalreactnative.mapFromReader +import com.stripeterminalreactnative.mapFromReaderDisconnectReason import com.stripeterminalreactnative.nativeMapOf class RNReaderReconnectionListener( @@ -18,7 +18,7 @@ class RNReaderReconnectionListener( ) : ReaderReconnectionListener { override fun onReaderReconnectFailed(reader: Reader) { - context.sendEvent(READER_RECONNECT_FAIL.listenerName) { + context.sendEvent(ReactNativeConstants.READER_RECONNECT_FAIL.listenerName) { putMap( "error", nativeMapOf { @@ -29,15 +29,19 @@ class RNReaderReconnectionListener( } } - override fun onReaderReconnectStarted(reader: Reader, cancelReconnect: Cancelable) { + override fun onReaderReconnectStarted( + reader: Reader, + cancelReconnect: Cancelable, + reason: DisconnectReason + ) { onReaderReconnectStarted(cancelReconnect) - context.sendEvent(START_READER_RECONNECT.listenerName) { - putMap("reader", mapFromReader(reader)) + context.sendEvent(ReactNativeConstants.START_READER_RECONNECT.listenerName) { + putString("reason", mapFromReaderDisconnectReason(reason)) } } override fun onReaderReconnectSucceeded(reader: Reader) { - context.sendEvent(READER_RECONNECT_SUCCEED.listenerName) { + context.sendEvent(ReactNativeConstants.READER_RECONNECT_SUCCEED.listenerName) { putMap("reader", mapFromReader(reader)) } } diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNTapToPayReaderListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNTapToPayReaderListener.kt new file mode 100644 index 00000000..2e4e724b --- /dev/null +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNTapToPayReaderListener.kt @@ -0,0 +1,13 @@ +package com.stripeterminalreactnative.listener + +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener +import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener +import com.stripe.stripeterminal.external.callable.TapToPayReaderListener + +class RNTapToPayReaderListener( + private val readerDisconnectListener: ReaderDisconnectListener, + private val readerReconnectionListener: ReaderReconnectionListener +) : TapToPayReaderListener, + ReaderDisconnectListener by readerDisconnectListener, + ReaderReconnectionListener by readerReconnectionListener + diff --git a/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt b/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt index fc729bc9..a592b947 100644 --- a/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt +++ b/android/src/main/java/com/stripeterminalreactnative/listener/RNTerminalListener.kt @@ -4,28 +4,13 @@ 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) { diff --git a/android/src/test/java/com/stripeterminalreactnative/listener/RNDiscoveryListenerTest.kt b/android/src/test/java/com/stripeterminalreactnative/listener/RNDiscoveryListenerTest.kt index c121f98f..bc7b7d03 100644 --- a/android/src/test/java/com/stripeterminalreactnative/listener/RNDiscoveryListenerTest.kt +++ b/android/src/test/java/com/stripeterminalreactnative/listener/RNDiscoveryListenerTest.kt @@ -3,8 +3,8 @@ package com.stripeterminalreactnative.listener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.TerminalErrorCode import com.stripe.stripeterminal.external.models.TerminalException -import com.stripe.stripeterminal.external.models.TerminalException.TerminalErrorCode import com.stripeterminalreactnative.NativeTypeFactory import com.stripeterminalreactnative.ReactExtensions.sendEvent import com.stripeterminalreactnative.ReactNativeConstants.FINISH_DISCOVERING_READERS diff --git a/android/src/test/java/com/stripeterminalreactnative/listener/RNHandoffReaderListenerTest.kt b/android/src/test/java/com/stripeterminalreactnative/listener/RNHandoffReaderListenerTest.kt index 440304c3..c45be7c7 100644 --- a/android/src/test/java/com/stripeterminalreactnative/listener/RNHandoffReaderListenerTest.kt +++ b/android/src/test/java/com/stripeterminalreactnative/listener/RNHandoffReaderListenerTest.kt @@ -1,11 +1,14 @@ package com.stripeterminalreactnative.listener import com.facebook.react.bridge.ReactApplicationContext +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener +import com.stripe.stripeterminal.external.models.DisconnectReason import com.stripe.stripeterminal.external.models.ReaderEvent import com.stripeterminalreactnative.ReactExtensions.sendEvent import com.stripeterminalreactnative.ReactNativeConstants.REQUEST_READER_INPUT import com.stripeterminalreactnative.ReactNativeTypeReplacementRule import com.stripeterminalreactnative.hasValue +import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.ClassRule @@ -27,11 +30,21 @@ class RNHandoffReaderListenerTest { @Test fun `should send onReportReaderEvent event`() { - val listener = RNHandoffReaderListener(context) + val listener = RNHandoffReaderListener(context, mockk()) listener.onReportReaderEvent(ReaderEvent.CARD_INSERTED) verify(exactly = 1) { context.sendEvent(REQUEST_READER_INPUT.listenerName, any()) } assertTrue(typeReplacer.sendEventSlot.captured.hasValue("event")) } + + @Test + fun `should call inner onDisconnect`() { + val readerDisconnectListener = mockk { + every { onDisconnect(any()) } returns Unit + } + val listener = RNHandoffReaderListener(context, readerDisconnectListener) + listener.onDisconnect(DisconnectReason.DISCONNECT_REQUESTED) + verify(exactly = 1) { readerDisconnectListener.onDisconnect(DisconnectReason.DISCONNECT_REQUESTED) } + } } diff --git a/android/src/test/java/com/stripeterminalreactnative/listener/RNInternetReaderListenerTest.kt b/android/src/test/java/com/stripeterminalreactnative/listener/RNInternetReaderListenerTest.kt new file mode 100644 index 00000000..42e1d16e --- /dev/null +++ b/android/src/test/java/com/stripeterminalreactnative/listener/RNInternetReaderListenerTest.kt @@ -0,0 +1,32 @@ +package com.stripeterminalreactnative.listener + +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener +import com.stripe.stripeterminal.external.models.DisconnectReason +import com.stripeterminalreactnative.ReactNativeTypeReplacementRule +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class RNInternetReaderListenerTest { + + companion object { + @ClassRule + @JvmField + val typeReplacer = ReactNativeTypeReplacementRule() + } + + @Test + fun `should call inner onDisconnect`() { + val readerDisconnectListener = mockk { + every { onDisconnect(any()) } returns Unit + } + val listener = RNInternetReaderListener(readerDisconnectListener) + listener.onDisconnect(DisconnectReason.DISCONNECT_REQUESTED) + verify(exactly = 1) { readerDisconnectListener.onDisconnect(DisconnectReason.DISCONNECT_REQUESTED) } + } +} diff --git a/android/src/test/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListenerTest.kt b/android/src/test/java/com/stripeterminalreactnative/listener/RNMobileReaderListenerTest.kt similarity index 66% rename from android/src/test/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListenerTest.kt rename to android/src/test/java/com/stripeterminalreactnative/listener/RNMobileReaderListenerTest.kt index 2bddd131..dd7b9c79 100644 --- a/android/src/test/java/com/stripeterminalreactnative/listener/RNBluetoothReaderListenerTest.kt +++ b/android/src/test/java/com/stripeterminalreactnative/listener/RNMobileReaderListenerTest.kt @@ -2,12 +2,15 @@ package com.stripeterminalreactnative.listener import com.facebook.react.bridge.ReactApplicationContext import com.stripe.stripeterminal.external.callable.Cancelable +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener +import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener +import com.stripe.stripeterminal.external.models.DisconnectReason import com.stripe.stripeterminal.external.models.ReaderDisplayMessage import com.stripe.stripeterminal.external.models.ReaderInputOptions import com.stripe.stripeterminal.external.models.ReaderInputOptions.ReaderInputOption import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate +import com.stripe.stripeterminal.external.models.TerminalErrorCode import com.stripe.stripeterminal.external.models.TerminalException -import com.stripe.stripeterminal.external.models.TerminalException.TerminalErrorCode import com.stripeterminalreactnative.ReactExtensions.sendEvent import com.stripeterminalreactnative.ReactNativeConstants.FINISH_INSTALLING_UPDATE import com.stripeterminalreactnative.ReactNativeConstants.REPORT_AVAILABLE_UPDATE @@ -25,12 +28,11 @@ import org.junit.ClassRule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import java.util.Date import kotlin.test.assertFalse import kotlin.test.assertTrue @RunWith(JUnit4::class) -class RNBluetoothReaderListenerTest { +class RNMobileReaderListenerTest { companion object { private val EXCEPTION = TerminalException(TerminalErrorCode.UNEXPECTED_SDK_ERROR, "message") @@ -46,16 +48,16 @@ class RNBluetoothReaderListenerTest { version } returns "1" every { - timeEstimate - } returns ReaderSoftwareUpdate.UpdateTimeEstimate.ONE_TO_TWO_MINUTES + durationEstimate + } returns ReaderSoftwareUpdate.UpdateDurationEstimate.ONE_TO_TWO_MINUTES every { - requiredAt - } returns Date() + requiredAtMs + } returns 0 } @Test fun `should send onReportAvailableUpdate event`() { - val listener = RNBluetoothReaderListener(context, mockk()) + val listener = RNMobileReaderListener(context, mockk(), mockk(), mockk()) listener.onReportAvailableUpdate(update) verify(exactly = 1) { context.sendEvent(REPORT_AVAILABLE_UPDATE.listenerName, any()) } @@ -67,7 +69,8 @@ class RNBluetoothReaderListenerTest { fun `should send onStartInstallingUpdate event`() { val mockOnStartInstallingUpdate = mockk<(Cancelable?) -> Unit>(relaxed = true) val mockCancelable = mockk() - val listener = RNBluetoothReaderListener(context, mockOnStartInstallingUpdate) + val listener = + RNMobileReaderListener(context, mockk(), mockk(), mockOnStartInstallingUpdate) listener.onStartInstallingUpdate(update, mockCancelable) verify(exactly = 1) { mockOnStartInstallingUpdate.invoke(mockCancelable) } @@ -78,7 +81,7 @@ class RNBluetoothReaderListenerTest { @Test fun `should send onReportReaderSoftwareUpdateProgress event`() { - val listener = RNBluetoothReaderListener(context, mockk()) + val listener = RNMobileReaderListener(context, mockk(), mockk(), mockk()) listener.onReportReaderSoftwareUpdateProgress(1.0f) verify(exactly = 1) { context.sendEvent(REPORT_UPDATE_PROGRESS.listenerName, any()) } @@ -88,7 +91,7 @@ class RNBluetoothReaderListenerTest { @Test fun `should send onFinishInstallingUpdate event`() { - val listener = RNBluetoothReaderListener(context, mockk()) + val listener = RNMobileReaderListener(context, mockk(), mockk(), mockk()) listener.onFinishInstallingUpdate(null) verify(exactly = 1) { context.sendEvent(FINISH_INSTALLING_UPDATE.listenerName, any()) } @@ -106,7 +109,7 @@ class RNBluetoothReaderListenerTest { @Test fun `should send onFinishInstallingUpdate error`() { - val listener = RNBluetoothReaderListener(context, mockk()) + val listener = RNMobileReaderListener(context, mockk(), mockk(), mockk()) listener.onFinishInstallingUpdate(update, EXCEPTION) verify(exactly = 1) { context.sendEvent(FINISH_INSTALLING_UPDATE.listenerName, any()) } @@ -117,7 +120,7 @@ class RNBluetoothReaderListenerTest { @Test fun `should send onRequestReaderInput event`() { - val listener = RNBluetoothReaderListener(context, mockk()) + val listener = RNMobileReaderListener(context, mockk(), mockk(), mockk()) listener.onRequestReaderInput(ReaderInputOptions(listOf(ReaderInputOption.INSERT))) verify(exactly = 1) { context.sendEvent(REQUEST_READER_INPUT.listenerName, any()) } @@ -127,7 +130,7 @@ class RNBluetoothReaderListenerTest { @Test fun `should send onRequestReaderDisplayMessage event`() { - val listener = RNBluetoothReaderListener(context, mockk()) + val listener = RNMobileReaderListener(context, mockk(), mockk(), mockk()) listener.onRequestReaderDisplayMessage(ReaderDisplayMessage.INSERT_OR_SWIPE_CARD) verify(exactly = 1) { @@ -136,4 +139,33 @@ class RNBluetoothReaderListenerTest { assertTrue(typeReplacer.sendEventSlot.captured.hasResult()) } + + @Test + fun `should call inner onDisconnect`() { + val readerDisconnectListener = mockk { + every { onDisconnect(any()) } returns Unit + } + val listener = RNMobileReaderListener(context, mockk(), readerDisconnectListener, mockk()) + listener.onDisconnect(DisconnectReason.DISCONNECT_REQUESTED) + verify(exactly = 1) { readerDisconnectListener.onDisconnect(DisconnectReason.DISCONNECT_REQUESTED) } + } + + @Test + fun `should call inner reconnectionListener`() { + val readerReconnectionListener = mockk(relaxed = true) + val listener = RNMobileReaderListener(context, readerReconnectionListener, mockk(), mockk()) + listener.onReaderReconnectStarted(mockk(), mockk(), mockk()) + verify(exactly = 1) { + readerReconnectionListener.onReaderReconnectStarted( + any(), + any(), + any() + ) + } + listener.onReaderReconnectSucceeded(mockk()) + verify(exactly = 1) { readerReconnectionListener.onReaderReconnectSucceeded(any()) } + listener.onReaderReconnectFailed(mockk()) + verify(exactly = 1) { readerReconnectionListener.onReaderReconnectFailed(any()) } + } + } diff --git a/android/src/test/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListenerTest.kt b/android/src/test/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListenerTest.kt index f58e5dea..b8bb8767 100644 --- a/android/src/test/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListenerTest.kt +++ b/android/src/test/java/com/stripeterminalreactnative/listener/RNReaderReconnectionListenerTest.kt @@ -2,12 +2,14 @@ package com.stripeterminalreactnative.listener import com.facebook.react.bridge.ReactApplicationContext import com.stripe.stripeterminal.external.callable.Cancelable +import com.stripe.stripeterminal.external.models.DisconnectReason import com.stripe.stripeterminal.external.models.Reader -import com.stripeterminalreactnative.* import com.stripeterminalreactnative.ReactExtensions.sendEvent import com.stripeterminalreactnative.ReactNativeConstants.READER_RECONNECT_FAIL -import com.stripeterminalreactnative.ReactNativeConstants.START_READER_RECONNECT import com.stripeterminalreactnative.ReactNativeConstants.READER_RECONNECT_SUCCEED +import com.stripeterminalreactnative.ReactNativeConstants.START_READER_RECONNECT +import com.stripeterminalreactnative.ReactNativeTypeReplacementRule +import com.stripeterminalreactnative.hasValue import io.mockk.mockk import io.mockk.verify import org.junit.ClassRule @@ -31,7 +33,7 @@ class RNReaderReconnectionListenerTest { fun `should send onReaderReconnectFailed event`() { val mockOnReaderReconnectStarted = mockk<(Cancelable?) -> Unit>(relaxed = true) val reader = mockk(relaxed = true) - val listener = RNReaderReconnectionListener(context,mockOnReaderReconnectStarted) + val listener = RNReaderReconnectionListener(context, mockOnReaderReconnectStarted) listener.onReaderReconnectFailed(reader) verify(exactly = 1) { @@ -46,20 +48,20 @@ class RNReaderReconnectionListenerTest { val mockOnReaderReconnectStarted = mockk<(Cancelable?) -> Unit>(relaxed = true) val mockCancelable = mockk() val reader = mockk(relaxed = true) - val listener = RNReaderReconnectionListener(context,mockOnReaderReconnectStarted) - listener.onReaderReconnectStarted(reader,mockCancelable) + val listener = RNReaderReconnectionListener(context, mockOnReaderReconnectStarted) + listener.onReaderReconnectStarted(reader, mockCancelable, DisconnectReason.UNKNOWN) verify(exactly = 1) { mockOnReaderReconnectStarted.invoke(mockCancelable) } verify(exactly = 1) { context.sendEvent(START_READER_RECONNECT.listenerName, any()) } - assertTrue(typeReplacer.sendEventSlot.captured.hasValue("reader")) + assertTrue(typeReplacer.sendEventSlot.captured.hasValue("reason")) } @Test fun `should send onReaderReconnectSucceeded event`() { val mockOnReaderReconnectStarted = mockk<(Cancelable?) -> Unit>(relaxed = true) val reader = mockk(relaxed = true) - val listener = RNReaderReconnectionListener(context,mockOnReaderReconnectStarted) + val listener = RNReaderReconnectionListener(context, mockOnReaderReconnectStarted) listener.onReaderReconnectSucceeded(reader) verify(exactly = 1) { context.sendEvent(READER_RECONNECT_SUCCEED.listenerName, any()) } diff --git a/android/src/test/java/com/stripeterminalreactnative/listener/RNTapToPayReaderListenerTest.kt b/android/src/test/java/com/stripeterminalreactnative/listener/RNTapToPayReaderListenerTest.kt new file mode 100644 index 00000000..b420b313 --- /dev/null +++ b/android/src/test/java/com/stripeterminalreactnative/listener/RNTapToPayReaderListenerTest.kt @@ -0,0 +1,51 @@ +package com.stripeterminalreactnative.listener + +import com.stripe.stripeterminal.external.callable.ReaderDisconnectListener +import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener +import com.stripe.stripeterminal.external.models.DisconnectReason +import com.stripeterminalreactnative.ReactNativeTypeReplacementRule +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class RNTapToPayReaderListenerTest { + + companion object { + @ClassRule + @JvmField + val typeReplacer = ReactNativeTypeReplacementRule() + } + + @Test + fun `should call inner reconnectionListener`() { + val readerReconnectionListener = mockk(relaxed = true) + val listener = RNTapToPayReaderListener(mockk(), readerReconnectionListener) + listener.onReaderReconnectStarted(mockk(), mockk(), mockk()) + verify(exactly = 1) { + readerReconnectionListener.onReaderReconnectStarted( + any(), + any(), + any() + ) + } + listener.onReaderReconnectSucceeded(mockk()) + verify(exactly = 1) { readerReconnectionListener.onReaderReconnectSucceeded(any()) } + listener.onReaderReconnectFailed(mockk()) + verify(exactly = 1) { readerReconnectionListener.onReaderReconnectFailed(any()) } + } + + @Test + fun `should call inner onDisconnect`() { + val readerDisconnectListener = mockk { + every { onDisconnect(any()) } returns Unit + } + val listener = RNTapToPayReaderListener(readerDisconnectListener, mockk()) + listener.onDisconnect(DisconnectReason.DISCONNECT_REQUESTED) + verify(exactly = 1) { readerDisconnectListener.onDisconnect(DisconnectReason.DISCONNECT_REQUESTED) } + } +} diff --git a/android/src/test/java/com/stripeterminalreactnative/listener/RNTerminalListenerTest.kt b/android/src/test/java/com/stripeterminalreactnative/listener/RNTerminalListenerTest.kt index b4541ac7..3173735a 100644 --- a/android/src/test/java/com/stripeterminalreactnative/listener/RNTerminalListenerTest.kt +++ b/android/src/test/java/com/stripeterminalreactnative/listener/RNTerminalListenerTest.kt @@ -6,10 +6,8 @@ import com.stripe.stripeterminal.external.models.PaymentStatus 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.ReactNativeTypeReplacementRule import com.stripeterminalreactnative.hasResult -import com.stripeterminalreactnative.hasValue import io.mockk.mockk import io.mockk.verify import org.junit.ClassRule @@ -29,18 +27,6 @@ class RNTerminalListenerTest { private val context = mockk() - @Test - fun `should send onUnexpectedReaderDisconnect event`() { - val listener = RNTerminalListener(context) - listener.onUnexpectedReaderDisconnect(mockk()) - - verify(exactly = 1) { - context.sendEvent(REPORT_UNEXPECTED_READER_DISCONNECT.listenerName, any()) - } - - assertTrue(typeReplacer.sendEventSlot.captured.hasValue("error")) - } - @Test fun `should send onConnectionStatusChange event`() { val listener = RNTerminalListener(context) diff --git a/android/src/test/java/com/stripeterminalreactnative/listener/RNUsbReaderListenerTest.kt b/android/src/test/java/com/stripeterminalreactnative/listener/RNUsbReaderListenerTest.kt deleted file mode 100644 index 9dab8c63..00000000 --- a/android/src/test/java/com/stripeterminalreactnative/listener/RNUsbReaderListenerTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.stripeterminalreactnative.listener - -import com.facebook.react.bridge.ReactApplicationContext -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.ReaderInputOptions.ReaderInputOption -import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate -import com.stripe.stripeterminal.external.models.TerminalException -import com.stripe.stripeterminal.external.models.TerminalException.TerminalErrorCode -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.ReactNativeTypeReplacementRule -import com.stripeterminalreactnative.hasError -import com.stripeterminalreactnative.hasResult -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.junit.ClassRule -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import java.util.Date -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -@RunWith(JUnit4::class) -class RNUsbReaderListenerTest { - - companion object { - private val EXCEPTION = TerminalException(TerminalErrorCode.UNEXPECTED_SDK_ERROR, "message") - - @ClassRule - @JvmField - val typeReplacer = ReactNativeTypeReplacementRule() - } - - private val context = mockk() - private val update = mockk(relaxed = true) { - every { - version - } returns "1" - every { - timeEstimate - } returns ReaderSoftwareUpdate.UpdateTimeEstimate.ONE_TO_TWO_MINUTES - every { - requiredAt - } returns Date() - } - - @Test - fun `should send onReportAvailableUpdate event`() { - val listener = RNUsbReaderListener(context, mockk()) - listener.onReportAvailableUpdate(update) - - verify(exactly = 1) { context.sendEvent(REPORT_AVAILABLE_UPDATE.listenerName, any()) } - - assertTrue(typeReplacer.sendEventSlot.captured.hasResult()) - } - - @Test - fun `should send onStartInstallingUpdate event`() { - val mockOnStartInstallingUpdate = mockk<(Cancelable?) -> Unit>(relaxed = true) - val mockCancelable = mockk() - val listener = RNUsbReaderListener(context, mockOnStartInstallingUpdate) - listener.onStartInstallingUpdate(update, mockCancelable) - - verify(exactly = 1) { mockOnStartInstallingUpdate.invoke(mockCancelable) } - verify(exactly = 1) { context.sendEvent(START_INSTALLING_UPDATE.listenerName, any()) } - - assertTrue(typeReplacer.sendEventSlot.captured.hasResult()) - } - - @Test - fun `should send onReportReaderSoftwareUpdateProgress event`() { - val listener = RNUsbReaderListener(context, mockk()) - listener.onReportReaderSoftwareUpdateProgress(1.0f) - - verify(exactly = 1) { context.sendEvent(REPORT_UPDATE_PROGRESS.listenerName, any()) } - - assertTrue(typeReplacer.sendEventSlot.captured.hasResult()) - } - - @Test - fun `should send onFinishInstallingUpdate event`() { - val listener = RNUsbReaderListener(context, mockk()) - listener.onFinishInstallingUpdate(null) - - verify(exactly = 1) { context.sendEvent(FINISH_INSTALLING_UPDATE.listenerName, any()) } - - assertFalse(typeReplacer.sendEventSlot.captured.hasError()) - assertFalse(typeReplacer.sendEventSlot.captured.hasResult()) - - listener.onFinishInstallingUpdate(update) - - verify(exactly = 2) { context.sendEvent(FINISH_INSTALLING_UPDATE.listenerName, any()) } - - assertFalse(typeReplacer.sendEventSlot.captured.hasError()) - assertTrue(typeReplacer.sendEventSlot.captured.hasResult()) - } - - @Test - fun `should send onFinishInstallingUpdate error`() { - val listener = RNUsbReaderListener(context, mockk()) - listener.onFinishInstallingUpdate(update, EXCEPTION) - - verify(exactly = 1) { context.sendEvent(FINISH_INSTALLING_UPDATE.listenerName, any()) } - - assertTrue(typeReplacer.sendEventSlot.captured.hasError()) - assertTrue(typeReplacer.sendEventSlot.captured.hasResult()) - } - - @Test - fun `should send onRequestReaderInput event`() { - val listener = RNUsbReaderListener(context, mockk()) - listener.onRequestReaderInput(ReaderInputOptions(listOf(ReaderInputOption.INSERT))) - - verify(exactly = 1) { context.sendEvent(REQUEST_READER_INPUT.listenerName, any()) } - - assertTrue(typeReplacer.sendEventSlot.captured.hasResult()) - } - - @Test - fun `should send onRequestReaderDisplayMessage event`() { - val listener = RNUsbReaderListener(context, mockk()) - listener.onRequestReaderDisplayMessage(ReaderDisplayMessage.INSERT_OR_SWIPE_CARD) - - verify(exactly = 1) { - context.sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName, any()) - } - - assertTrue(typeReplacer.sendEventSlot.captured.hasResult()) - } -} diff --git a/bitrise.yml b/bitrise.yml index 007eb2f6..dee19ca0 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -215,7 +215,7 @@ workflows: set -e # This is a terrible hack, as I haven't worked out how Bitrise's `pod install` step interacts with the rbenv set in this app. You definitely shouldn't copy this. cd dev-app/ios && asdf install ruby 3.2.3 && bundle install && \ - gem install cocoapods -v 1.15.2 && pod install && cd - && \ + gem install cocoapods -v 1.16.2 && pod install && cd - && \ npm rebuild detox echo "Checking for diffs in pod lockfile, if this fails please ensure all dependencies are up to date" && \ git diff --exit-code diff --git a/dev-app/android/app/build.gradle b/dev-app/android/app/build.gradle index 5d65b405..da85522b 100644 --- a/dev-app/android/app/build.gradle +++ b/dev-app/android/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" /** @@ -81,7 +82,6 @@ dependencies { } else { implementation jscFlavor } - implementation project(':stripeterminalreactnative') } diff --git a/dev-app/android/build.gradle b/dev-app/android/build.gradle index 4372bda1..c7b7040f 100644 --- a/dev-app/android/build.gradle +++ b/dev-app/android/build.gradle @@ -19,6 +19,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:8.1.1") classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } diff --git a/dev-app/ios/Podfile b/dev-app/ios/Podfile index 3f4bb3fa..ef8385dd 100644 --- a/dev-app/ios/Podfile +++ b/dev-app/ios/Podfile @@ -4,7 +4,7 @@ require Pod::Executable.execute_command('node', ['-p', "react-native/scripts/react_native_pods.rb", {paths: [process.argv[1]]}, )', __dir__]).strip -platform :ios, '13.0' +platform :ios, '14.0' prepare_react_native_project! # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded diff --git a/dev-app/ios/Podfile.lock b/dev-app/ios/Podfile.lock index 755c4b00..5c5cba56 100644 --- a/dev-app/ios/Podfile.lock +++ b/dev-app/ios/Podfile.lock @@ -440,11 +440,11 @@ PODS: - SocketRocket (0.6.1) - stripe-terminal-react-native (0.0.1-beta.23): - React-Core - - StripeTerminal (~> 3.9.1) + - StripeTerminal (~> 4.0.0) - stripe-terminal-react-native/Tests (0.0.1-beta.23): - React-Core - - StripeTerminal (~> 3.9.1) - - StripeTerminal (3.9.1) + - StripeTerminal (~> 4.0.0) + - StripeTerminal (4.0.0) - Yoga (1.14.0) DEPENDENCIES: @@ -644,10 +644,10 @@ SPEC CHECKSUMS: RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741 RNScreens: 3c2d122f5e08c192e254c510b212306da97d2581 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - stripe-terminal-react-native: ab37d4c51b9a86e77f2aa867b0cc21f87415ef95 - StripeTerminal: f7f5e176979224ed76edb3724f41138fbb28053c + stripe-terminal-react-native: 2ddb51910cbd52e8c4c040cf2de205b9c2de7fb5 + StripeTerminal: b8a0f7bc10534eb060f676c9b3b813b2613a137b Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 -PODFILE CHECKSUM: aa102efefd71dcb4ab5551258c2d9d171cb9fd6f +PODFILE CHECKSUM: 0fc269c07f7b9dd0472ad32eab1e1fd0e4e866f6 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/dev-app/ios/StripeTerminalReactNativeDevApp.xcodeproj/project.pbxproj b/dev-app/ios/StripeTerminalReactNativeDevApp.xcodeproj/project.pbxproj index fe102eb9..907f257e 100644 --- a/dev-app/ios/StripeTerminalReactNativeDevApp.xcodeproj/project.pbxproj +++ b/dev-app/ios/StripeTerminalReactNativeDevApp.xcodeproj/project.pbxproj @@ -300,7 +300,7 @@ ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; INFOPLIST_FILE = StripeTerminalReactNativeDevApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", @@ -326,7 +326,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = Y28TH9SHX7; INFOPLIST_FILE = StripeTerminalReactNativeDevApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", @@ -390,7 +390,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; @@ -457,7 +457,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; diff --git a/dev-app/src/screens/CollectCardPaymentScreen.tsx b/dev-app/src/screens/CollectCardPaymentScreen.tsx index 0f6b7169..f7f06434 100644 --- a/dev-app/src/screens/CollectCardPaymentScreen.tsx +++ b/dev-app/src/screens/CollectCardPaymentScreen.tsx @@ -16,6 +16,7 @@ import { PaymentIntent, StripeError, CommonError, + AllowRedisplay, } from '@stripe/stripe-terminal-react-native'; import { colors } from '../colors'; import List from '../components/List'; @@ -59,6 +60,12 @@ const OFFLINE_BEHAVIOR = [ { value: 'force_offline', label: 'force_offline' }, ]; +const ALLOW_REDISPLAY = [ + { value: 'unspecified', label: 'unspecified' }, + { value: 'limited', label: 'limited' }, + { value: 'always', label: 'always' }, +]; + export default function CollectCardPaymentScreen() { const { api, @@ -107,6 +114,8 @@ export default function CollectCardPaymentScreen() { const [enabledPaymentMethodTypes, setEnabledPaymentMethodTypes] = useState( DEFAULT_ENABLED_PAYMENT_METHOD_TYPES ); + const [allowRedisplay, setAllowRedisplay] = + useState('unspecified'); const { params } = useRoute>(); const { simulated, discoveryMethod, deviceType } = params; @@ -353,6 +362,7 @@ export default function CollectCardPaymentScreen() { enableCustomerCancellation: enableCustomerCancellation, requestDynamicCurrencyConversion: requestDcc, surchargeNotice: surchargeNotice ? surchargeNotice : undefined, + allowRedisplay: allowRedisplay, }); if (error) { @@ -688,7 +698,26 @@ export default function CollectCardPaymentScreen() { } /> - + + + setAllowRedisplay(value as AllowRedisplay) + } + > + {ALLOW_REDISPLAY.map((a) => ( + + ))} + + { if (finishError) { @@ -189,24 +193,33 @@ export default function DiscoverReadersScreen() { const handleConnectReader = async (reader: Reader.Type) => { let error: StripeError | undefined; + + setConnectingReader(reader); if (discoveryMethod === 'internet') { - const result = await handleConnectInternetReader(reader); - error = result.error; + error = await connectReaderWrapper( + getInternetParams(reader), + discoveryMethod + ); } else if ( discoveryMethod === 'bluetoothScan' || discoveryMethod === 'bluetoothProximity' ) { - const result = await handleConnectBluetoothReader(reader); - error = result.error; - } else if (discoveryMethod === 'localMobile') { - const result = await handleConnectLocalMobileReader(reader); - error = result.error; + error = await connectReaderWrapper( + getBluetoothParams(reader), + discoveryMethod + ); + } else if (discoveryMethod === 'tapToPay') { + error = await connectReaderWrapper( + getTapToPayParams(reader), + discoveryMethod + ); } else if (discoveryMethod === 'handoff') { - const result = await handleConnectHandoffReader(reader); - error = result.error; + error = await connectReaderWrapper( + getHandoffParams(reader), + discoveryMethod + ); } else if (discoveryMethod === 'usb') { - const result = await handleConnectUsbReader(reader); - error = result.error; + error = await connectReaderWrapper(getUsbParams(reader), discoveryMethod); } if (error) { setConnectingReader(undefined); @@ -216,86 +229,49 @@ export default function DiscoverReadersScreen() { } }; - const handleConnectHandoffReader = async (reader: Reader.Type) => { - setConnectingReader(reader); - - const { reader: connectedReader, error } = await connectHandoffReader({ - reader, - locationId: selectedLocation?.id, - }); - - if (error) { - console.log('connectHandoffReader error:', error); - } else { - console.log('Reader connected successfully', connectedReader); - } - return { error }; - }; - - const handleConnectLocalMobileReader = async (reader: Reader.Type) => { - setConnectingReader(reader); - - const { reader: connectedReader, error } = await connectLocalMobileReader({ - reader, - locationId: selectedLocation?.id, - autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect, - }); - - if (error) { - console.log('connectLocalMobileReader error:', error); - } else { - console.log('Reader connected successfully', connectedReader); - } - return { error }; - }; - - const handleConnectBluetoothReader = async (reader: Reader.Type) => { - setConnectingReader(reader); - - const { reader: connectedReader, error } = await connectBluetoothReader({ - reader, - locationId: selectedLocation?.id || reader?.location?.id, - autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect, - }); - - if (error) { - console.log('connectBluetoothReader error:', error); - } else { - console.log('Reader connected successfully', connectedReader); - } - return { error }; - }; + const getBluetoothParams = ( + reader: Reader.Type + ): ConnectBluetoothReaderParams => ({ + reader, + locationId: selectedLocation?.id || reader?.location?.id, + autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect, + }); - const handleConnectInternetReader = async (reader: Reader.Type) => { - setConnectingReader(reader); + const getInternetParams = ( + reader: Reader.Type + ): ConnectInternetReaderParams => ({ + reader, + }); - const { reader: connectedReader, error } = await connectInternetReader({ - reader, - }); + const getUsbParams = (reader: Reader.Type): ConnectUsbReaderParams => ({ + reader, + locationId: selectedLocation?.id || reader?.location?.id, + autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect, + }); - if (error) { - console.log('connectInternetReader error:', error); - } else { - console.log('Reader connected successfully', connectedReader); - } - return { error }; - }; + const getTapToPayParams = (reader: Reader.Type): ConnectTapToPayParams => ({ + reader, + locationId: selectedLocation?.id || reader?.location?.id, + autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect, + }); - const handleConnectUsbReader = async (reader: Reader.Type) => { - setConnectingReader(reader); + const getHandoffParams = (reader: Reader.Type): ConnectHandoffParams => ({ + reader, + locationId: selectedLocation?.id || reader?.location?.id, + }); - const { reader: connectedReader, error } = await connectUsbReader({ - reader, - locationId: selectedLocation?.id || reader?.location?.id, - autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect, - }); + const connectReaderWrapper = async ( + params: ConnectReaderParams, + discoveryMethod: Reader.DiscoveryMethod + ) => { + const { reader, error } = await connectReader(params, discoveryMethod); if (error) { - console.log('connectUsbReader error:', error); + console.log('connect Reader error:', error); } else { - console.log('Reader connected successfully', connectedReader); + console.log('Reader connected successfully', reader); } - return { error }; + return error; }; const handleChangeUpdatePlan = async (plan: Reader.SimulateUpdateType) => { @@ -364,7 +340,7 @@ export default function DiscoverReadersScreen() { {!simulated && (discoveryMethod === 'bluetoothScan' || discoveryMethod === 'usb' || - discoveryMethod === 'localMobile' || + discoveryMethod === 'tapToPay' || discoveryMethod === 'bluetoothProximity') && ( - onSelect('localMobile')} title="Local mobile" /> + onSelect('tapToPay')} title="Tap to Pay" /> {Platform.OS === 'android' && ( onSelect('handoff')} title="Handoff" /> diff --git a/dev-app/src/screens/HomeScreen.tsx b/dev-app/src/screens/HomeScreen.tsx index 8172e8af..d2503ec0 100644 --- a/dev-app/src/screens/HomeScreen.tsx +++ b/dev-app/src/screens/HomeScreen.tsx @@ -106,7 +106,9 @@ export default function HomeScreen() { 'Reader disconnected with reason ' + reason ); }, - onDidStartReaderReconnect() { + onDidStartReaderReconnect(reason) { + console.log('onDidStartReaderReconnect ' + reason); + setShowDisconnectAlert(true); setShowReconnectAlert(false); }, @@ -467,8 +469,8 @@ function mapFromDiscoveryMethod(method: Reader.DiscoveryMethod) { return 'Internet'; case 'handoff': return 'Handoff'; - case 'localMobile': - return 'Local mobile'; + case 'tapToPay': + return 'Tap To Pay'; case 'usb': return 'USB'; default: diff --git a/dev-app/src/screens/RegisterInternetReaderScreen.tsx b/dev-app/src/screens/RegisterInternetReaderScreen.tsx index e219bd8d..bd62a79b 100644 --- a/dev-app/src/screens/RegisterInternetReaderScreen.tsx +++ b/dev-app/src/screens/RegisterInternetReaderScreen.tsx @@ -37,7 +37,7 @@ export default function RegisterInternetReaderScreen() { label: '', }); - const { cancelDiscovering, discoverReaders, connectInternetReader } = + const { cancelDiscovering, discoverReaders, connectReader } = useStripeTerminal({ onFinishDiscoveringReaders: (finishError) => { if (finishError) { @@ -95,9 +95,10 @@ export default function RegisterInternetReaderScreen() { }, [navigation, discoverReaders, handleGoBack]); const handleConnectInternetReader = async (reader: Reader.Type) => { - const { reader: connectedReader, error } = await connectInternetReader({ - reader, - }); + const { reader: connectedReader, error } = await connectReader( + { reader }, + 'internet' + ); if (error) { console.log('connectInternetReader error:', error); diff --git a/dev-app/src/screens/SetupIntentScreen.tsx b/dev-app/src/screens/SetupIntentScreen.tsx index 508ad54e..1c407d48 100644 --- a/dev-app/src/screens/SetupIntentScreen.tsx +++ b/dev-app/src/screens/SetupIntentScreen.tsx @@ -1,11 +1,12 @@ import { RouteProp, useNavigation, useRoute } from '@react-navigation/core'; import React, { useCallback, useContext, useState } from 'react'; -import { StyleSheet, Switch } from 'react-native'; +import { StyleSheet, Switch, Platform } from 'react-native'; import { SetupIntent, useStripeTerminal, CommonError, StripeError, + AllowRedisplay, } from '@stripe/stripe-terminal-react-native'; import { colors } from '../colors'; import { LogContext } from '../components/LogContext'; @@ -15,6 +16,13 @@ import type { RouteParamList } from '../App'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import List from '../components/List'; import ListItem from '../components/ListItem'; +import { Picker } from '@react-native-picker/picker'; + +const ALLOW_REDISPLAY = [ + { value: 'unspecified', label: 'unspecified' }, + { value: 'limited', label: 'limited' }, + { value: 'always', label: 'always' }, +]; export default function SetupIntentScreen() { const { api } = useContext(AppContext); @@ -25,6 +33,9 @@ export default function SetupIntentScreen() { const [enableCustomerCancellation, setEnableCustomerCancellation] = useState(false); + const [allowRedisplay, setAllowRedisplay] = + useState('always'); + const { createSetupIntent, collectSetupIntentPaymentMethod, @@ -119,7 +130,7 @@ export default function SetupIntentScreen() { }); const { setupIntent, error } = await collectSetupIntentPaymentMethod({ setupIntent: si, - customerConsentCollected: true, + allowRedisplay: allowRedisplay, enableCustomerCancellation: enableCustomerCancellation, }); if (error) { @@ -278,7 +289,23 @@ export default function SetupIntentScreen() { /> )} - + + setAllowRedisplay(value as AllowRedisplay)} + > + {ALLOW_REDISPLAY.map((a) => ( + + ))} + String { switch type { - case DeviceType.appleBuiltIn: return "appleBuiltIn" + case DeviceType.tapToPay: return "tapToPay" case DeviceType.chipper1X: return "chipper1X" case DeviceType.chipper2X: return "chipper2X" case DeviceType.etna: return "etna" @@ -80,7 +80,7 @@ class Mappers { class func mapToDeviceType(_ type: String) -> DeviceType? { switch type { - case "appleBuiltIn": return DeviceType.appleBuiltIn + case "tapToPay": return DeviceType.tapToPay case "chipper1X": return DeviceType.chipper1X case "chipper2X": return DeviceType.chipper2X case "etna": return DeviceType.etna @@ -133,7 +133,7 @@ class Mappers { case "bluetoothProximity": return DiscoveryMethod.bluetoothProximity case "bluetoothScan": return DiscoveryMethod.bluetoothScan case "internet": return DiscoveryMethod.internet - case "localMobile": return DiscoveryMethod.localMobile + case "tapToPay": return DiscoveryMethod.tapToPay default: return DiscoveryMethod.internet } } @@ -148,8 +148,8 @@ class Mappers { return try BluetoothProximityDiscoveryConfigurationBuilder().setSimulated(simulated).build() case "internet": return try InternetDiscoveryConfigurationBuilder().setSimulated(simulated).setLocationId(locationId).build() - case "localMobile": - return try LocalMobileDiscoveryConfigurationBuilder().setSimulated(simulated).build() + case "tapToPay": + return try TapToPayDiscoveryConfigurationBuilder().setSimulated(simulated).build() @unknown default: print("⚠️ Unknown discovery method! Defaulting to Bluetooth Scan.") return try BluetoothScanDiscoveryConfigurationBuilder().setSimulated(simulated).setTimeout(timeout).build() @@ -231,7 +231,7 @@ class Mappers { metadataMap = NSDictionary(dictionary: metadata) } let result: NSDictionary = [ - "id": setupIntent.stripeId, + "id": setupIntent.stripeId ?? NSNull(), "sdkUuid": uuid, "created": convertDateToUnixTimestamp(date: setupIntent.created) ?? NSNull(), "customer": setupIntent.customer ?? NSNull(), @@ -377,7 +377,7 @@ class Mappers { } let result: [AnyHashable: Any?] = [ "deviceSoftwareVersion": unwrappedUpdate.deviceSoftwareVersion, - "estimatedUpdateTime": mapFromUpdateTimeEstimate(unwrappedUpdate.estimatedUpdateTime), + "estimatedUpdateTime": mapFromUpdateTimeEstimate(unwrappedUpdate.durationEstimate), "requiredAt": Mappers.convertDateToUnixTimestamp(date: unwrappedUpdate.requiredAt), ] return result @@ -462,6 +462,14 @@ class Mappers { return nil } + class func mapToAllowRedisplay(allowToredisplay: String) -> AllowRedisplay { + switch allowToredisplay { + case "always": return AllowRedisplay.always + case "limited": return AllowRedisplay.limited + default: return AllowRedisplay.unspecified + } + } + class func mapToSimulateReaderUpdate(_ update: String) -> SimulateReaderUpdate { switch update { case "available": return SimulateReaderUpdate.available @@ -527,7 +535,7 @@ class Mappers { } let result: NSDictionary = [ - "storedAt": convertDateToUnixTimestamp(date: offlineDetails.collectedAt) ?? NSNull(), + "storedAtMs": convertDateToUnixTimestamp(date: offlineDetails.storedAt) ?? NSNull(), "requiresUpload": offlineDetails.requiresUpload, "cardPresentDetails": offlineCardPresentDetails ?? NSNull(), "amountDetails": amountDetails ?? NSNull() @@ -544,7 +552,6 @@ class Mappers { let result: NSDictionary = [ "tip": amount ] - return result } @@ -744,6 +751,7 @@ class Mappers { case ConnectionStatus.connected: return "connected" case ConnectionStatus.connecting: return "connecting" case ConnectionStatus.notConnected: return "notConnected" + case ConnectionStatus.discovering: return "discovering" default: return "unknown" } } @@ -928,6 +936,17 @@ class Mappers { ] return result } + + class func mapPaymentMethodType(_ type: String) -> PaymentMethodType { + switch type { + case "card": return .card + case "card_present": return .cardPresent + case "interac_present": return .interacPresent + case "wechat_pay": return .wechatPay + default: return .unknown + } + } + } extension UInt { diff --git a/ios/StripeTerminalReactNative.m b/ios/StripeTerminalReactNative.m index c0130d93..65e73190 100644 --- a/ios/StripeTerminalReactNative.m +++ b/ios/StripeTerminalReactNative.m @@ -21,19 +21,8 @@ @interface RCT_EXTERN_MODULE(StripeTerminalReactNative, RCTEventEmitter) ) RCT_EXTERN_METHOD( - connectBluetoothReader:(NSDictionary *)params - resolver: (RCTPromiseResolveBlock)resolve - rejecter: (RCTPromiseRejectBlock)reject - ) - -RCT_EXTERN_METHOD( - connectInternetReader:(NSDictionary *)params - resolver: (RCTPromiseResolveBlock)resolve - rejecter: (RCTPromiseRejectBlock)reject - ) - -RCT_EXTERN_METHOD( - connectLocalMobileReader:(NSDictionary *)params + connectReader:(NSDictionary *)params + discoveryMethod: (NSString *)discoveryMethod resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) diff --git a/ios/StripeTerminalReactNative.swift b/ios/StripeTerminalReactNative.swift index b54e6ea5..a8ecbe93 100644 --- a/ios/StripeTerminalReactNative.swift +++ b/ios/StripeTerminalReactNative.swift @@ -4,7 +4,6 @@ import Foundation enum ReactNativeConstants: String, CaseIterable { case UPDATE_DISCOVERED_READERS = "didUpdateDiscoveredReaders" case FINISH_DISCOVERING_READERS = "didFinishDiscoveringReaders" - case REPORT_UNEXPECTED_READER_DISCONNECT = "didReportUnexpectedReaderDisconnect" case REPORT_AVAILABLE_UPDATE = "didReportAvailableUpdate" case START_INSTALLING_UPDATE = "didStartInstallingUpdate" case REPORT_UPDATE_PROGRESS = "didReportReaderSoftwareUpdateProgress" @@ -28,7 +27,8 @@ enum ReactNativeConstants: String, CaseIterable { } @objc(StripeTerminalReactNative) -class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothReaderDelegate, LocalMobileReaderDelegate, TerminalDelegate, ReconnectionDelegate, OfflineDelegate { +class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, MobileReaderDelegate, TerminalDelegate, OfflineDelegate, InternetReaderDelegate, TapToPayReaderDelegate, ReaderDelegate { + var discoveredReadersList: [Reader]? = nil var paymentIntents: [AnyHashable : PaymentIntent] = [:] var setupIntents: [AnyHashable : SetupIntent] = [:] @@ -61,7 +61,7 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe func terminal(_ terminal: Terminal, didUpdateDiscoveredReaders readers: [Reader]) { discoveredReadersList = readers - guard terminal.connectionStatus == .notConnected else { return } + guard terminal.connectionStatus == .notConnected || terminal.connectionStatus == .discovering else { return } sendEvent(withName: ReactNativeConstants.UPDATE_DISCOVERED_READERS.rawValue, body: ["readers": Mappers.mapFromReaders(readers)]) } @@ -225,8 +225,8 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe } - @objc(connectBluetoothReader:resolver:rejecter:) - func connectBluetoothReader(params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + @objc(connectReader:discoveryMethod:resolver:rejecter:) + func connectReader(params: NSDictionary, discoveryMethod: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { guard let reader = params["reader"] as? NSDictionary else { resolve(Errors.createError(code: CommonErrorType.InvalidRequiredParameter, message: "You must provide a reader object")) return @@ -234,7 +234,7 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe // since simulated readers don't contain `id` property we take serialNumber as a fallback let readerId = reader["serialNumber"] as? String - + let discoveryMethodType = Mappers.mapToDiscoveryMethod(discoveryMethod) guard let selectedReader = discoveredReadersList?.first(where: { $0.serialNumber == readerId }) else { resolve(Errors.createError(code: CommonErrorType.InvalidRequiredParameter, message: "Could not find reader with id \(readerId ?? "")")) return @@ -242,20 +242,29 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe } let locationId = params["locationId"] as? String - let autoReconnectOnUnexpectedDisconnect = params["autoReconnectOnUnexpectedDisconnect"] as? Bool ?? false - - let connectionConfig: BluetoothConnectionConfiguration + let autoReconnectOnUnexpectedDisconnect = params["autoReconnectOnUnexpectedDisconnect"] as? Bool ?? true + let failIfInUse: Bool = params["failIfInUse"] as? Bool ?? false + let onBehalfOf: String? = params["onBehalfOf"] as? String + let merchantDisplayName: String? = params["merchantDisplayName"] as? String + let tosAcceptancePermitted: Bool = params["tosAcceptancePermitted"] as? Bool ?? true + + let connectionConfig: ConnectionConfiguration do { - connectionConfig = try BluetoothConnectionConfigurationBuilder(locationId: locationId ?? selectedReader.locationId ?? "") - .setAutoReconnectOnUnexpectedDisconnect(autoReconnectOnUnexpectedDisconnect) - .setAutoReconnectionDelegate(autoReconnectOnUnexpectedDisconnect ? self : nil) - .build() + connectionConfig = try getConnectionConfig( + selectedReader: selectedReader, + locationId: locationId, + autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect, + failIfInUse: failIfInUse, + merchantDisplayName: merchantDisplayName, + onBehalfOf: onBehalfOf, + tosAcceptancePermitted: tosAcceptancePermitted, + discoveryMethod: discoveryMethodType)! // TODO find way to ! } catch { resolve(Errors.createError(nsError: error as NSError)) return } - Terminal.shared.connectBluetoothReader(selectedReader, delegate: self, connectionConfig: connectionConfig) { reader, error in + Terminal.shared.connectReader(selectedReader, connectionConfig: connectionConfig) { reader, error in if let reader = reader { resolve(["reader": Mappers.mapFromReader(reader)]) } else if let error = error as NSError? { @@ -265,85 +274,35 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe } } } - - @objc(connectInternetReader:resolver:rejecter:) - func connectInternetReader(params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { - guard let reader = params["reader"] as? NSDictionary else { - resolve(Errors.createError(code: CommonErrorType.InvalidRequiredParameter, message: "You must provide a reader object")) - return - } - - // since simulated readers don't contain `id` property we take serialNumber as a fallback - let readerId = reader["serialNumber"] as? String - - guard let selectedReader = discoveredReadersList?.first(where: { $0.serialNumber == readerId }) else { - resolve(Errors.createError(code: CommonErrorType.InvalidRequiredParameter, message: "Could not find reader with id \(readerId ?? "")")) - return - } - - let connectionConfig: InternetConnectionConfiguration - do { - connectionConfig = try InternetConnectionConfigurationBuilder() - .setFailIfInUse(params["failIfInUse"] as? Bool ?? false) + + private func getConnectionConfig( + selectedReader: Reader, + locationId: String?, + autoReconnectOnUnexpectedDisconnect: Bool, + failIfInUse: Bool, + merchantDisplayName: String?, + onBehalfOf: String?, + tosAcceptancePermitted: Bool, + discoveryMethod: DiscoveryMethod) throws -> ConnectionConfiguration? { + switch discoveryMethod { + case .bluetoothScan, .bluetoothProximity: + return try BluetoothConnectionConfigurationBuilder(delegate: self, locationId: locationId ?? selectedReader.locationId ?? "") + .setAutoReconnectOnUnexpectedDisconnect(autoReconnectOnUnexpectedDisconnect) + .build() + case .internet: + return try InternetConnectionConfigurationBuilder(delegate: self) + .setFailIfInUse(failIfInUse) .setAllowCustomerCancel(true) .build() - } catch { - resolve(Errors.createError(nsError: error as NSError)) - return - } - - Terminal.shared.connectInternetReader(selectedReader, connectionConfig: connectionConfig) { reader, error in - if let reader = reader { - resolve(["reader": Mappers.mapFromReader(reader)]) - } else if let error = error as NSError? { - resolve(Errors.createError(nsError: error)) - } - } - } - - @objc(connectLocalMobileReader:resolver:rejecter:) - func connectLocalMobileReader(params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { - guard let reader = params["reader"] as? NSDictionary else { - resolve(Errors.createError(code: CommonErrorType.InvalidRequiredParameter, message: "You must provide a reader object")) - return - } - - // since simulated readers don't contain `id` property we take serialNumber as a fallback - let readerId = reader["serialNumber"] as? String - - guard let selectedReader = discoveredReadersList?.first(where: { $0.serialNumber == readerId }) else { - resolve(Errors.createError(code: CommonErrorType.InvalidRequiredParameter, message: "Could not find reader with id \(readerId ?? "")")) - return - } - - let locationId = params["locationId"] as? String - let onBehalfOf: String? = params["onBehalfOf"] as? String - let merchantDisplayName: String? = params["merchantDisplayName"] as? String - let tosAcceptancePermitted: Bool = params["tosAcceptancePermitted"] as? Bool ?? true - let autoReconnectOnUnexpectedDisconnect = params["autoReconnectOnUnexpectedDisconnect"] as? Bool ?? false - - let connectionConfig: LocalMobileConnectionConfiguration - do { - connectionConfig = try LocalMobileConnectionConfigurationBuilder(locationId: locationId ?? selectedReader.locationId ?? "") + case .tapToPay: + return try TapToPayConnectionConfigurationBuilder(delegate: self, locationId: locationId ?? selectedReader.locationId ?? "") .setMerchantDisplayName(merchantDisplayName ?? nil) .setOnBehalfOf(onBehalfOf ?? nil) .setTosAcceptancePermitted(tosAcceptancePermitted) .setAutoReconnectOnUnexpectedDisconnect(autoReconnectOnUnexpectedDisconnect) - .setAutoReconnectionDelegate(autoReconnectOnUnexpectedDisconnect ? self : nil) .build() - } catch { - resolve(Errors.createError(nsError: error as NSError)) - return - } - - Terminal.shared.connectLocalMobileReader(selectedReader, delegate: self, connectionConfig: connectionConfig) { reader, error in - if let reader = reader { - resolve(["reader": Mappers.mapFromReader(reader)]) - } else if let error = error as NSError? { - resolve(Errors.createError(nsError: error)) - } else { - resolve([:]) - } + @unknown default: + return nil } } @@ -371,12 +330,7 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe } } - func terminal(_ terminal: Terminal, didReportUnexpectedReaderDisconnect reader: Reader) { - let error = Errors.createError(code: ErrorCode.unexpectedSdkError, message: "Reader has been disconnected unexpectedly") - sendEvent(withName: ReactNativeConstants.REPORT_UNEXPECTED_READER_DISCONNECT.rawValue, body: error) - } - - func localMobileReaderDidAcceptTermsOfService(_ reader: Reader) { + func tapToPayReaderDidAcceptTermsOfService(_ reader: Reader) { sendEvent(withName: ReactNativeConstants.ACCEPT_TERMS_OF_SERVICE.rawValue, body: ["reader": reader]) } @@ -418,7 +372,7 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe .setMetadata(metadata) if !paymentMethodTypes.isEmpty { - paymentParamsBuilder.setPaymentMethodTypes(paymentMethodTypes) + paymentParamsBuilder.setPaymentMethodTypes(paymentMethodTypes.map(Mappers.mapPaymentMethodType)) } let cardPresentParamsBuilder = CardPresentParametersBuilder() @@ -545,6 +499,13 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe .setRequestDynamicCurrencyConversion(requestDynamicCurrencyConversion) .setSurchargeNotice(surchargeNotice) + if let allowRedisplay = params["allowRedisplay"] as? String { + collectConfigBuilder.setAllowRedisplay(Mappers.mapToAllowRedisplay(allowToredisplay: allowRedisplay)) + } + if updatePaymentIntent, let surchargeNoticeValue = surchargeNotice { + collectConfigBuilder.setSurchargeNotice(surchargeNoticeValue) + } + if let eligibleAmount = params["tipEligibleAmount"] as? Int { do { let tippingConfig = try TippingConfigurationBuilder() @@ -695,10 +656,10 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe sendEvent(withName: ReactNativeConstants.CHANGE_CONNECTION_STATUS.rawValue, body: ["result": result]) } - func reader(_ reader: Reader, didStartReconnect cancelable: Cancelable) { + func reader(_ reader: Reader, didStartReconnect cancelable: Cancelable, disconnectReason: DisconnectReason) { self.cancelReaderConnectionCancellable = cancelable - let reader = Mappers.mapFromReader(reader) - sendEvent(withName: ReactNativeConstants.START_READER_RECONNECT.rawValue, body: ["reader": reader]) + let result = Mappers.mapFromReaderDisconnectReason(disconnectReason) + sendEvent(withName: ReactNativeConstants.START_READER_RECONNECT.rawValue, body: ["reason": result]) } func readerDidSucceedReconnect(_ reader: Reader) { @@ -873,19 +834,19 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe return } - let customerConsentCollected = params["customerConsentCollected"] as? Bool ?? false let enableCustomerCancellation = params["enableCustomerCancellation"] as? Bool ?? false + let allowRedisplay = params["allowRedisplay"] as? String ?? "unspecified" let setupIntentConfiguration: SetupIntentConfiguration do { - setupIntentConfiguration = try SetupIntentConfigurationBuilder().setEnableCustomerCancellation(enableCustomerCancellation) + setupIntentConfiguration = try SetupIntentConfigurationBuilder() + .setEnableCustomerCancellation(enableCustomerCancellation) .build() } catch { resolve(Errors.createError(nsError: error as NSError)) return } - - self.collectSetupIntentCancelable = Terminal.shared.collectSetupIntentPaymentMethod(setupIntent, customerConsentCollected: customerConsentCollected, setupConfig: setupIntentConfiguration) { si, collectError in + self.collectSetupIntentCancelable = Terminal.shared.collectSetupIntentPaymentMethod(setupIntent, allowRedisplay: Mappers.mapToAllowRedisplay(allowToredisplay: allowRedisplay), setupConfig: setupIntentConfiguration) { si, collectError in if let error = collectError as NSError? { resolve(Errors.createError(nsError: error)) } else if let setupIntent = si { @@ -1437,19 +1398,19 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe sendEvent(withName: ReactNativeConstants.DISCONNECT.rawValue, body: ["reason": result]) } - func localMobileReader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) { + func tapToPayReader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) { self.installUpdateCancelable = cancelable sendEvent(withName: ReactNativeConstants.START_INSTALLING_UPDATE.rawValue, body: ["result": Mappers.mapFromReaderSoftwareUpdate(update) ?? [:]]) } - func localMobileReader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) { + func tapToPayReader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) { let result: [AnyHashable : Any?] = [ "progress": String(progress), ] sendEvent(withName: ReactNativeConstants.REPORT_UPDATE_PROGRESS.rawValue, body: ["result": result]) } - func localMobileReader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: Error?) { + func tapToPayReader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: (any Error)?) { var result = Mappers.mapFromReaderSoftwareUpdate(update) ?? [:] if let nsError = error as NSError? { let errorAsDictionary = Errors.createError(nsError: nsError) @@ -1462,12 +1423,12 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe sendEvent(withName: ReactNativeConstants.FINISH_INSTALLING_UPDATE.rawValue, body: ["result": result]) } - func localMobileReader(_ reader: Reader, didRequestReaderInput inputOptions: ReaderInputOptions = []) { + func tapToPayReader(_ reader: Reader, didRequestReaderInput inputOptions: ReaderInputOptions = []) { let result = Mappers.mapFromReaderInputOptions(inputOptions) sendEvent(withName: ReactNativeConstants.REQUEST_READER_INPUT.rawValue, body: ["result": result]) } - func localMobileReader(_ reader: Reader, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) { + func tapToPayReader(_ reader: Reader, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) { let result = Mappers.mapFromReaderDisplayMessage(displayMessage) sendEvent(withName: ReactNativeConstants.REQUEST_READER_DISPLAY_MESSAGE.rawValue, body: ["result": result]) } diff --git a/ios/StripeTerminalReactNative.xcodeproj/project.pbxproj b/ios/StripeTerminalReactNative.xcodeproj/project.pbxproj index b853b640..6209a1c9 100644 --- a/ios/StripeTerminalReactNative.xcodeproj/project.pbxproj +++ b/ios/StripeTerminalReactNative.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13; + IPHONEOS_DEPLOYMENT_TARGET = 14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -220,7 +220,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13; + IPHONEOS_DEPLOYMENT_TARGET = 14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/src/StripeTerminalSdk.tsx b/src/StripeTerminalSdk.tsx index d8bf4744..4f155888 100644 --- a/src/StripeTerminalSdk.tsx +++ b/src/StripeTerminalSdk.tsx @@ -5,12 +5,9 @@ import type { DiscoverReadersParams, DiscoverReadersResultType, CancelDiscoveringResultType, - ConnectBluetoothReaderParams, DisconnectReaderResultType, RebootReaderResultType, Reader, - ConnectInternetReaderParams, - ConnectUsbReaderParams, CreatePaymentIntentParams, CollectSetupIntentPaymentMethodParams, PaymentIntentResultType, @@ -24,8 +21,6 @@ import type { CollectRefundPaymentMethodType, ConfirmRefundResultType, SetConnectionTokenParams, - ConnectHandoffParams, - ConnectLocalMobileParams, ConnectReaderResultType, CollectPaymentMethodParams, OfflineStatus, @@ -39,7 +34,8 @@ import type { CancelPaymentMethodParams, CollectDataParams, CollectDataResultType, - LocalMobileUxConfiguration, + TapToPayUxConfiguration, + ConnectReaderParams, } from './types'; const { StripeTerminalReactNative } = NativeModules; @@ -63,22 +59,9 @@ export interface StripeTerminalSdkType { // Cancel discovering readers cancelDiscovering(): CancelDiscoveringResultType; // Connect to reader via bluetooth - connectBluetoothReader( - params: ConnectBluetoothReaderParams - ): Promise; - // Connect to reader via internet - connectInternetReader( - params: ConnectInternetReaderParams - ): Promise; - connectHandoffReader( - params: ConnectHandoffParams - ): Promise; - connectLocalMobileReader( - params: ConnectLocalMobileParams - ): Promise; - // Connect to reader via USB - connectUsbReader( - params: ConnectUsbReaderParams + connectReader( + params: ConnectReaderParams, + discoveryMethod: Reader.DiscoveryMethod ): Promise; // Disconnect reader disconnectReader(): Promise; @@ -172,7 +155,7 @@ export interface StripeTerminalSdkType { supportsReadersOfType( params: Reader.ReaderSupportParams ): Promise; - setLocalMobileUxConfiguration(params: LocalMobileUxConfiguration): Promise<{ + setTapToPayUxConfiguration(params: TapToPayUxConfiguration): Promise<{ error?: StripeError; }>; getNativeSdkVersion(): Promise; diff --git a/src/__tests__/__snapshots__/functions.test.ts.snap b/src/__tests__/__snapshots__/functions.test.ts.snap index c1a01497..0fddbcc4 100644 --- a/src/__tests__/__snapshots__/functions.test.ts.snap +++ b/src/__tests__/__snapshots__/functions.test.ts.snap @@ -21,11 +21,7 @@ Object { "confirmPaymentIntent": [Function], "confirmRefund": [Function], "confirmSetupIntent": [Function], - "connectBluetoothReader": [Function], - "connectHandoffReader": [Function], - "connectInternetReader": [Function], - "connectLocalMobileReader": [Function], - "connectUsbReader": [Function], + "connectReader": [Function], "createPaymentIntent": [Function], "createSetupIntent": [Function], "disconnectReader": [Function], @@ -43,10 +39,10 @@ Object { "retrievePaymentIntent": [Function], "retrieveSetupIntent": [Function], "setConnectionToken": [Function], - "setLocalMobileUxConfiguration": [Function], "setReaderDisplay": [Function], "setReaderSettings": [Function], "setSimulatedCard": [Function], + "setTapToPayUxConfiguration": [Function], "simulateReaderUpdate": [Function], "supportsReadersOfType": [Function], } diff --git a/src/__tests__/__snapshots__/index.test.tsx.snap b/src/__tests__/__snapshots__/index.test.tsx.snap index 7a37f1ae..f941e0db 100644 --- a/src/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/__tests__/__snapshots__/index.test.tsx.snap @@ -37,7 +37,6 @@ Object { "READER_RECONNECT_SUCCEED": undefined, "REPORT_AVAILABLE_UPDATE": "REPORT_AVAILABLE_UPDATE", "REPORT_FORWARDING_ERROR": undefined, - "REPORT_UNEXPECTED_READER_DISCONNECT": "REPORT_UNEXPECTED_READER_DISCONNECT", "REPORT_UPDATE_PROGRESS": "REPORT_UPDATE_PROGRESS", "REQUEST_READER_DISPLAY_MESSAGE": "REQUEST_READER_DISPLAY_MESSAGE", "REQUEST_READER_INPUT": "REQUEST_READER_INPUT", diff --git a/src/__tests__/functions.test.ts b/src/__tests__/functions.test.ts index a50e8b70..74b40a66 100644 --- a/src/__tests__/functions.test.ts +++ b/src/__tests__/functions.test.ts @@ -64,19 +64,7 @@ describe('functions.test.ts', () => { discoverReaders: jest.fn().mockImplementation(() => ({})), cancelDiscovering: jest.fn().mockImplementation(() => ({})), - connectBluetoothReader: jest - .fn() - .mockImplementation(() => ({ reader: mockReader })), - connectHandoffReader: jest - .fn() - .mockImplementation(() => ({ reader: mockReader })), - connectInternetReader: jest - .fn() - .mockImplementation(() => ({ reader: mockReader })), - connectUsbReader: jest - .fn() - .mockImplementation(() => ({ reader: mockReader })), - connectLocalMobileReader: jest + connectReader: jest .fn() .mockImplementation(() => ({ reader: mockReader })), createPaymentIntent: jest @@ -156,42 +144,16 @@ describe('functions.test.ts', () => { }); }); - it('connectBluetoothReader returns a proper value', async () => { + it('connectReader returns a proper value', async () => { const functions = require('../functions'); await expect( - functions.connectBluetoothReader({} as any) + functions.connectReader({} as any, 'bluetooth') ).resolves.toEqual({ error: undefined, reader: mockReader, }); }); - it('connectHandoffReader returns a proper value', async () => { - const functions = require('../functions'); - await expect(functions.connectHandoffReader({} as any)).resolves.toEqual({ - error: undefined, - reader: mockReader, - }); - }); - - it('connectInternetReader returns a proper value', async () => { - const functions = require('../functions'); - await expect(functions.connectInternetReader({} as any)).resolves.toEqual( - { - error: undefined, - reader: mockReader, - } - ); - }); - - it('connectUsbReader returns a proper value', async () => { - const functions = require('../functions'); - await expect(functions.connectUsbReader({} as any)).resolves.toEqual({ - error: undefined, - reader: mockReader, - }); - }); - it('createPaymentIntent returns a proper value', async () => { const functions = require('../functions'); await expect(functions.createPaymentIntent({} as any)).resolves.toEqual({ @@ -355,16 +317,6 @@ describe('functions.test.ts', () => { await expect(functions.cancelCollectSetupIntent()).resolves.toEqual({}); }); - it('connectLocalMobileReader returns a proper value', async () => { - const functions = require('../functions'); - await expect( - functions.connectLocalMobileReader({} as any) - ).resolves.toEqual({ - error: undefined, - reader: mockReader, - }); - }); - it('setSimulatedCard returns a proper value', async () => { const functions = require('../functions'); await expect(functions.setSimulatedCard('_number')).resolves.toEqual({}); @@ -381,25 +333,13 @@ describe('functions.test.ts', () => { cancelDiscovering: jest .fn() .mockImplementation(() => ({ error: '_error' })), - connectBluetoothReader: jest - .fn() - .mockImplementation(() => ({ error: '_error' })), - connectHandoffReader: jest + connectReader: jest .fn() .mockImplementation(() => ({ error: '_error' })), disconnectReader: jest .fn() .mockImplementation(() => ({ error: '_error' })), rebootReader: jest.fn().mockImplementation(() => ({ error: '_error' })), - connectInternetReader: jest - .fn() - .mockImplementation(() => ({ error: '_error' })), - connectUsbReader: jest - .fn() - .mockImplementation(() => ({ error: '_error' })), - connectLocalMobileReader: jest - .fn() - .mockImplementation(() => ({ error: '_error' })), createPaymentIntent: jest .fn() .mockImplementation(() => ({ error: '_error' })), @@ -485,38 +425,15 @@ describe('functions.test.ts', () => { }); }); - it('connectBluetoothReader returns a proper value', async () => { + it('connectReader returns a proper value', async () => { const functions = require('../functions'); await expect( - functions.connectBluetoothReader({} as any) + functions.connectReader({} as any, 'bluetooth') ).resolves.toEqual({ error: '_error', }); }); - it('connectHandoffReader returns a proper value', async () => { - const functions = require('../functions'); - await expect(functions.connectHandoffReader({} as any)).resolves.toEqual({ - error: '_error', - }); - }); - - it('connectInternetReader returns a proper value', async () => { - const functions = require('../functions'); - await expect(functions.connectInternetReader({} as any)).resolves.toEqual( - { - error: '_error', - } - ); - }); - - it('connectUsbReader returns a proper value', async () => { - const functions = require('../functions'); - await expect(functions.connectUsbReader({} as any)).resolves.toEqual({ - error: '_error', - }); - }); - it('createPaymentIntent returns a proper value', async () => { const functions = require('../functions'); await expect(functions.createPaymentIntent({} as any)).resolves.toEqual({ @@ -635,16 +552,6 @@ describe('functions.test.ts', () => { }); }); - it('connectLocalMobileReader returns a proper value', async () => { - const functions = require('../functions'); - await expect( - functions.connectLocalMobileReader({} as any) - ).resolves.toEqual({ - error: '_error', - reader: undefined, - }); - }); - it('simulateReaderUpdate returns a proper value', async () => { const functions = require('../functions'); await expect(functions.simulateReaderUpdate({} as any)).resolves.toEqual({ diff --git a/src/components/StripeTerminalProvider.tsx b/src/components/StripeTerminalProvider.tsx index e5f755c3..73c596d6 100644 --- a/src/components/StripeTerminalProvider.tsx +++ b/src/components/StripeTerminalProvider.tsx @@ -25,7 +25,6 @@ const { REQUEST_READER_DISPLAY_MESSAGE, REQUEST_READER_INPUT, REPORT_AVAILABLE_UPDATE, - REPORT_UNEXPECTED_READER_DISCONNECT, REPORT_UPDATE_PROGRESS, START_INSTALLING_UPDATE, UPDATE_DISCOVERED_READERS, @@ -116,14 +115,6 @@ export function StripeTerminalProvider({ [log] ); - const didReportUnexpectedReaderDisconnect = useCallback( - ({ error }: { error?: StripeError }) => { - log('didReportUnexpectedReaderDisconnect', error); - emitter?.emit(REPORT_UNEXPECTED_READER_DISCONNECT, error); - }, - [log] - ); - const didReportAvailableUpdate = useCallback( ({ result }: EventResult) => { log('didReportAvailableUpdate', result); @@ -191,9 +182,9 @@ export function StripeTerminalProvider({ ); const didStartReaderReconnect = useCallback( - ({ reader }: { reader: Reader.Type }) => { - log('didStartReaderReconnect', reader); - emitter?.emit(START_READER_RECONNECT, reader); + ({ reason }: { reason: Reader.DisconnectReason }) => { + log('didStartReaderReconnect', reason); + emitter?.emit(START_READER_RECONNECT, reason); }, [log] ); @@ -282,10 +273,7 @@ export function StripeTerminalProvider({ useListener(UPDATE_DISCOVERED_READERS, didUpdateDiscoveredReaders); useListener(FINISH_DISCOVERING_READERS, didFinishDiscoveringReaders); - useListener( - REPORT_UNEXPECTED_READER_DISCONNECT, - didReportUnexpectedReaderDisconnect - ); + useListener(REQUEST_READER_INPUT, didRequestReaderInput); useListener(REQUEST_READER_DISPLAY_MESSAGE, didRequestReaderDisplayMessage); useListener(CHANGE_PAYMENT_STATUS, didChangePaymentStatus); diff --git a/src/functions.ts b/src/functions.ts index f901fb01..7c67ded0 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -6,12 +6,9 @@ import type { InitializeResultType, DiscoverReadersParams, DiscoverReadersResultType, - ConnectBluetoothReaderParams, CancelDiscoveringResultType, DisconnectReaderResultType, RebootReaderResultType, - ConnectInternetReaderParams, - ConnectUsbReaderParams, CreatePaymentIntentParams, CollectSetupIntentPaymentMethodParams, PaymentIntentResultType, @@ -25,9 +22,7 @@ import type { Reader, RefundParams, ConfirmRefundResultType, - ConnectLocalMobileParams, ConnectReaderResultType, - ConnectHandoffParams, CollectPaymentMethodParams, OfflineStatus, ICollectInputsParameters, @@ -40,7 +35,8 @@ import type { CancelPaymentMethodParams, CollectDataParams, CollectDataResultType, - LocalMobileUxConfiguration, + TapToPayUxConfiguration, + ConnectReaderParams, } from './types'; import { CommonError } from './types'; import { Platform } from 'react-native'; @@ -118,120 +114,15 @@ export async function cancelDiscovering(): Promise }, 'cancelDiscoverReaders')(); } -export async function connectBluetoothReader( - params: ConnectBluetoothReaderParams +export async function connectReader( + params: ConnectReaderParams, + discoveryMethod: Reader.DiscoveryMethod ): Promise { - return Logger.traceSdkMethod(async (innerParams) => { - try { - const { error, reader } = await StripeTerminalSdk.connectBluetoothReader( - innerParams - ); - - if (error) { - return { - error, - reader: undefined, - }; - } - return { - reader: reader!, - error: undefined, - }; - } catch (error) { - return { - error: error as any, - }; - } - }, 'connectBluetoothReader')(params); -} - -export async function connectHandoffReader( - params: ConnectHandoffParams -): Promise { - return Logger.traceSdkMethod(async (innerParams) => { - try { - const { error, reader } = await StripeTerminalSdk.connectHandoffReader( - innerParams - ); - - if (error) { - return { - error, - reader: undefined, - }; - } - return { - reader: reader!, - error: undefined, - }; - } catch (error) { - return { - error: error as any, - }; - } - }, 'connectHandoffReader')(params); -} - -export async function connectLocalMobileReader( - params: ConnectLocalMobileParams -): Promise { - return Logger.traceSdkMethod(async (innerParams) => { - try { - const { error, reader } = - await StripeTerminalSdk.connectLocalMobileReader(innerParams); - - if (error) { - return { - error, - reader: undefined, - }; - } - return { - reader: reader!, - error: undefined, - }; - } catch (error) { - return { - error: error as any, - }; - } - }, 'connectLocalMobileReader')(params); -} - -export async function connectInternetReader( - params: ConnectInternetReaderParams -): Promise { - return Logger.traceSdkMethod(async (innerParams) => { - try { - const { error, reader } = await StripeTerminalSdk.connectInternetReader( - innerParams - ); - - if (error) { - return { - error, - reader: undefined, - }; - } - return { - reader: reader!, - error: undefined, - }; - } catch (error) { - return { - error: error as any, - }; - } - }, 'connectInternetReader')(params); -} - -export async function connectUsbReader( - params: ConnectUsbReaderParams -): Promise { - return Logger.traceSdkMethod(async (innerParams) => { + return Logger.traceSdkMethod(async (innerParams, discoveryMethod) => { try { - const { error, reader } = await StripeTerminalSdk.connectUsbReader( - innerParams + const { error, reader } = await StripeTerminalSdk.connectReader( + innerParams, + discoveryMethod ); if (error) { @@ -249,7 +140,7 @@ export async function connectUsbReader( error: error as any, }; } - }, 'connectUsbReader')(params); + }, 'connectReader')(params, discoveryMethod); } export async function disconnectReader(): Promise { @@ -948,15 +839,15 @@ export async function supportsReadersOfType( }, 'supportsReadersOfType')(); } -export async function setLocalMobileUxConfiguration( - params: LocalMobileUxConfiguration +export async function setTapToPayUxConfiguration( + params: TapToPayUxConfiguration ): Promise<{ error?: StripeError; }> { if (Platform.OS === 'ios') { return { error: { - message: "'setLocalMobileUxConfiguration' is unsupported on iOS", + message: "'setTapToPayUxConfiguration' is unsupported on iOS", code: CommonError.Failed, }, }; @@ -964,14 +855,14 @@ export async function setLocalMobileUxConfiguration( return Logger.traceSdkMethod(async () => { try { - await StripeTerminalSdk.setLocalMobileUxConfiguration(params); + await StripeTerminalSdk.setTapToPayUxConfiguration(params); return {}; } catch (error) { return { error: error as any, }; } - }, 'setLocalMobileUxConfiguration')(); + }, 'setTapToPayUxConfiguration')(); } export async function getNativeSdkVersion(): Promise { diff --git a/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap b/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap index d04d14aa..6cbdb382 100644 --- a/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap +++ b/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap @@ -22,11 +22,7 @@ Object { "confirmPaymentIntent": [Function], "confirmRefund": [Function], "confirmSetupIntent": [Function], - "connectBluetoothReader": [Function], - "connectHandoffReader": [Function], - "connectInternetReader": [Function], - "connectLocalMobileReader": [Function], - "connectUsbReader": [Function], + "connectReader": [Function], "connectedReader": null, "createPaymentIntent": [Function], "createSetupIntent": [Function], @@ -48,10 +44,10 @@ Object { "rebootReader": [Function], "retrievePaymentIntent": [Function], "retrieveSetupIntent": [Function], - "setLocalMobileUxConfiguration": [Function], "setReaderDisplay": [Function], "setReaderSettings": [Function], "setSimulatedCard": [Function], + "setTapToPayUxConfiguration": [Function], "simulateReaderUpdate": [Function], "supportsReadersOfType": [Function], }, diff --git a/src/hooks/__tests__/useStripeTerminal.test.tsx b/src/hooks/__tests__/useStripeTerminal.test.tsx index 25310028..bfaae29f 100644 --- a/src/hooks/__tests__/useStripeTerminal.test.tsx +++ b/src/hooks/__tests__/useStripeTerminal.test.tsx @@ -14,10 +14,8 @@ function spyAllFunctions({ returnWith = null }: { returnWith?: any } = {}) { .spyOn(functions, 'createSetupIntent') .mockImplementation(createSetupIntent); // - const connectBluetoothReader = jest.fn(() => returnWith); - jest - .spyOn(functions, 'connectBluetoothReader') - .mockImplementation(connectBluetoothReader); + const connectReader = jest.fn(() => returnWith); + jest.spyOn(functions, 'connectReader').mockImplementation(connectReader); // const discoverReaders = jest.fn(() => returnWith); jest.spyOn(functions, 'discoverReaders').mockImplementation(discoverReaders); @@ -27,16 +25,6 @@ function spyAllFunctions({ returnWith = null }: { returnWith?: any } = {}) { .spyOn(functions, 'cancelDiscovering') .mockImplementation(cancelDiscovering); // - const connectInternetReader = jest.fn(() => returnWith); - jest - .spyOn(functions, 'connectInternetReader') - .mockImplementation(connectInternetReader); - // - const connectUsbReader = jest.fn(() => returnWith); - jest - .spyOn(functions, 'connectUsbReader') - .mockImplementation(connectUsbReader); - // const createPaymentIntent = jest.fn(() => returnWith); jest .spyOn(functions, 'createPaymentIntent') @@ -152,18 +140,6 @@ function spyAllFunctions({ returnWith = null }: { returnWith?: any } = {}) { .spyOn(functions, 'cancelCollectSetupIntent') .mockImplementation(cancelCollectSetupIntent); - // - const connectHandoffReader = jest.fn(() => returnWith); - jest - .spyOn(functions, 'connectHandoffReader') - .mockImplementation(connectHandoffReader); - - // - const connectLocalMobileReader = jest.fn(() => returnWith); - jest - .spyOn(functions, 'connectLocalMobileReader') - .mockImplementation(connectLocalMobileReader); - // const setSimulatedCard = jest.fn(() => returnWith); jest @@ -173,11 +149,9 @@ function spyAllFunctions({ returnWith = null }: { returnWith?: any } = {}) { return { discoverReaders, cancelDiscovering, - connectBluetoothReader, + connectReader, disconnectReader, rebootReader, - connectInternetReader, - connectUsbReader, createPaymentIntent, collectPaymentMethod, retrievePaymentIntent, @@ -199,8 +173,6 @@ function spyAllFunctions({ returnWith = null }: { returnWith?: any } = {}) { cancelCollectPaymentMethod, cancelCollectRefundPaymentMethod, cancelCollectSetupIntent, - connectHandoffReader, - connectLocalMobileReader, setSimulatedCard, }; } @@ -315,11 +287,7 @@ describe('useStripeTerminal.test.tsx', () => { result.current.collectRefundPaymentMethod({} as any); result.current.collectSetupIntentPaymentMethod({} as any); result.current.confirmSetupIntent({} as any); - result.current.connectBluetoothReader({} as any); - result.current.connectHandoffReader({} as any); - result.current.connectInternetReader({} as any); - result.current.connectLocalMobileReader({} as any); - result.current.connectUsbReader({} as any); + result.current.connectReader({} as any, {} as any); result.current.createPaymentIntent({} as any); result.current.createSetupIntent({} as any); result.current.disconnectReader(); @@ -354,7 +322,7 @@ describe('useStripeTerminal.test.tsx', () => { }); try { - await result.current.connectBluetoothReader({} as any); + await result.current.connectReader({} as any, {} as any); await result.current.discoverReaders({} as any); await result.current.cancelCollectPaymentMethod(); await result.current.cancelDiscovering(); @@ -367,11 +335,7 @@ describe('useStripeTerminal.test.tsx', () => { await result.current.collectRefundPaymentMethod({} as any); await result.current.collectSetupIntentPaymentMethod({} as any); await result.current.confirmSetupIntent({} as any); - await result.current.connectBluetoothReader({} as any); - await result.current.connectHandoffReader({} as any); - await result.current.connectInternetReader({} as any); - await result.current.connectLocalMobileReader({} as any); - await result.current.connectUsbReader({} as any); + await result.current.connectReader({} as any, {} as any); await result.current.createPaymentIntent({} as any); await result.current.createSetupIntent({} as any); await result.current.disconnectReader(); @@ -413,7 +377,7 @@ describe('useStripeTerminal.test.tsx', () => { }); await expect( - result.current.connectBluetoothReader({} as any) + result.current.connectReader({} as any, {} as any) ).resolves.toEqual('_value'); await expect(result.current.discoverReaders({} as any)).resolves.toEqual( '_value' @@ -451,18 +415,6 @@ describe('useStripeTerminal.test.tsx', () => { await expect( result.current.confirmSetupIntent({} as any) ).resolves.toEqual('_value'); - await expect( - result.current.connectHandoffReader({} as any) - ).resolves.toEqual('_value'); - await expect( - result.current.connectInternetReader({} as any) - ).resolves.toEqual('_value'); - await expect( - result.current.connectLocalMobileReader({} as any) - ).resolves.toEqual('_value'); - await expect(result.current.connectUsbReader({} as any)).resolves.toEqual( - '_value' - ); await expect( result.current.createPaymentIntent({} as any) ).resolves.toEqual('_value'); diff --git a/src/hooks/useStripeTerminal.tsx b/src/hooks/useStripeTerminal.tsx index bf5282d6..905e688c 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -2,17 +2,12 @@ import { useCallback, useContext } from 'react'; import type { DiscoverReadersParams, Reader, - ConnectInternetReaderParams, CreatePaymentIntentParams, - ConnectBluetoothReaderParams, - ConnectUsbReaderParams, GetLocationsParams, Cart, CreateSetupIntentParams, CollectSetupIntentPaymentMethodParams, RefundParams, - ConnectHandoffParams, - ConnectLocalMobileParams, CollectPaymentMethodParams, StripeError, PaymentStatus, @@ -27,16 +22,15 @@ import type { CancelSetupIntentMethodParams, CancelPaymentMethodParams, CollectDataParams, - LocalMobileUxConfiguration, + TapToPayUxConfiguration, + ConnectReaderParams, } from '../types'; import { discoverReaders, cancelDiscovering, - connectBluetoothReader, + connectReader, disconnectReader, rebootReader, - connectInternetReader, - connectUsbReader, createPaymentIntent, collectPaymentMethod, retrievePaymentIntent, @@ -58,8 +52,6 @@ import { clearCachedCredentials, cancelCollectPaymentMethod, cancelCollectSetupIntent, - connectHandoffReader, - connectLocalMobileReader, setSimulatedCard, cancelCollectRefundPaymentMethod, getOfflineStatus, @@ -73,7 +65,7 @@ import { getPaymentStatus, getConnectionStatus, getConnectedReader, - setLocalMobileUxConfiguration, + setTapToPayUxConfiguration, getNativeSdkVersion, } from '../functions'; import { StripeTerminalContext } from '../components/StripeTerminalContext'; @@ -92,7 +84,6 @@ export const { REQUEST_READER_DISPLAY_MESSAGE, REQUEST_READER_INPUT, REPORT_AVAILABLE_UPDATE, - REPORT_UNEXPECTED_READER_DISCONNECT, REPORT_UPDATE_PROGRESS, START_INSTALLING_UPDATE, UPDATE_DISCOVERED_READERS, @@ -155,7 +146,6 @@ export function useStripeTerminal(props?: Props) { onDidFinishInstallingUpdate, onDidReportAvailableUpdate, onDidReportReaderSoftwareUpdateProgress, - onDidReportUnexpectedReaderDisconnect, onDidStartInstallingUpdate, onDidRequestReaderInput, onDidRequestReaderDisplayMessage, @@ -207,19 +197,6 @@ export function useStripeTerminal(props?: Props) { [onFinishDiscoveringReaders] ); - const didReportUnexpectedReaderDisconnect = useCallback( - ({ error }: { error?: StripeError }) => { - setConnectedReader(null); - setDiscoveredReaders([]); - onDidReportUnexpectedReaderDisconnect?.(error); - }, - [ - onDidReportUnexpectedReaderDisconnect, - setConnectedReader, - setDiscoveredReaders, - ] - ); - const didReportAvailableUpdate = useCallback( ({ result }: EventResult) => { onDidReportAvailableUpdate?.(result); @@ -290,9 +267,12 @@ export function useStripeTerminal(props?: Props) { [onDidChangeConnectionStatus] ); - const didStartReaderReconnect = useCallback(() => { - onDidStartReaderReconnect?.(); - }, [onDidStartReaderReconnect]); + const didStartReaderReconnect = useCallback( + ({ reason }: { reason?: Reader.DisconnectReason }) => { + onDidStartReaderReconnect?.(reason); + }, + [onDidStartReaderReconnect] + ); const didSucceedReaderReconnect = useCallback(() => { onDidSucceedReaderReconnect?.(); @@ -330,8 +310,10 @@ export function useStripeTerminal(props?: Props) { const didDisconnect = useCallback( ({ reason }: { reason?: Reader.DisconnectReason }) => { onDidDisconnect?.(reason); + setConnectedReader(null); + setDiscoveredReaders([]); }, - [onDidDisconnect] + [onDidDisconnect, setConnectedReader, setDiscoveredReaders] ); const didUpdateBatteryLevel = useCallback( @@ -363,10 +345,7 @@ export function useStripeTerminal(props?: Props) { useListener(UPDATE_DISCOVERED_READERS, didUpdateDiscoveredReaders); useListener(FINISH_DISCOVERING_READERS, didFinishDiscoveringReaders); - useListener( - REPORT_UNEXPECTED_READER_DISCONNECT, - didReportUnexpectedReaderDisconnect - ); + useListener(REQUEST_READER_INPUT, didRequestReaderInput); useListener(REQUEST_READER_DISPLAY_MESSAGE, didRequestReaderDisplayMessage); useListener(CHANGE_PAYMENT_STATUS, didChangePaymentStatus); @@ -421,15 +400,18 @@ export function useStripeTerminal(props?: Props) { return response; }, [setLoading, setDiscoveredReaders, _isInitialized]); - const _connectBluetoothReader = useCallback( - async (params: ConnectBluetoothReaderParams) => { + const _connectReader = useCallback( + async ( + params: ConnectReaderParams, + discoveryMethod: Reader.DiscoveryMethod + ) => { if (!_isInitialized()) { console.error(NOT_INITIALIZED_ERROR_MESSAGE); throw Error(NOT_INITIALIZED_ERROR_MESSAGE); } setLoading(true); - const response = await connectBluetoothReader(params); + const response = await connectReader(params, discoveryMethod); if (response.reader && !response.error) { setConnectedReader(response.reader); @@ -441,86 +423,6 @@ export function useStripeTerminal(props?: Props) { [setConnectedReader, setLoading, _isInitialized] ); - const _connectInternetReader = useCallback( - async (params: ConnectInternetReaderParams) => { - if (!_isInitialized()) { - console.error(NOT_INITIALIZED_ERROR_MESSAGE); - throw Error(NOT_INITIALIZED_ERROR_MESSAGE); - } - setLoading(true); - - const response = await connectInternetReader(params); - - if (response.reader) { - setConnectedReader(response.reader); - } - setLoading(false); - - return response; - }, - [setConnectedReader, setLoading, _isInitialized] - ); - - const _connectUsbReader = useCallback( - async (params: ConnectUsbReaderParams) => { - if (!_isInitialized()) { - console.error(NOT_INITIALIZED_ERROR_MESSAGE); - throw Error(NOT_INITIALIZED_ERROR_MESSAGE); - } - setLoading(true); - - const response = await connectUsbReader(params); - - if (response.reader && !response.error) { - setConnectedReader(response.reader); - } - setLoading(false); - - return response; - }, - [_isInitialized, setConnectedReader, setLoading] - ); - - const _connectLocalMobileReader = useCallback( - async (params: ConnectLocalMobileParams) => { - if (!_isInitialized()) { - console.error(NOT_INITIALIZED_ERROR_MESSAGE); - throw Error(NOT_INITIALIZED_ERROR_MESSAGE); - } - setLoading(true); - - const response = await connectLocalMobileReader(params); - - if (response.reader) { - setConnectedReader(response.reader); - } - setLoading(false); - - return response; - }, - [_isInitialized, setConnectedReader, setLoading] - ); - - const _connectHandoffReader = useCallback( - async (params: ConnectHandoffParams) => { - if (!_isInitialized()) { - console.error(NOT_INITIALIZED_ERROR_MESSAGE); - throw Error(NOT_INITIALIZED_ERROR_MESSAGE); - } - setLoading(true); - - const response = await connectHandoffReader(params); - - if (response.reader) { - setConnectedReader(response.reader); - } - setLoading(false); - - return response; - }, - [_isInitialized, setConnectedReader, setLoading] - ); - const _disconnectReader = useCallback(async () => { if (!_isInitialized()) { console.error(NOT_INITIALIZED_ERROR_MESSAGE); @@ -1053,15 +955,15 @@ export function useStripeTerminal(props?: Props) { [_isInitialized, setLoading] ); - const _setLocalMobileUxConfiguration = useCallback( - async (params: LocalMobileUxConfiguration) => { + const _setTapToPayUxConfiguration = useCallback( + async (params: TapToPayUxConfiguration) => { if (!_isInitialized()) { console.error(NOT_INITIALIZED_ERROR_MESSAGE); throw Error(NOT_INITIALIZED_ERROR_MESSAGE); } setLoading(true); - const response = await setLocalMobileUxConfiguration(params); + const response = await setTapToPayUxConfiguration(params); setLoading(false); @@ -1078,11 +980,9 @@ export function useStripeTerminal(props?: Props) { initialize: _initialize, discoverReaders: _discoverReaders, cancelDiscovering: _cancelDiscovering, - connectBluetoothReader: _connectBluetoothReader, + connectReader: _connectReader, disconnectReader: _disconnectReader, rebootReader: _rebootReader, - connectInternetReader: _connectInternetReader, - connectUsbReader: _connectUsbReader, createPaymentIntent: _createPaymentIntent, collectPaymentMethod: _collectPaymentMethod, retrievePaymentIntent: _retrievePaymentIntent, @@ -1105,8 +1005,6 @@ export function useStripeTerminal(props?: Props) { cancelCollectPaymentMethod: _cancelCollectPaymentMethod, cancelCollectRefundPaymentMethod: _cancelCollectRefundPaymentMethod, cancelCollectSetupIntent: _cancelCollectSetupIntent, - connectHandoffReader: _connectHandoffReader, - connectLocalMobileReader: _connectLocalMobileReader, setSimulatedCard: _setSimulatedCard, getOfflineStatus: _getOfflineStatus, getPaymentStatus: _getPaymentStatus, @@ -1119,7 +1017,7 @@ export function useStripeTerminal(props?: Props) { collectData: _collectData, cancelReaderReconnection: _cancelReaderReconnection, supportsReadersOfType: _supportsReadersOfType, - setLocalMobileUxConfiguration: _setLocalMobileUxConfiguration, + setTapToPayUxConfiguration: _setTapToPayUxConfiguration, getNativeSdkVersion: _getNativeSdkVersion, emitter: emitter, discoveredReaders, diff --git a/src/index.tsx b/src/index.tsx index 9da0bf70..f722da9e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -18,7 +18,6 @@ export { REQUEST_READER_DISPLAY_MESSAGE, REQUEST_READER_INPUT, REPORT_AVAILABLE_UPDATE, - REPORT_UNEXPECTED_READER_DISCONNECT, REPORT_UPDATE_PROGRESS, START_INSTALLING_UPDATE, UPDATE_DISCOVERED_READERS, diff --git a/src/types/Reader.ts b/src/types/Reader.ts index 1c2fa2be..7089af14 100644 --- a/src/types/Reader.ts +++ b/src/types/Reader.ts @@ -30,7 +30,7 @@ export namespace Reader { | 'bluetoothProximity' | 'bluetoothScan' | 'internet' - | 'localMobile'; + | 'tapToPay'; } export namespace Android { @@ -51,7 +51,7 @@ export namespace Reader { export type DiscoveryMethod = | 'bluetoothScan' | 'internet' - | 'localMobile' + | 'tapToPay' | 'handoff' | 'usb'; } @@ -100,7 +100,7 @@ export namespace Reader { | 'stripeS710Devkit' | 'stripeS710' | 'cotsDevice' - | 'appleBuiltIn' + | 'tapToPay' | 'etna'; export type InputOptions = 'insertCard' | 'swipeCard' | 'tapCard'; @@ -117,7 +117,11 @@ export namespace Reader { | 'checkMobileDevice' | 'cardRemovedTooEarly'; - export type ConnectionStatus = 'connected' | 'connecting' | 'notConnected'; + export type ConnectionStatus = + | 'connected' + | 'connecting' + | 'notConnected' + | 'discovering'; export type DisconnectReason = | 'disconnectRequested' diff --git a/src/types/index.ts b/src/types/index.ts index 727a319a..3d27257b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,31 +35,35 @@ export type GetLocationsParams = { startingAfter?: string; }; -export type ConnectBluetoothReaderParams = { +export interface ConnectReaderParams { reader: Reader.Type; +} + +export interface ConnectBluetoothReaderParams extends ConnectReaderParams { locationId?: string; autoReconnectOnUnexpectedDisconnect?: boolean; -}; +} -export type ConnectUsbReaderParams = { - reader: Reader.Type; +export interface ConnectUsbReaderParams extends ConnectReaderParams { locationId?: string; autoReconnectOnUnexpectedDisconnect?: boolean; -}; +} -export type ConnectLocalMobileParams = { - reader: Reader.Type; +export interface ConnectTapToPayParams extends ConnectReaderParams { locationId?: string; onBehalfOf?: string; merchantDisplayName?: string; tosAcceptancePermitted?: boolean; autoReconnectOnUnexpectedDisconnect?: boolean; -}; +} -export type ConnectHandoffParams = { - reader: Reader.Type; +export interface ConnectHandoffParams extends ConnectReaderParams { locationId?: string; -}; +} + +export interface ConnectInternetReaderParams extends ConnectReaderParams { + failIfInUse?: boolean; +} export type LineItem = { displayName: string; @@ -74,11 +78,6 @@ export type Cart = { lineItems: LineItem[]; }; -export type ConnectInternetReaderParams = { - reader: Reader.Type; - failIfInUse?: boolean; -}; - export enum CommonError { Failed = 'Failed', Canceled = 'Canceled', @@ -201,6 +200,7 @@ export type CollectPaymentMethodParams = { enableCustomerCancellation?: boolean; requestDynamicCurrencyConversion?: boolean; surchargeNotice?: string; + allowRedisplay?: AllowRedisplay; }; export type ConfirmPaymentMethodParams = { @@ -221,11 +221,13 @@ export type CancelSetupIntentMethodParams = { }; export type CollectSetupIntentPaymentMethodParams = { - customerConsentCollected?: boolean; + allowRedisplay?: AllowRedisplay; enableCustomerCancellation?: boolean; setupIntent: SetupIntent.Type; }; +export type AllowRedisplay = 'always' | 'limited' | 'unspecified'; + export type CreateSetupIntentParams = { customer?: string; }; @@ -352,7 +354,11 @@ export type OfflineStatus = { export type ReaderEvent = 'cardInserted' | 'cardRemoved'; -export type ConnectionStatus = 'notConnected' | 'connecting' | 'connected'; +export type ConnectionStatus = + | 'notConnected' + | 'connecting' + | 'connected' + | 'discovering'; /** * @ignore @@ -364,7 +370,6 @@ export type EventResult = { export type UserCallbacks = { onUpdateDiscoveredReaders?(readers: Reader.Type[]): void; onFinishDiscoveringReaders?(error?: StripeError): void; - onDidReportUnexpectedReaderDisconnect?(error?: StripeError): void; onDidReportAvailableUpdate?(update: Reader.SoftwareUpdate): void; onDidStartInstallingUpdate?(update: Reader.SoftwareUpdate): void; onDidReportReaderSoftwareUpdateProgress?(progress: string): void; @@ -376,7 +381,7 @@ export type UserCallbacks = { onDidChangeConnectionStatus?(status: Reader.ConnectionStatus): void; onDidChangePaymentStatus?(status: PaymentStatus): void; - onDidStartReaderReconnect?(): void; + onDidStartReaderReconnect?(reason?: Reader.DisconnectReason): void; onDidSucceedReaderReconnect?(): void; onDidFailReaderReconnect?(): void; @@ -517,7 +522,7 @@ export enum ToggleResult { } export type OfflineDetails = { - storedAt: string; + storedAtMs: string; requiresUpload: boolean; cardPresentDetails: OfflineCardPresentDetails; amountDetails: AmountDetails; @@ -567,7 +572,7 @@ export type CollectDataResultType = error: StripeError; }; -export type LocalMobileUxConfiguration = { +export type TapToPayUxConfiguration = { tapZone?: TapZone; darkMode?: DarkMode; colors?: Colors; diff --git a/stripe-terminal-react-native.podspec b/stripe-terminal-react-native.podspec index 14267410..12b83f23 100644 --- a/stripe-terminal-react-native.podspec +++ b/stripe-terminal-react-native.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| s.license = package['license'] s.authors = package['author'] - s.platforms = { ios: '13.0' } + s.platforms = { ios: '14.0' } s.source = { git: 'https://github.com/stripe/stripe-terminal-react-native.git', tag: s.version.to_s } s.source_files = 'ios/**/*.{h,m,mm,swift}' @@ -21,5 +21,5 @@ Pod::Spec.new do |s| end s.dependency 'React-Core' - s.dependency 'StripeTerminal', '~> 3.9.1' + s.dependency 'StripeTerminal', '~> 4.0.0' end