Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support offline mode #564

Merged
merged 21 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions android/src/main/java/com/stripeterminalreactnative/Mappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.stripe.stripeterminal.external.models.ConnectionStatus
import com.stripe.stripeterminal.external.models.DeviceType
import com.stripe.stripeterminal.external.models.Location
import com.stripe.stripeterminal.external.models.LocationStatus
import com.stripe.stripeterminal.external.models.NetworkStatus
import com.stripe.stripeterminal.external.models.OfflineStatus
import com.stripe.stripeterminal.external.models.PaymentIntent
import com.stripe.stripeterminal.external.models.PaymentIntentStatus
import com.stripe.stripeterminal.external.models.PaymentMethod
Expand Down Expand Up @@ -487,3 +489,25 @@ fun mapFromReceiptDetails(receiptDetails: ReceiptDetails?): ReadableMap =
putString("tsi", receiptDetails?.tsi)
putString("tvr", receiptDetails?.tvr)
}

internal fun mapFromNetworkStatus(status: NetworkStatus): String {
return when (status) {
NetworkStatus.ONLINE -> "online"
NetworkStatus.OFFLINE -> "offline"
NetworkStatus.UNKNOWN -> "unknown"
}
}

fun mapFromOfflineStatus(offlineStatus: OfflineStatus): ReadableMap =
nativeMapOf {
putString("networkStatus", mapFromNetworkStatus(offlineStatus.sdk.networkStatus))
billfinn-stripe marked this conversation as resolved.
Show resolved Hide resolved
putInt("offlinePaymentsCount", offlineStatus.sdk.offlinePaymentsCount)

val map = nativeMapOf {
offlineStatus.sdk.offlinePaymentAmountsByCurrency.forEach {
putInt(it.key, it.value.toInt())
}
}
putMap("offlinePaymentAmountsByCurrency", map)
}

Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ enum class ReactNativeConstants(val listenerName: String) {
START_READER_RECONNECT("didStartReaderReconnect"),
READER_RECONNECT_SUCCEED("didSucceedReaderReconnect"),
READER_RECONNECT_FAIL("didFailReaderReconnect"),
CHANGE_OFFLINE_STATUS("didChangeOfflineStatus"),
FORWARD_PAYMENT_INTENT("didForwardPaymentIntent"),
REPORT_FORWARDING_ERROR("didReportForwardingError"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import com.stripe.stripeterminal.external.models.CardPresentParameters
import com.stripe.stripeterminal.external.models.CardPresentRoutingOptionParameters
import com.stripe.stripeterminal.external.models.Cart
import com.stripe.stripeterminal.external.models.CollectConfiguration
import com.stripe.stripeterminal.external.models.CreateConfiguration
import com.stripe.stripeterminal.external.models.DiscoveryConfiguration
import com.stripe.stripeterminal.external.models.ListLocationsParameters
import com.stripe.stripeterminal.external.models.OfflineBehavior
import com.stripe.stripeterminal.external.models.PaymentIntent
import com.stripe.stripeterminal.external.models.PaymentIntentParameters
import com.stripe.stripeterminal.external.models.PaymentMethodOptionsParameters
Expand All @@ -46,6 +48,7 @@ import com.stripeterminalreactnative.ktx.connectReader
import com.stripeterminalreactnative.listener.RNBluetoothReaderListener
import com.stripeterminalreactnative.listener.RNDiscoveryListener
import com.stripeterminalreactnative.listener.RNHandoffReaderListener
import com.stripeterminalreactnative.listener.RNOfflineListener
import com.stripeterminalreactnative.listener.RNReaderReconnectionListener
import com.stripeterminalreactnative.listener.RNTerminalListener
import com.stripeterminalreactnative.listener.RNUsbReaderListener
Expand Down Expand Up @@ -91,6 +94,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :

override fun getName(): String = "StripeTerminalReactNative"

@OptIn(OfflineMode::class)
@ReactMethod
@Suppress("unused")
fun initialize(params: ReadableMap, promise: Promise) = withExceptionResolver(promise) {
Expand All @@ -101,7 +105,8 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
this.context.applicationContext,
mapToLogLevel(params.getString("logLevel")),
tokenProvider,
RNTerminalListener(context)
RNTerminalListener(context),
RNOfflineListener(context),
)
NativeTypeFactory.writableNativeMap()
} else {
Expand Down Expand Up @@ -323,6 +328,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
getBoolean(paymentMethodOptions, "requestIncrementalAuthorizationSupport")
val requestedPriority = paymentMethodOptions?.getString("requestedPriority")
val captureMethod = params.getString("captureMethod")
val offlineBehavior = params.getString("offlineBehavior")

val paymentMethodTypes = paymentMethods?.toArrayList()?.mapNotNull {
if (it is String) PaymentMethodType.valueOf(it.uppercase())
Expand Down Expand Up @@ -397,11 +403,20 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
}
}

val offlineBehaviorParam = offlineBehavior.let {
when (it) {
"prefer_online" -> OfflineBehavior.PREFER_ONLINE
"require_online" -> OfflineBehavior.REQUIRE_ONLINE
"force_offline" -> OfflineBehavior.FORCE_OFFLINE
else -> OfflineBehavior.PREFER_ONLINE
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize we are following the same pattern we do elsewhere, but I think we should consider more strictly validating the input string parameters, rather than assuming a default value whenever the string doesn't match an expected value. Specifically, I think we should raise an exception here rather than defaulting to PREFER_ONLINE.

No change requested for this PR, but please consider making this change broadly throughout the RN code base in the future.

}
}
billfinn-stripe marked this conversation as resolved.
Show resolved Hide resolved

val uuid = UUID.randomUUID().toString()

terminal.createPaymentIntent(intentParams.build(), RNPaymentIntentCallback(promise, uuid) { pi ->
paymentIntents[uuid] = pi
})
}, CreateConfiguration(offlineBehaviorParam))
}

