Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
db0fa9f
Embedded components
gimenete-stripe Oct 31, 2025
070f198
Make react-native-webview optional and load it dynamically
gimenete-stripe Nov 4, 2025
e4f2e69
Extract types from @stripe/connect-js
gimenete-stripe Nov 4, 2025
c32af78
Custom font
gimenete-stripe Nov 5, 2025
8f4d601
Use a modal for the onboarding component
gimenete-stripe Nov 12, 2025
9f32ca6
Merge branch 'master' into gimenete/embedded-components
gimenete-stripe Nov 12, 2025
5ea7e9b
Update podfile.lcok
gimenete-stripe Nov 12, 2025
e62cd98
Hide "overrides" from public interface
gimenete-stripe Nov 12, 2025
7d03d7a
Change initialization API. Add toggles to switch appearance and locale
gimenete-stripe Nov 13, 2025
29f09f9
Remove some alerts
gimenete-stripe Nov 13, 2025
e2a597b
Implement secure webviews
gimenete-stripe Nov 14, 2025
c8654e2
Implement secure webviews
gimenete-stripe Nov 14, 2025
f1acd01
Merge branch 'master' into gimenete/embedded-components
gimenete-stripe Nov 14, 2025
91f52cc
Add endpoint to server
gimenete-stripe Nov 14, 2025
8e58819
Improvements for Android
gimenete-stripe Nov 17, 2025
a6e2c87
Extract callback
gimenete-stripe Nov 20, 2025
146b31c
Trying to fix deep linking on Android
gimenete-stripe Nov 20, 2025
3120ef3
Native navigation bar
gimenete-stripe Nov 21, 2025
ef8630a
Native navigation bar
gimenete-stripe Nov 21, 2025
2001d48
Small changes
gimenete-stripe Nov 25, 2025
3066e48
Remember previous screen
gimenete-stripe Nov 25, 2025
f41b48e
Merge branch 'master' into gimenete/embedded-components
gimenete-stripe Nov 25, 2025
6f9688b
Apply suggestions from code review
gimenete-stripe Nov 25, 2025
c3c69f9
Make onCloseButtonPress a constant
gimenete-stripe Nov 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'androidx.browser:browser:1.5.0'

// play-services-wallet is already included in stripe-android
compileOnly "com.google.android.gms:play-services-wallet:19.3.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.reactnativestripesdk

import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.NavigationBarManagerDelegate
import com.facebook.react.viewmanagers.NavigationBarManagerInterface

@ReactModule(name = NavigationBarManager.REACT_CLASS)
class NavigationBarManager :
SimpleViewManager<NavigationBarView>(),
NavigationBarManagerInterface<NavigationBarView> {
private val delegate = NavigationBarManagerDelegate(this)

override fun getName() = REACT_CLASS

override fun getDelegate() = delegate

override fun getExportedCustomDirectEventTypeConstants() =
mutableMapOf(
EVENT_ON_CLOSE_BUTTON_PRESS to mutableMapOf("registrationName" to EVENT_ON_CLOSE_BUTTON_PRESS),
)

@ReactProp(name = "title")
override fun setTitle(
view: NavigationBarView,
title: String?,
) {
view.setTitle(title)
}

override fun createViewInstance(reactContext: ThemedReactContext): NavigationBarView = NavigationBarView(reactContext)

companion object {
const val REACT_CLASS = "NavigationBar"
private const val EVENT_ON_CLOSE_BUTTON_PRESS = "onCloseButtonPress"
}
}
119 changes: 119 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/NavigationBarView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.reactnativestripesdk

import android.annotation.SuppressLint
import android.graphics.Color
import android.view.Gravity
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import com.facebook.react.bridge.Arguments
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.Event

@SuppressLint("ViewConstructor")
class NavigationBarView(
context: ThemedReactContext,
) : FrameLayout(context) {
private val toolbar: Toolbar
private val titleTextView: TextView
private var titleText: String? = null

init {
// Create Toolbar
toolbar =
Toolbar(context).apply {
layoutParams =
LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT,
)
setBackgroundColor(Color.WHITE)
elevation = 4f
}

// Create title TextView
titleTextView =
TextView(context).apply {
textSize = 17f
setTextColor(Color.BLACK)
gravity = Gravity.CENTER
}

// Add title to toolbar
val titleParams =
Toolbar
.LayoutParams(
Toolbar.LayoutParams.WRAP_CONTENT,
Toolbar.LayoutParams.WRAP_CONTENT,
).apply {
gravity = Gravity.CENTER
}
toolbar.addView(titleTextView, titleParams)

// Create close button
val closeButton =
ImageButton(context).apply {
setImageDrawable(
context.resources.getDrawable(
android.R.drawable.ic_menu_close_clear_cancel,
null,
),
)
setBackgroundColor(Color.TRANSPARENT)
setOnClickListener {
dispatchCloseButtonPress()
}
}

// Add close button to toolbar
val buttonParams =
Toolbar
.LayoutParams(
Toolbar.LayoutParams.WRAP_CONTENT,
Toolbar.LayoutParams.WRAP_CONTENT,
).apply {
gravity = Gravity.END or Gravity.CENTER_VERTICAL
marginEnd = 16
}
toolbar.addView(closeButton, buttonParams)

// Add toolbar to this view
addView(toolbar)
}

fun setTitle(title: String?) {
titleText = title
titleTextView.text = title
}

private fun dispatchCloseButtonPress() {
val event =
CloseButtonPressEvent(
getContext().surfaceId,
id,
)
UIManagerHelper.getEventDispatcherForReactTag(getContext(), id)?.dispatchEvent(event)
}

