diff --git a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt index c5208234..868783f8 100644 --- a/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt +++ b/android/src/main/java/com/stripeterminalreactnative/StripeTerminalReactNativeModule.kt @@ -3,15 +3,7 @@ package com.stripeterminalreactnative import android.app.Application import android.content.ComponentCallbacks2 import android.content.res.Configuration -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.UiThreadUtil -import com.facebook.react.bridge.WritableNativeArray -import com.facebook.react.bridge.WritableNativeMap -import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter +import com.facebook.react.bridge.* import com.stripe.stripeterminal.Terminal import com.stripe.stripeterminal.TerminalApplicationDelegate.onCreate import com.stripe.stripeterminal.TerminalApplicationDelegate.onTrimMemory @@ -119,6 +111,16 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : promise.resolve(WritableNativeMap()) } + @ReactMethod + @Suppress("unused") + fun setSimulatedCard(cardNumber: String, promise: Promise) { + terminal.simulatorConfiguration = SimulatorConfiguration( + terminal.simulatorConfiguration.update, + SimulatedCard(testCardNumber = cardNumber) + ) + promise.resolve(WritableNativeMap()) + } + @ReactMethod @Suppress("unused") fun setConnectionToken(params: ReadableMap, promise: Promise) { @@ -172,9 +174,7 @@ class StripeTerminalReactNativeModule(reactContext: ReactApplicationContext) : "Could not find a reader with serialNumber $serialNumber" } - val locationId = requireParam(params.getString("locationId") ?: selectedReader.location?.id) { - "You must provide a locationId" - } + val locationId = params.getString("locationId") ?: selectedReader.location?.id.orEmpty() CoroutineScope(Dispatchers.IO).launch { val connectedReader = diff --git a/e2e/app.e2e.js b/e2e/app.e2e.js index 04189bdc..be05dfc3 100644 --- a/e2e/app.e2e.js +++ b/e2e/app.e2e.js @@ -13,7 +13,7 @@ const { cleanPaymentMethods } = require('./clean'); jest.retryTimes(3); -describe('Payments', () => { +describe('Basic funtionalities', () => { beforeAll(async () => { await cleanPaymentMethods(); }); @@ -188,14 +188,16 @@ describe('Payments', () => { await navigateTo('In-Person Refund'); - const chargeIdInout = element(by.id('charge-id-text-field')); + const chargeIdInput = element(by.id('charge-id-text-field')); const amountInput = element(by.id('amount-text-field')); - await waitFor(chargeIdInout).toBeVisible().withTimeout(16000); + await waitFor(chargeIdInput).toBeVisible().withTimeout(16000); await waitFor(amountInput).toBeVisible(); await amountInput.replaceText('20000'); - await chargeIdInout.replaceText('ch_3JxsjUBDuqlYGNW21EL8UyOm'); + await chargeIdInput.replaceText('ch_3JxsjUBDuqlYGNW21EL8UyOm'); + + await element(by.id('refund-scroll-view')).scrollTo('bottom'); const button = element(by.id('collect-refund-button')); diff --git a/e2e/in-person-refund.e2e.js b/e2e/in-person-refund.e2e.js new file mode 100644 index 00000000..d3927958 --- /dev/null +++ b/e2e/in-person-refund.e2e.js @@ -0,0 +1,149 @@ +/* eslint-env detox/detox, jest */ + +const { + navigateTo, + connectReader, + checkIfLogExist, + checkIfConnected, + changeDiscoveryMethod, +} = require('./utils'); + +jest.retryTimes(3); + +describe('In-Person Refund', () => { + beforeEach(async () => { + await device.launchApp({ + permissions: { location: 'always' }, + launchArgs: { + canada: true, + }, + newInstance: true, + }); + }); + + afterAll(async () => { + await device.sendToHome(); + }); + + it('Collect CA card payment', async () => { + await navigateTo('Discover Readers'); + await connectReader('wisePad3'); + + await navigateTo('Collect card payment'); + + const currencyInput = element(by.id('currency-text-field')); + const amountInput = element(by.id('amount-text-field')); + const cardNumberInput = element(by.id('card-number-text-field')); + + await waitFor(currencyInput).toBeVisible().withTimeout(16000); + await waitFor(amountInput).toBeVisible().withTimeout(10000); + await waitFor(cardNumberInput).toBeVisible().withTimeout(10000); + + const enableInteracSwitch = element(by.id('enable-interac')); + await waitFor(enableInteracSwitch).toBeVisible().withTimeout(10000); + await enableInteracSwitch.tap(); + + await amountInput.replaceText('20000'); + await currencyInput.replaceText('CAD'); + + // set interac test card + await cardNumberInput.replaceText('4506445006931933'); + + await element(by.id('collect-scroll-view')).scrollTo('bottom'); + + const capturePaymentIntentSwitch = element(by.id('capture-payment-intent')); + await waitFor(capturePaymentIntentSwitch).toBeVisible().withTimeout(10000); + // do not capture PI because this specific card number captures it automatically. + await capturePaymentIntentSwitch.tap(); + + await element(by.id('collect-scroll-view')).scrollTo('bottom'); + + const button = element(by.text('Collect payment')); + + await waitFor(button).toBeVisible().withTimeout(10000); + + await button.tap(); + + const eventLogTitle = element(by.text('EVENT LOG')); + await waitFor(eventLogTitle).toBeVisible().withTimeout(16000); + + await checkIfLogExist('Create'); + await checkIfLogExist('Created'); + await checkIfLogExist('Collect'); + await checkIfLogExist('Collected'); + await checkIfLogExist('Process'); + await checkIfLogExist('Processed'); + }); + + it('via bluetooth reader', async () => { + // Temporary skipped on Android due to some issues with refunding payments + if (device.getPlatform() === 'android') { + return; + } + await navigateTo('Discover Readers'); + await connectReader('wisePad3'); + + await checkIfConnected({ device: 'wisePad3' }); + await element(by.id('home-screen')).scrollTo('bottom'); + + await navigateTo('In-Person Refund'); + + const amountInput = element(by.id('amount-text-field')); + await waitFor(amountInput).toBeVisible(); + + await amountInput.replaceText('100'); + + await element(by.id('refund-scroll-view')).scrollTo('bottom'); + + const button = element(by.id('collect-refund-button')); + + await waitFor(button).toBeVisible(); + + await button.tap(); + + const eventLogTitle = element(by.text('EVENT LOG')); + await waitFor(eventLogTitle).toBeVisible().withTimeout(16000); + + await checkIfLogExist('Collect'); + await checkIfLogExist('Collected'); + await checkIfLogExist('Processing'); + await checkIfLogExist('Succeeded'); + }); + + it('via internet reader', async () => { + // Temporary skipped on Android due to some issues with refunding payments + if (device.getPlatform() === 'android') { + return; + } + + await changeDiscoveryMethod('Internet'); + await navigateTo('Discover Readers'); + await connectReader('verifoneP400'); + + await checkIfConnected({ device: 'verifoneP400' }); + await element(by.id('home-screen')).scrollTo('bottom'); + + await navigateTo('In-Person Refund'); + + const amountInput = element(by.id('amount-text-field')); + await waitFor(amountInput).toBeVisible(); + + await amountInput.replaceText('100'); + + await element(by.id('refund-scroll-view')).scrollTo('bottom'); + + const button = element(by.id('collect-refund-button')); + + await waitFor(button).toBeVisible(); + + await button.tap(); + + const eventLogTitle = element(by.text('EVENT LOG')); + await waitFor(eventLogTitle).toBeVisible().withTimeout(16000); + + await checkIfLogExist('Collect'); + await checkIfLogExist('Collected'); + await checkIfLogExist('Processing'); + await checkIfLogExist('Succeeded'); + }); +}); diff --git a/e2e/internet-reader.e2e.js b/e2e/internet-reader.e2e.js new file mode 100644 index 00000000..6e587398 --- /dev/null +++ b/e2e/internet-reader.e2e.js @@ -0,0 +1,147 @@ +/* eslint-env detox/detox, jest */ + +const { + navigateTo, + connectReader, + checkIfLogExist, + checkIfConnected, + changeDiscoveryMethod, +} = require('./utils'); + +const { cleanPaymentMethods } = require('./clean'); + +jest.retryTimes(3); + +describe('Internet reader', () => { + beforeAll(async () => { + await cleanPaymentMethods(); + }); + + beforeEach(async () => { + await device.launchApp({ + permissions: { location: 'always' }, + newInstance: true, + }); + }); + + afterAll(async () => { + await device.sendToHome(); + }); + + it('Connect and disconnect', async () => { + await changeDiscoveryMethod('Internet'); + await navigateTo('Discover Readers'); + await connectReader('verifoneP400'); + await checkIfConnected({ device: 'verifoneP400' }); + }); + + it('Collect card payment', async () => { + await changeDiscoveryMethod('Internet'); + await navigateTo('Discover Readers'); + await connectReader('verifoneP400'); + + await navigateTo('Collect card payment'); + + const currencyInput = element(by.id('currency-text-field')); + const amountInput = element(by.id('amount-text-field')); + + await waitFor(currencyInput).toBeVisible().withTimeout(16000); + await waitFor(amountInput).toBeVisible(); + + await amountInput.replaceText('20000'); + await currencyInput.replaceText('USD'); + + await element(by.id('collect-scroll-view')).scrollTo('bottom'); + + const button = element(by.text('Collect payment')); + + await waitFor(button).toBeVisible(); + + await button.tap(); + + const eventLogTitle = element(by.text('EVENT LOG')); + await waitFor(eventLogTitle).toBeVisible().withTimeout(16000); + + await checkIfLogExist('Create'); + await checkIfLogExist('Created'); + await checkIfLogExist('Collect'); + await checkIfLogExist('Collected'); + await checkIfLogExist('Process'); + await checkIfLogExist('Processed'); + await checkIfLogExist('Capture'); + await checkIfLogExist('Captured'); + }); + + it('Store card via readReusableCard', async () => { + await changeDiscoveryMethod('Internet'); + await navigateTo('Discover Readers'); + await connectReader('verifoneP400'); + + await navigateTo('Store card via readReusableCard'); + + const eventLogTitle = element(by.text('EVENT LOG')); + await waitFor(eventLogTitle).toBeVisible().withTimeout(16000); + + await checkIfLogExist('Start'); + await checkIfLogExist('Finished'); + }); + + it('Store card via SetupIntent', async () => { + // Store card via SetupIntent is not available for verifoneP400 on iOS + // while this is the only available simulated reader on Android + const readerName = + device.getPlatform() === 'ios' ? 'wisePosE' : 'verifoneP400'; + await changeDiscoveryMethod('Internet'); + await navigateTo('Discover Readers'); + await connectReader(readerName); + + await checkIfConnected({ device: readerName }); + await element(by.id('home-screen')).scrollTo('bottom'); + + await navigateTo('Store card via Setup Intents'); + + const eventLogTitle = element(by.text('EVENT LOG')); + await waitFor(eventLogTitle).toBeVisible().withTimeout(16000); + + await checkIfLogExist('Create'); + await checkIfLogExist('Collect'); + await checkIfLogExist('Created'); + await checkIfLogExist('Process'); + await checkIfLogExist('Finished'); + }); + + it('setReaderDisplay/clearReaderDisplay', async () => { + // The only available verifoneP400 reader doesn't support setReaderDisplay funtionality in simulated mode. + if (device.getPlatform() === 'android') { + return; + } + await changeDiscoveryMethod('Internet'); + await navigateTo('Discover Readers'); + await connectReader('verifoneP400'); + + await checkIfConnected({ device: 'verifoneP400' }); + await element(by.id('home-screen')).scrollTo('bottom'); + + await navigateTo('Set reader display'); + + const setButton = element(by.id('set-reader-display')); + await waitFor(setButton).toBeVisible().withTimeout(10000); + await setButton.tap(); + + await waitFor(element(by.text('setReaderDisplay success'))) + .toBeVisible() + .withTimeout(16000); + + const confirmAlertButton = element(by.text('OK')); + await waitFor(confirmAlertButton).toBeVisible(); + await confirmAlertButton.tap(); + + const clearButton = element(by.id('clear-reader-display')); + await waitFor(clearButton).toBeVisible().withTimeout(10000); + await clearButton.tap(); + + await waitFor(element(by.text('clearReaderDisplay success'))) + .toBeVisible() + .withTimeout(16000); + }); +}); diff --git a/e2e/utils.js b/e2e/utils.js index 759df490..b1dcca19 100644 --- a/e2e/utils.js +++ b/e2e/utils.js @@ -1,12 +1,12 @@ /* eslint-env detox/detox, jest */ -export const navigateTo = async (buttonText: string) => { +export const navigateTo = async (buttonText) => { const button = element(by.text(buttonText)); await waitFor(button).toBeVisible().withTimeout(16000); await button.tap(); }; -export const connectReader = async (name?: string = 'chipper2X') => { +export const connectReader = async (name = 'chipper2X') => { await waitFor(element(by.text(`SimulatorID - ${name}`))) .toBeVisible() .withTimeout(16000); @@ -14,10 +14,7 @@ export const connectReader = async (name?: string = 'chipper2X') => { await button.tap(); }; -export const setSimulatedUpdatePlan = async ( - plan: string = 'Update required' -) => { - const defaultPlan = 'No Update'; +export const setSimulatedUpdatePlan = async (plan = 'Update required') => { const picker = element(by.id('update-plan-picker')); await picker.tap(); @@ -52,14 +49,14 @@ export const disconnectReader = async () => { .withTimeout(16000); }; -export const checkIfLogExist = async (log: string) => { +export const checkIfLogExist = async (log) => { await element(by.id('scroll-view')).scrollTo('bottom'); await waitFor(element(by.text(log))) .toBeVisible() .withTimeout(16000); }; -export const changeDiscoveryMethod = async (method: string) => { +export const changeDiscoveryMethod = async (method) => { const button = element(by.id('discovery-method-button')); await waitFor(button).toBeVisible().withTimeout(10000); await button.tap(); @@ -77,7 +74,7 @@ export const changeDiscoveryMethod = async (method: string) => { .withTimeout(10000); }; -export const goBack = async (label?: string) => { +export const goBack = async (label) => { if (device.getPlatform() === 'android') { await device.pressBack(); } else { diff --git a/example/.env.example b/example/.env.example index 47f7ca95..7d09e5f6 100644 --- a/example/.env.example +++ b/example/.env.example @@ -5,9 +5,13 @@ STRIPE_PRIVATE_KEY=sk_test_xxx # Note: this should be an ip/url your mobile device can reach when the example # app is loaded onto it. API_URL=https://your-merchant-backend.com +# Backend API URL for Canada +API_CA_URL=http://localhost:3003 # Android Backend API URL. # The example app will default to API_URL if no specific API_URL_ANDROID is defined # This is helpful when you're working with the android emulator as it remaps the localhost # ip value (default is 10.0.2.2) # API_URL_ANDROID=http://10.0.2.2:3002 +# Android Backend API URL for Canada. +API_CA_URL_ANDROID=http://10.0.2.2:3003 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a0f4928f..830248ac 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -213,6 +213,8 @@ PODS: - react-native-config/App (= 1.4.5) - react-native-config/App (1.4.5): - React-Core + - react-native-launch-arguments (3.0.1): + - React - react-native-safe-area-context (3.3.2): - React-Core - React-perflogger (0.66.3) @@ -344,6 +346,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-config (from `../node_modules/react-native-config`) + - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -407,6 +410,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" react-native-config: :path: "../node_modules/react-native-config" + react-native-launch-arguments: + :path: "../node_modules/react-native-launch-arguments" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" React-perflogger: @@ -468,6 +473,7 @@ SPEC CHECKSUMS: React-jsinspector: d9c8eb0b53f0da206fed56612b289fec84991157 React-logger: e522e76fa3e9ec3e7d7115b49485cc065cf4ae06 react-native-config: 6502b1879f97ed5ac570a029961fc35ea606cd14 + react-native-launch-arguments: 5d62e1f2c5995e219c497fc74cc39739a8d50d8a react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057 React-perflogger: 73732888d37d4f5065198727b167846743232882 React-RCTActionSheet: 96c6d774fa89b1f7c59fc460adc3245ba2d7fd79 diff --git a/example/package.json b/example/package.json index 6e4b1385..2da203bd 100644 --- a/example/package.json +++ b/example/package.json @@ -10,10 +10,11 @@ "build:server": "babel server --out-dir dist --extensions '.ts,.tsx' --ignore '**/__tests__/**' --source-maps --copy-files --delete-dir-on-start", "ios": "react-native run-ios", "ios:all": "concurrently \"yarn start\" \"yarn start:server\" \"yarn ios\"", - "postinstall": "scripts/fixRN.sh", + "postinstall": "scripts/fixRN.sh && patch-package", "start": "react-native start", "start:server": "ts-node --project ./tsconfig.server.json server/index.ts", - "start:server:ci": "yarn start:server &", + "start:server:canada": "PORT=3003 yarn start:server", + "start:server:ci": "yarn start:server & yarn start:server:canada &", "watch:server": "nodemon --exec yarn start:server" }, "dependencies": { @@ -25,6 +26,7 @@ "@types/react-native-dotenv": "^0.2.0", "dotenv": "^10.0.0", "jest": "^27.3.1", + "postinstall-postinstall": "^2.1.0", "react": "17.0.2", "react-native": "0.66.3", "react-native-config": "^1.4.5", @@ -32,6 +34,7 @@ "react-native-gesture-handler": "^1.10.3", "react-native-reanimated": "2.5.0", "react-native-keyboard-aware-scroll-view": "^0.9.5", + "react-native-launch-arguments": "^3.0.1", "react-native-safe-area-context": "^3.3.2", "react-native-screens": "^3.8.0" }, @@ -47,6 +50,7 @@ "express-winston": "^4.2.0", "metro-react-native-babel-preset": "^0.66.2", "nodemon": "^2.0.15", + "patch-package": "^6.4.7", "stripe": "^8.181.0", "ts-node": "^10.4.0", "winston": "^3.3.3" diff --git a/example/patches/react-native-launch-arguments+3.0.1.patch b/example/patches/react-native-launch-arguments+3.0.1.patch new file mode 100644 index 00000000..b6beeecf --- /dev/null +++ b/example/patches/react-native-launch-arguments+3.0.1.patch @@ -0,0 +1,52 @@ +diff --git a/node_modules/react-native-launch-arguments/android/build.gradle b/node_modules/react-native-launch-arguments/android/build.gradle +index 3d4f7b4..993e533 100644 +--- a/node_modules/react-native-launch-arguments/android/build.gradle ++++ b/node_modules/react-native-launch-arguments/android/build.gradle +@@ -101,47 +101,3 @@ def configureReactNativePom(def pom) { + } + } + } +- +-afterEvaluate { project -> +- // some Gradle build hooks ref: +- // https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html +- task androidJavadoc(type: Javadoc) { +- source = android.sourceSets.main.java.srcDirs +- classpath += files(android.bootClasspath) +- classpath += files(project.getConfigurations().getByName('compile').asList()) +- include '**/*.java' +- } +- +- task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) { +- classifier = 'javadoc' +- from androidJavadoc.destinationDir +- } +- +- task androidSourcesJar(type: Jar) { +- archiveClassifier = 'sources' +- from android.sourceSets.main.java.srcDirs +- include '**/*.java' +- } +- +- android.libraryVariants.all { variant -> +- def name = variant.name.capitalize() +- def javaCompileTask = variant.javaCompileProvider.get() +- +- task "jar${name}"(type: Jar, dependsOn: javaCompileTask) { +- from javaCompileTask.destinationDir +- } +- } +- +- artifacts { +- archives androidSourcesJar +- archives androidJavadocJar +- } +- +- publishing { +- publications { +- maven(MavenPublication) { +- artifact androidSourcesJar +- } +- } +- } +-} diff --git a/example/server/index.ts b/example/server/index.ts index ed9f6e02..412796d9 100644 --- a/example/server/index.ts +++ b/example/server/index.ts @@ -11,15 +11,40 @@ if (!process.env.STRIPE_PRIVATE_KEY) { process.exit(-1); } +const default_port = 3002; + +// Port reserved for Canada's accounts for testing purposes +const canada_port = 3003; + +const app = express(); +const port = process.env.PORT ? Number(process.env.PORT) : default_port; + const secret_key = process.env.STRIPE_PRIVATE_KEY; +const secret_ca_key = process.env.STRIPE_CA_PRIVATE_KEY; -const stripe = new Stripe(secret_key as string, { +const stripe_default = new Stripe(secret_key as string, { apiVersion: '2020-08-27', typescript: true, }); -const app = express(); -const port = process.env.PORT ? process.env.PORT : 3002; +const stripe = (() => { + // in case of Canada's port provided let's use proper Stripe instance. + if (port === canada_port) { + if (!secret_ca_key) { + console.error( + "You've provided `3003` port which is reserved for Canada's accounts, in order to use it you must provide STRIPE_CA_PRIVATE_KEY env as well." + ); + process.exit(-1); + } + const stripe_ca = new Stripe(secret_ca_key as string, { + apiVersion: '2020-08-27', + typescript: true, + }); + + return stripe_ca; + } + return stripe_default; +})(); app.use(express.json()); @@ -69,9 +94,12 @@ app.post( app.post( '/capture_payment_intent', async (req: express.Request, res: express.Response) => { - const intent = await stripe.paymentIntents.capture(req.body.id); - - res.json({ intent }); + try { + const intent = await stripe.paymentIntents.capture(req.body.id); + res.json({ intent }); + } catch (error) { + res.json({ error: (error as any).raw }); + } } ); @@ -116,6 +144,31 @@ app.get('/get_customers', async (_: express.Request, res: express.Response) => { res.json({ customers: customers.data }); }); +app.get( + '/fetch_latest_interac_charge', + async (_: express.Request, res: express.Response) => { + const charges = await stripe.charges.list(); + const paymentIntents = await stripe.paymentIntents.list(); + + const filteredCharges = charges.data.filter((charge) => { + const paymentIntent = paymentIntents.data.find( + (pi) => pi.id === charge.payment_intent + ); + + return paymentIntent?.payment_method_types.includes('interac_present'); + }); + + const charge = filteredCharges[0]; + + if (!charge) { + res.json({ error: 'Charges list is empty' }); + return; + } + + res.json({ id: charge.id }); + } +); + app.post( '/register_reader', async (req: express.Request, res: express.Response) => { diff --git a/example/src/Config.ts b/example/src/Config.ts index 33d34fd0..5a491a13 100644 --- a/example/src/Config.ts +++ b/example/src/Config.ts @@ -1,6 +1,23 @@ -// @ts-ignore -import { API_URL as defaultURL, API_URL_ANDROID as androidURL } from '@env'; +import { + API_URL as defaultURL, + API_URL_ANDROID as androidURL, + API_CA_URL as defaultCaURL, + API_CA_URL_ANDROID as androidCaURL, + // @ts-ignore +} from '@env'; import { Platform } from 'react-native'; +import { LaunchArguments } from 'react-native-launch-arguments'; -export const API_URL: string = - Platform.OS === 'android' && androidURL ? androidURL : defaultURL; +interface DetoxLaunchArguments { + canada?: boolean; +} + +const args = LaunchArguments.value(); + +const API_CA_URL = Platform.OS === 'android' ? androidCaURL : defaultCaURL; + +export const API_URL: string = args.canada + ? API_CA_URL + : Platform.OS === 'android' && androidURL + ? androidURL + : defaultURL; diff --git a/example/src/components/Button.tsx b/example/src/components/Button.tsx index 27f465a8..b9461208 100644 --- a/example/src/components/Button.tsx +++ b/example/src/components/Button.tsx @@ -15,6 +15,7 @@ type Props = AccessibilityProps & { disabled?: boolean; loading?: boolean; color?: string; + testID?: string; onPress(): void; }; @@ -23,6 +24,7 @@ export default function Button({ variant = 'default', disabled, loading, + testID, color, onPress, ...props @@ -44,6 +46,7 @@ export default function Button({ >(); const { simulated, discoveryMethod } = params; @@ -38,6 +40,7 @@ export default function CollectCardPaymentScreen() { processPayment, retrievePaymentIntent, cancelCollectPaymentMethod, + setSimulatedCard, } = useStripeTerminal({ onDidRequestReaderInput: (input) => { addLogs({ @@ -97,6 +100,8 @@ export default function CollectCardPaymentScreen() { }; const _createPaymentIntent = async () => { + await setSimulatedCard(testCardNumber); + clearLogs(); navigation.navigate('LogListScreen'); addLogs({ @@ -250,7 +255,9 @@ export default function CollectCardPaymentScreen() { }, ], }); - _capturePayment(paymentIntentId); + if (capturePI) { + _capturePayment(paymentIntentId); + } } }; @@ -261,6 +268,7 @@ export default function CollectCardPaymentScreen() { }); const { intent, error } = await capturePaymentIntent(paymentIntentId); + if (error) { addLogs({ name: 'Capture Payment', @@ -293,14 +301,25 @@ export default function CollectCardPaymentScreen() { testID="collect-scroll-view" contentContainerStyle={styles.container} keyboardShouldPersistTaps="always" + testID="collect-scroll-view" > + + setTestCardNumber(value)} + placeholder="card number" + /> + + onChangeText={(value) => setInputValues((state) => ({ ...state, amount: value })) } placeholder="amount" @@ -311,7 +330,7 @@ export default function CollectCardPaymentScreen() { testID="currency-text-field" style={styles.input} value={inputValues.currency} - onChangeText={(value: string) => + onChangeText={(value) => setInputValues((state) => ({ ...state, currency: value })) } placeholder="currency" @@ -323,6 +342,7 @@ export default function CollectCardPaymentScreen() { title="Enable Interac Present" rightElement={ setEnableInterac(value)} /> @@ -354,6 +374,16 @@ export default function CollectCardPaymentScreen() { })) } placeholder="Application Fee Amount" + + setCapturePI(value)} + /> + } /> diff --git a/example/src/screens/DiscoverReadersScreen.tsx b/example/src/screens/DiscoverReadersScreen.tsx index 7b979774..ca3e46b5 100644 --- a/example/src/screens/DiscoverReadersScreen.tsx +++ b/example/src/screens/DiscoverReadersScreen.tsx @@ -394,7 +394,7 @@ export default function DiscoverReadersScreen() { const styles = StyleSheet.create({ container: { backgroundColor: colors.light_gray, - flex: 1, + height: '100%', }, pickerContainer: { position: 'absolute', diff --git a/example/src/screens/ReadReusableCardScreen.tsx b/example/src/screens/ReadReusableCardScreen.tsx index 8a38a990..4a92f75f 100644 --- a/example/src/screens/ReadReusableCardScreen.tsx +++ b/example/src/screens/ReadReusableCardScreen.tsx @@ -108,7 +108,7 @@ export default function ReadReusableCardScreen() { const styles = StyleSheet.create({ container: { backgroundColor: colors.light_gray, - flex: 1, + height: '100%', paddingVertical: 22, }, json: { diff --git a/example/src/screens/ReaderDisplayScreen.tsx b/example/src/screens/ReaderDisplayScreen.tsx index b9d4e88c..1617729a 100644 --- a/example/src/screens/ReaderDisplayScreen.tsx +++ b/example/src/screens/ReaderDisplayScreen.tsx @@ -1,5 +1,11 @@ import React, { useState } from 'react'; -import { ScrollView, Platform, StyleSheet, TextInput } from 'react-native'; +import { + Alert, + ScrollView, + Platform, + StyleSheet, + TextInput, +} from 'react-native'; import List from '../components/List'; import ListItem from '../components/ListItem'; import { useStripeTerminal } from 'stripe-terminal-react-native'; @@ -35,9 +41,11 @@ export default function ReaderDisplayScreen() { if (error) { console.log('error', error); + Alert.alert('setReaderDisplay error', error.message); return; } + Alert.alert('setReaderDisplay success'); console.log('setReaderDisplay success'); }; @@ -46,6 +54,10 @@ export default function ReaderDisplayScreen() { if (error) { console.log('error', error); + Alert.alert('clearReaderDisplay error', error.message); + } else { + console.log('clearReaderDisplay success'); + Alert.alert('clearReaderDisplay success'); } }; @@ -68,6 +80,7 @@ export default function ReaderDisplayScreen() { setCart((c) => ({ ...c, currency: value }))} value={cart.currency} @@ -78,6 +91,7 @@ export default function ReaderDisplayScreen() { setCart((c) => ({ ...c, tax: value }))} value={String(cart.tax)} @@ -86,6 +100,7 @@ export default function ReaderDisplayScreen() { setCart((c) => ({ ...c, amount: value }))} @@ -96,10 +111,15 @@ export default function ReaderDisplayScreen() { - + ); @@ -119,11 +139,7 @@ const styles = StyleSheet.create({ marginTop: 50, }, buttonsContainer: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - paddingHorizontal: 22, + marginTop: 14, }, row: { flexDirection: 'row', @@ -134,7 +150,6 @@ const styles = StyleSheet.create({ input: { height: 44, backgroundColor: colors.white, - color: colors.dark_gray, paddingLeft: 16, marginBottom: 12, borderBottomColor: colors.gray, diff --git a/example/src/screens/RefundPaymentScreen.tsx b/example/src/screens/RefundPaymentScreen.tsx index 793c48a2..703417f3 100644 --- a/example/src/screens/RefundPaymentScreen.tsx +++ b/example/src/screens/RefundPaymentScreen.tsx @@ -1,19 +1,16 @@ import { useNavigation } from '@react-navigation/core'; -import React, { useContext, useState } from 'react'; -import { - Platform, - ScrollView, - StyleSheet, - Text, - TextInput, -} from 'react-native'; +import React, { useContext, useEffect, useState } from 'react'; +import { Platform, StyleSheet, Text, TextInput } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { useStripeTerminal } from 'stripe-terminal-react-native'; import { colors } from '../colors'; import List from '../components/List'; import ListItem from '../components/ListItem'; import { LogContext } from '../components/LogContext'; +import { API_URL } from '../Config'; export default function RefundPaymentScreen() { + const [testCardNumber, setTestCardNumber] = useState('4506445006931933'); const [inputValues, setInputValues] = useState<{ chargeId: string; amount: string; @@ -21,15 +18,41 @@ export default function RefundPaymentScreen() { }>({ chargeId: '', amount: '100', - currency: 'USD', + currency: 'CAD', }); const navigation = useNavigation(); const { addLogs, clearLogs } = useContext(LogContext); - const { collectRefundPaymentMethod, processRefund } = useStripeTerminal(); + const { collectRefundPaymentMethod, processRefund, setSimulatedCard } = + useStripeTerminal(); + + const fetchLatestChargeId = async (): Promise => { + const response = await fetch(`${API_URL}/fetch_latest_interac_charge`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const { id } = await response.json(); + return id; + }; + + useEffect(() => { + async function getLatestRefundId() { + const id = await fetchLatestChargeId(); + + if (id) { + setInputValues((state) => ({ ...state, chargeId: id })); + } + } + getLatestRefundId(); + }, []); const _collectRefundPaymentMethod = async () => { clearLogs(); + + await setSimulatedCard(testCardNumber); + navigation.navigate('LogListScreen'); addLogs({ name: 'Collect Refund Payment Method', @@ -133,7 +156,21 @@ export default function RefundPaymentScreen() { }; return ( - + + + setTestCardNumber(value)} + placeholder="card number" + /> + - + ); } const styles = StyleSheet.create({ container: { backgroundColor: colors.light_gray, - flex: 1, - paddingVertical: 22, + paddingBottom: 22, + height: '100%', }, buttonWrapper: { marginBottom: 60, diff --git a/example/src/screens/RegisterInternetReaderScreen.tsx b/example/src/screens/RegisterInternetReaderScreen.tsx index 486713fd..f63aa5d8 100644 --- a/example/src/screens/RegisterInternetReaderScreen.tsx +++ b/example/src/screens/RegisterInternetReaderScreen.tsx @@ -129,7 +129,7 @@ export default function RegisterInternetReaderScreen() { const styles = StyleSheet.create({ container: { backgroundColor: colors.light_gray, - flex: 1, + height: '100%', paddingVertical: 22, }, input: { diff --git a/example/src/screens/SetupIntentScreen.tsx b/example/src/screens/SetupIntentScreen.tsx index 35f8654a..53c8fc18 100644 --- a/example/src/screens/SetupIntentScreen.tsx +++ b/example/src/screens/SetupIntentScreen.tsx @@ -242,7 +242,7 @@ export default function SetupIntentScreen() { const styles = StyleSheet.create({ container: { backgroundColor: colors.light_gray, - flex: 1, + height: '100%', paddingVertical: 22, }, json: { diff --git a/example/yarn.lock b/example/yarn.lock index f9fd4541..3a231078 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1481,6 +1481,11 @@ dependencies: "@types/yargs-parser" "*" +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -2510,7 +2515,7 @@ cross-spawn@^4.0.2: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^6.0.0: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -3167,6 +3172,13 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" @@ -3236,6 +3248,15 @@ fs-extra@^4.0.2: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -3691,6 +3712,11 @@ is-directory@^0.3.1: resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -3807,6 +3833,13 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +is-wsl@^2.1.1: + version "2.2.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" @@ -4531,6 +4564,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" @@ -5362,6 +5402,14 @@ open@^6.2.0: dependencies: is-wsl "^1.1.0" +open@^7.4.2: + version "7.4.2" + resolved "https://registry.npmjs.org/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -5391,7 +5439,7 @@ ora@^3.4.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" -os-tmpdir@^1.0.0: +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -5465,6 +5513,25 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +patch-package@^6.4.7: + version "6.4.7" + resolved "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148" + integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^2.4.2" + cross-spawn "^6.0.5" + find-yarn-workspace-root "^2.0.0" + fs-extra "^7.0.1" + is-ci "^2.0.0" + klaw-sync "^6.0.0" + minimist "^1.2.0" + open "^7.4.2" + rimraf "^2.6.3" + semver "^5.6.0" + slash "^2.0.0" + tmp "^0.0.33" + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -5566,6 +5633,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postinstall-postinstall@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -5803,6 +5875,11 @@ react-native-keyboard-aware-scroll-view@^0.9.5: prop-types "^15.6.2" react-native-iphone-x-helper "^1.0.3" +react-native-launch-arguments@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/react-native-launch-arguments/-/react-native-launch-arguments-3.0.1.tgz#daabd9792ad7c4e35d3620df318bbc77b447f057" + integrity sha512-BPXyZFLz6RZTf2n/h0A2faOJeGHoQSS0Uv6dJDjJVk5JhCuIVASFDS/5KbU7tdt0x6iYqJmopdRti0zzBTBx8w== + react-native-reanimated@2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.5.0.tgz#315de3a23269afd150df5359252e62937a1a8068" @@ -6097,7 +6174,7 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -rimraf@^2.5.4: +rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -6761,6 +6838,13 @@ through2@^2.0.1: readable-stream "~2.3.6" xtend "~4.0.1" +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" diff --git a/ios/StripeTerminalReactNative.m b/ios/StripeTerminalReactNative.m index 3dcec06e..77cda0b5 100644 --- a/ios/StripeTerminalReactNative.m +++ b/ios/StripeTerminalReactNative.m @@ -136,6 +136,12 @@ @interface RCT_EXTERN_MODULE(StripeTerminalReactNative, RCTEventEmitter) rejecter: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + setSimulatedCard:(NSString *)cardNumber + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) + RCT_EXTERN_METHOD( collectRefundPaymentMethod:(NSDictionary *)params resolver: (RCTPromiseResolveBlock)resolve diff --git a/ios/StripeTerminalReactNative.swift b/ios/StripeTerminalReactNative.swift index 577b7811..8f202010 100644 --- a/ios/StripeTerminalReactNative.swift +++ b/ios/StripeTerminalReactNative.swift @@ -132,6 +132,12 @@ class StripeTerminalReactNative: RCTEventEmitter, DiscoveryDelegate, BluetoothRe Terminal.shared.simulatorConfiguration.availableReaderUpdate = Mappers.mapToSimulateReaderUpdate(update) resolve([:]) } + + @objc(setSimulatedCard:resolver:rejecter:) + func setSimulatedCard(cardNumber: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + Terminal.shared.simulatorConfiguration.simulatedCard = SimulatedCard(testCardNumber: cardNumber) + resolve([:]) + } @objc(setConnectionToken:resolver:rejecter:) func setConnectionToken(params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { diff --git a/package.json b/package.json index 5d9df213..bc1b3f7f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "e2e:test:ios": "detox test --configuration ios --take-screenshots failing --loglevel verbose", "e2e:build:android:release": "detox build --configuration android.emu.release", "e2e:build:ios:release": "detox build --configuration ios.sim.release", - "e2e:test:android:release": "yarn --cwd example start:server:ci && yarn get:testbutler && detox test --configuration android.emu.release --headless --take-screenshots failing --record-logs all --loglevel verbose", + "e2e:test:android:release": "yarn --cwd example start:server:ci && yarn get:testbutler && detox test --configuration android.emu.release --headless --take-screenshots failing --record-logs failing --loglevel verbose", "e2e:test:ios:release": "yarn --cwd example start:server:ci && detox test --configuration ios.sim.release --take-screenshots failing --loglevel verbose", "get:testbutler": "curl -f -o ./test-butler-app.apk https://repo1.maven.org/maven2/com/linkedin/testbutler/test-butler-app/2.2.1/test-butler-app-2.2.1.apk", "docs": "npx typedoc ./src/index.tsx --out ./docs/api-reference --tsconfig ./tsconfig.json", diff --git a/src/StripeTerminalSdk.tsx b/src/StripeTerminalSdk.tsx index fa508892..b0d9a22a 100644 --- a/src/StripeTerminalSdk.tsx +++ b/src/StripeTerminalSdk.tsx @@ -131,6 +131,9 @@ type StripeTerminalSdkType = { cancelReadReusableCard(): Promise<{ error?: StripeError; }>; + setSimulatedCard(cardNumber: string): Promise<{ + error?: StripeError; + }>; }; export default StripeTerminalReactNative as StripeTerminalSdkType; diff --git a/src/functions.ts b/src/functions.ts index 4a8ece4d..2006e380 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -600,6 +600,20 @@ export async function simulateReaderUpdate( } } +export async function setSimulatedCard( + cardNumber: string +): Promise<{ error?: StripeError }> { + try { + await StripeTerminalSdk.setSimulatedCard(cardNumber); + + return {}; + } catch (error) { + return { + error: error as any, + }; + } +} + export async function collectRefundPaymentMethod( params: RefundParams ): Promise<{ diff --git a/src/hooks/useStripeTerminal.tsx b/src/hooks/useStripeTerminal.tsx index 27e26d20..3757f7e3 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -49,6 +49,7 @@ import { connectEmbeddedReader, connectHandoffReader, connectLocalMobileReader, + setSimulatedCard, } from '../functions'; import { StripeTerminalContext } from '../components/StripeTerminalContext'; import { useListener } from './useListener'; @@ -549,6 +550,18 @@ export function useStripeTerminal(props?: Props) { [setLoading, _isInitialized] ); + const _setSimulatedCard = useCallback( + async (cardNumber: string) => { + setLoading(true); + + const response = await setSimulatedCard(cardNumber); + setLoading(false); + + return response; + }, + [setLoading] + ); + const _simulateReaderUpdate = useCallback( async (update: Reader.SimulateUpdateType) => { if (!_isInitialized()) { @@ -703,6 +716,7 @@ export function useStripeTerminal(props?: Props) { connectEmbeddedReader: _connectEmbeddedReader, connectHandoffReader: _connectHandoffReader, connectLocalMobileReader: _connectLocalMobileReader, + setSimulatedCard: _setSimulatedCard, emitter: emitter, discoveredReaders, connectedReader,