@stripe/stripe-react-native 0.58.0 → 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 (145) hide show
  1. package/android/build.gradle +2 -0
  2. package/android/src/main/AndroidManifest.xml +27 -1
  3. package/android/src/main/java/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt +2 -0
  4. package/android/src/main/java/com/reactnativestripesdk/EventEmitterCompat.kt +8 -0
  5. package/android/src/main/java/com/reactnativestripesdk/PaymentElementConfig.kt +8 -0
  6. package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementConfig.kt +147 -0
  7. package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementView.kt +164 -0
  8. package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementViewManager.kt +65 -0
  9. package/android/src/main/java/com/reactnativestripesdk/PaymentSheetAppearance.kt +1 -1
  10. package/android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt +55 -26
  11. package/android/src/main/java/com/reactnativestripesdk/StripeConnectDeepLinkInterceptorActivity.kt +77 -0
  12. package/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt +325 -22
  13. package/android/src/main/java/com/reactnativestripesdk/StripeSdkPackage.kt +1 -0
  14. package/android/src/main/java/com/reactnativestripesdk/customersheet/CustomerSheetManager.kt +3 -0
  15. package/android/src/main/java/com/reactnativestripesdk/utils/Errors.kt +8 -0
  16. package/android/src/main/res/xml/file_paths.xml +4 -0
  17. package/android/src/oldarch/java/com/facebook/react/viewmanagers/PaymentMethodMessagingElementViewManagerDelegate.java +36 -0
  18. package/android/src/oldarch/java/com/facebook/react/viewmanagers/PaymentMethodMessagingElementViewManagerInterface.java +18 -0
  19. package/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java +20 -0
  20. package/android/src/test/java/com/reactnativestripesdk/PaymentElementConfigTest.kt +37 -0
  21. package/android/src/test/java/com/reactnativestripesdk/PaymentMethodMessagingElementConfigTest.kt +543 -0
  22. package/android/src/test/java/com/reactnativestripesdk/PaymentSheetManagerTest.kt +70 -0
  23. package/ios/CustomerSheet/CustomerSheetUtils.swift +4 -0
  24. package/ios/OldArch/StripeSdkEventEmitterCompat.h +2 -0
  25. package/ios/OldArch/StripeSdkEventEmitterCompat.m +13 -1
  26. package/ios/PaymentMethodMessagingElementConfig.swift +116 -0
  27. package/ios/PaymentMethodMessagingElementHandler.m +9 -0
  28. package/ios/PaymentMethodMessagingElementView.swift +139 -0
  29. package/ios/StripeSdk.mm +40 -0
  30. package/ios/StripeSdkEmitter.swift +2 -0
  31. package/ios/StripeSdkImpl+CustomerSheet.swift +1 -0
  32. package/ios/StripeSdkImpl+Embedded.swift +4 -0
  33. package/ios/StripeSdkImpl+PaymentSheet.swift +16 -0
  34. package/ios/StripeSdkImpl.swift +132 -0
  35. package/jest/mock.js +20 -0
  36. package/lib/commonjs/connect/Components.js.map +1 -1
  37. package/lib/commonjs/connect/ConnectComponentsProvider.js +1 -1
  38. package/lib/commonjs/connect/ConnectComponentsProvider.js.map +1 -1
  39. package/lib/commonjs/connect/EmbeddedComponent.js +5 -5
  40. package/lib/commonjs/connect/EmbeddedComponent.js.map +1 -1
  41. package/lib/commonjs/connect/analytics/AnalyticsClient.js +2 -0
  42. package/lib/commonjs/connect/analytics/AnalyticsClient.js.map +1 -0
  43. package/lib/commonjs/connect/analytics/ComponentAnalyticsClient.js +2 -0
  44. package/lib/commonjs/connect/analytics/ComponentAnalyticsClient.js.map +1 -0
  45. package/lib/commonjs/connect/analytics/events.js +2 -0
  46. package/lib/commonjs/connect/analytics/events.js.map +1 -0
  47. package/lib/commonjs/connect/testUtils.js +2 -0
  48. package/lib/commonjs/connect/testUtils.js.map +1 -0
  49. package/lib/commonjs/events.js.map +1 -1
  50. package/lib/commonjs/functions.js +1 -1
  51. package/lib/commonjs/functions.js.map +1 -1
  52. package/lib/commonjs/hooks/useStripe.js +1 -1
  53. package/lib/commonjs/hooks/useStripe.js.map +1 -1
  54. package/lib/commonjs/specs/NativePaymentMethodMessagingElement.js +2 -0
  55. package/lib/commonjs/specs/NativePaymentMethodMessagingElement.js.map +1 -0
  56. package/lib/commonjs/specs/NativeStripeSdkModule.js.map +1 -1
  57. package/lib/commonjs/types/EmbeddedPaymentElement.js.map +1 -1
  58. package/lib/commonjs/types/Errors.js +1 -1
  59. package/lib/commonjs/types/Errors.js.map +1 -1
  60. package/lib/commonjs/types/PaymentSheet.js.map +1 -1
  61. package/lib/commonjs/types/components/PaymentMethodMessagingElementComponent.js +2 -0
  62. package/lib/commonjs/types/components/PaymentMethodMessagingElementComponent.js.map +1 -0
  63. package/lib/commonjs/types/index.js.map +1 -1
  64. package/lib/module/connect/Components.js.map +1 -1
  65. package/lib/module/connect/ConnectComponentsProvider.js +1 -1
  66. package/lib/module/connect/ConnectComponentsProvider.js.map +1 -1
  67. package/lib/module/connect/EmbeddedComponent.js +5 -5
  68. package/lib/module/connect/EmbeddedComponent.js.map +1 -1
  69. package/lib/module/connect/analytics/AnalyticsClient.js +2 -0
  70. package/lib/module/connect/analytics/AnalyticsClient.js.map +1 -0
  71. package/lib/module/connect/analytics/ComponentAnalyticsClient.js +2 -0
  72. package/lib/module/connect/analytics/ComponentAnalyticsClient.js.map +1 -0
  73. package/lib/module/connect/analytics/events.js +2 -0
  74. package/lib/module/connect/analytics/events.js.map +1 -0
  75. package/lib/module/connect/testUtils.js +2 -0
  76. package/lib/module/connect/testUtils.js.map +1 -0
  77. package/lib/module/events.js.map +1 -1
  78. package/lib/module/functions.js +1 -1
  79. package/lib/module/functions.js.map +1 -1
  80. package/lib/module/hooks/useStripe.js +1 -1
  81. package/lib/module/hooks/useStripe.js.map +1 -1
  82. package/lib/module/specs/NativePaymentMethodMessagingElement.js +2 -0
  83. package/lib/module/specs/NativePaymentMethodMessagingElement.js.map +1 -0
  84. package/lib/module/specs/NativeStripeSdkModule.js.map +1 -1
  85. package/lib/module/types/EmbeddedPaymentElement.js.map +1 -1
  86. package/lib/module/types/Errors.js +1 -1
  87. package/lib/module/types/Errors.js.map +1 -1
  88. package/lib/module/types/PaymentSheet.js.map +1 -1
  89. package/lib/module/types/components/PaymentMethodMessagingElementComponent.js +2 -0
  90. package/lib/module/types/components/PaymentMethodMessagingElementComponent.js.map +1 -0
  91. package/lib/module/types/index.js.map +1 -1
  92. package/lib/typescript/src/connect/Components.d.ts +91 -0
  93. package/lib/typescript/src/connect/Components.d.ts.map +1 -1
  94. package/lib/typescript/src/connect/ConnectComponentsProvider.d.ts +61 -0
  95. package/lib/typescript/src/connect/ConnectComponentsProvider.d.ts.map +1 -1
  96. package/lib/typescript/src/connect/EmbeddedComponent.d.ts.map +1 -1
  97. package/lib/typescript/src/connect/analytics/AnalyticsClient.d.ts +32 -0
  98. package/lib/typescript/src/connect/analytics/AnalyticsClient.d.ts.map +1 -0
  99. package/lib/typescript/src/connect/analytics/ComponentAnalyticsClient.d.ts +94 -0
  100. package/lib/typescript/src/connect/analytics/ComponentAnalyticsClient.d.ts.map +1 -0
  101. package/lib/typescript/src/connect/analytics/events.d.ts +215 -0
  102. package/lib/typescript/src/connect/analytics/events.d.ts.map +1 -0
  103. package/lib/typescript/src/connect/testUtils.d.ts +45 -0
  104. package/lib/typescript/src/connect/testUtils.d.ts.map +1 -0
  105. package/lib/typescript/src/events.d.ts +2 -0
  106. package/lib/typescript/src/events.d.ts.map +1 -1
  107. package/lib/typescript/src/functions.d.ts +13 -1
  108. package/lib/typescript/src/functions.d.ts.map +1 -1
  109. package/lib/typescript/src/hooks/useStripe.d.ts +2 -1
  110. package/lib/typescript/src/hooks/useStripe.d.ts.map +1 -1
  111. package/lib/typescript/src/specs/NativePaymentMethodMessagingElement.d.ts +16 -0
  112. package/lib/typescript/src/specs/NativePaymentMethodMessagingElement.d.ts.map +1 -0
  113. package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts +16 -1
  114. package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts.map +1 -1
  115. package/lib/typescript/src/types/CustomerSheet.d.ts +5 -0
  116. package/lib/typescript/src/types/CustomerSheet.d.ts.map +1 -1
  117. package/lib/typescript/src/types/EmbeddedPaymentElement.d.ts +5 -0
  118. package/lib/typescript/src/types/EmbeddedPaymentElement.d.ts.map +1 -1
  119. package/lib/typescript/src/types/Errors.d.ts +4 -0
  120. package/lib/typescript/src/types/Errors.d.ts.map +1 -1
  121. package/lib/typescript/src/types/PaymentSheet.d.ts +5 -0
  122. package/lib/typescript/src/types/PaymentSheet.d.ts.map +1 -1
  123. package/lib/typescript/src/types/components/PaymentMethodMessagingElementComponent.d.ts +69 -0
  124. package/lib/typescript/src/types/components/PaymentMethodMessagingElementComponent.d.ts.map +1 -0
  125. package/lib/typescript/src/types/index.d.ts +8 -1
  126. package/lib/typescript/src/types/index.d.ts.map +1 -1
  127. package/package.json +1 -1
  128. package/src/connect/Components.tsx +91 -0
  129. package/src/connect/ConnectComponentsProvider.tsx +69 -2
  130. package/src/connect/EmbeddedComponent.tsx +254 -30
  131. package/src/connect/analytics/AnalyticsClient.ts +75 -0
  132. package/src/connect/analytics/ComponentAnalyticsClient.ts +315 -0
  133. package/src/connect/analytics/events.ts +253 -0
  134. package/src/connect/testUtils.ts +37 -0
  135. package/src/events.ts +2 -0
  136. package/src/functions.ts +10 -0
  137. package/src/hooks/useStripe.tsx +8 -0
  138. package/src/specs/NativePaymentMethodMessagingElement.ts +25 -0
  139. package/src/specs/NativeStripeSdkModule.ts +21 -1
  140. package/src/types/CustomerSheet.ts +5 -0
  141. package/src/types/EmbeddedPaymentElement.tsx +5 -0
  142. package/src/types/Errors.ts +5 -0
  143. package/src/types/PaymentSheet.ts +6 -1
  144. package/src/types/components/PaymentMethodMessagingElementComponent.tsx +74 -0
  145. package/src/types/index.ts +11 -0
