@stripe/stripe-react-native 0.57.3 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/android/build.gradle +2 -0
  2. package/android/gradle.properties +1 -1
  3. package/android/src/main/AndroidManifest.xml +27 -1
  4. package/android/src/main/java/com/reactnativestripesdk/EmbeddedPaymentElementView.kt +0 -3
  5. package/android/src/main/java/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt +7 -3
  6. package/android/src/main/java/com/reactnativestripesdk/EventEmitterCompat.kt +8 -0
  7. package/android/src/main/java/com/reactnativestripesdk/NavigationBarView.kt +12 -1
  8. package/android/src/main/java/com/reactnativestripesdk/PaymentElementConfig.kt +26 -0
  9. package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementConfig.kt +147 -0
  10. package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementView.kt +164 -0
  11. package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementViewManager.kt +65 -0
  12. package/android/src/main/java/com/reactnativestripesdk/PaymentSheetAppearance.kt +1 -1
  13. package/android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt +60 -31
  14. package/android/src/main/java/com/reactnativestripesdk/StripeAbstractComposeView.kt +17 -5
  15. package/android/src/main/java/com/reactnativestripesdk/StripeConnectDeepLinkInterceptorActivity.kt +77 -0
  16. package/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt +334 -24
  17. package/android/src/main/java/com/reactnativestripesdk/StripeSdkPackage.kt +1 -0
  18. package/android/src/main/java/com/reactnativestripesdk/customersheet/CustomerSheetManager.kt +3 -0
  19. package/android/src/main/java/com/reactnativestripesdk/utils/Errors.kt +8 -0
  20. package/android/src/main/java/com/reactnativestripesdk/utils/Mappers.kt +0 -2
  21. package/android/src/main/res/xml/file_paths.xml +4 -0
  22. package/android/src/oldarch/java/com/facebook/react/viewmanagers/PaymentMethodMessagingElementViewManagerDelegate.java +36 -0
  23. package/android/src/oldarch/java/com/facebook/react/viewmanagers/PaymentMethodMessagingElementViewManagerInterface.java +18 -0
  24. package/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java +20 -0
  25. package/android/src/test/java/com/reactnativestripesdk/PaymentElementConfigTest.kt +175 -1
  26. package/android/src/test/java/com/reactnativestripesdk/PaymentMethodMessagingElementConfigTest.kt +543 -0
  27. package/android/src/test/java/com/reactnativestripesdk/PaymentSheetManagerTest.kt +70 -0
  28. package/ios/ConnectAccountOnboarding/ConnectAccountOnboardingView.swift +13 -19
  29. package/ios/CustomerSheet/CustomerSheetUtils.swift +4 -0
  30. package/ios/OldArch/StripeSdkEventEmitterCompat.h +2 -0
  31. package/ios/OldArch/StripeSdkEventEmitterCompat.m +13 -1
  32. package/ios/PaymentMethodMessagingElementConfig.swift +116 -0
  33. package/ios/PaymentMethodMessagingElementHandler.m +9 -0
  34. package/ios/PaymentMethodMessagingElementView.swift +139 -0
  35. package/ios/StripeSdk.mm +40 -0
  36. package/ios/StripeSdkEmitter.swift +2 -0
  37. package/ios/StripeSdkImpl+CustomerSheet.swift +1 -0
  38. package/ios/StripeSdkImpl+Embedded.swift +8 -1
  39. package/ios/StripeSdkImpl+PaymentSheet.swift +44 -1
  40. package/ios/StripeSdkImpl.swift +158 -2
  41. package/jest/mock.js +26 -0
  42. package/jest/setup.js +30 -0
  43. package/lib/commonjs/components/AddToWalletButton.js +1 -1
  44. package/lib/commonjs/components/AddToWalletButton.js.map +1 -1
  45. package/lib/commonjs/components/AddressSheet.js +1 -1
  46. package/lib/commonjs/components/AddressSheet.js.map +1 -1
  47. package/lib/commonjs/components/AuBECSDebitForm.js +1 -1
  48. package/lib/commonjs/components/AuBECSDebitForm.js.map +1 -1
  49. package/lib/commonjs/components/CardField.js +1 -1
  50. package/lib/commonjs/components/CardField.js.map +1 -1
  51. package/lib/commonjs/components/CardForm.js +1 -1
  52. package/lib/commonjs/components/CardForm.js.map +1 -1
  53. package/lib/commonjs/components/PlatformPayButton.js +1 -1
  54. package/lib/commonjs/components/PlatformPayButton.js.map +1 -1
  55. package/lib/commonjs/components/StripeContainer.js +1 -1
  56. package/lib/commonjs/components/StripeContainer.js.map +1 -1
  57. package/lib/commonjs/connect/Components.js +1 -1
  58. package/lib/commonjs/connect/Components.js.map +1 -1
  59. package/lib/commonjs/connect/ConnectComponentsProvider.js +1 -1
  60. package/lib/commonjs/connect/ConnectComponentsProvider.js.map +1 -1
  61. package/lib/commonjs/connect/EmbeddedComponent.js +10 -5
  62. package/lib/commonjs/connect/EmbeddedComponent.js.map +1 -1
  63. package/lib/commonjs/connect/ModalCloseButton.js +1 -1
  64. package/lib/commonjs/connect/ModalCloseButton.js.map +1 -1
  65. package/lib/commonjs/connect/NavigationBar.js +1 -1
  66. package/lib/commonjs/connect/NavigationBar.js.map +1 -1
  67. package/lib/commonjs/connect/analytics/AnalyticsClient.js +2 -0
  68. package/lib/commonjs/connect/analytics/AnalyticsClient.js.map +1 -0
  69. package/lib/commonjs/connect/analytics/ComponentAnalyticsClient.js +2 -0
  70. package/lib/commonjs/connect/analytics/ComponentAnalyticsClient.js.map +1 -0
  71. package/lib/commonjs/connect/analytics/events.js +2 -0
  72. package/lib/commonjs/connect/analytics/events.js.map +1 -0
  73. package/lib/commonjs/connect/testUtils.js +2 -0
  74. package/lib/commonjs/connect/testUtils.js.map +1 -0
  75. package/lib/commonjs/events.js.map +1 -1
  76. package/lib/commonjs/functions.js +1 -1
  77. package/lib/commonjs/functions.js.map +1 -1
  78. package/lib/commonjs/helpers.js +1 -1
  79. package/lib/commonjs/hooks/useStripe.js +1 -1
  80. package/lib/commonjs/hooks/useStripe.js.map +1 -1
  81. package/lib/commonjs/specs/NativeAddToWalletButton.js +1 -1
  82. package/lib/commonjs/specs/NativeAddressSheet.js +1 -1
  83. package/lib/commonjs/specs/NativeApplePayButton.js +1 -1
  84. package/lib/commonjs/specs/NativeAuBECSDebitForm.js +1 -1
  85. package/lib/commonjs/specs/NativeCardField.js +1 -1
  86. package/lib/commonjs/specs/NativeCardField.js.map +1 -1
  87. package/lib/commonjs/specs/NativeCardForm.js +1 -1
  88. package/lib/commonjs/specs/NativeCardForm.js.map +1 -1
  89. package/lib/commonjs/specs/NativeConnectAccountOnboardingView.js +1 -1
  90. package/lib/commonjs/specs/NativeEmbeddedPaymentElement.js +1 -1
  91. package/lib/commonjs/specs/NativeEmbeddedPaymentElement.js.map +1 -1
  92. package/lib/commonjs/specs/NativeGooglePayButton.js +1 -1
  93. package/lib/commonjs/specs/NativeNavigationBar.js +1 -1
  94. package/lib/commonjs/specs/NativePaymentMethodMessagingElement.js +2 -0
  95. package/lib/commonjs/specs/NativePaymentMethodMessagingElement.js.map +1 -0
  96. package/lib/commonjs/specs/NativeStripeContainer.js +1 -1
  97. package/lib/commonjs/specs/NativeStripeSdkModule.js.map +1 -1
  98. package/lib/commonjs/types/EmbeddedPaymentElement.js +1 -1
  99. package/lib/commonjs/types/EmbeddedPaymentElement.js.map +1 -1
  100. package/lib/commonjs/types/Errors.js +1 -1
  101. package/lib/commonjs/types/Errors.js.map +1 -1
  102. package/lib/commonjs/types/FinancialConnections.js.map +1 -1
  103. package/lib/commonjs/types/PaymentSheet.js +1 -1
  104. package/lib/commonjs/types/PaymentSheet.js.map +1 -1
  105. package/lib/commonjs/types/components/PaymentMethodMessagingElementComponent.js +2 -0
  106. package/lib/commonjs/types/components/PaymentMethodMessagingElementComponent.js.map +1 -0
  107. package/lib/commonjs/types/index.js.map +1 -1
  108. package/lib/module/components/AddToWalletButton.js +1 -1
  109. package/lib/module/components/AddToWalletButton.js.map +1 -1
  110. package/lib/module/components/AddressSheet.js +1 -1
  111. package/lib/module/components/AddressSheet.js.map +1 -1
  112. package/lib/module/components/AuBECSDebitForm.js +1 -1
  113. package/lib/module/components/AuBECSDebitForm.js.map +1 -1
  114. package/lib/module/components/CardField.js +1 -1
  115. package/lib/module/components/CardField.js.map +1 -1
  116. package/lib/module/components/CardForm.js +1 -1
  117. package/lib/module/components/CardForm.js.map +1 -1
  118. package/lib/module/components/PlatformPayButton.js +1 -1
  119. package/lib/module/components/PlatformPayButton.js.map +1 -1
  120. package/lib/module/components/StripeContainer.js +1 -1
  121. package/lib/module/components/StripeContainer.js.map +1 -1
  122. package/lib/module/connect/Components.js +1 -1
  123. package/lib/module/connect/Components.js.map +1 -1
  124. package/lib/module/connect/ConnectComponentsProvider.js +1 -1
  125. package/lib/module/connect/ConnectComponentsProvider.js.map +1 -1
  126. package/lib/module/connect/EmbeddedComponent.js +10 -5
  127. package/lib/module/connect/EmbeddedComponent.js.map +1 -1
  128. package/lib/module/connect/ModalCloseButton.js +1 -1
  129. package/lib/module/connect/ModalCloseButton.js.map +1 -1
  130. package/lib/module/connect/NavigationBar.js +1 -1
  131. package/lib/module/connect/NavigationBar.js.map +1 -1
  132. package/lib/module/connect/analytics/AnalyticsClient.js +2 -0
  133. package/lib/module/connect/analytics/AnalyticsClient.js.map +1 -0
  134. package/lib/module/connect/analytics/ComponentAnalyticsClient.js +2 -0
  135. package/lib/module/connect/analytics/ComponentAnalyticsClient.js.map +1 -0
  136. package/lib/module/connect/analytics/events.js +2 -0
  137. package/lib/module/connect/analytics/events.js.map +1 -0
  138. package/lib/module/connect/testUtils.js +2 -0
  139. package/lib/module/connect/testUtils.js.map +1 -0
  140. package/lib/module/events.js.map +1 -1
  141. package/lib/module/functions.js +1 -1
  142. package/lib/module/functions.js.map +1 -1
  143. package/lib/module/helpers.js +1 -1
  144. package/lib/module/hooks/useStripe.js +1 -1
  145. package/lib/module/hooks/useStripe.js.map +1 -1
  146. package/lib/module/specs/NativeAddToWalletButton.js +1 -1
  147. package/lib/module/specs/NativeAddressSheet.js +1 -1
  148. package/lib/module/specs/NativeApplePayButton.js +1 -1
  149. package/lib/module/specs/NativeAuBECSDebitForm.js +1 -1
  150. package/lib/module/specs/NativeCardField.js +1 -1
  151. package/lib/module/specs/NativeCardField.js.map +1 -1
  152. package/lib/module/specs/NativeCardForm.js +1 -1
  153. package/lib/module/specs/NativeCardForm.js.map +1 -1
  154. package/lib/module/specs/NativeConnectAccountOnboardingView.js +1 -1
  155. package/lib/module/specs/NativeEmbeddedPaymentElement.js +1 -1
  156. package/lib/module/specs/NativeEmbeddedPaymentElement.js.map +1 -1
  157. package/lib/module/specs/NativeGooglePayButton.js +1 -1
  158. package/lib/module/specs/NativeNavigationBar.js +1 -1
  159. package/lib/module/specs/NativePaymentMethodMessagingElement.js +2 -0
  160. package/lib/module/specs/NativePaymentMethodMessagingElement.js.map +1 -0
  161. package/lib/module/specs/NativeStripeContainer.js +1 -1
  162. package/lib/module/specs/NativeStripeSdkModule.js.map +1 -1
  163. package/lib/module/types/EmbeddedPaymentElement.js +1 -1
  164. package/lib/module/types/EmbeddedPaymentElement.js.map +1 -1
  165. package/lib/module/types/Errors.js +1 -1
  166. package/lib/module/types/Errors.js.map +1 -1
  167. package/lib/module/types/FinancialConnections.js.map +1 -1
  168. package/lib/module/types/PaymentSheet.js +1 -1
  169. package/lib/module/types/PaymentSheet.js.map +1 -1
  170. package/lib/module/types/components/PaymentMethodMessagingElementComponent.js +2 -0
  171. package/lib/module/types/components/PaymentMethodMessagingElementComponent.js.map +1 -0
  172. package/lib/module/types/index.js.map +1 -1
  173. package/lib/typescript/src/connect/Components.d.ts +91 -0
  174. package/lib/typescript/src/connect/Components.d.ts.map +1 -1
  175. package/lib/typescript/src/connect/ConnectComponentsProvider.d.ts +61 -0
  176. package/lib/typescript/src/connect/ConnectComponentsProvider.d.ts.map +1 -1
  177. package/lib/typescript/src/connect/EmbeddedComponent.d.ts.map +1 -1
  178. package/lib/typescript/src/connect/analytics/AnalyticsClient.d.ts +32 -0
  179. package/lib/typescript/src/connect/analytics/AnalyticsClient.d.ts.map +1 -0
  180. package/lib/typescript/src/connect/analytics/ComponentAnalyticsClient.d.ts +94 -0
  181. package/lib/typescript/src/connect/analytics/ComponentAnalyticsClient.d.ts.map +1 -0
  182. package/lib/typescript/src/connect/analytics/events.d.ts +215 -0
  183. package/lib/typescript/src/connect/analytics/events.d.ts.map +1 -0
  184. package/lib/typescript/src/connect/connectTypes.d.ts +5 -1
  185. package/lib/typescript/src/connect/connectTypes.d.ts.map +1 -1
  186. package/lib/typescript/src/connect/testUtils.d.ts +45 -0
  187. package/lib/typescript/src/connect/testUtils.d.ts.map +1 -0
  188. package/lib/typescript/src/events.d.ts +2 -0
  189. package/lib/typescript/src/events.d.ts.map +1 -1
  190. package/lib/typescript/src/functions.d.ts +13 -1
  191. package/lib/typescript/src/functions.d.ts.map +1 -1
  192. package/lib/typescript/src/hooks/useStripe.d.ts +2 -1
  193. package/lib/typescript/src/hooks/useStripe.d.ts.map +1 -1
  194. package/lib/typescript/src/specs/NativePaymentMethodMessagingElement.d.ts +16 -0
  195. package/lib/typescript/src/specs/NativePaymentMethodMessagingElement.d.ts.map +1 -0
  196. package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts +16 -1
  197. package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts.map +1 -1
  198. package/lib/typescript/src/types/CustomerSheet.d.ts +5 -0
  199. package/lib/typescript/src/types/CustomerSheet.d.ts.map +1 -1
  200. package/lib/typescript/src/types/EmbeddedPaymentElement.d.ts +11 -1
  201. package/lib/typescript/src/types/EmbeddedPaymentElement.d.ts.map +1 -1
  202. package/lib/typescript/src/types/Errors.d.ts +4 -0
  203. package/lib/typescript/src/types/Errors.d.ts.map +1 -1
  204. package/lib/typescript/src/types/FinancialConnections.d.ts +2 -0
  205. package/lib/typescript/src/types/FinancialConnections.d.ts.map +1 -1
  206. package/lib/typescript/src/types/PaymentSheet.d.ts +35 -0
  207. package/lib/typescript/src/types/PaymentSheet.d.ts.map +1 -1
  208. package/lib/typescript/src/types/components/PaymentMethodMessagingElementComponent.d.ts +69 -0
  209. package/lib/typescript/src/types/components/PaymentMethodMessagingElementComponent.d.ts.map +1 -0
  210. package/lib/typescript/src/types/index.d.ts +8 -1
  211. package/lib/typescript/src/types/index.d.ts.map +1 -1
  212. package/package.json +4 -1
  213. package/src/connect/Components.tsx +109 -11
  214. package/src/connect/ConnectComponentsProvider.tsx +69 -2
  215. package/src/connect/EmbeddedComponent.tsx +458 -23
  216. package/src/connect/analytics/AnalyticsClient.ts +75 -0
  217. package/src/connect/analytics/ComponentAnalyticsClient.ts +315 -0
  218. package/src/connect/analytics/events.ts +253 -0
  219. package/src/connect/connectTypes.ts +5 -1
  220. package/src/connect/testUtils.ts +37 -0
  221. package/src/events.ts +2 -0
  222. package/src/functions.ts +10 -0
  223. package/src/hooks/useStripe.tsx +8 -0
  224. package/src/specs/NativePaymentMethodMessagingElement.ts +25 -0
  225. package/src/specs/NativeStripeSdkModule.ts +21 -1
  226. package/src/types/CustomerSheet.ts +5 -0
  227. package/src/types/EmbeddedPaymentElement.tsx +11 -1
  228. package/src/types/Errors.ts +5 -0
  229. package/src/types/FinancialConnections.ts +2 -0
  230. package/src/types/PaymentSheet.ts +38 -1
  231. package/src/types/components/PaymentMethodMessagingElementComponent.tsx +74 -0
  232. package/src/types/index.ts +11 -0
  233. package/stripe-react-native.podspec +1 -1
