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

Bluetooth/Usb automatic reconnection #516

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ enum class ReactNativeConstants(val listenerName: String) {
REPORT_UPDATE_PROGRESS("didReportReaderSoftwareUpdateProgress"),
START_INSTALLING_UPDATE("didStartInstallingUpdate"),
UPDATE_DISCOVERED_READERS("didUpdateDiscoveredReaders"),
START_READER_RECONNECT("didStartReaderReconnect"),
READER_RECONNECT_SUCCEED("didSucceedReaderReconnect"),
READER_RECONNECT_FAIL("didFailReaderReconnect"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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.RNReaderReconnectionListener
import com.stripeterminalreactnative.listener.RNTerminalListener
import com.stripeterminalreactnative.listener.RNUsbReaderListener
import kotlinx.coroutines.CoroutineScope
Expand All @@ -62,6 +63,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
private var collectSetupIntentCancelable: Cancelable? = null
private var installUpdateCancelable: Cancelable? = null
private var readReusableCardCancelable: Cancelable? = null
private var cancelReaderConnectionCancellable: Cancelable? = null

private var paymentIntents: HashMap<String, PaymentIntent?> = HashMap()
private var setupIntents: HashMap<String, SetupIntent?> = HashMap()
Expand Down Expand Up @@ -221,11 +223,27 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
val locationId =
params.getString("locationId") ?: selectedReader.location?.id.orEmpty()

val autoReconnectOnUnexpectedDisconnect = if (discoveryMethod == DiscoveryMethod.BLUETOOTH_SCAN || discoveryMethod == DiscoveryMethod.USB) {
params.getBoolean("autoReconnectOnUnexpectedDisconnect") ?: false
} else false

val reconnectionListener = RNReaderReconnectionListener(context) {
cancelReaderConnectionCancellable = it
}
val connectedReader =
terminal.connectReader(discoveryMethod, selectedReader, locationId, listener)
promise.resolve(nativeMapOf {
putMap("reader", mapFromReader(connectedReader))
})
terminal.connectReader(
discoveryMethod,
selectedReader,
locationId,
autoReconnectOnUnexpectedDisconnect,
listener,
reconnectionListener
)
promise.resolve(
nativeMapOf {
putMap("reader", mapFromReader(connectedReader))
}
)
}
}
}
Expand Down Expand Up @@ -274,6 +292,12 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
terminal.disconnectReader(NoOpCallback(promise))
}

@ReactMethod
@Suppress("unused")
fun cancelReaderReconnection(promise: Promise) {
cancelOperation(promise, cancelReaderConnectionCancellable, "readerReconnection")
}

