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

USB connection type #188

Merged
merged 1 commit into from
Mar 22, 2022
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 @@ -35,6 +35,12 @@ fun putIntOrNull(mapTarget: WritableMap, key: String, value: Int?) {
}
}

internal fun nativeMapOf(block: WritableNativeMap.() -> Unit): ReadableMap {
return WritableNativeMap().apply {
block()
}
}

internal fun mapFromReaders(readers: List<Reader>): WritableArray =
readers.collectToWritableArray { mapFromReader(it) }

Expand Down Expand Up @@ -101,6 +107,7 @@ internal fun mapToDiscoveryMethod(method: String?): DiscoveryMethod? {
"embedded" -> DiscoveryMethod.EMBEDDED
"localMobile" -> DiscoveryMethod.LOCAL_MOBILE
"handoff" -> DiscoveryMethod.HANDOFF
"usb" -> DiscoveryMethod.USB
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEm
import com.stripe.stripeterminal.Terminal
import com.stripe.stripeterminal.TerminalApplicationDelegate.onCreate
import com.stripe.stripeterminal.TerminalApplicationDelegate.onTrimMemory
import com.stripe.stripeterminal.external.UsbConnectivity
import com.stripe.stripeterminal.external.callable.BluetoothReaderListener
import com.stripe.stripeterminal.external.callable.Callback
import com.stripe.stripeterminal.external.callable.Cancelable
Expand All @@ -26,6 +27,7 @@ import com.stripe.stripeterminal.external.callable.ReaderCallback
import com.stripe.stripeterminal.external.callable.RefundCallback
import com.stripe.stripeterminal.external.callable.SetupIntentCallback
import com.stripe.stripeterminal.external.callable.TerminalListener
import com.stripe.stripeterminal.external.callable.UsbReaderListener
import com.stripe.stripeterminal.external.models.Cart
import com.stripe.stripeterminal.external.models.ConnectionConfiguration
import com.stripe.stripeterminal.external.models.ConnectionStatus
Expand Down Expand Up @@ -488,6 +490,121 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
)
}

@OptIn(UsbConnectivity::class)
Copy link
Contributor

Choose a reason for hiding this comment

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

oh wow, didn't expect this much code to be needed, is this because the android SDK doesn't surface these messages properly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah perhaps I misunderstood when we were talking earlier. Nope, this error handling logic is required because we're going from untyped JS to typed Kotlin. If we didn't have these checks we'd end up throwing null pointer exceptions when invalid input is received.

@ReactMethod
@Suppress("unused")
fun connectUsbReader(params: ReadableMap, promise: Promise) {
val reader = getMapOr(params, "reader") ?: run {
promise.resolve(
createError(
TerminalException(
TerminalException.TerminalErrorCode.INVALID_REQUIRED_PARAMETER,
"You must provide a reader object"
)
)
)
return
}
val readerId = getStringOr(reader, "serialNumber")

val selectedReader = discoveredReadersList.find {
it.serialNumber == readerId
} ?: run {
promise.resolve(
createError(
TerminalException(
TerminalException.TerminalErrorCode.INVALID_REQUIRED_PARAMETER,
"Could not find reader with id $readerId"
)
)
)
return
}

val locationId = getStringOr(params, "locationId") ?: selectedReader.location?.id ?: run {
promise.resolve(
createError(
TerminalException(
TerminalException.TerminalErrorCode.INVALID_REQUIRED_PARAMETER,
"You must provide a locationId"
)
)
)
return
}

val listener: UsbReaderListener = object : UsbReaderListener {
override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) {
sendEvent(REPORT_AVAILABLE_UPDATE.listenerName) {
putMap("result", mapFromReaderSoftwareUpdate(update))
}
}

override fun onStartInstallingUpdate(
update: ReaderSoftwareUpdate,
cancelable: Cancelable?
) {
installUpdateCancelable = cancelable
sendEvent(START_INSTALLING_UPDATE.listenerName) {
putMap("result", mapFromReaderSoftwareUpdate(update))
}
}

override fun onReportReaderSoftwareUpdateProgress(progress: Float) {
sendEvent(REPORT_UPDATE_PROGRESS.listenerName) {
putMap("result", nativeMapOf {
putString("progress", progress.toString())
})
}
}

override fun onFinishInstallingUpdate(
update: ReaderSoftwareUpdate?,
e: TerminalException?
) {
sendEvent(FINISH_INSTALLING_UPDATE.listenerName) {
update?.let {
putMap("result", mapFromReaderSoftwareUpdate(update))
} ?: run {
putMap("result", WritableNativeMap())
}
}
}

override fun onRequestReaderInput(options: ReaderInputOptions) {
sendEvent(REQUEST_READER_INPUT.listenerName) {
putArray("result", mapFromReaderInputOptions(options))
}
}

override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) {
sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) {
putString("result", mapFromReaderDisplayMessage(message))
}
sendEvent(REQUEST_READER_DISPLAY_MESSAGE.listenerName) {
putString("result", mapFromReaderDisplayMessage(message))
}
}
}