@@ -8,19 +8,25 @@ import React, {
8
8
  import {
9
9
  AppState,
10
10
  AppStateStatus,
11
+ Linking,
11
12
  Platform,
12
13
  StyleProp,
13
14
  ViewStyle,
14
15
  } from 'react-native';
15
16
  import type { WebView, WebViewMessageEvent } from 'react-native-webview';
17
+ import type { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTypes';
18
+ import type { EventSubscription } from 'react-native';
16
19
  import pjson from '../../package.json';
17
20
  import NativeStripeSdk from '../specs/NativeStripeSdkModule';
21
+ import { addListener } from '../events';
18
22
  import { useConnectComponents } from './ConnectComponentsProvider';
19
23
  import type {
20
24
  LoadError,
21
25
  LoaderStart,
22
26
  StripeConnectInitParams,
23
27
  } from './connectTypes';
28
+ import type { FinancialConnections } from '../types';
29
+ import { ComponentAnalyticsClient } from './analytics/ComponentAnalyticsClient';
24
30
 
25
31
  const DEVELOPMENT_MODE = false;
26
32
  const DEVELOPMENT_URL =
@@ -30,6 +36,12 @@ const BASE_URL = DEVELOPMENT_MODE ? DEVELOPMENT_URL : PRODUCTION_URL;
30
36
 
31
37
  const sdkVersion = pjson.version;
32
38
 
39
+ // Android deep link polling configuration
40
+ // These constants control the polling mechanism that prevents Expo Router from dismissing screens
41
+ const POLLING_INTERVAL_MS = 500; // How often to check for pending deep links
42
+ const URL_DEDUPLICATION_TIMEOUT_MS = 1000; // How long to remember handled URLs
43
+ const DEEP_LINK_GRACE_PERIOD_MS = 500; // Time to wait for deep link after app resumes
44
+
33
45
  // react-native-webview.html will only load versions in the format X.Y.Z
34
46
  if (!/^\d+\.\d+\.\d+$/.test(sdkVersion)) {
35
47
  throw new Error(
@@ -37,12 +49,26 @@ if (!/^\d+\.\d+\.\d+$/.test(sdkVersion)) {
37
49
  );
38
50
  }
39
51
 
52
+ // Required for ua-parser-js to detect mobile platforms correctly
53
+ const platformPrefix = Platform.select({
54
+ ios: 'iPhone',
55
+ android: 'Android',
56
+ default: 'Mobile',
57
+ });
40
58
  const userAgent = [
41
- 'Mobile',
59
+ platformPrefix,
42
60
  `Stripe ReactNative SDK ${Platform.OS}/${Platform.Version}`,
43
61
  `stripe-react_native/${sdkVersion}`,
44
62
  ].join(' - ');
45
63
 
64
+ // Allowed domains for in-WebView navigation (matching iOS SDK behavior)
65
+ const ALLOWED_STRIPE_HOSTS = [
66
+ 'connect-js.stripe.com',
67
+ 'connect.stripe.com',
68
+ 'verify.stripe.com',
69
+ ...(DEVELOPMENT_MODE ? ['10.0.2.2:3001', 'localhost:3001'] : []),
70
+ ];
71
+
46
72
  export interface CommonComponentProps {
47
73
  onLoaderStart?: ({ elementTagName }: LoaderStart) => void;
48
74
  onLoadError?: ({ error, elementTagName }: LoadError) => void;
@@ -120,12 +146,29 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
120
146
  WebView: typeof WebView | null;
121
147
  } | null>(null);
122
148
 
123
- // Store pending authenticated webview promise (for Android Custom Tabs)
124
- const pendingAuthWebViewPromise = useRef<{
149
+ // Store pending authenticated webview promises (for Android Custom Tabs)
150
+ // Uses a Map keyed by auth session ID to support multiple auth flows.
151
+ // Currently processes promises in FIFO order (first entry resolved first).
152
+ const pendingAuthWebViewPromises = useRef<
153
+ Map<
154
+ string,
155
+ {
156
+ callback: (id: string, url: string | null) => void;
157
+ timeoutId?: NodeJS.Timeout;
158
+ }
159
+ >
160
+ >(new Map());
161
+
162
+ // Store pending Financial Connections promise
163
+ const pendingFinancialConnectionsPromise = useRef<{
125
164
  id: string;
126
- callback: (id: string, url: string | null) => void;
165
+ cleanup: () => void;
127
166
  } | null>(null);
128
167
 
168
+ // Track recently handled URLs to prevent duplicate processing
169
+ // This is needed because the SDK uses dual delivery paths (setIntent + direct emit)
170
+ const recentlyHandledUrls = useRef<Set<string>>(new Set());
171
+
129
172
  const loadWebViewComponent = useCallback(async () => {
130
173
  if (dynamicWebview) return;
131
174
 
@@ -145,16 +188,106 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
145
188
 
146
189
  const appState = useRef<AppStateStatus>(AppState.currentState);
147
190
 
191
+ // Android deep link polling mechanism (Android-only)
192
+ //
193
+ // PROBLEM: On Android, completing auth in Custom Tabs causes a stripe-connect:// deep link
194
+ // that gets broadcast to React Native's Linking module, which triggers Expo Router and
195
+ // dismisses the current screen.
196
+ //
197
+ // SOLUTION: This polling mechanism intercepts the deep link before Expo Router receives it:
198
+ // 1. MainActivity.onNewIntent() captures stripe-connect:// URLs and stores them
199
+ // 2. This effect polls for pending URLs while auth is active
200
+ // 3. URLs are processed directly and never broadcast to Expo Router
201
+ // 4. Deduplication prevents the same URL from being processed twice
202
+ //
203
+ // This preserves the navigation stack and prevents unwanted screen dismissals.
204
+ useEffect(() => {
205
+ if (Platform.OS !== 'android') return;
206
+
207
+ const pollInterval = setInterval(async () => {
208
+ if (pendingAuthWebViewPromises.current.size === 0) {
209
+ return;
210
+ }
211
+
212
+ try {
213
+ const pendingUrls =
214
+ await NativeStripeSdk.pollAndClearPendingStripeConnectUrls();
215
+
216
+ if (pendingUrls && pendingUrls.length > 0) {
217
+ pendingUrls.forEach((url: string) => {
218
+ if (url.startsWith('stripe-connect://')) {
219
+ // Deduplication: Skip if we've handled this exact URL
220
+ if (recentlyHandledUrls.current.has(url)) {
221
+ return;
222
+ }
223
+
224
+ // Mark as handled
225
+ recentlyHandledUrls.current.add(url);
226
+
227
+ // Clear from the set after deduplication timeout
228
+ setTimeout(() => {
229
+ recentlyHandledUrls.current.delete(url);
230
+ }, URL_DEDUPLICATION_TIMEOUT_MS);
231
+
232
+ const firstEntry = pendingAuthWebViewPromises.current
233
+ .entries()
234
+ .next().value;
235
+
236
+ if (firstEntry) {
237
+ const [id, promiseData] = firstEntry;
238
+
239
+ // Clear the timeout if it exists
240
+ if (promiseData.timeoutId) {
241
+ clearTimeout(promiseData.timeoutId);
242
+ }
243
+
244
+ // Remove from Map and invoke callback
245
+ pendingAuthWebViewPromises.current.delete(id);
246
+ promiseData.callback(id, url);
247
+
248
+ // Reset the Android flag
249
+ NativeStripeSdk.authWebViewDeepLinkHandled(id).catch(() => {
250
+ // Intentionally silent - flag reset is not critical
251
+ });
252
+ }
253
+ }
254
+ });
255
+ }
256
+ } catch (_error) {
257
+ // Intentionally silent - polling will retry on next interval
258
+ }
259
+ }, POLLING_INTERVAL_MS);
260
+
261
+ return () => {
262
+ clearInterval(pollInterval);
263
+ };
264
+ }, []);
265
+
266
+ // Handle app state changes to detect when user returns from auth flow
148
267
  useEffect(() => {
149
268
  const subscription = AppState.addEventListener('change', (nextAppState) => {
150
269
  if (
151
270
  appState.current.match(/inactive|background/) &&
152
271
  nextAppState === 'active'
153
272
  ) {
154
- if (pendingAuthWebViewPromise.current) {
155
- const { id, callback } = pendingAuthWebViewPromise.current;
156
- pendingAuthWebViewPromise.current = null;
157
- callback(id, null);
273
+ if (pendingAuthWebViewPromises.current.size > 0) {
274
+ // Give the deep link handler time to process the URL
275
+ // If no deep link arrives within the grace period, assume the user cancelled
276
+ pendingAuthWebViewPromises.current.forEach((promiseData, id) => {
277
+ // Only set timeout if one doesn't already exist
278
+ if (!promiseData.timeoutId) {
279
+ const timeoutId = setTimeout(() => {
280
+ const stillPending = pendingAuthWebViewPromises.current.get(id);
281
+ if (stillPending) {
282
+ pendingAuthWebViewPromises.current.delete(id);
283
+ stillPending.callback(id, null);
284
+ }
285
+ }, DEEP_LINK_GRACE_PERIOD_MS);
286
+
287
+ // Store the timeout ID so we can clear it later if needed
288
+ promiseData.timeoutId = timeoutId;
289
+ }
290
+ });
158
291
  }
159
292
  }
160
293
 
@@ -162,12 +295,23 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
162
295
  });
163
296
 
164
297
  return () => {
165
- pendingAuthWebViewPromise.current = null;
298
+ // Clear all pending timeouts and promises
299
+ // We intentionally want the latest ref value at cleanup time
300
+ // eslint-disable-next-line react-hooks/exhaustive-deps
301
+ const promises = pendingAuthWebViewPromises.current;
302
+ promises.forEach((promiseData, _id) => {
303
+ if (promiseData.timeoutId) {
304
+ clearTimeout(promiseData.timeoutId);
305
+ }
306
+ });
307
+ promises.clear();
308
+ pendingFinancialConnectionsPromise.current?.cleanup();
166
309
  subscription.remove();
167
310
  };
168
311
  }, []);
169
312
 
170
- const { connectInstance, appearance, locale } = useConnectComponents();
313
+ const { connectInstance, appearance, locale, analyticsClient } =
314
+ useConnectComponents();
171
315
  const { fonts, publishableKey, fetchClientSecret, overrides } =
172
316
  connectInstance.initParams as StripeConnectInitParamsInternal;
173
317
 
@@ -181,6 +325,22 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
181
325
  style,
182
326
  } = props;
183
327
 
328
+ // Initialize component analytics client
329
+ const componentAnalytics = useMemo(
330
+ () =>
331
+ new ComponentAnalyticsClient(analyticsClient, {
332
+ publishableKey,
333
+ platformId: overrides?.platformId,
334
+ merchantId: overrides?.merchantId,
335
+ livemode:
336
+ typeof overrides?.livemode === 'boolean'
337
+ ? overrides.livemode
338
+ : publishableKey?.startsWith('pk_live_'),
339
+ component,
340
+ }),
341
+ [analyticsClient, publishableKey, overrides, component]
342
+ );
343
+
184
344
  const hashParams = {
185
345
  component,
186
346
  publicKey: publishableKey,
@@ -202,6 +362,7 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
202
362
  const source = useMemo(() => ({ uri: connectURL }), [connectURL]);
203
363
 
204
364
  const ref = useRef<WebView>(null);
365
+ const hasTriedSourceReload = useRef(false);
205
366
 
206
367
  const [prevAppearance, setPrevAppearance] = useState(appearance);
207
368
  const [prevLocale, setPrevLocale] = useState(locale);
@@ -244,6 +405,42 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
244
405
 
245
406
  const WebViewComponent = dynamicWebview?.WebView;
246
407
 
408
+ // Workaround for react-native-webview new architecture bug on iOS
409
+ // https://github.com/react-native-webview/react-native-webview/pull/3880
410
+ // The source prop doesn't get set properly on iOS with new architecture,
411
+ // so we force reload after the component mounts
412
+ useEffect(() => {
413
+ if (
414
+ Platform.OS === 'ios' &&
415
+ !hasTriedSourceReload.current &&
416
+ WebViewComponent &&
417
+ ref.current
418
+ ) {
419
+ hasTriedSourceReload.current = true;
420
+ // Force reload after mount to ensure source is set
421
+ const timer = setTimeout(() => {
422
+ ref.current?.reload();
423
+ }, 100);
424
+ return () => clearTimeout(timer);
425
+ }
426
+ return undefined;
427
+ }, [WebViewComponent]);
428
+
429
+ // Track component lifecycle events
430
+ useEffect(() => {
431
+ // Log component created
432
+ componentAnalytics.logComponentCreated();
433
+ }, [componentAnalytics]);
434
+
435
+ // Track component viewed (when web view is visible)
436
+ const [hasBeenViewed, setHasBeenViewed] = useState(false);
437
+ const handleLayout = useCallback(() => {
438
+ if (!hasBeenViewed) {
439
+ setHasBeenViewed(true);
440
+ componentAnalytics.logComponentViewed();
441
+ }
442
+ }, [hasBeenViewed, componentAnalytics]);
443
+
247
444
  const handleAuthWebViewResult = (id: string, resultUrl: string | null) => {
248
445
  ref.current?.injectJavaScript(`
249
446
  (function() {
@@ -256,12 +453,51 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
256
453
  `);
257
454
  };
258
455
 
456
+ const handleFinancialConnectionsResult = (
457
+ id: string,
458
+ result: {
459
+ session?: FinancialConnections.Session;
460
+ token?: FinancialConnections.BankAccountToken;
461
+ error?: {
462
+ code: string;
463
+ message: string;
464
+ localizedMessage?: string;
465
+ type?: string;
466
+ };
467
+ }
468
+ ) => {
469
+ ref.current?.injectJavaScript(`
470
+ (function() {
471
+ window.callSetterWithSerializableValue(${JSON.stringify({
472
+ setter: 'setCollectMobileFinancialConnectionsResult',
473
+ value: {
474
+ id: id,
475
+ financialConnectionsSession: result.session
476
+ ? {
477
+ accounts: result.session.accounts,
478
+ }
479
+ : null,
480
+ token: result.token ?? null,
481
+ error: result.error ?? null,
482
+ },
483
+ })});
484
+ true;
485
+ })();
486
+ `);
487
+ };
488
+
259
489
  const onMessageCallback = useCallback(
260
490
  async (event: WebViewMessageEvent) => {
261
- const message = JSON.parse(event.nativeEvent.data) as {
262
- type: string;
263
- data?: unknown;
264
- };
491
+ let message: { type: string; data?: unknown };
492
+ try {
493
+ message = JSON.parse(event.nativeEvent.data);
494
+ } catch (error) {
495
+ componentAnalytics.logDeserializeMessageError(
496
+ 'unknown',
497
+ error instanceof Error ? error : new Error(String(error))
498
+ );
499
+ return;
500
+ }
265
501
 
266
502
  if (message.type === 'fetchClientSecret') {
267
503
  const clientSecret = await fetchClientSecret().catch((error) => {
@@ -279,11 +515,121 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
279
515
  // message.data is of type string
280
516
  console.debug(`[EmbeddedComponent ${component}]: ${message.data}`);
281
517
  } else if (message.type === 'pageDidLoad') {
518
+ const pageViewId = (message.data as { pageViewId?: string })
519
+ ?.pageViewId;
520
+ componentAnalytics.logComponentWebPageLoaded(pageViewId);
282
521
  onPageDidLoad?.();
522
+ } else if (message.type === 'componentLoaded') {
523
+ // Connect JS fully initialized
524
+ componentAnalytics.logComponentLoaded();
283
525
  } else if (message.type === 'accountSessionClaimed') {
284
526
  // message.data is of type {elementTagName: string, merchantId: string}
285
527
  } else if (message.type === 'openFinancialConnections') {
286
- // message.data is of type {clientSecret: string; id: string; connectedAccountId: string;}
528
+ const messageData = message.data as {
529
+ clientSecret: string;
530
+ id: string;
531
+ connectedAccountId: string;
532
+ };
533
+
534
+ const { clientSecret, id, connectedAccountId } = messageData;
535
+
536
+ // Validate client secret
537
+ if (!clientSecret || typeof clientSecret !== 'string') {
538
+ handleFinancialConnectionsResult(id, {
539
+ error: {
540
+ code: 'InvalidClientSecret',
541
+ message: 'Invalid or missing clientSecret parameter',
542
+ },
543
+ });
544
+ return;
545
+ }
546
+
547
+ // Prevent multiple simultaneous flows
548
+ if (pendingFinancialConnectionsPromise.current) {
549
+ handleFinancialConnectionsResult(id, {
550
+ error: {
551
+ code: 'AlreadyInProgress',
552
+ message: 'Financial Connections flow already in progress',
553
+ },
554
+ });
555
+ return;
556
+ }
557
+
558
+ // Setup event listener for debugging
559
+ let eventListener: EventSubscription | null = null;
560
+ if (__DEV__) {
561
+ eventListener = addListener(
562
+ 'onFinancialConnectionsEvent',
563
+ (fcEvent: FinancialConnections.FinancialConnectionsEvent) => {
564
+ console.debug(
565
+ `[FinancialConnections ${component}]: ${fcEvent.name}`,
566
+ fcEvent.metadata
567
+ );
568
+ }
569
+ );
570
+ }
571
+
572
+ // Store cleanup function
573
+ const cleanup = () => {
574
+ eventListener?.remove();
575
+ pendingFinancialConnectionsPromise.current = null;
576
+ };
577
+
578
+ pendingFinancialConnectionsPromise.current = {
579
+ id,
580
+ cleanup,
581
+ };
582
+
583
+ // Call native Financial Connections
584
+ NativeStripeSdk.collectFinancialConnectionsAccounts(clientSecret, {
585
+ connectedAccountId,
586
+ })
587
+ .then(({ session, error }) => {
588
+ cleanup();
589
+
590
+ if (error) {
591
+ handleFinancialConnectionsResult(id, {
592
+ session: undefined,
593
+ token: undefined,
594
+ error: {
595
+ code: error.code,
596
+ message: error.message,
597
+ localizedMessage: error.localizedMessage,
598
+ type: error.type,
599
+ },
600
+ });
601
+ } else if (session) {
602
+ // Note: collectFinancialConnectionsAccounts doesn't return a token
603
+ // Only collectBankAccountToken returns both session and token
604
+ handleFinancialConnectionsResult(id, {
605
+ session,
606
+ token: undefined,
607
+ error: undefined,
608
+ });
609
+ } else {
610
+ // Defensive: should never happen
611
+ handleFinancialConnectionsResult(id, {
612
+ error: {
613
+ code: 'UnexpectedError',
614
+ message:
615
+ 'No session or error returned from Financial Connections',
616
+ },
617
+ });
618
+ }
619
+ })
620
+ .catch((unexpectedError) => {
621
+ cleanup();
622
+ handleUnexpectedError(unexpectedError);
623
+ handleFinancialConnectionsResult(id, {
624
+ error: {
625
+ code: 'UnexpectedError',
626
+ message:
627
+ unexpectedError instanceof Error
628
+ ? unexpectedError.message
629
+ : 'An unexpected error occurred during Financial Connections',
630
+ },
631
+ });
632
+ });
287
633
  } else if (message.type === 'closeWebView') {
288
634
  // message.data is empty
289
635
  callbacks?.onCloseWebView?.({});
@@ -303,7 +649,12 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
303
649
  // remove the 'set' prefix and lowercase the first letter
304
650
  const functionName =
305
651
  setter.charAt(3).toLowerCase() + setter.substring(4);
306
- callbacks?.[functionName]?.(value);
652
+ if (callbacks?.[functionName]) {
653
+ callbacks[functionName](value);
654
+ } else {
655
+ // Unrecognized setter function
656
+ componentAnalytics.logUnrecognizedSetter(setter);
657
+ }
307
658
  }
308
659
  } else if (message.type === 'openAuthenticatedWebView') {
309
660
  const { url, id } = message.data as { id: string; url: string };
@@ -316,22 +667,51 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
316
667
  return;
317
668
  }
318
669
 
670
+ // Log authenticated web view opened
671
+ componentAnalytics.logAuthenticatedWebViewOpened(id);
672
+
319
673
  // On Android, we need to wait for the deep link callback
320
674
  // On iOS, the promise resolves with the redirect URL
321
675
  NativeStripeSdk.openAuthenticatedWebView(id, url)
322
676
  .then((result) => {
323
677
  if (Platform.OS === 'ios') {
324
678
  // iOS returns the redirect URL directly
325
- handleAuthWebViewResult(id, result?.url ?? null);
679
+ const resultUrl = result?.url ?? null;
680
+ if (resultUrl) {
681
+ componentAnalytics.logAuthenticatedWebViewRedirected(id);
682
+ } else {
683
+ componentAnalytics.logAuthenticatedWebViewCanceled(id);
684
+ }
685
+ handleAuthWebViewResult(id, resultUrl);
326
686
  } else {
327
- // Android: Store promise to be resolved by deep link listener
328
- pendingAuthWebViewPromise.current = {
329
- id,
330
- callback: handleAuthWebViewResult,
331
- };
687
+ // Android: Store promise in Map to be resolved by deep link listener
688
+ pendingAuthWebViewPromises.current.set(id, {
689
+ callback: (authId: string, resultUrl: string | null) => {
690
+ if (resultUrl) {
691
+ componentAnalytics.logAuthenticatedWebViewRedirected(
692
+ authId
693
+ );
694
+ } else {
695
+ componentAnalytics.logAuthenticatedWebViewCanceled(authId);
696
+ }
697
+ handleAuthWebViewResult(authId, resultUrl);
698
+ },
699
+ });
332
700
  }
333
701
  })
334
- .catch(handleUnexpectedError);
702
+ .catch((error) => {
703
+ if (__DEV__) {
704
+ console.error(
705
+ `[EmbeddedComponent] Error opening authenticated webview:`,
706
+ error
707
+ );
708
+ }
709
+ componentAnalytics.logAuthenticatedWebViewError(
710
+ id,
711
+ error instanceof Error ? error : new Error(String(error))
712
+ );
713
+ handleUnexpectedError(error);
714
+ });
335
715
  } else {
336
716
  // unhandled message
337
717
  }
@@ -339,6 +719,7 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
339
719
  [
340
720
  callbacks,
341
721
  component,
722
+ componentAnalytics,
342
723
  fetchClientSecret,
343
724
  handleUnexpectedError,
344
725
  onLoadError,
@@ -347,6 +728,41 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
347
728
  ]
348
729
  );
349
730
 
731
+ const onShouldStartLoadWithRequest = useCallback(
732
+ (event: ShouldStartLoadRequest) => {
733
+ const { url, navigationType } = event;
734
+
735
+ // Handle CSV export downloads
736
+ if (isCsvExportUrl(url)) {
737
+ NativeStripeSdk.downloadAndShareFile(url, null)
738
+ .then((result) => {
739
+ if (!result.success) {
740
+ console.warn('CSV export share failed:', result.error);
741
+ }
742
+ })
743
+ .catch((error) => {
744
+ handleUnexpectedError(error);
745
+ });
746
+ return false; // Block WebView navigation
747
+ }
748
+
749
+ if (navigationType !== 'click') return true;
750
+
751
+ // Allow navigation within allowed Stripe domains (matching iOS SDK behavior)
752
+ if (ALLOWED_STRIPE_HOSTS.some((host) => url.includes(host))) {
753
+ return true; // Allow in-WebView navigation
754
+ }
755
+
756
+ // Open external links in system browser
757
+ Linking.openURL(url).catch((error) => {
758
+ handleUnexpectedError(error);
759
+ });
760
+
761
+ return false; // Block in-WebView navigation for external links
762
+ },
763
+ [handleUnexpectedError]
764
+ );
765
+
350
766
  const backgroundColor = appearance?.variables?.colorBackground || '#FFFFFF';
351
767
 
352
768
  const mergedStyle = useMemo(
@@ -375,6 +791,12 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
375
791
  // Fixes injectedJavaScriptObject in Android https://github.com/react-native-webview/react-native-webview/issues/3326#issuecomment-3048111789
376
792
  injectedJavaScriptBeforeContentLoaded={'(function() {})();'}
377
793
  onMessage={onMessageCallback}
794
+ onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
795
+ onLayout={handleLayout}
796
+ // Camera/Media Permissions - matches iOS SDK behavior
797
+ mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"
798
+ allowsInlineMediaPlayback={true}
799
+ mediaPlaybackRequiresUserAction={false}
378
800
  />
379
801
  );
380
802
  }
@@ -405,3 +827,16 @@ function isValidUrl(url: string): boolean {
405
827
  return false;
406
828
  }
407
829
  }
830
+
831
+ // Detects Stripe CSV export URLs
832
+ function isCsvExportUrl(url: string): boolean {
833
+ try {
834
+ const parsedUrl = new URL(url);
835
+ return (
836
+ parsedUrl.hostname.includes('stripe-data-exports') ||
837
+ parsedUrl.pathname.includes('stripe-data-exports')
838
+ );
839
+ } catch {
840
+ return false;
841
+ }
842
+ }