@@ -26,6 +26,7 @@ import type {
26
26
  StripeConnectInitParams,
27
27
  } from './connectTypes';
28
28
  import type { FinancialConnections } from '../types';
29
+ import { ComponentAnalyticsClient } from './analytics/ComponentAnalyticsClient';
29
30
 
30
31
  const DEVELOPMENT_MODE = false;
31
32
  const DEVELOPMENT_URL =
@@ -35,6 +36,12 @@ const BASE_URL = DEVELOPMENT_MODE ? DEVELOPMENT_URL : PRODUCTION_URL;
35
36
 
36
37
  const sdkVersion = pjson.version;
37
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
+
38
45
  // react-native-webview.html will only load versions in the format X.Y.Z
39
46
  if (!/^\d+\.\d+\.\d+$/.test(sdkVersion)) {
40
47
  throw new Error(
@@ -139,11 +146,18 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
139
146
  WebView: typeof WebView | null;
140
147
  } | null>(null);
141
148
 
142
- // Store pending authenticated webview promise (for Android Custom Tabs)
143
- const pendingAuthWebViewPromise = useRef<{
144
- id: string;
145
- callback: (id: string, url: string | null) => void;
146
- } | null>(null);
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());
147
161
 
148
162
  // Store pending Financial Connections promise
