├── .tool-versions ├── .npmignore ├── .babelrc ├── .travis.yml ├── .flowconfig ├── .npmrc ├── android ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── reactnativepayments │ │ ├── ReactNativePaymentsPackage.java │ │ └── ReactNativePaymentsModule.java └── build.gradle ├── js ├── index.js ├── PaymentRequest │ ├── constants.js │ ├── __mocks__ │ │ └── index.js │ ├── errors │ │ ├── __tests__ │ │ │ └── index.test.js │ │ └── index.js │ ├── helpers │ │ ├── __tests__ │ │ │ └── index.test.js │ │ └── index.js │ ├── __tests__ │ │ ├── constants.test.js │ │ ├── PaymentResponse.test.js │ │ ├── index.test.js │ │ └── PaymentRequestUpdateEvent.test.js │ ├── PaymentResponse.js │ ├── types.js │ ├── PaymentRequestUpdateEvent.js │ └── index.js ├── PKPaymentButton │ └── index.js └── NativeBridge │ └── index.js ├── ios ├── ReactNativePayments.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── project.pbxproj ├── Views │ ├── PKPaymentButtonManager.h │ ├── PKPaymentButtonView.h │ ├── PKPaymentButtonManager.m │ └── PKPaymentButtonView.m ├── GatewayManager.h ├── ReactNativePayments.h ├── GatewayManager.m └── ReactNativePayments.m ├── .gitignore ├── react-native-payments.podspec ├── .github └── dependabot.yaml ├── docs ├── PaymentRequestUpdateEvent.md ├── PaymentResponse.md ├── ApplePayButton.md ├── PaymentRequest.md └── NativePayments.md ├── .eslintrc.js ├── package.json └── README.md /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.16.0 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | .babelrc 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["module:metro-react-native-babel-preset"] 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | script: npm test -- --verbose --coverage -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | unsafe.enable_getters_and_setters=true -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @appfolio:registry=https://npm.pkg.github.com 2 | //npm.pkg.github.com/:_authToken=${GITHUB_NPM_TOKEN} 3 | 4 | always-auth=true 5 | cache=tmp/node_cache -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | import _PaymentRequest from './PaymentRequest'; 2 | import { PKPaymentButton } from './PKPaymentButton'; 3 | 4 | export const ApplePayButton = PKPaymentButton; 5 | export const PaymentRequest = _PaymentRequest; -------------------------------------------------------------------------------- /ios/ReactNativePayments.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Views/PKPaymentButtonManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // ApplePayPaymentButtonManager.h 3 | // ReactNativePaymentsExample 4 | // 5 | // Created by Andrej on 15/05/2018. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface PKPaymentButtonManager : RCTViewManager 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Views/PKPaymentButtonView.h: -------------------------------------------------------------------------------- 1 | // 2 | // ApplePayPaymentButton.h 3 | // ReactNativePaymentsExample 4 | // 5 | // Created by Andrej on 16/05/2018. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface PKPaymentButtonView : RCTView 13 | 14 | @property (strong, nonatomic) NSString *buttonStyle; 15 | @property (strong, nonatomic) NSString *buttonType; 16 | @property (nonatomic) CGFloat cornerRadius; 17 | @property (nonatomic, readonly) PKPaymentButton *button; 18 | @property (nonatomic, copy) RCTBubblingEventBlock onPress; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android 25 | **/android/**/.idea 26 | **/android/**/*.iml 27 | **/android/**/gradlew* 28 | **/android/**/*.properties 29 | **/android/**/.gradle 30 | **/android/**/gradle 31 | **/android/**/.settings 32 | **/android/**/.project 33 | 34 | # npm 35 | # 36 | node_modules/ 37 | npm-debug.log 38 | lerna-debug.log 39 | 40 | # editors 41 | # 42 | jsconfig.json 43 | .vscode/* 44 | .idea 45 | *.iml 46 | 47 | # project 48 | # 49 | coverage 50 | -------------------------------------------------------------------------------- /ios/GatewayManager.h: -------------------------------------------------------------------------------- 1 | @import PassKit; 2 | 3 | #import 4 | 5 | @interface GatewayManager : NSObject 6 | 7 | + (NSArray *_Nonnull)getSupportedGateways; 8 | - (void)configureGateway:(NSDictionary *_Nonnull)gatewayParameters 9 | merchantIdentifier:(NSString *_Nonnull)merchantId; 10 | - (void)createTokenWithPayment:(PKPayment *_Nonnull)payment 11 | completion:(void (^_Nullable)(NSString * _Nullable token, NSError * _Nullable error))completion; 12 | 13 | // Stripe 14 | - (void)configureStripeGateway:(NSDictionary *_Nonnull)gatewayParameters 15 | merchantIdentifier:(NSString *_Nonnull)merchantId; 16 | - (void)createStripeTokenWithPayment:(PKPayment *_Nonnull)payment 17 | completion:(void (^_Nullable)(NSString * _Nullable token, NSError * _Nullable error))completion; 18 | @end 19 | -------------------------------------------------------------------------------- /react-native-payments.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | pkg = JSON.parse(File.read("package.json")) 3 | 4 | Pod::Spec.new do |s| 5 | s.name = "react-native-payments" 6 | s.version = pkg["version"] 7 | s.summary = pkg["description"] 8 | s.requires_arc = true 9 | s.license = pkg["license"] 10 | s.homepage = pkg["homepage"] 11 | s.author = pkg["author"] 12 | s.source = { :git => pkg["repository"]["url"], :tag => "#{s.version}" } 13 | s.source_files = 'ios/**/*.{h,m}' 14 | s.platform = :ios, "8.0" 15 | s.requires_arc = true 16 | 17 | s.dependency 'React' 18 | 19 | # Stripe support on this fork is completely untested. This change to depend on 20 | # stripe 23 is to fix simulator build issues in our app. We do not use stripe, 21 | # and I make no guarantee that this works. 22 | s.dependency 'Stripe', '~> 23' 23 | end 24 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | registries: 4 | npm-github: 5 | type: npm-registry 6 | url: https://npm.pkg.github.com 7 | token: "${{secrets.READ_ONLY_PACKAGES_CCIMU}}" 8 | updates: 9 | - package-ecosystem: npm 10 | directory: "/examples/native-next" 11 | schedule: 12 | interval: daily 13 | pull-request-branch-name: 14 | separator: "-" 15 | registries: "*" 16 | - package-ecosystem: npm 17 | directory: "/examples/native" 18 | schedule: 19 | interval: daily 20 | pull-request-branch-name: 21 | separator: "-" 22 | registries: "*" 23 | - package-ecosystem: npm 24 | directory: "/examples/web" 25 | schedule: 26 | interval: daily 27 | pull-request-branch-name: 28 | separator: "-" 29 | registries: "*" 30 | - package-ecosystem: npm 31 | directory: "/" 32 | schedule: 33 | interval: daily 34 | pull-request-branch-name: 35 | separator: "-" 36 | registries: "*" 37 | -------------------------------------------------------------------------------- /js/PaymentRequest/constants.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | 3 | export const MODULE_SCOPING = 'NativePayments'; 4 | export const SHIPPING_ADDRESS_CHANGE_EVENT = 'shippingaddresschange'; 5 | export const SHIPPING_OPTION_CHANGE_EVENT = 'shippingoptionchange'; 6 | export const PAYMENT_METHOD_CHANGE_EVENT = 'paymentmethodchange'; 7 | export const INTERNAL_SHIPPING_ADDRESS_CHANGE_EVENT = `${MODULE_SCOPING}:on${SHIPPING_ADDRESS_CHANGE_EVENT}`; 8 | export const INTERNAL_SHIPPING_OPTION_CHANGE_EVENT = `${MODULE_SCOPING}:on${SHIPPING_OPTION_CHANGE_EVENT}`; 9 | export const INTERNAL_PAYMENT_METHOD_CHANGE_EVENT = `${MODULE_SCOPING}:on${PAYMENT_METHOD_CHANGE_EVENT}`; 10 | export const USER_DISMISS_EVENT = `${MODULE_SCOPING}:onuserdismiss`; 11 | export const USER_ACCEPT_EVENT = `${MODULE_SCOPING}:onuseraccept`; 12 | export const GATEWAY_ERROR_EVENT = `${MODULE_SCOPING}:ongatewayerror`; 13 | export const SUPPORTED_METHOD_NAME = 14 | Platform.OS === 'ios' ? 'apple-pay' : 'android-pay'; 15 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | def safeExtGet(prop, fallback) { 4 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 5 | } 6 | 7 | android { 8 | compileSdkVersion safeExtGet("compileSdkVersion", 28) 9 | buildToolsVersion safeExtGet("buildToolsVersion", "28.0.3") 10 | 11 | defaultConfig { 12 | minSdkVersion safeExtGet("minSdkVersion", 21) 13 | targetSdkVersion safeExtGet("targetSdkVersion", 28) 14 | versionCode 1 15 | versionName "1.0" 16 | ndk { 17 | abiFilters "armeabi-v7a", "x86" 18 | } 19 | } 20 | lintOptions { 21 | warning 'InvalidPackage' 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation 'com.facebook.react:react-native:+' 27 | implementation 'com.google.android.gms:play-services-base:17.0.0' 28 | implementation 'com.google.android.gms:play-services-identity:17.0.0' 29 | implementation 'com.google.android.gms:play-services-wallet:17.0.0' 30 | } 31 | -------------------------------------------------------------------------------- /js/PaymentRequest/__mocks__/index.js: -------------------------------------------------------------------------------- 1 | const mockReactNativeIOS = { 2 | Platform: { 3 | OS: "ios", 4 | }, 5 | DeviceEventEmitter: { 6 | removeSubscription: () => {}, 7 | addListener: () => {}, 8 | }, 9 | }; 10 | 11 | const mockReactNativeAndroid = Object.assign({}, mockReactNativeIOS, { 12 | Platform: { 13 | OS: "android", 14 | }, 15 | }); 16 | 17 | const mockNativePaymentsSupportedIOS = { 18 | canMakePayments: () => true, 19 | createPaymentRequest: () => {}, 20 | handleDetailsUpdate: async () => {}, 21 | show: (cb) => cb(), // TODO, may have to fire an event that DeviceEventEmitter will listen to 22 | abort: async () => {}, 23 | complete: (paymentStatus, cb) => cb(), 24 | }; 25 | 26 | const mockNativePaymentsUnsupportedIOS = Object.assign( 27 | {}, 28 | mockNativePaymentsSupportedIOS, 29 | { 30 | canMakePayments: () => false, 31 | } 32 | ); 33 | 34 | module.exports = { 35 | mockReactNativeIOS, 36 | mockReactNativeAndroid, 37 | mockNativePaymentsSupportedIOS, 38 | mockNativePaymentsUnsupportedIOS 39 | }; 40 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativepayments/ReactNativePaymentsPackage.java: -------------------------------------------------------------------------------- 1 | 2 | package com.reactnativepayments; 3 | 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.bridge.NativeModule; 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.uimanager.ViewManager; 12 | import com.facebook.react.bridge.JavaScriptModule; 13 | public class ReactNativePaymentsPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new ReactNativePaymentsModule(reactContext)); 17 | } 18 | 19 | // Deprecated RN 0.47 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.emptyList(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ios/Views/PKPaymentButtonManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // ApplePayPaymentButtonManager.m 3 | // ReactNativePaymentsExample 4 | // 5 | // Created by Andrej on 15/05/2018. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "PKPaymentButtonManager.h" 11 | #import "PKPaymentButtonView.h" 12 | 13 | @implementation PKPaymentButtonManager 14 | 15 | RCT_EXPORT_MODULE() 16 | 17 | RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) 18 | 19 | RCT_CUSTOM_VIEW_PROPERTY(buttonType, NSString, PKPaymentButtonView) 20 | { 21 | if (json) { 22 | [view setButtonType:[RCTConvert NSString:json]]; 23 | } 24 | } 25 | 26 | RCT_CUSTOM_VIEW_PROPERTY(buttonStyle, NSString, PKPaymentButtonView) 27 | { 28 | if (json) { 29 | [view setButtonStyle:[RCTConvert NSString:json]]; 30 | } 31 | } 32 | 33 | RCT_CUSTOM_VIEW_PROPERTY(cornerRadius, CGFloat, PKPaymentButtonView) 34 | { 35 | if (json) { 36 | [view setCornerRadius:[RCTConvert CGFloat:json]]; 37 | } 38 | } 39 | 40 | - (UIView *) view 41 | { 42 | return [PKPaymentButtonView new]; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /js/PaymentRequest/errors/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const { DOMException } = require('..'); 2 | 3 | describe('errors', () => { 4 | describe('DOMException', () => { 5 | it('should init a `AbortError` `DOMException`', () => { 6 | expect(() => { 7 | throw new DOMException('AbortError'); 8 | }).toThrow('The operation was aborted.'); 9 | }); 10 | 11 | it('should init a `InvalidStateError` `DOMException`', () => { 12 | expect(() => { 13 | throw new DOMException('InvalidStateError'); 14 | }).toThrow('The object is in an invalid state.'); 15 | }); 16 | 17 | it('should init a `NotAllowedError` `DOMException`', () => { 18 | expect(() => { 19 | throw new DOMException('NotAllowedError'); 20 | }).toThrow( 21 | 'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.' 22 | ); 23 | }); 24 | 25 | it('should init a `NotSupportedError` `DOMException`', () => { 26 | expect(() => { 27 | throw new DOMException('NotSupportedError'); 28 | }).toThrow('The operation is not supported.'); 29 | }); 30 | 31 | it('should init a `SecurityError` `DOMException`', () => { 32 | expect(() => { 33 | throw new DOMException('SecurityError'); 34 | }).toThrow('The operation is insecure.'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /js/PaymentRequest/errors/index.js: -------------------------------------------------------------------------------- 1 | import ExtendableError from 'es6-error'; 2 | 3 | const ERROR_MESSAGES = { 4 | AbortError: 'The operation was aborted.', // Request cancelled 5 | InvalidStateError: 'The object is in an invalid state.', 6 | NotAllowedError: 7 | 'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.', 8 | NotSupportedError: 'The operation is not supported.', 9 | SecurityError: 'The operation is insecure.' 10 | }; 11 | 12 | class ReactNativePaymentsError extends ExtendableError { 13 | constructor(errorMessage) { 14 | super(`[ReactNativePayments] ${errorMessage}`); 15 | } 16 | } 17 | 18 | export class DOMException extends ReactNativePaymentsError { 19 | constructor(errorType) { 20 | const errorMessage = ERROR_MESSAGES[errorType] || errorType; 21 | 22 | super(`DOMException: ${errorMessage}`); 23 | } 24 | } 25 | 26 | export class TypeError extends ReactNativePaymentsError { 27 | constructor(errorMessage) { 28 | super(`TypeError: ${errorMessage}`); 29 | } 30 | } 31 | 32 | export class ConstructorError extends ReactNativePaymentsError { 33 | constructor(errorMessage) { 34 | super(`Failed to construct 'PaymentRequest': ${errorMessage}`); 35 | } 36 | } 37 | 38 | export class GatewayError extends ExtendableError { 39 | constructor(errorMessage) { 40 | super(`${errorMessage}`); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/PaymentRequestUpdateEvent.md: -------------------------------------------------------------------------------- 1 | # PaymentRequestUpdateEvent 2 | ### constructor(name, paymentRequest) 3 | Initializes the payment request update event. 4 | 5 | __Arguments__ 6 | - name - `onshippingaddresschange | onshippingoptionchange` 7 | - paymentRequest - `PaymentRequest` 8 | 9 |
10 | Example 11 | 12 | ```es6 13 | const event = new PaymentRequestUpdateEvent('onshippingaddresschange', paymentRequest); 14 | ``` 15 | 16 |
17 | 18 | --- 19 | 20 | ### updateWith(details) 21 | Updates the payment request with the details provided. 22 | 23 | __Arguments__ 24 | - details - `PaymentDetailsUpdate` 25 | 26 |
27 | Example 28 | 29 | ```es6 30 | event.updateWith({ 31 | displayItems: [ 32 | { 33 | label: 'Movie Ticket', 34 | amount: { currency: 'USD', value: '15.00' } 35 | }, 36 | { 37 | label: 'Shipping', 38 | amount: { currency: 'USD', value: '5.00' } 39 | } 40 | ], 41 | total: { 42 | label: 'Merchant Name', 43 | amount: { currency: 'USD', value: '20.00' } 44 | }, 45 | shippingOptions: [ 46 | { 47 | id: 'economy', 48 | label: 'Economy Shipping', 49 | amount: { currency: 'USD', value: '0.00' }, 50 | detail: 'Arrives in 3-5 days' 51 | }, 52 | { 53 | id: 'express', 54 | label: 'Express Shipping', 55 | amount: { currency: 'USD', value: '5.00' }, 56 | detail: 'Arrives tomorrow', 57 | selected 58 | } 59 | ] 60 | }); 61 | ``` 62 | 63 |
64 | 65 | --- 66 | -------------------------------------------------------------------------------- /ios/ReactNativePayments.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | @import PassKit; 3 | @import AddressBook; 4 | 5 | #import 6 | 7 | #import "GatewayManager.h" 8 | 9 | @interface ReactNativePayments : NSObject 10 | 11 | @property (nonatomic, strong) RCTResponseSenderBlock callback; 12 | @property (nonatomic, strong) PKPaymentRequest *paymentRequest; 13 | @property (nonatomic, strong) NSDictionary *initialOptions; 14 | @property (nonatomic, strong) GatewayManager *gatewayManager; 15 | @property BOOL *hasGatewayParameters; 16 | @property (nonatomic, strong) PKPaymentAuthorizationViewController *viewController; 17 | @property (nonatomic, copy) void (^completion)(PKPaymentAuthorizationStatus); 18 | @property (nonatomic, copy) void (^shippingContactCompletion)(PKPaymentAuthorizationStatus, NSArray * _Nonnull, NSArray * _Nonnull); 19 | @property (nonatomic, copy) void (^shippingMethodCompletion)(PKPaymentAuthorizationStatus, NSArray * _Nonnull); 20 | @property (nonatomic, copy) void (^paymentMethodCompletion)(NSArray * _Nonnull); 21 | 22 | // Private methods 23 | - (NSArray *_Nonnull)getSupportedNetworksFromMethodData:(NSDictionary *_Nonnull)methodData; 24 | - (NSArray *_Nonnull)getPaymentSummaryItemsFromDetails:(NSDictionary *_Nonnull)details; 25 | - (NSArray *_Nonnull)getShippingMethodsFromDetails:(NSDictionary *_Nonnull)details; 26 | - (PKPaymentSummaryItem *_Nonnull)convertDisplayItemToPaymentSummaryItem:(NSDictionary *_Nonnull)displayItem; 27 | - (PKShippingMethod *_Nonnull)convertShippingOptionToShippingMethod:(NSDictionary *_Nonnull)shippingOption; 28 | - (void)handleUserAccept:(PKPayment *_Nonnull)payment 29 | paymentToken:(NSString *_Nullable)token; 30 | - (void)handleGatewayError:(NSError *_Nonnull)error; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const IGNORE = 0; 2 | const WARN = 1; 3 | const ERROR = 2; 4 | 5 | module.exports = { 6 | "extends": "airbnb-base", 7 | "parser": "babel-eslint", 8 | "plugins": [ 9 | "flowtype" 10 | ], 11 | "rules": { 12 | "import/no-named-as-default-member": IGNORE, 13 | "import/no-named-as-default": IGNORE, 14 | "no-tabs": IGNORE, 15 | "camelcase": IGNORE, 16 | "no-console": IGNORE, 17 | "no-param-reassign": IGNORE, 18 | "import/prefer-default-export": IGNORE, 19 | "consistent-return": IGNORE, 20 | "max-len": IGNORE, 21 | "no-continue": IGNORE, 22 | 'no-case-declarations': IGNORE, 23 | "indent": [ERROR, 2, { "SwitchCase": 1, "VariableDeclarator": 1, "ignoredNodes": ["TemplateLiteral > *"] }], 24 | "class-methods-use-this": IGNORE, 25 | "no-restricted-syntax": IGNORE, 26 | "prefer-template": IGNORE, 27 | "no-plusplus": IGNORE, 28 | "default-case": IGNORE, 29 | "no-useless-constructor": IGNORE, 30 | "jsx-a11y/accessible-emoji": IGNORE, 31 | "no-use-before-define": IGNORE, 32 | "curly": IGNORE, 33 | "no-unused-expressions": [ERROR, { "allowShortCircuit": true }], 34 | "prefer-destructuring": IGNORE, 35 | "no-await-in-loop": IGNORE, 36 | "global-require": IGNORE, 37 | "func-names": IGNORE, 38 | "linebreak-style": IGNORE, 39 | "no-empty-function": IGNORE, 40 | "no-labels": IGNORE, 41 | "func-names": IGNORE, 42 | "guard-for-in": IGNORE, 43 | "radix": IGNORE, 44 | "import/no-dynamic-require": IGNORE, 45 | "quote-props": IGNORE, 46 | "no-shadow": IGNORE, 47 | "no-extra-label": IGNORE, 48 | "arrow-parens": IGNORE, 49 | "quotes": IGNORE, 50 | "prefer-rest-params": IGNORE, 51 | "no-nested-ternary": IGNORE, 52 | "newline-per-chained-call": IGNORE, 53 | "no-restricted-globals": IGNORE, 54 | "dot-notation": IGNORE, 55 | "arrow-body-style": IGNORE, 56 | "no-loop-func": IGNORE, 57 | "no-useless-escape": IGNORE, 58 | "no-trailing-spaces": IGNORE, 59 | "import/order": IGNORE, 60 | "no-lonely-if": IGNORE, 61 | }, 62 | "env": { 63 | "jest": true 64 | }, 65 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@appfolio/react-native-payments", 3 | "version": "0.14.2-1", 4 | "description": "Welcome to the best and most comprehensive library for integrating payments like Apple Pay and Google Pay into your React Native app.", 5 | "scripts": { 6 | "run:packager": "cd examples/native && yarn run:packager", 7 | "run:ios": "cd examples/native && yarn run:ios", 8 | "run:web": "cd examples/web && yarn run:web", 9 | "run:demo": "cd examples/native && yarn run:demo", 10 | "lint": "eslint ./lib/js", 11 | "lint:fix": "npm run lint -- --fix", 12 | "test": "jest" 13 | }, 14 | "homepage": "https://github.com/appfolio/react-native-payments", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/appfolio/react-native-payments.git" 18 | }, 19 | "publishConfig": { 20 | "registry": "https://npm.pkg.github.com/" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "react-native", 25 | "apple-pay", 26 | "stripe", 27 | "payments", 28 | "payment-request", 29 | "sca", 30 | "strong customer authentication", 31 | "2 factor", 32 | "2fa", 33 | "android", 34 | "ios", 35 | "payment intents", 36 | "cross platform" 37 | ], 38 | "author": "Naoufal Kadhom & Freeman Industries", 39 | "license": "MIT", 40 | "dependencies": { 41 | "es6-error": "^4.0.2", 42 | "uuid": "^11.1.0", 43 | "validator": "^13.12.0" 44 | }, 45 | "devDependencies": { 46 | "babel-eslint": "^10.1.0", 47 | "babel-jest": "27.5.1", 48 | "metro-react-native-babel-preset": "^0.72.3", 49 | "eslint": "^6.1.0", 50 | "eslint-config-airbnb-base": "^14.0.0", 51 | "eslint-plugin-flowtype": "^4.7.0", 52 | "eslint-plugin-import": "^2.17.3", 53 | "husky": "^0.14.1", 54 | "jest": "27.5.1", 55 | "react-native": "0.74.4", 56 | "react-test-renderer": "18.1.0" 57 | }, 58 | "peerDependencies": { 59 | "react": ">=18", 60 | "react-native": ">=0.70" 61 | }, 62 | "jest": { 63 | "testPathIgnorePatterns": [ 64 | "/node_modules/", 65 | "/examples/" 66 | ] 67 | }, 68 | "main": "js/index.js" 69 | } 70 | -------------------------------------------------------------------------------- /ios/GatewayManager.m: -------------------------------------------------------------------------------- 1 | #import "GatewayManager.h" 2 | 3 | #if __has_include() 4 | #import 5 | #endif 6 | 7 | @implementation GatewayManager 8 | 9 | + (NSArray *)getSupportedGateways 10 | { 11 | NSMutableArray *supportedGateways = [NSMutableArray array]; 12 | 13 | #if __has_include() 14 | [supportedGateways addObject:@"stripe"]; 15 | #endif 16 | 17 | return [supportedGateways copy]; 18 | } 19 | 20 | - (void)configureGateway:(NSDictionary *_Nonnull)gatewayParameters 21 | merchantIdentifier:(NSString *_Nonnull)merchantId 22 | { 23 | #if __has_include() 24 | if ([gatewayParameters[@"gateway"] isEqualToString:@"stripe"]) { 25 | [self configureStripeGateway:gatewayParameters merchantIdentifier:merchantId]; 26 | } 27 | #endif 28 | } 29 | 30 | - (void)createTokenWithPayment:(PKPayment *_Nonnull)payment 31 | completion:(void (^_Nullable)(NSString * _Nullable token, NSError * _Nullable error))completion 32 | { 33 | #if __has_include() 34 | [self createStripeTokenWithPayment:payment completion:completion]; 35 | #endif 36 | } 37 | 38 | // Stripe 39 | - (void)configureStripeGateway:(NSDictionary *_Nonnull)gatewayParameters 40 | merchantIdentifier:(NSString *_Nonnull)merchantId 41 | { 42 | #if __has_include() 43 | NSString *stripePublishableKey = gatewayParameters[@"stripe:publishableKey"]; 44 | [[STPPaymentConfiguration sharedConfiguration] setPublishableKey:stripePublishableKey]; 45 | [[STPPaymentConfiguration sharedConfiguration] setAppleMerchantIdentifier:merchantId]; 46 | #endif 47 | } 48 | 49 | - (void)createStripeTokenWithPayment:(PKPayment *)payment completion:(void (^)(NSString * _Nullable, NSError * _Nullable))completion 50 | { 51 | #if __has_include() 52 | [[STPAPIClient sharedClient] createTokenWithPayment:payment completion:^(STPToken * _Nullable token, NSError * _Nullable error) 53 | { 54 | if (error) { 55 | completion(nil, error); 56 | } else { 57 | completion(token.tokenId, nil); 58 | } 59 | }]; 60 | #endif 61 | } 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /js/PaymentRequest/helpers/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | isValidDecimalMonetaryValue, 3 | isValidStringAmount, 4 | toNumber 5 | } = require('..'); 6 | 7 | describe('helpers', () => { 8 | describe('isValidDecimalMonetaryValue', () => { 9 | it('`Array` should not be a valid string amount', () => { 10 | expect(isValidDecimalMonetaryValue([])).toBe(false); 11 | }); 12 | 13 | it('`Object` should not be a valid string amount', () => { 14 | expect(isValidDecimalMonetaryValue({})).toBe(false); 15 | }); 16 | 17 | it('`Function` should not be a valid string amount', () => { 18 | expect(isValidDecimalMonetaryValue(() => {})).toBe(false); 19 | }); 20 | 21 | it('`undefined` should not be a valid string amount', () => { 22 | expect(isValidDecimalMonetaryValue(undefined)).toBe(false); 23 | }); 24 | 25 | it('`null` should not be a valid string amount', () => { 26 | expect(isValidDecimalMonetaryValue(null)).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('isValidStringAmount', () => { 31 | it('`9.999` should not be a valid string amount', () => { 32 | expect(isValidStringAmount('9.999')).toBe(true); 33 | }); 34 | 35 | it('`9.99` should be a valid string amount', () => { 36 | expect(isValidStringAmount('9.99')).toBe(true); 37 | }); 38 | 39 | it('`9.9` should be a valid string amount', () => { 40 | expect(isValidStringAmount('9.9')).toBe(true); 41 | }); 42 | 43 | it('`9` should be a valid string amount', () => { 44 | expect(isValidStringAmount('9')).toBe(true); 45 | }); 46 | 47 | it('`9.` should not be a valid string amount', () => { 48 | expect(isValidStringAmount('9.')).toBe(false); 49 | }); 50 | }); 51 | 52 | describe('toNumber', () => { 53 | it('"9.999" should convert to 9.999', () => { 54 | expect(toNumber('9.999')).toBe(9.999); 55 | }); 56 | 57 | it('"9.99" should convert to 9.99', () => { 58 | expect(toNumber('9.99')).toBe(9.99); 59 | }); 60 | 61 | it('"9.9" should convert to 9.9', () => { 62 | expect(toNumber('9.9')).toBe(9.9); 63 | }); 64 | 65 | it('"9" should convert to 9', () => { 66 | expect(toNumber('9')).toBe(9); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /js/PKPaymentButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from "react"; 4 | import { NativeModules, requireNativeComponent } from "react-native"; 5 | 6 | type PKPaymentButtonType = 7 | // A button with the Apple Pay logo only. 8 | | "plain" 9 | // A button with the text “Buy with” and the Apple Pay logo. 10 | | "buy" 11 | // A button with the text “Set up” and the Apple Pay logo. 12 | | "setUp" 13 | // A button with the text “Pay with” and the Apple Pay logo. 14 | | "inStore" 15 | // A button with the text "Donate with" and the Apple Pay logo. 16 | | "donate" 17 | // A button with the text "Continue with" and the Apple Pay logo. 18 | | "continue"; 19 | 20 | type PKPaymentButtonStyle = 21 | // A white button with black lettering (shown here against a gray background to ensure visibility). 22 | | "white" 23 | // A white button with black lettering and a black outline. 24 | | "whiteOutline" 25 | // A black button with white lettering. 26 | | "black"; 27 | 28 | type Props = $Exact<{ 29 | type: ButtonType, 30 | style: ButtonStyle, 31 | onPress: Function, 32 | width?: number, 33 | height?: number, 34 | cornerRadius?: number, 35 | minWidth?: number, 36 | minHeight?: number, 37 | maxWidth?: number, 38 | maxHeight?: number, 39 | }>; 40 | 41 | const RNPKPaymentButton = requireNativeComponent("PKPaymentButton", null, { 42 | nativeOnly: { onPress: true }, 43 | }); 44 | 45 | export type ButtonType = PKPaymentButtonType; 46 | export type ButtonStyle = PKPaymentButtonStyle; 47 | 48 | export class PKPaymentButton extends React.Component { 49 | static defaultProps = { 50 | buttonStyle: "black", 51 | buttonType: "plain", 52 | minWidth: 100, 53 | minHeight: 30, 54 | cornerRadius: 4, 55 | }; 56 | 57 | render() { 58 | return ( 59 | 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /js/PaymentRequest/__tests__/constants.test.js: -------------------------------------------------------------------------------- 1 | const { mockReactNativeIOS } = require('../__mocks__'); 2 | 3 | jest.mock('react-native', () => mockReactNativeIOS); 4 | const { 5 | MODULE_SCOPING, 6 | SHIPPING_ADDRESS_CHANGE_EVENT, 7 | SHIPPING_OPTION_CHANGE_EVENT, 8 | INTERNAL_SHIPPING_ADDRESS_CHANGE_EVENT, 9 | INTERNAL_SHIPPING_OPTION_CHANGE_EVENT, 10 | USER_DISMISS_EVENT, 11 | USER_ACCEPT_EVENT, 12 | SUPPORTED_METHOD_NAME 13 | } = require('../constants'); 14 | 15 | describe('constants', () => { 16 | describe('MODULE_SCOPING', () => { 17 | it('should be equal to `NativePayments`', () => { 18 | expect(MODULE_SCOPING).toBe('NativePayments'); 19 | }); 20 | }); 21 | 22 | describe('SHIPPING_ADDRESS_CHANGE_EVENT', () => { 23 | it('should be equal to `shippingaddresschange`', () => { 24 | expect(SHIPPING_ADDRESS_CHANGE_EVENT).toBe('shippingaddresschange'); 25 | }); 26 | }); 27 | 28 | describe('SHIPPING_OPTION_CHANGE_EVENT', () => { 29 | it('should be equal to `shippingoptionchange`', () => { 30 | expect(SHIPPING_OPTION_CHANGE_EVENT).toBe('shippingoptionchange'); 31 | }); 32 | }); 33 | 34 | describe('INTERNAL_SHIPPING_ADDRESS_CHANGE_EVENT', () => { 35 | it('should be equal to `NativePayments:onshippingaddresschange`', () => { 36 | expect(INTERNAL_SHIPPING_ADDRESS_CHANGE_EVENT).toBe( 37 | 'NativePayments:onshippingaddresschange' 38 | ); 39 | }); 40 | }); 41 | 42 | describe('INTERNAL_SHIPPING_OPTION_CHANGE_EVENT', () => { 43 | it('should be equal to `NativePayments:onshippingoptionchange`', () => { 44 | expect(INTERNAL_SHIPPING_OPTION_CHANGE_EVENT).toBe( 45 | 'NativePayments:onshippingoptionchange' 46 | ); 47 | }); 48 | }); 49 | 50 | describe('USER_DISMISS_EVENT', () => { 51 | it('should be equal to `NativePayments:onuserdismiss`', () => { 52 | expect(USER_DISMISS_EVENT).toBe('NativePayments:onuserdismiss'); 53 | }); 54 | }); 55 | 56 | describe('USER_ACCEPT_EVENT', () => { 57 | it('should be equal to `NativePayments:onuseraccept`', () => { 58 | expect(USER_ACCEPT_EVENT).toBe('NativePayments:onuseraccept'); 59 | }); 60 | }); 61 | 62 | describe('SUPPORTED_METHOD_NAME', () => { 63 | it('should be equal to `apple-pay` when platform is `ios`', () => { 64 | expect(SUPPORTED_METHOD_NAME).toBe('apple-pay'); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /docs/PaymentResponse.md: -------------------------------------------------------------------------------- 1 | # PaymentResponse 2 | ### constructor(paymentResponse) 3 | Initializes the payment response. 4 | 5 | __Arguments__ 6 | - paymentResponse - `Array` 7 | 8 |
9 | Example 10 | 11 | ```es6 12 | const PAYMENT_RESPONSE = { 13 | requestId: 'demo', 14 | methodName: 'apple-pay', 15 | details: { 16 | transactionIdentifier: 'some-id', 17 | paymentData: {} 18 | } 19 | }; 20 | 21 | const paymentResponse = new PaymentResponse(PAYMENT_RESPONSE); 22 | ``` 23 | 24 |
25 | 26 | --- 27 | 28 | ### complete() 29 | Displays a success/failure animation and dismisses the payment request based on the payment status provided. 30 | 31 | __Arguments__ 32 | - paymentStatus - `PaymentComplete` 33 | 34 |
35 | Example 36 | 37 | ```es6 38 | paymentResponse.complete('success'); 39 | ``` 40 | 41 |
42 | 43 | --- 44 | 45 | ### methodName 46 | 47 |
48 | Example 49 | 50 | ```es6 51 | console.log(paymentResponse.methodName); // apple-pay 52 | ``` 53 | 54 |
55 | 56 | --- 57 | 58 | ### details 59 | 60 |
61 | Example 62 | 63 | ```es6 64 | console.log(paymentResponse.details); // {} 65 | ``` 66 | 67 |
68 | 69 | --- 70 | 71 | ### shippingAddress 72 | 73 |
74 | Example 75 | 76 | ```es6 77 | console.log(paymentResponse.shippingAddress); // null 78 | ``` 79 | 80 |
81 | 82 | --- 83 | 84 | ### shippingOption 85 | 86 |
87 | Example 88 | 89 | ```es6 90 | console.log(paymentResponse.shippingOption); // null 91 | ``` 92 | 93 |
94 | 95 | --- 96 | 97 | ### payerName 98 | 99 |
100 | Example 101 | 102 | ```es6 103 | console.log(paymentResponse.payerName); // null 104 | ``` 105 | 106 |
107 | 108 | --- 109 | 110 | ### payerEmail 111 | 112 |
113 | Example 114 | 115 | ```es6 116 | console.log(paymentResponse.payerEmail); // null 117 | ``` 118 | 119 |
120 | 121 | --- 122 | 123 | ### payerPhone 124 | 125 |
126 | Example 127 | 128 | ```es6 129 | console.log(paymentResponse.payerPhone); // null 130 | ``` 131 | 132 |
133 | 134 | --- 135 | 136 | ### demo 137 | 138 |
139 | Example 140 | 141 | ```es6 142 | console.log(paymentResponse.requestId); // demo 143 | ``` 144 | 145 |
146 | 147 | --- -------------------------------------------------------------------------------- /js/PaymentRequest/PaymentResponse.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Types 4 | import type { 5 | PaymentComplete, 6 | PaymentDetailsInit, 7 | PaymentAddress 8 | } from './types'; 9 | 10 | // Modules 11 | import NativePayments from '../NativeBridge'; 12 | 13 | export default class PaymentResponse { 14 | // Internal Slots 15 | _requestId: string; 16 | _methodName: string; 17 | _details: PaymentDetailsInit; 18 | _shippingAddress: null | PaymentAddress; 19 | _shippingOption: null | string; 20 | _payerName: null | string; 21 | _payerPhone: null | string; 22 | _payerEmail: null | string; 23 | _completeCalled: boolean; 24 | 25 | constructor(paymentResponse: Object) { 26 | // Set properties as readOnly 27 | this._requestId = paymentResponse.requestId; 28 | this._methodName = paymentResponse.methodName; 29 | this._details = paymentResponse.details; 30 | this._shippingAddress = paymentResponse.shippingAddress; 31 | this._shippingOption = paymentResponse.shippingOption; 32 | this._payerName = paymentResponse.payerName; 33 | this._payerPhone = paymentResponse.payerPhone; 34 | this._payerEmail = paymentResponse.payerEmail; 35 | 36 | // Internal Slots 37 | this._completeCalled = false; 38 | } 39 | 40 | // https://www.w3.org/TR/payment-request/#requestid-attribute 41 | get requestId(): string { 42 | return this._requestId; 43 | } 44 | 45 | // https://www.w3.org/TR/payment-request/#methodname-attribute 46 | get methodName(): string { 47 | return this._methodName; 48 | } 49 | 50 | // https://www.w3.org/TR/payment-request/#details-attribute 51 | get details(): PaymentDetailsInit { 52 | return this._details; 53 | } 54 | 55 | // https://www.w3.org/TR/payment-request/#shippingaddress-attribute-1 56 | get shippingAddress(): null | PaymentAddress { 57 | return this._shippingAddress; 58 | } 59 | 60 | // https://www.w3.org/TR/payment-request/#shippingoption-attribute-1 61 | get shippingOption(): null | string { 62 | return this._shippingOption; 63 | } 64 | 65 | // https://www.w3.org/TR/payment-request/#payername-attribute 66 | get payerName(): null | string { 67 | return this._payerName; 68 | } 69 | 70 | // https://www.w3.org/TR/payment-request/#payerphone-attribute 71 | get payerPhone(): null | string { 72 | return this._payerPhone; 73 | } 74 | 75 | // https://www.w3.org/TR/payment-request/#payeremail-attribute 76 | get payerEmail(): null | string { 77 | return this._payerEmail; 78 | } 79 | 80 | // https://www.w3.org/TR/payment-request/#complete-method 81 | complete(paymentStatus: PaymentComplete) { 82 | if (this._completeCalled === true) { 83 | throw new Error('InvalidStateError'); 84 | } 85 | 86 | this._completeCalled = true; 87 | 88 | return new Promise((resolve, reject) => { 89 | return NativePayments.complete(paymentStatus, () => { 90 | return resolve(undefined); 91 | }); 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /docs/ApplePayButton.md: -------------------------------------------------------------------------------- 1 | # Apple Pay button 2 | 3 | Provides a button that is used either to trigger payments through Apple Pay or to prompt the user to set up a card. 4 | 5 | `ApplePayButton` uses native API provided by Apple. Due to this fact, button meets User Interface guidelines required by Apple in review process. Make sure you consult [Human Interface Guidelines](https://developer.apple.com/ios/human-interface-guidelines/technologies/apple-pay/) prior to submitting app to App Store. 6 | 7 | ## Styling 8 | 9 | ### Button type 10 | 11 | Type dictates button's call to action word and a message. Each option 12 | includes the Apple Pay logo alone and the call to action word (based on button type) with 13 | message along with the logo. The API will provide a localization of the action word with 14 | message based on the user’s language settings. Do 15 | not create your own localized payment button. 16 | 17 | ![Apple pay button - types](https://github.com/appfolio/react-native-payments/assets/37466295/f6c7b0a0-0b4b-43e0-9add-49c9bc5e4a24) 18 | 19 | 20 | ### Button style 21 | 22 | In addition to button's type, you can set button's visual appearance. For iOS and web, button artwork is provided in black, white, and white with an outline rule. 23 | 24 | ![Apple pay button - styles](https://user-images.githubusercontent.com/829963/40891711-daca8ff8-678a-11e8-89f2-26a0c3dcf9ed.png) 25 | 26 | ## Props 27 | | Prop Name | Required | Type | Default Value | 28 | |--------------|----------|-------------|---------------| 29 | | type | yes | ButtonType | | 30 | | style | yes | ButtonStyle | | 31 | | onPress | yes | Function | | 32 | | width | no | number | | 33 | | height | no | number | | 34 | | cornerRadius | no | number | 4 | 35 | | minWidth | no | number | 100 | 36 | | minHeight | no | number | 30 | 37 | | maxWidth | no | number | | 38 | | maxHeight | no | number | | 39 | 40 | ## Types 41 | ```javascript 42 | type ButtonType = 43 | // A button with the Apple Pay logo only. 44 | | 'plain' 45 | // A button with the text “Buy with” and the Apple Pay logo. 46 | | 'buy' 47 | // A button with the text “Set up” and the Apple Pay logo. 48 | | 'setUp' 49 | // A button with the text “Pay with” and the Apple Pay logo. 50 | | 'inStore' 51 | // A button with the text "Donate with" and the Apple Pay logo. 52 | | 'donate' 53 | // A button with the text "Continue with" and the Apple Pay logo. 54 | | 'continue'; 55 | 56 | type ButtonStyle = 57 | // A white button with black lettering (shown here against a gray background to ensure visibility). 58 | | 'white' 59 | // A white button with black lettering and a black outline. 60 | | 'whiteOutline' 61 | // A black button with white lettering. 62 | | 'black'; 63 | ``` -------------------------------------------------------------------------------- /js/PaymentRequest/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // https://www.w3.org/TR/payment-request/#paymentmethoddata-dictionary 4 | export type PaymentMethodData = { 5 | supportedMethods: Array, 6 | data: Object 7 | }; 8 | 9 | // https://www.w3.org/TR/payment-request/#dom-paymentcurrencyamount 10 | export type PaymentCurrencyAmount = { 11 | currency: string, 12 | value: string 13 | }; 14 | 15 | // https://www.w3.org/TR/payment-request/#paymentdetailsbase-dictionary 16 | export type PaymentDetailsBase = { 17 | displayItems: Array, 18 | shippingOptions: Array, 19 | modifiers: Array 20 | }; 21 | 22 | // https://www.w3.org/TR/payment-request/#paymentdetailsinit-dictionary 23 | export type PaymentDetailsInit = { 24 | ...PaymentDetailsBase, 25 | id?: string, 26 | total: PaymentItem 27 | }; 28 | 29 | // https://www.w3.org/TR/payment-request/#paymentdetailsupdate-dictionary 30 | export type PaymentDetailsUpdate = { 31 | ...PaymentDetailsBase, 32 | error: string, 33 | total: PaymentItem 34 | }; 35 | 36 | // https://www.w3.org/TR/payment-request/#paymentdetailsmodifier-dictionary 37 | export type PaymentDetailsModifier = { 38 | supportedMethods: Array, 39 | total: PaymentItem, 40 | additionalDisplayItems: Array, 41 | data: Object 42 | }; 43 | 44 | // https://www.w3.org/TR/payment-request/#paymentshippingtype-enum 45 | export type PaymentShippingType = 'shipping' | 'delivery' | 'pickup'; 46 | 47 | // https://www.w3.org/TR/payment-request/#paymentoptions-dictionary 48 | export type PaymentOptions = { 49 | requestPayerName: boolean, 50 | requestPayerEmail: boolean, 51 | requestPayerPhone: boolean, 52 | requestShipping: boolean, 53 | requestBilling: boolean, 54 | shippingType: PaymentShippingType 55 | }; 56 | 57 | // https://www.w3.org/TR/payment-request/#paymentitem-dictionary 58 | export type PaymentItem = { 59 | label: string, 60 | amount: PaymentCurrencyAmount, 61 | pending: boolean 62 | }; 63 | 64 | // https://www.w3.org/TR/payment-request/#paymentaddress-interface 65 | export type PaymentAddress = { 66 | recipient: null | string, 67 | organization: null | string, 68 | addressLine: null | string, 69 | city: string, 70 | region: string, 71 | country: string, 72 | postalCode: string, 73 | phone: null | string, 74 | languageCode: null | string, 75 | sortingCode: null | string, 76 | dependentLocality: null | string 77 | }; 78 | 79 | // https://www.w3.org/TR/payment-request/#paymentshippingoption-dictionary 80 | export type PaymentShippingOption = { 81 | id: string, 82 | label: string, 83 | amount: PaymentCurrencyAmount, 84 | selected: boolean 85 | }; 86 | 87 | // https://www.w3.org/TR/payment-request/#paymentshippingoption-dictionary 88 | export type PaymentMethod = { 89 | paymentMethodType: string 90 | } 91 | 92 | // https://www.w3.org/TR/payment-request/#paymentcomplete-enum 93 | export type PaymentComplete = 'fail' | 'success' | 'unknown'; 94 | 95 | export type PaymentDetailsIOS = { 96 | paymentData: ?Object, 97 | billingContact?: ?Object, 98 | shippingContact?: ?Object, 99 | paymentToken?: string, 100 | transactionIdentifier: string, 101 | paymentMethod: Object 102 | }; 103 | 104 | export type PaymentDetailsIOSRaw = { 105 | paymentData: string, 106 | billingContact?: string, 107 | shippingContact?: string, 108 | paymentToken?: string, 109 | transactionIdentifier: string, 110 | paymentMethod: Object 111 | }; 112 | -------------------------------------------------------------------------------- /ios/Views/PKPaymentButtonView.m: -------------------------------------------------------------------------------- 1 | // 2 | // ApplePayPaymentButtonManager.m 3 | // ReactNativePaymentsExample 4 | // 5 | // Created by Andrej on 15/05/2018. 6 | // Copyright © 2018 Facebook. All rights reserved. 7 | // 8 | 9 | #import "PKPaymentButtonView.h" 10 | 11 | NSString * const DEFAULT_BUTTON_TYPE = @"plain"; 12 | NSString * const DEFAULT_BUTTON_STYLE = @"black"; 13 | CGFloat const DEFAULT_CORNER_RADIUS = 4.0; 14 | 15 | @implementation PKPaymentButtonView 16 | 17 | @synthesize buttonType = _buttonType; 18 | @synthesize buttonStyle = _buttonStyle; 19 | @synthesize cornerRadius = _cornerRadius; 20 | @synthesize button = _button; 21 | 22 | - (instancetype) init { 23 | self = [super init]; 24 | 25 | [self setButtonType:DEFAULT_BUTTON_TYPE andStyle:DEFAULT_BUTTON_STYLE withRadius:DEFAULT_CORNER_RADIUS]; 26 | 27 | return self; 28 | } 29 | 30 | - (void)setButtonType:(NSString *) value { 31 | if (_buttonType != value) { 32 | [self setButtonType:value andStyle:_buttonStyle withRadius:_cornerRadius]; 33 | } 34 | 35 | _buttonType = value; 36 | } 37 | 38 | - (void)setButtonStyle:(NSString *) value { 39 | if (_buttonStyle != value) { 40 | [self setButtonType:_buttonType andStyle:value withRadius:_cornerRadius]; 41 | } 42 | 43 | _buttonStyle = value; 44 | } 45 | 46 | - (void)setCornerRadius:(CGFloat) value { 47 | if(_cornerRadius != value) { 48 | [self setButtonType:_buttonType andStyle:_buttonStyle withRadius:value]; 49 | } 50 | 51 | _cornerRadius = value; 52 | } 53 | 54 | /** 55 | * PKPayment button cannot be modified. Due to this limitation, we have to 56 | * unmount existing button and create a new one whenever its style and/or 57 | * type is changed. 58 | */ 59 | - (void)setButtonType:(NSString *) buttonType andStyle:(NSString *) buttonStyle withRadius:(CGFloat) cornerRadius { 60 | for (UIView *view in self.subviews) { 61 | [view removeFromSuperview]; 62 | } 63 | 64 | PKPaymentButtonType type; 65 | PKPaymentButtonStyle style; 66 | 67 | if ([buttonType isEqualToString: @"buy"]) { 68 | type = PKPaymentButtonTypeBuy; 69 | } else if ([buttonType isEqualToString: @"setUp"]) { 70 | type = PKPaymentButtonTypeSetUp; 71 | } else if ([buttonType isEqualToString: @"inStore"]) { 72 | type = PKPaymentButtonTypeInStore; 73 | } else if ([buttonType isEqualToString: @"donate"]) { 74 | type = PKPaymentButtonTypeDonate; 75 | } else if ([buttonType isEqualToString: @"continue"]) { 76 | type = PKPaymentButtonTypeContinue; 77 | } else { 78 | type = PKPaymentButtonTypePlain; 79 | } 80 | 81 | if ([buttonStyle isEqualToString: @"white"]) { 82 | style = PKPaymentButtonStyleWhite; 83 | } else if ([buttonStyle isEqualToString: @"whiteOutline"]) { 84 | style = PKPaymentButtonStyleWhiteOutline; 85 | } else { 86 | style = PKPaymentButtonStyleBlack; 87 | } 88 | 89 | _button = [[PKPaymentButton alloc] initWithPaymentButtonType:type paymentButtonStyle:style]; 90 | [_button addTarget:self action:@selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside]; 91 | 92 | _button.layer.cornerRadius = cornerRadius; 93 | _button.layer.masksToBounds = true; 94 | 95 | _button.layer.cornerRadius = cornerRadius; 96 | _button.layer.masksToBounds = true; 97 | 98 | [self addSubview:_button]; 99 | } 100 | 101 | /** 102 | * Respond to touch event 103 | */ 104 | - (void)touchUpInside:(PKPaymentButton *)button { 105 | if (self.onPress) { 106 | self.onPress(nil); 107 | } 108 | } 109 | 110 | /** 111 | * Set button frame to what React sets for parent view. 112 | */ 113 | - (void)layoutSubviews 114 | { 115 | [super layoutSubviews]; 116 | _button.frame = self.bounds; 117 | } 118 | 119 | @end 120 | -------------------------------------------------------------------------------- /docs/PaymentRequest.md: -------------------------------------------------------------------------------- 1 | # PaymentRequest 2 | ### constructor(methodData, details, ?options) 3 | Initializes the payment request. 4 | 5 | __Arguments__ 6 | - methodData - `Array` 7 | - details - `PaymentDetailsInit` 8 | - ?options - `PaymentOptions` 9 | 10 |
11 | Example 12 | 13 | ```es6 14 | const METHOD_DATA = [ 15 | { 16 | supportedMethods: ['apple-pay'], 17 | data: { 18 | merchantIdentifier: 'merchant.com.your-app.namespace', 19 | supportedNetworks: ['visa', 'mastercard', 'amex'], 20 | countryCode: 'US', 21 | currencyCode: 'USD' 22 | } 23 | } 24 | ]; 25 | 26 | const DETAILS = { 27 | id: 'demo', 28 | displayItems: [ 29 | { 30 | label: 'Movie Ticket', 31 | amount: { currency: 'USD', value: '15.00' } 32 | }, 33 | { 34 | label: 'Shipping', 35 | amount: { currency: 'USD', value: '0.00' } 36 | } 37 | ], 38 | total: { 39 | label: 'Merchant Name', 40 | amount: { currency: 'USD', value: '15.00' } 41 | }, 42 | shippingOptions: [ 43 | { 44 | id: 'economy', 45 | label: 'Economy Shipping', 46 | amount: { currency: 'USD', value: '0.00' }, 47 | detail: 'Arrives in 3-5 days', 48 | selected: true 49 | }, 50 | { 51 | id: 'express', 52 | label: 'Express Shipping', 53 | amount: { currency: 'USD', value: '5.00' }, 54 | detail: 'Arrives tomorrow' 55 | } 56 | ] 57 | }; 58 | 59 | const OPTIONS = { 60 | requestPayerName: true, 61 | requestPayerPhone: true, 62 | requestPayerEmail: true, 63 | requestShipping: true 64 | }; 65 | 66 | const paymentRequest = new PaymentRequest(METHOD_DATA, DETAILS, OPTIONS); 67 | ``` 68 | 69 |
70 | 71 | --- 72 | 73 | ### canMakePayments() 74 | Determines if a payment request can be used to accept a payment based on the supported networks provided in the payment method data. 75 | 76 |
77 | Example 78 | 79 | ```es6 80 | paymentRequest.canMakePayments() 81 | .then(canMakePayments => { 82 | if (canMakePayments) { 83 | return paymentRequest.show(); 84 | } 85 | 86 | // Show fallback payment method 87 | }); 88 | ``` 89 | 90 |
91 | 92 | --- 93 | 94 | ### static canMakePaymentsUsingNetworks() 95 | **(IOS only)** Determines if user has active cards in Apple pay that matches passed networks. 96 | 97 | __Arguments__ 98 | - usingNetworks - `Array` 99 | 100 |
101 | Example 102 | 103 | ```es6 104 | PaymentRequest 105 | .canMakePaymentsUsingNetworks(['Visa', 'AmEx', 'MasterCard']) 106 | .then(canMakePayments => { 107 | if (canMakePayments) { 108 | // do some stuff 109 | } 110 | }); 111 | ``` 112 | 113 |
114 | 115 | --- 116 | 117 | ### openPaymentSetup() 118 | **(IOS Only)** Takes the user to the wallet app to add a payment method. 119 | 120 |
121 | 122 | Example 123 | 124 | 125 | ```es6 126 | PaymentRequest.openPaymentSetup(); 127 | ``` 128 | 129 |
130 | 131 | --- 132 | 133 | ### show() 134 | Displays the payment request to the user. 135 | 136 |
137 | Example 138 | 139 | ```es6 140 | paymentRequest 141 | .show() 142 | .then(paymentResponse => chargePaymentResponse(paymentResponse)); 143 | ``` 144 | 145 |
146 | 147 | --- 148 | 149 | ### abort() 150 | Dismisses the payment request. 151 | 152 |
153 | Example 154 | 155 | ```es6 156 | paymentRequest.abort(); 157 | ``` 158 | 159 |
160 | 161 | --- 162 | 163 | ### id 164 | Returns the payment requests's `details.id` 165 | 166 |
167 | Example 168 | 169 | ```es6 170 | console.log(paymentRequest.id); // demo 171 | ``` 172 | 173 |
174 | 175 | --- 176 | 177 | ### shippingAddress 178 | A payment request's `shippingAddress` is populated when the user provides a shipping address. It is `null` by default. 179 | 180 |
181 | Example 182 | 183 | ```es6 184 | console.log(paymentRequest.shippingAddress); // null 185 | ``` 186 | 187 |
188 | 189 | --- 190 | 191 | ### shippingOption 192 | A payment request's `shippingOption` is populated when the user chooses a shipping option. It is `null` by default. 193 | 194 |
195 | Example 196 | 197 | ```es6 198 | console.log(paymentRequest.shippingOption); // economy 199 | ``` 200 | 201 |
202 | 203 | --- 204 | -------------------------------------------------------------------------------- /docs/NativePayments.md: -------------------------------------------------------------------------------- 1 | # NativePayments 2 | ### createPaymentRequest(methodData, details, options) 3 | Sends methodData, details and options over the bridge to initialize Apple Pay/Android Pay. 4 | 5 | __Arguments__ 6 | - methodData - `PaymentMethodData` 7 | - details - `PaymentDetailsInit` 8 | - ?options - `PaymentOptions` 9 | 10 |
11 | Example 12 | 13 | ```es6 14 | const METHOD_DATA = [ 15 | { 16 | supportedMethods: ['apple-pay'], 17 | data: { 18 | merchantIdentifier: 'merchant.com.your-app.namespace', 19 | supportedNetworks: ['visa', 'mastercard', 'amex'], 20 | countryCode: 'US', 21 | currencyCode: 'USD' 22 | } 23 | } 24 | ]; 25 | 26 | const DETAILS = { 27 | id: 'demo', 28 | displayItems: [ 29 | { 30 | label: 'Movie Ticket', 31 | amount: { currency: 'USD', value: '15.00' } 32 | }, 33 | { 34 | label: 'Shipping', 35 | amount: { currency: 'USD', value: '0.00' } 36 | } 37 | ], 38 | total: { 39 | label: 'Merchant Name', 40 | amount: { currency: 'USD', value: '15.00' } 41 | }, 42 | shippingOptions: [ 43 | { 44 | id: 'economy', 45 | label: 'Economy Shipping', 46 | amount: { currency: 'USD', value: '0.00' }, 47 | detail: 'Arrives in 3-5 days', 48 | selected: true 49 | }, 50 | { 51 | id: 'express', 52 | label: 'Express Shipping', 53 | amount: { currency: 'USD', value: '5.00' }, 54 | detail: 'Arrives tomorrow' 55 | } 56 | ] 57 | }; 58 | 59 | const OPTIONS = { 60 | requestPayerName: true, 61 | requestPayerPhone: true, 62 | requestPayerEmail: true, 63 | requestShipping: true 64 | }; 65 | 66 | NativePayments.createPaymentRequest(METHOD_DATA, DETAILS, OPTIONS); 67 | ``` 68 | 69 |
70 | 71 | --- 72 | 73 | ### handleDetailsUpdate(details) 74 | Sends details over the bridge to update Apple Pay/Android Pay. 75 | 76 | __Arguments__ 77 | - details - `PaymentDetailsUpdate` 78 | 79 |
80 | Example 81 | 82 | ```es6 83 | NativePayments.handleDetailsUpdate({ 84 | displayItems: [ 85 | { 86 | label: 'Movie Ticket', 87 | amount: { currency: 'USD', value: '15.00' } 88 | }, 89 | { 90 | label: 'Shipping', 91 | amount: { currency: 'USD', value: '5.00' } 92 | } 93 | ], 94 | total: { 95 | label: 'Merchant Name', 96 | amount: { currency: 'USD', value: '20.00' } 97 | }, 98 | shippingOptions: [ 99 | { 100 | id: 'economy', 101 | label: 'Economy Shipping', 102 | amount: { currency: 'USD', value: '0.00' }, 103 | detail: 'Arrives in 3-5 days' 104 | }, 105 | { 106 | id: 'express', 107 | label: 'Express Shipping', 108 | amount: { currency: 'USD', value: '5.00' }, 109 | detail: 'Arrives tomorrow', 110 | selected 111 | } 112 | ] 113 | }); 114 | ``` 115 | 116 |
117 | 118 | --- 119 | 120 | ### canMakePayments() 121 | Returns if Apple Pay/Android Pay is available given the device and the supportNetworks provided. 122 | 123 | __Arguments__ 124 | 125 |
126 | Example 127 | 128 | ```es6 129 | NativePayments.canMakePayments(); 130 | ``` 131 | 132 |
133 | 134 | --- 135 | 136 | ### canMakePaymentsUsingNetworks() 137 | **(IOS only)** Returns if user has available cards at Apple Pay that matches passed networks. 138 | 139 | __Arguments__ 140 | - usingNetworks - `Array` 141 | 142 | 143 |
144 | 145 | Example 146 | 147 | 148 | ```es6 149 | NativePayments 150 | .canMakePaymentsUsingNetworks(['Visa', 'AmEx', 'MasterCard']) 151 | .then(canMakePayments => { 152 | if (canMakePayments) { 153 | // do some stuff 154 | } 155 | }); 156 | ``` 157 | 158 |
159 | 160 | --- 161 | 162 | ### openPaymentSetup() 163 | **(IOS Only)** Takes the user to the wallet app to add a payment method. 164 | 165 |
166 | 167 | Example 168 | 169 | 170 | ```es6 171 | NativePayments.openPaymentSetup(); 172 | ``` 173 | 174 |
175 | 176 | --- 177 | 178 | ### show() 179 | Displays Apple Pay/Android Pay to the user. 180 | 181 |
182 | Example 183 | 184 | ```es6 185 | NativePayments.show(); 186 | ``` 187 | 188 |
189 | 190 | --- 191 | 192 | ### abort() 193 | Dismisses the Apple Pay/Android Pay sheet. 194 | 195 |
196 | Example 197 | 198 | ```es6 199 | NativePayments.abort(); 200 | ``` 201 | 202 |
203 | 204 | --- 205 | 206 | ### complete(paymentStatus) 207 | Displays a success/failure animation and dismisses Apple Pay/Android Pay based on the payment status provided. 208 | 209 | __Arguments__ 210 | - paymentStatus - `PaymentComplete` 211 | 212 |
213 | Example 214 | 215 | ```es6 216 | NativePayments.complete('success'); 217 | ``` 218 | 219 |
220 | 221 | --- 222 | -------------------------------------------------------------------------------- /js/PaymentRequest/__tests__/PaymentResponse.test.js: -------------------------------------------------------------------------------- 1 | const { mockNativePaymentsSupportedIOS } = require('../__mocks__'); 2 | 3 | jest.mock('react-native', () => mockReactNativeIOS); 4 | jest.mock('../../NativeBridge', () => mockNativePaymentsSupportedIOS); 5 | const PaymentResponse = require('../PaymentResponse').default; 6 | 7 | // helpers 8 | function createCompletedPaymentResponse(paymentResponseData) { 9 | const paymentResponse = new PaymentResponse(paymentResponseData); 10 | paymentResponse._completeCalled = true; 11 | 12 | return paymentResponse; 13 | } 14 | 15 | // constants 16 | const paymentResponseData = { 17 | requestId: 'bar', 18 | methodName: 'apple-pay', 19 | details: {}, 20 | shippingAddress: null, 21 | shippingOption: 'next-day', 22 | payerName: 'Drake', 23 | payerPhone: '555-555-5555', 24 | payerEmail: 'drizzy@octobersveryown.com' 25 | }; 26 | 27 | describe('PaymentResponse', () => { 28 | describe('attributes', () => { 29 | const paymentRequest = new PaymentResponse(paymentResponseData); 30 | 31 | describe('requestId', () => { 32 | it('should return `requestId`', () => { 33 | expect(paymentRequest.requestId).toBe(paymentResponseData.requestId); 34 | }); 35 | 36 | // it('should throw if user attempts to assign value', () => { 37 | // expect(() => { 38 | // paymentRequest.requestId = 'foo'; 39 | // }).toThrow(); 40 | // }); 41 | }); 42 | 43 | describe('methodName', () => { 44 | it('should return `methodName`', () => { 45 | expect(paymentRequest.methodName).toBe(paymentResponseData.methodName); 46 | }); 47 | 48 | // it('should throw if user attempts to assign value', () => { 49 | // expect(() => { 50 | // paymentRequest.methodName = 'foo'; 51 | // }).toThrow(); 52 | // }); 53 | }); 54 | 55 | describe('details', () => { 56 | it('should return `details`', () => { 57 | expect(paymentRequest.details).toEqual(paymentResponseData.details); 58 | }); 59 | 60 | // it('should throw if user attempts to assign value', () => { 61 | // expect(() => { 62 | // paymentRequest.details = 'foo'; 63 | // }).toThrow(); 64 | // }); 65 | }); 66 | 67 | describe('shippingAddress', () => { 68 | it('should return `shippingAddress`', () => { 69 | expect(paymentRequest.shippingAddress).toBe( 70 | paymentResponseData.shippingAddress 71 | ); 72 | }); 73 | 74 | // it('should throw if user attempts to assign value', () => { 75 | // expect(() => { 76 | // paymentRequest.shippingAddress = 'foo'; 77 | // }).toThrow(); 78 | // }); 79 | }); 80 | 81 | describe('shippingOption', () => { 82 | it('should return `shippingOption`', () => { 83 | expect(paymentRequest.shippingOption).toBe( 84 | paymentResponseData.shippingOption 85 | ); 86 | }); 87 | 88 | // it('should throw if user attempts to assign value', () => { 89 | // expect(() => { 90 | // paymentRequest.shippingOption = 'foo'; 91 | // }).toThrow(); 92 | // }); 93 | }); 94 | 95 | describe('payerName', () => { 96 | it('should return `payerName`', () => { 97 | expect(paymentRequest.payerName).toBe(paymentResponseData.payerName); 98 | }); 99 | 100 | // it('should throw if user attempts to assign value', () => { 101 | // expect(() => { 102 | // paymentRequest.payerName = 'foo'; 103 | // }).toThrow(); 104 | // }); 105 | }); 106 | 107 | describe('payerPhone', () => { 108 | it('should return `payerPhone`', () => { 109 | expect(paymentRequest.payerPhone).toBe(paymentResponseData.payerPhone); 110 | }); 111 | 112 | // it('should throw if user attempts to assign value', () => { 113 | // expect(() => { 114 | // paymentRequest.payerPhone = 'foo'; 115 | // }).toThrow(); 116 | // }); 117 | }); 118 | 119 | describe('payerEmail', () => { 120 | it('should return `payerEmail`', () => { 121 | expect(paymentRequest.payerEmail).toBe(paymentResponseData.payerEmail); 122 | }); 123 | 124 | // it('should throw if user attempts to assign value', () => { 125 | // expect(() => { 126 | // paymentRequest.payerEmail = 'foo'; 127 | // }).toThrow(); 128 | // }); 129 | }); 130 | }); 131 | 132 | describe('methods', () => { 133 | describe('complete', () => { 134 | it('should throw if `_completeCalled` is already true', () => { 135 | const completedPaymentResponse = createCompletedPaymentResponse( 136 | paymentResponseData 137 | ); 138 | 139 | expect(() => { 140 | completedPaymentResponse.complete(); 141 | }).toThrow(new Error('InvalidStateError')); 142 | }); 143 | 144 | it('should toggle `_completeCalled` to true', done => { 145 | const paymentResponse = new PaymentResponse(paymentResponseData); 146 | 147 | paymentResponse.complete().then(() => { 148 | expect(paymentResponse._completeCalled).toBe(true); 149 | done(); 150 | }); 151 | }); 152 | 153 | it('should resolve to `undefined`', () => { 154 | const paymentResponse = new PaymentResponse(paymentResponseData); 155 | 156 | expect(paymentResponse.complete()).resolves.toBe(undefined); 157 | }); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /js/PaymentRequest/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | mockReactNativeIOS, 3 | mockNativePaymentsSupportedIOS, 4 | mockNativePaymentsUnsupportedIOS 5 | } = require('../__mocks__'); 6 | 7 | jest.mock('react-native', () => mockReactNativeIOS); 8 | jest.mock('../../NativeBridge', () => mockNativePaymentsSupportedIOS); 9 | const PaymentRequest = require('../').default; 10 | 11 | // helpers 12 | export function createCreatedPaymentRequest(methodData, details, options) { 13 | const paymentRequest = new PaymentRequest(methodData, details, options); 14 | 15 | return paymentRequest; 16 | } 17 | 18 | export function createInteractivePaymentRequest(methodData, details, options) { 19 | const paymentRequest = new PaymentRequest(methodData, details, options); 20 | paymentRequest._state = 'interactive'; 21 | 22 | return paymentRequest; 23 | } 24 | 25 | export function createClosedPaymentRequest(methodData, details, options) { 26 | const paymentRequest = new PaymentRequest(methodData, details, options); 27 | paymentRequest._state = 'closed'; 28 | 29 | return paymentRequest; 30 | } 31 | 32 | export function createUpdatingPaymentRequest(methodData, details, options) { 33 | const paymentRequest = new PaymentRequest(methodData, details, options); 34 | paymentRequest._state = 'interactive'; 35 | paymentRequest._updating = true; 36 | 37 | return paymentRequest; 38 | } 39 | 40 | // constants 41 | const METHOD_DATA = [ 42 | { 43 | supportedMethods: ['apple-pay'], 44 | data: { 45 | merchantId: '12345' 46 | } 47 | } 48 | ]; 49 | const id = 'foo'; 50 | const total = { 51 | label: 'Total', 52 | amount: { currency: 'USD', value: '20.00' } 53 | }; 54 | const displayItems = [ 55 | { 56 | label: 'Subtotal', 57 | amount: { currency: 'USD', value: '20.00' } 58 | } 59 | ]; 60 | const DETAILS = { 61 | id, 62 | total, 63 | displayItems 64 | }; 65 | 66 | describe('PaymentRequest', () => { 67 | describe('constructor', () => {}); 68 | 69 | describe('attributes', () => { 70 | describe('id', () => { 71 | it('should have the same id as `details.id`', () => { 72 | const request = new PaymentRequest(METHOD_DATA, DETAILS); 73 | 74 | expect(request.id).toBe('foo'); 75 | }); 76 | 77 | it('should generate id when `details.id` is not provided', () => { 78 | const request = new PaymentRequest(METHOD_DATA, DETAILS); 79 | 80 | expect(request.id).toBeTruthy(); 81 | }); 82 | }); 83 | 84 | describe('shippingAddress', () => { 85 | it('should have a `null` default shippingAddress', () => { 86 | const request = new PaymentRequest(METHOD_DATA, DETAILS); 87 | 88 | expect(request.shippingAddress).toBe(null); 89 | }); 90 | }); 91 | 92 | describe('shippingOption', () => { 93 | it('should have a `null` default shippingOption', () => { 94 | const request = new PaymentRequest(METHOD_DATA, DETAILS); 95 | 96 | expect(request.shippingOption).toBe(null); 97 | }); 98 | 99 | it('should default to first `shippingOption.id`', () => { 100 | const shippingOptions = [ 101 | { 102 | id: 'next-day', 103 | label: 'Next Day Delivery', 104 | amount: { currency: 'USD', value: '12.00' } 105 | } 106 | ]; 107 | const detailsWithShippingOptions = Object.assign({}, DETAILS, { 108 | shippingOptions 109 | }); 110 | 111 | const request = new PaymentRequest( 112 | METHOD_DATA, 113 | detailsWithShippingOptions 114 | ); 115 | 116 | expect(request.shippingOption).toBe('next-day'); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('methods', () => { 122 | describe('show', () => { 123 | it('should set `_state` to `interactive`', () => {}); 124 | 125 | it('should set `_acceptPromise` to a `Promise`', () => {}); 126 | 127 | it('should return a `PaymentResponse` with a `requestId`, `methodName`, and `details`', () => {}); 128 | }); 129 | 130 | describe('abort', () => { 131 | it('should reject `_state` is not equal to `interactive`', async () => { 132 | const createdPaymentRequest = createCreatedPaymentRequest( 133 | METHOD_DATA, 134 | DETAILS 135 | ); 136 | 137 | let error = null; 138 | 139 | try { 140 | await createdPaymentRequest.abort(); 141 | } catch(e) { 142 | error = e; 143 | } 144 | 145 | expect(error.message).toBe('InvalidStateError'); 146 | }); 147 | 148 | it('should resolve to `undefined`', async () => { 149 | const interactivePaymentRequest = createInteractivePaymentRequest( 150 | METHOD_DATA, 151 | DETAILS 152 | ); 153 | 154 | const result = await interactivePaymentRequest.abort(); 155 | 156 | expect(result).toBe(undefined); 157 | }); 158 | 159 | it('should set `_state` to `closed`', async () => { 160 | const interactivePaymentRequest = createInteractivePaymentRequest( 161 | METHOD_DATA, 162 | DETAILS 163 | ); 164 | 165 | await interactivePaymentRequest.abort(); 166 | 167 | expect(interactivePaymentRequest._state).toBe('closed'); 168 | }); 169 | }); 170 | 171 | describe('canMakePayments', () => { 172 | it('should return true when Payments is available', async () => { 173 | const request = new PaymentRequest(METHOD_DATA, DETAILS); 174 | 175 | const result = await request.canMakePayments(); 176 | 177 | expect(result).toBe(true); 178 | }); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /js/NativeBridge/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { PaymentDetailsBase, PaymentComplete } from './types'; 4 | 5 | import { NativeModules, Platform } from 'react-native'; 6 | const { ReactNativePayments } = NativeModules; 7 | 8 | const IS_ANDROID = Platform.OS === 'android'; 9 | 10 | const NativePayments: { 11 | canMakePayments: boolean, 12 | applePayEnabled: () => boolean, 13 | canMakePaymentsUsingNetworks: boolean, 14 | openPaymentSetup: void, 15 | supportedGateways: Array, 16 | createPaymentRequest: PaymentDetailsBase => Promise, 17 | handleDetailsUpdate: PaymentDetailsBase => Promise, 18 | show: () => Promise, 19 | abort: () => Promise, 20 | complete: PaymentComplete => Promise, 21 | getFullWalletAndroid: string => Promise 22 | } = { 23 | supportedGateways: IS_ANDROID 24 | ? ['stripe'] // On Android, Payment Gateways are supported out of the gate. 25 | : ReactNativePayments ? ReactNativePayments.supportedGateways : [], 26 | 27 | canMakePayments(methodData: object) { 28 | return new Promise((resolve, reject) => { 29 | if (IS_ANDROID) { 30 | ReactNativePayments.canMakePayments( 31 | methodData, 32 | (err) => reject(err), 33 | (canMakePayments) => resolve(true) 34 | ); 35 | 36 | return; 37 | } 38 | 39 | // On iOS, canMakePayments is exposed as a constant. 40 | resolve(ReactNativePayments.canMakePayments); 41 | }); 42 | }, 43 | 44 | applePayEnabled() { 45 | if (Platform.OS === 'ios') { 46 | return ReactNativePayments.canMakePayments; 47 | } 48 | return false; 49 | }, 50 | 51 | // TODO based on Naoufal's talk on YouTube the intention of canMakePayments is for it to work like this, so I'm thinking we can integrate Yegor's code into canMakePayments. 52 | // NF 2020-11-18 53 | canMakePaymentsUsingNetworks(usingNetworks: []) { 54 | // IOS method to check that user has available cards at Apple Pay 55 | // https://developer.apple.com/documentation/passkit/pkpaymentauthorizationviewcontroller/1616187-canmakepaymentsusingnetworks?language=occ 56 | 57 | return new Promise((resolve) => { 58 | if (IS_ANDROID) { 59 | resolve(false); 60 | } 61 | 62 | ReactNativePayments.canMakePaymentsUsingNetworks( 63 | usingNetworks, 64 | (err, data) => resolve(data) 65 | ); 66 | }); 67 | }, 68 | 69 | openPaymentSetup() { 70 | if (IS_ANDROID) { 71 | return; 72 | } 73 | 74 | ReactNativePayments.openPaymentSetup(); 75 | }, 76 | 77 | createPaymentRequest(methodData, details, options = {}) { 78 | return new Promise((resolve, reject) => { 79 | // Android Pay doesn't a PaymentRequest interface on the 80 | // Java side. So we create and show Android Pay when 81 | // the user calls `.show`. 82 | if (IS_ANDROID) { 83 | return resolve(); 84 | } 85 | 86 | ReactNativePayments.createPaymentRequest( 87 | methodData, 88 | details, 89 | options, 90 | err => { 91 | if (err) return reject(err); 92 | 93 | resolve(); 94 | } 95 | ); 96 | }); 97 | }, 98 | 99 | handleDetailsUpdate(details) { 100 | return new Promise((resolve, reject) => { 101 | // Android doesn't have display items, so we noop. 102 | // Users need to create a new Payment Request if they 103 | // need to update pricing. 104 | if (IS_ANDROID) { 105 | resolve(undefined); 106 | 107 | return; 108 | } 109 | 110 | ReactNativePayments.handleDetailsUpdate(details, err => { 111 | if (err) return reject(err); 112 | 113 | resolve(); 114 | }); 115 | }); 116 | }, 117 | 118 | show(methodData, details, options = {}) { 119 | return new Promise((resolve, reject) => { 120 | if (IS_ANDROID) { 121 | ReactNativePayments.show( 122 | methodData, 123 | details, 124 | options, 125 | (err) => reject(err), 126 | (...args) => { console.log(args); resolve(true) } 127 | ); 128 | 129 | return; 130 | } 131 | 132 | ReactNativePayments.show((err, paymentToken) => { 133 | if (err) return reject(err); 134 | 135 | resolve(true); 136 | }); 137 | }); 138 | }, 139 | 140 | abort() { 141 | return new Promise((resolve, reject) => { 142 | if (IS_ANDROID) { 143 | // TODO 144 | resolve(undefined); 145 | 146 | return; 147 | } 148 | 149 | ReactNativePayments.abort(err => { 150 | if (err) return reject(err); 151 | 152 | resolve(true); 153 | }); 154 | }); 155 | }, 156 | 157 | complete(paymentStatus) { 158 | return new Promise((resolve, reject) => { 159 | // Android doesn't have a loading state, so we noop. 160 | if (IS_ANDROID) { 161 | resolve(undefined); 162 | 163 | return; 164 | } 165 | 166 | ReactNativePayments.complete(paymentStatus, err => { 167 | if (err) return reject(err); 168 | 169 | resolve(true); 170 | }); 171 | }); 172 | }, 173 | 174 | getFullWalletAndroid(googleTransactionId: string, paymentMethodData: object, details: object): Promise { 175 | return new Promise((resolve, reject) => { 176 | if (!IS_ANDROID) { 177 | reject(new Error('This method is only available on Android.')); 178 | 179 | return; 180 | } 181 | 182 | ReactNativePayments.getFullWalletAndroid( 183 | googleTransactionId, 184 | paymentMethodData, 185 | details, 186 | (err) => reject(err), 187 | (serializedPaymentToken) => resolve({ 188 | serializedPaymentToken, 189 | paymentToken: JSON.parse(serializedPaymentToken), 190 | /** Leave previous typo in order not to create a breaking change **/ 191 | serializedPaymenToken: serializedPaymentToken, 192 | paymenToken: JSON.parse(serializedPaymentToken) 193 | }) 194 | ); 195 | }); 196 | } 197 | }; 198 | 199 | export default NativePayments; 200 | -------------------------------------------------------------------------------- /js/PaymentRequest/PaymentRequestUpdateEvent.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | 3 | import { 4 | MODULE_SCOPING, 5 | SHIPPING_ADDRESS_CHANGE_EVENT, 6 | SHIPPING_OPTION_CHANGE_EVENT, 7 | PAYMENT_METHOD_CHANGE_EVENT, 8 | INTERNAL_SHIPPING_ADDRESS_CHANGE_EVENT, 9 | INTERNAL_SHIPPING_OPTION_CHANGE_EVENT, 10 | INTERNAL_PAYMENT_METHOD_CHANGE_EVENT, 11 | USER_DISMISS_EVENT, 12 | USER_ACCEPT_EVENT, 13 | SUPPORTED_METHOD_NAME 14 | } from './constants'; 15 | const noop = () => {}; 16 | 17 | import PaymentRequest from '.'; 18 | import NativePayments from '../NativeBridge'; 19 | 20 | // Helpers 21 | import { 22 | validateTotal, 23 | validateDisplayItems, 24 | validateShippingOptions, 25 | convertDetailAmountsToString 26 | } from './helpers'; 27 | 28 | // Errors 29 | import { DOMException } from './errors'; 30 | 31 | export default class PaymentRequestUpdateEvent { 32 | name: string; 33 | target: PaymentRequest; 34 | _waitForUpdate: boolean; 35 | _handleDetailsChange: PaymentDetailsModifier => Promise; 36 | _resetEvent: any; 37 | 38 | constructor(name, target) { 39 | if ( 40 | name !== SHIPPING_ADDRESS_CHANGE_EVENT && 41 | name !== SHIPPING_OPTION_CHANGE_EVENT && 42 | name !== PAYMENT_METHOD_CHANGE_EVENT 43 | ) { 44 | throw new Error( 45 | `Only "${SHIPPING_ADDRESS_CHANGE_EVENT}", "${SHIPPING_OPTION_CHANGE_EVENT} and "${PAYMENT_METHOD_CHANGE_EVENT}"" event listeners are supported.` 46 | ); 47 | } 48 | 49 | this.name = name; 50 | this.target = target; 51 | this._waitForUpdate = false; 52 | 53 | this._handleDetailsChange = this._handleDetailsChange.bind(this); 54 | this._resetEvent = this._resetEvent.bind(this); 55 | } 56 | 57 | _handleDetailsChange(value: PaymentDetailsBase) { 58 | const target = this.target; 59 | 60 | validateTotal(value.total, DOMException); 61 | validateDisplayItems(value.displayItems, DOMException); 62 | validateShippingOptions(value, DOMException); 63 | 64 | // 1. Let details be the result of converting value to a PaymentDetailsUpdate dictionary. If this throws an exception, abort the update with the thrown exception. 65 | const details: PaymentDetailsUpdate = Object.assign(target._details, value); 66 | 67 | // 2. Let serializedModifierData be an empty list. 68 | let serializedModifierData = []; 69 | 70 | // 3. Let selectedShippingOption be null. 71 | let selectedShippingOption = null; 72 | 73 | // 4. Let shippingOptions be an empty sequence. 74 | let shippingOptions = []; 75 | 76 | // 5. Validate and canonicalize the details: 77 | // TODO: implmentation 78 | 79 | // 6. Update the PaymentRequest using the new details: 80 | target._details = details; 81 | 82 | // 6.1 If the total member of details is present, then: 83 | // if (details.total) { 84 | // target._details = Object.assign({}, target._details, { total: details.total }); 85 | // } 86 | 87 | // // 6.2 If the displayItems member of details is present, then: 88 | // if (details.displayItems) { 89 | // target._details = Object.assign({}, target._details, { displayItems: details.displayItems }); 90 | // } 91 | 92 | // // 6.3 If the shippingOptions member of details is present, and target.[[options]].requestShipping is true, then: 93 | // if (details.shippingOptions && target._options.requestShipping === true) { 94 | // // 6.3.1 Set target.[[details]].shippingOptions to shippingOptions. 95 | // shippingOptions = details.shippingOptions; 96 | // target._details = Object.assign({}, target._details, { shippingOptions }); 97 | 98 | // // 6.3.2 Set the value of target's shippingOption attribute to selectedShippingOption. 99 | // selectedShippingOption = target.shippingOption; 100 | // } 101 | 102 | // // 6.4 If the modifiers member of details is present, then: 103 | // if (details.modifiers) { 104 | // // 6.4.1 Set target.[[details]].modifiers to details.modifiers. 105 | // target._details = Object.assign({}, target._details, { modifiers: details.modifiers }); 106 | 107 | // // 6.4.2 Set target.[[serializedModifierData]] to serializedModifierData. 108 | // target._serializedModifierData = serializedModifierData; 109 | // } 110 | 111 | // 6.5 If target.[[options]].requestShipping is true, and target.[[details]].shippingOptions is empty, 112 | // then the developer has signified that there are no valid shipping options for the currently-chosen shipping address (given by target's shippingAddress). 113 | // In this case, the user agent should display an error indicating this, and may indicate that that the currently-chosen shipping address is invalid in some way. 114 | // The user agent should use the error member of details, if it is present, to give more information about why there are no valid shipping options for that address. 115 | 116 | // React Native Payments specific 👇 117 | // --------------------------------- 118 | const normalizedDetails = convertDetailAmountsToString(target._details); 119 | return ( 120 | NativePayments.handleDetailsUpdate(normalizedDetails, DOMException) 121 | // 14. Upon fulfillment of detailsPromise with value value 122 | .then(this._resetEvent()) 123 | // On iOS the `selectedShippingMethod` defaults back to the first option 124 | // when updating shippingMethods. So we call the `_handleShippingOptionChange` 125 | // method with the first shippingOption id so that JS is in sync with Apple Pay. 126 | .then(() => { 127 | if (Platform.OS !== 'ios') { 128 | return; 129 | } 130 | 131 | if ( 132 | target._details.shippingOptions 133 | && target._details.shippingOptions.length > 0 134 | && value.shippingOptions 135 | && ((value.shippingOptions.find(op => op.selected) || {}).id || null) !== target._shippingOption 136 | ) { 137 | target._handleShippingOptionChange({ 138 | selectedShippingOptionId: target._details.shippingOptions[0].id 139 | }); 140 | } 141 | }) 142 | // 13. Upon rejection of detailsPromise: 143 | .catch(e => { 144 | this._resetEvent(); 145 | 146 | throw new Error('AbortError'); 147 | }) 148 | ); 149 | } 150 | 151 | _resetEvent() { 152 | // 1. Set event.[[waitForUpdate]] to false. 153 | this._waitForUpdate = false; 154 | 155 | // 2. Set target.[[updating]] to false. 156 | this.target._updating = false; 157 | 158 | // 3. The user agent should update the user interface based on any changed values in target. 159 | // The user agent should re-enable user interface elements that might have been disabled in the steps above if appropriate. 160 | noop(); 161 | } 162 | 163 | async updateWith( 164 | PaymentDetailsModifierOrPromise: 165 | | PaymentDetailsUpdate 166 | | (( 167 | PaymentDetailsModifier, 168 | PaymentAddress 169 | ) => Promise) 170 | ) { 171 | // 1. Let event be this PaymentRequestUpdateEvent instance. 172 | let event = this; 173 | 174 | // 2. Let target be the value of event's target attribute. 175 | let target = this.target; 176 | 177 | // 3. If target is not a PaymentRequest object, then throw a TypeError. 178 | if (!(target instanceof PaymentRequest)) { 179 | throw new Error('TypeError'); 180 | } 181 | 182 | // 5. If event.[[waitForUpdate]] is true, then throw an "InvalidStateError" DOMException. 183 | if (event._waitForUpdate === true) { 184 | throw new Error('InvalidStateError'); 185 | } 186 | 187 | // 6. If target.[[state]] is not " interactive", then throw an " InvalidStateError" DOMException. 188 | if (target._state !== 'interactive') { 189 | throw new Error('InvalidStateError'); 190 | } 191 | 192 | // 7. If target.[[updating]] is true, then throw an "InvalidStateError" DOMException. 193 | if (target._updating === true) { 194 | throw new Error('InvalidStateError'); 195 | } 196 | 197 | // 8. Set event's stop propagation flag and stop immediate propagation flag. 198 | noop(); 199 | 200 | // 9. Set event.[[waitForUpdate]] to true. 201 | event._waitForUpdate = true; 202 | 203 | // 10. Set target.[[updating]] to true. 204 | target._updating = true; 205 | 206 | // 11. The user agent should disable the user interface that allows the user to accept the payment request. 207 | // This is to ensure that the payment is not accepted until the web page has made changes required by the change. 208 | // The web page must settle the detailsPromise to indicate that the payment request is valid again. 209 | // The user agent should disable any part of the user interface that could cause another update event to be fired. 210 | // Only one update may be processed at a time. 211 | noop(); // NativePayments does this for us (iOS does at the time of this comment) 212 | 213 | // 12. Return from the method and perform the remaining steps in parallel. 214 | 215 | if (typeof PaymentDetailsModifierOrPromise === 'object') { 216 | const paymentDetailsModifier = PaymentDetailsModifierOrPromise; 217 | 218 | return this._handleDetailsChange(paymentDetailsModifier); 219 | } 220 | 221 | if (typeof PaymentDetailsModifierOrPromise === 'function') { 222 | const detailsFromPromise = await PaymentDetailsModifierOrPromise(); 223 | 224 | this._handleDetailsChange(detailsFromPromise); 225 | } 226 | 227 | // 13 & 14 happen in `this._handleDetailsChange`. 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /js/PaymentRequest/__tests__/PaymentRequestUpdateEvent.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | mockReactNativeIOS, 3 | mockNativePaymentsSupportedIOS, 4 | mockNativePaymentsUnsupportedIOS 5 | } = require('../__mocks__'); 6 | 7 | jest.mock('react-native', () => mockReactNativeIOS); 8 | jest.mock('../../NativeBridge', () => mockNativePaymentsSupportedIOS); 9 | const PaymentRequest = require('../').default; 10 | const PaymentRequestUpdateEvent = require('../PaymentRequestUpdateEvent') 11 | .default; 12 | 13 | // helpers 14 | import { 15 | createCreatedPaymentRequest, 16 | createInteractivePaymentRequest, 17 | createClosedPaymentRequest, 18 | createUpdatingPaymentRequest 19 | } from './index.test.js'; 20 | 21 | // constants 22 | const id = 'foo'; 23 | const total = { 24 | label: 'Total', 25 | amount: { currency: 'USD', value: '20.00' } 26 | }; 27 | const displayItems = [ 28 | { 29 | label: 'Subtotal', 30 | amount: { currency: 'USD', value: '20.00' } 31 | } 32 | ]; 33 | const shippingOptions = [ 34 | { 35 | id: 'economy', 36 | label: 'Economy Shipping (5-7 Days)', 37 | amount: { 38 | currency: 'USD', 39 | value: '0.00' 40 | }, 41 | selected: true 42 | } 43 | ]; 44 | const METHOD_DATA = [ 45 | { 46 | supportedMethods: ['apple-pay'], 47 | data: { 48 | merchantId: '12345' 49 | } 50 | } 51 | ]; 52 | const DETAILS = { 53 | id, 54 | total, 55 | displayItems 56 | }; 57 | const OPTIONS = { 58 | requestShipping: true 59 | }; 60 | 61 | ['shippingaddresschange', 'shippingoptionchange', 'paymentmethodchange'].forEach((eventName) => { 62 | describe(`PaymentRequestUpdateEvent with name="${eventName}"`, () => { 63 | const paymentRequest = new PaymentRequest(METHOD_DATA, DETAILS, OPTIONS); 64 | 65 | describe('constructor', () => { 66 | it("should throw if `name` isn't `shippingaddresschange` or `shippingoptionchange` or `paymentmethodchange`", () => { 67 | expect(() => { 68 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 69 | 'foo', 70 | paymentRequest 71 | ); 72 | }).toThrow(); 73 | }); 74 | 75 | it("should not throw if `name` is valid", () => { 76 | expect(() => { 77 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 78 | eventName, 79 | paymentRequest 80 | ); 81 | }).not.toThrow(); 82 | }); 83 | }); 84 | 85 | describe('attributes', () => { 86 | describe('name', () => { 87 | it('should return the event name', () => { 88 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 89 | eventName, 90 | paymentRequest 91 | ); 92 | 93 | expect(paymentRequestUpdateEvent.name).toBe(eventName); 94 | }); 95 | }); 96 | 97 | describe('target', () => { 98 | it('should return the PaymentRequest instance', () => { 99 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 100 | eventName, 101 | paymentRequest 102 | ); 103 | 104 | expect(paymentRequestUpdateEvent.target).toBeInstanceOf(PaymentRequest); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('methods', () => { 110 | describe('updateWith', () => { 111 | const updateDetails = { 112 | displayItems: [ 113 | ...displayItems, 114 | { 115 | label: 'Shipping', 116 | amount: { currency: 'USD', value: '25.00' } 117 | } 118 | ], 119 | total: { 120 | label: 'Total', 121 | amount: { currency: 'USD', value: '25.00' } 122 | }, 123 | shippingOptions: [ 124 | { 125 | id: 'economy', 126 | label: 'Economy Shipping (5-7 Days)', 127 | amount: { 128 | currency: 'USD', 129 | value: '5.00' 130 | }, 131 | selected: true 132 | } 133 | ], 134 | modifiers: 'foo' // Not sure how these are used yet 135 | }; 136 | const updatedDetails = Object.assign({}, DETAILS, updateDetails); 137 | 138 | it('should throw if `target` is not an instance of PaymentRequest', async () => { 139 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 140 | eventName 141 | ); 142 | 143 | let error = null; 144 | 145 | try { 146 | await paymentRequestUpdateEvent.updateWith(updateDetails); 147 | } catch(e) { 148 | error = e; 149 | } 150 | 151 | expect(error.message).toBe('TypeError'); 152 | }); 153 | 154 | it('should throw if `_waitForUpdate` is `true`', async () => { 155 | const interactivePaymentRequest = createInteractivePaymentRequest( 156 | METHOD_DATA, 157 | DETAILS, 158 | OPTIONS 159 | ); 160 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 161 | eventName, 162 | interactivePaymentRequest 163 | ); 164 | 165 | let error = null; 166 | 167 | try { 168 | // While waiting for the first `updateWith` to resolve, 169 | // it's internal `_waitForUpdate` property gets set 170 | // to `true`. 171 | await paymentRequestUpdateEvent.updateWith(); 172 | 173 | // When the second `updateWith` is fired, the class 174 | // throws. 175 | await paymentRequestUpdateEvent.updateWith(updateDetails); 176 | } catch(e) { 177 | error = e; 178 | } 179 | 180 | expect(error.message).toBe('InvalidStateError'); 181 | }); 182 | 183 | it('should throw if `target._state` is not equal to `interactive`', async () => { 184 | let error1 = null; 185 | 186 | try { 187 | const createdPaymentRequest = createCreatedPaymentRequest( 188 | METHOD_DATA, 189 | DETAILS, 190 | OPTIONS 191 | ); 192 | const event1 = new PaymentRequestUpdateEvent( 193 | eventName, 194 | createdPaymentRequest 195 | ); 196 | 197 | await event1.updateWith(updateDetails); 198 | } catch(e1) { 199 | error1 = e1; 200 | } 201 | 202 | expect(error1.message).toBe('InvalidStateError'); 203 | 204 | let error2 = null; 205 | 206 | try { 207 | const closedPaymentRequest = createClosedPaymentRequest( 208 | METHOD_DATA, 209 | DETAILS, 210 | OPTIONS 211 | ); 212 | const event2 = new PaymentRequestUpdateEvent( 213 | eventName, 214 | closedPaymentRequest 215 | ); 216 | 217 | await event2.updateWith(updateDetails); 218 | } catch(e2) { 219 | error2 = e2; 220 | } 221 | 222 | expect(error2.message).toBe('InvalidStateError'); 223 | }); 224 | 225 | it('should throw if `target.updating` is `true`', async () => { 226 | const updatingPaymentRequest = createUpdatingPaymentRequest( 227 | METHOD_DATA, 228 | DETAILS, 229 | OPTIONS 230 | ); 231 | 232 | let error = null; 233 | 234 | try { 235 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 236 | eventName, 237 | updatingPaymentRequest 238 | ); 239 | 240 | await paymentRequestUpdateEvent.updateWith({}); 241 | } catch(e) { 242 | error = e; 243 | } 244 | 245 | expect(error.message).toBe('InvalidStateError'); 246 | }); 247 | 248 | it('should successfully update target details when passed an Object', async () => { 249 | const interactivePaymentRequest = createInteractivePaymentRequest( 250 | METHOD_DATA, 251 | DETAILS, 252 | OPTIONS 253 | ); 254 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 255 | eventName, 256 | interactivePaymentRequest 257 | ); 258 | 259 | await paymentRequestUpdateEvent.updateWith(updateDetails); 260 | 261 | expect(interactivePaymentRequest._details).toEqual(updatedDetails); 262 | }); 263 | 264 | it('should successfully update target details when passed a Promise that returns an Object', async () => { 265 | const interactivePaymentRequest = createInteractivePaymentRequest( 266 | METHOD_DATA, 267 | DETAILS, 268 | OPTIONS 269 | ); 270 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 271 | eventName, 272 | interactivePaymentRequest 273 | ); 274 | 275 | await paymentRequestUpdateEvent.updateWith(() => { 276 | return Promise.resolve(updateDetails); 277 | }); 278 | 279 | expect(interactivePaymentRequest._details).toEqual(updatedDetails); 280 | }); 281 | 282 | it('should return a rejected promise upon rejection of the details Promise', async () => { 283 | const interactivePaymentRequest = createInteractivePaymentRequest( 284 | METHOD_DATA, 285 | DETAILS, 286 | OPTIONS 287 | ); 288 | const paymentRequestUpdateEvent = new PaymentRequestUpdateEvent( 289 | eventName, 290 | interactivePaymentRequest 291 | ); 292 | 293 | let error = null; 294 | 295 | try { 296 | await paymentRequestUpdateEvent.updateWith(() => { 297 | return Promise.reject(new Error('Error fetching shipping prices.')); 298 | }); 299 | } catch(e) { 300 | error = e; 301 | } 302 | 303 | expect(error.message).toBe('Error fetching shipping prices.'); 304 | }); 305 | }); 306 | }); 307 | }); 308 | }); 309 | -------------------------------------------------------------------------------- /js/PaymentRequest/helpers/index.js: -------------------------------------------------------------------------------- 1 | import type { 2 | PaymentDetailsInit, 3 | PaymentItem, 4 | PaymentShippingOption 5 | } from '../types'; 6 | 7 | import { isDecimal, isFloat, isInt, toFloat, toInt } from 'validator'; 8 | import { DOMException, ConstructorError } from '../errors'; 9 | 10 | type AmountValue = string | number; 11 | 12 | function isNumber(value) { 13 | return typeof value === 'number'; 14 | } 15 | 16 | function isString(value) { 17 | return typeof value === 'string'; 18 | } 19 | 20 | export function isValidDecimalMonetaryValue( 21 | amountValue: AmountValue | any 22 | ): boolean { 23 | if (!isNumber(amountValue) && !isString(amountValue)) { 24 | return false; 25 | } 26 | 27 | return isNumber(amountValue) || isValidStringAmount(amountValue); 28 | } 29 | 30 | export function isNegative(amountValue: AmountValue): boolean { 31 | return isNumber(amountValue) ? amountValue < 0 : amountValue.startsWith('-'); 32 | } 33 | 34 | export function isValidStringAmount(stringAmount): boolean { 35 | if (stringAmount.endsWith('.')) { 36 | return false; 37 | } 38 | 39 | return isDecimal(stringAmount); 40 | } 41 | 42 | export function toNumber(string: string): number { 43 | if (isFloat(string)) { 44 | return toFloat(string); 45 | } 46 | 47 | if (isInt(string)) { 48 | return toInt(string); 49 | } 50 | } 51 | 52 | export function toString(amountValue: AmountValue) { 53 | return isNumber(amountValue) ? amountValue.toString() : amountValue; 54 | } 55 | 56 | export function convertObjectAmountToString( 57 | objectWithAmount: PaymentItem | PaymentShippingOption 58 | ): PaymentItem | PaymentShippingOption { 59 | return Object.assign({}, objectWithAmount, { 60 | amount: Object.assign({}, { 61 | value: toString(objectWithAmount.amount.value), 62 | currency: objectWithAmount.amount.currency 63 | }) 64 | }); 65 | } 66 | 67 | export function convertDetailAmountsToString( 68 | details: PaymentDetailsInit 69 | ): PaymentDetailsInit { 70 | const nextDetails = Object.keys(details).reduce((acc, key) => { 71 | if (key === 'total') { 72 | return Object.assign({}, acc, { 73 | [key]: convertObjectAmountToString(details[key]) 74 | }); 75 | } 76 | 77 | if ( 78 | Array.isArray(details[key]) && 79 | (key === 'displayItems' || key === 'shippingOptions') 80 | ) { 81 | return Object.assign({}, acc, { 82 | [key]: details[key].map(paymentItemOrShippingOption => 83 | convertObjectAmountToString(paymentItemOrShippingOption) 84 | ) 85 | }); 86 | } 87 | 88 | return acc; 89 | }, {}); 90 | 91 | return nextDetails; 92 | } 93 | 94 | export function getPlatformMethodData( 95 | methodData: Array, 96 | platformOS: 'ios' | 'android' 97 | ) { 98 | const platformSupportedMethod = 99 | platformOS === 'ios' ? 'apple-pay' : 'android-pay'; 100 | 101 | const platformMethod = methodData.find(paymentMethodData => 102 | paymentMethodData.supportedMethods.includes(platformSupportedMethod) 103 | ); 104 | 105 | if (!platformMethod) { 106 | throw new DOMException('The payment method is not supported'); 107 | } 108 | 109 | return platformMethod.data; 110 | } 111 | 112 | // Validators 113 | 114 | export function validateTotal(total, errorType = Error): void { 115 | // Should Vailidator take an errorType to prepopulate "Failed to construct 'PaymentRequest'" 116 | 117 | if (total === undefined) { 118 | throw new errorType(`required member total is undefined.`); 119 | } 120 | 121 | const hasTotal = total && total.amount && (total.amount.value || total.amount.value === 0) 122 | // Check that there is a total 123 | if (!hasTotal) { 124 | throw new errorType(`Missing required member(s): amount, label.`); 125 | } 126 | 127 | const totalAmountValue = total.amount.value; 128 | 129 | // Check that total is a valid decimal monetary value. 130 | if (!isValidDecimalMonetaryValue(totalAmountValue)) { 131 | throw new errorType( 132 | `'${totalAmountValue}' is not a valid amount format for total` 133 | ); 134 | } 135 | 136 | // Check that total isn't negative 137 | if (isNegative(totalAmountValue)) { 138 | throw new errorType(`Total amount value should be non-negative`); 139 | } 140 | } 141 | 142 | export function validatePaymentMethods(methodData): Array { 143 | // Check that at least one payment method is passed in 144 | if (methodData.length < 1) { 145 | throw new ConstructorError(`At least one payment method is required`); 146 | } 147 | 148 | let serializedMethodData = []; 149 | // Check that each payment method has at least one payment method identifier 150 | methodData.forEach(paymentMethod => { 151 | if (paymentMethod.supportedMethods === undefined) { 152 | throw new ConstructorError( 153 | `required member supportedMethods is undefined.` 154 | ); 155 | } 156 | 157 | if (!Array.isArray(paymentMethod.supportedMethods)) { 158 | throw new ConstructorError( 159 | `required member supportedMethods is not iterable.` 160 | ); 161 | } 162 | 163 | if (paymentMethod.supportedMethods.length < 1) { 164 | throw new ConstructorError( 165 | `Each payment method needs to include at least one payment method identifier` 166 | ); 167 | } 168 | 169 | const serializedData = paymentMethod.data 170 | ? JSON.stringify(paymentMethod.data) 171 | : null; 172 | 173 | serializedMethodData.push([paymentMethod.supportedMethods, serializedData]); 174 | }); 175 | 176 | return serializedMethodData; 177 | } 178 | 179 | 180 | export function validateDisplayItems(displayItems, errorType = Error): void { 181 | // Check that the value of each display item is a valid decimal monetary value 182 | if (displayItems) { 183 | displayItems.forEach((item: PaymentItem) => { 184 | const amountValue = item && item.amount && item.amount.value; 185 | 186 | if (!amountValue && amountValue !== 0) { 187 | throw new errorType(`required member value is undefined.`); 188 | } 189 | 190 | if (!isValidDecimalMonetaryValue(amountValue)) { 191 | throw new errorType( 192 | `'${amountValue}' is not a valid amount format for display items` 193 | ); 194 | } 195 | }); 196 | } 197 | } 198 | 199 | export function validateShippingOptions(details, errorType = Error): void { 200 | if (details.shippingOptions === undefined) { 201 | return undefined; 202 | } 203 | 204 | let selectedShippingOption = null; 205 | if (!Array.isArray(details.shippingOptions)) { 206 | throw new errorType(`Iterator getter is not callable.`); 207 | } 208 | 209 | if (details.shippingOptions) { 210 | let seenIDs = []; 211 | 212 | details.shippingOptions.forEach((shippingOption: PaymentShippingOption) => { 213 | if (shippingOption.id === undefined) { 214 | throw new errorType(`required member id is undefined.`); 215 | } 216 | 217 | // Reproducing how Chrome handlers `null` 218 | if (shippingOption.id === null) { 219 | shippingOption.id = 'null'; 220 | } 221 | 222 | // 8.2.3.1 If option.amount.value is not a valid decimal monetary value, then throw a TypeError, optionally informing the developer that the value is invalid. 223 | const amountValue = shippingOption.amount.value; 224 | if (!isValidDecimalMonetaryValue(amountValue)) { 225 | throw new errorType( 226 | `'${amountValue}' is not a valid amount format for shippingOptions` 227 | ); 228 | } 229 | 230 | // 8.2.3.2 If seenIDs contains option.id, then set options to an empty sequence and break. 231 | if (seenIDs.includes(shippingOption.id)) { 232 | details.shippingOptions = []; 233 | console.warn( 234 | `[ReactNativePayments] Duplicate shipping option identifier '${shippingOption.id}' is treated as an invalid address indicator.` 235 | ); 236 | 237 | return undefined; 238 | } 239 | 240 | // 8.2.3.3 Append option.id to seenIDs. 241 | seenIDs.push(shippingOption.id); 242 | }); 243 | } 244 | } 245 | 246 | export function getSelectedShippingOption(shippingOptions) { 247 | // Return null if shippingOptions isn't an Array 248 | if (!Array.isArray(shippingOptions)) { 249 | return null; 250 | } 251 | 252 | // Return null if shippingOptions is empty 253 | if (shippingOptions.length === 0) { 254 | return null; 255 | } 256 | 257 | const selectedShippingOption = shippingOptions.find( 258 | shippingOption => shippingOption.selected 259 | ); 260 | 261 | // Return selectedShippingOption id 262 | if (selectedShippingOption) { 263 | return selectedShippingOption.id; 264 | } 265 | 266 | // Return first shippingOption if no shippingOption was marked as selected 267 | return shippingOptions[0].id; 268 | } 269 | 270 | // Gateway helpers 271 | export function hasGatewayConfig(platformMethodData = {}) { 272 | if (!platformMethodData) { 273 | return false; 274 | } 275 | 276 | if (!platformMethodData.paymentMethodTokenizationParameters) { 277 | return false; 278 | } 279 | 280 | if (!platformMethodData.paymentMethodTokenizationParameters.parameters) { 281 | return false; 282 | } 283 | 284 | if ( 285 | typeof platformMethodData.paymentMethodTokenizationParameters.parameters !== 286 | 'object' 287 | ) { 288 | return false; 289 | } 290 | 291 | if ( 292 | !platformMethodData.paymentMethodTokenizationParameters.parameters.gateway 293 | ) { 294 | return false; 295 | } 296 | 297 | if ( 298 | typeof platformMethodData.paymentMethodTokenizationParameters.parameters 299 | .gateway !== 'string' 300 | ) { 301 | return false; 302 | } 303 | 304 | return true; 305 | } 306 | 307 | export function getGatewayName(platformMethodData) { 308 | return platformMethodData.paymentMethodTokenizationParameters.parameters 309 | .gateway; 310 | } 311 | 312 | export function validateGateway(selectedGateway = '', supportedGateways = []) { 313 | if (!supportedGateways.includes(selectedGateway)) { 314 | throw new ConstructorError( 315 | `"${selectedGateway}" is not a supported gateway. Visit https://goo.gl/fsxSFi for more info.` 316 | ); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /ios/ReactNativePayments.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 770B1FE620C48E72009115EF /* PKPaymentButtonManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 770B1FE220C48E71009115EF /* PKPaymentButtonManager.m */; }; 11 | 770B1FE720C48E72009115EF /* PKPaymentButtonView.m in Sources */ = {isa = PBXBuildFile; fileRef = 770B1FE420C48E72009115EF /* PKPaymentButtonView.m */; }; 12 | ADB95AAF1F199853007C2365 /* GatewayManager.m in Sources */ = {isa = PBXBuildFile; fileRef = ADB95AAE1F199853007C2365 /* GatewayManager.m */; }; 13 | B3E7B58A1CC2AC0600A0062D /* ReactNativePayments.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* ReactNativePayments.m */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXCopyFilesBuildPhase section */ 17 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 18 | isa = PBXCopyFilesBuildPhase; 19 | buildActionMask = 2147483647; 20 | dstPath = "include/$(PRODUCT_NAME)"; 21 | dstSubfolderSpec = 16; 22 | files = ( 23 | ); 24 | runOnlyForDeploymentPostprocessing = 0; 25 | }; 26 | /* End PBXCopyFilesBuildPhase section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 134814201AA4EA6300B7C361 /* libReactNativePayments.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactNativePayments.a; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 770B1FE220C48E71009115EF /* PKPaymentButtonManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PKPaymentButtonManager.m; sourceTree = ""; }; 31 | 770B1FE320C48E71009115EF /* PKPaymentButtonView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PKPaymentButtonView.h; sourceTree = ""; }; 32 | 770B1FE420C48E72009115EF /* PKPaymentButtonView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PKPaymentButtonView.m; sourceTree = ""; }; 33 | 770B1FE520C48E72009115EF /* PKPaymentButtonManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PKPaymentButtonManager.h; sourceTree = ""; }; 34 | ADB95AAD1F199853007C2365 /* GatewayManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GatewayManager.h; sourceTree = ""; }; 35 | ADB95AAE1F199853007C2365 /* GatewayManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GatewayManager.m; sourceTree = ""; }; 36 | B3E7B5881CC2AC0600A0062D /* ReactNativePayments.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReactNativePayments.h; sourceTree = ""; }; 37 | B3E7B5891CC2AC0600A0062D /* ReactNativePayments.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReactNativePayments.m; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 134814211AA4EA7D00B7C361 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | 134814201AA4EA6300B7C361 /* libReactNativePayments.a */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | 58B511D21A9E6C8500147676 = { 60 | isa = PBXGroup; 61 | children = ( 62 | 770B1FE820C4A644009115EF /* Views */, 63 | ADB95AAD1F199853007C2365 /* GatewayManager.h */, 64 | ADB95AAE1F199853007C2365 /* GatewayManager.m */, 65 | B3E7B5881CC2AC0600A0062D /* ReactNativePayments.h */, 66 | B3E7B5891CC2AC0600A0062D /* ReactNativePayments.m */, 67 | 134814211AA4EA7D00B7C361 /* Products */, 68 | ); 69 | sourceTree = ""; 70 | }; 71 | 770B1FE820C4A644009115EF /* Views */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 770B1FE520C48E72009115EF /* PKPaymentButtonManager.h */, 75 | 770B1FE220C48E71009115EF /* PKPaymentButtonManager.m */, 76 | 770B1FE320C48E71009115EF /* PKPaymentButtonView.h */, 77 | 770B1FE420C48E72009115EF /* PKPaymentButtonView.m */, 78 | ); 79 | path = Views; 80 | sourceTree = ""; 81 | }; 82 | /* End PBXGroup section */ 83 | 84 | /* Begin PBXNativeTarget section */ 85 | 58B511DA1A9E6C8500147676 /* ReactNativePayments */ = { 86 | isa = PBXNativeTarget; 87 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativePayments" */; 88 | buildPhases = ( 89 | 58B511D71A9E6C8500147676 /* Sources */, 90 | 58B511D81A9E6C8500147676 /* Frameworks */, 91 | 58B511D91A9E6C8500147676 /* CopyFiles */, 92 | ); 93 | buildRules = ( 94 | ); 95 | dependencies = ( 96 | ); 97 | name = ReactNativePayments; 98 | productName = RCTDataManager; 99 | productReference = 134814201AA4EA6300B7C361 /* libReactNativePayments.a */; 100 | productType = "com.apple.product-type.library.static"; 101 | }; 102 | /* End PBXNativeTarget section */ 103 | 104 | /* Begin PBXProject section */ 105 | 58B511D31A9E6C8500147676 /* Project object */ = { 106 | isa = PBXProject; 107 | attributes = { 108 | LastUpgradeCheck = 0610; 109 | ORGANIZATIONNAME = Facebook; 110 | TargetAttributes = { 111 | 58B511DA1A9E6C8500147676 = { 112 | CreatedOnToolsVersion = 6.1.1; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativePayments" */; 117 | compatibilityVersion = "Xcode 3.2"; 118 | developmentRegion = English; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | ); 123 | mainGroup = 58B511D21A9E6C8500147676; 124 | productRefGroup = 58B511D21A9E6C8500147676; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | 58B511DA1A9E6C8500147676 /* ReactNativePayments */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXSourcesBuildPhase section */ 134 | 58B511D71A9E6C8500147676 /* Sources */ = { 135 | isa = PBXSourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | 770B1FE720C48E72009115EF /* PKPaymentButtonView.m in Sources */, 139 | 770B1FE620C48E72009115EF /* PKPaymentButtonManager.m in Sources */, 140 | ADB95AAF1F199853007C2365 /* GatewayManager.m in Sources */, 141 | B3E7B58A1CC2AC0600A0062D /* ReactNativePayments.m in Sources */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | /* End PBXSourcesBuildPhase section */ 146 | 147 | /* Begin XCBuildConfiguration section */ 148 | 58B511ED1A9E6C8500147676 /* Debug */ = { 149 | isa = XCBuildConfiguration; 150 | buildSettings = { 151 | ALWAYS_SEARCH_USER_PATHS = NO; 152 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 153 | CLANG_CXX_LIBRARY = "libc++"; 154 | CLANG_ENABLE_MODULES = YES; 155 | CLANG_ENABLE_OBJC_ARC = YES; 156 | CLANG_WARN_BOOL_CONVERSION = YES; 157 | CLANG_WARN_CONSTANT_CONVERSION = YES; 158 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 159 | CLANG_WARN_EMPTY_BODY = YES; 160 | CLANG_WARN_ENUM_CONVERSION = YES; 161 | CLANG_WARN_INT_CONVERSION = YES; 162 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 163 | CLANG_WARN_UNREACHABLE_CODE = YES; 164 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 165 | COPY_PHASE_STRIP = NO; 166 | ENABLE_STRICT_OBJC_MSGSEND = YES; 167 | GCC_C_LANGUAGE_STANDARD = gnu99; 168 | GCC_DYNAMIC_NO_PIC = NO; 169 | GCC_OPTIMIZATION_LEVEL = 0; 170 | GCC_PREPROCESSOR_DEFINITIONS = ( 171 | "DEBUG=1", 172 | "$(inherited)", 173 | ); 174 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 175 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 176 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 177 | GCC_WARN_UNDECLARED_SELECTOR = YES; 178 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 179 | GCC_WARN_UNUSED_FUNCTION = YES; 180 | GCC_WARN_UNUSED_VARIABLE = YES; 181 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 182 | MTL_ENABLE_DEBUG_INFO = YES; 183 | ONLY_ACTIVE_ARCH = YES; 184 | SDKROOT = iphoneos; 185 | }; 186 | name = Debug; 187 | }; 188 | 58B511EE1A9E6C8500147676 /* Release */ = { 189 | isa = XCBuildConfiguration; 190 | buildSettings = { 191 | ALWAYS_SEARCH_USER_PATHS = NO; 192 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 193 | CLANG_CXX_LIBRARY = "libc++"; 194 | CLANG_ENABLE_MODULES = YES; 195 | CLANG_ENABLE_OBJC_ARC = YES; 196 | CLANG_WARN_BOOL_CONVERSION = YES; 197 | CLANG_WARN_CONSTANT_CONVERSION = YES; 198 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 199 | CLANG_WARN_EMPTY_BODY = YES; 200 | CLANG_WARN_ENUM_CONVERSION = YES; 201 | CLANG_WARN_INT_CONVERSION = YES; 202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 203 | CLANG_WARN_UNREACHABLE_CODE = YES; 204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 205 | COPY_PHASE_STRIP = YES; 206 | ENABLE_NS_ASSERTIONS = NO; 207 | ENABLE_STRICT_OBJC_MSGSEND = YES; 208 | GCC_C_LANGUAGE_STANDARD = gnu99; 209 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 210 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 211 | GCC_WARN_UNDECLARED_SELECTOR = YES; 212 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 213 | GCC_WARN_UNUSED_FUNCTION = YES; 214 | GCC_WARN_UNUSED_VARIABLE = YES; 215 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 216 | MTL_ENABLE_DEBUG_INFO = NO; 217 | SDKROOT = iphoneos; 218 | VALIDATE_PRODUCT = YES; 219 | }; 220 | name = Release; 221 | }; 222 | 58B511F01A9E6C8500147676 /* Debug */ = { 223 | isa = XCBuildConfiguration; 224 | buildSettings = { 225 | HEADER_SEARCH_PATHS = ( 226 | "$(inherited)", 227 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 228 | "$(SRCROOT)/../../../React/**", 229 | "$(SRCROOT)/../../react-native/React/**", 230 | "$(SRCROOT)/../../../../ios/**" 231 | ); 232 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 233 | OTHER_LDFLAGS = "-ObjC"; 234 | PRODUCT_NAME = ReactNativePayments; 235 | SKIP_INSTALL = YES; 236 | }; 237 | name = Debug; 238 | }; 239 | 58B511F11A9E6C8500147676 /* Release */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | HEADER_SEARCH_PATHS = ( 243 | "$(inherited)", 244 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 245 | "$(SRCROOT)/../../../React/**", 246 | "$(SRCROOT)/../../react-native/React/**", 247 | "$(SRCROOT)/../../../../ios/**" 248 | ); 249 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 250 | OTHER_LDFLAGS = "-ObjC"; 251 | PRODUCT_NAME = ReactNativePayments; 252 | SKIP_INSTALL = YES; 253 | }; 254 | name = Release; 255 | }; 256 | /* End XCBuildConfiguration section */ 257 | 258 | /* Begin XCConfigurationList section */ 259 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativePayments" */ = { 260 | isa = XCConfigurationList; 261 | buildConfigurations = ( 262 | 58B511ED1A9E6C8500147676 /* Debug */, 263 | 58B511EE1A9E6C8500147676 /* Release */, 264 | ); 265 | defaultConfigurationIsVisible = 0; 266 | defaultConfigurationName = Release; 267 | }; 268 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativePayments" */ = { 269 | isa = XCConfigurationList; 270 | buildConfigurations = ( 271 | 58B511F01A9E6C8500147676 /* Debug */, 272 | 58B511F11A9E6C8500147676 /* Release */, 273 | ); 274 | defaultConfigurationIsVisible = 0; 275 | defaultConfigurationName = Release; 276 | }; 277 | /* End XCConfigurationList section */ 278 | }; 279 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 280 | } 281 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativepayments/ReactNativePaymentsModule.java: -------------------------------------------------------------------------------- 1 | package com.reactnativepayments; 2 | 3 | import android.view.WindowManager; 4 | 5 | import android.app.Activity; 6 | import android.content.Intent; 7 | import android.os.Bundle; 8 | import androidx.annotation.Nullable; 9 | import androidx.annotation.NonNull; 10 | import android.app.Fragment; 11 | import android.app.FragmentManager; 12 | import android.util.Log; 13 | 14 | import com.facebook.react.bridge.Callback; 15 | import com.facebook.react.bridge.ReactBridge; 16 | import com.facebook.react.bridge.ReadableArray; 17 | import com.facebook.react.bridge.ReadableMapKeySetIterator; 18 | import com.google.android.gms.common.api.GoogleApiClient; 19 | import com.google.android.gms.common.api.BooleanResult; 20 | import com.google.android.gms.common.api.ResultCallback; 21 | import com.google.android.gms.common.ConnectionResult; 22 | import com.google.android.gms.identity.intents.model.UserAddress; 23 | import com.google.android.gms.wallet.*; 24 | 25 | import com.facebook.react.bridge.ActivityEventListener; 26 | import com.facebook.react.bridge.BaseActivityEventListener; 27 | import com.facebook.react.bridge.ReactApplicationContext; 28 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 29 | import com.facebook.react.bridge.ReactMethod; 30 | import com.facebook.react.bridge.ReadableMap; 31 | import com.facebook.react.bridge.WritableNativeArray; 32 | import com.facebook.react.bridge.WritableNativeMap; 33 | import com.facebook.react.modules.core.DeviceEventManagerModule; 34 | 35 | import java.util.ArrayList; 36 | import java.util.HashMap; 37 | import java.util.List; 38 | import java.util.Map; 39 | 40 | public class ReactNativePaymentsModule extends ReactContextBaseJavaModule implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { 41 | private static final int LOAD_MASKED_WALLET_REQUEST_CODE = 88; 42 | private static final int LOAD_FULL_WALLET_REQUEST_CODE = 89; 43 | 44 | 45 | // Google API Client 46 | private GoogleApiClient mGoogleApiClient = null; 47 | 48 | // Callbacks 49 | private static Callback mShowSuccessCallback = null; 50 | private static Callback mShowErrorCallback = null; 51 | private static Callback mGetFullWalletSuccessCallback= null; 52 | private static Callback mGetFullWalletErrorCallback = null; 53 | 54 | public static final String REACT_CLASS = "ReactNativePayments"; 55 | 56 | private static ReactApplicationContext reactContext = null; 57 | 58 | private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() { 59 | @Override 60 | public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { 61 | // retrieve the error code, if available 62 | int errorCode = -1; 63 | if (data != null) { 64 | errorCode = data.getIntExtra(WalletConstants.EXTRA_ERROR_CODE, -1); 65 | } 66 | switch (requestCode) { 67 | case LOAD_MASKED_WALLET_REQUEST_CODE: 68 | switch (resultCode) { 69 | case Activity.RESULT_OK: 70 | if (data != null) { 71 | MaskedWallet maskedWallet = 72 | data.getParcelableExtra(WalletConstants.EXTRA_MASKED_WALLET); 73 | 74 | Log.i(REACT_CLASS, "ANDROID PAY SUCCESS" + maskedWallet.getEmail()); 75 | Log.i(REACT_CLASS, "ANDROID PAY SUCCESS" + buildAddressFromUserAddress(maskedWallet.getBuyerBillingAddress())); 76 | 77 | UserAddress userAddress = maskedWallet.getBuyerShippingAddress(); 78 | WritableNativeMap shippingAddress = userAddress != null 79 | ? buildAddressFromUserAddress(userAddress) 80 | : null; 81 | 82 | 83 | // TODO: Move into function 84 | WritableNativeMap paymentDetails = new WritableNativeMap(); 85 | paymentDetails.putString("paymentDescription", maskedWallet.getPaymentDescriptions()[0]); 86 | paymentDetails.putString("payerEmail", maskedWallet.getEmail()); 87 | paymentDetails.putMap("shippingAddress", shippingAddress); 88 | paymentDetails.putString("googleTransactionId", maskedWallet.getGoogleTransactionId()); 89 | 90 | sendEvent(reactContext, "NativePayments:onuseraccept", paymentDetails); 91 | } 92 | break; 93 | case Activity.RESULT_CANCELED: 94 | sendEvent(reactContext, "NativePayments:onuserdismiss", null); 95 | 96 | break; 97 | default: 98 | Log.i(REACT_CLASS, "ANDROID PAY ERROR? " + errorCode); 99 | mShowErrorCallback.invoke(errorCode); 100 | 101 | break; 102 | } 103 | break; 104 | case LOAD_FULL_WALLET_REQUEST_CODE: 105 | if (resultCode == Activity.RESULT_OK && data != null) { 106 | FullWallet fullWallet = data.getParcelableExtra(WalletConstants.EXTRA_FULL_WALLET); 107 | String tokenJSON = fullWallet.getPaymentMethodToken().getToken(); 108 | Log.i(REACT_CLASS, "FULL WALLET SUCCESS" + tokenJSON); 109 | 110 | mGetFullWalletSuccessCallback.invoke(tokenJSON); 111 | } else { 112 | Log.i(REACT_CLASS, "FULL WALLET FAILURE"); 113 | mGetFullWalletErrorCallback.invoke(); 114 | } 115 | case WalletConstants.RESULT_ERROR:activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); 116 | // handleError(errorCode); 117 | break; 118 | 119 | default: 120 | super.onActivityResult(requestCode, resultCode, data); 121 | break; 122 | } 123 | } 124 | }; 125 | 126 | public ReactNativePaymentsModule(ReactApplicationContext context) { 127 | // Pass in the context to the constructor and save it so you can emit events 128 | // https://facebook.github.io/react-native/docs/native-modules-android.html#the-toast-module 129 | super(context); 130 | 131 | reactContext = context; 132 | 133 | reactContext.addActivityEventListener(mActivityEventListener); 134 | } 135 | 136 | @Override 137 | public String getName() { 138 | // Tell React the name of the module 139 | // https://facebook.github.io/react-native/docs/native-modules-android.html#the-toast-module 140 | return REACT_CLASS; 141 | } 142 | 143 | // Public Methods 144 | // --------------------------------------------------------------------------------------------- 145 | @ReactMethod 146 | public void getSupportedGateways(Callback errorCallback, Callback successCallback) { 147 | WritableNativeArray supportedGateways = new WritableNativeArray(); 148 | 149 | successCallback.invoke(supportedGateways); 150 | } 151 | 152 | @ReactMethod 153 | public void canMakePayments(ReadableMap paymentMethodData, Callback errorCallback, Callback successCallback) { 154 | final Callback callback = successCallback; 155 | IsReadyToPayRequest req = IsReadyToPayRequest.newBuilder() 156 | .addAllowedCardNetwork(WalletConstants.CardNetwork.MASTERCARD) 157 | .addAllowedCardNetwork(WalletConstants.CardNetwork.VISA) 158 | .build(); 159 | 160 | int environment = getEnvironmentFromPaymentMethodData(paymentMethodData); 161 | if (mGoogleApiClient == null) { 162 | buildGoogleApiClient(getCurrentActivity(), environment); 163 | } 164 | 165 | Wallet.Payments.isReadyToPay(mGoogleApiClient, req) 166 | .setResultCallback(new ResultCallback() { 167 | @Override 168 | public void onResult(@NonNull BooleanResult booleanResult) { 169 | callback.invoke(booleanResult.getValue()); 170 | } 171 | }); 172 | } 173 | 174 | @ReactMethod 175 | public void abort(Callback errorCallback, Callback successCallback) { 176 | Log.i(REACT_CLASS, "ANDROID PAY ABORT" + getCurrentActivity().toString()); 177 | successCallback.invoke(); 178 | } 179 | 180 | @ReactMethod 181 | public void show( 182 | ReadableMap paymentMethodData, 183 | ReadableMap details, 184 | ReadableMap options, 185 | Callback errorCallback, 186 | Callback successCallback 187 | ) { 188 | mShowSuccessCallback = successCallback; 189 | mShowErrorCallback = errorCallback; 190 | 191 | Log.i(REACT_CLASS, "ANDROID PAY SHOW" + options); 192 | 193 | Boolean shouldRequestShipping = options.hasKey("requestShipping") && options.getBoolean("requestShipping") 194 | || options.hasKey("requestPayerName") && options.getBoolean("requestPayerName") 195 | || options.hasKey("requestPayerPhone") && options.getBoolean("requestPayerPhone"); 196 | Boolean shouldRequestPayerPhone = options.hasKey("requestPayerPhone") && options.getBoolean("requestPayerPhone"); 197 | 198 | final PaymentMethodTokenizationParameters parameters = buildTokenizationParametersFromPaymentMethodData(paymentMethodData); 199 | 200 | // TODO: clean up MaskedWalletRequest 201 | ReadableMap total = details.getMap("total").getMap("amount"); 202 | final MaskedWalletRequest maskedWalletRequest = MaskedWalletRequest.newBuilder() 203 | .setPaymentMethodTokenizationParameters(parameters) 204 | .setPhoneNumberRequired(shouldRequestPayerPhone) 205 | .setShippingAddressRequired(shouldRequestShipping) 206 | .setEstimatedTotalPrice(total.getString("value")) 207 | .setCurrencyCode(total.getString("currency")) 208 | .build(); 209 | 210 | int environment = getEnvironmentFromPaymentMethodData(paymentMethodData); 211 | if (mGoogleApiClient == null) { 212 | buildGoogleApiClient(getCurrentActivity(), environment); 213 | } 214 | 215 | Wallet.Payments.loadMaskedWallet(mGoogleApiClient, maskedWalletRequest, LOAD_MASKED_WALLET_REQUEST_CODE); 216 | } 217 | 218 | @ReactMethod 219 | public void getFullWalletAndroid( 220 | String googleTransactionId, 221 | ReadableMap paymentMethodData, 222 | ReadableMap details, 223 | Callback errorCallback, 224 | Callback successCallback 225 | ) { 226 | mGetFullWalletSuccessCallback = successCallback; 227 | mGetFullWalletErrorCallback = errorCallback; 228 | 229 | ReadableMap total = details.getMap("total").getMap("amount"); 230 | Log.i(REACT_CLASS, "ANDROID PAY getFullWalletAndroid" + details.getMap("total").getMap("amount")); 231 | 232 | FullWalletRequest fullWalletRequest = FullWalletRequest.newBuilder() 233 | .setGoogleTransactionId(googleTransactionId) 234 | .setCart(Cart.newBuilder() 235 | .setCurrencyCode(total.getString("currency")) 236 | .setTotalPrice(total.getString("value")) 237 | .setLineItems(buildLineItems(details.getArray("displayItems"))) 238 | .build()) 239 | .build(); 240 | 241 | int environment = getEnvironmentFromPaymentMethodData(paymentMethodData); 242 | if (mGoogleApiClient == null) { 243 | buildGoogleApiClient(getCurrentActivity(), environment); 244 | } 245 | 246 | Wallet.Payments.loadFullWallet(mGoogleApiClient, fullWalletRequest, LOAD_FULL_WALLET_REQUEST_CODE); 247 | } 248 | 249 | // Private Method 250 | // --------------------------------------------------------------------------------------------- 251 | private static PaymentMethodTokenizationParameters buildTokenizationParametersFromPaymentMethodData(ReadableMap paymentMethodData) { 252 | ReadableMap tokenizationParameters = paymentMethodData.getMap("paymentMethodTokenizationParameters"); 253 | String tokenizationType = tokenizationParameters.getString("tokenizationType"); 254 | 255 | 256 | if (tokenizationType.equals("GATEWAY_TOKEN")) { 257 | ReadableMap parameters = tokenizationParameters.getMap("parameters"); 258 | PaymentMethodTokenizationParameters.Builder parametersBuilder = PaymentMethodTokenizationParameters.newBuilder() 259 | .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.PAYMENT_GATEWAY) 260 | .addParameter("gateway", parameters.getString("gateway")); 261 | 262 | ReadableMapKeySetIterator iterator = parameters.keySetIterator(); 263 | 264 | while (iterator.hasNextKey()) { 265 | String key = iterator.nextKey(); 266 | 267 | parametersBuilder.addParameter(key, parameters.getString(key)); 268 | } 269 | 270 | return parametersBuilder.build(); 271 | 272 | } else { 273 | String publicKey = tokenizationParameters.getMap("parameters").getString("publicKey"); 274 | 275 | return PaymentMethodTokenizationParameters.newBuilder() 276 | .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.NETWORK_TOKEN) 277 | .addParameter("publicKey", publicKey) 278 | .build(); 279 | } 280 | } 281 | 282 | private static List buildLineItems(ReadableArray displayItems) { 283 | List list = new ArrayList(); 284 | 285 | 286 | for (int i = 0; i < (displayItems.size() - 1); i++) { 287 | ReadableMap displayItem = displayItems.getMap(i); 288 | ReadableMap amount = displayItem.getMap("amount"); 289 | 290 | list.add(LineItem.newBuilder() 291 | .setCurrencyCode(amount.getString("currency")) 292 | .setDescription(displayItem.getString("label")) 293 | .setQuantity("1") 294 | .setUnitPrice(amount.getString("value")) 295 | .setTotalPrice(amount.getString("value")) 296 | .build()); 297 | } 298 | 299 | Log.i(REACT_CLASS, "ANDROID PAY getFullWalletAndroid" + list); 300 | 301 | return list; 302 | } 303 | 304 | private static WritableNativeMap buildAddressFromUserAddress(UserAddress userAddress) { 305 | WritableNativeMap address = new WritableNativeMap(); 306 | 307 | address.putString("recipient", userAddress.getName()); 308 | address.putString("organization", userAddress.getCompanyName()); 309 | address.putString("addressLine", userAddress.getAddress1()); 310 | address.putString("city", userAddress.getLocality()); 311 | address.putString("region", userAddress.getAdministrativeArea()); 312 | address.putString("country", userAddress.getCountryCode()); 313 | address.putString("postalCode", userAddress.getPostalCode()); 314 | address.putString("phone", userAddress.getPhoneNumber()); 315 | address.putNull("languageCode"); 316 | address.putString("sortingCode", userAddress.getSortingCode()); 317 | address.putString("dependentLocality", userAddress.getLocality()); 318 | 319 | return address; 320 | } 321 | 322 | private void sendEvent( 323 | ReactApplicationContext reactContext, 324 | String eventName, 325 | @Nullable WritableNativeMap params 326 | ) { 327 | reactContext 328 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) 329 | .emit(eventName, params); 330 | } 331 | 332 | private int getEnvironmentFromPaymentMethodData(ReadableMap paymentMethodData) { 333 | return paymentMethodData.hasKey("environment") && paymentMethodData.getString("environment").equals("TEST") 334 | ? WalletConstants.ENVIRONMENT_TEST 335 | : WalletConstants.ENVIRONMENT_PRODUCTION; 336 | } 337 | 338 | // Google API Client 339 | // --------------------------------------------------------------------------------------------- 340 | private void buildGoogleApiClient(Activity currentActivity, int environment) { 341 | mGoogleApiClient = new GoogleApiClient.Builder(currentActivity) 342 | .addConnectionCallbacks(this) 343 | .addOnConnectionFailedListener(this) 344 | .addApi(Wallet.API, new Wallet.WalletOptions.Builder() 345 | .setEnvironment(environment) 346 | .setTheme(WalletConstants.THEME_LIGHT) 347 | .build()) 348 | .build(); 349 | mGoogleApiClient.connect(); 350 | } 351 | 352 | @Override 353 | public void onConnected(Bundle connectionHint) { 354 | // mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); 355 | } 356 | 357 | 358 | @Override 359 | public void onConnectionFailed(ConnectionResult result) { 360 | // Refer to Google Play documentation for what errors can be logged 361 | Log.i(REACT_CLASS, "Connection failed: ConnectionResult.getErrorCode() = " + result.getErrorCode()); 362 | } 363 | 364 | @Override 365 | public void onConnectionSuspended(int cause) { 366 | // Attempts to reconnect if a disconnect occurs 367 | Log.i(REACT_CLASS, "Connection suspended"); 368 | mGoogleApiClient.connect(); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /js/PaymentRequest/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 'use strict'; 3 | 4 | // Types 5 | import type { 6 | PaymentMethodData, 7 | PaymentDetailsInit, 8 | PaymentDetailsBase, 9 | PaymentDetailsUpdate, 10 | PaymentOptions, 11 | PaymentShippingOption, 12 | PaymentItem, 13 | PaymentAddress, 14 | PaymentShippingType, 15 | PaymentDetailsIOS, 16 | PaymentDetailsIOSRaw, 17 | PaymentMethod 18 | } from '../types'; 19 | import type PaymentResponseType from './PaymentResponse'; 20 | 21 | // Modules 22 | import { DeviceEventEmitter, Platform } from 'react-native'; 23 | import { v1 as uuid } from 'uuid/v1'; 24 | 25 | import NativePayments from '../NativeBridge'; 26 | import PaymentResponse from './PaymentResponse'; 27 | import PaymentRequestUpdateEvent from './PaymentRequestUpdateEvent'; 28 | 29 | // Helpers 30 | import { 31 | isValidDecimalMonetaryValue, 32 | isNegative, 33 | convertDetailAmountsToString, 34 | getPlatformMethodData, 35 | validateTotal, 36 | validatePaymentMethods, 37 | validateDisplayItems, 38 | validateShippingOptions, 39 | getSelectedShippingOption, 40 | hasGatewayConfig, 41 | getGatewayName, 42 | validateGateway 43 | } from './helpers'; 44 | 45 | import { ConstructorError, GatewayError } from './errors'; 46 | 47 | // Constants 48 | import { 49 | MODULE_SCOPING, 50 | SHIPPING_ADDRESS_CHANGE_EVENT, 51 | SHIPPING_OPTION_CHANGE_EVENT, 52 | PAYMENT_METHOD_CHANGE_EVENT, 53 | INTERNAL_SHIPPING_ADDRESS_CHANGE_EVENT, 54 | INTERNAL_SHIPPING_OPTION_CHANGE_EVENT, 55 | INTERNAL_PAYMENT_METHOD_CHANGE_EVENT, 56 | USER_DISMISS_EVENT, 57 | USER_ACCEPT_EVENT, 58 | GATEWAY_ERROR_EVENT, 59 | SUPPORTED_METHOD_NAME 60 | } from './constants'; 61 | 62 | const noop = () => {}; 63 | const IS_ANDROID = Platform.OS === 'android'; 64 | const IS_IOS = Platform.OS === 'ios' 65 | 66 | // function processPaymentDetailsModifiers(details, serializedModifierData) { 67 | // let modifiers = []; 68 | 69 | // if (details.modifiers) { 70 | // modifiers = details.modifiers; 71 | 72 | // modifiers.forEach((modifier) => { 73 | // if (modifier.total && modifier.total.amount && modifier.total.amount.value) { 74 | // // TODO: refactor validateTotal so that we can display proper error messages (should remove construct 'PaymentRequest') 75 | // validateTotal(modifier.total); 76 | // } 77 | 78 | // if (modifier.additionalDisplayItems) { 79 | // modifier.additionalDisplayItems.forEach((displayItem) => { 80 | // let value = displayItem && displayItem.amount.value && displayItem.amount.value; 81 | 82 | // isValidDecimalMonetaryValue(value); 83 | // }); 84 | // } 85 | 86 | // let serializedData = modifier.data 87 | // ? JSON.stringify(modifier.data) 88 | // : null; 89 | 90 | // serializedModifierData.push(serializedData); 91 | 92 | // if (modifier.data) { 93 | // delete modifier.data; 94 | // } 95 | // }); 96 | // } 97 | 98 | // details.modifiers = modifiers; 99 | // } 100 | 101 | export default class PaymentRequest { 102 | _id: string; 103 | _paymentMethod: null | string; 104 | _shippingAddress: null | PaymentAddress; 105 | _shippingOption: null | string; 106 | _shippingType: null | PaymentShippingType; 107 | 108 | // Internal Slots 109 | _serializedMethodData: string; 110 | _serializedModifierData: string; 111 | _details: Object; 112 | _options: Object; 113 | _state: 'created' | 'interactive' | 'closed'; 114 | _updating: boolean; 115 | _acceptPromise: Promise; 116 | _acceptPromiseResolver: (value: any) => void; 117 | _acceptPromiseRejecter: (reason: any) => void; 118 | _shippingAddressChangeSubscription: any; // TODO: - add proper type annotation 119 | _shippingOptionChangeSubscription: any; // TODO: - add proper type annotation 120 | _paymentMethodChangeSubscription: any; // TODO: - add proper type annotation 121 | _userDismissSubscription: any; // TODO: - add proper type annotation 122 | _userAcceptSubscription: any; // TODO: - add proper type annotation 123 | _gatewayErrorSubscription: any; // TODO: - add proper type annotation 124 | _shippingAddressChangesCount: number; 125 | 126 | _shippingAddressChangeFn: PaymentRequestUpdateEvent => void; // function provided by user 127 | _shippingOptionChangeFn: PaymentRequestUpdateEvent => void; // function provided by user 128 | _paymentMethodChangeFn: PaymentRequestUpdateEvent => void; // function provided by user 129 | 130 | constructor( 131 | methodData: Array = [], 132 | details?: PaymentDetailsInit = [], 133 | options?: PaymentOptions = {} 134 | ) { 135 | // 1. If the current settings object's responsible document is not allowed to use the feature indicated by attribute name allowpaymentrequest, then throw a " SecurityError" DOMException. 136 | noop(); 137 | 138 | // 2. Let serializedMethodData be an empty list. 139 | // happens in `processPaymentMethods` 140 | 141 | // 3. Establish the request's id: 142 | if (!details.id) { 143 | details.id = uuid(); 144 | } 145 | 146 | // 4. Process payment methods 147 | const serializedMethodData = validatePaymentMethods(methodData); 148 | 149 | // 5. Process the total 150 | validateTotal(details.total, ConstructorError); 151 | 152 | // 6. If the displayItems member of details is present, then for each item in details.displayItems: 153 | validateDisplayItems(details.displayItems, ConstructorError); 154 | 155 | // 7. Let selectedShippingOption and payment method be null. 156 | let selectedShippingOption = null; 157 | 158 | // 8. Process shipping options 159 | validateShippingOptions(details, ConstructorError); 160 | 161 | if (IS_IOS) { 162 | selectedShippingOption = getSelectedShippingOption(details.shippingOptions); 163 | } 164 | 165 | // 9. Let serializedModifierData be an empty list. 166 | let serializedModifierData = []; 167 | 168 | // 10. Process payment details modifiers: 169 | // TODO 170 | // - Look into how payment details modifiers are used. 171 | // processPaymentDetailsModifiers(details, serializedModifierData) 172 | 173 | // 11. Let request be a new PaymentRequest. 174 | 175 | // 12. Set request.[[options]] to options. 176 | this._options = options; 177 | 178 | // 13. Set request.[[state]] to "created". 179 | this._state = 'created'; 180 | 181 | // 14. Set request.[[updating]] to false. 182 | this._updating = false; 183 | 184 | // 15. Set request.[[details]] to details. 185 | this._details = details; 186 | 187 | // 16. Set request.[[serializedModifierData]] to serializedModifierData. 188 | this._serializedModifierData = serializedModifierData; 189 | 190 | // 17. Set request.[[serializedMethodData]] to serializedMethodData. 191 | this._serializedMethodData = JSON.stringify(methodData); 192 | 193 | // Set attributes (18-20) 194 | this._id = details.id; 195 | this._paymentMethod = null; 196 | 197 | // 18. Set the value of request's shippingOption attribute to selectedShippingOption. 198 | this._shippingOption = selectedShippingOption; 199 | 200 | // 19. Set the value of the shippingAddress attribute on request to null. 201 | this._shippingAddress = null; 202 | // 20. If options.requestShipping is set to true, then set the value of the shippingType attribute on request to options.shippingType. Otherwise, set it to null. 203 | this._shippingType = IS_IOS && options.requestShipping === true 204 | ? options.shippingType 205 | : null; 206 | 207 | // React Native Payments specific 👇 208 | // --------------------------------- 209 | 210 | // Setup event listeners 211 | this._setupEventListeners(); 212 | 213 | // Set the amount of times `_handleShippingAddressChange` has been called. 214 | // This is used on iOS to noop the first call. 215 | this._shippingAddressChangesCount = 0; 216 | 217 | const platformMethodData = getPlatformMethodData(methodData, Platform.OS); 218 | const normalizedDetails = convertDetailAmountsToString(details); 219 | 220 | // Validate gateway config if present 221 | if (hasGatewayConfig(platformMethodData)) { 222 | validateGateway( 223 | getGatewayName(platformMethodData), 224 | NativePayments.supportedGateways 225 | ); 226 | } 227 | 228 | NativePayments.createPaymentRequest( 229 | platformMethodData, 230 | normalizedDetails, 231 | options 232 | ); 233 | } 234 | 235 | // initialize acceptPromiseResolver/Rejecter 236 | // mainly for unit tests to work without going through the complete flow. 237 | _acceptPromiseResolver = () => {} 238 | _acceptPromiseRejecter = () => {} 239 | 240 | _setupEventListeners() { 241 | // Internal Events 242 | this._userDismissSubscription = DeviceEventEmitter.addListener( 243 | USER_DISMISS_EVENT, 244 | this._closePaymentRequest.bind(this) 245 | ); 246 | this._userAcceptSubscription = DeviceEventEmitter.addListener( 247 | USER_ACCEPT_EVENT, 248 | this._handleUserAccept.bind(this) 249 | ); 250 | 251 | if (IS_IOS) { 252 | this._gatewayErrorSubscription = DeviceEventEmitter.addListener( 253 | GATEWAY_ERROR_EVENT, 254 | this._handleGatewayError.bind(this) 255 | ); 256 | 257 | // https://www.w3.org/TR/payment-request/#onshippingoptionchange-attribute 258 | this._shippingOptionChangeSubscription = DeviceEventEmitter.addListener( 259 | INTERNAL_SHIPPING_OPTION_CHANGE_EVENT, 260 | this._handleShippingOptionChange.bind(this) 261 | ); 262 | 263 | // https://www.w3.org/TR/payment-request/#onshippingaddresschange-attribute 264 | this._shippingAddressChangeSubscription = DeviceEventEmitter.addListener( 265 | INTERNAL_SHIPPING_ADDRESS_CHANGE_EVENT, 266 | this._handleShippingAddressChange.bind(this) 267 | ); 268 | 269 | this._paymentMethodChangeSubscription = DeviceEventEmitter.addListener( 270 | INTERNAL_PAYMENT_METHOD_CHANGE_EVENT, 271 | this._handlePaymentMethodChange.bind(this) 272 | ); 273 | } 274 | } 275 | 276 | _handlePaymentMethodChange(paymentMethod: PaymentMethod) { 277 | this._paymentMethod = paymentMethod.paymentMethodType; 278 | 279 | if (IS_IOS && this._paymentMethod) { 280 | this._paymentMethod = this._paymentMethod.replace('PKPaymentMethodType','').toLowerCase(); 281 | } 282 | 283 | const event = new PaymentRequestUpdateEvent(PAYMENT_METHOD_CHANGE_EVENT, this); 284 | 285 | // Eventually calls `PaymentRequestUpdateEvent._handleDetailsUpdate` when 286 | // after a details are returned 287 | this._paymentMethodChangeFn && this._paymentMethodChangeFn(event); 288 | } 289 | 290 | _handleShippingAddressChange(postalAddress: PaymentAddress) { 291 | this._shippingAddress = postalAddress; 292 | 293 | const event = new PaymentRequestUpdateEvent( 294 | SHIPPING_ADDRESS_CHANGE_EVENT, 295 | this 296 | ); 297 | this._shippingAddressChangesCount++; 298 | 299 | // On iOS, this event fires when the PKPaymentRequest is initialized. 300 | // So on iOS, we track the amount of times `_handleShippingAddressChange` gets called 301 | // and noop the first call. 302 | if (IS_IOS && this._shippingAddressChangesCount === 1) { 303 | return event.updateWith(this._details); 304 | } 305 | 306 | // Eventually calls `PaymentRequestUpdateEvent._handleDetailsUpdate` when 307 | // after a details are returned 308 | this._shippingAddressChangeFn && this._shippingAddressChangeFn(event); 309 | } 310 | 311 | _handleShippingOptionChange({ selectedShippingOptionId }: Object) { 312 | // Update the `shippingOption` 313 | this._shippingOption = selectedShippingOptionId; 314 | 315 | const event = new PaymentRequestUpdateEvent( 316 | SHIPPING_OPTION_CHANGE_EVENT, 317 | this 318 | ); 319 | 320 | this._shippingOptionChangeFn && this._shippingOptionChangeFn(event); 321 | } 322 | 323 | _getPlatformDetails(details: *) { 324 | return IS_IOS 325 | ? this._getPlatformDetailsIOS(details) 326 | : this._getPlatformDetailsAndroid(details); 327 | } 328 | 329 | _getPlatformDetailsIOS(details: PaymentDetailsIOSRaw): PaymentDetailsIOS { 330 | const { 331 | paymentData: serializedPaymentData, 332 | billingContact: serializedBillingContact, 333 | shippingContact: serializedShippingContact, 334 | paymentToken, 335 | transactionIdentifier, 336 | paymentMethod 337 | } = details; 338 | 339 | const isSimulator = transactionIdentifier === 'Simulated Identifier'; 340 | 341 | let billingContact = null; 342 | let shippingContact = null; 343 | 344 | if (serializedBillingContact && serializedBillingContact !== ""){ 345 | try{ 346 | billingContact = JSON.parse(serializedBillingContact); 347 | }catch(e){} 348 | } 349 | 350 | if (serializedShippingContact && serializedShippingContact !== ""){ 351 | try{ 352 | shippingContact = JSON.parse(serializedShippingContact); 353 | }catch(e){} 354 | } 355 | 356 | return { 357 | paymentData: isSimulator ? null : JSON.parse(serializedPaymentData), 358 | billingContact, 359 | shippingContact, 360 | paymentToken, 361 | transactionIdentifier, 362 | paymentMethod 363 | }; 364 | } 365 | 366 | _getPlatformDetailsAndroid(details: { 367 | googleTransactionId: string, 368 | payerEmail: string, 369 | paymentDescription: string, 370 | shippingAddress: Object, 371 | }) { 372 | const { 373 | googleTransactionId, 374 | paymentDescription 375 | } = details; 376 | 377 | return { 378 | googleTransactionId, 379 | paymentDescription, 380 | // On Android, the recommended flow is to have user's confirm prior to 381 | // retrieving the full wallet. 382 | getPaymentToken: () => NativePayments.getFullWalletAndroid( 383 | googleTransactionId, 384 | getPlatformMethodData(JSON.parse(this._serializedMethodData, Platform.OS)), 385 | convertDetailAmountsToString(this._details) 386 | ) 387 | }; 388 | } 389 | 390 | _handleUserAccept(details: { 391 | transactionIdentifier: string, 392 | paymentData: string, 393 | shippingAddress: Object, 394 | payerEmail: string, 395 | paymentToken?: string, 396 | paymentMethod: Object 397 | }) { 398 | // On Android, we don't have `onShippingAddressChange` events, so we 399 | // set the shipping address when the user accepts. 400 | // 401 | // Developers will only have access to it in the `PaymentResponse`. 402 | if (IS_ANDROID) { 403 | const { shippingAddress } = details; 404 | this._shippingAddress = shippingAddress; 405 | } 406 | 407 | const paymentResponse = new PaymentResponse({ 408 | requestId: this.id, 409 | methodName: IS_IOS ? 'apple-pay' : 'android-pay', 410 | shippingAddress: this._options.requestShipping ? this._shippingAddress : null, 411 | details: this._getPlatformDetails(details), 412 | shippingOption: IS_IOS ? this._shippingOption : null, 413 | payerName: this._options.requestPayerName ? this._shippingAddress?.recipient : null, 414 | payerPhone: this._options.requestPayerPhone ? this._shippingAddress?.phone : null, 415 | payerEmail: IS_ANDROID && this._options.requestPayerEmail 416 | ? details.payerEmail 417 | : null 418 | }); 419 | 420 | return this._acceptPromiseResolver(paymentResponse); 421 | } 422 | 423 | _handleGatewayError(details: { error: string }) { 424 | return this._acceptPromiseRejecter(new GatewayError(details.error)); 425 | } 426 | 427 | _closePaymentRequest() { 428 | this._state = 'closed'; 429 | 430 | this._acceptPromiseRejecter(new Error('AbortError')); 431 | 432 | // Remove event listeners before aborting. 433 | this._removeEventListeners(); 434 | } 435 | 436 | _removeEventListeners() { 437 | // Internal Events 438 | this._userDismissSubscription?.remove(); 439 | this._userAcceptSubscription?.remove(); 440 | 441 | if (IS_IOS) { 442 | this._shippingAddressChangeSubscription?.remove(); 443 | this._shippingOptionChangeSubscription?.remove(); 444 | this._paymentMethodChangeSubscription?.remove(); 445 | } 446 | } 447 | 448 | // https://www.w3.org/TR/payment-request/#onshippingaddresschange-attribute 449 | // https://www.w3.org/TR/payment-request/#onshippingoptionchange-attribute 450 | addEventListener( 451 | eventName: 'shippingaddresschange' | 'shippingoptionchange', 452 | fn: e => Promise 453 | ) { 454 | if (eventName === SHIPPING_ADDRESS_CHANGE_EVENT) { 455 | return (this._shippingAddressChangeFn = fn.bind(this)); 456 | } 457 | 458 | if (eventName === SHIPPING_OPTION_CHANGE_EVENT) { 459 | return (this._shippingOptionChangeFn = fn.bind(this)); 460 | } 461 | 462 | if (eventName === PAYMENT_METHOD_CHANGE_EVENT) { 463 | return (this._paymentMethodChangeFn = fn.bind(this)); 464 | } 465 | } 466 | 467 | // https://www.w3.org/TR/payment-request/#id-attribute 468 | get id(): string { 469 | return this._id; 470 | } 471 | 472 | // https://www.w3.org/TR/payment-request/#shippingaddress-attribute 473 | get shippingAddress(): null | PaymentAddress { 474 | return this._shippingAddress; 475 | } 476 | 477 | // https://www.w3.org/TR/payment-request/#shippingoption-attribute 478 | get shippingOption(): null | string { 479 | return this._shippingOption; 480 | } 481 | 482 | get paymentMethod(): null | string { 483 | return this._paymentMethod; 484 | } 485 | 486 | // https://www.w3.org/TR/payment-request/#show-method 487 | show(): Promise { 488 | this._acceptPromise = new Promise((resolve, reject) => { 489 | this._acceptPromiseResolver = resolve; 490 | this._acceptPromiseRejecter = reject; 491 | if (this._state !== 'created') { 492 | return reject(new Error('InvalidStateError')); 493 | } 494 | 495 | this._state = 'interactive'; 496 | 497 | 498 | // These arguments are passed because on Android we don't call createPaymentRequest. 499 | const platformMethodData = getPlatformMethodData(JSON.parse(this._serializedMethodData), Platform.OS); 500 | const normalizedDetails = convertDetailAmountsToString(this._details); 501 | const options = this._options; 502 | 503 | // Note: resolve will be triggered via _acceptPromiseResolver() from somwhere else 504 | return NativePayments.show(platformMethodData, normalizedDetails, options).catch(reject); 505 | }); 506 | 507 | return this._acceptPromise; 508 | } 509 | 510 | // https://www.w3.org/TR/payment-request/#abort-method 511 | abort(): Promise { 512 | return new Promise((resolve, reject) => { 513 | // We can't abort if the PaymentRequest isn't shown or already closed 514 | if (this._state !== 'interactive') { 515 | return reject(new Error('InvalidStateError')); 516 | } 517 | 518 | // Try to dismiss the UI 519 | NativePayments.abort() 520 | .then((_bool) => { 521 | this._closePaymentRequest(); 522 | // Return `undefined` as proposed in the spec. 523 | return resolve(undefined); 524 | }) 525 | .catch((_err) => { 526 | reject(new Error('InvalidStateError'))}); 527 | }); 528 | } 529 | 530 | // https://www.w3.org/TR/payment-request/#canmakepayment-method 531 | canMakePayments(): Promise { 532 | return NativePayments.canMakePayments( 533 | getPlatformMethodData(JSON.parse(this._serializedMethodData), Platform.OS) 534 | ); 535 | } 536 | 537 | static applePayEnabled = NativePayments.applePayEnabled; 538 | static canMakePaymentsUsingNetworks = NativePayments.canMakePaymentsUsingNetworks; 539 | static openPaymentSetup = NativePayments.openPaymentSetup; 540 | } 541 | 542 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-payments 2 | 3 | [![Build Status](https://travis-ci.org/naoufal/react-native-payments.svg?branch=master)](https://travis-ci.org/naoufal/react-native-payments) [![Codeship Status for freeman-industries/react-native-payments](https://app.codeship.com/projects/d6d17e65-23f0-4154-b7ce-33ce59471b08/status?branch=master)](https://app.codeship.com/projects/418096) 4 | 5 | Welcome to the best and most comprehensive library for integrating payments like Apple Pay and Google Pay into your React Native app. 6 | 7 | This library is designed to be fully compatible with React Native 0.61 and onwards. 8 | 9 |
10 | 11 | 12 |
13 | 14 | # Installation 15 | 16 | ``` 17 | npm install --save react-native-payments 18 | ``` 19 | 20 | You'll need to autolink on each platform: 21 | 22 | ### Android 23 | 24 | ``` 25 | npx jetify 26 | ``` 27 | 28 | ### iOS 29 | 30 | ``` 31 | cd ios 32 | pod install 33 | ``` 34 | 35 | # Guides 36 | 37 | ## Example projects 38 | 39 | - [iOS](https://github.com/freeman-industries/react-native-payments-example-ios) 40 | 41 | ## Live demo 42 | 43 | For a step by step guide, check out this talk by @naoufal. 44 | 45 | https://www.youtube.com/watch?v=XrmUuir9OHc&t=652 46 | 47 | ## API Spec 48 | 49 | Down below we have a detailed specification for PaymentRequest and instructions for configuring Apple Pay and Google Pay, which is hopefully enough to get you started. 50 | 51 | We also have some legacy example projects in the `examples` directory that will be rewritten soon and linked above. 52 | 53 | Bear with us while we organize things a bit. 54 | 55 | # Roadmap 56 | 57 | ## Completed 58 | 59 | - Apple Pay Stripe 60 | 61 | ## Completed, untested 62 | 63 | - Apple Pay 64 | - Google Pay (Stripe) 65 | - Web 66 | 67 | ## In progress 68 | 69 | - Stripe: Payment Intents (for SCA) 70 | 71 | ## Planned 72 | 73 | - Tutorial docs 74 | 75 | Naoufal, the original author of this library, has done a lot of the hard work integrating iOS, Android, Web platforms and Stripe gateways. 76 | 77 | The library has fallen out of regular maintenance and we're working to test and update all parts to be compatible for RN in the 2020s. 78 | 79 | If you're feeling brave give the untested platforms a try and let us know how it worked. 80 | 81 | # Contributors 82 | 83 | Many people have contributed to the development of `react-native-payments` over time. The people below are currently available to help. 84 | 85 | - [@nabilfreeman](https://github.com/nabilfreeman) ⚙️ ✏️ 86 | - [@runticle](https://github.com/runticle) ✏️ 87 | 88 | --- 89 | Merge PRs: ⚙️ | Review issues: ✏️ 90 | 91 | ## Join us! 92 | 93 | All contributions, big or small are welcomed. 94 | 95 | For large PRs, please open an issue and have a discussion with us first before you dive in. 96 | 97 | Our plan for this library is for it to be useful to all React Native developers so we want to architect it carefully. 98 | 99 | # In the wild 100 | 101 | These amazing people use `react-native-payments` in their projects. 102 | 103 | - [LeSalon (@lesalonapp)](https://github.com/lesalonapp) 104 | - [Truphone (My Truphone App)](https://truphone.com/consumer/esim-for-smartphone) 105 | 106 | To add your organization, open a PR updating this list. 107 | 108 | --- 109 | 110 | 🚧 111 | 112 | 🚧 113 | 114 | 🚧 115 | 116 | 🚧 117 | 118 | 🚧 119 | 120 | --- 121 | 122 | > This project is currently in __beta and APIs are subject to change.__ 123 | 124 | # React Native Payments 125 | [![react-native version](https://img.shields.io/badge/react--native-0.41-0ba7d3.svg?style=flat-square)](http://facebook.github.io/react-native/releases/0.40) 126 | [![npm](https://img.shields.io/npm/v/react-native-payments.svg?style=flat-square)](https://www.npmjs.com/package/react-native-payments) 127 | [![npm](https://img.shields.io/npm/dm/react-native-payments.svg?style=flat-square)](https://www.npmjs.com/package/react-native-payments) 128 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 129 | 130 | Accept Payments with Apple Pay and Android Pay using the [Payment Request API](https://paymentrequest.show). 131 | 132 | __Features__ 133 | - __Simple.__ No more checkout forms. 134 | - __Effective__. Faster checkouts that increase conversion. 135 | - __Future-proof__. Use a W3C Standards API, supported by companies like Google, Firefox and others. 136 | - __Cross-platform__. Share payments code between your iOS, Android, and web apps. 137 | - __Add-ons__. Easily enable support for Stripe via add-ons. 138 | 139 | --- 140 | 141 | ## Table of Contents 142 | - [Demo](#demo) 143 | - [Installation](#installation) 144 | - [Usage](#usage) 145 | - [Testing Payments](#testing-payments) 146 | - [Apple Pay button](#apple-pay-button) 147 | - [Add-ons](#add-ons) 148 | - [API](#api) 149 | - [Resources](#resources) 150 | - [License](#license) 151 | 152 | ## Demo 153 | You can run the demo by cloning the project and running: 154 | 155 | ```shell 156 | $ yarn run:demo 157 | ``` 158 | 159 | In a rush? Check out the [browser version](https://rnp.nof.me) of the demo. 160 | 161 | _Note that you'll need to run it from a browser with [Payment Request support](https://caniuse.com/#search=payment%20request)._ 162 | 163 | ## Installation 164 | First, download the package: 165 | ```shell 166 | $ yarn add react-native-payments 167 | ``` 168 | Second, link the native dependencies: 169 | ```shell 170 | $ react-native link react-native-payments 171 | ``` 172 | 173 | ## Usage 174 | - [Setting up Apple Pay/Android Pay](#setting-up-apple-payandroid-pay) 175 | - [Importing the Library](#importing-the-library) 176 | - [Initializing the Payment Request](#initializing-the-payment-request) 177 | - [Displaying the Payment Request](#displaying-the-payment-request) 178 | - [Aborting the Payment Request](#aborting-the-payment-request) 179 | - [Requesting Contact Information](#requesting-contact-information) 180 | - [Requesting a Shipping Address](#requesting-a-shipping-address) 181 | - [Processing Payments](#processing-payments) 182 | - [Dismissing the Payment Request](#dismissing-the-payment-request) 183 | 184 | 185 | ### Setting up Apple Pay/Android Pay 186 | Before you can start accepting payments in your App, you'll need to setup Apple Pay and/or Android Pay. 187 | 188 | #### Apple Pay 189 | 1. Register as an Apple Developer 190 | 1. Obtain a merchant ID 191 | 1. Enable Apple Pay in your app 192 | 193 | Apple has a documentation on how to do this in their _[Configuring your Environment](https://developer.apple.com/library/content/ApplePay_Guide/Configuration.html)_ guide. 194 | 195 | #### Android Pay 196 | 197 | 1. Add Android Pay and Google Play Services to your dependencies 198 | 1. Enable Android Pay in your Manifest 199 | 200 | Google has documentation on how to do this in their _[Setup Android Pay](https://developers.google.com/pay/api/android/guides/setup)_ guide. 201 | 202 | ### Importing the Library 203 | Once Apple Pay/Android Pay is enabled in your app, jump into your app's entrypoint and make the `PaymentRequest` globally available to your app. 204 | 205 | ```es6 206 | // index.ios.js 207 | global.PaymentRequest = require('react-native-payments').PaymentRequest; 208 | ``` 209 | 210 | ### Initializing the Payment Request 211 | To initialize a Payment Request, you'll need to provide `PaymentMethodData` and `PaymentDetails`. 212 | 213 | #### Payment Method Data 214 | The Payment Method Data is where you defined the forms of payment that you accept. To enable Apple Pay, we'll define a `supportedMethod` of `apple-pay`. We're also required to pass a `data` object to configures Apple Pay. This is where we provide our merchant id, define the supported card types and the currency we'll be operating in. 215 | 216 | ```es6 217 | const METHOD_DATA = [{ 218 | supportedMethods: ['apple-pay'], 219 | data: { 220 | merchantIdentifier: 'merchant.com.your-app.namespace', 221 | supportedNetworks: ['visa', 'mastercard', 'amex'], 222 | countryCode: 'US', 223 | currencyCode: 'USD' 224 | } 225 | }]; 226 | ``` 227 | 228 |
229 | See Android Pay Example 230 |
231 | 232 | ```es6 233 | const METHOD_DATA = [{ 234 | supportedMethods: ['android-pay'], 235 | data: { 236 | supportedNetworks: ['visa', 'mastercard', 'amex'], 237 | currencyCode: 'USD', 238 | environment: 'TEST', // defaults to production 239 | paymentMethodTokenizationParameters: { 240 | tokenizationType: 'NETWORK_TOKEN', 241 | parameters: { 242 | publicKey: 'your-pubic-key' 243 | } 244 | } 245 | } 246 | }]; 247 | ``` 248 | 249 |
250 | 251 | #### Payment Details 252 | Payment Details is where define transaction details like display items, a total and optionally shipping options. 253 | 254 | Google has excellent documentation for [Defining Payment Details](https://developers.google.com/web/fundamentals/discovery-and-monetization/payment-request/deep-dive-into-payment-request#defining_payment_details). 255 | 256 | ```es6 257 | const DETAILS = { 258 | id: 'basic-example', 259 | displayItems: [ 260 | { 261 | label: 'Movie Ticket', 262 | amount: { currency: 'USD', value: '15.00' } 263 | } 264 | ], 265 | total: { 266 | label: 'Merchant Name', 267 | amount: { currency: 'USD', value: '15.00' } 268 | } 269 | }; 270 | ``` 271 | 272 | Once you've defined your `methodData` and `details`, you're ready to initialize your Payment Request. 273 | 274 | ```es6 275 | const paymentRequest = new PaymentRequest(METHOD_DATA, DETAILS); 276 | ``` 277 | 278 | 🚨 _Note: On Android, display items are not displayed within the Android Pay view. Instead, the _[User Flows documentation](https://developers.google.com/android-pay/payment-flows)_ suggests showing users a confirmation view where you list the display items. When using React Native Payments, show this view after receiving the `PaymentResponse`._ 279 | 280 | ### Displaying the Payment Request 281 | Now that you've setup your Payment Request, displaying it is as simple as calling the `show` method. 282 | 283 | ```es6 284 | paymentRequest.show(); 285 | ``` 286 | 287 |
288 | See Screenshots 289 |
290 | 291 | 292 | 293 |
294 | 295 | ### Aborting the Payment Request 296 | You can abort the Payment Request at any point by calling the `abort` method. 297 | 298 | ```es6 299 | paymentRequest.abort(); 300 | ``` 301 | 302 | 🚨 _Note: Not yet implemented on Android Pay_ 303 | 304 | ### Requesting Contact Information 305 | Some apps may require contact information from a user. You can do so by providing a [`PaymentOptions`]() as a third argument when initializing a Payment Request. Using Payment Options, you can request a contact name, phone number and/or email. 306 | 307 | #### Requesting a Contact Name 308 | Set `requestPayerName` to `true` to request a contact name. 309 | 310 | ```es6 311 | const OPTIONS = { 312 | requestPayerName: true 313 | }; 314 | ``` 315 | 316 |
317 | See Screenshots 318 |
319 | 320 | 321 | 322 |
323 |
324 | 325 | 🚨 _Note: On Android, requesting a contact name will present the user with a shipping address selector. If you're not shipping anything to the user, consider capturing the contact name outside of Android Pay._ 326 | 327 | #### Requesting a Phone Number 328 | Set `requestPayerPhone` to `true` to request a phone number. 329 | 330 | ```es6 331 | const OPTIONS = { 332 | requestPayerPhone: true 333 | }; 334 | ``` 335 | 336 |
337 | See Screenshots 338 |
339 | 340 | 341 | 342 |
343 |
344 | 345 | 🚨 _Note: On Android, requesting a phone number will present the user with a shipping address selector. If you're not shipping anything to the user, consider capturing the phone number outside of Android Pay._ 346 | 347 | #### Requesting an Email Address 348 | Set `requestPayerEmail` to `true` to request an email address. 349 | 350 | ```es6 351 | const OPTIONS = { 352 | requestPayerEmail: true 353 | }; 354 | ``` 355 | 356 |
357 | See Screenshots 358 |
359 | 360 | 361 | 362 |
363 |
364 | 365 | You can also request all three by setting them all to `true`. 366 | 367 | ```es6 368 | const OPTIONS = { 369 | requestPayerName: true, 370 | requestPayerPhone: true, 371 | requestPayerEmail: true 372 | }; 373 | ``` 374 | 375 | ### Requesting a Shipping Address 376 | Requesting a shipping address is done in three steps. 377 | 378 | First, you'll need to set `requestShipping` to `true` within `PaymentOptions`. 379 | 380 | ```es6 381 | const OPTIONS = { 382 | requestShipping: true 383 | }; 384 | ``` 385 | 386 | Second, you'll need to include `shippingOptions` in your Payment Details. 387 | 388 | ```diff 389 | const DETAILS = { 390 | id: 'basic-example', 391 | displayItems: [ 392 | { 393 | label: 'Movie Ticket', 394 | amount: { currency: 'USD', value: '15.00' } 395 | } 396 | ], 397 | + shippingOptions: [{ 398 | + id: 'economy', 399 | + label: 'Economy Shipping', 400 | + amount: { currency: 'USD', value: '0.00' }, 401 | + detail: 'Arrives in 3-5 days' // `detail` is specific to React Native Payments 402 | + }], 403 | total: { 404 | label: 'Merchant Name', 405 | amount: { currency: 'USD', value: '15.00' } 406 | } 407 | }; 408 | ``` 409 | 410 | Lastly, you'll need to register event listeners for when a user selects a `shippingAddress` and/or a `shippingOption`. In the callback each event, you'll need to provide new `PaymentDetails` that will update your PaymentRequest. 411 | 412 | ```es6 413 | paymentRequest.addEventListener('shippingaddresschange', e => { 414 | const updatedDetails = getUpdatedDetailsForShippingAddress(paymentRequest.shippingAddress; 415 | 416 | e.updateWith(updatedDetails); 417 | }); 418 | 419 | paymentRequest.addEventListener('shippingoptionchange', e => { 420 | const updatedDetails = getUpdatedDetailsForShippingOption(paymentRequest.shippingOption); 421 | 422 | e.updateWith(updatedDetails); 423 | }); 424 | ``` 425 | 426 | For a deeper dive on handling shipping in Payment Request, checkout Google's _[Shipping in Payment Request](https://developers.google.com/web/fundamentals/discovery-and-monetization/payment-request/deep-dive-into-payment-request#shipping_in_payment_request_api)_. 427 | 428 | 🚨 _Note: On Android, there are no `shippingaddresschange` and `shippingoptionchange` events. To allow users to update their shipping address, you'll need to trigger a new `PaymentRequest`. Updating shipping options typically happens after the receiving the `PaymentResponse` and before calling its `getPaymentToken` method._ 429 | 430 | ### Processing Payments 431 | Now that we know how to initialize, display, and dismiss a Payment Request, let's take a look at how to process payments. 432 | 433 | When a user accepts to pay, `PaymentRequest.show` will resolve to a Payment Response. 434 | 435 | ```es6 436 | paymentRequest.show() 437 | .then(paymentResponse => { 438 | // Your payment processing code goes here 439 | 440 | return processPayment(paymentResponse); 441 | }); 442 | ``` 443 | 444 | There are two ways to process Apple Pay/Android Pay payments -- on your server or using a payment processor. 445 | 446 | #### Processing Payments on Your Server 447 | If you're equipped to process Apple Pay/Android Pay payments on your server, all you have to do is send the Payment Response data to your server. 448 | 449 | > ⚠️ **Note:** When running Apple Pay on simulator, `paymentData` equals to `null`. 450 | 451 | ```es6 452 | import { NativeModules } from 'react-native'; 453 | 454 | paymentRequest.show() 455 | .then(paymentResponse => { 456 | const { transactionIdentifier, paymentData } = paymentResponse.details; 457 | 458 | return fetch('...', { 459 | method: 'POST', 460 | body: { 461 | transactionIdentifier, 462 | paymentData 463 | } 464 | }) 465 | .then(res => res.json()) 466 | .then(successHandler) 467 | .catch(errorHandler) 468 | }); 469 | ``` 470 | 471 |
472 | See Android Pay Example 473 |
474 | 475 | ```es6 476 | paymentRequest.show() 477 | .then(paymentResponse => { 478 | const { getPaymentToken } = paymentResponse.details; 479 | 480 | return getPaymentToken() 481 | .then(paymentToken => { 482 | const { ephemeralPublicKey, encryptedMessage, tag } = paymentResponse.details; 483 | 484 | return fetch('...', { 485 | method: 'POST', 486 | body: { 487 | ephemeralPublicKey, 488 | encryptedMessage, 489 | tag 490 | } 491 | }) 492 | .then(res => res.json()) 493 | .then(successHandler) 494 | .catch(errorHandler) 495 | }); 496 | }); 497 | ``` 498 | 499 |
500 |
501 | 502 | You can learn more about server-side decrypting of Payment Tokens on Apple's [Payment Token Format Reference](https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html) documentation. 503 | 504 | #### Processing Payments with a Payment Processor 505 | When using a payment processor, you'll receive a `paymentToken` field within the `details` of the `PaymentResponse`. Use this token to charge customers with your payment processor. 506 | 507 | ```es6 508 | paymentRequest.show() 509 | .then(paymentResponse => { 510 | const { paymentToken } = paymentResponse.details; // On Android, you need to invoke the `getPaymentToken` method to receive the `paymentToken`. 511 | 512 | return fetch('...', { 513 | method: 'POST', 514 | body: { 515 | paymentToken 516 | } 517 | }) 518 | .then(res => res.json()) 519 | .then(successHandler) 520 | .catch(errorHandler); 521 | }); 522 | ``` 523 | 524 |
525 | See Android Pay Example 526 |
527 | 528 | ```es6 529 | paymentRequest.show() 530 | .then(paymentResponse => { 531 | const { getPaymentToken } = paymentResponse.details; 532 | 533 | return getPaymentToken() 534 | .then(paymentToken => fetch('...', { 535 | method: 'POST', 536 | body: { 537 | paymentToken 538 | } 539 | }) 540 | .then(res => res.json()) 541 | .then(successHandler) 542 | .catch(errorHandler); 543 | }); 544 | }); 545 | ``` 546 | 547 |
548 |
549 | 550 | For a list of supported payment processors and how to enable them, see the [Add-ons](#add-ons) section. 551 | 552 | ### Dismissing the Payment Request 553 | Dismissing the Payment Request is as simple as calling the `complete` method on of the `PaymentResponse`. 554 | 555 | ```es6 556 | paymentResponse.complete('success'); // Alternatively, you can call it with `fail` or `unknown` 557 | ``` 558 | 559 | 🚨 _Note: On Android, there is no need to call `paymentResponse.complete` -- the PaymentRequest dismisses itself._ 560 | 561 | ## Testing Payments 562 | 563 | ### Apple Pay 564 | 565 | The sandbox environment is a great way to test offline implementation of Apple Pay for apps, websites, and point of sale systems. Apple offers [detailed guide](https://developer.apple.com/support/apple-pay-sandbox/) for setting up sandbox environment. 566 | > ⚠️ **Note:** It is also important to test Apple Pay in your production environment. Real cards must be used in the production environment. Test cards will not work. 567 | > 568 | > ⚠️ **Note:** There are known differences when running Apple Pay on simulator and real device. Make sure you test Apple Pay on real device before going into production. 569 | 570 | ## Apple Pay Button 571 | 572 | Provides a button that is used either to trigger payments through Apple Pay or to prompt the user to set up a card. 573 | [Detailed docs and examples](docs/ApplePayButton.md) 574 | 575 | ## Add-ons 576 | Here's a list of Payment Processors that you can enable via add-ons: 577 | - [Stripe](https://github.com/naoufal/react-native-payments/blob/master/packages/react-native-payments-addon-stripe) 578 | 579 | 🚨 _Note: On Android, Payment Processors are enabled by default._ 580 | 581 | ## API 582 | ### [NativePayments](docs/NativePayments.md) 583 | ### [PaymentRequest](docs/PaymentRequest.md) 584 | ### [PaymentRequestUpdateEvent](docs/PaymentRequestUpdateEvent.md) 585 | ### [PaymentResponse](docs/PaymentResponse.md) 586 | 587 | ## Resources 588 | ### Payment Request 589 | - [Introducing the Payment Request API](https://developers.google.com/web/fundamentals/discovery-and-monetization/payment-request) 590 | - [Deep Dive into the Payment Request API](https://developers.google.com/web/fundamentals/discovery-and-monetization/payment-request/deep-dive-into-payment-request) 591 | - [W3C API Working Draft](https://www.w3.org/TR/payment-request/) 592 | - [Web Payments](https://www.youtube.com/watch?v=U0LkQijSeko) 593 | - [The Future of Web Payments](https://www.youtube.com/watch?v=hU89pPBmhds) 594 | 595 | ### Apple Pay 596 | - [Getting Started with Apple Pay](https://developer.apple.com/apple-pay/get-started) 597 | - [Configuring your Environment](https://developer.apple.com/library/content/ApplePay_Guide/Configuration.html) 598 | - [Processing Payments](https://developer.apple.com/library/content/ApplePay_Guide/ProcessPayment.html#//apple_ref/doc/uid/TP40014764-CH5-SW4) 599 | - [Payment Token Format Reference](https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html#//apple_ref/doc/uid/TP40014929) 600 | 601 | ### Android Pay 602 | - [Setup Android Pay](https://developers.google.com/pay/api/android/guides/setup) 603 | - [Tutorial](https://developers.google.com/pay/api/android/guides/tutorial) 604 | - [Brand Guidelines](https://developers.google.com/pay/api/android/guides/brand-guidelines) 605 | - [Gateway Token Approach](https://developers.google.com/web/fundamentals/discovery-and-monetization/payment-request/android-pay#gateway_token_approach) 606 | - [Network Token Approach](https://developers.google.com/web/fundamentals/discovery-and-monetization/payment-request/android-pay#network_token_approach) 607 | 608 | # License 609 | Licensed under the MIT License, Copyright © 2017, [Naoufal Kadhom](https://twitter.com/naoufal). 610 | 611 | See [LICENSE](https://github.com/naoufal/react-native-payments/blob/master/LICENSE) for more information. -------------------------------------------------------------------------------- /ios/ReactNativePayments.m: -------------------------------------------------------------------------------- 1 | #import "ReactNativePayments.h" 2 | #import 3 | #import 4 | 5 | @implementation ReactNativePayments 6 | @synthesize bridge = _bridge; 7 | 8 | RCT_EXPORT_MODULE() 9 | 10 | - (dispatch_queue_t)methodQueue 11 | { 12 | return dispatch_get_main_queue(); 13 | } 14 | 15 | + (BOOL)requiresMainQueueSetup 16 | { 17 | return YES; 18 | } 19 | 20 | - (NSDictionary *)constantsToExport 21 | { 22 | return @{ 23 | @"canMakePayments": @([PKPaymentAuthorizationViewController canMakePayments]), 24 | @"supportedGateways": [GatewayManager getSupportedGateways] 25 | }; 26 | } 27 | 28 | RCT_EXPORT_METHOD(canMakePaymentsUsingNetworks: 29 | (NSArray *)paymentNetworks 30 | callback:(RCTResponseSenderBlock)callback) 31 | { 32 | callback(@[[NSNull null], @([PKPaymentAuthorizationViewController canMakePaymentsUsingNetworks:paymentNetworks])]); 33 | } 34 | 35 | RCT_EXPORT_METHOD(openPaymentSetup) 36 | { 37 | [[[PKPassLibrary alloc] init] openPaymentSetup]; 38 | } 39 | 40 | RCT_EXPORT_METHOD(createPaymentRequest: (NSDictionary *)methodData 41 | details: (NSDictionary *)details 42 | options: (NSDictionary *)options 43 | callback: (RCTResponseSenderBlock)callback) 44 | { 45 | NSString *merchantId = methodData[@"merchantIdentifier"]; 46 | NSDictionary *gatewayParameters = methodData[@"paymentMethodTokenizationParameters"][@"parameters"]; 47 | 48 | if (gatewayParameters) { 49 | self.hasGatewayParameters = true; 50 | self.gatewayManager = [GatewayManager new]; 51 | [self.gatewayManager configureGateway:gatewayParameters merchantIdentifier:merchantId]; 52 | } 53 | 54 | self.paymentRequest = [[PKPaymentRequest alloc] init]; 55 | self.paymentRequest.merchantIdentifier = merchantId; 56 | self.paymentRequest.merchantCapabilities = PKMerchantCapability3DS; 57 | self.paymentRequest.countryCode = methodData[@"countryCode"]; 58 | self.paymentRequest.currencyCode = methodData[@"currencyCode"]; 59 | self.paymentRequest.supportedNetworks = [self getSupportedNetworksFromMethodData:methodData]; 60 | self.paymentRequest.paymentSummaryItems = [self getPaymentSummaryItemsFromDetails:details]; 61 | self.paymentRequest.shippingMethods = [self getShippingMethodsFromDetails:details]; 62 | 63 | [self setRequiredAddressFieldsFromOptions:options]; 64 | 65 | // Set options so that we can later access it. 66 | self.initialOptions = options; 67 | 68 | callback(@[[NSNull null]]); 69 | } 70 | 71 | RCT_EXPORT_METHOD(show:(RCTResponseSenderBlock)callback) 72 | { 73 | 74 | self.viewController = [[PKPaymentAuthorizationViewController alloc] initWithPaymentRequest: self.paymentRequest]; 75 | self.viewController.delegate = self; 76 | 77 | dispatch_async(dispatch_get_main_queue(), ^{ 78 | UIViewController *rootViewController = RCTPresentedViewController(); 79 | [rootViewController presentViewController:self.viewController animated:YES completion:nil]; 80 | callback(@[[NSNull null]]); 81 | }); 82 | } 83 | 84 | RCT_EXPORT_METHOD(abort: (RCTResponseSenderBlock)callback) 85 | { 86 | [self.viewController dismissViewControllerAnimated:YES completion:nil]; 87 | 88 | callback(@[[NSNull null]]); 89 | } 90 | 91 | RCT_EXPORT_METHOD(complete: (NSString *)paymentStatus 92 | callback: (RCTResponseSenderBlock)callback) 93 | { 94 | if ([paymentStatus isEqualToString: @"success"]) { 95 | self.completion(PKPaymentAuthorizationStatusSuccess); 96 | } else { 97 | self.completion(PKPaymentAuthorizationStatusFailure); 98 | } 99 | 100 | callback(@[[NSNull null]]); 101 | } 102 | 103 | 104 | -(void) paymentAuthorizationViewControllerDidFinish:(PKPaymentAuthorizationViewController *)controller 105 | { 106 | [controller dismissViewControllerAnimated:YES completion:nil]; 107 | [self.bridge.eventDispatcher sendDeviceEventWithName:@"NativePayments:onuserdismiss" body:nil]; 108 | } 109 | 110 | RCT_EXPORT_METHOD(handleDetailsUpdate: (NSDictionary *)details 111 | callback: (RCTResponseSenderBlock)callback) 112 | 113 | { 114 | if (!self.shippingContactCompletion && !self.shippingMethodCompletion && !self.paymentMethodCompletion) { 115 | // TODO: 116 | // - Call callback with error saying shippingContactCompletion was never called; 117 | 118 | return; 119 | } 120 | 121 | NSArray * shippingMethods = [self getShippingMethodsFromDetails:details]; 122 | 123 | NSArray * paymentSummaryItems = [self getPaymentSummaryItemsFromDetails:details]; 124 | 125 | if (self.paymentMethodCompletion) { 126 | self.paymentMethodCompletion (paymentSummaryItems); 127 | self.paymentMethodCompletion = nil; 128 | } 129 | 130 | if (self.shippingMethodCompletion) { 131 | self.shippingMethodCompletion( 132 | PKPaymentAuthorizationStatusSuccess, 133 | paymentSummaryItems 134 | ); 135 | 136 | // Invalidate `self.shippingMethodCompletion` 137 | self.shippingMethodCompletion = nil; 138 | } 139 | 140 | if (self.shippingContactCompletion) { 141 | // Display shipping address error when shipping is needed and shipping method count is below 1 142 | if ([self.initialOptions[@"requestShipping"] boolValue] && [shippingMethods count] == 0) { 143 | return self.shippingContactCompletion( 144 | PKPaymentAuthorizationStatusInvalidShippingPostalAddress, 145 | shippingMethods, 146 | paymentSummaryItems 147 | ); 148 | } else { 149 | self.shippingContactCompletion( 150 | PKPaymentAuthorizationStatusSuccess, 151 | shippingMethods, 152 | paymentSummaryItems 153 | ); 154 | } 155 | // Invalidate `aself.shippingContactCompletion` 156 | self.shippingContactCompletion = nil; 157 | 158 | } 159 | 160 | // Call callback 161 | callback(@[[NSNull null]]); 162 | 163 | } 164 | 165 | // DELEGATES 166 | // --------------- 167 | - (void) paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller 168 | didAuthorizePayment:(PKPayment *)payment 169 | completion:(void (^)(PKPaymentAuthorizationStatus))completion 170 | { 171 | // Store completion for later use 172 | self.completion = completion; 173 | 174 | if (self.hasGatewayParameters) { 175 | [self.gatewayManager createTokenWithPayment:payment completion:^(NSString * _Nullable token, NSError * _Nullable error) { 176 | if (error) { 177 | [self handleGatewayError:error]; 178 | return; 179 | } 180 | 181 | [self handleUserAccept:payment paymentToken:token]; 182 | }]; 183 | } else { 184 | [self handleUserAccept:payment paymentToken:nil]; 185 | } 186 | } 187 | 188 | 189 | // Shipping Contact 190 | - (void) paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller 191 | didSelectShippingContact:(PKContact *)contact 192 | completion:(nonnull void (^)(PKPaymentAuthorizationStatus, NSArray * _Nonnull, NSArray * _Nonnull))completion 193 | { 194 | if ([self.initialOptions[@"requestShipping"] boolValue]) { 195 | self.shippingContactCompletion = completion; 196 | 197 | CNPostalAddress *postalAddress = contact.postalAddress; 198 | // street, subAdministrativeArea, and subLocality are supressed for privacy 199 | [self.bridge.eventDispatcher sendDeviceEventWithName:@"NativePayments:onshippingaddresschange" 200 | body:@{ 201 | @"recipient": [NSNull null], 202 | @"organization": [NSNull null], 203 | @"addressLine": [NSNull null], 204 | @"city": postalAddress.city, 205 | @"region": postalAddress.state, 206 | @"country": [postalAddress.ISOCountryCode uppercaseString], 207 | @"postalCode": postalAddress.postalCode, 208 | @"phone": [NSNull null], 209 | @"languageCode": [NSNull null], 210 | @"sortingCode": [NSNull null], 211 | @"dependentLocality": [NSNull null] 212 | }]; 213 | } 214 | } 215 | 216 | // Shipping Method delegates 217 | - (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller 218 | didSelectShippingMethod:(PKShippingMethod *)shippingMethod 219 | completion:(void (^)(PKPaymentAuthorizationStatus, NSArray * _Nonnull))completion 220 | { 221 | self.shippingMethodCompletion = completion; 222 | 223 | [self.bridge.eventDispatcher sendDeviceEventWithName:@"NativePayments:onshippingoptionchange" body:@{ 224 | @"selectedShippingOptionId": shippingMethod.identifier 225 | }]; 226 | } 227 | 228 | // Payment method changes 229 | - (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller 230 | didSelectPaymentMethod:(PKPaymentMethod *)paymentMethod 231 | completion:(nonnull void (^)(NSArray * _Nonnull))completion 232 | { 233 | self.paymentMethodCompletion = completion; 234 | NSString *type = [self paymentMethodTypeToString:paymentMethod.type]; 235 | [self.bridge.eventDispatcher sendDeviceEventWithName:@"NativePayments:onpaymentmethodchange" body:@{ 236 | @"paymentMethodType": type 237 | }]; 238 | } 239 | 240 | // PRIVATE METHODS 241 | // https://developer.apple.com/reference/passkit/pkpaymentnetwork 242 | // --------------- 243 | - (NSArray *_Nonnull)getSupportedNetworksFromMethodData:(NSDictionary *_Nonnull)methodData 244 | { 245 | NSMutableDictionary *supportedNetworksMapping = [[NSMutableDictionary alloc] init]; 246 | 247 | CGFloat iOSVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; 248 | 249 | if (iOSVersion >= 8) { 250 | [supportedNetworksMapping setObject:PKPaymentNetworkAmex forKey:@"amex"]; 251 | [supportedNetworksMapping setObject:PKPaymentNetworkMasterCard forKey:@"mastercard"]; 252 | [supportedNetworksMapping setObject:PKPaymentNetworkVisa forKey:@"visa"]; 253 | } 254 | 255 | if (iOSVersion >= 9) { 256 | [supportedNetworksMapping setObject:PKPaymentNetworkDiscover forKey:@"discover"]; 257 | [supportedNetworksMapping setObject:PKPaymentNetworkPrivateLabel forKey:@"privatelabel"]; 258 | } 259 | 260 | if (iOSVersion >= 9.2) { 261 | [supportedNetworksMapping setObject:PKPaymentNetworkChinaUnionPay forKey:@"chinaunionpay"]; 262 | [supportedNetworksMapping setObject:PKPaymentNetworkInterac forKey:@"interac"]; 263 | } 264 | 265 | if (iOSVersion >= 10.1) { 266 | [supportedNetworksMapping setObject:PKPaymentNetworkJCB forKey:@"jcb"]; 267 | [supportedNetworksMapping setObject:PKPaymentNetworkSuica forKey:@"suica"]; 268 | } 269 | 270 | if (iOSVersion >= 10.3) { 271 | [supportedNetworksMapping setObject:PKPaymentNetworkCarteBancaire forKey:@"cartebancaires"]; 272 | [supportedNetworksMapping setObject:PKPaymentNetworkIDCredit forKey:@"idcredit"]; 273 | [supportedNetworksMapping setObject:PKPaymentNetworkQuicPay forKey:@"quicpay"]; 274 | } 275 | 276 | if (iOSVersion >= 11) { 277 | [supportedNetworksMapping setObject:PKPaymentNetworkCarteBancaires forKey:@"cartebancaires"]; 278 | } 279 | 280 | if (iOSVersion >= 12.1) { 281 | [supportedNetworksMapping setObject:PKPaymentNetworkMada forKey:@"mada"]; 282 | } 283 | 284 | if (iOSVersion >= 12.1) { 285 | [supportedNetworksMapping setObject:PKPaymentNetworkMada forKey:@"mada"]; 286 | } 287 | 288 | // Setup supportedNetworks 289 | NSArray *jsSupportedNetworks = methodData[@"supportedNetworks"]; 290 | NSMutableArray *supportedNetworks = [NSMutableArray array]; 291 | for (NSString *supportedNetwork in jsSupportedNetworks) { 292 | [supportedNetworks addObject: supportedNetworksMapping[supportedNetwork]]; 293 | } 294 | 295 | return supportedNetworks; 296 | } 297 | 298 | - (NSArray *_Nonnull)getPaymentSummaryItemsFromDetails:(NSDictionary *_Nonnull)details 299 | { 300 | // Setup `paymentSummaryItems` array 301 | NSMutableArray * paymentSummaryItems = [NSMutableArray array]; 302 | 303 | // Add `displayItems` to `paymentSummaryItems` 304 | NSArray *displayItems = details[@"displayItems"]; 305 | if (displayItems.count > 0) { 306 | for (NSDictionary *displayItem in displayItems) { 307 | [paymentSummaryItems addObject: [self convertDisplayItemToPaymentSummaryItem:displayItem]]; 308 | } 309 | } 310 | 311 | // Add total to `paymentSummaryItems` 312 | NSDictionary *total = details[@"total"]; 313 | [paymentSummaryItems addObject: [self convertDisplayItemToPaymentSummaryItem:total]]; 314 | 315 | return paymentSummaryItems; 316 | } 317 | 318 | - (NSArray *_Nonnull)getShippingMethodsFromDetails:(NSDictionary *_Nonnull)details 319 | { 320 | // Setup `shippingMethods` array 321 | NSMutableArray * shippingMethods = nil; 322 | if ([self.initialOptions[@"requestShipping"] boolValue]) { 323 | shippingMethods = [NSMutableArray array]; 324 | 325 | // Add `shippingOptions` to `shippingMethods` 326 | NSArray *shippingOptions = details[@"shippingOptions"]; 327 | if (shippingOptions.count > 0) { 328 | for (NSDictionary *shippingOption in shippingOptions) { 329 | [shippingMethods addObject: [self convertShippingOptionToShippingMethod:shippingOption]]; 330 | } 331 | } 332 | } 333 | 334 | return shippingMethods; 335 | } 336 | 337 | - (PKPaymentSummaryItem *_Nonnull)convertDisplayItemToPaymentSummaryItem:(NSDictionary *_Nonnull)displayItem; 338 | { 339 | NSDecimalNumber *decimalNumberAmount = [NSDecimalNumber decimalNumberWithString:displayItem[@"amount"][@"value"]]; 340 | PKPaymentSummaryItemType itemType = PKPaymentSummaryItemTypeFinal; 341 | if ([displayItem[@"pending"] respondsToSelector:@selector(boolValue)] && [displayItem[@"pending"] boolValue] == YES) { 342 | itemType = PKPaymentSummaryItemTypePending; 343 | } 344 | PKPaymentSummaryItem *paymentSummaryItem = [PKPaymentSummaryItem 345 | summaryItemWithLabel:displayItem[@"label"] 346 | amount:decimalNumberAmount 347 | type:itemType]; 348 | 349 | return paymentSummaryItem; 350 | } 351 | 352 | - (PKShippingMethod *_Nonnull)convertShippingOptionToShippingMethod:(NSDictionary *_Nonnull)shippingOption 353 | { 354 | PKShippingMethod *shippingMethod = [PKShippingMethod summaryItemWithLabel:shippingOption[@"label"] amount:[NSDecimalNumber decimalNumberWithString: shippingOption[@"amount"][@"value"]]]; 355 | shippingMethod.identifier = shippingOption[@"id"]; 356 | 357 | // shippingOption.detail is not part of the PaymentRequest spec. 358 | if ([shippingOption[@"detail"] isKindOfClass:[NSString class]]) { 359 | shippingMethod.detail = shippingOption[@"detail"]; 360 | } else { 361 | shippingMethod.detail = @""; 362 | } 363 | 364 | return shippingMethod; 365 | } 366 | 367 | - (void)setRequiredAddressFieldsFromOptions:(NSDictionary *_Nonnull)options 368 | { 369 | // Request Shipping 370 | if ([options[@"requestShipping"] boolValue]) { 371 | NSMutableSet *shippingContactFields = [NSMutableSet setWithArray:@[PKContactFieldPostalAddress]]; 372 | if ([options[@"requestPayerName"] boolValue]) { 373 | [shippingContactFields addObject:PKContactFieldName]; 374 | } 375 | 376 | if ([options[@"requestPayerPhone"] boolValue]) { 377 | [shippingContactFields addObject:PKContactFieldPhoneNumber]; 378 | } 379 | 380 | if ([options[@"requestPayerEmail"] boolValue]) { 381 | [shippingContactFields addObject:PKContactFieldEmailAddress]; 382 | } 383 | self.paymentRequest.requiredShippingContactFields = shippingContactFields; 384 | } 385 | 386 | if ([options[@"requestBilling"] boolValue]) { 387 | self.paymentRequest.requiredBillingContactFields = [NSSet setWithArray:@[PKContactFieldPostalAddress]]; 388 | } 389 | } 390 | 391 | - (NSString *_Nonnull)contactToString:(PKContact *_Nonnull)contact 392 | { 393 | NSString *namePrefix = contact.name.namePrefix; 394 | NSString *givenName = contact.name.givenName; 395 | NSString *middleName = contact.name.middleName; 396 | NSString *familyName = contact.name.familyName; 397 | NSString *nameSuffix = contact.name.nameSuffix; 398 | NSString *nickname = contact.name.nickname; 399 | NSString *street = contact.postalAddress.street; 400 | NSString *subLocality = contact.postalAddress.subLocality; 401 | NSString *city = contact.postalAddress.city; 402 | NSString *subAdministrativeArea = contact.postalAddress.subAdministrativeArea; 403 | NSString *state = contact.postalAddress.state; 404 | NSString *postalCode = contact.postalAddress.postalCode; 405 | NSString *country = contact.postalAddress.country; 406 | NSString *ISOCountryCode = contact.postalAddress.ISOCountryCode; 407 | NSString *phoneNumber = contact.phoneNumber.stringValue; 408 | NSString *emailAddress = contact.emailAddress; 409 | 410 | NSDictionary *contactDict = @{ 411 | @"name" : @{ 412 | @"namePrefix" : namePrefix ?: @"", 413 | @"givenName" : givenName ?: @"", 414 | @"middleName" : middleName ?: @"", 415 | @"familyName" : familyName ?: @"", 416 | @"nameSuffix" : nameSuffix ?: @"", 417 | @"nickname" : nickname ?: @"", 418 | }, 419 | @"postalAddress" : @{ 420 | @"street" : street ?: @"", 421 | @"subLocality" : subLocality ?: @"", 422 | @"city" : city ?: @"", 423 | @"subAdministrativeArea" : subAdministrativeArea ?: @"", 424 | @"state" : state ?: @"", 425 | @"postalCode" : postalCode ?: @"", 426 | @"country" : country ?: @"", 427 | @"ISOCountryCode" : ISOCountryCode ?: @"" 428 | }, 429 | @"phoneNumber" : phoneNumber ?: @"", 430 | @"emailAddress" : emailAddress ?: @"" 431 | }; 432 | 433 | NSError *error; 434 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:contactDict options:0 error:&error]; 435 | 436 | if (! jsonData) { 437 | return @""; 438 | } else { 439 | return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 440 | } 441 | 442 | } 443 | 444 | - (NSDictionary *_Nonnull)paymentMethodToString:(PKPaymentMethod *_Nonnull)paymentMethod 445 | { 446 | NSMutableDictionary *result = [[NSMutableDictionary alloc]initWithCapacity:4]; 447 | 448 | if(paymentMethod.displayName) { 449 | [result setObject:paymentMethod.displayName forKey:@"displayName"]; 450 | } 451 | if (paymentMethod.network) { 452 | [result setObject:paymentMethod.network forKey:@"network"]; 453 | } 454 | NSString *type = [self paymentMethodTypeToString:paymentMethod.type]; 455 | [result setObject:type forKey:@"type"]; 456 | if(paymentMethod.paymentPass) { 457 | NSDictionary *paymentPass = [self paymentPassToDictionary:paymentMethod.paymentPass]; 458 | [result setObject:paymentPass forKey:@"paymentPass"]; 459 | } 460 | 461 | return result; 462 | } 463 | 464 | - (NSString *_Nonnull)paymentMethodTypeToString:(PKPaymentMethodType)paymentMethodType 465 | { 466 | NSArray *arr = @[@"PKPaymentMethodTypeUnknown", 467 | @"PKPaymentMethodTypeDebit", 468 | @"PKPaymentMethodTypeCredit", 469 | @"PKPaymentMethodTypePrepaid", 470 | @"PKPaymentMethodTypeStore"]; 471 | return (NSString *)[arr objectAtIndex:paymentMethodType]; 472 | } 473 | 474 | - (NSDictionary *_Nonnull)paymentPassToDictionary:(PKPaymentPass *_Nonnull)paymentPass 475 | { 476 | return @{ 477 | @"primaryAccountIdentifier" : paymentPass.primaryAccountIdentifier, 478 | @"primaryAccountNumberSuffix" : paymentPass.primaryAccountNumberSuffix, 479 | @"deviceAccountIdentifier" : paymentPass.deviceAccountIdentifier, 480 | @"deviceAccountNumberSuffix" : paymentPass.deviceAccountNumberSuffix, 481 | @"activationState" : [self paymentPassActivationStateToString:paymentPass.activationState] 482 | }; 483 | } 484 | 485 | - (NSString *_Nonnull)paymentPassActivationStateToString:(PKPaymentPassActivationState)paymentPassActivationState 486 | { 487 | NSArray *arr = @[@"PKPaymentPassActivationStateActivated", 488 | @"PKPaymentPassActivationStateRequiresActivation", 489 | @"PKPaymentPassActivationStateActivating", 490 | @"PKPaymentPassActivationStateSuspended", 491 | @"PKPaymentPassActivationStateDeactivated"]; 492 | return (NSString *)[arr objectAtIndex:paymentPassActivationState]; 493 | } 494 | 495 | - (void)handleUserAccept:(PKPayment *_Nonnull)payment 496 | paymentToken:(NSString *_Nullable)token 497 | { 498 | NSMutableDictionary *paymentResponse = [[NSMutableDictionary alloc]initWithCapacity:6]; 499 | 500 | NSString *transactionId = payment.token.transactionIdentifier; 501 | [paymentResponse setObject:transactionId forKey:@"transactionIdentifier"]; 502 | 503 | NSString *paymentData = [[NSString alloc] initWithData:payment.token.paymentData encoding:NSUTF8StringEncoding]; 504 | [paymentResponse setObject:paymentData forKey:@"paymentData"]; 505 | 506 | NSDictionary *paymentMethod = [self paymentMethodToString:payment.token.paymentMethod]; 507 | [paymentResponse setObject:paymentMethod forKey:@"paymentMethod"]; 508 | 509 | if (token) { 510 | [paymentResponse setObject:token forKey:@"paymentToken"]; 511 | } 512 | 513 | if (payment.billingContact) { 514 | paymentResponse[@"billingContact"] = [self contactToString:payment.billingContact]; 515 | } 516 | 517 | if (payment.shippingContact) { 518 | paymentResponse[@"shippingContact"] = [self contactToString:payment.shippingContact]; 519 | } 520 | 521 | [self.bridge.eventDispatcher sendDeviceEventWithName:@"NativePayments:onuseraccept" 522 | body:paymentResponse 523 | ]; 524 | } 525 | 526 | - (void)handleGatewayError:(NSError *_Nonnull)error 527 | { 528 | [self.bridge.eventDispatcher sendDeviceEventWithName:@"NativePayments:ongatewayerror" 529 | body: @{ 530 | @"error": [error localizedDescription] 531 | } 532 | ]; 533 | } 534 | 535 | @end 536 | --------------------------------------------------------------------------------