@OptIn(OfflineMode::class)
Expand Down Expand Up @@ -673,6 +688,40 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
terminal.confirmRefund(RNRefundCallback(promise))
}

@OptIn(OfflineMode::class)
@ReactMethod
@Suppress("unused")
fun getOfflineStatus(promise: Promise) {
promise.resolve(
nativeMapOf {
val sdkMap = nativeMapOf {
putInt("offlinePaymentsCount", terminal.offlineStatus.sdk.offlinePaymentsCount)

val map = nativeMapOf {
terminal.offlineStatus.sdk.offlinePaymentAmountsByCurrency.forEach {
putInt(it.key, it.value.toInt())
}
}
putMap("offlinePaymentAmountsByCurrency", map)
}

val readerMap = nativeMapOf {
putInt("offlinePaymentsCount", terminal.offlineStatus.reader?.offlinePaymentsCount?:0)

val map = nativeMapOf {
terminal.offlineStatus.reader?.offlinePaymentAmountsByCurrency?.forEach {
putInt(it.key, it.value.toInt())
}
}
putMap("offlinePaymentAmountsByCurrency", map)
}

putMap("sdk", sdkMap)
putMap("reader", readerMap)
}
)
}

private fun cancelOperation(
promise: Promise,
cancelable: Cancelable?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.stripeterminalreactnative.listener

import com.facebook.react.bridge.ReactApplicationContext
import com.stripe.stripeterminal.external.OfflineMode
import com.stripe.stripeterminal.external.callable.OfflineListener
import com.stripe.stripeterminal.external.models.OfflineStatus
import com.stripe.stripeterminal.external.models.PaymentIntent
import com.stripe.stripeterminal.external.models.TerminalException
import com.stripeterminalreactnative.ReactExtensions.sendEvent
import com.stripeterminalreactnative.ReactNativeConstants
import com.stripeterminalreactnative.createError
import com.stripeterminalreactnative.mapFromOfflineStatus
import com.stripeterminalreactnative.mapFromPaymentIntent
import com.stripeterminalreactnative.nativeMapOf

@OptIn(OfflineMode::class)
class RNOfflineListener(
private val context: ReactApplicationContext,
): OfflineListener {
override fun onOfflineStatusChange(offlineStatus: OfflineStatus) {
context.sendEvent(ReactNativeConstants.CHANGE_OFFLINE_STATUS.listenerName) {
putMap("result", mapFromOfflineStatus(offlineStatus))
}
}

override fun onPaymentIntentForwarded(paymentIntent: PaymentIntent, e: TerminalException?) {
context.sendEvent(ReactNativeConstants.FORWARD_PAYMENT_INTENT.listenerName) {
putMap("result", mapFromPaymentIntent(paymentIntent, ""))
billfinn-stripe marked this conversation as resolved.
Show resolved Hide resolved
putMap("error", nativeMapOf {
putString("code", e?.errorCode.toString())
putString("message", e?.errorMessage)
})
}
}

override fun onForwardingFailure(e: TerminalException) {
context.sendEvent(ReactNativeConstants.REPORT_FORWARDING_ERROR.listenerName) {
putMap("result", createError(e))
}
}
}
1 change: 1 addition & 0 deletions bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ workflows:
# 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.2 && bundle install && \
gem install cocoapods -v 1.14.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
title: Set up cocoapods
Expand Down
14 changes: 7 additions & 7 deletions dev-app/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,11 @@ PODS:
- React-jsi (= 0.72.7)
- React-logger (= 0.72.7)
- React-perflogger (= 0.72.7)
- RNCAsyncStorage (1.19.5):
- RNCAsyncStorage (1.19.8):
- React-Core
- RNCPicker (2.5.1):
- React-Core
- RNGestureHandler (2.13.4):
- RNGestureHandler (2.14.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- RNScreens (3.27.0):
Expand Down Expand Up @@ -689,9 +689,9 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
boost: 57d2868c099736d80fcd648bf211b4431e51a558
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 5fbbff1d7734827299274638deb8ba3024f6c597
FBReactNativeSpec: 638095fe8a01506634d77b260ef8a322019ac671
Flipper: 6edb735e6c3e332975d1b17956bcc584eccf5818
Expand All @@ -703,7 +703,7 @@ SPEC CHECKSUMS:
Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9
FlipperKit: 2efad7007d6745a3f95e4034d547be637f89d3f6
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 476ee3e89abb49e07f822b48323c51c57124b572
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 9180d43df05c1ed658a87cc733dc3044cf90c00a
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
Expand Down Expand Up @@ -740,9 +740,9 @@ SPEC CHECKSUMS:
React-runtimescheduler: 7649c3b46c8dee1853691ecf60146a16ae59253c
React-utils: 56838edeaaf651220d1e53cd0b8934fb8ce68415
ReactCommon: 5f704096ccf7733b390f59043b6fa9cc180ee4f6
RNCAsyncStorage: f2974eca860c16a3e56eea5771fda8d12e2d2057
RNCAsyncStorage: 687bb9e85dd3d45b966662440dcfc0cd962347e6
RNCPicker: 529d564911e93598cc399b56cc0769ce3675f8c8
RNGestureHandler: 6e46dde1f87e5f018a54fe5d40cd0e0b942b49ee
RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741
RNScreens: 3c2d122f5e08c192e254c510b212306da97d2581
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
stripe-terminal-react-native: 61209e78f472f094096b940ddfa2316f90cbead0
Expand Down
3 changes: 2 additions & 1 deletion dev-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"react-native-gesture-handler": "^2.13.4",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-safe-area-context": "^4.7.4",
"react-native-screens": "^3.27.0"
"react-native-screens": "^3.27.0",
"react-native-root-toast":"^3.5.1"
},
"devDependencies": {
"@babel/cli": "^7.15.7",
Expand Down
6 changes: 6 additions & 0 deletions dev-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { Alert, LogBox } from 'react-native';

import { AppContext } from './AppContext';
import DatabaseScreen from './screens/DatabaseScreen';

export type RouteParamList = {
UpdateReader: {
Expand Down Expand Up @@ -214,6 +215,11 @@ export default function App() {
options={{ headerTitle: 'Merchant Select' }}
component={MerchantSelectScreen}
/>
<Stack.Screen
name="DatabaseScreen"
options={{ headerTitle: 'DatabaseScreen' }}
component={DatabaseScreen}
/>
<Stack.Screen
name="DiscoverReadersScreen"
options={{ headerTitle: 'Discovery' }}
Expand Down
1 change: 1 addition & 0 deletions dev-app/src/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export const colors = {
slate: '#0A2540',
gray: '#A3B3C1',
red: '#ff3a30',
green: '#00FF00',
};
Loading