init
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
12
.idea/material_theme_project_new.xml
generated
Normal file
12
.idea/material_theme_project_new.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MaterialThemeProjectNewConfig">
|
||||||
|
<option name="metadata">
|
||||||
|
<MTProjectMetadataState>
|
||||||
|
<option name="migrated" value="true" />
|
||||||
|
<option name="pristineConfig" value="false" />
|
||||||
|
<option name="userId" value="75b04292:19b05f04fec:-7ffe" />
|
||||||
|
</MTProjectMetadataState>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/react-native-braintree-dropin-turbo.iml" filepath="$PROJECT_DIR$/.idea/react-native-braintree-dropin-turbo.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/react-native-braintree-dropin-turbo.iml
generated
Normal file
12
.idea/react-native-braintree-dropin-turbo.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
237
INSTALLATION.md
Normal file
237
INSTALLATION.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# Installation Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- React Native >= 0.68
|
||||||
|
- iOS >= 13.0
|
||||||
|
- Android minSdkVersion >= 21
|
||||||
|
- Node.js >= 14
|
||||||
|
- CocoaPods (for iOS)
|
||||||
|
|
||||||
|
## Step-by-Step Installation
|
||||||
|
|
||||||
|
### 1. Install the Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react-native-braintree-dropin-turbo
|
||||||
|
# or
|
||||||
|
yarn add react-native-braintree-dropin-turbo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. iOS Setup
|
||||||
|
|
||||||
|
#### 2.1 Install Pods
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ios && pod install && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 Configure Apple Pay (Optional)
|
||||||
|
|
||||||
|
If you want to use Apple Pay, add the following to your `Info.plist`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>com.apple.developer.in-app-payments</key>
|
||||||
|
<array>
|
||||||
|
<string>merchant.your.merchant.identifier</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
You also need to enable Apple Pay in your Apple Developer account:
|
||||||
|
1. Go to Certificates, Identifiers & Profiles
|
||||||
|
2. Select your App ID
|
||||||
|
3. Enable "Apple Pay"
|
||||||
|
4. Create a Merchant ID
|
||||||
|
|
||||||
|
#### 2.3 Swift Bridging (if needed)
|
||||||
|
|
||||||
|
If you don't have a Swift bridging header yet, Xcode will create one automatically when you build the project.
|
||||||
|
|
||||||
|
### 3. Android Setup
|
||||||
|
|
||||||
|
#### 3.1 Update MainActivity
|
||||||
|
|
||||||
|
Open `android/app/src/main/java/[your-package]/MainActivity.kt` (or `.java`) and add:
|
||||||
|
|
||||||
|
**For Kotlin:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.braintreedroptinturbo.RNBraintreeDropInModule
|
||||||
|
|
||||||
|
class MainActivity : FragmentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
RNBraintreeDropInModule.initDropInClient(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Java:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
import android.os.Bundle;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import com.braintreedroptinturbo.RNBraintreeDropInModule;
|
||||||
|
|
||||||
|
public class MainActivity extends FragmentActivity {
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
RNBraintreeDropInModule.initDropInClient(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 Update build.gradle (if needed)
|
||||||
|
|
||||||
|
Make sure your `android/build.gradle` has:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
buildscript {
|
||||||
|
ext {
|
||||||
|
minSdkVersion = 21
|
||||||
|
compileSdkVersion = 33
|
||||||
|
targetSdkVersion = 33
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 Configure Google Pay (Optional)
|
||||||
|
|
||||||
|
Add to `android/app/src/main/AndroidManifest.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<application>
|
||||||
|
...
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.wallet.api.enabled"
|
||||||
|
android:value="true" />
|
||||||
|
</application>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. New Architecture (Optional)
|
||||||
|
|
||||||
|
This library supports the New Architecture out of the box. To enable it:
|
||||||
|
|
||||||
|
#### 4.1 iOS
|
||||||
|
|
||||||
|
In `ios/Podfile`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
```bash
|
||||||
|
cd ios && pod install && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 Android
|
||||||
|
|
||||||
|
In `android/gradle.properties`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
newArchEnabled=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Rebuild Your App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# iOS
|
||||||
|
npx react-native run-ios
|
||||||
|
|
||||||
|
# Android
|
||||||
|
npx react-native run-android
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### iOS Issues
|
||||||
|
|
||||||
|
**Pod Install Fails:**
|
||||||
|
```bash
|
||||||
|
cd ios
|
||||||
|
pod repo update
|
||||||
|
pod install --repo-update
|
||||||
|
```
|
||||||
|
|
||||||
|
**Swift Version Mismatch:**
|
||||||
|
Make sure your project is using Swift 5.0+. Check in Xcode:
|
||||||
|
Project Settings → Build Settings → Swift Language Version
|
||||||
|
|
||||||
|
**Apple Pay Not Working:**
|
||||||
|
- Verify your Merchant ID in Apple Developer Console
|
||||||
|
- Check that the Merchant ID matches in your Info.plist
|
||||||
|
- Ensure Apple Pay is enabled for your App ID
|
||||||
|
|
||||||
|
### Android Issues
|
||||||
|
|
||||||
|
**FragmentActivity Error:**
|
||||||
|
Make sure your MainActivity extends `FragmentActivity` instead of `ReactActivity`.
|
||||||
|
|
||||||
|
**Gradle Build Fails:**
|
||||||
|
Try:
|
||||||
|
```bash
|
||||||
|
cd android
|
||||||
|
./gradlew clean
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
**Drop-In Client Not Initialized:**
|
||||||
|
Ensure you called `RNBraintreeDropInModule.initDropInClient(this)` in `MainActivity.onCreate()`.
|
||||||
|
|
||||||
|
**Google Pay Not Working:**
|
||||||
|
- Add the Google Pay meta-data to AndroidManifest.xml
|
||||||
|
- Verify your Google Pay Merchant ID
|
||||||
|
- Test with a test card in sandbox mode first
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**"No Client Token" Error:**
|
||||||
|
Make sure you're passing a valid client token from your server.
|
||||||
|
|
||||||
|
**"User Cancellation" on Android:**
|
||||||
|
This is normal when the user closes the Drop-In UI. Handle it gracefully in your error handling.
|
||||||
|
|
||||||
|
**TypeScript Errors:**
|
||||||
|
Make sure you're using TypeScript 4.0+ and have proper type definitions installed.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
To verify the installation, create a simple test:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import BraintreeDropIn from 'react-native-braintree-dropin-turbo';
|
||||||
|
|
||||||
|
// Test device data collection (doesn't require UI)
|
||||||
|
const testInstallation = async () => {
|
||||||
|
try {
|
||||||
|
const deviceData = await BraintreeDropIn.collectDeviceData('sandbox_test_token');
|
||||||
|
console.log('✅ Installation successful!', deviceData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Installation issue:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
testInstallation();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Get your Braintree client token from your server
|
||||||
|
- Review the [API Reference](./README.md#api-reference)
|
||||||
|
- Check out the [Example App](./example/App.tsx)
|
||||||
|
- Test in sandbox mode before production
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues:
|
||||||
|
1. Check this guide
|
||||||
|
2. Review closed issues on GitHub
|
||||||
|
3. Open a new issue with:
|
||||||
|
- React Native version
|
||||||
|
- OS and version
|
||||||
|
- Error messages
|
||||||
|
- Steps to reproduce
|
||||||
80
android/build.gradle
Normal file
80
android/build.gradle
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
buildscript {
|
||||||
|
ext.safeExtGet = {prop, fallback ->
|
||||||
|
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:7.4.2")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "com.android.library"
|
||||||
|
apply plugin: "kotlin-android"
|
||||||
|
|
||||||
|
def isNewArchitectureEnabled() {
|
||||||
|
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion safeExtGet('compileSdkVersion', 33)
|
||||||
|
|
||||||
|
namespace "com.braintreedroptinturbo"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion safeExtGet('minSdkVersion', 21)
|
||||||
|
targetSdkVersion safeExtGet('targetSdkVersion', 33)
|
||||||
|
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable "GradleCompatible"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_11
|
||||||
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "11"
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
if (isNewArchitectureEnabled()) {
|
||||||
|
java.srcDirs += ["src/newarch"]
|
||||||
|
} else {
|
||||||
|
java.srcDirs += ["src/oldarch"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "com.facebook.react:react-native:+"
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.21"
|
||||||
|
|
||||||
|
// Braintree Dependencies - Latest versions
|
||||||
|
implementation 'com.braintreepayments.api:drop-in:6.16.0'
|
||||||
|
implementation 'com.braintreepayments.api:card:4.45.0'
|
||||||
|
implementation 'com.braintreepayments.api:data-collector:4.45.0'
|
||||||
|
implementation 'com.braintreepayments.api:google-pay:4.45.0'
|
||||||
|
implementation 'com.braintreepayments.api:venmo:4.45.0'
|
||||||
|
}
|
||||||
2
android/src/main/AndroidManifest.xml
Normal file
2
android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
package com.braintreedroptinturbo
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.braintreepayments.api.BraintreeClient
|
||||||
|
import com.braintreepayments.api.Card
|
||||||
|
import com.braintreepayments.api.CardClient
|
||||||
|
import com.braintreepayments.api.CardNonce
|
||||||
|
import com.braintreepayments.api.ClientTokenCallback
|
||||||
|
import com.braintreepayments.api.DataCollector
|
||||||
|
import com.braintreepayments.api.DropInClient
|
||||||
|
import com.braintreepayments.api.DropInListener
|
||||||
|
import com.braintreepayments.api.DropInRequest
|
||||||
|
import com.braintreepayments.api.DropInResult
|
||||||
|
import com.braintreepayments.api.GooglePayRequest
|
||||||
|
import com.braintreepayments.api.ThreeDSecureRequest
|
||||||
|
import com.braintreepayments.api.UserCanceledException
|
||||||
|
import com.braintreepayments.api.VenmoPaymentMethodUsage
|
||||||
|
import com.braintreepayments.api.VenmoRequest
|
||||||
|
import com.facebook.react.bridge.Arguments
|
||||||
|
import com.facebook.react.bridge.Promise
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
import com.facebook.react.bridge.ReadableMap
|
||||||
|
import com.facebook.react.module.annotations.ReactModule
|
||||||
|
import com.google.android.gms.wallet.TransactionInfo
|
||||||
|
import com.google.android.gms.wallet.WalletConstants
|
||||||
|
|
||||||
|
@ReactModule(name = RNBraintreeDropInModule.NAME)
|
||||||
|
class RNBraintreeDropInModule(reactContext: ReactApplicationContext) :
|
||||||
|
NativeRNBraintreeDropInSpec(reactContext) {
|
||||||
|
|
||||||
|
private var isVerifyingThreeDSecure = false
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
override fun show(options: ReadableMap, promise: Promise) {
|
||||||
|
isVerifyingThreeDSecure = false
|
||||||
|
|
||||||
|
if (!options.hasKey("clientToken")) {
|
||||||
|
promise.reject("NO_CLIENT_TOKEN", "You must provide a client token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentActivity = currentActivity as? FragmentActivity
|
||||||
|
if (currentActivity == null) {
|
||||||
|
promise.reject("NO_ACTIVITY", "There is no current activity")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val dropInRequest = DropInRequest()
|
||||||
|
|
||||||
|
// Vault Manager
|
||||||
|
if (options.hasKey("vaultManager")) {
|
||||||
|
dropInRequest.isVaultManagerEnabled = options.getBoolean("vaultManager")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google Pay
|
||||||
|
if (options.hasKey("googlePay") && options.getBoolean("googlePay")) {
|
||||||
|
val googlePayRequest = GooglePayRequest().apply {
|
||||||
|
setTransactionInfo(
|
||||||
|
TransactionInfo.newBuilder()
|
||||||
|
.setTotalPrice(options.getString("orderTotal") ?: "0.00")
|
||||||
|
.setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_FINAL)
|
||||||
|
.setCurrencyCode(options.getString("currencyCode") ?: "USD")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
isBillingAddressRequired = true
|
||||||
|
googleMerchantId = options.getString("googlePayMerchantId")
|
||||||
|
}
|
||||||
|
|
||||||
|
dropInRequest.isGooglePayDisabled = false
|
||||||
|
dropInRequest.googlePayRequest = googlePayRequest
|
||||||
|
} else {
|
||||||
|
dropInRequest.isGooglePayDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card Disabled
|
||||||
|
if (options.hasKey("cardDisabled")) {
|
||||||
|
dropInRequest.isCardDisabled = options.getBoolean("cardDisabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Venmo
|
||||||
|
if (options.hasKey("venmo") && options.getBoolean("venmo")) {
|
||||||
|
val venmoRequest = VenmoRequest(VenmoPaymentMethodUsage.MULTI_USE)
|
||||||
|
dropInRequest.venmoRequest = venmoRequest
|
||||||
|
dropInRequest.isVenmoDisabled = false
|
||||||
|
} else {
|
||||||
|
dropInRequest.isVenmoDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3D Secure
|
||||||
|
if (options.hasKey("threeDSecure")) {
|
||||||
|
val threeDSecureOptions = options.getMap("threeDSecure")
|
||||||
|
if (threeDSecureOptions == null || !threeDSecureOptions.hasKey("amount")) {
|
||||||
|
promise.reject("NO_3DS_AMOUNT", "You must provide an amount for 3D Secure")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isVerifyingThreeDSecure = true
|
||||||
|
|
||||||
|
val threeDSecureRequest = ThreeDSecureRequest().apply {
|
||||||
|
amount = threeDSecureOptions.getString("amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
dropInRequest.threeDSecureRequest = threeDSecureRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayPal
|
||||||
|
dropInRequest.isPayPalDisabled = !options.hasKey("payPal") || !options.getBoolean("payPal")
|
||||||
|
|
||||||
|
clientToken = options.getString("clientToken")
|
||||||
|
|
||||||
|
if (dropInClient == null) {
|
||||||
|
promise.reject(
|
||||||
|
"DROP_IN_CLIENT_UNINITIALIZED",
|
||||||
|
"Did you forget to call RNBraintreeDropInModule.initDropInClient(this) in MainActivity.onCreate?"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dropInClient!!.setListener(object : DropInListener {
|
||||||
|
override fun onDropInSuccess(dropInResult: DropInResult) {
|
||||||
|
val paymentMethodNonce = dropInResult.paymentMethodNonce
|
||||||
|
|
||||||
|
if (isVerifyingThreeDSecure && paymentMethodNonce is CardNonce) {
|
||||||
|
val threeDSecureInfo = paymentMethodNonce.threeDSecureInfo
|
||||||
|
if (!threeDSecureInfo.isLiabilityShiftPossible) {
|
||||||
|
promise.reject(
|
||||||
|
"3DSECURE_NOT_ABLE_TO_SHIFT_LIABILITY",
|
||||||
|
"3D Secure liability cannot be shifted"
|
||||||
|
)
|
||||||
|
} else if (!threeDSecureInfo.isLiabilityShifted) {
|
||||||
|
promise.reject(
|
||||||
|
"3DSECURE_LIABILITY_NOT_SHIFTED",
|
||||||
|
"3D Secure liability was not shifted"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
resolvePayment(dropInResult, promise)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolvePayment(dropInResult, promise)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDropInFailure(exception: Exception) {
|
||||||
|
if (exception is UserCanceledException) {
|
||||||
|
promise.reject("USER_CANCELLATION", "The user cancelled")
|
||||||
|
} else {
|
||||||
|
promise.reject(exception.message, exception.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dropInClient!!.launchDropIn(dropInRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
override fun fetchMostRecentPaymentMethod(clientToken: String, promise: Promise) {
|
||||||
|
val currentActivity = currentActivity as? FragmentActivity
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
Companion.clientToken = clientToken
|
||||||
|
|
||||||
|
dropInClient!!.fetchMostRecentPaymentMethod(
|
||||||
|
currentActivity
|
||||||
|
) { dropInResult: DropInResult?, error: Exception? ->
|
||||||
|
if (error != null) {
|
||||||
|
promise.reject(error.message, error.message)
|
||||||
|
} else if (dropInResult == null) {
|
||||||
|
promise.resolve(null)
|
||||||
|
} else {
|
||||||
|
resolvePayment(dropInResult, promise)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
override fun tokenizeCard(clientToken: String, cardInfo: ReadableMap, promise: Promise) {
|
||||||
|
if (!cardInfo.hasKey("cvv")) {
|
||||||
|
promise.reject("INVALID_CARD_INFO", "CVV is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val onlyCVV = cardInfo.hasKey("onlyCVV") && cardInfo.getBoolean("onlyCVV")
|
||||||
|
|
||||||
|
if (!onlyCVV) {
|
||||||
|
if (!cardInfo.hasKey("number") ||
|
||||||
|
!cardInfo.hasKey("expirationMonth") ||
|
||||||
|
!cardInfo.hasKey("expirationYear") ||
|
||||||
|
!cardInfo.hasKey("postalCode")
|
||||||
|
) {
|
||||||
|
promise.reject("INVALID_CARD_INFO", "Invalid card info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentActivity = currentActivity
|
||||||
|
if (currentActivity == null) {
|
||||||
|
promise.reject("NO_ACTIVITY", "There is no current activity")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val braintreeClient = BraintreeClient(currentActivity, clientToken)
|
||||||
|
val cardClient = CardClient(braintreeClient)
|
||||||
|
|
||||||
|
val card = Card().apply {
|
||||||
|
if (!onlyCVV) {
|
||||||
|
number = cardInfo.getString("number")
|
||||||
|
expirationMonth = cardInfo.getString("expirationMonth")
|
||||||
|
expirationYear = cardInfo.getString("expirationYear")
|
||||||
|
postalCode = cardInfo.getString("postalCode")
|
||||||
|
}
|
||||||
|
cvv = cardInfo.getString("cvv")
|
||||||
|
}
|
||||||
|
|
||||||
|
cardClient.tokenize(card) { cardNonce: CardNonce?, error: Exception? ->
|
||||||
|
if (error != null) {
|
||||||
|
promise.reject("TOKENIZE_ERROR", error.message, error)
|
||||||
|
} else if (cardNonce == null) {
|
||||||
|
promise.reject("NO_CARD_NONCE", "Card nonce is null")
|
||||||
|
} else {
|
||||||
|
val jsResult = Arguments.createMap().apply {
|
||||||
|
putString("type", "card")
|
||||||
|
putString("nonce", cardNonce.string)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataCollector = DataCollector(braintreeClient)
|
||||||
|
dataCollector.collectDeviceData(currentActivity) { deviceData: String?, err: Exception? ->
|
||||||
|
if (deviceData != null) {
|
||||||
|
jsResult.putString("deviceData", deviceData)
|
||||||
|
promise.resolve(jsResult)
|
||||||
|
} else {
|
||||||
|
promise.reject("DEVICE_DATA_ERROR", "Failed to collect device data", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
override fun collectDeviceData(clientToken: String, promise: Promise) {
|
||||||
|
val currentActivity = currentActivity
|
||||||
|
if (currentActivity == null) {
|
||||||
|
promise.reject("NO_ACTIVITY", "There is no current activity")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val braintreeClient = BraintreeClient(currentActivity, clientToken)
|
||||||
|
val dataCollector = DataCollector(braintreeClient)
|
||||||
|
|
||||||
|
dataCollector.collectDeviceData(currentActivity) { deviceData: String?, error: Exception? ->
|
||||||
|
if (deviceData != null) {
|
||||||
|
promise.resolve(deviceData)
|
||||||
|
} else {
|
||||||
|
promise.reject("DEVICE_DATA_ERROR", "Failed to collect device data", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePayment(dropInResult: DropInResult, promise: Promise) {
|
||||||
|
val deviceData = dropInResult.deviceData
|
||||||
|
val paymentMethodNonce = dropInResult.paymentMethodNonce
|
||||||
|
|
||||||
|
if (paymentMethodNonce == null) {
|
||||||
|
promise.resolve(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentActivity = currentActivity
|
||||||
|
if (currentActivity == null) {
|
||||||
|
promise.reject("NO_ACTIVITY", "There is no current activity")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val dropInPaymentMethod = dropInResult.paymentMethodType
|
||||||
|
if (dropInPaymentMethod == null) {
|
||||||
|
promise.reject("NO_PAYMENT_METHOD", "There is no payment method")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsResult = Arguments.createMap().apply {
|
||||||
|
putString("nonce", paymentMethodNonce.string)
|
||||||
|
putString("type", currentActivity.getString(dropInPaymentMethod.localizedName))
|
||||||
|
putString("description", dropInResult.paymentDescription)
|
||||||
|
putBoolean("isDefault", paymentMethodNonce.isDefault)
|
||||||
|
putString("deviceData", deviceData)
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.resolve(jsResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val NAME = "RNBraintreeDropIn"
|
||||||
|
|
||||||
|
private var dropInClient: DropInClient? = null
|
||||||
|
private var clientToken: String? = null
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun initDropInClient(activity: FragmentActivity) {
|
||||||
|
dropInClient = DropInClient(activity) { callback: ClientTokenCallback ->
|
||||||
|
if (clientToken != null) {
|
||||||
|
callback.onSuccess(clientToken!!)
|
||||||
|
} else {
|
||||||
|
callback.onFailure(Exception("Client token is null"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.braintreedroptinturbo
|
||||||
|
|
||||||
|
import com.facebook.react.TurboReactPackage
|
||||||
|
import com.facebook.react.bridge.NativeModule
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.module.model.ReactModuleInfo
|
||||||
|
import com.facebook.react.module.model.ReactModuleInfoProvider
|
||||||
|
|
||||||
|
class RNBraintreeDropInPackage : TurboReactPackage() {
|
||||||
|
|
||||||
|
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
||||||
|
return if (name == RNBraintreeDropInModule.NAME) {
|
||||||
|
RNBraintreeDropInModule(reactContext)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
||||||
|
return ReactModuleInfoProvider {
|
||||||
|
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
||||||
|
val isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||||
|
moduleInfos[RNBraintreeDropInModule.NAME] = ReactModuleInfo(
|
||||||
|
RNBraintreeDropInModule.NAME,
|
||||||
|
RNBraintreeDropInModule::class.java.name,
|
||||||
|
false, // canOverrideExistingModule
|
||||||
|
false, // needsEagerInit
|
||||||
|
true, // hasConstants
|
||||||
|
false, // isCxxModule
|
||||||
|
isTurboModule // isTurboModule
|
||||||
|
)
|
||||||
|
moduleInfos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.braintreedroptinturbo
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
|
||||||
|
abstract class NativeRNBraintreeDropInSpec(context: ReactApplicationContext) :
|
||||||
|
NativeRNBraintreeDropInSpecBase(context)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.braintreedroptinturbo
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
|
import com.facebook.react.bridge.Promise
|
||||||
|
import com.facebook.react.bridge.ReadableMap
|
||||||
|
|
||||||
|
abstract class NativeRNBraintreeDropInSpec(context: ReactApplicationContext) :
|
||||||
|
ReactContextBaseJavaModule(context) {
|
||||||
|
|
||||||
|
abstract fun show(options: ReadableMap, promise: Promise)
|
||||||
|
abstract fun fetchMostRecentPaymentMethod(clientToken: String, promise: Promise)
|
||||||
|
abstract fun tokenizeCard(clientToken: String, cardInfo: ReadableMap, promise: Promise)
|
||||||
|
abstract fun collectDeviceData(clientToken: String, promise: Promise)
|
||||||
|
}
|
||||||
561
example/App.tsx
Normal file
561
example/App.tsx
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
SafeAreaView,
|
||||||
|
ScrollView,
|
||||||
|
StatusBar,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
TouchableOpacity,
|
||||||
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
|
import BraintreeDropIn, {
|
||||||
|
type PaymentResult,
|
||||||
|
type DropInOptions,
|
||||||
|
} from 'react-native-braintree-dropin-turbo';
|
||||||
|
|
||||||
|
// IMPORTANT: Replace with your actual Braintree client token from your server
|
||||||
|
const BRAINTREE_CLIENT_TOKEN = 'sandbox_g42y39zw_348pk9cgf3bgyw2b';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [paymentResult, setPaymentResult] = useState<PaymentResult | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [deviceData, setDeviceData] = useState<string>('');
|
||||||
|
|
||||||
|
const showAlert = (title: string, message: string) => {
|
||||||
|
Alert.alert(title, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 1: Basic Drop-In UI with all payment methods
|
||||||
|
const handleBasicDropIn = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const options: DropInOptions = {
|
||||||
|
clientToken: BRAINTREE_CLIENT_TOKEN,
|
||||||
|
orderTotal: 29.99,
|
||||||
|
currencyCode: 'USD',
|
||||||
|
venmo: true,
|
||||||
|
payPal: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BraintreeDropIn.show(options);
|
||||||
|
setPaymentResult(result);
|
||||||
|
showAlert('Success!', `Payment nonce received: ${result.nonce.substring(0, 20)}...`);
|
||||||
|
console.log('Payment Result:', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'USER_CANCELLATION') {
|
||||||
|
console.log('User cancelled payment');
|
||||||
|
} else {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
console.error('Payment Error:', error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 2: Apple Pay (iOS only)
|
||||||
|
const handleApplePay = async () => {
|
||||||
|
if (Platform.OS !== 'ios') {
|
||||||
|
showAlert('iOS Only', 'Apple Pay is only available on iOS devices');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const options: DropInOptions = {
|
||||||
|
clientToken: BRAINTREE_CLIENT_TOKEN,
|
||||||
|
orderTotal: 49.99,
|
||||||
|
currencyCode: 'USD',
|
||||||
|
countryCode: 'US',
|
||||||
|
applePay: true,
|
||||||
|
merchantIdentifier: 'merchant.com.yourcompany.app', // Replace with your merchant ID
|
||||||
|
merchantName: 'Your Store Name',
|
||||||
|
venmo: true,
|
||||||
|
payPal: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BraintreeDropIn.show(options);
|
||||||
|
setPaymentResult(result);
|
||||||
|
showAlert('Success!', `Apple Pay payment received!`);
|
||||||
|
console.log('Apple Pay Result:', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'USER_CANCELLATION') {
|
||||||
|
console.log('User cancelled Apple Pay');
|
||||||
|
} else {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
console.error('Apple Pay Error:', error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 3: Google Pay (Android only)
|
||||||
|
const handleGooglePay = async () => {
|
||||||
|
if (Platform.OS !== 'android') {
|
||||||
|
showAlert('Android Only', 'Google Pay is only available on Android devices');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const options: DropInOptions = {
|
||||||
|
clientToken: BRAINTREE_CLIENT_TOKEN,
|
||||||
|
orderTotal: 39.99,
|
||||||
|
currencyCode: 'USD',
|
||||||
|
googlePay: true,
|
||||||
|
googlePayMerchantId: 'BCR2DN4T6Z3WWIJJ', // Replace with your Google merchant ID
|
||||||
|
venmo: true,
|
||||||
|
payPal: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BraintreeDropIn.show(options);
|
||||||
|
setPaymentResult(result);
|
||||||
|
showAlert('Success!', `Google Pay payment received!`);
|
||||||
|
console.log('Google Pay Result:', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'USER_CANCELLATION') {
|
||||||
|
console.log('User cancelled Google Pay');
|
||||||
|
} else {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
console.error('Google Pay Error:', error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 4: 3D Secure
|
||||||
|
const handle3DSecure = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const options: DropInOptions = {
|
||||||
|
clientToken: BRAINTREE_CLIENT_TOKEN,
|
||||||
|
orderTotal: 100.00,
|
||||||
|
currencyCode: 'USD',
|
||||||
|
threeDSecure: {
|
||||||
|
amount: 100.00,
|
||||||
|
},
|
||||||
|
venmo: true,
|
||||||
|
payPal: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BraintreeDropIn.show(options);
|
||||||
|
setPaymentResult(result);
|
||||||
|
showAlert('Success!', '3D Secure verification completed!');
|
||||||
|
console.log('3D Secure Result:', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'USER_CANCELLATION') {
|
||||||
|
console.log('User cancelled 3D Secure');
|
||||||
|
} else if (error.message.includes('3DSECURE')) {
|
||||||
|
showAlert('3D Secure Failed', error.message);
|
||||||
|
} else {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
}
|
||||||
|
console.error('3D Secure Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 5: Tokenize Card Directly
|
||||||
|
const handleTokenizeCard = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Test card number - DO NOT use in production
|
||||||
|
const result = await BraintreeDropIn.tokenizeCard(
|
||||||
|
BRAINTREE_CLIENT_TOKEN,
|
||||||
|
{
|
||||||
|
number: '4111111111111111',
|
||||||
|
expirationMonth: '12',
|
||||||
|
expirationYear: '2025',
|
||||||
|
cvv: '123',
|
||||||
|
postalCode: '12345',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setPaymentResult(result);
|
||||||
|
showAlert('Success!', 'Card tokenized successfully!');
|
||||||
|
console.log('Tokenized Card Result:', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
console.error('Tokenization Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 6: Collect Device Data
|
||||||
|
const handleCollectDeviceData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await BraintreeDropIn.collectDeviceData(BRAINTREE_CLIENT_TOKEN);
|
||||||
|
setDeviceData(data);
|
||||||
|
showAlert('Device Data Collected', `Length: ${data.length} characters`);
|
||||||
|
console.log('Device Data:', data.substring(0, 100) + '...');
|
||||||
|
} catch (error: any) {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
console.error('Device Data Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 7: Fetch Most Recent Payment Method
|
||||||
|
const handleFetchLastPayment = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await BraintreeDropIn.fetchMostRecentPaymentMethod(
|
||||||
|
BRAINTREE_CLIENT_TOKEN
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
setPaymentResult(result);
|
||||||
|
showAlert('Last Payment Method', result.description);
|
||||||
|
console.log('Last Payment:', result);
|
||||||
|
} else {
|
||||||
|
showAlert('No Payment Found', 'No previous payment method available');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
console.error('Fetch Payment Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 8: Vault Manager
|
||||||
|
const handleVaultManager = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const options: DropInOptions = {
|
||||||
|
clientToken: BRAINTREE_CLIENT_TOKEN,
|
||||||
|
vaultManager: true,
|
||||||
|
orderTotal: 25.00,
|
||||||
|
currencyCode: 'USD',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BraintreeDropIn.show(options);
|
||||||
|
setPaymentResult(result);
|
||||||
|
showAlert('Success!', 'Payment method saved to vault!');
|
||||||
|
console.log('Vault Manager Result:', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'USER_CANCELLATION') {
|
||||||
|
console.log('User cancelled vault manager');
|
||||||
|
} else {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 9: Dark Theme (iOS)
|
||||||
|
const handleDarkTheme = async () => {
|
||||||
|
if (Platform.OS !== 'ios') {
|
||||||
|
showAlert('iOS Only', 'Dark theme is only available on iOS');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const options: DropInOptions = {
|
||||||
|
clientToken: BRAINTREE_CLIENT_TOKEN,
|
||||||
|
darkTheme: true,
|
||||||
|
orderTotal: 19.99,
|
||||||
|
currencyCode: 'USD',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BraintreeDropIn.show(options);
|
||||||
|
setPaymentResult(result);
|
||||||
|
showAlert('Success!', 'Payment processed with dark theme!');
|
||||||
|
console.log('Dark Theme Result:', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'USER_CANCELLATION') {
|
||||||
|
console.log('User cancelled dark theme');
|
||||||
|
} else {
|
||||||
|
showAlert('Error', error.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestButton = ({ title, onPress, color = '#007AFF' }: any) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, { backgroundColor: color }]}
|
||||||
|
onPress={onPress}
|
||||||
|
disabled={loading}>
|
||||||
|
<Text style={styles.buttonText}>{title}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<StatusBar barStyle="dark-content" />
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}>
|
||||||
|
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>Braintree Drop-In</Text>
|
||||||
|
<Text style={styles.subtitle}>React Native Turbo Module Example</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color="#007AFF" />
|
||||||
|
<Text style={styles.loadingText}>Processing...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Basic Tests</Text>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title="1. Show Drop-In UI"
|
||||||
|
onPress={handleBasicDropIn}
|
||||||
|
color="#007AFF"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title="2. Tokenize Test Card"
|
||||||
|
onPress={handleTokenizeCard}
|
||||||
|
color="#34C759"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title="3. Collect Device Data"
|
||||||
|
onPress={handleCollectDeviceData}
|
||||||
|
color="#5856D6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title="4. Fetch Last Payment"
|
||||||
|
onPress={handleFetchLastPayment}
|
||||||
|
color="#AF52DE"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Platform-Specific</Text>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title={`5. Apple Pay (iOS Only)`}
|
||||||
|
onPress={handleApplePay}
|
||||||
|
color="#000000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title={`6. Google Pay (Android Only)`}
|
||||||
|
onPress={handleGooglePay}
|
||||||
|
color="#4285F4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title={`7. Dark Theme (iOS Only)`}
|
||||||
|
onPress={handleDarkTheme}
|
||||||
|
color="#1C1C1E"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Advanced Features</Text>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title="8. 3D Secure Authentication"
|
||||||
|
onPress={handle3DSecure}
|
||||||
|
color="#FF9500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TestButton
|
||||||
|
title="9. Vault Manager"
|
||||||
|
onPress={handleVaultManager}
|
||||||
|
color="#FF2D55"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{paymentResult && (
|
||||||
|
<View style={styles.resultContainer}>
|
||||||
|
<Text style={styles.resultTitle}>Last Payment Result:</Text>
|
||||||
|
|
||||||
|
<View style={styles.resultRow}>
|
||||||
|
<Text style={styles.resultLabel}>Type:</Text>
|
||||||
|
<Text style={styles.resultValue}>{paymentResult.type}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.resultRow}>
|
||||||
|
<Text style={styles.resultLabel}>Description:</Text>
|
||||||
|
<Text style={styles.resultValue}>{paymentResult.description}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.resultRow}>
|
||||||
|
<Text style={styles.resultLabel}>Nonce:</Text>
|
||||||
|
<Text style={styles.resultValue} numberOfLines={1}>
|
||||||
|
{paymentResult.nonce.substring(0, 30)}...
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.resultRow}>
|
||||||
|
<Text style={styles.resultLabel}>Is Default:</Text>
|
||||||
|
<Text style={styles.resultValue}>
|
||||||
|
{paymentResult.isDefault ? 'Yes' : 'No'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.resultRow}>
|
||||||
|
<Text style={styles.resultLabel}>Device Data:</Text>
|
||||||
|
<Text style={styles.resultValue} numberOfLines={1}>
|
||||||
|
{paymentResult.deviceData.substring(0, 30)}...
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deviceData && (
|
||||||
|
<View style={styles.deviceDataContainer}>
|
||||||
|
<Text style={styles.resultTitle}>Device Data:</Text>
|
||||||
|
<Text style={styles.deviceDataText} numberOfLines={3}>
|
||||||
|
{deviceData}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<Text style={styles.footerText}>
|
||||||
|
Platform: {Platform.OS === 'ios' ? 'iOS' : 'Android'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.footerText}>
|
||||||
|
⚠️ Remember to replace BRAINTREE_CLIENT_TOKEN with your actual token
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F2F2F7',
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginVertical: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#000',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
padding: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#FFF',
|
||||||
|
borderRadius: 12,
|
||||||
|
marginVertical: 10,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 10,
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginVertical: 10,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#000',
|
||||||
|
marginBottom: 12,
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: '#007AFF',
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginVertical: 6,
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#FFF',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
resultContainer: {
|
||||||
|
backgroundColor: '#FFF',
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginTop: 20,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
resultTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#000',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
resultRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#F2F2F7',
|
||||||
|
},
|
||||||
|
resultLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#666',
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
resultValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#000',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
deviceDataContainer: {
|
||||||
|
backgroundColor: '#FFF',
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
deviceDataText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#666',
|
||||||
|
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
marginTop: 30,
|
||||||
|
marginBottom: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
footerText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#999',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginVertical: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default App;
|
||||||
27
ios/RNBraintreeDropIn.m
Normal file
27
ios/RNBraintreeDropIn.m
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#import <React/RCTBridgeModule.h>
|
||||||
|
|
||||||
|
@interface RCT_EXTERN_MODULE(RNBraintreeDropIn, NSObject)
|
||||||
|
|
||||||
|
RCT_EXTERN_METHOD(show:(NSDictionary *)options
|
||||||
|
resolve:(RCTPromiseResolveBlock)resolve
|
||||||
|
reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
|
||||||
|
RCT_EXTERN_METHOD(fetchMostRecentPaymentMethod:(NSString *)clientToken
|
||||||
|
resolve:(RCTPromiseResolveBlock)resolve
|
||||||
|
reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
|
||||||
|
RCT_EXTERN_METHOD(tokenizeCard:(NSString *)clientToken
|
||||||
|
cardInfo:(NSDictionary *)cardInfo
|
||||||
|
resolve:(RCTPromiseResolveBlock)resolve
|
||||||
|
reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
|
||||||
|
RCT_EXTERN_METHOD(collectDeviceData:(NSString *)clientToken
|
||||||
|
resolve:(RCTPromiseResolveBlock)resolve
|
||||||
|
reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
|
||||||
|
+ (BOOL)requiresMainQueueSetup
|
||||||
|
{
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
394
ios/RNBraintreeDropIn.swift
Normal file
394
ios/RNBraintreeDropIn.swift
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import PassKit
|
||||||
|
import BraintreeCore
|
||||||
|
import BraintreeDropIn
|
||||||
|
import BraintreeCard
|
||||||
|
import BraintreeDataCollector
|
||||||
|
import BraintreeApplePay
|
||||||
|
import BraintreeVenmo
|
||||||
|
|
||||||
|
@objc(RNBraintreeDropIn)
|
||||||
|
class RNBraintreeDropIn: NSObject {
|
||||||
|
|
||||||
|
private var dataCollector: BTDataCollector?
|
||||||
|
private var braintreeClient: BTAPIClient?
|
||||||
|
private var paymentRequest: PKPaymentRequest?
|
||||||
|
private var viewController: PKPaymentAuthorizationViewController?
|
||||||
|
private var deviceDataCollector: String = ""
|
||||||
|
private var currentResolve: RCTPromiseResolveBlock?
|
||||||
|
private var currentReject: RCTPromiseRejectBlock?
|
||||||
|
private var applePayAuthorized: Bool = false
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
static func requiresMainQueueSetup() -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func show(_ options: NSDictionary,
|
||||||
|
resolve: @escaping RCTPromiseResolveBlock,
|
||||||
|
reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.currentResolve = resolve
|
||||||
|
self.currentReject = reject
|
||||||
|
self.applePayAuthorized = false
|
||||||
|
|
||||||
|
guard let clientToken = options["clientToken"] as? String else {
|
||||||
|
reject("NO_CLIENT_TOKEN", "You must provide a client token", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup color scheme
|
||||||
|
var colorScheme: BTDropInUICustomization.ColorScheme = .light
|
||||||
|
if let darkTheme = options["darkTheme"] as? Bool, darkTheme {
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
colorScheme = .dynamic
|
||||||
|
} else {
|
||||||
|
colorScheme = .dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uiCustomization = BTDropInUICustomization(colorScheme: colorScheme)
|
||||||
|
|
||||||
|
if let fontFamily = options["fontFamily"] as? String {
|
||||||
|
uiCustomization.fontFamily = fontFamily
|
||||||
|
}
|
||||||
|
if let boldFontFamily = options["boldFontFamily"] as? String {
|
||||||
|
uiCustomization.boldFontFamily = boldFontFamily
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = BTDropInRequest()
|
||||||
|
request.uiCustomization = uiCustomization
|
||||||
|
|
||||||
|
// 3D Secure setup
|
||||||
|
if let threeDSecureOptions = options["threeDSecure"] as? NSDictionary {
|
||||||
|
guard let amount = threeDSecureOptions["amount"] as? NSNumber else {
|
||||||
|
reject("NO_3DS_AMOUNT", "You must provide an amount for 3D Secure", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let threeDSecureRequest = BTThreeDSecureRequest()
|
||||||
|
threeDSecureRequest.amount = NSDecimalNumber(value: amount.doubleValue)
|
||||||
|
request.threeDSecureRequest = threeDSecureRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize API client and data collector
|
||||||
|
let apiClient = BTAPIClient(authorization: clientToken)!
|
||||||
|
self.dataCollector = BTDataCollector(apiClient: apiClient)
|
||||||
|
self.dataCollector?.collectDeviceData { [weak self] deviceData in
|
||||||
|
self?.deviceDataCollector = deviceData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vault manager
|
||||||
|
if let vaultManager = options["vaultManager"] as? Bool, vaultManager {
|
||||||
|
request.vaultManager = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card disabled
|
||||||
|
if let cardDisabled = options["cardDisabled"] as? Bool, cardDisabled {
|
||||||
|
request.cardDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apple Pay setup
|
||||||
|
if let applePay = options["applePay"] as? Bool, applePay {
|
||||||
|
self.setupApplePay(options: options, clientToken: clientToken, reject: reject)
|
||||||
|
// Don't disable Apple Pay in request
|
||||||
|
} else {
|
||||||
|
request.applePayDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Venmo setup
|
||||||
|
if let venmo = options["venmo"] as? Bool, venmo {
|
||||||
|
request.venmoDisabled = false
|
||||||
|
} else {
|
||||||
|
request.venmoDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayPal setup
|
||||||
|
if let payPal = options["payPal"] as? Bool, !payPal {
|
||||||
|
request.paypalDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Drop-In controller
|
||||||
|
let dropIn = BTDropInController(authorization: clientToken, request: request) { [weak self] (controller, result, error) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
controller.dismiss(animated: true) {
|
||||||
|
if let error = error {
|
||||||
|
self.currentReject?("DROP_IN_ERROR", error.localizedDescription, error)
|
||||||
|
} else if result?.isCanceled == true {
|
||||||
|
self.currentReject?("USER_CANCELLATION", "The user cancelled", nil)
|
||||||
|
} else if let result = result {
|
||||||
|
self.handleDropInResult(result, threeDSecureOptions: options["threeDSecure"] as? NSDictionary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let dropIn = dropIn {
|
||||||
|
guard let rootViewController = self.getRootViewController() else {
|
||||||
|
reject("NO_ROOT_VC", "Could not find root view controller", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rootViewController.present(dropIn, animated: true, completion: nil)
|
||||||
|
} else {
|
||||||
|
reject("INVALID_CLIENT_TOKEN", "The client token seems invalid", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupApplePay(options: NSDictionary, clientToken: String, reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
guard let merchantIdentifier = options["merchantIdentifier"] as? String,
|
||||||
|
let countryCode = options["countryCode"] as? String,
|
||||||
|
let currencyCode = options["currencyCode"] as? String,
|
||||||
|
let merchantName = options["merchantName"] as? String,
|
||||||
|
let orderTotal = options["orderTotal"] as? NSNumber else {
|
||||||
|
reject("MISSING_OPTIONS", "Not all required Apple Pay options were provided", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.braintreeClient = BTAPIClient(authorization: clientToken)
|
||||||
|
|
||||||
|
let paymentRequest = PKPaymentRequest()
|
||||||
|
paymentRequest.merchantIdentifier = merchantIdentifier
|
||||||
|
paymentRequest.merchantCapabilities = .capability3DS
|
||||||
|
paymentRequest.countryCode = countryCode
|
||||||
|
paymentRequest.currencyCode = currencyCode
|
||||||
|
paymentRequest.supportedNetworks = [.amex, .visa, .masterCard, .discover, .chinaUnionPay]
|
||||||
|
|
||||||
|
let paymentSummaryItem = PKPaymentSummaryItem(
|
||||||
|
label: merchantName,
|
||||||
|
amount: NSDecimalNumber(value: orderTotal.doubleValue)
|
||||||
|
)
|
||||||
|
paymentRequest.paymentSummaryItems = [paymentSummaryItem]
|
||||||
|
|
||||||
|
self.paymentRequest = paymentRequest
|
||||||
|
|
||||||
|
if let vc = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) {
|
||||||
|
vc.delegate = self
|
||||||
|
self.viewController = vc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleDropInResult(_ result: BTDropInResult, threeDSecureOptions: NSDictionary?) {
|
||||||
|
// Check for 3D Secure
|
||||||
|
if let threeDSecureOptions = threeDSecureOptions,
|
||||||
|
let cardNonce = result.paymentMethod as? BTCardNonce {
|
||||||
|
|
||||||
|
if let threeDSecureInfo = cardNonce.threeDSecureInfo {
|
||||||
|
if !threeDSecureInfo.liabilityShiftPossible && threeDSecureInfo.wasVerified {
|
||||||
|
self.currentReject?("3DSECURE_NOT_ABLE_TO_SHIFT_LIABILITY", "3D Secure liability cannot be shifted", nil)
|
||||||
|
return
|
||||||
|
} else if !threeDSecureInfo.liabilityShifted && threeDSecureInfo.wasVerified {
|
||||||
|
self.currentReject?("3DSECURE_LIABILITY_NOT_SHIFTED", "3D Secure liability was not shifted", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Apple Pay
|
||||||
|
if result.paymentMethod == nil,
|
||||||
|
let paymentMethodType = result.paymentMethodType?.rawValue,
|
||||||
|
(16...18).contains(paymentMethodType) {
|
||||||
|
// Apple Pay flow
|
||||||
|
if let viewController = self.viewController,
|
||||||
|
let rootViewController = self.getRootViewController() {
|
||||||
|
rootViewController.present(viewController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Venmo
|
||||||
|
if let venmoNonce = result.paymentMethod as? BTVenmoAccountNonce {
|
||||||
|
let resultDict: [String: Any] = [
|
||||||
|
"nonce": venmoNonce.nonce,
|
||||||
|
"type": "Venmo",
|
||||||
|
"description": "Venmo \(venmoNonce.username)",
|
||||||
|
"isDefault": false,
|
||||||
|
"deviceData": self.deviceDataCollector
|
||||||
|
]
|
||||||
|
self.currentResolve?(resultDict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default payment method handling
|
||||||
|
self.resolvePayment(result: result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func fetchMostRecentPaymentMethod(_ clientToken: String,
|
||||||
|
resolve: @escaping RCTPromiseResolveBlock,
|
||||||
|
reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
|
||||||
|
BTDropInResult.mostRecentPaymentMethod(forClientToken: clientToken) { [weak self] (result, error) in
|
||||||
|
if let error = error {
|
||||||
|
reject("FETCH_ERROR", error.localizedDescription, error)
|
||||||
|
} else if result?.isCanceled == true {
|
||||||
|
reject("USER_CANCELLATION", "The user cancelled", nil)
|
||||||
|
} else if let result = result {
|
||||||
|
self?.resolvePayment(result: result)
|
||||||
|
resolve(self?.buildResultDictionary(from: result))
|
||||||
|
} else {
|
||||||
|
resolve(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func tokenizeCard(_ clientToken: String,
|
||||||
|
cardInfo: NSDictionary,
|
||||||
|
resolve: @escaping RCTPromiseResolveBlock,
|
||||||
|
reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
|
||||||
|
let cvv = cardInfo["cvv"] as? String
|
||||||
|
let onlyCVV = cardInfo["onlyCVV"] as? Bool ?? false
|
||||||
|
|
||||||
|
if onlyCVV {
|
||||||
|
guard cvv != nil else {
|
||||||
|
reject("INVALID_CARD_INFO", "Please enter cvv", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
guard let _ = cardInfo["number"] as? String,
|
||||||
|
let _ = cardInfo["expirationMonth"] as? String,
|
||||||
|
let _ = cardInfo["expirationYear"] as? String,
|
||||||
|
cvv != nil,
|
||||||
|
let _ = cardInfo["postalCode"] as? String else {
|
||||||
|
reject("INVALID_CARD_INFO", "Invalid card info", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let braintreeClient = BTAPIClient(authorization: clientToken)!
|
||||||
|
let cardClient = BTCardClient(apiClient: braintreeClient)
|
||||||
|
let card = BTCard()
|
||||||
|
|
||||||
|
if !onlyCVV {
|
||||||
|
card.number = cardInfo["number"] as? String
|
||||||
|
card.expirationMonth = cardInfo["expirationMonth"] as? String
|
||||||
|
card.expirationYear = cardInfo["expirationYear"] as? String
|
||||||
|
card.postalCode = cardInfo["postalCode"] as? String
|
||||||
|
}
|
||||||
|
card.cvv = cvv
|
||||||
|
|
||||||
|
cardClient.tokenize(card) { [weak self] (tokenizedCard, error) in
|
||||||
|
if let error = error {
|
||||||
|
reject("TOKENIZE_ERROR", "Error tokenizing card", error)
|
||||||
|
} else if let tokenizedCard = tokenizedCard {
|
||||||
|
var jsResult: [String: Any] = [
|
||||||
|
"nonce": tokenizedCard.nonce,
|
||||||
|
"type": "card"
|
||||||
|
]
|
||||||
|
|
||||||
|
let dataCollector = BTDataCollector(apiClient: braintreeClient)
|
||||||
|
dataCollector.collectDeviceData { deviceData in
|
||||||
|
if !deviceData.isEmpty {
|
||||||
|
jsResult["deviceData"] = deviceData
|
||||||
|
resolve(jsResult)
|
||||||
|
} else {
|
||||||
|
reject("DEVICE_DATA_ERROR", "Failed to collect device data", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func collectDeviceData(_ clientToken: String,
|
||||||
|
resolve: @escaping RCTPromiseResolveBlock,
|
||||||
|
reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
|
||||||
|
let apiClient = BTAPIClient(authorization: clientToken)!
|
||||||
|
let dataCollector = BTDataCollector(apiClient: apiClient)
|
||||||
|
|
||||||
|
dataCollector.collectDeviceData { deviceData in
|
||||||
|
if !deviceData.isEmpty {
|
||||||
|
resolve(deviceData)
|
||||||
|
} else {
|
||||||
|
reject("DEVICE_DATA_ERROR", "Failed to collect device data", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvePayment(result: BTDropInResult) {
|
||||||
|
guard let resolve = self.currentResolve else { return }
|
||||||
|
let resultDict = buildResultDictionary(from: result)
|
||||||
|
resolve(resultDict)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildResultDictionary(from result: BTDropInResult) -> [String: Any]? {
|
||||||
|
guard let paymentMethod = result.paymentMethod else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"nonce": paymentMethod.nonce,
|
||||||
|
"type": paymentMethod.type,
|
||||||
|
"description": result.paymentDescription ?? "",
|
||||||
|
"isDefault": paymentMethod.isDefault,
|
||||||
|
"deviceData": self.deviceDataCollector
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getRootViewController() -> UIViewController? {
|
||||||
|
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootViewController = window.rootViewController
|
||||||
|
if let presented = rootViewController?.presentedViewController {
|
||||||
|
rootViewController = presented
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootViewController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - PKPaymentAuthorizationViewControllerDelegate
|
||||||
|
extension RNBraintreeDropIn: PKPaymentAuthorizationViewControllerDelegate {
|
||||||
|
|
||||||
|
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController,
|
||||||
|
didAuthorizePayment payment: PKPayment,
|
||||||
|
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
|
||||||
|
|
||||||
|
guard let braintreeClient = self.braintreeClient else {
|
||||||
|
completion(PKPaymentAuthorizationResult(status: .failure, errors: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let applePayClient = BTApplePayClient(apiClient: braintreeClient)
|
||||||
|
applePayClient.tokenize(payment) { [weak self] (tokenizedPayment, error) in
|
||||||
|
if let tokenizedPayment = tokenizedPayment {
|
||||||
|
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
|
||||||
|
self?.applePayAuthorized = true
|
||||||
|
|
||||||
|
let result: [String: Any] = [
|
||||||
|
"nonce": tokenizedPayment.nonce,
|
||||||
|
"type": "Apple Pay",
|
||||||
|
"description": "Apple Pay \(tokenizedPayment.type ?? "")",
|
||||||
|
"isDefault": false,
|
||||||
|
"deviceData": self?.deviceDataCollector ?? ""
|
||||||
|
]
|
||||||
|
|
||||||
|
self?.currentResolve?(result)
|
||||||
|
} else {
|
||||||
|
completion(PKPaymentAuthorizationResult(status: .failure, errors: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
|
||||||
|
controller.dismiss(animated: true) { [weak self] in
|
||||||
|
if self?.applePayAuthorized == false {
|
||||||
|
self?.currentReject?("USER_CANCELLATION", "The user cancelled", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/commonjs/NativeRNBraintreeDropIn.js
Normal file
9
lib/commonjs/NativeRNBraintreeDropIn.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = void 0;
|
||||||
|
var _reactNative = require("react-native");
|
||||||
|
var _default = exports.default = _reactNative.TurboModuleRegistry.getEnforcing('RNBraintreeDropIn');
|
||||||
|
//# sourceMappingURL=NativeRNBraintreeDropIn.js.map
|
||||||
1
lib/commonjs/NativeRNBraintreeDropIn.js.map
Normal file
1
lib/commonjs/NativeRNBraintreeDropIn.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeRNBraintreeDropIn.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAAmD,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GAgDpCC,gCAAmB,CAACC,YAAY,CAAO,mBAAmB,CAAC","ignoreList":[]}
|
||||||
48
lib/commonjs/index.js
Normal file
48
lib/commonjs/index.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = void 0;
|
||||||
|
var _NativeRNBraintreeDropIn = _interopRequireDefault(require("./NativeRNBraintreeDropIn"));
|
||||||
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
||||||
|
class BraintreeDropIn {
|
||||||
|
/**
|
||||||
|
* Show the Braintree Drop-In UI
|
||||||
|
* @param options Configuration options for Drop-In
|
||||||
|
* @returns Promise resolving to payment result
|
||||||
|
*/
|
||||||
|
static show(options) {
|
||||||
|
return _NativeRNBraintreeDropIn.default.show(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the most recent payment method for a client token
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @returns Promise resolving to payment result or null
|
||||||
|
*/
|
||||||
|
static fetchMostRecentPaymentMethod(clientToken) {
|
||||||
|
return _NativeRNBraintreeDropIn.default.fetchMostRecentPaymentMethod(clientToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenize a card
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @param cardInfo Card information
|
||||||
|
* @returns Promise resolving to payment result
|
||||||
|
*/
|
||||||
|
static tokenizeCard(clientToken, cardInfo) {
|
||||||
|
return _NativeRNBraintreeDropIn.default.tokenizeCard(clientToken, cardInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect device data for fraud detection
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @returns Promise resolving to device data string
|
||||||
|
*/
|
||||||
|
static collectDeviceData(clientToken) {
|
||||||
|
return _NativeRNBraintreeDropIn.default.collectDeviceData(clientToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.default = BraintreeDropIn;
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
lib/commonjs/index.js.map
Normal file
1
lib/commonjs/index.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"names":["_NativeRNBraintreeDropIn","_interopRequireDefault","require","e","__esModule","default","BraintreeDropIn","show","options","NativeRNBraintreeDropIn","fetchMostRecentPaymentMethod","clientToken","tokenizeCard","cardInfo","collectDeviceData","exports"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;AAAA,IAAAA,wBAAA,GAAAC,sBAAA,CAAAC,OAAA;AAAgE,SAAAD,uBAAAE,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAKjD,MAAMG,eAAe,CAAC;EACnC;AACF;AACA;AACA;AACA;EACE,OAAOC,IAAIA,CAACC,OAAsB,EAA0B;IAC1D,OAAOC,gCAAuB,CAACF,IAAI,CAACC,OAAO,CAAC;EAC9C;;EAEA;AACF;AACA;AACA;AACA;EACE,OAAOE,4BAA4BA,CACjCC,WAAmB,EACY;IAC/B,OAAOF,gCAAuB,CAACC,4BAA4B,CAACC,WAAW,CAAC;EAC1E;;EAEA;AACF;AACA;AACA;AACA;AACA;EACE,OAAOC,YAAYA,CACjBD,WAAmB,EACnBE,QAAkB,EACM;IACxB,OAAOJ,gCAAuB,CAACG,YAAY,CAACD,WAAW,EAAEE,QAAQ,CAAC;EACpE;;EAEA;AACF;AACA;AACA;AACA;EACE,OAAOC,iBAAiBA,CAACH,WAAmB,EAAmB;IAC7D,OAAOF,gCAAuB,CAACK,iBAAiB,CAACH,WAAW,CAAC;EAC/D;AACF;AAACI,OAAA,CAAAV,OAAA,GAAAC,eAAA","ignoreList":[]}
|
||||||
3
lib/module/NativeRNBraintreeDropIn.js
Normal file
3
lib/module/NativeRNBraintreeDropIn.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { TurboModuleRegistry } from 'react-native';
|
||||||
|
export default TurboModuleRegistry.getEnforcing('RNBraintreeDropIn');
|
||||||
|
//# sourceMappingURL=NativeRNBraintreeDropIn.js.map
|
||||||
1
lib/module/NativeRNBraintreeDropIn.js.map
Normal file
1
lib/module/NativeRNBraintreeDropIn.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeRNBraintreeDropIn.ts"],"mappings":"AACA,SAASA,mBAAmB,QAAQ,cAAc;AAgDlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,mBAAmB,CAAC","ignoreList":[]}
|
||||||
40
lib/module/index.js
Normal file
40
lib/module/index.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import NativeRNBraintreeDropIn from './NativeRNBraintreeDropIn';
|
||||||
|
export default class BraintreeDropIn {
|
||||||
|
/**
|
||||||
|
* Show the Braintree Drop-In UI
|
||||||
|
* @param options Configuration options for Drop-In
|
||||||
|
* @returns Promise resolving to payment result
|
||||||
|
*/
|
||||||
|
static show(options) {
|
||||||
|
return NativeRNBraintreeDropIn.show(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the most recent payment method for a client token
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @returns Promise resolving to payment result or null
|
||||||
|
*/
|
||||||
|
static fetchMostRecentPaymentMethod(clientToken) {
|
||||||
|
return NativeRNBraintreeDropIn.fetchMostRecentPaymentMethod(clientToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenize a card
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @param cardInfo Card information
|
||||||
|
* @returns Promise resolving to payment result
|
||||||
|
*/
|
||||||
|
static tokenizeCard(clientToken, cardInfo) {
|
||||||
|
return NativeRNBraintreeDropIn.tokenizeCard(clientToken, cardInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect device data for fraud detection
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @returns Promise resolving to device data string
|
||||||
|
*/
|
||||||
|
static collectDeviceData(clientToken) {
|
||||||
|
return NativeRNBraintreeDropIn.collectDeviceData(clientToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
lib/module/index.js.map
Normal file
1
lib/module/index.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"names":["NativeRNBraintreeDropIn","BraintreeDropIn","show","options","fetchMostRecentPaymentMethod","clientToken","tokenizeCard","cardInfo","collectDeviceData"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":"AAAA,OAAOA,uBAAuB,MAAM,2BAA2B;AAK/D,eAAe,MAAMC,eAAe,CAAC;EACnC;AACF;AACA;AACA;AACA;EACE,OAAOC,IAAIA,CAACC,OAAsB,EAA0B;IAC1D,OAAOH,uBAAuB,CAACE,IAAI,CAACC,OAAO,CAAC;EAC9C;;EAEA;AACF;AACA;AACA;AACA;EACE,OAAOC,4BAA4BA,CACjCC,WAAmB,EACY;IAC/B,OAAOL,uBAAuB,CAACI,4BAA4B,CAACC,WAAW,CAAC;EAC1E;;EAEA;AACF;AACA;AACA;AACA;AACA;EACE,OAAOC,YAAYA,CACjBD,WAAmB,EACnBE,QAAkB,EACM;IACxB,OAAOP,uBAAuB,CAACM,YAAY,CAACD,WAAW,EAAEE,QAAQ,CAAC;EACpE;;EAEA;AACF;AACA;AACA;AACA;EACE,OAAOC,iBAAiBA,CAACH,WAAmB,EAAmB;IAC7D,OAAOL,uBAAuB,CAACQ,iBAAiB,CAACH,WAAW,CAAC;EAC/D;AACF","ignoreList":[]}
|
||||||
94
package.json
Normal file
94
package.json
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"name": "react-native-braintree-dropin-turbo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Braintree Drop-In UI for React Native with Turbo Modules",
|
||||||
|
"main": "lib/commonjs/index",
|
||||||
|
"module": "lib/module/index",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"react-native": "src/index.ts",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"lib",
|
||||||
|
"android",
|
||||||
|
"ios",
|
||||||
|
"cpp",
|
||||||
|
"*.podspec",
|
||||||
|
"!lib/typescript/example",
|
||||||
|
"!ios/build",
|
||||||
|
"!android/build",
|
||||||
|
"!android/gradle",
|
||||||
|
"!android/gradlew",
|
||||||
|
"!android/gradlew.bat",
|
||||||
|
"!**/__tests__",
|
||||||
|
"!**/__fixtures__",
|
||||||
|
"!**/__mocks__",
|
||||||
|
"!**/.*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
||||||
|
"prepack": "bob build",
|
||||||
|
"release": "release-it",
|
||||||
|
"example": "yarn --cwd example",
|
||||||
|
"bootstrap": "yarn example && yarn install"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"react-native",
|
||||||
|
"ios",
|
||||||
|
"android",
|
||||||
|
"braintree",
|
||||||
|
"payment",
|
||||||
|
"turbo-modules"
|
||||||
|
],
|
||||||
|
"repository": "https://github.com/aveekshan/react-native-braintree-dropin-turbo",
|
||||||
|
"author": "Veekshan <alladavekshan@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/aveekshan/react-native-braintree-dropin-turbo/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/aveekshan/react-native-braintree-dropin-turbo#readme",
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@react-native-community/eslint-config": "^3.2.0",
|
||||||
|
"@types/react": "~18.2.45",
|
||||||
|
"@types/react-native": "0.72.8",
|
||||||
|
"eslint": "^8.51.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-native": "0.73.0",
|
||||||
|
"react-native-builder-bob": "^0.23.2",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
},
|
||||||
|
"codegenConfig": {
|
||||||
|
"name": "RNBraintreeDropInSpec",
|
||||||
|
"type": "modules",
|
||||||
|
"jsSrcsDir": "src",
|
||||||
|
"android": {
|
||||||
|
"javaPackageName": "com.braintreedroptinturbo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"react-native-builder-bob": {
|
||||||
|
"source": "src",
|
||||||
|
"output": "lib",
|
||||||
|
"targets": [
|
||||||
|
"commonjs",
|
||||||
|
"module",
|
||||||
|
[
|
||||||
|
"typescript",
|
||||||
|
{
|
||||||
|
"project": "tsconfig.build.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/NativeRNBraintreeDropIn.ts
Normal file
50
src/NativeRNBraintreeDropIn.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { TurboModule } from 'react-native';
|
||||||
|
import { TurboModuleRegistry } from 'react-native';
|
||||||
|
|
||||||
|
export interface DropInOptions {
|
||||||
|
clientToken: string;
|
||||||
|
darkTheme?: boolean;
|
||||||
|
fontFamily?: string;
|
||||||
|
boldFontFamily?: string;
|
||||||
|
vaultManager?: boolean;
|
||||||
|
cardDisabled?: boolean;
|
||||||
|
applePay?: boolean;
|
||||||
|
merchantIdentifier?: string;
|
||||||
|
countryCode?: string;
|
||||||
|
currencyCode?: string;
|
||||||
|
merchantName?: string;
|
||||||
|
orderTotal?: number;
|
||||||
|
venmo?: boolean;
|
||||||
|
payPal?: boolean;
|
||||||
|
googlePay?: boolean;
|
||||||
|
googlePayMerchantId?: string;
|
||||||
|
threeDSecure?: {
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardInfo {
|
||||||
|
number?: string;
|
||||||
|
expirationMonth?: string;
|
||||||
|
expirationYear?: string;
|
||||||
|
cvv: string;
|
||||||
|
postalCode?: string;
|
||||||
|
onlyCVV?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentResult {
|
||||||
|
nonce: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
deviceData: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Spec extends TurboModule {
|
||||||
|
show(options: DropInOptions): Promise<PaymentResult>;
|
||||||
|
fetchMostRecentPaymentMethod(clientToken: string): Promise<PaymentResult | null>;
|
||||||
|
tokenizeCard(clientToken: string, cardInfo: CardInfo): Promise<PaymentResult>;
|
||||||
|
collectDeviceData(clientToken: string): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TurboModuleRegistry.getEnforcing<Spec>('RNBraintreeDropIn');
|
||||||
48
src/index.ts
Normal file
48
src/index.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import NativeRNBraintreeDropIn from './NativeRNBraintreeDropIn';
|
||||||
|
import type { DropInOptions, CardInfo, PaymentResult } from './NativeRNBraintreeDropIn';
|
||||||
|
|
||||||
|
export type { DropInOptions, CardInfo, PaymentResult };
|
||||||
|
|
||||||
|
export default class BraintreeDropIn {
|
||||||
|
/**
|
||||||
|
* Show the Braintree Drop-In UI
|
||||||
|
* @param options Configuration options for Drop-In
|
||||||
|
* @returns Promise resolving to payment result
|
||||||
|
*/
|
||||||
|
static show(options: DropInOptions): Promise<PaymentResult> {
|
||||||
|
return NativeRNBraintreeDropIn.show(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the most recent payment method for a client token
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @returns Promise resolving to payment result or null
|
||||||
|
*/
|
||||||
|
static fetchMostRecentPaymentMethod(
|
||||||
|
clientToken: string
|
||||||
|
): Promise<PaymentResult | null> {
|
||||||
|
return NativeRNBraintreeDropIn.fetchMostRecentPaymentMethod(clientToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenize a card
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @param cardInfo Card information
|
||||||
|
* @returns Promise resolving to payment result
|
||||||
|
*/
|
||||||
|
static tokenizeCard(
|
||||||
|
clientToken: string,
|
||||||
|
cardInfo: CardInfo
|
||||||
|
): Promise<PaymentResult> {
|
||||||
|
return NativeRNBraintreeDropIn.tokenizeCard(clientToken, cardInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect device data for fraud detection
|
||||||
|
* @param clientToken Braintree client token
|
||||||
|
* @returns Promise resolving to device data string
|
||||||
|
*/
|
||||||
|
static collectDeviceData(clientToken: string): Promise<string> {
|
||||||
|
return NativeRNBraintreeDropIn.collectDeviceData(clientToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
tsconfig.build.json
Normal file
11
tsconfig.build.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"example"
|
||||||
|
]
|
||||||
|
}
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["es2017"],
|
||||||
|
"allowJs": false,
|
||||||
|
"jsx": "react-native",
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"lib",
|
||||||
|
"example"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user