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