├── .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 | 
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 | 
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 | [](https://travis-ci.org/naoufal/react-native-payments) [](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 | [](http://facebook.github.io/react-native/releases/0.40)
126 | [](https://www.npmjs.com/package/react-native-payments)
127 | [](https://www.npmjs.com/package/react-native-payments)
128 | [](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 |
--------------------------------------------------------------------------------