diff --git a/README.md b/README.md index 06eb1088..cd30b69e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ IOS ```bash npm install react-native-braintree-dropin-ui --save +OR + +yarn add react-native-braintree-dropin-ui + cd ./ios pod install ``` @@ -22,11 +26,15 @@ pod install Android ```bash npm install react-native-braintree-dropin-ui --save + +OR + +yarn add react-native-braintree-dropin-u ``` ## Configurate Payment Method(For ALL RN VERSIONS) -See Braintree's documentation, [Apple Pay][8], [Google Pay][9], [Paypal][10], [Venmo][11] -Once you have finished setting up all the configurations, it will shows in the dropin UI. +See Braintree's documentation, [Apple Pay][7], [Google Pay][8], [Paypal][9], [Venmo][10] +Once you have finished setting up all the configurations, it will show in the dropin UI. For React Native versions < 0.60 @@ -38,7 +46,7 @@ react-native link react-native-braintree-dropin-ui #### iOS specific -You must have a iOS deployment target \>= 9.0. +You must have a iOS deployment target \>= 12.0. If you don't have a Podfile or are unsure on how to proceed, see the [CocoaPods][1] usage guide. @@ -58,7 +66,7 @@ pod 'Braintree' pod 'BraintreeDropIn' # comment the next line to disable Apple pay -pod 'Braintree/Apple-Pay' +pod 'Braintree/ApplePay' # comment the next line to disable PayPal pod 'Braintree/PayPal' @@ -80,22 +88,33 @@ pod install #### Apple Pay -The Drop-in will show Apple Pay as a payment option as long as you've completed the [Apple Pay integration][6] and the customer's [device and card type are supported][7]. +The Drop-in will show Apple Pay as a payment option as long as you've completed the [Apple Pay integration][5] and the customer's [device and card type are supported][6]. #### PayPal -To enable paypal payments in iOS, you will need to add `setReturnURLScheme` to `launchOptions` of your `AppDelegate.m` +To enable paypal payments in iOS, you will need to add `setReturnURLScheme` to `launchOptions` of your `AppDelegate.m` / `AppDelegate.mm` ```objective-c - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [BTAppSwitch setReturnURLScheme:@"com.your-company-name.your-app-name.payments"]; // ADD THIS LINE + [BTAppContextSwitcher setReturnURLScheme:@"com.your-company-name.your-app-name.payments"]; // ADD THIS LINE return YES; } ``` #### Android specific -Note: Only complete these steps if using React Native versions < 0.60, autolinking will do these steps automatically. +Add in your `MainActivity.java`: +``` + import tech.power.RNBraintreeDropIn.RNBraintreeDropInModule; + + @Override + protected void onCreate(Bundle savedInstanceState) { + // ... + RNBraintreeDropInModule.initDropInClient(this); + } +``` + +Note: Only complete the next steps if using React Native versions < 0.60, autolinking will do these steps automatically. Add in your `app/build.gradle`: @@ -182,7 +201,7 @@ In your `AppDelegate.m`: - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... - [BTAppSwitch setReturnURLScheme:self.paymentsURLScheme]; + [BTAppContextSwitcher setReturnURLScheme:self.paymentsURLScheme]; ... } @@ -191,7 +210,7 @@ In your `AppDelegate.m`: options:(NSDictionary *)options { if ([url.scheme localizedCaseInsensitiveCompare:self.paymentsURLScheme] == NSOrderedSame) { - return [BTAppSwitch handleOpenURL:url options:options]; + return [BTAppContextSwitcher handleOpenURL:url]; } return [RCTLinkingManager application:application openURL:url options:options]; @@ -210,13 +229,13 @@ import Braintree func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { ... - BTAppSwitch.setReturnURLScheme(self.paymentsURLScheme) + BTAppContextSwitcher.setReturnURLScheme(self.paymentsURLScheme) ... } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { if let scheme = url.scheme, scheme.localizedCaseInsensitiveCompare(self.paymentsURLScheme) == .orderedSame { - return BTAppSwitch.handleOpen(url, options: options) + return BTAppContextSwitcher.handleOpen(url) } return RCTLinkingManager.application(app, open: url, options: options) } @@ -227,14 +246,9 @@ private var paymentsURLScheme: String { } ``` -##### Android - -Setup [browser switch][4]. - - ## Usage -For the API, see the [Flow typings][5]. +For the API, see the [Flow typings][4]. ### Basic @@ -252,6 +266,7 @@ BraintreeDropIn.show({ googlePay: true, applePay: true, vaultManager: true, + payPal: true, cardDisabled: false, darkTheme: true, }) @@ -284,6 +299,7 @@ BraintreeDropIn.show({ googlePay: true, applePay: true, vaultManager: true, + payPal: true, cardDisabled: false, darkTheme: true, }) @@ -298,6 +314,36 @@ BraintreeDropIn.show({ }); ``` +### Fetch more recent payment method + +```javascript +import BraintreeDropIn from 'react-native-braintree-dropin-ui'; + +BraintreeDropIn.fetchMostRecentPaymentMethod(clientToken) +.then(result => console.log(result)) +.catch((error) => { + // Handle error +}); +``` + +### Tokenize card + +```javascript +import BraintreeDropIn from 'react-native-braintree-dropin-ui'; + +BraintreeDropIn.tokenizeCard(clientToken, { + number: '4111111111111111', + expirationMonth: '10', + expirationYear: '23', + cvv: '123', + postalCode: '12345', +}) +.then(cardNonce => console.log(cardNonce)) +.catch((error) => { + // Handle error +}); +``` + ### Custom Fonts ``` BraintreeDropIn.show({ @@ -306,14 +352,14 @@ BraintreeDropIn.show({ boldFontFamily: 'Averta-Semibold', }) ``` + [1]: http://guides.cocoapods.org/using/using-cocoapods.html [2]: https://github.com/braintree/braintree-ios-drop-in [3]: https://github.com/braintree/braintree-android-drop-in -[4]: https://developers.braintreepayments.com/guides/client-sdk/setup/android/v2#browser-switch-setup -[5]: ./index.js.flow -[6]: https://developers.braintreepayments.com/guides/apple-pay/configuration/ios/v4 -[7]: https://articles.braintreepayments.com/guides/payment-methods/apple-pay#compatibility -[8]: https://developers.braintreepayments.com/guides/apple-pay/overview -[9]: https://developers.braintreepayments.com/guides/google-pay/overview -[10]: https://developers.braintreepayments.com/guides/paypal/overview/ios/v4 -[11]: https://developers.braintreepayments.com/guides/venmo/overview +[4]: ./index.js.flow +[5]: https://developers.braintreepayments.com/guides/apple-pay/configuration/ios/v5 +[6]: https://articles.braintreepayments.com/guides/payment-methods/apple-pay#compatibility +[7]: https://developers.braintreepayments.com/guides/apple-pay/overview +[8]: https://developers.braintreepayments.com/guides/google-pay/overview +[9]: https://developers.braintreepayments.com/guides/paypal/overview/ios/v5 +[10]: https://developers.braintreepayments.com/guides/venmo/overview diff --git a/ios/RNBraintreeDropIn.podspec b/RNBraintreeDropIn.podspec similarity index 57% rename from ios/RNBraintreeDropIn.podspec rename to RNBraintreeDropIn.podspec index 88352688..3bfda253 100644 --- a/ios/RNBraintreeDropIn.podspec +++ b/RNBraintreeDropIn.podspec @@ -1,22 +1,22 @@ Pod::Spec.new do |s| s.name = "RNBraintreeDropIn" - s.version = "1.0.0" + s.version = "1.1.6" s.summary = "RNBraintreeDropIn" s.description = <<-DESC RNBraintreeDropIn DESC s.homepage = "https://github.com/bamlab/react-native-braintree-payments-drop-in" s.license = "MIT" - # s.license = { :type => "MIT", :file => "../LICENSE" } + # s.license = { :type => "MIT", :file => "./LICENSE" } s.author = { "author" => "lagrange.louis@gmail.com" } - s.platform = :ios, "9.0" + s.platform = :ios, "12.0" s.source = { :git => "https://github.com/BradyShober/react-native-braintree-dropin-ui.git", :tag => "master" } - s.source_files = "*.{h,m}" + s.source_files = "ios/**/*.{h,m}" s.requires_arc = true s.dependency 'React' - s.dependency 'Braintree' - s.dependency 'BraintreeDropIn' - s.dependency 'Braintree/DataCollector' - s.dependency 'Braintree/Apple-Pay' - s.dependency 'Braintree/Venmo' + s.dependency 'Braintree', '5.20.1' + s.dependency 'BraintreeDropIn', '9.8.1' + s.dependency 'Braintree/DataCollector', '5.20.1' + s.dependency 'Braintree/ApplePay', '5.20.1' + s.dependency 'Braintree/Venmo', '5.20.1' end diff --git a/android/build.gradle b/android/build.gradle index 318eac5f..25d0106d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,23 +1,23 @@ buildscript { repositories { - jcenter() google() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:7.3.1' } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 - buildToolsVersion '28.0.3' + compileSdkVersion 33 + buildToolsVersion '33.0.1' defaultConfig { minSdkVersion 21 - targetSdkVersion 27 + targetSdkVersion 33 versionCode 1 versionName "1.0" } @@ -32,10 +32,7 @@ repositories { } dependencies { - implementation 'com.braintreepayments.api:google-payment:3.2.0' - implementation 'com.google.android.gms:play-services-wallet:16.0.1' - implementation 'com.braintreepayments.api:data-collector:2.+' - implementation 'com.braintreepayments.api:drop-in:4.+' + implementation 'com.braintreepayments.api:drop-in:6.11.0' implementation 'com.facebook.react:react-native:+' } @@ -43,10 +40,10 @@ dependencies { rootProject.allprojects { repositories { maven { - url "https://cardinalcommerce.bintray.com/android" + url "https://cardinalcommerceprod.jfrog.io/artifactory/android" credentials { - username 'braintree-team-sdk@cardinalcommerce' - password '220cc9476025679c4e5c843666c27d97cfb0f951' + username 'braintree_team_sdk' + password 'AKCp8jQcoDy2hxSWhDAUQKXLDPDx6NYRkqrgFLRc3qDrayg6rrCbJpsKKyMwaykVL8FWusJpp' } } } diff --git a/android/node_modules-react-native-braintree-dropin-ui-android-react-native-braintree-dropin-ui~1.iml b/android/node_modules-react-native-braintree-dropin-ui-android-react-native-braintree-dropin-ui~1.iml deleted file mode 100644 index 187164d4..00000000 --- a/android/node_modules-react-native-braintree-dropin-ui-android-react-native-braintree-dropin-ui~1.iml +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/src/main/java/tech/power/RNBraintreeDropIn/RNBraintreeDropInModule.java b/android/src/main/java/tech/power/RNBraintreeDropIn/RNBraintreeDropInModule.java index 8fdd4849..9d8a9dd9 100644 --- a/android/src/main/java/tech/power/RNBraintreeDropIn/RNBraintreeDropInModule.java +++ b/android/src/main/java/tech/power/RNBraintreeDropIn/RNBraintreeDropInModule.java @@ -1,36 +1,53 @@ package tech.power.RNBraintreeDropIn; import android.app.Activity; -import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; + +import com.braintreepayments.api.BraintreeClient; +import com.braintreepayments.api.Card; +import com.braintreepayments.api.CardClient; +import com.braintreepayments.api.DropInClient; +import com.braintreepayments.api.DropInListener; +import com.braintreepayments.api.DropInPaymentMethod; +import com.braintreepayments.api.ThreeDSecureRequest; +import com.braintreepayments.api.UserCanceledException; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.ActivityEventListener; -import com.facebook.react.bridge.BaseActivityEventListener; import com.facebook.react.bridge.Promise; -import com.braintreepayments.api.dropin.DropInActivity; -import com.braintreepayments.api.dropin.DropInRequest; -import com.braintreepayments.api.dropin.DropInResult; -import com.braintreepayments.api.models.PaymentMethodNonce; -import com.braintreepayments.api.models.CardNonce; -import com.braintreepayments.api.models.ThreeDSecureInfo; -import com.braintreepayments.api.models.GooglePaymentRequest; +import com.braintreepayments.api.DropInRequest; +import com.braintreepayments.api.DropInResult; +import com.braintreepayments.api.PaymentMethodNonce; +import com.braintreepayments.api.CardNonce; +import com.braintreepayments.api.ThreeDSecureInfo; +import com.braintreepayments.api.GooglePayRequest; import com.google.android.gms.wallet.TransactionInfo; import com.google.android.gms.wallet.WalletConstants; -public class RNBraintreeDropInModule extends ReactContextBaseJavaModule { - - private Promise mPromise; - private static final int DROP_IN_REQUEST = 0x444; +import java.util.Objects; +public class RNBraintreeDropInModule extends ReactContextBaseJavaModule { private boolean isVerifyingThreeDSecure = false; + private static DropInClient dropInClient = null; + private static String clientToken = null; + + public static void initDropInClient(FragmentActivity activity) { + dropInClient = new DropInClient(activity, callback -> { + if (clientToken != null) { + callback.onSuccess(clientToken); + } else { + callback.onFailure(new Exception("Client token is null")); + } + }); + } public RNBraintreeDropInModule(ReactApplicationContext reactContext) { super(reactContext); - reactContext.addActivityEventListener(mActivityListener); } @ReactMethod @@ -42,106 +59,205 @@ public void show(final ReadableMap options, final Promise promise) { return; } - Activity currentActivity = getCurrentActivity(); + FragmentActivity currentActivity = (FragmentActivity) getCurrentActivity(); if (currentActivity == null) { promise.reject("NO_ACTIVITY", "There is no current activity"); return; } - DropInRequest dropInRequest = new DropInRequest().clientToken(options.getString("clientToken")); + DropInRequest dropInRequest = new DropInRequest(); if(options.hasKey("vaultManager")) { - dropInRequest.vaultManager(options.getBoolean("vaultManager")); + dropInRequest.setVaultManagerEnabled(options.getBoolean("vaultManager")); } - dropInRequest.collectDeviceData(true); - - if(options.getBoolean("googlePay")){ - GooglePaymentRequest googlePaymentRequest = new GooglePaymentRequest() - .transactionInfo(TransactionInfo.newBuilder() - .setTotalPrice(options.getString("orderTotal")) + if(options.hasKey("googlePay") && options.getBoolean("googlePay")){ + GooglePayRequest googlePayRequest = new GooglePayRequest(); + googlePayRequest.setTransactionInfo(TransactionInfo.newBuilder() + .setTotalPrice(Objects.requireNonNull(options.getString("orderTotal"))) .setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_FINAL) - .setCurrencyCode(options.getString("currencyCode")) - .build()) - .billingAddressRequired(true) - .googleMerchantId(options.getString("googlePayMerchantId")); + .setCurrencyCode(Objects.requireNonNull(options.getString("currencyCode"))) + .build()); + googlePayRequest.setBillingAddressRequired(true); + googlePayRequest.setGoogleMerchantId(options.getString("googlePayMerchantId")); - dropInRequest.googlePaymentRequest(googlePaymentRequest); + dropInRequest.setGooglePayDisabled(false); + dropInRequest.setGooglePayRequest(googlePayRequest); }else{ - dropInRequest.disableGooglePayment(); + dropInRequest.setGooglePayDisabled(true); } - // if(options.hasKey("cardDisabled")) { - // dropInRequest.disableCard(); - // } + if(options.hasKey("cardDisabled")) { + dropInRequest.setCardDisabled(options.getBoolean("cardDisabled")); + } if (options.hasKey("threeDSecure")) { final ReadableMap threeDSecureOptions = options.getMap("threeDSecure"); - if (!threeDSecureOptions.hasKey("amount")) { + if (threeDSecureOptions == null || !threeDSecureOptions.hasKey("amount")) { promise.reject("NO_3DS_AMOUNT", "You must provide an amount for 3D Secure"); return; } isVerifyingThreeDSecure = true; - dropInRequest - .amount(String.valueOf(threeDSecureOptions.getDouble("amount"))) - .requestThreeDSecureVerification(true); - } + ThreeDSecureRequest threeDSecureRequest = new ThreeDSecureRequest(); + threeDSecureRequest.setAmount(threeDSecureOptions.getString("amount")); - mPromise = promise; - currentActivity.startActivityForResult(dropInRequest.getIntent(currentActivity), DROP_IN_REQUEST); - } + dropInRequest.setThreeDSecureRequest(threeDSecureRequest); + } - private final ActivityEventListener mActivityListener = new BaseActivityEventListener() { - @Override - public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); + dropInRequest.setPayPalDisabled(!options.hasKey("payPal") || !options.getBoolean("payPal")); - if (requestCode != DROP_IN_REQUEST || mPromise == null) { - return; - } + clientToken = options.getString("clientToken"); - if (resultCode == Activity.RESULT_OK) { - DropInResult result = data.getParcelableExtra(DropInResult.EXTRA_DROP_IN_RESULT); - PaymentMethodNonce paymentMethodNonce = result.getPaymentMethodNonce(); - String deviceData = result.getDeviceData(); + if (dropInClient == null) { + promise.reject( + "DROP_IN_CLIENT_UNINITIALIZED", + "Did you forget to call RNBraintreeDropInModule.initDropInClient(this) in MainActivity.onCreate?" + ); + return; + } + dropInClient.setListener(new DropInListener() { + @Override + public void onDropInSuccess(@NonNull DropInResult dropInResult) { + PaymentMethodNonce paymentMethodNonce = dropInResult.getPaymentMethodNonce(); if (isVerifyingThreeDSecure && paymentMethodNonce instanceof CardNonce) { CardNonce cardNonce = (CardNonce) paymentMethodNonce; ThreeDSecureInfo threeDSecureInfo = cardNonce.getThreeDSecureInfo(); if (!threeDSecureInfo.isLiabilityShiftPossible()) { - mPromise.reject("3DSECURE_NOT_ABLE_TO_SHIFT_LIABILITY", "3D Secure liability cannot be shifted"); + promise.reject("3DSECURE_NOT_ABLE_TO_SHIFT_LIABILITY", "3D Secure liability cannot be shifted"); } else if (!threeDSecureInfo.isLiabilityShifted()) { - mPromise.reject("3DSECURE_LIABILITY_NOT_SHIFTED", "3D Secure liability was not shifted"); + promise.reject("3DSECURE_LIABILITY_NOT_SHIFTED", "3D Secure liability was not shifted"); } else { - resolvePayment(paymentMethodNonce, deviceData); + resolvePayment(dropInResult, promise); } } else { - resolvePayment(paymentMethodNonce, deviceData); + resolvePayment(dropInResult, promise); + } + } + + @Override + public void onDropInFailure(@NonNull Exception exception) { + if (exception instanceof UserCanceledException) { + promise.reject("USER_CANCELLATION", "The user cancelled"); + } else { + promise.reject(exception.getMessage(), exception.getMessage()); } - } else if (resultCode == Activity.RESULT_CANCELED) { - mPromise.reject("USER_CANCELLATION", "The user cancelled"); + } + }); + dropInClient.launchDropIn(dropInRequest); + } + + @ReactMethod + public void fetchMostRecentPaymentMethod(final String clientToken, final Promise promise) { + FragmentActivity currentActivity = (FragmentActivity) getCurrentActivity(); + + if (currentActivity == null) { + promise.reject("NO_ACTIVITY", "There is no current activity"); + return; + } + + if (dropInClient == null) { + promise.reject( + "DROP_IN_CLIENT_UNINITIALIZED", + "Did you forget to call RNBraintreeDropInModule.initDropInClient(this) in MainActivity.onCreate?" + ); + return; + } + + RNBraintreeDropInModule.clientToken = clientToken; + + dropInClient.fetchMostRecentPaymentMethod(currentActivity, (dropInResult, error) -> { + if (error != null) { + promise.reject(error.getMessage(), error.getMessage()); + } else if (dropInResult == null) { + promise.reject("NO_DROP_IN_RESULT", "dropInResult is null"); } else { - Exception exception = (Exception) data.getSerializableExtra(DropInActivity.EXTRA_ERROR); - mPromise.reject(exception.getMessage(), exception.getMessage()); + resolvePayment(dropInResult, promise); } + }); + } + + @ReactMethod + public void tokenizeCard(final String clientToken, final ReadableMap cardInfo, final Promise promise) { + if (clientToken == null) { + promise.reject("NO_CLIENT_TOKEN", "You must provide a client token"); + return; + } + + if ( + !cardInfo.hasKey("number") || + !cardInfo.hasKey("expirationMonth") || + !cardInfo.hasKey("expirationYear") || + !cardInfo.hasKey("cvv") || + !cardInfo.hasKey("postalCode") + ) { + promise.reject("INVALID_CARD_INFO", "Invalid card info"); + return; + } + + Activity currentActivity = getCurrentActivity(); - mPromise = null; + if (currentActivity == null) { + promise.reject("NO_ACTIVITY", "There is no current activity"); + return; } - }; - private final void resolvePayment(PaymentMethodNonce paymentMethodNonce, String deviceData) { + BraintreeClient braintreeClient = new BraintreeClient(getCurrentActivity(), clientToken); + CardClient cardClient = new CardClient(braintreeClient); + + Card card = new Card(); + card.setNumber(cardInfo.getString("number")); + card.setExpirationMonth(cardInfo.getString("expirationMonth")); + card.setExpirationYear(cardInfo.getString("expirationYear")); + card.setCvv(cardInfo.getString("cvv")); + card.setPostalCode(cardInfo.getString("postalCode")); + + cardClient.tokenize(card, (cardNonce, error) -> { + if (error != null) { + promise.reject(error.getMessage(), error.getMessage()); + } else if (cardNonce == null) { + promise.reject("NO_CARD_NONCE", "Card nonce is null"); + } else { + promise.resolve(cardNonce.getString()); + } + }); + } + + private void resolvePayment(DropInResult dropInResult, Promise promise) { + String deviceData = dropInResult.getDeviceData(); + PaymentMethodNonce paymentMethodNonce = dropInResult.getPaymentMethodNonce(); + WritableMap jsResult = Arguments.createMap(); - jsResult.putString("nonce", paymentMethodNonce.getNonce()); - jsResult.putString("type", paymentMethodNonce.getTypeLabel()); - jsResult.putString("description", paymentMethodNonce.getDescription()); + + if (paymentMethodNonce == null) { + promise.resolve(null); + return; + } + + Activity currentActivity = getCurrentActivity(); + if (currentActivity == null) { + promise.reject("NO_ACTIVITY", "There is no current activity"); + return; + } + + DropInPaymentMethod dropInPaymentMethod = dropInResult.getPaymentMethodType(); + if (dropInPaymentMethod == null) { + promise.reject("NO_PAYMENT_METHOD", "There is no payment method"); + return; + } + + jsResult.putString("nonce", paymentMethodNonce.getString()); + jsResult.putString("type", currentActivity.getString(dropInPaymentMethod.getLocalizedName())); + jsResult.putString("description", dropInResult.getPaymentDescription()); jsResult.putBoolean("isDefault", paymentMethodNonce.isDefault()); jsResult.putString("deviceData", deviceData); - mPromise.resolve(jsResult); + promise.resolve(jsResult); } + @NonNull @Override public String getName() { return "RNBraintreeDropIn"; diff --git a/index.js.flow b/index.js.flow index 3f86d009..a46e7b58 100644 --- a/index.js.flow +++ b/index.js.flow @@ -3,6 +3,28 @@ type ShowOptions = {| threeDSecure?: {| amount: number, |}, + vaultManager?: boolean, + cardDisabled?: boolean, + googlePay?: boolean, + orderTotal?: string, + currencyCode?: string, + googlePayMerchantId?: string, + payPal?: boolean, + applePay?: boolean, + merchantIdentifier?: string, + countryCode?: string, + merchantName?: string, + darkTheme?: boolean, + fontFamily?: string, + boldFontFamily?: string, +|}; + +type CardInfo = {| + number: string, + expirationMonth: string, + expirationYear: string, + cvv: string, + postalCode: string, |}; type ShowResult = {| @@ -10,8 +32,11 @@ type ShowResult = {| description: string, type: string, isDefault: boolean, + deviceData: string, |}; declare module.exports: { show: (options: ShowOptions) => Promise, + fetchMostRecentPaymentMethod: (clientToken: string) => Promise, + tokenizeCard: (clientToken: string, cardInfo: CardInfo) => Promise, }; diff --git a/ios/RNBraintreeDropIn.h b/ios/RNBraintreeDropIn.h index 3aa5195c..43dbbff3 100644 --- a/ios/RNBraintreeDropIn.h +++ b/ios/RNBraintreeDropIn.h @@ -14,7 +14,7 @@ #import "BraintreeApplePay.h" -@interface RNBraintreeDropIn : NSObject +@interface RNBraintreeDropIn : NSObject @property (nonatomic, strong) UIViewController *_Nonnull reactRoot; @@ -33,7 +33,7 @@ @property (nonatomic) RCTPromiseRejectBlock _Nonnull reject; -@property (nonatomic, assign) BOOL *_Nonnull applePayAuthorized; +@property (nonatomic, assign) BOOL applePayAuthorized; + (void)resolvePayment:(BTDropInResult* _Nullable)result deviceData:(NSString * _Nonnull)deviceDataCollector resolver:(RCTPromiseResolveBlock _Nonnull)resolve; diff --git a/ios/RNBraintreeDropIn.m b/ios/RNBraintreeDropIn.m index 1eefa718..07df5d3c 100644 --- a/ios/RNBraintreeDropIn.m +++ b/ios/RNBraintreeDropIn.m @@ -12,22 +12,25 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_METHOD(show:(NSDictionary*)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - + BTDropInColorScheme colorScheme; + if([options[@"darkTheme"] boolValue]){ if (@available(iOS 13.0, *)) { - BTUIKAppearance.sharedInstance.colorScheme = BTUIKColorSchemeDynamic; + colorScheme = BTDropInColorSchemeDynamic; } else { - BTUIKAppearance.sharedInstance.colorScheme = BTUIKColorSchemeDark; + colorScheme = BTDropInColorSchemeDark; } } else { - BTUIKAppearance.sharedInstance.colorScheme = BTUIKColorSchemeLight; + colorScheme = BTDropInColorSchemeLight; } + BTDropInUICustomization *uiCustomization = [[BTDropInUICustomization alloc] initWithColorScheme:colorScheme]; + if(options[@"fontFamily"]){ - [BTUIKAppearance sharedInstance].fontFamily = options[@"fontFamily"]; + uiCustomization.fontFamily = options[@"fontFamily"]; } if(options[@"boldFontFamily"]){ - [BTUIKAppearance sharedInstance].boldFontFamily = options[@"boldFontFamily"]; + uiCustomization.boldFontFamily = options[@"boldFontFamily"]; } self.resolve = resolve; @@ -41,6 +44,7 @@ - (dispatch_queue_t)methodQueue } BTDropInRequest *request = [[BTDropInRequest alloc] init]; + request.uiCustomization = uiCustomization; NSDictionary* threeDSecureOptions = options[@"threeDSecure"]; if (threeDSecureOptions) { @@ -50,7 +54,6 @@ - (dispatch_queue_t)methodQueue return; } - request.threeDSecureVerification = YES; BTThreeDSecureRequest *threeDSecureRequest = [[BTThreeDSecureRequest alloc] init]; threeDSecureRequest.amount = [NSDecimalNumber decimalNumberWithString:threeDSecureAmount.stringValue]; request.threeDSecureRequest = threeDSecureRequest; @@ -59,7 +62,7 @@ - (dispatch_queue_t)methodQueue BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:clientToken]; self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:apiClient]; - [self.dataCollector collectCardFraudData:^(NSString * _Nonnull deviceDataCollector) { + [self.dataCollector collectDeviceData:^(NSString * _Nonnull deviceDataCollector) { // Save deviceData self.deviceDataCollector = deviceDataCollector; }]; @@ -100,6 +103,10 @@ - (dispatch_queue_t)methodQueue }else{ request.applePayDisabled = YES; } + + if(![options[@"payPal"] boolValue]){ //disable paypal + request.paypalDisabled = YES; + } BTDropInController *dropIn = [[BTDropInController alloc] initWithAuthorization:clientToken request:request handler:^(BTDropInController * _Nonnull controller, BTDropInResult * _Nullable result, NSError * _Nullable error) { [self.reactRoot dismissViewControllerAnimated:YES completion:nil]; @@ -109,7 +116,7 @@ - (dispatch_queue_t)methodQueue if (error != nil) { reject(error.localizedDescription, error.localizedDescription, error); - } else if (result.cancelled) { + } else if (result.canceled) { reject(@"USER_CANCELLATION", @"The user cancelled", nil); } else { if (threeDSecureOptions && [result.paymentMethod isKindOfClass:[BTCardNonce class]]) { @@ -121,7 +128,7 @@ - (dispatch_queue_t)methodQueue } else{ [[self class] resolvePayment:result deviceData:self.deviceDataCollector resolver:resolve]; } - } else if(result.paymentMethod == nil && (result.paymentOptionType == 16 || result.paymentOptionType == 18)){ //Apple Pay + } else if(result.paymentMethod == nil && (result.paymentMethodType == 16 || result.paymentMethodType == 17 || result.paymentMethodType == 18)){ //Apple Pay // UIViewController *ctrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; // [ctrl presentViewController:self.viewController animated:YES completion:nil]; UIViewController *rootViewController = RCTPresentedViewController(); @@ -131,12 +138,67 @@ - (dispatch_queue_t)methodQueue } } }]; - [self.reactRoot presentViewController:dropIn animated:YES completion:nil]; + + if (dropIn != nil) { + [self.reactRoot presentViewController:dropIn animated:YES completion:nil]; + } else { + reject(@"INVALID_CLIENT_TOKEN", @"The client token seems invalid", nil); + } +} + +RCT_EXPORT_METHOD(fetchMostRecentPaymentMethod:(NSString*)clientToken + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + [BTDropInResult mostRecentPaymentMethodForClientToken:clientToken completion:^(BTDropInResult * _Nullable result, NSError * _Nullable error) { + if (error != nil) { + reject(error.localizedDescription, error.localizedDescription, error); + } else if (result.canceled) { + reject(@"USER_CANCELLATION", @"The user cancelled", nil); + } else { + [[self class] resolvePayment:result deviceData:result.deviceData resolver:resolve]; + } + }]; +} + +RCT_EXPORT_METHOD(tokenizeCard:(NSString*)clientToken + info:(NSDictionary*)cardInfo + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *number = cardInfo[@"number"]; + NSString *expirationMonth = cardInfo[@"expirationMonth"]; + NSString *expirationYear = cardInfo[@"expirationYear"]; + NSString *cvv = cardInfo[@"cvv"]; + NSString *postalCode = cardInfo[@"postalCode"]; + + if (!number || !expirationMonth || !expirationYear || !cvv || !postalCode) { + reject(@"INVALID_CARD_INFO", @"Invalid card info", nil); + return; + } + + BTAPIClient *braintreeClient = [[BTAPIClient alloc] initWithAuthorization:clientToken]; + BTCardClient *cardClient = [[BTCardClient alloc] initWithAPIClient:braintreeClient]; + BTCard *card = [[BTCard alloc] init]; + card.number = number; + card.expirationMonth = expirationMonth; + card.expirationYear = expirationYear; + card.cvv = cvv; + card.postalCode = postalCode; + + [cardClient tokenizeCard:card + completion:^(BTCardNonce *tokenizedCard, NSError *error) { + if (error == nil) { + resolve(tokenizedCard.nonce); + } else { + reject(@"TOKENIZE_ERROR", @"Error tokenizing card.", error); + } + }]; } - (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller didAuthorizePayment:(PKPayment *)payment - completion:(void (^)(PKPaymentAuthorizationStatus))completion + handler:(nonnull void (^)(PKPaymentAuthorizationResult * _Nonnull))completion { // Example: Tokenize the Apple Pay payment @@ -150,7 +212,7 @@ - (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController // If applicable, address information is accessible in `payment`. // NSLog(@"description = %@", tokenizedApplePayPayment.localizedDescription); - completion(PKPaymentAuthorizationStatusSuccess); + completion([[PKPaymentAuthorizationResult alloc] initWithStatus:PKPaymentAuthorizationStatusSuccess errors:nil]); self.applePayAuthorized = YES; @@ -167,7 +229,7 @@ - (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController // Tokenization failed. Check `error` for the cause of the failure. // Indicate failure via the completion callback: - completion(PKPaymentAuthorizationStatusFailure); + completion([[PKPaymentAuthorizationResult alloc] initWithStatus:PKPaymentAuthorizationStatusFailure errors:nil]); } }]; } @@ -183,6 +245,11 @@ - (void)paymentAuthorizationViewControllerDidFinish:(PKPaymentAuthorizationViewC + (void)resolvePayment:(BTDropInResult* _Nullable)result deviceData:(NSString * _Nonnull)deviceDataCollector resolver:(RCTPromiseResolveBlock _Nonnull)resolve { //NSLog(@"result = %@", result); + if (!result) { + resolve(nil); + return; + } + NSMutableDictionary* jsResult = [NSMutableDictionary new]; //NSLog(@"paymentMethod = %@", result.paymentMethod); diff --git a/ios/RNBraintreeDropIn.xcodeproj/project.pbxproj b/ios/RNBraintreeDropIn.xcodeproj/project.pbxproj index 39e24391..399ba0e7 100644 --- a/ios/RNBraintreeDropIn.xcodeproj/project.pbxproj +++ b/ios/RNBraintreeDropIn.xcodeproj/project.pbxproj @@ -170,7 +170,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -207,7 +207,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; @@ -230,7 +230,7 @@ "$(PODS_ROOT)/Braintree/**", "$(PODS_ROOT)/BraintreeDropIn/**", ); - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PODS_ROOT = "${SRCROOT}/../../../ios/Pods"; @@ -255,7 +255,7 @@ "$(PODS_ROOT)/Braintree/**", "$(PODS_ROOT)/BraintreeDropIn/**", ); - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PODS_ROOT = "${SRCROOT}/../../../ios/Pods"; diff --git a/package.json b/package.json index f0391708..318b9369 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-braintree-dropin-ui", - "version": "1.1.1", + "version": "1.1.7", "description": "> React Native integration of Braintree Drop-in IOS V4 ANDROID V2 (Apple Pay &Android Pay Enabled)", "main": "index.js", "dependencies": {}, diff --git a/react-native.config.js b/react-native.config.js index f162c2b5..6550b2f7 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -3,7 +3,7 @@ const path = require('path'); module.exports = { dependency: { platforms: { - ios: { podspecPath: path.join(__dirname, 'ios', 'RNBraintreeDropIn.podspec') }, + ios: {}, android: { packageImportPath: 'import tech.power.RNBraintreeDropIn.RNBraintreeDropInPackage;', packageInstance: 'new RNBraintreeDropInPackage()',