149
163
  const pendingFinancialConnectionsPromise = useRef<{
@@ -151,6 +165,10 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
151
165
  cleanup: () => void;
152
166
  } | null>(null);
153
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
+
154
172
  const loadWebViewComponent = useCallback(async () => {
155
173
  if (dynamicWebview) return;
156
174
 
@@ -170,16 +188,106 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
170
188
 
171
189
  const appState = useRef<AppStateStatus>(AppState.currentState);
172
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
173
267
  useEffect(() => {
174
268
  const subscription = AppState.addEventListener('change', (nextAppState) => {
175
269
  if (
176
270
  appState.current.match(/inactive|background/) &&
177
271
  nextAppState === 'active'
178
272
  ) {
179
- if (pendingAuthWebViewPromise.current) {
180
- const { id, callback } = pendingAuthWebViewPromise.current;
181
- pendingAuthWebViewPromise.current = null;
182
- 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
+ });
183
291
  }
184
292
  }
185
293
 
@@ -187,13 +295,23 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
187
295
  });
188
296
 
189
297
  return () => {
190
- 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();
191
308
  pendingFinancialConnectionsPromise.current?.cleanup();
192
309
  subscription.remove();
193
310
  };
194
311
  }, []);
195
312
 
196
- const { connectInstance, appearance, locale } = useConnectComponents();
313
+ const { connectInstance, appearance, locale, analyticsClient } =
314
+ useConnectComponents();
197
315
  const { fonts, publishableKey, fetchClientSecret, overrides } =
