From 5f8d36f162baf9a8b279b12136cc021aefabb8d8 Mon Sep 17 00:00:00 2001 From: Arek Kubaczkowski Date: Fri, 18 Mar 2022 12:32:47 +0100 Subject: [PATCH] internet reader e2e tests internet reader e2e fixes fix typo fix e2e tests tests fixes and improvements setReadDisplay/clearReaderDisplay e2e test run setReaderDisplay e2e only on iOS fix setReaderDisplay test fix setReaderDisplay scenario fix test fix e2e test improve reader display screen layout prettier/eslint js files (#172) * updates * updates linting settings, adds prettier target * npm reg removed tvos target (#179) templates (#181) yarn diff check (#180) * add manual yarn.lock diff check * force fail * how about this * and this * restore lockfile Create expo plugin (#113) * chore: create expo plugin * chore: add plugin params * chore: add expo docs * fix package.json path upgrade tsc and clean up example app routing param types (#185) * upgrade tsc * clean up routing param types fix: TextInput color on Android (#84) npm release script (#87) * chore: add publish script * chore: update docs test connection token on init (#177) * chore: test connection token on init * chore: render app conditionally Handle other Android connection methods (#111) * chore: handle other connection methods * chore: add proper e2e tests * fix: e2e tests * fix: e2e test * fix indentations * fix methods params * fix merge issues Revert "Handle other Android connection methods (#111)" (#186) This reverts commit 18db810c86a4761f47f95411ef109e6b19bf46e4. fix dependabot minimist issue (#189) usb connection type (#188) upgrade minimist in example app as well (#190) duplicated event listeners (#182) * fix: duplicated event listeners * temporary listener fix * fix typings * cleanup * fix updateing callbacks refs fix labels handle multiple secret keys, in-person refund tests updated android sdk to 2.7.1 (#200) Android validation and transformation helpers (#192) * making use of new transformation functions * validation helpers * removed some mappers Handle other Android connection methods #111 (#187) * chore: handle other connection methods * chore: add proper e2e tests * fix: e2e tests * fix: e2e test * fix methods params * rebase fixes * rebase fix Co-authored-by: David Henry StripeTerminalReactNativeModule reduced scope (#202) * separated callbacks and listeners from RN module * cancelOperation added * ReactNativeConstants imports * extracting reader.serialNumber [wip] update readme with expo guidance and re-order content (#206) * update readme with expo guidance and re-order content * more * shufflin things * toc more doc fixes (#207) * more fixes * refactor * add note about links not being available adding codeowners (#208) [WIP] Android unit tests (#204) * separated callbacks and listeners from RN module * TokenProvider unit tests * run unit tests in circleci * unit test yarn command * BluetoothReaderListener unit tests * store test results * DiscoveryListener unit tests * updated test output path * test results path updated * UsbListenerTest * TerminalListener and HandoffReaderListener unit tests * one more try with the test output path Bump plist from 3.0.4 to 3.0.5 (#216) Bumps [plist](https://github.com/TooTallNate/node-plist) from 3.0.4 to 3.0.5. - [Release notes](https://github.com/TooTallNate/node-plist/releases) - [Changelog](https://github.com/TooTallNate/plist.js/blob/master/History.md) - [Commits](https://github.com/TooTallNate/node-plist/commits) --- updated-dependencies: - dependency-name: plist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Bump ansi-regex from 4.1.0 to 4.1.1 (#215) Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/chalk/ansi-regex/releases) - [Commits](https://github.com/chalk/ansi-regex/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: ansi-regex dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Bump ansi-regex from 4.1.0 to 4.1.1 in /example (#213) Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/chalk/ansi-regex/releases) - [Commits](https://github.com/chalk/ansi-regex/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: ansi-regex dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Bump plist from 3.0.4 to 3.0.5 in /example (#214) Bumps [plist](https://github.com/TooTallNate/node-plist) from 3.0.4 to 3.0.5. - [Release notes](https://github.com/TooTallNate/node-plist/releases) - [Changelog](https://github.com/TooTallNate/plist.js/blob/master/History.md) - [Commits](https://github.com/TooTallNate/node-plist/commits) --- updated-dependencies: - dependency-name: plist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Reader update errors received by RN SDK (#217) * reader update errors received by RN SDK * lint * ios SDK sends update errors to RN SDK Prevent from calling methods before SDK initializing SDK (#203) * prevent from calling methods before SDK initializing SDK * improve error message * isInitialized as a util function Memoize StripeTerminalProvider values (#221) * fixing hooks deps * fix android all target * pushing looping location check * memoize context * discover * rrc * setupIntents android permissions util (#218) * android permissions util * improve location permission message add android expo steps to readme (#210) * add android steps * shuffle * update both files * toc test tokenProvider on init (#211) * test tokenProvider on init * catchable tokenProvider error Revert "Prevent from calling methods before SDK initializing SDK (#203)" (#225) This reverts commit 38972ad08640bd70bc7d12b902f5fc985b85c986. added metadata property to enable Android SDK RN identification (#227) rename e2e file patch package revert API_URL handle CA server fix commands script rename fix server:ci command use canada API url from env fix server endpoint fix createPaymentIntent via server Add android expo support and permissions checker (#224) * update perms helper * it works * docs * refactor permissions utils logLevel only on StripeTerminalProvider component level (#223) * logLevel only on StripeTerminalProvider component level * fix typo createPaymentIntent uses INTERAC_PRESENT for CAD currency (#232) * createPaymentIntent uses INTERAC_PRESENT for CAD currency * createPaymentIntent receives paymentMethodTypes from the POS application * enum mapping fix add setSimulatedCard method clean up use bluetooth reader for collecting CA payments fix e2e tests use keyboard aware scroll view fix linter set unique testID fix tests fix in-person refund e2e scroll to bottom before tap fix log name fetch latest interac charge refund - set test card number fix refund e2e fix e2e fix android ui fix e2e test, record failing tests prevent call methods being not initialized (#228) * prevent from calling methods before SDK initializing SDK * improve error message * isInitialized as a util function * improvements * remove direct functions export implement onDidReportAvailableUpdate in example app (#236) enable interac for e2e android scenario skip refunding tests on android fix e2e test add comments --- .../StripeTerminalReactNativeModule.kt | 24 +-- docs/deploying-example-app.md | 26 +++ e2e/app.e2e.js | 10 +- e2e/in-person-refund.e2e.js | 149 ++++++++++++++++++ e2e/internet-reader.e2e.js | 147 +++++++++++++++++ e2e/utils.js | 15 +- example/.env.example | 4 + example/ios/Podfile.lock | 6 + example/package.json | 8 +- .../react-native-launch-arguments+3.0.1.patch | 52 ++++++ example/server/index.ts | 65 +++++++- example/src/Config.ts | 25 ++- example/src/components/Button.tsx | 3 + .../src/screens/CollectCardPaymentScreen.tsx | 36 ++++- example/src/screens/DiscoverReadersScreen.tsx | 2 +- example/src/screens/HomeScreen.tsx | 6 + .../src/screens/ReadReusableCardScreen.tsx | 2 +- example/src/screens/ReaderDisplayScreen.tsx | 24 ++- example/src/screens/RefundPaymentScreen.tsx | 65 ++++++-- .../screens/RegisterInternetReaderScreen.tsx | 2 +- example/src/screens/SetupIntentScreen.tsx | 2 +- ios/StripeTerminalReactNative.m | 6 + ios/StripeTerminalReactNative.swift | 6 + package.json | 2 +- src/StripeTerminalSdk.tsx | 3 + src/functions.ts | 14 ++ src/hooks/useStripeTerminal.tsx | 30 ++++ 27 files changed, 667 insertions(+), 67 deletions(-) create mode 100644 docs/deploying-example-app.md create mode 100644 e2e/in-person-refund.e2e.js create mode 100644 e2e/internet-reader.e2e.js create mode 100644 example/patches/react-native-launch-arguments+3.0.1.patch 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/docs/deploying-example-app.md b/docs/deploying-example-app.md new file mode 100644 index 00000000..b973d753 --- /dev/null +++ b/docs/deploying-example-app.md @@ -0,0 +1,26 @@ +## Deploying Example App + +// TODO - find a better location for this Stripe-specifc section prior to launch + +### Android + +The Android example app is deployed to [Firebase App Distribution](https://firebase.google.com/docs/app-distribution) via a CI job that executes after a successful merge to main: + +https://github.com/stripe/stripe-terminal-react-native/blob/e285cc9710cada5bc99434cb0d157354efbd621d/.circleci/config.yml#L265 + +A unique APK is generated for each supported region (EU and US). See the [App Distribution Console](https://console.firebase.google.com/project/internal-terminal/appdistribution/app/android:com.example.stripeterminalreactnative/releases) to view releases, enable build access for users, and generate invite links. + +### iOS + +// TODO + +### Backend + +The Example backend is deployed to Heroku via a CI job that executes after a successful merge to main: + +https://github.com/stripe/stripe-terminal-react-native/blob/e285cc9710cada5bc99434cb0d157354efbd621d/.circleci/config.yml#L296 + +A separate backend instance is generated for each supported region (EU and US): + +- https://stripe-terminal-rn-example-eu.herokuapp.com/ +- https://stripe-terminal-rn-example-us.herokuapp.com/ 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/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index 6dbf299f..f859136f 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -68,6 +68,12 @@ export default function HomeScreen() { navigation.navigate('RefundPaymentScreen'); }} /> + { + navigation.navigate('ReaderDisplayScreen'); + }} + /> ); 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..74771ed3 100644 --- a/example/src/screens/ReaderDisplayScreen.tsx +++ b/example/src/screens/ReaderDisplayScreen.tsx @@ -1,5 +1,5 @@ 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 +35,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 +48,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 +74,7 @@ export default function ReaderDisplayScreen() { setCart((c) => ({ ...c, currency: value }))} value={cart.currency} @@ -78,6 +85,7 @@ export default function ReaderDisplayScreen() { setCart((c) => ({ ...c, tax: value }))} value={String(cart.tax)} @@ -86,6 +94,7 @@ export default function ReaderDisplayScreen() { setCart((c) => ({ ...c, amount: value }))} @@ -96,10 +105,14 @@ export default function ReaderDisplayScreen() { - + ); @@ -119,11 +132,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 +143,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/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..1bf43517 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -49,6 +49,10 @@ import { connectEmbeddedReader, connectHandoffReader, connectLocalMobileReader, +<<<<<<< HEAD +======= + setSimulatedCard, +>>>>>>> 9e02c8a (internet reader e2e tests) } from '../functions'; import { StripeTerminalContext } from '../components/StripeTerminalContext'; import { useListener } from './useListener'; @@ -56,10 +60,17 @@ import { NativeModules } from 'react-native'; const { FETCH_TOKEN_PROVIDER } = NativeModules.StripeTerminalReactNative.getConstants(); +<<<<<<< HEAD const NOT_INITIALIZED_ERROR_MESSAGE = 'First initialize the Stripe Terminal SDK before performing any action'; +======= + +const NOT_INITIALIZED_ERROR_MESSAGE = + 'First initialize the Stripe Terminal SDK before performing any action'; + +>>>>>>> 9e02c8a (internet reader e2e tests) /** * useStripeTerminal hook Props */ @@ -547,6 +558,21 @@ export function useStripeTerminal(props?: Props) { return response; }, [setLoading, _isInitialized] +<<<<<<< HEAD +======= + ); + + const _setSimulatedCard = useCallback( + async (cardNumber: string) => { + setLoading(true); + + const response = await setSimulatedCard(cardNumber); + setLoading(false); + + return response; + }, + [setLoading] +>>>>>>> 9e02c8a (internet reader e2e tests) ); const _simulateReaderUpdate = useCallback( @@ -703,6 +729,10 @@ export function useStripeTerminal(props?: Props) { connectEmbeddedReader: _connectEmbeddedReader, connectHandoffReader: _connectHandoffReader, connectLocalMobileReader: _connectLocalMobileReader, +<<<<<<< HEAD +======= + setSimulatedCard: _setSimulatedCard, +>>>>>>> 9e02c8a (internet reader e2e tests) emitter: emitter, discoveredReaders, connectedReader,