Terminal.getInstance().connectUsbReader(
selectedReader,
ConnectionConfiguration.UsbConnectionConfiguration(locationId),
listener,
object : ReaderCallback {
override fun onSuccess(reader: Reader) {
promise.resolve(nativeMapOf {
putMap("reader", mapFromReader(reader))
})
}

override fun onFailure(e: TerminalException) {
promise.resolve(createError(e))
}
}
)
}

@ReactMethod
@Suppress("unused")
fun disconnectReader(promise: Promise) {
Expand Down Expand Up @@ -962,6 +1079,14 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) :
.emit(eventName, result)
}

private fun sendEvent(eventName: String, resultBuilder: WritableNativeMap.() -> Unit) {
reactApplicationContext
.getJSModule(RCTDeviceEventEmitter::class.java)
.emit(eventName, WritableNativeMap().apply {
resultBuilder()
})
}

private fun onPaymentIntentCallback(paymentIntent: PaymentIntent, promise: Promise) {
val pi = mapFromPaymentIntent(paymentIntent)
val result = WritableNativeMap().apply {
Expand Down
20 changes: 20 additions & 0 deletions example/src/screens/DiscoverReadersScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default function DiscoverReadersScreen() {
connectBluetoothReader,
discoveredReaders,
connectInternetReader,
connectUsbReader,
simulateReaderUpdate,
} = useStripeTerminal({
onFinishDiscoveringReaders: (finishError) => {
Expand Down Expand Up @@ -153,6 +154,9 @@ export default function DiscoverReadersScreen() {
) {
const result = await handleConnectBluetoothReader(reader);
error = result.error;
} else if (discoveryMethod === 'usb') {
const result = await handleConnectUsbReader(reader);
error = result.error;
}
if (error) {
setConnectingReader(undefined);
Expand Down Expand Up @@ -194,6 +198,22 @@ export default function DiscoverReadersScreen() {
return { error };
};

const handleConnectUsbReader = async (reader: Reader.Type) => {
setConnectingReader(reader);

const { reader: connectedReader, error } = await connectUsbReader({
reader,
locationId: selectedLocation?.id || reader?.location?.id,
});

if (error) {
console.log('connectUsbReader error:', error);
} else {
console.log('Reader connected successfully', connectedReader);
}
return { error };
};

const handleChangeUpdatePlan = async (plan: Reader.SimulateUpdateType) => {
await simulateReaderUpdate(plan);
setSelectedUpdatePlan(plan);
Expand Down
8 changes: 6 additions & 2 deletions example/src/screens/DiscoveryMethodScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export default function DiscoveryMethodScreen() {
<View style={styles.container}>
<ListItem
onPress={() => onSelect('bluetoothScan')}
title="Bluetooth scan"
title="Bluetooth Scan"
/>
<Text style={styles.info}>
Discover a reader by scanning for Bluetooth LE devices.
Discover a reader by scanning for Bluetooth or Bluetooth LE devices.
</Text>
<ListItem
onPress={() => onSelect('bluetoothProximity')}
Expand All @@ -47,6 +47,10 @@ export default function DiscoveryMethodScreen() {
Discovers readers that have been registered to your account via the
Stripe API or Dashboard.
</Text>
<ListItem title="USB" onPress={() => onSelect('usb')} />
<Text style={styles.info}>
Discover a reader connected to this device via USB.
</Text>
</View>
);
}
Expand Down
2 changes: 2 additions & 0 deletions example/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ function mapFromDiscoveryMethod(method: Reader.DiscoveryMethod) {
return 'Bluetooth Proximity';
case 'internet':
return 'Internet';
case 'usb':
return 'USB';
default:
return '';
}
Expand Down
6 changes: 6 additions & 0 deletions src/StripeTerminalSdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {
Reader,
ConnectInternetResultType,
ConnectInternetReaderParams,
ConnectUsbReaderResultType,
ConnectUsbReaderParams,
CreatePaymentIntentParams,
CollectSetupIntentPaymentMethodParams,
PaymentIntentResultType,
Expand Down Expand Up @@ -52,6 +54,10 @@ type StripeTerminalSdkType = {
connectInternetReader(
params: ConnectInternetReaderParams
): Promise<ConnectInternetResultType>;
// Connect to reader via USB
connectUsbReader(
params: ConnectUsbReaderParams
): Promise<ConnectUsbReaderResultType>;
// Disconnect reader
disconnectReader(): Promise<DisconnectReaderResultType>;
// Create a payment intent
Expand Down
25 changes: 25 additions & 0 deletions src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type {
DisconnectReaderResultType,
ConnectInternetReaderParams,
ConnectInternetResultType,
ConnectUsbReaderParams,
ConnectUsbReaderResultType,
CreatePaymentIntentParams,
CollectSetupIntentPaymentMethodParams,
PaymentIntentResultType,
Expand Down Expand Up @@ -142,6 +144,29 @@ export async function connectInternetReader(
}
}

export async function connectUsbReader(
params: ConnectUsbReaderParams
): Promise<ConnectUsbReaderResultType> {
try {
const { error, reader } = await StripeTerminalSdk.connectUsbReader(params);

if (error) {
return {
error,
reader: undefined,
};
}
return {
reader: reader!,
error: undefined,
};
} catch (error) {
return {
error: error as any,
};
}
}

export async function disconnectReader(): Promise<DisconnectReaderResultType> {
try {
const { error } = await StripeTerminalSdk.disconnectReader();
Expand Down
19 changes: 19 additions & 0 deletions src/hooks/useStripeTerminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ConnectInternetReaderParams,
CreatePaymentIntentParams,
ConnectBluetoothReaderParams,
ConnectUsbReaderParams,
GetLocationsParams,
Cart,
CreateSetupIntentParams,
Expand All @@ -21,6 +22,7 @@ import {
connectBluetoothReader,
disconnectReader,
connectInternetReader,
connectUsbReader,
createPaymentIntent,
collectPaymentMethod,
retrievePaymentIntent,
Expand Down Expand Up @@ -343,6 +345,22 @@ export function useStripeTerminal(props?: Props) {
[setConnectedReader, setLoading]
);

const _connectUsbReader = useCallback(
async (params: ConnectUsbReaderParams) => {
setLoading(true);

const response = await connectUsbReader(params);

if (response.reader && !response.error) {
setConnectedReader(response.reader);
}
setLoading(false);

return response;
},
[setConnectedReader, setLoading]
);

const _disconnectReader = useCallback(async () => {
setLoading(true);

Expand Down Expand Up @@ -636,6 +654,7 @@ export function useStripeTerminal(props?: Props) {
connectBluetoothReader: _connectBluetoothReader,
disconnectReader: _disconnectReader,
connectInternetReader: _connectInternetReader,
connectUsbReader: _connectUsbReader,
createPaymentIntent: _createPaymentIntent,
collectPaymentMethod: _collectPaymentMethod,
retrievePaymentIntent: _retrievePaymentIntent,
Expand Down
3 changes: 2 additions & 1 deletion src/types/Reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export namespace Reader {
| 'internet'
| 'embedded'
| 'localMobile'
| 'handoff';
| 'handoff'
| 'usb';

export type SimulateUpdateType =
| 'random'
Expand Down
12 changes: 12 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export type ConnectBluetoothReaderParams = {
locationId?: string;
};

export type ConnectUsbReaderParams = {
reader: Reader.Type;
locationId?: string;
};

export type LineItem = {
displayName: string;
quantity: number;
Expand Down Expand Up @@ -97,6 +102,13 @@ export type ConnectInternetResultType =
}
| { reader?: undefined; error: StripeError };

export type ConnectUsbReaderResultType =
| {
reader: Reader.Type;
error?: undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hrm, I don't love this type, but I see it's already a pattern so we can just go with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You'd expect the error type to be defined?

Copy link
Contributor

Choose a reason for hiding this comment

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

So for the JS SDK we return one or the other and the user is expected to refine

Promise<ErrorResponse | {paymentIntent: ISdkManagedPaymentIntent}> 

I'm not quite sure how TS handles this type of union as it looks like in the relevant code we're still checking if the reader is not null even if we don't have an error. Either way seems like we can be a bit more accurate here, I'll ticket to follow up as long as it's not a behavior we're forced into from android / ios.

}
| { reader?: undefined; error: StripeError };

export type DisconnectReaderResultType = {
error: StripeError;
};
Expand Down