@ReactMethod
@Suppress("unused")
fun createPaymentIntent(params: ReadableMap, promise: Promise) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import com.stripe.stripeterminal.external.callable.BluetoothReaderListener
import com.stripe.stripeterminal.external.callable.HandoffReaderListener
import com.stripe.stripeterminal.external.callable.ReaderCallback
import com.stripe.stripeterminal.external.callable.ReaderListenable
import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener
import com.stripe.stripeterminal.external.callable.UsbReaderListener
import com.stripe.stripeterminal.external.models.ConnectionConfiguration.BluetoothConnectionConfiguration
import com.stripe.stripeterminal.external.models.ConnectionConfiguration.EmbeddedConnectionConfiguration
import com.stripe.stripeterminal.external.models.ConnectionConfiguration.HandoffConnectionConfiguration
import com.stripe.stripeterminal.external.models.ConnectionConfiguration.InternetConnectionConfiguration
import com.stripe.stripeterminal.external.models.ConnectionConfiguration.LocalMobileConnectionConfiguration
import com.stripe.stripeterminal.external.models.ConnectionConfiguration.UsbConnectionConfiguration
import com.stripe.stripeterminal.external.models.DiscoveryMethod
import com.stripe.stripeterminal.external.models.DiscoveryMethod.BLUETOOTH_SCAN
import com.stripe.stripeterminal.external.models.DiscoveryMethod.EMBEDDED
import com.stripe.stripeterminal.external.models.DiscoveryMethod.HANDOFF
import com.stripe.stripeterminal.external.models.DiscoveryMethod.INTERNET
import com.stripe.stripeterminal.external.models.DiscoveryMethod.LOCAL_MOBILE
Expand Down Expand Up @@ -104,12 +103,17 @@ suspend fun Terminal.connectReader(
discoveryMethod: DiscoveryMethod,
reader: Reader,
locationId: String,
autoReconnectOnUnexpectedDisconnect: Boolean = false,
listener: ReaderListenable? = null,
reconnectionListener: ReaderReconnectionListener
): Reader = when (discoveryMethod) {
BLUETOOTH_SCAN -> {
if (listener is BluetoothReaderListener)
connectBluetoothReader(reader, BluetoothConnectionConfiguration(locationId), listener)
else connectBluetoothReader(reader, BluetoothConnectionConfiguration(locationId))
val connConfig = BluetoothConnectionConfiguration(locationId, autoReconnectOnUnexpectedDisconnect, reconnectionListener)
if (listener is BluetoothReaderListener) {
connectBluetoothReader(reader, connConfig, listener)
} else {
connectBluetoothReader(reader, connConfig)
}
}
LOCAL_MOBILE -> connectLocalMobileReader(reader, LocalMobileConnectionConfiguration(locationId))
INTERNET -> connectInternetReader(reader, InternetConnectionConfiguration())
Expand All @@ -119,9 +123,12 @@ suspend fun Terminal.connectReader(
else connectHandoffReader(reader, HandoffConnectionConfiguration(locationId))
}
USB -> {
if (listener is UsbReaderListener)
connectUsbReader(reader, UsbConnectionConfiguration(locationId), listener)
else connectUsbReader(reader, UsbConnectionConfiguration(locationId))
val connConfig = UsbConnectionConfiguration(locationId, autoReconnectOnUnexpectedDisconnect, reconnectionListener)
if (listener is UsbReaderListener) {
connectUsbReader(reader, connConfig, listener)
} else {
connectUsbReader(reader, connConfig)
}
}
else -> {
throw IllegalArgumentException("Unsupported discovery method: ${discoveryMethod}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.stripeterminalreactnative.listener
Copy link
Collaborator

Choose a reason for hiding this comment

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


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.Reader
import com.stripe.stripeterminal.external.models.TerminalException.TerminalErrorCode
import com.stripeterminalreactnative.ReactExtensions.sendEvent
import com.stripeterminalreactnative.ReactNativeConstants.*
import com.stripeterminalreactnative.mapFromReader
import com.stripeterminalreactnative.nativeMapOf

class RNReaderReconnectionListener(
private val context: ReactApplicationContext,
private val onReaderReconnectStarted: (cancelable: Cancelable?) -> Unit,
) : ReaderReconnectionListener {

override fun onReaderReconnectFailed(reader: Reader) {
context.sendEvent(READER_RECONNECT_FAIL.listenerName) {
putMap("error", nativeMapOf {
putString("code", TerminalErrorCode.UNEXPECTED_SDK_ERROR.toString())
putString("message", "Reader reconnect fail")
})
}
}

override fun onReaderReconnectStarted(reader: Reader, cancelReconnect: Cancelable) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

onReaderReconnectStarted(cancelReconnect)
context.sendEvent(START_READER_RECONNECT.listenerName) {
putMap("reader", mapFromReader(reader))
}
}

override fun onReaderReconnectSucceeded(reader: Reader) {
context.sendEvent(READER_RECONNECT_SUCCEED.listenerName) {
putMap("reader", mapFromReader(reader))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.stripeterminalreactnative.listener

import com.facebook.react.bridge.ReactApplicationContext
import com.stripe.stripeterminal.external.callable.Cancelable
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 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 kotlin.test.assertTrue

@RunWith(JUnit4::class)
class RNReaderReconnectionListenerTest {

companion object {
@ClassRule
@JvmField
val typeReplacer = ReactNativeTypeReplacementRule()
}

private val context = mockk<ReactApplicationContext>()

@Test
fun `should send onReaderReconnectFailed event`() {
val mockOnReaderReconnectStarted = mockk<(Cancelable?) -> Unit>(relaxed = true)
val reader = mockk<Reader>(relaxed = true)
val listener = RNReaderReconnectionListener(context,mockOnReaderReconnectStarted)
listener.onReaderReconnectFailed(reader)

verify(exactly = 1) {
context.sendEvent(READER_RECONNECT_FAIL.listenerName, any())
}

assertTrue(typeReplacer.sendEventSlot.captured.hasValue("error"))
}

@Test
fun `should send onReaderReconnectStarted event`() {
val mockOnReaderReconnectStarted = mockk<(Cancelable?) -> Unit>(relaxed = true)
val mockCancelable = mockk<Cancelable>()
val reader = mockk<Reader>(relaxed = true)
val listener = RNReaderReconnectionListener(context,mockOnReaderReconnectStarted)
listener.onReaderReconnectStarted(reader,mockCancelable)

verify(exactly = 1) { mockOnReaderReconnectStarted.invoke(mockCancelable) }
verify(exactly = 1) { context.sendEvent(START_READER_RECONNECT.listenerName, any()) }

assertTrue(typeReplacer.sendEventSlot.captured.hasValue("reader"))
}

@Test
fun `should send onReaderReconnectSucceeded event`() {
val mockOnReaderReconnectStarted = mockk<(Cancelable?) -> Unit>(relaxed = true)
val reader = mockk<Reader>(relaxed = true)
val listener = RNReaderReconnectionListener(context,mockOnReaderReconnectStarted)
listener.onReaderReconnectSucceeded(reader)

verify(exactly = 1) { context.sendEvent(READER_RECONNECT_SUCCEED.listenerName, any()) }

assertTrue(typeReplacer.sendEventSlot.captured.hasValue("reader"))
}
}
2 changes: 2 additions & 0 deletions dev-app/src/AppContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ export const AppContext = React.createContext<IAppContext>({
setAccount: (_k) => null,
lastSuccessfulChargeId: null,
setLastSuccessfulChargeId: (_id) => null,
autoReconnectOnUnexpectedDisconnect: false,
setAutoReconnectOnUnexpectedDisconnect: (_b) => null,
});
7 changes: 7 additions & 0 deletions dev-app/src/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default function Root() {
const [lastSuccessfulChargeId, setLastSuccessfulChargeId] = useState<
string | null
>(null);
const [
autoReconnectOnUnexpectedDisconnect,
setAutoReconnectOnUnexpectedDisconnect,
] = useState<boolean | false>(false);

useEffect(() => {
// var is a string in CI
Expand Down Expand Up @@ -82,6 +86,9 @@ export default function Root() {
setAccount: onSelectAccount,
setLastSuccessfulChargeId: (id) => setLastSuccessfulChargeId(id),
lastSuccessfulChargeId,
autoReconnectOnUnexpectedDisconnect,
setAutoReconnectOnUnexpectedDisconnect: (b) =>
setAutoReconnectOnUnexpectedDisconnect(b),
}}
>
<StripeTerminalProvider
Expand Down
47 changes: 45 additions & 2 deletions dev-app/src/screens/DiscoverReadersScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import {
StyleSheet,
Text,
Expand All @@ -8,6 +14,7 @@ import {
View,
TouchableWithoutFeedback,
Platform,
Switch,
} from 'react-native';
import {
useStripeTerminal,
Expand All @@ -23,6 +30,7 @@ import ListItem from '../components/ListItem';
import List from '../components/List';

import type { RouteParamList } from '../App';
import { AppContext } from '../AppContext';

const SIMULATED_UPDATE_PLANS = [
'random',
Expand All @@ -39,7 +47,10 @@ export default function DiscoverReadersScreen() {
const [connectingReader, setConnectingReader] = useState<Reader.Type>();
const [showPicker, setShowPicker] = useState(false);
const pickerRef = useRef<Picker<string>>();

const {
autoReconnectOnUnexpectedDisconnect,
setAutoReconnectOnUnexpectedDisconnect,
} = useContext(AppContext);
const { simulated, discoveryMethod } = params;

const {
Expand Down Expand Up @@ -225,6 +236,7 @@ export default function DiscoverReadersScreen() {
const { reader: connectedReader, error } = await connectBluetoothReader({
reader,
locationId: selectedLocation?.id || reader?.location?.id,
autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect,
});

if (error) {
Expand Down Expand Up @@ -256,6 +268,7 @@ export default function DiscoverReadersScreen() {
const { reader: connectedReader, error } = await connectUsbReader({
reader,
locationId: selectedLocation?.id || reader?.location?.id,
autoReconnectOnUnexpectedDisconnect: autoReconnectOnUnexpectedDisconnect,
});

if (error) {
Expand Down Expand Up @@ -325,6 +338,36 @@ export default function DiscoverReadersScreen() {
</List>
)}

{!simulated &&
(discoveryMethod === 'bluetoothScan' ||
discoveryMethod === 'usb' ||
discoveryMethod === 'bluetoothProximity') && (
<List
bolded={false}
topSpacing={false}
title="AUTOMATIC RECONNECTION"
>
<ListItem
title="Enable Auto-Reconnect"
rightElement={
<Switch
testID="enable-automatic-reconnection"
value={autoReconnectOnUnexpectedDisconnect}
onValueChange={(value) => {
setAutoReconnectOnUnexpectedDisconnect(value);
}}
/>
}
/>

<Text style={styles.infoText}>
Automatic reconnection support for Bluetooth in iOS and Bluetooth
and USB in Android, where if the reader loses connection the SDK
will automatically attempts to reconnect to the reader.
</Text>
</List>
)}

<List
title="NEARBY READERS"
loading={discoveringLoading}
Expand Down
2 changes: 2 additions & 0 deletions dev-app/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type IAppContext = {
}) => void;
lastSuccessfulChargeId: string | null;
setLastSuccessfulChargeId: (id: string) => void;
autoReconnectOnUnexpectedDisconnect: boolean | false;
setAutoReconnectOnUnexpectedDisconnect: (b: boolean) => void;
};

export type IShortAccount = {
Expand Down
Loading