override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int,
) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// Set a fixed height for the navigation bar
val desiredHeight = (56 * resources.displayMetrics.density).toInt()
val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(desiredHeight, MeasureSpec.EXACTLY)
super.onMeasure(widthMeasureSpec, newHeightMeasureSpec)
}

private class CloseButtonPressEvent(
surfaceId: Int,
viewId: Int,
) : Event<CloseButtonPressEvent>(surfaceId, viewId) {
override fun getEventName() = "onCloseButtonPress"

override fun getEventData() = Arguments.createMap()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,39 @@ class StripeSdkModule(
// noop, iOS only
}

@ReactMethod
override fun openAuthenticatedWebView(
id: String,
url: String,
promise: Promise,
) {
val activity = getCurrentActivityOrResolveWithError(promise) ?: return

UiThreadUtil.runOnUiThread {
try {
val uri = android.net.Uri.parse(url)
val builder =
androidx.browser.customtabs.CustomTabsIntent
.Builder()

// Set toolbar color for better UX
builder.setShowTitle(true)
builder.setUrlBarHidingEnabled(true)

val customTabsIntent = builder.build()
Comment on lines +1342 to +1351

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: You can probably move this outside of runOnUiThread


// Note: Custom Tabs doesn't have built-in redirect handling like iOS ASWebAuthenticationSession.
// The redirect will be handled via deep linking when the auth server redirects to stripe-connect://
// The React Native Linking module will capture the deep link and pass it back to the JS layer.
customTabsIntent.launchUrl(activity, uri)

promise.resolve(null)
} catch (e: Exception) {
promise.resolve(createError("Failed", e))
}
}
}

override fun addListener(eventType: String?) {
// noop, iOS only
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ class StripeSdkPackage : BaseReactPackage() {
AddToWalletButtonManager(reactContext),
AddressSheetViewManager(),
EmbeddedPaymentElementViewManager(),
NavigationBarManager(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ private void invoke(String eventName) {
@DoNotStrip
public abstract void setFinancialConnectionsForceNativeFlow(boolean enabled, Promise promise);

@ReactMethod
@DoNotStrip
public abstract void openAuthenticatedWebView(String id, String url, Promise promise);

@ReactMethod
@DoNotStrip
public abstract void addListener(String eventType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.microsoft.reacttestapp

import android.content.Intent
import android.os.Bundle
import android.util.Log

/**
* Custom MainActivity that extends RNTA's base to handle deep links with singleTask.
* This file shadows the one in node_modules/react-native-test-app.
*/
class MainActivity : com.facebook.react.ReactActivity() {

companion object {
private const val TAG = "MainActivity"
const val REQUEST_CODE_PERMISSIONS = 42
}

private val testApp: TestApp
get() = application as TestApp

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate with intent: ${intent?.dataString}")
}

/**
* CRITICAL: This method handles deep links when the app is already running.
* With launchMode="singleTask", this is called instead of onCreate when
* a deep link brings the app to foreground.
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) // ReactActivity handles sending to Linking module
Log.d(TAG, "onNewIntent with data: ${intent.dataString}")
setIntent(intent) // Update current intent
}

override fun getMainComponentName(): String? {
return testApp.manifest.singleApp ?: "example"
}
}
38 changes: 38 additions & 0 deletions example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,41 @@ allprojects {
google()
}
}

// Fix MainActivity launchMode and add deep link for stripe-connect
subprojects { subproject ->
subproject.afterEvaluate {
subproject.tasks.matching { task ->
task.name == 'generateAndroidManifest'
}.configureEach { task ->
task.doLast {
def manifestFile = file("${subproject.buildDir}/generated/rnta/src/main/AndroidManifest.xml")
if (manifestFile.exists()) {
def manifest = manifestFile.text

// Add launchMode="singleTask"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MainActivity with launchMode=singleTask will destroy all activities that previously existed on top of it.

Is that what we want?

In any case, can't we define a standard AndroidManifest.xml file, instead of doing this?

Copy link
Author

@gimenete-stripe gimenete-stripe Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

launchMode=singleTask is necessary for deep linking with React Navigation:
https://reactnavigation.org/docs/deep-linking/#setup-on-android

And deep linking is necessary for using "secure webviews" (implemented with chrome tabs on Android) to return back to the app.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we can't use AndroidManifest.xml file, instead of modifying the manifest in the build.gradle?

manifest = manifest.replace(
'<activity android:name="com.microsoft.reacttestapp.MainActivity" android:exported="true">',
'<activity android:name="com.microsoft.reacttestapp.MainActivity" android:exported="true" android:launchMode="singleTask">'
)

// Add deep link intent filter for stripe-connect
def intentFilterXml = '''<intent-filter>
<action android:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.DEFAULT"></category>
<category android:name="android.intent.category.BROWSABLE"></category>
<data android:scheme="stripe-connect"></data>
</intent-filter>
'''
manifest = manifest.replace(
'</intent-filter>\n </activity>',
'</intent-filter>\n ' + intentFilterXml + '</activity>'
)

manifestFile.text = manifest
println "✓ Updated MainActivity: launchMode=singleTask + stripe-connect deep link"
}
}
}
}
}
7 changes: 6 additions & 1 deletion example/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
"bundleIdentifier": "com.stripe.react.native",
"codeSignEntitlements": {
"com.apple.developer.in-app-payments": ["merchant.com.stripe.react.native"]
}
},
"urlTypes": [
{
"CFBundleURLSchemes": ["stripe-connect"]
}
]
},
"android": {
"package": "com.stripe.react.native",
Expand Down
Loading