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 @@

+
+[](#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)):
+
+
+
+
+
+
+
+
+
-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"