From 96a9b5348de40d87bd0d3e1a0ed95040f46aab3f Mon Sep 17 00:00:00 2001 From: Twig Date: Mon, 18 Aug 2025 17:07:20 -0400 Subject: [PATCH 001/103] Configuring successfully so far --- .../reactnativestripesdk/StripeSdkModule.kt | 130 ++++++++++++++++++ .../NativeStripeSdkModuleSpec.java | 8 ++ example/src/App.tsx | 6 + example/src/screens/CryptoOnrampScreen.tsx | 109 +++++++++++++++ example/src/screens/HomeScreen.tsx | 8 ++ src/functions.ts | 8 ++ src/hooks/useStripe.tsx | 18 +++ src/specs/NativeStripeSdkModule.ts | 2 + 8 files changed, 289 insertions(+) create mode 100644 example/src/screens/CryptoOnrampScreen.tsx diff --git a/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt b/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt index ae5dba8a7e..a307af3fcf 100644 --- a/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt +++ b/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt @@ -71,6 +71,20 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.json.JSONObject +import androidx.compose.ui.graphics.Color +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.stripe.android.crypto.onramp.OnrampCoordinator +import com.stripe.android.crypto.onramp.model.OnrampConfiguration +import com.stripe.android.crypto.onramp.model.OnrampLinkLookupResult +import com.stripe.android.link.model.LinkAppearance +import com.stripe.android.link.model.LinkAppearance.Colors +import com.stripe.android.link.model.LinkAppearance.PrimaryButton +import com.stripe.android.link.model.LinkAppearance.Style +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext +import androidx.lifecycle.SavedStateHandle + @ReactModule(name = StripeSdkModule.NAME) class StripeSdkModule( reactContext: ReactApplicationContext, @@ -97,6 +111,8 @@ class StripeSdkModule( internal var composeCompatView: StripeAbstractComposeView.CompatView? = null + private var coordinator: OnrampCoordinator? = null + // If you create a new Fragment, you must put the tag here, otherwise result callbacks for that // Fragment will not work on RN < 0.65 private val allStripeFragmentTags: List @@ -1403,6 +1419,120 @@ class StripeSdkModule( } } + @ReactMethod + override fun configureOnramp( + config: ReadableMap, + promise: Promise, + ) { + val application = currentActivity?.application ?: (reactApplicationContext.applicationContext as? Application) + if (application == null) { + promise.reject("NO_APPLICATION", "Could not get Application instance") + return + } + + coordinator = OnrampCoordinator.Builder() + .build(application, SavedStateHandle()) + + CoroutineScope(Dispatchers.IO).launch { + val appearanceMap = config.getMap("appearance") + val appearance = + if (appearanceMap != null) { + mapAppearance(appearanceMap) + } else { + LinkAppearance(style = Style.AUTOMATIC) + } + + val configuration = OnrampConfiguration(appearance) + coordinator?.configure(configuration) + promise.resolve(true) + } + } + + @ReactMethod + override fun lookupLinkUser( + email: String, + promise: Promise, + ) { + CoroutineScope(Dispatchers.IO).launch { + when (val result = coordinator?.lookupLinkUser(email)) { + is OnrampLinkLookupResult.Completed -> { + promise.resolve( + Arguments.createMap().apply { + putBoolean("isLinkUser", result.isLinkUser) + }, + ) + } + is OnrampLinkLookupResult.Failed -> { + promise.reject("LookupError", result.error) + } + else -> { + promise.reject("LookupError", "Unknown result") + } + } + } + } + + private fun mapAppearance(appearanceMap: ReadableMap): LinkAppearance { + val lightColorsMap = appearanceMap.getMap("lightColors") + val darkColorsMap = appearanceMap.getMap("darkColors") + val styleStr = appearanceMap.getString("style") + val primaryButtonMap = appearanceMap.getMap("primaryButton") + + val lightColors = + if (lightColorsMap != null) { + Colors( + primary = Color(lightColorsMap.getInt("primary")), + borderSelected = Color(lightColorsMap.getInt("borderSelected")), + ) + } else { + Colors.default(isDark = false) + } + + val darkColors = + if (darkColorsMap != null) { + Colors( + primary = Color(darkColorsMap.getInt("primary")), + borderSelected = Color(darkColorsMap.getInt("borderSelected")), + ) + } else { + Colors.default(isDark = true) + } + + val style = + when (styleStr) { + "ALWAYS_LIGHT" -> Style.ALWAYS_LIGHT + "ALWAYS_DARK" -> Style.ALWAYS_DARK + else -> Style.AUTOMATIC + } + + val primaryButton = + if (primaryButtonMap != null) { + PrimaryButton( + cornerRadiusDp = + if (primaryButtonMap.hasKey("cornerRadiusDp")) { + primaryButtonMap.getDouble("cornerRadiusDp").toFloat() + } else { + null + }, + heightDp = + if (primaryButtonMap.hasKey("heightDp")) { + primaryButtonMap.getDouble("heightDp").toFloat() + } else { + null + }, + ) + } else { + PrimaryButton() + } + + return LinkAppearance( + lightColors = lightColors, + darkColors = darkColors, + style = style, + primaryButton = primaryButton, + ) + } + companion object { const val NAME = NativeStripeSdkModuleSpec.NAME } diff --git a/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java b/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java index 04e64dba26..b61dee82b8 100644 --- a/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java +++ b/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java @@ -315,4 +315,12 @@ protected final void emitOnCustomPaymentMethodConfirmHandlerCallback(ReadableMap @ReactMethod @DoNotStrip public abstract void clearEmbeddedPaymentOption(double viewTag, Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void configureOnramp(ReadableMap config, Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void lookupLinkUser(String email, Promise promise); } diff --git a/example/src/App.tsx b/example/src/App.tsx index 556eb00677..5fed1e3208 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -49,6 +49,7 @@ import CustomerSheetScreen from './screens/CustomerSheetScreen'; import RevolutPayScreen from './screens/RevolutPayScreen'; import type { EmbeddedPaymentElementResult } from '@stripe/stripe-react-native'; import PaymentSheetWithPmoSfuScreen from './screens/PaymentSheetWithPmoSfuScreen'; +import CryptoOnrampScreen from './screens/CryptoOnrampScreen'; const Stack = createNativeStackNavigator(); @@ -100,6 +101,7 @@ export type RootStackParamList = { CustomerSheetScreen: undefined; RevolutPayScreen: undefined; PaymentSheetWithPmoSfuScreen: undefined; + CryptoOnrampScreen: undefined; }; declare global { @@ -275,6 +277,10 @@ export default function App() { name="PaymentSheetWithPmoSfuScreen" component={PaymentSheetWithPmoSfuScreen} /> + diff --git a/example/src/screens/CryptoOnrampScreen.tsx b/example/src/screens/CryptoOnrampScreen.tsx new file mode 100644 index 0000000000..7364aa4aab --- /dev/null +++ b/example/src/screens/CryptoOnrampScreen.tsx @@ -0,0 +1,109 @@ +import React, { useCallback, useState, useEffect } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { + StyleSheet, + View, + ScrollView, + Alert, + Text, + TextInput, +} from 'react-native'; +import { colors } from '../colors'; +import Button from '../components/Button'; +import { useStripe } from '@stripe/stripe-react-native'; + +export default function CryptoOnrampScreen() { + const navigation = useNavigation(); + const { configureOnramp, lookupLinkUser } = useStripe(); + const [email, setEmail] = useState(''); + + useEffect(() => { + const config = { + appearance: { + lightColors: { + primary: 0xff6200ee, // Example: purple + borderSelected: 0xff03dac6, // Example: teal + }, + darkColors: { + primary: 0xffbb86fc, // Example: light purple + borderSelected: 0xff3700b3, // Example: dark purple + }, + style: 'ALWAYS_DARK', // or "ALWAYS_LIGHT", "ALWAYS_DARK" + primaryButton: { + cornerRadiusDp: 8, + heightDp: 48, + }, + }, + }; + + configureOnramp(config) + .then(() => { + console.error('Onramp configured successfully.'); + }) + .catch((error: any) => { + console.error('Error configuring Onramp:', error); + }); + }, [configureOnramp]); + + const checkIsLinkUser = useCallback(async () => { + try { + const result = await lookupLinkUser(email); + const isLinkUser = result?.isLinkUser ?? false; + Alert.alert('Result', `Is Link User: ${isLinkUser}`); + } catch (error) { + console.error('Error checking link user:', error); + Alert.alert('Error', 'An error occurred while checking link user.'); + } + }, [email, lookupLinkUser]); + + return ( + + + Enter your email address: + +