This commit is contained in:
Condi
2026-02-13 15:22:47 +08:00
parent dd42dc04ce
commit 1c49093a73
29 changed files with 8311 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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>

View 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
View 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
View 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
View 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'
}

View File

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -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"))
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,6 @@
package com.braintreedroptinturbo
import com.facebook.react.bridge.ReactApplicationContext
abstract class NativeRNBraintreeDropInSpec(context: ReactApplicationContext) :
NativeRNBraintreeDropInSpecBase(context)

View File

@@ -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
View 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
View 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
View 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)
}
}
}
}

View 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

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

View 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":[]}

View File

@@ -0,0 +1,3 @@
import { TurboModuleRegistry } from 'react-native';
export default TurboModuleRegistry.getEnforcing('RNBraintreeDropIn');
//# sourceMappingURL=NativeRNBraintreeDropIn.js.map

View 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
View 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
View 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
View 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"
}
]
]
}
}

View 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
View 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
View File

@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true
},
"exclude": [
"node_modules",
"example"
]
}

22
tsconfig.json Normal file
View 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"
]
}

6255
yarn.lock Normal file

File diff suppressed because it is too large Load Diff