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

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