198
316
  connectInstance.initParams as StripeConnectInitParamsInternal;
199
317
 
@@ -207,6 +325,22 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
207
325
  style,
208
326
  } = props;
209
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
+
210
344
  const hashParams = {
211
345
  component,
212
346
  publicKey: publishableKey,
@@ -292,6 +426,21 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
292
426
  return undefined;
293
427
  }, [WebViewComponent]);
294
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
+
295
444
  const handleAuthWebViewResult = (id: string, resultUrl: string | null) => {
296
445
  ref.current?.injectJavaScript(`
297
446
  (function() {
@@ -339,10 +488,16 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
339
488
 
340
489
  const onMessageCallback = useCallback(
341
490
  async (event: WebViewMessageEvent) => {
342
- const message = JSON.parse(event.nativeEvent.data) as {
343
- type: string;
344
- data?: unknown;
345
- };
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
+ }
346
501
 
347
502
  if (message.type === 'fetchClientSecret') {
348
503
  const clientSecret = await fetchClientSecret().catch((error) => {
@@ -360,7 +515,13 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
360
515
  // message.data is of type string
361
516
  console.debug(`[EmbeddedComponent ${component}]: ${message.data}`);
362
517
  } else if (message.type === 'pageDidLoad') {
518
+ const pageViewId = (message.data as { pageViewId?: string })
519
+ ?.pageViewId;
520
+ componentAnalytics.logComponentWebPageLoaded(pageViewId);
363
521
  onPageDidLoad?.();
522
+ } else if (message.type === 'componentLoaded') {
523
+ // Connect JS fully initialized
524
+ componentAnalytics.logComponentLoaded();
364
525
  } else if (message.type === 'accountSessionClaimed') {
365
526
  // message.data is of type {elementTagName: string, merchantId: string}
366
527
  } else if (message.type === 'openFinancialConnections') {
@@ -488,7 +649,12 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
488
649
  // remove the 'set' prefix and lowercase the first letter
489
650
  const functionName =
490
651
  setter.charAt(3).toLowerCase() + setter.substring(4);
491
- callbacks?.[functionName]?.(value);
652
+ if (callbacks?.[functionName]) {
653
+ callbacks[functionName](value);
654
+ } else {
655
+ // Unrecognized setter function
656
+ componentAnalytics.logUnrecognizedSetter(setter);
657
+ }
492
658
  }
493
659
  } else if (message.type === 'openAuthenticatedWebView') {
494
660
  const { url, id } = message.data as { id: string; url: string };
@@ -501,22 +667,51 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
501
667
  return;
502
668
  }
503
669
 
670
+ // Log authenticated web view opened
671
+ componentAnalytics.logAuthenticatedWebViewOpened(id);
672
+
504
673
  // On Android, we need to wait for the deep link callback
505
674
  // On iOS, the promise resolves with the redirect URL
506
675
  NativeStripeSdk.openAuthenticatedWebView(id, url)
507
676
  .then((result) => {
508
677
  if (Platform.OS === 'ios') {
509
678
  // iOS returns the redirect URL directly
510
- 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);
511
686
  } else {
512
- // Android: Store promise to be resolved by deep link listener
513
- pendingAuthWebViewPromise.current = {
514
- id,
515
- callback: handleAuthWebViewResult,
516
- };
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
+ });
517
700
  }
518
701
  })
519
- .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
+ });
520
715
  } else {
521
716
  // unhandled message
522
717
  }
@@ -524,6 +719,7 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
524
719
  [
525
720
  callbacks,
526
721
  component,
722
+ componentAnalytics,
527
723
  fetchClientSecret,
528
724
  handleUnexpectedError,
529
725
  onLoadError,
@@ -534,19 +730,33 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
534
730
 
535
731
  const onShouldStartLoadWithRequest = useCallback(
536
732
  (event: ShouldStartLoadRequest) => {
537
- const { url } = event;
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;
538
750
 
539
751
  // Allow navigation within allowed Stripe domains (matching iOS SDK behavior)
540
752
  if (ALLOWED_STRIPE_HOSTS.some((host) => url.includes(host))) {
541
753
  return true; // Allow in-WebView navigation
542
754
  }
543
755
 
544
- // Validate and open external links in system browser
545
- if (isValidUrl(url)) {
546
- Linking.openURL(url).catch((error) => {
547
- handleUnexpectedError(error);
548
- });
549
- }
756
+ // Open external links in system browser
757
+ Linking.openURL(url).catch((error) => {
758
+ handleUnexpectedError(error);
759
+ });
550
760
 
551
761
  return false; // Block in-WebView navigation for external links
552
762
  },
@@ -582,6 +792,7 @@ export function EmbeddedComponent(props: EmbeddedComponentProps) {
582
792
  injectedJavaScriptBeforeContentLoaded={'(function() {})();'}
583
793
  onMessage={onMessageCallback}
584
794
  onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
795
+ onLayout={handleLayout}
585
796
  // Camera/Media Permissions - matches iOS SDK behavior
586
797
  mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"
587
798
  allowsInlineMediaPlayback={true}
@@ -616,3 +827,16 @@ function isValidUrl(url: string): boolean {
616
827
  return false;
617
828
  }
618
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
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Analytics HTTP client for sending events to Stripe backend
3
+ * Matches the Swift iOS SDK's AnalyticsClientV2 user agent format
4
+ */
5
+
6
+ import { Platform } from 'react-native';
7
+ import type { AnalyticsPayload } from './events';
8
+
9
+ const ANALYTICS_URL = 'https://r.stripe.com/0';
10
+ const CLIENT_ID = 'mobile_connect_sdk';
11
+ const ORIGIN = 'stripe-connect-react-native';
12
+
13
+ /**
14
+ * System information for analytics
15
+ */
16
+ export interface SystemInfo {
17
+ sdkVersion: string;
18
+ osVersion: string;
19
+ deviceType: string;
20
+ appName: string;
21
+ appVersion: string;
22
+ }
23
+
24
+ /**
25
+ * Analytics client for sending events to Stripe backend
26
+ */
27
+ export class AnalyticsClient {
28
+ private systemInfo: SystemInfo;
29
+
30
+ constructor(systemInfo: SystemInfo) {
31
+ this.systemInfo = systemInfo;
32
+ }
33
+
34
+ /**
35
+ * Send an analytics event to the Stripe backend
36
+ * Silently fails if network request fails - analytics should never break app functionality
37
+ */
38
+ async sendEvent(payload: AnalyticsPayload): Promise<void> {
39
+ try {
40
+ const fullPayload = {
41
+ ...payload,
42
+ client_id: CLIENT_ID,
43
+ origin: ORIGIN,
44
+ sdk_platform: Platform.OS as 'ios' | 'android',
45
+ sdk_version: this.systemInfo.sdkVersion,
46
+ os_version: this.systemInfo.osVersion,
47
+ device_type: this.systemInfo.deviceType,
48
+ app_name: this.systemInfo.appName,
49
+ app_version: this.systemInfo.appVersion,
50
+ };
51
+
52
+ await fetch(ANALYTICS_URL, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'User-Agent': this.buildUserAgent(),
57
+ },
58
+ body: JSON.stringify(fullPayload),
59
+ });
60
+ } catch (error) {
61
+ // Silently fail - analytics should never break app functionality
62
+ if (__DEV__) {
63
+ console.warn('[StripeConnect] Analytics event failed:', error);
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Build user agent string for analytics requests
70
+ */
71
+ private buildUserAgent(): string {
72
+ const platform = Platform.OS; // 'ios' or 'android'
73
+ return `Stripe/v1 ${platform}/${this.systemInfo.sdkVersion}`;
74
+ }
75
+ }