diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..dfaf3f0 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,46 @@ +{ + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": false, + "contributors": [ + { + "login": "lukebrandonfarrell", + "name": "Luke Brandon Farrell", + "avatar_url": "https://avatars3.githubusercontent.com/u/18139277?v=4", + "profile": "http://www.lukebrandonfarrell.com", + "contributions": [ + "code", + "infra", + "projectManagement" + ] + }, + { + "login": "ChesterSim", + "name": "Chester Sim", + "avatar_url": "https://avatars2.githubusercontent.com/u/12388321?v=4", + "profile": "https://github.com/ChesterSim", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "amogh-jrules", + "name": "Amogh Jahagirdar", + "avatar_url": "https://avatars3.githubusercontent.com/u/31567169?v=4", + "profile": "https://jramogh.co", + "contributions": [ + "doc", + "code" + ] + } + ], + "contributorsPerLine": 7, + "projectName": "react-native-stripe-payments", + "projectOwner": "aspect-apps", + "repoType": "github", + "repoHost": "https://github.com", + "skipCi": true +} diff --git a/.gitignore b/.gitignore index c5464d9..46aa1a5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,35 +8,4 @@ node_modules/ npm-debug.log yarn-error.log -# Xcode -# -build/ -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate -project.xcworkspace - -# Android/IntelliJ -# -build/ -.idea -.gradle -local.properties -*.iml - -# BUCK -buck-out/ -\.buckd/ -*.keystore +android/build/ diff --git a/README.md b/README.md index 1ab48d7..b77b733 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ ![React Native Stripe payments](https://raw.githubusercontent.com/Fitpassu/react-native-stripe-payments/master/react-native-stripe-payments.png) + +[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) + A well typed React Native library providing support for Stripe payments on both iOS and Android. @@ -20,21 +23,23 @@ The library ships with platform native code that needs to be compiled together w ## Usage -### Setup - -First of all you have to obtain Stripe account [publishabe key](https://stripe.com/docs/keys). And then you need to set it for module. +To use the module, import it first. ```javascript import stripe from 'react-native-stripe-payments'; +``` + +### Setup + +First of all you have to obtain a Stripe account [publishable key](https://stripe.com/docs/keys), which you need to set it for the module. +```javascript stripe.setOptions({ publishingKey: 'STRIPE_PUBLISHING_KEY' }); ``` ### Validate the given card details ```javascript -import stripe from 'react-native-stripe-payments'; - const isCardValid = stripe.isCardValid({ number: '4242424242424242', expMonth: 10, @@ -42,27 +47,83 @@ const isCardValid = stripe.isCardValid({ cvc: '888', }); ``` +The argument for `isCardValid` is of type `CardParams`, which is used across the other APIs. -### One-time payments +### Set up a payment method for future payments (Setup Intent) ```javascript -import stripe from 'react-native-stripe-payments'; +stripe.confirmSetup('client_secret_from_backend', cardParams) + .then(result => { + // result of type SetupIntentResult + // { + // paymentMethodId, + // liveMode, + // last4, + // created, + // brand + // } + }) + .catch(err => + // error performing payment + ) +``` +The `brand` is the provider of the card, and we use the [module](https://www.npmjs.com/package/credit-card-type) `credit-card-type` to achieve that. +### One-time payment using the `id` of a `PaymentMethod` + +```javascript +stripe.confirmPaymentWithPaymentMethodId('client_secret_from_backend', paymentMethodId) + .then(result => { + // result of type PaymentResult + // { + // id, + // paymentMethodId + // } + }) + .catch(err => + // error performing payment + ) +``` + +### One-time payment using `cardParams` + +```javascript const cardDetails = { number: '4242424242424242', expMonth: 10, expYear: 21, cvc: '888', } -stripe.confirmPayment('client_secret_from_backend', cardDetails) +stripe.confirmPaymentWithCardParams('client_secret_from_backend', cardParams) .then(result => { // result of type PaymentResult + // { + // id, + // paymentMethodId + // } }) .catch(err => // error performing payment ) ``` -### Reusing cards +## Contributors ✨ + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + +

Luke Brandon Farrell

πŸ’» πŸš‡ πŸ“†

Chester Sim

πŸ“– πŸ’»

Amogh Jahagirdar

πŸ“– πŸ’»
+ + + + -Not supported yet, though as we're highly invested in development of our product which depends on this library we'll do it as soon as possible! +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index e0585e9..4a8496d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -49,6 +49,7 @@ android { targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) versionCode 1 versionName "1.0" + multiDexEnabled true } lintOptions { abortOnError false @@ -73,7 +74,8 @@ repositories { dependencies { //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules - implementation 'com.stripe:stripe-android:14.2.1' + implementation 'com.stripe:stripe-android:15.1.0' + implementation 'com.android.support:multidex:1.0.3' } def configureReactNativePom(def pom) { diff --git a/android/src/main/java/com/fitpassu/stripepayments/BridgeEphemeralKeyProvider.java b/android/src/main/java/com/fitpassu/stripepayments/BridgeEphemeralKeyProvider.java new file mode 100644 index 0000000..8c1702b --- /dev/null +++ b/android/src/main/java/com/fitpassu/stripepayments/BridgeEphemeralKeyProvider.java @@ -0,0 +1,90 @@ +package com.fitpassu.stripepayments; + +import com.stripe.android.EphemeralKeyProvider; +import com.stripe.android.EphemeralKeyUpdateListener; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.UiThreadUtil; + + +/** + * + * Ephemeral Key Provider that works with the JS thread through the bridge + * + */ +public class BridgeEphemeralKeyProvider implements EphemeralKeyProvider, EphemeralKeyUpdateListener { + private ReactApplicationContext reactContext; + private EphemeralKeyUpdateListener pendingKeyUpdateListener; //will hold the key update listener between the time when the event is raised and the JS responds back with the key + + //save the context for future use + BridgeEphemeralKeyProvider(ReactApplicationContext context) { + this.reactContext = context; + } + + @Override + public void createEphemeralKey( + String apiVersion, + final EphemeralKeyUpdateListener keyUpdateListener) { + + //build params to send to the JS thread + WritableMap params = Arguments.createMap(); + params.putString("apiVersion", apiVersion); + + this.pendingKeyUpdateListener = keyUpdateListener; + + //async call the JS land + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("stripeCreateEphemeralKey", params); + } + + /** + * + * Called from the JS land once we have the key + * + */ + @Override + public void onKeyUpdate(final String stripeResponseJson){ + if(this.pendingKeyUpdateListener != null) { + final EphemeralKeyUpdateListener keyUpdateListener = pendingKeyUpdateListener; + + // + // we need to make sure the listener is updated on the UI thread + // + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + keyUpdateListener.onKeyUpdate(stripeResponseJson); + }}); + + this.pendingKeyUpdateListener = null; //avoid memory leaks. the listener is not used anymore + } + } + + + /** + * + * Called from the JS land once we have the key + * + */ + @Override + public void onKeyUpdateFailure(final int responseCode, final String message) { + if(this.pendingKeyUpdateListener != null) { + final EphemeralKeyUpdateListener keyUpdateListener = pendingKeyUpdateListener; + + // + // we need to make sure the listener is updated on the UI thread + // + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + keyUpdateListener.onKeyUpdateFailure(responseCode, message); + }}); + + this.pendingKeyUpdateListener = null; //avoid memory leaks. the listener is not used anymore + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/fitpassu/stripepayments/StripePaymentsModule.java b/android/src/main/java/com/fitpassu/stripepayments/StripePaymentsModule.java index db2701b..6260df9 100644 --- a/android/src/main/java/com/fitpassu/stripepayments/StripePaymentsModule.java +++ b/android/src/main/java/com/fitpassu/stripepayments/StripePaymentsModule.java @@ -1,7 +1,15 @@ package com.fitpassu.stripepayments; +import java.lang.String; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + import android.app.Activity; import android.content.Intent; +import androidx.activity.ComponentActivity; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -9,8 +17,10 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.BaseActivityEventListener; +import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; import com.stripe.android.ApiResultCallback; @@ -21,25 +31,77 @@ import com.stripe.android.model.ConfirmPaymentIntentParams; import com.stripe.android.model.PaymentIntent; import com.stripe.android.model.PaymentMethodCreateParams; +import com.stripe.android.CustomerSession; +import com.stripe.android.PaymentSession; +import com.stripe.android.PaymentSessionConfig; +import com.stripe.android.PaymentSessionData; + +import com.stripe.android.model.SetupIntent; +import com.stripe.android.SetupIntentResult; +import com.stripe.android.model.ConfirmSetupIntentParams; +import com.stripe.android.model.PaymentMethod; +import com.stripe.android.model.PaymentMethod.BillingDetails; +import com.stripe.android.view.AddPaymentMethodActivityStarter; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import android.util.Log; public class StripePaymentsModule extends ReactContextBaseJavaModule { + private static final String TAG = "StripePayments"; private static ReactApplicationContext reactContext; + private BridgeEphemeralKeyProvider ephemeralKeyProvider; private Stripe stripe; - private Promise paymentPromise; - + private Promise paymentPromise, setupPromise, addCardPromise; + private PaymentSession paymentSession; + private final ActivityEventListener activityListener = new BaseActivityEventListener() { @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - if (paymentPromise == null || stripe == null) { - super.onActivityResult(activity, requestCode, resultCode, data); - return; + // + //Rewritten according to the official sample + //https://github.com/stripe-samples/sample-store-android/blob/5ab40e92dc9a2fb1881396dc5050e760d8e27c77/app/src/main/java/com/stripe/android/samplestore/PaymentActivity.kt#L151 + // + super.onActivityResult(activity, requestCode, resultCode, data); + + boolean isPaymentIntentResult = stripe != null && stripe.onPaymentResult(requestCode, data, new PaymentResultCallback(paymentPromise)); + if(!isPaymentIntentResult) { + boolean isSetupIntentResult = stripe != null && stripe.onSetupResult(requestCode, data, new SetupResultCallback(setupPromise)); + if (!isSetupIntentResult && data != null && paymentSession != null) { + paymentSession.handlePaymentData(requestCode, resultCode, data); + } } - boolean handled = stripe.onPaymentResult(requestCode, data, new PaymentResultCallback(paymentPromise)); - if (!handled) { - super.onActivityResult(activity, requestCode, resultCode, data); + + /** + * handle the add payment results (if needed) + * see: https://github.com/stripe/stripe-android/blob/02f0a75b11143fb9618b482b6b0e2f9b28a9953f/stripe/src/main/java/com/stripe/android/view/PaymentMethodsActivity.kt#L174 + */ + if(requestCode == AddPaymentMethodActivityStarter.REQUEST_CODE && addCardPromise != null) { + + AddPaymentMethodActivityStarter.Result result = AddPaymentMethodActivityStarter.Result.fromIntent(data); + + if(result instanceof AddPaymentMethodActivityStarter.Result.Success) { + + AddPaymentMethodActivityStarter.Result.Success successResult = (AddPaymentMethodActivityStarter.Result.Success) result; + + WritableMap map = convertPaymentMethod(successResult.getPaymentMethod()); + + addCardPromise.resolve(map); + } else if (result instanceof AddPaymentMethodActivityStarter.Result.Canceled) { + addCardPromise.reject("StripeModule.cancelled", "The user cancelled"); + } else if (result instanceof AddPaymentMethodActivityStarter.Result.Failure) { + + AddPaymentMethodActivityStarter.Result.Failure failureResult = (AddPaymentMethodActivityStarter.Result.Failure) result; + + addCardPromise.reject("StripeModule.failed", failureResult.getException().getMessage()); + } + else { + // no-op + } + + addCardPromise = null; //release it } } }; @@ -50,11 +112,13 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, context.addActivityEventListener(activityListener); reactContext = context; + + this.ephemeralKeyProvider = new BridgeEphemeralKeyProvider(reactContext); } @Override public String getName() { - return "StripePaymentsModule"; + return TAG; } @ReactMethod(isBlockingSynchronousMethod = true) @@ -67,7 +131,7 @@ public void init(String publishableKey) { @ReactMethod(isBlockingSynchronousMethod = true) public boolean isCardValid(ReadableMap cardParams) { - Card card = new Card.Builder( + Card card = new Card.Builder( cardParams.getString("number"), cardParams.getInt("expMonth"), cardParams.getInt("expYear"), @@ -78,7 +142,12 @@ public boolean isCardValid(ReadableMap cardParams) { } @ReactMethod - public void confirmPayment(String secret, ReadableMap cardParams, final Promise promise) { + public void confirmPaymentWithCardParams(String secret, ReadableMap cardParams, final Promise promise) { + stripe = new Stripe( + reactContext, + PaymentConfiguration.getInstance(reactContext).getPublishableKey() + ); + PaymentMethodCreateParams.Card card = new PaymentMethodCreateParams.Card( cardParams.getString("number"), cardParams.getInt("expMonth"), @@ -96,11 +165,30 @@ public void confirmPayment(String secret, ReadableMap cardParams, final Promise } paymentPromise = promise; + + stripe.confirmPayment(getCurrentActivity(), confirmParams); + } + + @ReactMethod + public void confirmPaymentWithPaymentMethodId(String secret, String paymentMethodId, final Promise promise) { stripe = new Stripe( reactContext, PaymentConfiguration.getInstance(reactContext).getPublishableKey() ); - stripe.confirmPayment(getCurrentActivity(), confirmParams); + + if (paymentMethodId == null) { + promise.reject("", "StripeModule.invalidPaymentIntentParams"); + return; + } + paymentPromise = promise; + + stripe.confirmPayment( + getCurrentActivity(), + ConfirmPaymentIntentParams.createWithPaymentMethodId( + paymentMethodId, + secret + ) + ); } private static final class PaymentResultCallback implements ApiResultCallback { @@ -126,7 +214,7 @@ public void onSuccess(PaymentIntentResult result) { } else if (status == PaymentIntent.Status.Canceled) { promise.reject("StripeModule.cancelled", ""); } else { - promise.reject("StripeModule.failed", status.toString()); + promise.reject("StripeModule.failed", paymentIntent.getLastPaymentError().getMessage()); } } @@ -135,4 +223,234 @@ public void onError(Exception e) { promise.reject("StripeModule.failed", e.toString()); } } + @ReactMethod + public void confirmSetup(String secret, ReadableMap cardParams, final Promise promise) { + PaymentMethodCreateParams.Card card = new PaymentMethodCreateParams.Card( + cardParams.getString("number"), + cardParams.getInt("expMonth"), + cardParams.getInt("expYear"), + cardParams.getString("cvc"), + null, + null + ); + PaymentMethod.BillingDetails billingDetails = (new PaymentMethod.BillingDetails.Builder()).setEmail(cardParams.getString("email")).build(); + // PaymentMethodCreateParams params = PaymentMethodCreateParams.create(card); + if(card == null){ + promise.reject("", "StripeModule.invalidCardParams"); + return; + } + PaymentMethodCreateParams params = PaymentMethodCreateParams.create(card, billingDetails); + + if (params == null) { + promise.reject("", "StripeModule.invalidSetupIntentParams"); + return; + } + ConfirmSetupIntentParams confirmParams = ConfirmSetupIntentParams.create(params, secret); + + + + setupPromise = promise; + stripe = new Stripe( + reactContext, + PaymentConfiguration.getInstance(reactContext).getPublishableKey() + ); + stripe.confirmSetupIntent(getCurrentActivity(), confirmParams); + + } + private static final class SetupResultCallback implements ApiResultCallback { + private final Promise promise; + + SetupResultCallback(Promise promise) { + this.promise = promise; + } + + @Override + public void onSuccess(SetupIntentResult result) { + SetupIntent setupIntent = result.getIntent(); + SetupIntent.Status status = setupIntent.getStatus(); + + if ( + status == SetupIntent.Status.Succeeded || + status == SetupIntent.Status.Processing + ) { + WritableMap map = Arguments.createMap(); + map.putString("id", setupIntent.getPaymentMethodId()); + map.putBoolean("liveMode", setupIntent.isLiveMode()); + map.putDouble("created", setupIntent.getCreated()); + promise.resolve(map); + } else if (status == SetupIntent.Status.Canceled) { + promise.reject("StripeModule.cancelled", ""); + } else { + promise.reject("StripeModule.failed", setupIntent.getLastSetupError().getMessage()); + } + } + @Override + public void onError(Exception e) { + promise.reject("StripeModule.failed", e.toString()); + } + } + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + constants.put("PaymentMethod", Collections.unmodifiableMap(new HashMap() { + { + for (PaymentMethod.Type type : PaymentMethod.Type.values()) { + put(type.code, type.name()); + } + } + })); + return constants; + } + + @ReactMethod + public void onEphemeralKeyUpdate(String rawKey) { + this.ephemeralKeyProvider.onKeyUpdate(rawKey); + } + + @ReactMethod + public void onEphemeralKeyUpdateFailure(Integer responseCode, String message) { + this.ephemeralKeyProvider.onKeyUpdateFailure(responseCode, message); + } + + + @ReactMethod + public void initCustomerSession() { + CustomerSession.initCustomerSession(reactContext, this.ephemeralKeyProvider); + } + + @ReactMethod + public void createPaymentSession(final ReadableArray paymentMethodTypes) { + + // + // Stripe Payment Session Listeners work only on the main thread + // + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + + PaymentSessionConfig.Builder config = new PaymentSessionConfig.Builder() + + // collect shipping information + .setShippingInfoRequired(false) + + // collect shipping method + .setShippingMethodsRequired(false); + + + //convert the JS enum to Stripe SDK enums + if (paymentMethodTypes != null) { + List result = new ArrayList(paymentMethodTypes.size()); + for (int i = 0; i < paymentMethodTypes.size(); i++) { + result.add(PaymentMethod.Type.valueOf(paymentMethodTypes.getString(i))); + } + + //TODO: if we set this, then on the fragment there won't be an +Add card option anymore + //investigate why that is and fix the bug + //config.setPaymentMethodTypes(result); + } + + paymentSession = new PaymentSession( + (ComponentActivity) getCurrentActivity(), + config.build() + ); + + paymentSession.init( + new PaymentSession.PaymentSessionListener() { + @Override + public void onCommunicatingStateChanged( + boolean isCommunicating + ) { + // update UI, such as hiding or showing a progress bar + } + + @Override + public void onError( + int errorCode, + String errorMessage + ) { + // handle error + } + + @Override + public void onPaymentSessionDataChanged( + PaymentSessionData data + ) { + if (data.isPaymentReadyToCharge()) { + final PaymentMethod paymentMethod = data.getPaymentMethod(); + + WritableMap map = convertPaymentMethod(paymentMethod); + + //async call the JS land + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("stripePaymentMethodSelected", map); + } + } + } + ); + } + }); + } + + @ReactMethod + public void presentPaymentMethodSelection(final String paymentMethodId, final Promise promise) { + + // + // Stripe Payment Session Listeners work only on the main thread + // + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + + paymentSession.presentPaymentMethodSelection(paymentMethodId); + } + }); + } + + @ReactMethod + public void addPaymentMethod(String paymentMethodType, final Promise promise) { + + addCardPromise = promise; + new AddPaymentMethodActivityStarter(getCurrentActivity()) + .startForResult(new AddPaymentMethodActivityStarter.Args.Builder() + .setPaymentMethodType(PaymentMethod.Type.valueOf(paymentMethodType)) + .setShouldAttachToCustomer(true) + + .build() + ); + } + + @ReactMethod + public void endCustomerSession() { + CustomerSession.endCustomerSession(); + } + + /** + * + * Convert a payment method to a JS object so it can be sent across the bridge + * + */ + protected WritableMap convertPaymentMethod(PaymentMethod paymentMethod) { + + //finish the promise; + WritableMap map = Arguments.createMap(); + map.putString("id", paymentMethod.id); + map.putDouble("created", paymentMethod.created); + map.putBoolean("liveMode", paymentMethod.liveMode); + + if(paymentMethod.card != null) { + + WritableMap cardMap = Arguments.createMap(); + cardMap.putString("brand", paymentMethod.card.brand.getCode()); //mimic the stripe.js model https://stripe.com/docs/api/cards/object#card_object-brand + cardMap.putInt("expiryMonth", paymentMethod.card.expiryMonth); + cardMap.putInt("expiryYear", paymentMethod.card.expiryYear); + cardMap.putString("funding", paymentMethod.card.funding); + cardMap.putString("last4", paymentMethod.card.last4); + + map.putMap("card", cardMap); + } + + return map; + } } diff --git a/android/src/main/java/com/fitpassu/stripepayments/StripePaymentsPackage.java b/android/src/main/java/com/fitpassu/stripepayments/StripePaymentsPackage.java index 4913afc..2547fed 100644 --- a/android/src/main/java/com/fitpassu/stripepayments/StripePaymentsPackage.java +++ b/android/src/main/java/com/fitpassu/stripepayments/StripePaymentsPackage.java @@ -3,6 +3,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.ArrayList; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; diff --git a/build/index.d.ts b/build/index.d.ts new file mode 100644 index 0000000..b685fd2 --- /dev/null +++ b/build/index.d.ts @@ -0,0 +1,103 @@ +import { EmitterSubscription } from "react-native"; +export interface InitParams { + publishingKey: string; +} +export interface CardParams { + number: string; + expMonth: number; + expYear: number; + cvc: string; +} +export interface PaymentResult { + id: string; + paymentMethodId: string; +} +export interface SetupIntentResult { + id: string; + exp_month: string; + exp_year: string; + live_mode: boolean; + last4: string; + created: number; + brand: string; +} +export declare type createEphemeralKeyCallback = (apiVersion: string) => Promise; +export declare enum PaymentMethodType { + Alipay = "Alipay", + AuBecsDebit = "AuBecsDebit", + BacsDebit = "BacsDebit", + Bancontact = "Bancontact", + Card = "Card", + CardPresent = "CardPresent", + Eps = "Eps", + Fpx = "Fpx", + Giropay = "Giropay", + Ideal = "Ideal", + Oxxo = "Oxxo", + P24 = "P24", + SepaDebit = "SepaDebit", + Sofort = "Sofort" +} +export interface PaymentMethod { + id: string; + created: number; + liveMode: boolean; + card?: { + brand: string; + brandDisplayName: string; + expiryMonth: number; + expiryYear: number; + funding: string; + last4: string; + }; +} +export declare const STRIPE_CANCELLED_ERROR_CODE = "StripeModule.cancelled"; +export declare type StripeEventName = "stripeCreateEphemeralKey" | "stripePaymentMethodSelected"; +export declare type StripeEvent = ""; +declare class Stripe { + _stripeInitialized: boolean; + ephemeralKeyListener?: EmitterSubscription; + setOptions: (options: InitParams) => void; + confirmPaymentWithCardParams(clientSecret: string, cardParams: CardParams): Promise; + confirmPaymentWithPaymentMethodId(clientSecret: string, paymentMethodId: string): Promise; + confirmSetup(clientSecret: string, cardParams: CardParams): Promise; + isCardValid(cardDetails: CardParams): boolean; + /*** + * + * Customer session management + * + */ + initCustomerSession(createEphemeralKey: createEphemeralKeyCallback): any; + /** + * + * Start a payment session for the customer session + * @param paymentMethodTypes + */ + createPaymentSession(paymentMethodTypes?: (PaymentMethodType.Card | PaymentMethodType.Fpx)[]): any; + /** + * + * According to this + * https://github.com/stripe/stripe-android/blob/master/stripe/src/main/java/com/stripe/android/view/PaymentMethodsAdapter.kt#L78 + * The only payment methods supported by the basic integration is currently Card, Fpx + * + * @param paymentMethodTypes + */ + presentPaymentMethodSelection(paymentMethodId?: string | null): any; + /** + * + * + * @param paymentMethodTypes + */ + addPaymentMethod(paymentMethodType?: PaymentMethodType.Card | PaymentMethodType.Fpx): Promise; + endCustomerSession(): any; + /** + * Event Listeners + */ + addListener(eventName: "stripePaymentMethodSelected", callback: (paymentMethod: PaymentMethod) => void): void; + /** + * + */ + removeListener(eventName: "stripePaymentMethodSelected", callback: (paymentMethod: PaymentMethod) => void): void; +} +declare const stripe: Stripe; +export default stripe; diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..2d05331 --- /dev/null +++ b/build/index.js @@ -0,0 +1,205 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { NativeModules } from 'react-native'; +import EventEmitter from "react-native/Libraries/vendor/emitter/EventEmitter"; +import creditCardType from 'credit-card-type'; +import { NativeEventEmitter, } from "react-native"; +import invariant from "invariant"; +var StripePayments = NativeModules.StripePayments; +export var PaymentMethodType; +(function (PaymentMethodType) { + PaymentMethodType["Alipay"] = "Alipay"; + PaymentMethodType["AuBecsDebit"] = "AuBecsDebit"; + PaymentMethodType["BacsDebit"] = "BacsDebit"; + PaymentMethodType["Bancontact"] = "Bancontact"; + PaymentMethodType["Card"] = "Card"; + PaymentMethodType["CardPresent"] = "CardPresent"; + PaymentMethodType["Eps"] = "Eps"; + PaymentMethodType["Fpx"] = "Fpx"; + PaymentMethodType["Giropay"] = "Giropay"; + PaymentMethodType["Ideal"] = "Ideal"; + PaymentMethodType["Oxxo"] = "Oxxo"; + PaymentMethodType["P24"] = "P24"; + PaymentMethodType["SepaDebit"] = "SepaDebit"; + PaymentMethodType["Sofort"] = "Sofort"; +})(PaymentMethodType || (PaymentMethodType = {})); +export var STRIPE_CANCELLED_ERROR_CODE = "StripeModule.cancelled"; +//inspired from the Keyboard module +//https://github.com/facebook/react-native/blob/master/Libraries/Components/Keyboard/Keyboard.js +//protect against people deciding to link only on platform from their react-native-config.js +var eventEmitter = StripePayments ? new NativeEventEmitter(StripePayments) : new EventEmitter(); //fallback to a dummy implementation, so the app does not crash on these platforms +var Stripe = /** @class */ (function () { + function Stripe() { + var _this = this; + this._stripeInitialized = false; + this.setOptions = function (options) { + if (_this._stripeInitialized) { + return; + } + StripePayments.init(options.publishingKey); + _this._stripeInitialized = true; + }; + } + Stripe.prototype.confirmPaymentWithCardParams = function (clientSecret, cardParams) { + return StripePayments.confirmPaymentWithCardParams(clientSecret, cardParams); + }; + Stripe.prototype.confirmPaymentWithPaymentMethodId = function (clientSecret, paymentMethodId) { + return StripePayments.confirmPaymentWithPaymentMethodId(clientSecret, paymentMethodId); + }; + Stripe.prototype.confirmSetup = function (clientSecret, cardParams) { + return __awaiter(this, void 0, void 0, function () { + var nativeSetupIntentResult, cardNumber, cardType, brand, setupIntentResult; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, StripePayments.confirmSetup(clientSecret, cardParams)]; + case 1: + nativeSetupIntentResult = _a.sent(); + cardNumber = cardParams.number; + cardType = creditCardType(cardNumber); + brand = ""; + if (cardType.length > 0) { + brand = cardType[0].type; + } + setupIntentResult = __assign({ exp_month: cardParams.expMonth, exp_year: cardParams.expYear, last4: cardNumber.substr(cardNumber.length - 4), brand: brand }, nativeSetupIntentResult); + return [2 /*return*/, setupIntentResult]; + } + }); + }); + }; + Stripe.prototype.isCardValid = function (cardDetails) { + return StripePayments.isCardValid(cardDetails) == true; + }; + /*** + * + * Customer session management + * + */ + Stripe.prototype.initCustomerSession = function (createEphemeralKey) { + var _this = this; + //we already have a listner setup, so remove it + //this can happen especially during dev when certain UI components refresh + if (this.ephemeralKeyListener) { + this.ephemeralKeyListener.remove(); + } + //we communicate with the native side with events, as that is the only way to be able + //to call the callback multiple times (each time the ephemeral key expires) + this.ephemeralKeyListener = eventEmitter.addListener("stripeCreateEphemeralKey", function (event) { return __awaiter(_this, void 0, void 0, function () { + var rawKey, e_1; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _b.trys.push([0, 2, , 3]); + return [4 /*yield*/, createEphemeralKey(event.apiVersion)]; + case 1: + rawKey = _b.sent(); + //while we use typescript and such errors will be determined at compile time + //if somebody is using the module in JS, the app will simply crash, so + //provide a better dev experience (as you see the red box) + invariant(rawKey, "EphemeralKey cannot be null"); + invariant(typeof rawKey === "string", "EphemeralKey needs to be a string"); + StripePayments.onEphemeralKeyUpdate(rawKey); + return [3 /*break*/, 3]; + case 2: + e_1 = _b.sent(); + StripePayments.onEphemeralKeyUpdateFailure(0, (_a = e_1 === null || e_1 === void 0 ? void 0 : e_1.message) !== null && _a !== void 0 ? _a : "UNKOWN ERROR"); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); }); + //this will internally call the CreateStripeEphemeralKey every time when needed + return StripePayments.initCustomerSession(); + }; + /** + * + * Start a payment session for the customer session + * @param paymentMethodTypes + */ + Stripe.prototype.createPaymentSession = function (paymentMethodTypes) { + if (paymentMethodTypes === void 0) { paymentMethodTypes = []; } + return StripePayments.createPaymentSession(paymentMethodTypes); + }; + /** + * + * According to this + * https://github.com/stripe/stripe-android/blob/master/stripe/src/main/java/com/stripe/android/view/PaymentMethodsAdapter.kt#L78 + * The only payment methods supported by the basic integration is currently Card, Fpx + * + * @param paymentMethodTypes + */ + Stripe.prototype.presentPaymentMethodSelection = function (paymentMethodId) { + if (paymentMethodId === void 0) { paymentMethodId = null; } + return StripePayments.presentPaymentMethodSelection(paymentMethodId); + }; + /** + * + * + * @param paymentMethodTypes + */ + Stripe.prototype.addPaymentMethod = function (paymentMethodType) { + if (paymentMethodType === void 0) { paymentMethodType = PaymentMethodType.Card; } + return StripePayments.addPaymentMethod(paymentMethodType); + }; + Stripe.prototype.endCustomerSession = function () { + if (this.ephemeralKeyListener) { + this.ephemeralKeyListener.remove(); //Removes the listener + } + return StripePayments.endCustomerSession(); + }; + Stripe.prototype.addListener = function (eventName, callback) { + return eventEmitter.addListener(eventName, callback); + }; + Stripe.prototype.removeListener = function (eventName, callback) { + return eventEmitter.removeListener(eventName, callback); + }; + return Stripe; +}()); +var stripe = new Stripe(); //helps with autocomplete in VS Code +export default stripe; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/build/index.js.map b/build/index.js.map new file mode 100644 index 0000000..fc8c643 --- /dev/null +++ b/build/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.tsx"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/build/usePaymentSession.d.ts b/build/usePaymentSession.d.ts new file mode 100644 index 0000000..6b0601f --- /dev/null +++ b/build/usePaymentSession.d.ts @@ -0,0 +1,7 @@ +import { PaymentMethod } from "./index"; +/** + * + * Manages the payment session and links it to React's lifecycle + * + */ +export default function usePaymentSession(): PaymentMethod | undefined; diff --git a/build/usePaymentSession.js b/build/usePaymentSession.js new file mode 100644 index 0000000..bf49eac --- /dev/null +++ b/build/usePaymentSession.js @@ -0,0 +1,20 @@ +import { useEffect, useState } from "react"; +import stripe from "./index"; +/** + * + * Manages the payment session and links it to React's lifecycle + * + */ +export default function usePaymentSession() { + var _a = useState(), paymentMethod = _a[0], setPaymentMethod = _a[1]; + useEffect(function () { + //create a listener for paymentMethod + stripe.addListener("stripePaymentMethodSelected", setPaymentMethod); + stripe.createPaymentSession(); + return function () { + return stripe.removeListener("stripePaymentMethodSelected", setPaymentMethod); + }; //when ui is destroyed, stop listening anymore to this session + }, []); + return paymentMethod; +} +//# sourceMappingURL=usePaymentSession.js.map \ No newline at end of file diff --git a/build/usePaymentSession.js.map b/build/usePaymentSession.js.map new file mode 100644 index 0000000..b645d05 --- /dev/null +++ b/build/usePaymentSession.js.map @@ -0,0 +1 @@ +{"version":3,"file":"usePaymentSession.js","sourceRoot":"","sources":["../lib/usePaymentSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,MAAyB,MAAM,SAAS,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,iBAAiB;IACjC,IAAA,KAAoC,QAAQ,EAAiB,EAA5D,aAAa,QAAA,EAAE,gBAAgB,QAA6B,CAAC;IAEpE,SAAS,CAAC;QACR,qCAAqC;QACrC,MAAM,CAAC,WAAW,CAAC,6BAA6B,EAAE,gBAAgB,CAAC,CAAC;QAEpE,MAAM,CAAC,oBAAoB,EAAE,CAAC;QAE9B,OAAO;YACL,OAAA,MAAM,CAAC,cAAc,CAAC,6BAA6B,EAAE,gBAAgB,CAAC;QAAtE,CAAsE,CAAC,CAAC,8DAA8D;IAC1I,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,aAAa,CAAC;AACvB,CAAC"} \ No newline at end of file diff --git a/ios/StripePayments.m b/ios/StripePayments.m index d0142c2..da31946 100644 --- a/ios/StripePayments.m +++ b/ios/StripePayments.m @@ -22,22 +22,31 @@ @implementation StripePayments return [NSString stringWithFormat:@"%@", @(result)]; } -RCT_EXPORT_METHOD(confirmPayment:(NSString *)secret cardParams:(NSDictionary *)cardParams resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(confirmSetup:(NSString *)clientSecret cardParams:(NSDictionary *)cardParams resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - // Collect card details - STPPaymentMethodCardParams *card = [[STPPaymentMethodCardParams alloc] init]; + + // Collect card params + STPCardParams *card = [[STPCardParams alloc] init]; card.number = [RCTConvert NSString:cardParams[@"number"]]; - card.expYear = [RCTConvert NSNumber:cardParams[@"expYear"]]; - card.expMonth = [RCTConvert NSNumber:cardParams[@"expMonth"]]; + card.expYear = [[RCTConvert NSNumber:cardParams[@"expYear"]] unsignedIntegerValue]; + card.expMonth = [[RCTConvert NSNumber:cardParams[@"expMonth"]] unsignedIntegerValue]; card.cvc = [RCTConvert NSString:cardParams[@"cvc"]]; - STPPaymentMethodParams *paymentMethodParams = [STPPaymentMethodParams paramsWithCard:card billingDetails:nil metadata:nil]; - STPPaymentIntentParams *paymentIntentParams = [[STPPaymentIntentParams alloc] initWithClientSecret:secret]; - paymentIntentParams.paymentMethodParams = paymentMethodParams; - paymentIntentParams.setupFutureUsage = @(STPPaymentIntentSetupFutureUsageOnSession); + RCTLogInfo(@"Message: %@", card); - // Submit the payment + // Collect the customer's email to know which customer the PaymentMethod belongs to + STPPaymentMethodBillingDetails *billingDetails = [[STPPaymentMethodBillingDetails alloc] init]; + billingDetails.email = [RCTConvert NSString:cardParams[@"email"]]; + billingDetails.address.postalCode = [RCTConvert NSString:cardParams[@"postalCode"]]; + + // Create SetupIntent confirm parameters with the above + STPPaymentMethodCardParams *paymentMethodCardParams =[[STPPaymentMethodCardParams alloc] initWithCardSourceParams:card]; + STPPaymentMethodParams *paymentMethodParams = [STPPaymentMethodParams paramsWithCard:paymentMethodCardParams billingDetails:billingDetails metadata:nil]; + STPSetupIntentConfirmParams *setupIntentConfirmParams = [[STPSetupIntentConfirmParams alloc] initWithClientSecret:clientSecret]; + setupIntentConfirmParams.paymentMethodParams = paymentMethodParams; + + // Submit payment intent STPPaymentHandler *paymentHandler = [STPPaymentHandler sharedHandler]; - [paymentHandler confirmPayment:paymentIntentParams withAuthenticationContext:self completion:^(STPPaymentHandlerActionStatus status, STPPaymentIntent *paymentIntent, NSError *error) { + [paymentHandler confirmSetupIntent:setupIntentConfirmParams withAuthenticationContext:self completion:^(STPPaymentHandlerActionStatus status, STPSetupIntent *setupIntent, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ switch (status) { case STPPaymentHandlerActionStatusFailed: { @@ -49,19 +58,79 @@ @implementation StripePayments break; } case STPPaymentHandlerActionStatusSucceeded: { + NSString *customerID = [setupIntent customerID]; + if (!customerID) { + customerID = [NSString string]; + } + resolve(@{ - @"id": paymentIntent.allResponseFields[@"id"], - @"paymentMethodId": paymentIntent.paymentMethodId + @"id": [setupIntent paymentMethodID], + @"created": [NSNumber numberWithDouble:[[setupIntent created] timeIntervalSince1970]], + @"liveMode": @([setupIntent livemode]), }); break; } default: + reject(@"StripeModule.unknown", error.localizedDescription, nil); break; } }); }]; } +RCT_EXPORT_METHOD(confirmPaymentWithPaymentMethodId:(NSString *)secret paymentMethodId:(NSString *)paymentMethodId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + // Creates the payment intent for 3D secure 2 + STPPaymentIntentParams *paymentIntentParams = [[STPPaymentIntentParams alloc] initWithClientSecret:secret]; + paymentIntentParams.paymentMethodId = [RCTConvert NSString:paymentMethodId]; + + [self stripeConfirmPayment:paymentIntentParams resolver:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(confirmPaymentWithCardParams:(NSString *)secret cardParams:(NSDictionary *)cardParams resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + // Collect card details + STPPaymentMethodCardParams *card = [[STPPaymentMethodCardParams alloc] init]; + card.number = [RCTConvert NSString:cardParams[@"number"]]; + card.expYear = [RCTConvert NSNumber:cardParams[@"expYear"]]; + card.expMonth = [RCTConvert NSNumber:cardParams[@"expMonth"]]; + card.cvc = [RCTConvert NSString:cardParams[@"cvc"]]; + STPPaymentMethodParams *paymentMethodParams = [STPPaymentMethodParams paramsWithCard:card billingDetails:nil metadata:nil]; + STPPaymentIntentParams *paymentIntentParams = [[STPPaymentIntentParams alloc] initWithClientSecret:secret]; + paymentIntentParams.paymentMethodParams = paymentMethodParams; + paymentIntentParams.setupFutureUsage = @(STPPaymentIntentSetupFutureUsageOnSession); + + [self stripeConfirmPayment:paymentIntentParams resolver:resolve rejecter:reject]; +} + +- (void) stripeConfirmPayment:(STPPaymentIntentParams *)paymentIntentParams resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject +{ + STPPaymentHandler *paymentHandler = [STPPaymentHandler sharedHandler]; + [paymentHandler confirmPayment:paymentIntentParams withAuthenticationContext:self completion:^(STPPaymentHandlerActionStatus status, STPPaymentIntent *paymentIntent, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + switch (status) { + case STPPaymentHandlerActionStatusFailed: { + reject(@"StripeModule.failed", error.localizedDescription, nil); + break; + } + case STPPaymentHandlerActionStatusCanceled: { + reject(@"StripeModule.cancelled", @"", nil); + break; + } + case STPPaymentHandlerActionStatusSucceeded: { + resolve(@{ + @"id": paymentIntent.allResponseFields[@"id"], + @"paymentMethodId": paymentIntent.paymentMethodId + }); + break; + } + default: + break; + } + }); + }]; +} + - (UIViewController *)authenticationPresentingViewController { return RCTPresentedViewController(); diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts new file mode 100644 index 0000000..7f1074a --- /dev/null +++ b/lib/declarations.d.ts @@ -0,0 +1,2 @@ +//until @types/react-native adds the new EventEmitter from vendor +declare module 'react-native/Libraries/vendor/emitter/EventEmitter'; \ No newline at end of file diff --git a/lib/index.tsx b/lib/index.tsx index 2065079..ece5971 100644 --- a/lib/index.tsx +++ b/lib/index.tsx @@ -1,4 +1,11 @@ import { NativeModules } from 'react-native'; +import EventEmitter from "react-native/Libraries/vendor/emitter/EventEmitter"; +import creditCardType from 'credit-card-type'; +import { + NativeEventEmitter, + EmitterSubscription, +} from "react-native"; +import invariant from "invariant"; const { StripePayments } = NativeModules; @@ -6,7 +13,7 @@ export interface InitParams { publishingKey: string } -export interface CardDetails { +export interface CardParams { number: string, expMonth: number, expYear: number, @@ -18,8 +25,67 @@ export interface PaymentResult { paymentMethodId: string, } +export interface SetupIntentResult { + id: string, + exp_month: string, + exp_year: string, + live_mode: boolean, + last4: string, + created: number, + brand: string +} + +export type createEphemeralKeyCallback = ( + apiVersion: string +) => Promise; + +export enum PaymentMethodType { + Alipay = "Alipay", + AuBecsDebit = "AuBecsDebit", + BacsDebit = "BacsDebit", + Bancontact = "Bancontact", + Card = "Card", + CardPresent = "CardPresent", + Eps = "Eps", + Fpx = "Fpx", + Giropay = "Giropay", + Ideal = "Ideal", + Oxxo = "Oxxo", + P24 = "P24", + SepaDebit = "SepaDebit", + Sofort = "Sofort", +} + +/* Partial implementation (Card only / for now) */ +export interface PaymentMethod { + id: string; + created: number; + liveMode: boolean; + card?: { + brand: string; + brandDisplayName: string; + expiryMonth: number; + expiryYear: number; + funding: string; + last4: string; + }; +} + +export const STRIPE_CANCELLED_ERROR_CODE = "StripeModule.cancelled"; + +//inspired from the Keyboard module +//https://github.com/facebook/react-native/blob/master/Libraries/Components/Keyboard/Keyboard.js +//protect against people deciding to link only on platform from their react-native-config.js +const eventEmitter = StripePayments ? new NativeEventEmitter(StripePayments) : new EventEmitter(); //fallback to a dummy implementation, so the app does not crash on these platforms + +export type StripeEventName = + | "stripeCreateEphemeralKey" + | "stripePaymentMethodSelected"; +export type StripeEvent = ""; + class Stripe { - _stripeInitialized = false + _stripeInitialized = false; + ephemeralKeyListener?: EmitterSubscription; setOptions = (options: InitParams) => { if (this._stripeInitialized) { return; } @@ -27,13 +93,143 @@ class Stripe { this._stripeInitialized = true; } - confirmPayment(clientSecret: string, cardDetails: CardDetails): Promise { - return StripePayments.confirmPayment(clientSecret, cardDetails) + confirmPaymentWithCardParams(clientSecret: string, cardParams: CardParams): Promise { + return StripePayments.confirmPaymentWithCardParams(clientSecret, cardParams) + } + + confirmPaymentWithPaymentMethodId(clientSecret: string, paymentMethodId: string): Promise { + return StripePayments.confirmPaymentWithPaymentMethodId(clientSecret, paymentMethodId); + } + + async confirmSetup(clientSecret: string, cardParams: CardParams): Promise{ + const nativeSetupIntentResult = await StripePayments.confirmSetup(clientSecret, cardParams); + const cardNumber = cardParams.number; + const cardType = creditCardType(cardNumber); + let brand = ""; + if (cardType.length > 0) { + brand = cardType[0].type; + } + let setupIntentResult = { + exp_month: cardParams.expMonth, + exp_year: cardParams.expYear, + last4: cardNumber.substr(cardNumber.length - 4), + brand: brand, + ...nativeSetupIntentResult + } + return setupIntentResult } - isCardValid(cardDetails: CardDetails): boolean { + isCardValid(cardDetails: CardParams): boolean { return StripePayments.isCardValid(cardDetails) == true; } + + /*** + * + * Customer session management + * + */ + initCustomerSession(createEphemeralKey: createEphemeralKeyCallback) { + //we already have a listner setup, so remove it + //this can happen especially during dev when certain UI components refresh + if (this.ephemeralKeyListener) { + this.ephemeralKeyListener.remove(); + } + + //we communicate with the native side with events, as that is the only way to be able + //to call the callback multiple times (each time the ephemeral key expires) + this.ephemeralKeyListener = eventEmitter.addListener( + "stripeCreateEphemeralKey", + async (event: { apiVersion: string }) => { + try { + const rawKey = await createEphemeralKey(event.apiVersion); + //while we use typescript and such errors will be determined at compile time + //if somebody is using the module in JS, the app will simply crash, so + //provide a better dev experience (as you see the red box) + invariant(rawKey, "EphemeralKey cannot be null"); + invariant( + typeof rawKey === "string", + "EphemeralKey needs to be a string" + ); + StripePayments.onEphemeralKeyUpdate(rawKey); + } catch (e) { + StripePayments.onEphemeralKeyUpdateFailure( + 0, + e?.message ?? "UNKOWN ERROR" + ); + } + } + ); + + //this will internally call the CreateStripeEphemeralKey every time when needed + return StripePayments.initCustomerSession(); + } + + /** + * + * Start a payment session for the customer session + * @param paymentMethodTypes + */ + createPaymentSession( + paymentMethodTypes: (PaymentMethodType.Card | PaymentMethodType.Fpx)[] = [] + ) { + return StripePayments.createPaymentSession(paymentMethodTypes); + } + + /** + * + * According to this + * https://github.com/stripe/stripe-android/blob/master/stripe/src/main/java/com/stripe/android/view/PaymentMethodsAdapter.kt#L78 + * The only payment methods supported by the basic integration is currently Card, Fpx + * + * @param paymentMethodTypes + */ + presentPaymentMethodSelection(paymentMethodId: string | null = null) { + return StripePayments.presentPaymentMethodSelection(paymentMethodId); + } + + /** + * + * + * @param paymentMethodTypes + */ + addPaymentMethod( + paymentMethodType: + | PaymentMethodType.Card + | PaymentMethodType.Fpx = PaymentMethodType.Card + ): Promise { + return StripePayments.addPaymentMethod(paymentMethodType); + } + + endCustomerSession() { + if (this.ephemeralKeyListener) { + this.ephemeralKeyListener.remove(); //Removes the listener + } + return StripePayments.endCustomerSession(); + } + + /** + * Event Listeners + */ + addListener( + eventName: "stripePaymentMethodSelected", + callback: (paymentMethod: PaymentMethod) => void + ): void; //while the native eventEmitter does return a EventSubscription, calling remove on it durin unmount will fail. This is how all @react-native-community/hooks work + addListener(eventName: StripeEventName, callback: (e: any) => void) { + return eventEmitter.addListener(eventName, callback); + } + + /** + * + */ + removeListener( + eventName: "stripePaymentMethodSelected", + callback: (paymentMethod: PaymentMethod) => void + ): void; + removeListener(eventName: StripeEventName, callback: (e: any) => void) { + return eventEmitter.removeListener(eventName, callback); + } } -export default new Stripe(); +const stripe = new Stripe(); //helps with autocomplete in VS Code + +export default stripe; diff --git a/lib/usePaymentSession.ts b/lib/usePaymentSession.ts new file mode 100644 index 0000000..d22d8ef --- /dev/null +++ b/lib/usePaymentSession.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; +import stripe, { PaymentMethod } from "./index"; + +/** + * + * Manages the payment session and links it to React's lifecycle + * + */ +export default function usePaymentSession() { + const [paymentMethod, setPaymentMethod] = useState(); + + useEffect(() => { + //create a listener for paymentMethod + stripe.addListener("stripePaymentMethodSelected", setPaymentMethod); + + stripe.createPaymentSession(); + + return () => + stripe.removeListener("stripePaymentMethodSelected", setPaymentMethod); //when ui is destroyed, stop listening anymore to this session + }, []); + + return paymentMethod; +} diff --git a/package.json b/package.json index 5e04191..952295b 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,12 @@ "devDependencies": { "@types/react": "^16.9.0", "@types/react-native": "^0.62.16", + "@types/invariant": "2.2.29", "react": "^16.9.0", "react-native": ">=0.60", "typescript": "^3.9.6" + }, + "dependencies": { + "credit-card-type": "^9.0.1" } } diff --git a/react-native-stripe-payments.podspec b/react-native-stripe-payments.podspec index 2f2f6a3..1f5d205 100644 --- a/react-native-stripe-payments.podspec +++ b/react-native-stripe-payments.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |s| s.homepage = "https://github.com/Fitpassu/react-native-stripe-payments" s.license = { :type => "MIT", :file => "LICENSE" } s.authors = { "Viktoras Laukevičius" => "viktoras.laukevicius@yahoo.com" } - s.platforms = { :ios => "11.0" } + s.platforms = { :ios => "10.0" } s.source = { :git => "https://github.com/Fitpassu/react-native-stripe-payments.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,c,m,swift}" diff --git a/tsconfig.json b/tsconfig.json index 37ddaf4..dd5b25e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,8 @@ "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true, "noUnusedParameters": true, - "skipLibCheck": true + "skipLibCheck": true, + "allowSyntheticDefaultImports": true }, "include": ["lib"], "exclude": ["node_modules", "build"] diff --git a/yarn.lock b/yarn.lock index 4d379b7..e079fcd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -870,6 +870,11 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/invariant@2.2.29": + version "2.2.29" + resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.29.tgz#aa845204cd0a289f65d47e0de63a6a815e30cc66" + integrity sha512-lRVw09gOvgviOfeUrKc/pmTiRZ7g7oDOU6OAutyuSHpm1/o2RaBQvRhgK8QEdu+FFuw/wnWb29A/iuxv9i8OpQ== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1543,6 +1548,11 @@ cosmiconfig@^5.0.5, cosmiconfig@^5.1.0: js-yaml "^3.13.1" parse-json "^4.0.0" +credit-card-type@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/credit-card-type/-/credit-card-type-9.0.1.tgz#b800ae4089ece5e9362626ff1bb44fb3bb8757ed" + integrity sha512-GtJr+IyIUN67fRXJGIFTw192kgAAneqZ0DDpKKM78Nh32jwfcuTVzJRe3xioECPZybiKHGIHGsbXg//Hu6PKSQ== + cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"