diff --git a/.gitignore b/.gitignore
index c5464d9..bab1edf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,7 @@ project.xcworkspace
build/
.idea
.gradle
+gradle/
local.properties
*.iml
diff --git a/README.md b/README.md
index 96cfce2..11df9d3 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,165 @@
-# react-native-braintree
+# @ekreative/react-native-braintree
## Getting started
-`$ npm install react-native-braintree --save`
+## Android Specific
+Add this to your `build.gradle`
-### Mostly automatic installation
+```groovy
+ repositories {
+ maven {
+ url "https://cardinalcommerce.bintray.com/android"
+ credentials {
+ username 'braintree-team-sdk@cardinalcommerce'
+ password '220cc9476025679c4e5c843666c27d97cfb0f951'
+ }
+ }
+ }
+```
+
+In Your `AndroidManifest.xml`, `android:allowBackup="false"` can be replaced `android:allowBackup="true"`, it is responsible for app backup.
+
+## iOS Specific
+```bash
+cd ios
+pod install
+```
+###### Configure a new URL scheme
+Add a bundle url scheme {BUNDLE_IDENTIFIER}.payments in your app Info via XCode or manually in the Info.plist. In your Info.plist, you should have something like:
+
+```xml
+CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLName
+ com.myapp
+ CFBundleURLSchemes
+
+ com.myapp.payments
+
+
+
+```
+###### Update your code
+In your `AppDelegate.m`:
+
+```objective-c
+#import "BraintreeCore.h"
+
+...
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+ ...
+ [BTAppSwitch setReturnURLScheme:self.paymentsURLScheme];
+}
+
+- (BOOL)application:(UIApplication *)application
+ openURL:(NSURL *)url
+ options:(NSDictionary *)options {
+
+ if ([url.scheme localizedCaseInsensitiveCompare:self.paymentsURLScheme] == NSOrderedSame) {
+ return [BTAppSwitch handleOpenURL:url options:options];
+ }
+
+ return [RCTLinkingManager application:application openURL:url options:options];
+}
+
+- (NSString *)paymentsURLScheme {
+ NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
+ return [NSString stringWithFormat:@"%@.%@", bundleIdentifier, @"payments"];
+}
+```
-`$ react-native link react-native-braintree`
## Usage
+
+##### Show PayPall module
+
```javascript
-import RNBraintree from 'react-native-braintree';
+import RNBraintree from '@ekreative/react-native-braintree';
+
+RNBraintree.showPayPalModule({
+ clientToken: 'CLIENT_TOKEN_GENERATED_ON_SERVER_SIDE',
+ amount: '1.0',
+ currencyCode: 'EUR'
+ })
+ .then(result => console.log(result))
+ .catch((error) => console.log(error));
+
-// TODO: What to do with the module?
-RNBraintree;
```
+
+##### Card tokenization
+```javascript
+import RNBraintree from '@ekreative/react-native-braintree';
+
+RNBraintree.tokenizeCard({
+ clientToken: 'CLIENT_TOKEN_GENERATED_ON_SERVER_SIDE',
+ number: '1111222233334444',
+ expirationMonth: '11',
+ expirationYear: '24',
+ cvv: '123',
+ postalCode: '',
+ })
+ .then(result => console.log(result))
+ .catch((error) => console.log(error));
+
+```
+##### Make Payment
+```javascript
+import RNBraintree from '@ekreative/react-native-braintree';
+
+RNBraintree.run3DSecureCheck({
+ clientToken: 'CLIENT_TOKEN_GENERATED_ON_SERVER_SIDE',
+ nonce: 'CARD_NONCE',
+ amount: '122.00',
+ email: 'email@mail.com',
+ firstname: '',
+ lastname: '',
+ phoneNumber: '',
+ streetAddress: '',
+ streetAddress2: '',
+ city: '',
+ region: '',
+ postalCode: '',
+ countryCode: ''
+ })
+ .then(result => console.log(result))
+ .catch((error) => console.log(error));
+
+```
+### iOS
+##### Get if Apple Pay available
+```javascript
+import RNBraintree from '@ekreative/react-native-braintree';
+
+console.log(RNBraintree.isApplePayAvailable())
+```
+##### Make payment using Apple Pay
+```javascript
+import RNBraintree from '@ekreative/react-native-braintree';
+
+RNBraintree.runApplePay({
+ clientToken: 'CLIENT_TOKEN_GENERATED_ON_SERVER_SIDE',
+ companyName: 'Company',
+ amount: '100.0',
+ currencyCode: 'EUR'
+ })
+ .then(result => console.log(result))
+ .catch((error) => console.log(error));
+```
+### Android
+##### Make payment using Google Pay
+```javascript
+import RNBraintree from '@ekreative/react-native-braintree';
+
+RNBraintree.runGooglePay({
+ clientToken: 'CLIENT_TOKEN_GENERATED_ON_SERVER_SIDE',
+ amount: '100.0',
+ currencyCode: 'EUR'
+ })
+ .then(result => console.log(result))
+ .catch((error) => console.log(error));
+```
\ No newline at end of file
diff --git a/android/build.gradle b/android/build.gradle
index fa45ded..eab6cb9 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -32,6 +32,7 @@ buildscript {
if (project == rootProject) {
repositories {
google()
+ jcenter()
}
dependencies {
// This should reflect the Gradle plugin version used by
@@ -66,12 +67,22 @@ repositories {
// Android JSC is installed from npm
url "$rootDir/../node_modules/jsc-android/dist"
}
+ maven {
+ url "https://cardinalcommerce.bintray.com/android"
+ credentials {
+ username 'braintree-team-sdk@cardinalcommerce'
+ password '220cc9476025679c4e5c843666c27d97cfb0f951'
+ }
+ }
google()
+ jcenter()
}
dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules
+ implementation 'com.braintreepayments.api:drop-in:4.6.0'
+ implementation 'com.google.android.gms:play-services-wallet:18.1.2'
}
def configureReactNativePom(def pom) {
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index aa2e057..6d7f97c 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -3,4 +3,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/main/java/com/ekreative/reactnativebraintree/RNBraintreeModule.java b/android/src/main/java/com/ekreative/reactnativebraintree/RNBraintreeModule.java
index 011bd8a..19e217c 100644
--- a/android/src/main/java/com/ekreative/reactnativebraintree/RNBraintreeModule.java
+++ b/android/src/main/java/com/ekreative/reactnativebraintree/RNBraintreeModule.java
@@ -2,18 +2,85 @@
package com.ekreative.reactnativebraintree;
+import android.app.Activity;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.braintreepayments.api.BraintreeFragment;
+import com.braintreepayments.api.Card;
+import com.braintreepayments.api.DataCollector;
+import com.braintreepayments.api.GooglePayment;
+import com.braintreepayments.api.PayPal;
+import com.braintreepayments.api.ThreeDSecure;
+import com.braintreepayments.api.dropin.DropInResult;
+import com.braintreepayments.api.exceptions.InvalidArgumentException;
+import com.braintreepayments.api.interfaces.BraintreeCancelListener;
+import com.braintreepayments.api.interfaces.BraintreeErrorListener;
+import com.braintreepayments.api.interfaces.BraintreeResponseListener;
+import com.braintreepayments.api.interfaces.ConfigurationListener;
+import com.braintreepayments.api.interfaces.PaymentMethodNonceCreatedListener;
+import com.braintreepayments.api.interfaces.ThreeDSecureLookupListener;
+import com.braintreepayments.api.models.CardBuilder;
+import com.braintreepayments.api.models.CardNonce;
+import com.braintreepayments.api.models.Configuration;
+import com.braintreepayments.api.models.GooglePaymentRequest;
+import com.braintreepayments.api.models.PayPalAccountNonce;
+import com.braintreepayments.api.models.PayPalRequest;
+import com.braintreepayments.api.models.PaymentMethodNonce;
+import com.braintreepayments.api.models.ThreeDSecureAdditionalInformation;
+import com.braintreepayments.api.models.ThreeDSecureLookup;
+import com.braintreepayments.api.models.ThreeDSecurePostalAddress;
+import com.braintreepayments.api.models.ThreeDSecureRequest;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
-import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.BaseActivityEventListener;
+import com.facebook.react.bridge.WritableMap;
+import com.google.android.gms.wallet.PaymentData;
+import com.google.android.gms.wallet.TransactionInfo;
+import com.google.android.gms.wallet.WalletConstants;
+
+import static android.app.Activity.RESULT_OK;
+import static android.app.Activity.RESULT_CANCELED;
public class RNBraintreeModule extends ReactContextBaseJavaModule {
- private final ReactApplicationContext reactContext;
+ private Promise mPromise;
+ private BraintreeFragment mBraintreeFragment;
+ private String mDeviceData;
+ private static final int GOOGLE_PAYMENT_REQUEST_CODE = 79129;
public RNBraintreeModule(ReactApplicationContext reactContext) {
super(reactContext);
- this.reactContext = reactContext;
+
+ reactContext.addActivityEventListener(new BaseActivityEventListener() {
+ @Override
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(activity, requestCode, resultCode, data);
+ if (requestCode == GOOGLE_PAYMENT_REQUEST_CODE && resultCode == RESULT_OK) {
+ GooglePayment.tokenize(mBraintreeFragment, PaymentData.getFromIntent(data));
+ }
+
+ if (requestCode == GOOGLE_PAYMENT_REQUEST_CODE && resultCode == RESULT_CANCELED) {
+ mPromise.reject("USER_CANCELLATION", "The user cancelled");
+ }
+
+ // FIXME: PaymentMethodNonceCreatedListener doesn't call when using ThreeDSecureRequest.VERSION_2
+ // SOURCE: https://github.com/braintree/braintree_android/issues/268#issuecomment-567268780
+
+ final int unmaskedRequestCode = requestCode & 0x0000ffff;
+ boolean isBraintreeFragment = mBraintreeFragment != null && mBraintreeFragment.getTag().indexOf("Braintree") != -1;
+
+ if (isBraintreeFragment) {
+ mBraintreeFragment.onActivityResult(unmaskedRequestCode, resultCode, data);
+ }
+ }
+ });
}
@Override
@@ -22,8 +89,251 @@ public String getName() {
}
@ReactMethod
- public void sampleMethod(String stringArgument, int numberArgument, Callback callback) {
- // TODO: Implement some actually useful functionality
- callback.invoke("Received numberArgument: " + numberArgument + " stringArgument: " + stringArgument);
+ public void showPayPalModule(final ReadableMap parameters, final Promise promise) {
+ mPromise = promise;
+
+ if (!parameters.hasKey("clientToken")) {
+ promise.reject("You must provide a clientToken");
+ } else {
+ setup(parameters.getString("clientToken"));
+
+ String currency = "USD";
+ if (!parameters.hasKey("amount")) {
+ promise.reject("You must provide a amount");
+ }
+ if (parameters.hasKey("currencyCode")) {
+ currency = parameters.getString("currencyCode");
+ }
+
+ try {
+ PayPalRequest request = new PayPalRequest(parameters.getString("amount"))
+ .currencyCode(currency)
+ .intent(PayPalRequest.INTENT_AUTHORIZE);
+ PayPal.requestOneTimePayment(mBraintreeFragment, request);
+ } catch (Exception e) {
+ promise.reject(e.getMessage());
+ }
+ }
+ }
+
+ @ReactMethod
+ public void requestPayPalBillingAgreement(final ReadableMap parameters, final Promise promise) {
+ mPromise = promise;
+
+ if (!parameters.hasKey("clientToken")) {
+ promise.reject("MISSING_CLIENT_TOKEN", "You must provide a clientToken");
+ }
+
+ setup(parameters.getString("clientToken"));
+
+ try {
+ String description = parameters.hasKey("description") ? parameters.getString("description") : "";
+ String localeCode = parameters.hasKey("localeCode") ? parameters.getString("localeCode") : "US";
+
+ PayPalRequest request = new PayPalRequest()
+ .localeCode(localeCode)
+ .billingAgreementDescription(description);
+
+ PayPal.requestBillingAgreement(mBraintreeFragment, request);
+ } catch (Exception e) {
+ promise.reject("REQUEST_BILLING_AGREEMENT_FAILED", e.getMessage());
+ }
+
+ }
+
+ @ReactMethod
+ public void runGooglePay(final ReadableMap parameters, final Promise promise) {
+ mPromise = promise;
+
+ if (!parameters.hasKey("clientToken")) {
+ promise.reject("You must provide a clientToken");
+ } else {
+ setup(parameters.getString("clientToken"));
+
+ String currency = "USD";
+ if (!parameters.hasKey("amount")) {
+ promise.reject("You must provide a amount");
+ }
+ if (parameters.hasKey("currencyCode")) {
+ currency = parameters.getString("currencyCode");
+ }
+
+ try {
+ GooglePaymentRequest googlePaymentRequest = new GooglePaymentRequest()
+ .transactionInfo(TransactionInfo.newBuilder()
+ .setCurrencyCode(currency)
+ .setTotalPrice(parameters.getString("amount"))
+ .setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_FINAL)
+ .build())
+ .billingAddressRequired(true);
+
+ if (parameters.hasKey("merchantId")) {
+ String merchantId = parameters.getString("merchantId");
+ googlePaymentRequest.googleMerchantId(merchantId);
+ String env = "test".equals(merchantId) ? "TEST" : "PRODUCTION";
+ googlePaymentRequest.environment(env);
+ }
+
+ GooglePayment.requestPayment(mBraintreeFragment, googlePaymentRequest);
+ } catch (Exception e) {
+ promise.reject(e.getMessage());
+ }
+ }
+ }
+
+ @ReactMethod
+ public void tokenizeCard(final ReadableMap parameters, final Promise promise) {
+ mPromise = promise;
+
+ if (!parameters.hasKey("clientToken")) {
+ promise.reject("You must provide a clientToken");
+ } else {
+ setup(parameters.getString("clientToken"));
+
+ try {
+ CardBuilder cardBuilder = new CardBuilder().validate(false);
+
+ if (parameters.hasKey("number")) {
+ cardBuilder.cardNumber(parameters.getString("number"));
+ }
+
+ if (parameters.hasKey("expirationMonth")) {
+ cardBuilder.expirationMonth(parameters.getString("expirationMonth"));
+ }
+
+ if (parameters.hasKey("expirationYear")) {
+ cardBuilder.expirationYear(parameters.getString("expirationYear"));
+ }
+
+ if (parameters.hasKey("cvv")) {
+ cardBuilder.cvv(parameters.getString("cvv"));
+ }
+
+ if (parameters.hasKey("postalCode")) {
+ cardBuilder.postalCode(parameters.getString("postalCode"));
+ }
+
+ Card.tokenize(mBraintreeFragment, cardBuilder);
+ } catch (Exception e) {
+ promise.reject(e.getMessage());
+ }
+ }
+ }
+
+ @ReactMethod
+ public void run3DSecureCheck(final ReadableMap parameters, final Promise promise) {
+ mPromise = promise;
+
+ if (!parameters.hasKey("clientToken")) {
+ promise.reject("You must provide a clientToken");
+ }
+
+ try {
+ setup(parameters.getString("clientToken"));
+
+ ThreeDSecurePostalAddress address = new ThreeDSecurePostalAddress();
+
+ if (parameters.hasKey("firstname")) {
+ address.givenName(parameters.getString("firstname"));
+ }
+
+ if (parameters.hasKey("lastname")) {
+ address.surname(parameters.getString("lastname"));
+ }
+
+ if (parameters.hasKey("phoneNumber")) {
+ address.phoneNumber(parameters.getString("phoneNumber"));
+ }
+
+ if (parameters.hasKey("countryCode")) {
+ address.countryCodeAlpha2(parameters.getString("countryCode"));
+ }
+
+ if (parameters.hasKey("city")) {
+ address.locality(parameters.getString("city"));
+ }
+
+ if (parameters.hasKey("postalCode")) {
+ address.postalCode(parameters.getString("postalCode"));
+ }
+
+ if (parameters.hasKey("region")) {
+ address.region(parameters.getString("region"));
+ }
+
+ if (parameters.hasKey("streetAddress")) {
+ address.streetAddress(parameters.getString("streetAddress"));
+ }
+
+ if (parameters.hasKey("streetAddress2")) {
+ address.extendedAddress(parameters.getString("streetAddress2"));
+ }
+
+ // For best results, provide as many additional elements as possible.
+ ThreeDSecureAdditionalInformation additionalInformation = new ThreeDSecureAdditionalInformation()
+ .shippingAddress(address);
+
+ ThreeDSecureRequest threeDSecureRequest = new ThreeDSecureRequest()
+ .nonce(parameters.getString("nonce"))
+ .email(parameters.getString("email"))
+ .billingAddress(address)
+ .versionRequested(ThreeDSecureRequest.VERSION_2)
+ .additionalInformation(additionalInformation)
+ .amount(parameters.getString("amount"));
+
+ ThreeDSecure.performVerification(mBraintreeFragment, threeDSecureRequest, new ThreeDSecureLookupListener() {
+ @Override
+ public void onLookupComplete(ThreeDSecureRequest request, ThreeDSecureLookup lookup) {
+ // Optionally inspect the lookup result and prepare UI if a challenge is required
+ ThreeDSecure.continuePerformVerification(mBraintreeFragment, request, lookup);
+ }
+ });
+ } catch (Exception e) {
+ promise.reject(e.getMessage());
+ }
+ }
+
+ public void setup(final String token) {
+ if (mBraintreeFragment == null) {
+ try {
+ mBraintreeFragment = BraintreeFragment.newInstance((AppCompatActivity) getCurrentActivity(), token);
+ mBraintreeFragment.addListener(new BraintreeCancelListener() {
+ @Override
+ public void onCancel(int requestCode) {
+ mPromise.reject("USER_CANCELLATION", "The user cancelled");
+ }
+ });
+ mBraintreeFragment.addListener(new PaymentMethodNonceCreatedListener() {
+ @Override
+ public void onPaymentMethodNonceCreated(PaymentMethodNonce paymentMethodNonce) {
+
+ WritableMap map = Arguments.createMap();
+ if (paymentMethodNonce instanceof PayPalAccountNonce) {
+ PayPalAccountNonce payPalAccountNonce = (PayPalAccountNonce) paymentMethodNonce;
+
+ // Access additional information
+ String email = payPalAccountNonce.getEmail();
+ map.putString("email", email);
+
+ }
+ map.putString("nonce", paymentMethodNonce.getNonce());
+ sendResult(map);
+ }
+ });
+ DataCollector.collectDeviceData(mBraintreeFragment, new BraintreeResponseListener() {
+ @Override
+ public void onResponse(String deviceData) {
+ mDeviceData = deviceData;
+ }
+ });
+ } catch (Exception e) {
+ mPromise.reject(e.getMessage());
+ }
+ }
+ }
+
+ public void sendResult(final WritableMap result) {
+ result.putString("deviceData", mDeviceData);
+ mPromise.resolve(result);
}
}
diff --git a/android/src/main/java/com/ekreative/reactnativebraintree/RNBraintreePackage.java b/android/src/main/java/com/ekreative/reactnativebraintree/RNBraintreePackage.java
index 4737232..0346f50 100644
--- a/android/src/main/java/com/ekreative/reactnativebraintree/RNBraintreePackage.java
+++ b/android/src/main/java/com/ekreative/reactnativebraintree/RNBraintreePackage.java
@@ -10,6 +10,7 @@
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
+import com.facebook.react.bridge.JavaScriptModule;
public class RNBraintreePackage implements ReactPackage {
@Override
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..2ab2a69
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,56 @@
+declare module '@ekreative/react-native-braintree' {
+ interface BraintreeResponse {
+ nonce: string;
+ deviceData: string;
+ }
+
+ interface BraintreeOptions {
+ clientToken: string;
+ amount: string;
+ currencyCode: string;
+ }
+
+ interface Run3DSecureCheckOptions
+ extends Omit {
+ nonce: string;
+ email: string;
+ firstname: string;
+ lastname: string;
+ phoneNumber: string;
+ streetAddress: string;
+ streetAddress2?: string;
+ city: string;
+ region?: string;
+ postalCode: string;
+ countryCode: string;
+ }
+
+ interface TokenizeCardOptions {
+ clientToken: string;
+ number: string;
+ expirationMonth: string;
+ expirationYear: string;
+ cvv: string;
+ postalCode?: string;
+ }
+
+ interface RunApplePayOptions extends BraintreeOptions {
+ companyName: string;
+ }
+
+ // Export
+
+ interface RNBraintreeModule {
+ showPayPalModule(options: BraintreeOptions): Promise;
+ runGooglePay(options: BraintreeOptions): Promise;
+ run3DSecureCheck(
+ options: Run3DSecureCheckOptions,
+ ): Promise;
+ tokenizeCard(options: TokenizeCardOptions): Promise;
+ runApplePay(options: RunApplePayOptions): Promise;
+ }
+
+ const RNBraintree: RNBraintreeModule;
+
+ export default RNBraintree;
+}
diff --git a/index.js b/index.js
index 9bcf729..57b0cc0 100644
--- a/index.js
+++ b/index.js
@@ -3,5 +3,12 @@
import { NativeModules } from 'react-native';
const { RNBraintree } = NativeModules;
+const { RNBraintreeApplePay } = NativeModules;
-export default RNBraintree;
+export default {
+ showPayPalModule: RNBraintree.showPayPalModule,
+ runGooglePay: RNBraintree.runGooglePay,
+ run3DSecureCheck: RNBraintree.run3DSecureCheck,
+ tokenizeCard: RNBraintree.tokenizeCard,
+ runApplePay: RNBraintreeApplePay && RNBraintreeApplePay.runApplePay,
+}
diff --git a/ios/RNBraintree.m b/ios/RNBraintree.m
index c362701..14e80c3 100644
--- a/ios/RNBraintree.m
+++ b/ios/RNBraintree.m
@@ -1,16 +1,200 @@
-// RNBraintree.m
-
#import "RNBraintree.h"
+#import "BraintreeCore.h"
+#import "BTCardClient.h"
+#import "BraintreePayPal.h"
+#import "BTDataCollector.h"
+#import "BraintreePaymentFlow.h"
+@interface RNBraintree()
+@property (nonatomic, strong) BTAPIClient *apiClient;
+@property (nonatomic, strong) BTDataCollector *dataCollector;
+@end
@implementation RNBraintree
RCT_EXPORT_MODULE()
-RCT_EXPORT_METHOD(sampleMethod:(NSString *)stringArgument numberParameter:(nonnull NSNumber *)numberArgument callback:(RCTResponseSenderBlock)callback)
-{
- // TODO: Implement some actually useful functionality
- callback(@[[NSString stringWithFormat: @"numberArgument: %@ stringArgument: %@", numberArgument, stringArgument]]);
+RCT_EXPORT_METHOD(showPayPalModule: (NSDictionary *)options
+ resolver: (RCTPromiseResolveBlock)resolve
+ rejecter: (RCTPromiseRejectBlock)reject) {
+ NSString *clientToken = options[@"clientToken"];
+ NSString *amount = options[@"amount"];
+ NSString *currencyCode = options[@"currencyCode"];
+
+ self.apiClient = [[BTAPIClient alloc] initWithAuthorization: clientToken];
+ self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:self.apiClient];
+ BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient: self.apiClient];
+ payPalDriver.viewControllerPresentingDelegate = self;
+
+ BTPayPalRequest *request= [[BTPayPalRequest alloc] initWithAmount:amount];
+ request.currencyCode = currencyCode;
+ [payPalDriver requestOneTimePayment:request completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) {
+ if (error) {
+ reject(@"ONE_TIME_PAYMENT_FAILED", error.localizedDescription, nil);
+ return;
+ }
+ if (!tokenizedPayPalAccount) {
+ reject(@"ONE_TIME_PAYMENT_CANCELLED", @"Payment has been cancelled", nil);
+ return;
+ }
+ [self.dataCollector collectDeviceData:^(NSString * _Nonnull deviceData) {
+ resolve(@{@"deviceData": deviceData,
+ @"email": tokenizedPayPalAccount.email,
+ @"nonce": tokenizedPayPalAccount.nonce,});
+ }];
+ }];
+}
+
+RCT_EXPORT_METHOD(requestPayPalBillingAgreement: (NSDictionary *)options
+ resolver: (RCTPromiseResolveBlock)resolve
+ rejecter: (RCTPromiseRejectBlock)reject) {
+ NSString *clientToken = options[@"clientToken"];
+ NSString *description = options[@"description"];
+
+ self.apiClient = [[BTAPIClient alloc] initWithAuthorization: clientToken];
+ self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:self.apiClient];
+ BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient: self.apiClient];
+ payPalDriver.viewControllerPresentingDelegate = self;
+
+ BTPayPalRequest *request= [[BTPayPalRequest alloc] init];
+ if (description) {
+ request.billingAgreementDescription = description;
+ }
+ [payPalDriver requestBillingAgreement:request completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) {
+ if (error) {
+ reject(@"REQUEST_BILLING_AGREEMENT_FAILED", error.localizedDescription, nil);
+ return;
+ }
+ if (!tokenizedPayPalAccount) {
+ reject(@"REQUEST_BILLING_AGREEMENT_CANCELLED", @"Request billing agreement has been cancelled", nil);
+ return;
+ }
+ [self.dataCollector collectDeviceData:^(NSString * _Nonnull deviceData) {
+ resolve(@{@"deviceData": deviceData,
+ @"nonce": tokenizedPayPalAccount.nonce,});
+ }];
+ }];
+}
+
+RCT_EXPORT_METHOD(tokenizeCard: (NSDictionary *)parameters
+ resolver: (RCTPromiseResolveBlock)resolve
+ rejecter: (RCTPromiseRejectBlock)reject) {
+
+ NSString *clientToken = parameters[@"clientToken"];
+ self.apiClient = [[BTAPIClient alloc] initWithAuthorization: clientToken];
+ self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:self.apiClient];
+ BTCardClient *cardClient = [[BTCardClient alloc] initWithAPIClient: self.apiClient];
+
+ BTCard *card = [[BTCard alloc] initWithNumber:parameters[@"number"]
+ expirationMonth:parameters[@"expirationMonth"]
+ expirationYear:parameters[@"expirationYear"]
+ cvv:parameters[@"cvv"]];
+
+ card.postalCode = parameters[@"postalCode"];
+ card.shouldValidate = NO;
+
+ [cardClient tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) {
+ if (error) {
+ reject(@"TOKENIZE_FAILED", error.localizedDescription, nil);
+ return;
+ }
+ [self.dataCollector collectDeviceData:^(NSString * _Nonnull deviceData) {
+ resolve(@{@"deviceData": deviceData,
+ @"nonce": tokenizedCard.nonce,});
+ }];
+ }];
+}
+
+RCT_EXPORT_METHOD(run3DSecureCheck: (NSDictionary *)parameters
+ resolver: (RCTPromiseResolveBlock)resolve
+ rejecter: (RCTPromiseRejectBlock)reject) {
+ [self startPaymentFlow:parameters
+ resolver:resolve
+ rejecter:reject];
+}
+
+#pragma mark - 3D Secure
+- (void)startPaymentFlow: (NSDictionary *)parameters
+ resolver: (RCTPromiseResolveBlock)resolve
+ rejecter: (RCTPromiseRejectBlock)reject {
+ NSString *clientToken = parameters[@"clientToken"];
+ self.apiClient = [[BTAPIClient alloc] initWithAuthorization: clientToken];
+ self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:self.apiClient];
+
+ BTThreeDSecureRequest *threeDSecureRequest = [[BTThreeDSecureRequest alloc] init];
+ threeDSecureRequest.amount = [NSDecimalNumber decimalNumberWithString: parameters[@"amount"]];
+ threeDSecureRequest.nonce = parameters[@"nonce"];
+ threeDSecureRequest.threeDSecureRequestDelegate = self;
+ threeDSecureRequest.email = parameters[@"email"];
+ threeDSecureRequest.versionRequested = BTThreeDSecureVersion2;
+
+ BTThreeDSecurePostalAddress *address = [BTThreeDSecurePostalAddress new];
+ address.givenName = parameters[@"firstname"]; // ASCII-printable characters required, else will throw a validation error
+ address.surname = parameters[@"lastname"]; // ASCII-printable characters required, else will throw a validation error
+ address.phoneNumber = parameters[@"phoneNumber"];
+ address.streetAddress = parameters[@"streetAddress"];
+ address.extendedAddress = parameters[@"streetAddress2"];
+ address.locality = parameters[@"city"];
+ address.region = parameters[@"region"];
+ address.postalCode = parameters[@"postalCode"];
+ address.countryCodeAlpha2 = parameters[@"countryCode"];
+
+ BTThreeDSecureAdditionalInformation *additionalInformation = [BTThreeDSecureAdditionalInformation new];
+ additionalInformation.shippingAddress = address;
+
+ threeDSecureRequest.additionalInformation = additionalInformation;
+ threeDSecureRequest.billingAddress = address;
+
+ BTPaymentFlowDriver *paymentFlowDriver = [[BTPaymentFlowDriver alloc] initWithAPIClient:self.apiClient];
+ paymentFlowDriver.viewControllerPresentingDelegate = self;
+
+ [paymentFlowDriver startPaymentFlow:threeDSecureRequest completion:^(BTPaymentFlowResult * _Nonnull result, NSError * _Nonnull error) {
+ if (error) {
+ reject(@"PAYMENT_FAILED", error.localizedDescription, nil);
+ return;
+ }
+ BTThreeDSecureResult *threeDSecureResult = (BTThreeDSecureResult *)result;
+ if (!threeDSecureResult.tokenizedCard.threeDSecureInfo.liabilityShiftPossible && threeDSecureResult.tokenizedCard.threeDSecureInfo.wasVerified) {
+ reject(@"3DSECURE_NOT_ABLE_TO_SHIFT_LIABILITY", @"3D Secure liability cannot be shifted", nil);
+ return;
+ }
+ if (!threeDSecureResult.tokenizedCard.threeDSecureInfo.liabilityShifted && threeDSecureResult.tokenizedCard.threeDSecureInfo.wasVerified) {
+ reject(@"3DSECURE_LIABILITY_NOT_SHIFTED", @"3D Secure liability was not shifted", nil);
+ }
+ if (!threeDSecureResult.tokenizedCard.nonce) {
+ reject(@"PAYMENT_3D_SECURE_FAILED", @"Something went wrong", nil);
+ return;
+ }
+ [self.dataCollector collectDeviceData:^(NSString * _Nonnull deviceData) {
+ resolve(@{@"deviceData": deviceData,
+ @"nonce": threeDSecureResult.tokenizedCard.nonce});
+ }];
+ return;
+ }];
+}
+
+#pragma mark - BTViewControllerPresentingDelegate
+- (void)paymentDriver:(nonnull id)driver requestsPresentationOfViewController:(nonnull UIViewController *)viewController {
+ [self.reactRoot presentViewController:viewController animated:YES completion:nil];
+}
+
+- (void)paymentDriver:(nonnull id)driver requestsDismissalOfViewController:(nonnull UIViewController *)viewController {
+ [viewController dismissViewControllerAnimated:YES completion:nil];
+}
+
+#pragma mark - BTThreeDSecureRequestDelegate
+
+- (void)onLookupComplete:(BTThreeDSecureRequest *)request result:(BTThreeDSecureLookup *)result next:(void (^)(void))next {
+ next();
+}
+
+#pragma mark - RootController
+- (UIViewController*)reactRoot {
+ UIViewController *topViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
+ if (topViewController.presentedViewController) {
+ topViewController = topViewController.presentedViewController;
+ }
+ return topViewController;
}
@end
diff --git a/ios/RNBraintreeApplePay.h b/ios/RNBraintreeApplePay.h
new file mode 100644
index 0000000..26b10d8
--- /dev/null
+++ b/ios/RNBraintreeApplePay.h
@@ -0,0 +1,5 @@
+#import
+
+@interface RNBraintreeApplePay : NSObject
+
+@end
diff --git a/ios/RNBraintreeApplePay.m b/ios/RNBraintreeApplePay.m
new file mode 100644
index 0000000..3753b7f
--- /dev/null
+++ b/ios/RNBraintreeApplePay.m
@@ -0,0 +1,127 @@
+@import PassKit;
+#import "RNBraintreeApplePay.h"
+#import "BraintreeCore.h"
+#import "BTDataCollector.h"
+#import "BraintreePaymentFlow.h"
+#import "BraintreeApplePay.h"
+
+@interface RNBraintreeApplePay()
+
+@property (nonatomic, strong) BTAPIClient *apiClient;
+@property (nonatomic, strong) BTDataCollector *dataCollector;
+@property (nonatomic, strong) RCTPromiseResolveBlock resolve;
+@property (nonatomic, strong) RCTPromiseRejectBlock reject;
+@property (nonatomic, assign) BOOL isApplePaymentAuthorized;
+
+@end
+
+@implementation RNBraintreeApplePay
+
+RCT_EXPORT_MODULE()
+
+RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isApplePayAvailable) {
+ BOOL canMakePayments = [PKPaymentAuthorizationViewController canMakePaymentsUsingNetworks:
+ @[PKPaymentNetworkVisa, PKPaymentNetworkMasterCard, PKPaymentNetworkAmex, PKPaymentNetworkDiscover]];
+ return [NSNumber numberWithBool:canMakePayments];
+}
+
+RCT_EXPORT_METHOD(runApplePay: (NSDictionary *)options
+ resolver: (RCTPromiseResolveBlock)resolve
+ rejecter: (RCTPromiseRejectBlock)reject) {
+ NSString *companyName = options[@"companyName"];
+ NSString *amount = options[@"amount"];
+ NSString *clientToken = options[@"clientToken"];
+ NSString *currencyCode = options[@"currencyCode"];
+ if (!companyName) {
+ reject(@"NO_COMPANY_NAME", @"You must provide a `companyName`", nil);
+ return;
+ }
+ if (!amount) {
+ reject(@"NO_TOTAL_PRICE", @"You must provide a `amount`", nil);
+ return;
+ }
+
+ self.apiClient = [[BTAPIClient alloc] initWithAuthorization: clientToken];
+ self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:self.apiClient];
+
+ BTApplePayClient *applePayClient = [[BTApplePayClient alloc] initWithAPIClient: self.apiClient];
+
+ [applePayClient paymentRequest:^(PKPaymentRequest * _Nullable paymentRequest, NSError * _Nullable error) {
+ if (error) {
+ reject(@"APPLE_PAY_PAYMENT_REQUEST_FAILED", error.localizedDescription, nil);
+ return;
+ }
+
+ if (@available(iOS 11.0, *)) {
+ paymentRequest.requiredBillingContactFields = [NSSet setWithObject:PKContactFieldPostalAddress];
+ }
+ paymentRequest.merchantCapabilities = PKMerchantCapability3DS;
+ paymentRequest.paymentSummaryItems = @[
+ [PKPaymentSummaryItem summaryItemWithLabel:companyName amount:[NSDecimalNumber decimalNumberWithString:amount]]
+ ];
+ paymentRequest.currencyCode = currencyCode;
+ self.resolve = resolve;
+ self.reject = reject;
+ [self setIsApplePaymentAuthorized:NO];
+ PKPaymentAuthorizationViewController *paymentController = [[PKPaymentAuthorizationViewController alloc] initWithPaymentRequest:paymentRequest];
+ paymentController.delegate = self;
+ [[self reactRoot] presentViewController:paymentController animated:YES completion:NULL];
+ }];
+}
+
+- (void)handleTokenizationResult: (BTApplePayCardNonce *)tokenizedApplePayPayment
+ error: (NSError *)error
+ completion: (void (^)(PKPaymentAuthorizationStatus))completion{
+ if (!tokenizedApplePayPayment) {
+ self.reject(error.localizedDescription, error.localizedDescription, error);
+ completion(PKPaymentAuthorizationStatusFailure);
+ [self resetPaymentResolvers];
+ return;
+ }
+ [self.dataCollector collectDeviceData:^(NSString * _Nonnull deviceData) {
+ self.resolve(@{@"deviceData": deviceData,
+ @"nonce": tokenizedApplePayPayment.nonce});
+ completion(PKPaymentAuthorizationStatusSuccess);
+ [self resetPaymentResolvers];
+ }];
+}
+
+- (void)resetPaymentResolvers {
+ self.resolve = NULL;
+ self.reject = NULL;
+}
+
+#pragma mark - PKPaymentAuthorizationViewControllerDelegate
+- (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller
+ didAuthorizePayment:(PKPayment *)payment
+ completion:(void (^)(PKPaymentAuthorizationStatus))completion {
+ [self setIsApplePaymentAuthorized: YES];
+ BTApplePayClient *applePayClient = [[BTApplePayClient alloc] initWithAPIClient:self.apiClient];
+ [applePayClient tokenizeApplePayPayment:payment
+ completion:^(BTApplePayCardNonce *tokenizedApplePayPayment, NSError *error) {
+ [self handleTokenizationResult:tokenizedApplePayPayment error:error completion:completion];
+ }];
+}
+
+- (void)paymentAuthorizationViewControllerDidFinish:(nonnull PKPaymentAuthorizationViewController *)controller {
+ [controller dismissViewControllerAnimated:YES completion:NULL];
+ if (self.reject && [self isApplePaymentAuthorized]) {
+ self.reject(@"APPLE_PAY_FAILED", @"Apple Pay failed", nil);
+ }
+ if (self.isApplePaymentAuthorized == NO) {
+ self.reject(@"USER_CANCELLATION", @"The user cancelled", nil);
+ }
+ [self resetPaymentResolvers];
+ self.isApplePaymentAuthorized = NULL;
+}
+
+#pragma mark - RootController
+- (UIViewController*)reactRoot {
+ UIViewController *topViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
+ if (topViewController.presentedViewController) {
+ topViewController = topViewController.presentedViewController;
+ }
+ return topViewController;
+}
+
+@end
diff --git a/package.json b/package.json
index 706b845..91e06ba 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "react-native-braintree",
+ "name": "@ekreative/react-native-braintree",
"title": "React Native Braintree",
"version": "1.0.0",
"description": "TODO",
@@ -8,6 +8,7 @@
"README.md",
"android",
"index.js",
+ "index.d.ts",
"ios",
"react-native-braintree.podspec"
],
diff --git a/react-native-braintree.podspec b/react-native-braintree.podspec
index 8432bb0..d54347c 100644
--- a/react-native-braintree.podspec
+++ b/react-native-braintree.podspec
@@ -24,7 +24,10 @@ Pod::Spec.new do |s|
s.requires_arc = true
s.dependency "React"
- # ...
- # s.dependency "..."
+ s.dependency "Braintree"
+ s.dependency "Braintree/DataCollector"
+ s.dependency "Braintree/PaymentFlow"
+ s.dependency "Braintree/Apple-Pay"
+
end