From bfeb30129750e5eccd1e5218248fc9436bed59f5 Mon Sep 17 00:00:00 2001 From: arekkubaczkowski Date: Wed, 23 Feb 2022 07:26:03 +0100 Subject: [PATCH] connection token error handling (#86) * chore: connection token error handling * chore: init module refactor * handle init error * chore: log error * fix: ts errors * fix: variable name --- README.md | 27 ++- .../StripeTerminalReactNativeModule.kt | 6 +- .../TokenProvider.kt | 12 +- docs/set-up-your-sdk.md | 26 +- example/index.js | 4 +- .../project.pbxproj | 4 +- example/src/App.tsx | 226 +++++++++--------- example/src/Root.tsx | 28 +++ ios/StripeTerminalReactNative.m | 2 +- ios/StripeTerminalReactNative.swift | 7 +- ios/TokenProvider.swift | 15 +- src/StripeTerminalSdk.tsx | 3 +- src/components/StripeTerminalContext.ts | 6 +- src/components/StripeTerminalProvider.tsx | 37 ++- src/functions.ts | 14 +- src/hooks/useStripeTerminal.tsx | 23 +- src/index.tsx | 2 - src/types/index.ts | 8 +- 18 files changed, 272 insertions(+), 178 deletions(-) create mode 100644 example/src/Root.tsx diff --git a/README.md b/README.md index 1b9fb2b8..de007217 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,12 @@ First, create an endpoint on your backend server that creates a new connection t Next, create a token provider that will fetch connection token from your server and provide it to StripeTerminalProvider as a parameter. Stripe Terminal SDK will fetch it when it's needed. + ```tsx -// App.ts +// Root.ts import { StripeTerminalProvider } from '@stripe/stripe-terminal-react-native'; -function App() { +function Root() { const fetchTokenProvider = async () => { const response = await fetch(`${API_URL}/connection_token`, { method: 'POST', @@ -85,12 +86,32 @@ function App() { logLevel="verbose" tokenProvider={fetchTokenProvider} > - + ); } ``` +As a last step, simply call `initialize` method from `useStripeTerminal` hook. +Please note that `initialize` method must be called from the nested component of `StripeTerminalProvider`. + +```tsx +// App.tsx +function App() { + const { initialize } = useStripeTerminal() + + useEffect(() => { + initialize({ + logLevel: 'verbose', + }); + }, []) + + return ( + + ); +} +``` + ## Configure your app ### Android diff --git a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt index 80e460fa..3fd9d652 100644 --- a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt +++ b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt @@ -135,8 +135,10 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : } @ReactMethod - fun setConnectionToken(token: String, promise: Promise) { - TokenProvider.setConnectionToken(token) + fun setConnectionToken(params: ReadableMap, promise: Promise) { + val token = getStringOr(params, "token") + val error = getStringOr(params, "error") + TokenProvider.setConnectionToken(token, error) promise.resolve(null) } diff --git a/android/src/main/java/com/stripeterminalreactnative/TokenProvider.kt b/android/src/main/java/com/stripeterminalreactnative/TokenProvider.kt index 899dd1d8..0c18a04b 100644 --- a/android/src/main/java/com/stripeterminalreactnative/TokenProvider.kt +++ b/android/src/main/java/com/stripeterminalreactnative/TokenProvider.kt @@ -12,10 +12,16 @@ class TokenProvider() { private var connectionTokenCallback: ConnectionTokenCallback? = null - fun setConnectionToken(token: String) { + fun setConnectionToken(token: String?, error: String?) { try { - connectionTokenCallback?.onSuccess(token) - connectionTokenCallback = null + if (!token.isNullOrEmpty()) { + connectionTokenCallback?.onSuccess(token) + connectionTokenCallback = null + } else { + connectionTokenCallback?.onFailure( + ConnectionTokenException(error ?: "", null) + ) + } } catch (e: Exception) { connectionTokenCallback?.onFailure( ConnectionTokenException("Failed to fetch connection token", e) diff --git a/docs/set-up-your-sdk.md b/docs/set-up-your-sdk.md index 8ce7a0c2..53bdfa3a 100644 --- a/docs/set-up-your-sdk.md +++ b/docs/set-up-your-sdk.md @@ -147,10 +147,10 @@ This function is called whenever the SDK needs to authenticate with Stripe or th To get started, provide your token provider implemented in [Step 3](#set-up-the-connection-token-endpoint) to `StripeTerminalProvider` as a prop. ```tsx -// App.ts +// Root.ts import { StripeTerminalProvider } from '@stripe/stripe-terminal-react-native'; -function App() { +function Root() { const fetchTokenProvider = async () => { const response = await fetch(`${API_URL}/connection_token`, { method: 'POST', @@ -167,8 +167,28 @@ function App() { logLevel="verbose" tokenProvider={fetchTokenProvider} > - + ); } ``` + +As a last step, simply call `initialize` method from `useStripeTerminal` hook. +Please note that `initialize` method must be called from the nested component of `StripeTerminalProvider`. + +```tsx +// App.tsx +function App() { + const { initialize } = useStripeTerminal() + + useEffect(() => { + initialize({ + logLevel: 'verbose', + }); + }, []) + + return ( + + ); +} +``` diff --git a/example/index.js b/example/index.js index 117ddcae..9b1184c1 100644 --- a/example/index.js +++ b/example/index.js @@ -1,5 +1,5 @@ import { AppRegistry } from 'react-native'; -import App from './src/App'; +import Root from './src/Root'; import { name as appName } from './app.json'; -AppRegistry.registerComponent(appName, () => App); +AppRegistry.registerComponent(appName, () => Root); diff --git a/example/ios/StripeTerminalReactNativeExample.xcodeproj/project.pbxproj b/example/ios/StripeTerminalReactNativeExample.xcodeproj/project.pbxproj index 860db68c..cfc9d288 100644 --- a/example/ios/StripeTerminalReactNativeExample.xcodeproj/project.pbxproj +++ b/example/ios/StripeTerminalReactNativeExample.xcodeproj/project.pbxproj @@ -753,7 +753,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 "; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -810,7 +810,7 @@ COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 "; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/example/src/App.tsx b/example/src/App.tsx index a7b1c5b3..fa7005ed 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -6,6 +6,7 @@ import { } from '@react-navigation/stack'; import HomeScreen from './screens/HomeScreen'; import { + Alert, PermissionsAndroid, Platform, StatusBar, @@ -14,8 +15,6 @@ import { import { colors } from './colors'; import { LogContext, Log } from './components/LogContext'; import DiscoverReadersScreen from './screens/DiscoverReadersScreen'; -import { StripeTerminalProvider } from 'stripe-terminal-react-native'; -import { API_URL } from './Config'; import ReaderDisplayScreen from './screens/ReaderDisplayScreen'; import LocationListScreen from './screens/LocationListScreen'; import UpdateReaderScreen from './screens/UpdateReaderScreen'; @@ -27,6 +26,7 @@ import ReadReusableCardScreen from './screens/ReadReusableCardScreen'; import LogScreen from './screens/LogScreen'; import RegisterInternetReaderScreen from './screens/RegisterInternetReaderScreen'; import { isAndroid12orHigher } from './utils'; +import { useStripeTerminal } from 'stripe-terminal-react-native'; const Stack = createStackNavigator(); @@ -56,22 +56,10 @@ const screenOptions = { export default function App() { const [logs, setlogs] = useState([]); const clearLogs = () => setlogs([]); - const [permissionsGranted, setPermissionsGranted] = useState(false); - - const fetchTokenProvider = async () => { - const response = await fetch(`${API_URL}/connection_token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({}), - }); - const { secret } = await response.json(); - return secret; - }; + const { initialize: initStripe } = useStripeTerminal(); useEffect(() => { - async function init() { + async function handlePermissions() { try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, @@ -106,8 +94,10 @@ export default function App() { hasGrantedPermission(grantedBTScan) ) { handlePermissionsSuccess(); + return; } else { handlePermissionsError(); + return; } } handlePermissionsSuccess(); @@ -117,10 +107,11 @@ export default function App() { } catch {} } if (Platform.OS === 'android') { - init(); + handlePermissions(); } else { handlePermissionsSuccess(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handlePermissionsError = () => { @@ -129,9 +120,15 @@ export default function App() { ); }; - const handlePermissionsSuccess = () => { - console.log('You can use the Location and BT'); - setPermissionsGranted(true); + const handlePermissionsSuccess = async () => { + const { error } = await initStripe({ + logLevel: 'verbose', + }); + if (error) { + Alert.alert('StripeTerminal init failed', error.message); + } else { + console.log('StripeTerminal has been initialized properly'); + } }; const hasGrantedPermission = (status: string) => { @@ -151,105 +148,96 @@ export default function App() { }; return ( - <> - {permissionsGranted && ( - - - <> - + + <> + - - - - - - - - - - - - - - - - - - - - )} - + + + + + + + + + + + + + + + + + + ); } diff --git a/example/src/Root.tsx b/example/src/Root.tsx new file mode 100644 index 00000000..c050a84e --- /dev/null +++ b/example/src/Root.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import { StripeTerminalProvider } from 'stripe-terminal-react-native'; +import App from './App'; +import { API_URL } from './Config'; + +export default function Root() { + const fetchTokenProvider = async () => { + const response = await fetch(`${API_URL}/connection_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }); + const { secret } = await response.json(); + return secret; + }; + + return ( + + + + ); +} diff --git a/ios/StripeTerminalReactNative.m b/ios/StripeTerminalReactNative.m index fc8acbc9..2e313ca4 100644 --- a/ios/StripeTerminalReactNative.m +++ b/ios/StripeTerminalReactNative.m @@ -85,7 +85,7 @@ @interface RCT_EXTERN_MODULE(StripeTerminalReactNative, RCTEventEmitter) ) RCT_EXTERN_METHOD( - setConnectionToken:(NSString *)token + setConnectionToken:(NSDictionary *)params resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) diff --git a/ios/StripeTerminalReactNative.swift b/ios/StripeTerminalReactNative.swift index fbad50fe..2c95b1d0 100644 --- a/ios/StripeTerminalReactNative.swift +++ b/ios/StripeTerminalReactNative.swift @@ -132,8 +132,11 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe } @objc(setConnectionToken:resolver:rejecter:) - func setConnectionToken(token: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { - TokenProvider.shared.setConnectionToken(token: token) + func setConnectionToken(params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + let token = params["token"] as? String + let error = params["error"] as? String + + TokenProvider.shared.setConnectionToken(token: token, error: error) resolve([:]) } diff --git a/ios/TokenProvider.swift b/ios/TokenProvider.swift index f99b3d0f..5c8b3363 100644 --- a/ios/TokenProvider.swift +++ b/ios/TokenProvider.swift @@ -1,13 +1,22 @@ import StripeTerminal +enum TokenError: Error { + case runtimeError(String) +} + class TokenProvider: ConnectionTokenProvider { static let shared = TokenProvider() var completionCallback: ConnectionTokenCompletionBlock? = nil static var delegate: RCTEventEmitter? = nil - - func setConnectionToken(token: String) { - self.completionCallback?(token, nil) + + func setConnectionToken(token: String?, error: String?) { + let unwrappedToken = token ?? "" + if (!unwrappedToken.isEmpty) { + self.completionCallback?(token, nil) + } else { + self.completionCallback?(nil, TokenError.runtimeError(error ?? "") ) + } } func fetchConnectionToken(_ completion: @escaping ConnectionTokenCompletionBlock) { diff --git a/src/StripeTerminalSdk.tsx b/src/StripeTerminalSdk.tsx index 382070b1..7a0731ec 100644 --- a/src/StripeTerminalSdk.tsx +++ b/src/StripeTerminalSdk.tsx @@ -25,6 +25,7 @@ import type { ProcessRefundResultType, ReadReusableCardParamsType, PaymentMethodResultType, + SetConnectionTokenParams, } from './types'; const { StripeTerminalReactNative } = NativeModules; @@ -38,7 +39,7 @@ type StripeTerminalSdkType = { // Initialize StripeTerminalSdk native module initialize(params: InitParams): InitializeResultNativeType; // Set connection token - setConnectionToken(token: string): Promise; + setConnectionToken(params: SetConnectionTokenParams): Promise; // Discover readers by connection type discoverReaders(params: DiscoverReadersParams): DiscoverReadersResultType; // Cancel discovering readers diff --git a/src/components/StripeTerminalContext.ts b/src/components/StripeTerminalContext.ts index e08f606a..1f567ac3 100644 --- a/src/components/StripeTerminalContext.ts +++ b/src/components/StripeTerminalContext.ts @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import type { Reader, InitParams } from '../types'; +import type { Reader, InitParams, InitializeResultType } from '../types'; type ContextType = { loading: boolean; @@ -10,7 +10,7 @@ type ContextType = { setIsInitialized(value: boolean): void; setConnectedReader(value: Reader.Type | null): void; setDiscoveredReaders(value: Reader.Type[]): void; - initialize(params: InitParams): void; + initialize?(params: InitParams): Promise; log(code: string, message?: any): void; }; @@ -22,6 +22,6 @@ export const StripeTerminalContext = createContext({ setLoading: () => {}, setIsInitialized: () => {}, setConnectedReader: () => {}, - initialize: () => {}, + initialize: undefined, setDiscoveredReaders: () => {}, }); diff --git a/src/components/StripeTerminalProvider.tsx b/src/components/StripeTerminalProvider.tsx index f509667c..2536a992 100644 --- a/src/components/StripeTerminalProvider.tsx +++ b/src/components/StripeTerminalProvider.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import type { Reader, InitParams, LogLevel } from '../types'; import { StripeTerminalContext } from './StripeTerminalContext'; import { initialize, setConnectionToken } from '../functions'; @@ -59,11 +59,21 @@ export function StripeTerminalProvider({ [logLevel] ); - const tokenProviderHandler = useCallback(async () => { - const connectionToken = await tokenProvider(); + const tokenProviderHandler = async () => { + try { + const connectionToken = await tokenProvider(); - setConnectionToken(connectionToken); - }, [tokenProvider]); + setConnectionToken(connectionToken); + } catch (error) { + const errorMessage = + "Couldn't fetch connection token. Please check your tokenProvider method"; + + setConnectionToken(undefined, errorMessage); + + console.error(error); + console.error(errorMessage); + } + }; useListener(FETCH_TOKEN_PROVIDER_LISTENER_NAME, tokenProviderHandler); @@ -91,23 +101,6 @@ export function StripeTerminalProvider({ [setLoading, setConnectedReader, setIsInitialized, log] ); - useEffect(() => { - async function init() { - const { initialized, error: initError } = await _initialize({ - logLevel, - }); - if (!initError) { - setIsInitialized(initialized); - } else { - console.error(initError); - } - setLoading(false); - } - if (!isInitialized) { - init(); - } - }, [_initialize, isInitialized, logLevel]); - return ( { +export async function setConnectionToken( + token?: string, + error?: string +): Promise { try { - await StripeTerminalSdk.setConnectionToken(token); - } catch (error) { - console.warn('Unexpected error:', error); + await StripeTerminalSdk.setConnectionToken({ token, error }); + } catch (e) { + console.warn('Unexpected error:', e); } } diff --git a/src/hooks/useStripeTerminal.tsx b/src/hooks/useStripeTerminal.tsx index 91e6333b..9e921ebc 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -13,6 +13,7 @@ import type { RefundParams, ReadReusableCardParamsType, PaymentStatus, + InitParams, } from '../types'; import { discoverReaders, @@ -292,6 +293,26 @@ export function useStripeTerminal(props?: Props) { [setLoading] ); + const _initialize = useCallback( + async (params: InitParams) => { + if (!initialize || typeof initialize !== 'function') { + const errorMessage = + 'StripeTerminalProvider component is not found or has not been mounted properly'; + log('Failed', errorMessage); + return { + error: { + code: 'Failed', + message: errorMessage, + }, + }; + } + + const res = initialize(params); + return res; + }, + [initialize, log] + ); + const _cancelDiscovering = useCallback(async () => { setLoading(true); @@ -623,7 +644,7 @@ export function useStripeTerminal(props?: Props) { }, [setLoading]); return { - initialize, + initialize: _initialize, discoverReaders: _discoverReaders, cancelDiscovering: _cancelDiscovering, connectBluetoothReader: _connectBluetoothReader, diff --git a/src/index.tsx b/src/index.tsx index d5ad1993..bd3ec8d8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,6 @@ export * from './types'; export * from './functions'; -console.disableYellowBox = true; - // hooks export { useStripeTerminal, diff --git a/src/types/index.ts b/src/types/index.ts index 56c236c0..52cbd2a5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,6 +12,11 @@ export type InitParams = { logLevel?: LogLevel; }; +export type SetConnectionTokenParams = { + token?: string; + error?: string; +}; + export type LogLevel = LogLevelIOS | LogLevelAndroid; export type LogLevelIOS = 'none' | 'verbose'; export type LogLevelAndroid = 'none' | 'verbose' | 'error' | 'warning'; @@ -65,11 +70,10 @@ export type StripeError = { export type InitializeResultType = | { - initialized: true; reader?: Reader.Type; error?: undefined; } - | { initialized: false; error: StripeError; reader?: undefined }; + | { error: StripeError; reader?: undefined }; export type DiscoverReadersResultType = Promise<{ error?: StripeError;