Skip to content

Commit b2f3be9

Browse files
Embedded component fixes (#2274)
* Fix glitch when mounting React tree in native component * Add render_error to EmbeddedErrorType * Open external links in external browser * Fix camera/media permissions * Example app android fixes * Android UI improvements * Allow inline navigation to hosts in development mode * Workaround webview issue on iOS * Open verify.stripe.com URLs inline * Add missing import * Fix another compilation error * Fix * Fix Android API compatibility * Financial connections * Revert NavigationBar textColor functionality and StatusBar padding This reverts the changes from commit 6a04dfe ("Android UI improvements") which introduced textColor prop to NavigationBar and StatusBar.currentHeight padding. These changes were causing Alipay E2E tests to fail across all platforms (Android/iOS, new/old arch). Breaking commit identified via CI analysis: - 2bb15b6 "Fix camera/media permissions" - 8/8 tests passed ✓ - 6a04dfe "Android UI improvements" - 6/7 tests FAILED ✗ Changes reverted: - Removed textColorValue prop from NavigationBar native modules (iOS/Android) - Removed textColor prop from NavigationBar React component - Removed StatusBar.currentHeight padding from ConnectAccountOnboarding - Removed StatusBar import from Components.tsx The textColor feature can be re-implemented more carefully in a separate PR after investigating why it caused test failures. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Committed-By-Agent: claude * Fix Jest tests after removing textColor functionality After removing the textColor prop from NavigationBar, two test suites were failing due to missing mocks and outdated assertions: 1. Added jest/setup.js to mock NativeOnrampSdkModule which was causing test failures in EmbeddedComponent and Components tests 2. Updated Jest configuration in package.json to use setupFilesAfterEnv 3. Removed textColor assertion from Components.test.tsx that was expecting the removed prop All tests now pass (55 tests, 4 test suites). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Committed-By-Agent: claude * Camera fix and other smaller improvements --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 56d0f85 commit b2f3be9

File tree

12 files changed

+323
-53
lines changed

12 files changed

+323
-53
lines changed

android/src/main/java/com/reactnativestripesdk/NavigationBarView.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package com.reactnativestripesdk
22

33
import android.annotation.SuppressLint
44
import android.graphics.Color
5+
import android.graphics.PorterDuff
6+
import android.graphics.PorterDuffColorFilter
7+
import android.os.Build
58
import android.view.Gravity
9+
import android.view.View.MeasureSpec
610
import android.widget.FrameLayout
711
import android.widget.ImageButton
812
import android.widget.TextView
@@ -18,6 +22,7 @@ class NavigationBarView(
1822
) : FrameLayout(context) {
1923
private val toolbar: Toolbar
2024
private val titleTextView: TextView
25+
private val closeButton: ImageButton
2126
private var titleText: String? = null
2227

2328
init {
@@ -53,14 +58,20 @@ class NavigationBarView(
5358
toolbar.addView(titleTextView, titleParams)
5459

5560
// Create close button
56-
val closeButton =
61+
closeButton =
5762
ImageButton(context).apply {
5863
setImageDrawable(
5964
context.resources.getDrawable(
6065
android.R.drawable.ic_menu_close_clear_cancel,
6166
null,
6267
),
6368
)
69+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
70+
drawable?.setColorFilter(android.graphics.BlendModeColorFilter(Color.BLACK, android.graphics.BlendMode.SRC_IN))
71+
} else {
72+
@Suppress("DEPRECATION")
73+
drawable?.setColorFilter(PorterDuffColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN))
74+
}
6475
setBackgroundColor(Color.TRANSPARENT)
6576
setOnClickListener {
6677
dispatchCloseButtonPress()

example-stripe-connect/app/(settings)/_layout.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { Stack } from 'expo-router';
2+
import { Platform } from 'react-native';
23

34
export default function SettingsLayout() {
45
return (
56
<Stack
67
screenOptions={{
7-
headerTransparent: true,
8+
headerTransparent: Platform.select({ ios: true, default: false }),
89
headerBlurEffect: 'systemChromeMaterial',
10+
headerBackButtonDisplayMode: 'minimal',
911
headerLargeStyle: {
1012
backgroundColor: 'transparent',
1113
},
12-
headerStyle: {
13-
backgroundColor: 'transparent',
14-
},
14+
headerStyle: Platform.select({
15+
ios: { backgroundColor: 'transparent' },
16+
default: {},
17+
}),
1518
}}
1619
>
1720
<Stack.Screen name="index" options={{ title: 'Settings' }} />

example-stripe-connect/app/_layout.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,16 @@ function RootLayoutNav() {
6060
<StripeProvider publishableKey={key || 'pk_test_placeholder'}>
6161
<Stack
6262
screenOptions={{
63-
headerTransparent: true,
63+
headerTransparent: Platform.select({ ios: true, default: false }),
6464
headerBlurEffect: 'systemChromeMaterial',
65+
headerBackButtonDisplayMode: 'minimal',
6566
headerLargeStyle: {
6667
backgroundColor: 'transparent',
6768
},
68-
headerStyle: {
69-
backgroundColor: 'transparent',
70-
},
69+
headerStyle: Platform.select({
70+
ios: { backgroundColor: 'transparent' },
71+
default: {},
72+
}),
7173
}}
7274
>
7375
<Stack.Screen name="index" options={{ headerShown: true }} />

example-stripe-connect/src/screens/ConfigureAppearanceScreen.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
TouchableOpacity,
66
ScrollView,
77
StyleSheet,
8+
Platform,
89
} from 'react-native';
910
import { SafeAreaView } from 'react-native-safe-area-context';
1011
import { useRouter, Stack } from 'expo-router';
@@ -37,11 +38,14 @@ const ConfigureAppearanceScreen: React.FC = () => {
3738
<>
3839
<Stack.Screen
3940
options={{
40-
headerLeft: () => (
41-
<TouchableOpacity onPress={() => router.back()}>
42-
<Text style={styles.cancelButton}>Cancel</Text>
43-
</TouchableOpacity>
44-
),
41+
headerLeft:
42+
Platform.OS === 'ios'
43+
? () => (
44+
<TouchableOpacity onPress={() => router.back()}>
45+
<Text style={styles.cancelButton}>Cancel</Text>
46+
</TouchableOpacity>
47+
)
48+
: undefined,
4549
headerRight: () => (
4650
<TouchableOpacity onPress={handleSave} disabled={!hasChanges}>
4751
<Text

example-stripe-connect/src/screens/SettingsScreen.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useRouter, Stack } from 'expo-router';
22
import React, { useCallback, useState } from 'react';
33
import {
44
Keyboard,
5+
Platform,
56
StyleSheet,
67
Text,
78
TextInput,
@@ -100,11 +101,14 @@ const SettingsScreen: React.FC = () => {
100101
<Stack.Screen
101102
options={{
102103
title: 'Settings',
103-
headerLeft: () => (
104-
<TouchableOpacity onPress={() => router.back()}>
105-
<Text style={styles.backButton}>Cancel</Text>
106-
</TouchableOpacity>
107-
),
104+
headerLeft:
105+
Platform.OS === 'ios'
106+
? () => (
107+
<TouchableOpacity onPress={() => router.back()}>
108+
<Text style={styles.backButton}>Cancel</Text>
109+
</TouchableOpacity>
110+
)
111+
: undefined,
108112
headerRight: () => (
109113
<TouchableOpacity onPress={handleSave} disabled={!hasChanges}>
110114
<Text

ios/ConnectAccountOnboarding/ConnectAccountOnboardingView.swift

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class ConnectAccountOnboardingView: UIView {
2121
override public init(frame: CGRect) {
2222
super.init(frame: frame)
2323
super.backgroundColor = .clear
24+
self.isHidden = true // Hide until modal animation completes
2425
}
2526

2627
required init?(coder: NSCoder) {
@@ -29,10 +30,7 @@ public class ConnectAccountOnboardingView: UIView {
2930

3031
@objc public func didSetProps() {
3132
if visible && !wasVisible {
32-
// Delay presentation to ensure React Native has rendered children
33-
DispatchQueue.main.async { [weak self] in
34-
self?.presentModal()
35-
}
33+
presentModal()
3634
wasVisible = true
3735
} else if !visible && wasVisible {
3836
dismissModal()
@@ -47,6 +45,9 @@ public class ConnectAccountOnboardingView: UIView {
4745
}
4846

4947
private func presentModal() {
48+
// Keep view hidden during modal presentation
49+
self.isHidden = true
50+
5051
// Create the view controller that wraps THIS view
5152
viewController = ConnectAccountOnboardingViewController()
5253
viewController?.title = title
@@ -56,17 +57,21 @@ public class ConnectAccountOnboardingView: UIView {
5657
self?.handleClose()
5758
}
5859

59-
// Add this entire React Native view to the view controller
60-
viewController?.setReactContentView(self)
61-
6260
// Wrap in a navigation controller
6361
navigationController = UINavigationController(rootViewController: viewController!)
6462
navigationController?.modalPresentationStyle = .fullScreen
6563
navigationController?.modalTransitionStyle = .coverVertical
6664

6765
// Find the presenting view controller and present
6866
let presenter = findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController())
69-
presenter.present(navigationController!, animated: true)
67+
68+
// Present the empty modal first, then add React content after animation completes
69+
presenter.present(navigationController!, animated: true) { [weak self] in
70+
guard let self = self else { return }
71+
// Add React content after presentation animation finishes
72+
self.viewController?.setReactContentView(self)
73+
self.isHidden = false // Show the view now that modal is presented
74+
}
7075
}
7176

7277
private func dismissModal() {
@@ -83,15 +88,4 @@ public class ConnectAccountOnboardingView: UIView {
8388
dismissModal()
8489
}
8590

86-
override public func didMoveToSuperview() {
87-
super.didMoveToSuperview()
88-
89-
// When added to native view hierarchy, ensure layout is triggered
90-
if let superview = superview {
91-
// Match the superview's bounds immediately
92-
self.frame = superview.bounds
93-
setNeedsLayout()
94-
layoutIfNeeded()
95-
}
96-
}
9791
}

jest/setup.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-disable no-undef */
2+
/**
3+
* Jest setup file to mock native modules
4+
*/
5+
6+
// Mock OnrampSdk TurboModule
7+
jest.mock('../src/specs/NativeOnrampSdkModule', () => ({
8+
__esModule: true,
9+
default: {
10+
initialise: jest.fn(),
11+
configureOnramp: jest.fn(),
12+
hasLinkAccount: jest.fn(),
13+
registerLinkUser: jest.fn(),
14+
registerWalletAddress: jest.fn(),
15+
attachKycInfo: jest.fn(),
16+
presentKycInfoVerification: jest.fn(),
17+
updatePhoneNumber: jest.fn(),
18+
authenticateUser: jest.fn(),
19+
authenticateUserWithToken: jest.fn(),
20+
verifyIdentity: jest.fn(),
21+
collectPaymentMethod: jest.fn(),
22+
provideCheckoutClientSecret: jest.fn(),
23+
onCheckoutClientSecretRequested: null,
24+
createCryptoPaymentToken: jest.fn(),
25+
performCheckout: jest.fn(),
26+
onrampAuthorize: jest.fn(),
27+
getCryptoTokenDisplayData: jest.fn(),
28+
logout: jest.fn(),
29+
},
30+
}));

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@
106106
},
107107
"jest": {
108108
"preset": "react-native",
109+
"setupFilesAfterEnv": [
110+
"<rootDir>/jest/setup.js"
111+
],
109112
"modulePathIgnorePatterns": [
110113
"<rootDir>/example/node_modules",
111114
"<rootDir>/lib/",

src/connect/Components.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Modal,
55
Platform,
66
SafeAreaView,
7+
StatusBar,
78
StyleSheet,
89
View,
910
useWindowDimensions,
@@ -52,10 +53,6 @@ export function ConnectAccountOnboarding({
5253
return appearance?.variables?.colorBackground || '#FFFFFF';
5354
}, [appearance]);
5455

55-
const textColor = useMemo(() => {
56-
return appearance?.variables?.colorText || '#000000';
57-
}, [appearance]);
58-
5956
const loadingIndicatorColor = useMemo(() => {
6057
return appearance?.variables?.colorSecondaryText || '#888888';
6158
}, [appearance]);
@@ -102,7 +99,10 @@ export function ConnectAccountOnboarding({
10299

103100
const { width, height } = useWindowDimensions();
104101

105-
const containerStyle = useMemo(() => ({ width, height }), [width, height]);
102+
const containerStyle = useMemo(
103+
() => ({ width, height, position: 'absolute' as const }),
104+
[width, height]
105+
);
106106

107107
// iOS: Use native modal with native navigation bar
108108
if (Platform.OS === 'ios') {
@@ -111,7 +111,6 @@ export function ConnectAccountOnboarding({
111111
visible={visible}
112112
title={title}
113113
backgroundColor={backgroundColor}
114-
textColor={textColor}
115114
onExitAction={onExitCallback}
116115
style={containerStyle}
117116
>
@@ -143,11 +142,19 @@ export function ConnectAccountOnboarding({
143142
presentationStyle="fullScreen"
144143
>
145144
<SafeAreaView style={styles.flex1}>
146-
<NavigationBar
147-
title={title}
148-
onCloseButtonPress={onExitCallback}
149-
style={styles.navBar}
150-
/>
145+
<View
146+
style={[
147+
Platform.OS === 'android' && {
148+
paddingTop: StatusBar.currentHeight || 0,
149+
},
150+
]}
151+
>
152+
<NavigationBar
153+
title={title}
154+
onCloseButtonPress={onExitCallback}
155+
style={styles.navBar}
156+
/>
157+
</View>
151158
<View style={styles.onboardingWrapper}>
152159
{loading ? (
153160
<ActivityIndicator

0 commit comments

Comments
 (0)