@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.
- package/android/build.gradle +2 -0
- package/android/src/main/AndroidManifest.xml +27 -1
- package/android/src/main/java/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt +2 -0
- package/android/src/main/java/com/reactnativestripesdk/EventEmitterCompat.kt +8 -0
- package/android/src/main/java/com/reactnativestripesdk/PaymentElementConfig.kt +8 -0
- package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementConfig.kt +147 -0
- package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementView.kt +164 -0
- package/android/src/main/java/com/reactnativestripesdk/PaymentMethodMessagingElementViewManager.kt +65 -0
- package/android/src/main/java/com/reactnativestripesdk/PaymentSheetAppearance.kt +1 -1
- package/android/src/main/java/com/reactnativestripesdk/PaymentSheetManager.kt +55 -26
- package/android/src/main/java/com/reactnativestripesdk/StripeConnectDeepLinkInterceptorActivity.kt +77 -0
- package/android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt +325 -22
- package/android/src/main/java/com/reactnativestripesdk/StripeSdkPackage.kt +1 -0
- package/android/src/main/java/com/reactnativestripesdk/customersheet/CustomerSheetManager.kt +3 -0
- package/android/src/main/java/com/reactnativestripesdk/utils/Errors.kt +8 -0
- package/android/src/main/res/xml/file_paths.xml +4 -0
- package/android/src/oldarch/java/com/facebook/react/viewmanagers/PaymentMethodMessagingElementViewManagerDelegate.java +36 -0
- package/android/src/oldarch/java/com/facebook/react/viewmanagers/PaymentMethodMessagingElementViewManagerInterface.java +18 -0
- package/android/src/oldarch/java/com/reactnativestripesdk/NativeStripeSdkModuleSpec.java +20 -0
- package/android/src/test/java/com/reactnativestripesdk/PaymentElementConfigTest.kt +37 -0
- package/android/src/test/java/com/reactnativestripesdk/PaymentMethodMessagingElementConfigTest.kt +543 -0
- package/android/src/test/java/com/reactnativestripesdk/PaymentSheetManagerTest.kt +70 -0
- package/ios/CustomerSheet/CustomerSheetUtils.swift +4 -0
- package/ios/OldArch/StripeSdkEventEmitterCompat.h +2 -0
- package/ios/OldArch/StripeSdkEventEmitterCompat.m +13 -1
- package/ios/PaymentMethodMessagingElementConfig.swift +116 -0
- package/ios/PaymentMethodMessagingElementHandler.m +9 -0
- package/ios/PaymentMethodMessagingElementView.swift +139 -0
- package/ios/StripeSdk.mm +40 -0
- package/ios/StripeSdkEmitter.swift +2 -0
- package/ios/StripeSdkImpl+CustomerSheet.swift +1 -0
- package/ios/StripeSdkImpl+Embedded.swift +4 -0
- package/ios/StripeSdkImpl+PaymentSheet.swift +16 -0
- package/ios/StripeSdkImpl.swift +132 -0
- package/jest/mock.js +20 -0
- package/lib/commonjs/connect/Components.js.map +1 -1
- package/lib/commonjs/connect/ConnectComponentsProvider.js +1 -1
- package/lib/commonjs/connect/ConnectComponentsProvider.js.map +1 -1
- package/lib/commonjs/connect/EmbeddedComponent.js +5 -5
- package/lib/commonjs/connect/EmbeddedComponent.js.map +1 -1
- package/lib/commonjs/connect/analytics/AnalyticsClient.js +2 -0
- package/lib/commonjs/connect/analytics/AnalyticsClient.js.map +1 -0
- package/lib/commonjs/connect/analytics/ComponentAnalyticsClient.js +2 -0
- package/lib/commonjs/connect/analytics/ComponentAnalyticsClient.js.map +1 -0
- package/lib/commonjs/connect/analytics/events.js +2 -0
- package/lib/commonjs/connect/analytics/events.js.map +1 -0
- package/lib/commonjs/connect/testUtils.js +2 -0
- package/lib/commonjs/connect/testUtils.js.map +1 -0
- package/lib/commonjs/events.js.map +1 -1
- package/lib/commonjs/functions.js +1 -1
- package/lib/commonjs/functions.js.map +1 -1
- package/lib/commonjs/hooks/useStripe.js +1 -1
- package/lib/commonjs/hooks/useStripe.js.map +1 -1
- package/lib/commonjs/specs/NativePaymentMethodMessagingElement.js +2 -0
- package/lib/commonjs/specs/NativePaymentMethodMessagingElement.js.map +1 -0
- package/lib/commonjs/specs/NativeStripeSdkModule.js.map +1 -1
- package/lib/commonjs/types/EmbeddedPaymentElement.js.map +1 -1
- package/lib/commonjs/types/Errors.js +1 -1
- package/lib/commonjs/types/Errors.js.map +1 -1
- package/lib/commonjs/types/PaymentSheet.js.map +1 -1
- package/lib/commonjs/types/components/PaymentMethodMessagingElementComponent.js +2 -0
- package/lib/commonjs/types/components/PaymentMethodMessagingElementComponent.js.map +1 -0
- package/lib/commonjs/types/index.js.map +1 -1
- package/lib/module/connect/Components.js.map +1 -1
- package/lib/module/connect/ConnectComponentsProvider.js +1 -1
- package/lib/module/connect/ConnectComponentsProvider.js.map +1 -1
- package/lib/module/connect/EmbeddedComponent.js +5 -5
- package/lib/module/connect/EmbeddedComponent.js.map +1 -1
- package/lib/module/connect/analytics/AnalyticsClient.js +2 -0
- package/lib/module/connect/analytics/AnalyticsClient.js.map +1 -0
- package/lib/module/connect/analytics/ComponentAnalyticsClient.js +2 -0
- package/lib/module/connect/analytics/ComponentAnalyticsClient.js.map +1 -0
- package/lib/module/connect/analytics/events.js +2 -0
- package/lib/module/connect/analytics/events.js.map +1 -0
- package/lib/module/connect/testUtils.js +2 -0
- package/lib/module/connect/testUtils.js.map +1 -0
- package/lib/module/events.js.map +1 -1
- package/lib/module/functions.js +1 -1
- package/lib/module/functions.js.map +1 -1
- package/lib/module/hooks/useStripe.js +1 -1
- package/lib/module/hooks/useStripe.js.map +1 -1
- package/lib/module/specs/NativePaymentMethodMessagingElement.js +2 -0
- package/lib/module/specs/NativePaymentMethodMessagingElement.js.map +1 -0
- package/lib/module/specs/NativeStripeSdkModule.js.map +1 -1
- package/lib/module/types/EmbeddedPaymentElement.js.map +1 -1
- package/lib/module/types/Errors.js +1 -1
- package/lib/module/types/Errors.js.map +1 -1
- package/lib/module/types/PaymentSheet.js.map +1 -1
- package/lib/module/types/components/PaymentMethodMessagingElementComponent.js +2 -0
- package/lib/module/types/components/PaymentMethodMessagingElementComponent.js.map +1 -0
- package/lib/module/types/index.js.map +1 -1
- package/lib/typescript/src/connect/Components.d.ts +91 -0
- package/lib/typescript/src/connect/Components.d.ts.map +1 -1
- package/lib/typescript/src/connect/ConnectComponentsProvider.d.ts +61 -0
- package/lib/typescript/src/connect/ConnectComponentsProvider.d.ts.map +1 -1
- package/lib/typescript/src/connect/EmbeddedComponent.d.ts.map +1 -1
- package/lib/typescript/src/connect/analytics/AnalyticsClient.d.ts +32 -0
- package/lib/typescript/src/connect/analytics/AnalyticsClient.d.ts.map +1 -0
- package/lib/typescript/src/connect/analytics/ComponentAnalyticsClient.d.ts +94 -0
- package/lib/typescript/src/connect/analytics/ComponentAnalyticsClient.d.ts.map +1 -0
- package/lib/typescript/src/connect/analytics/events.d.ts +215 -0
- package/lib/typescript/src/connect/analytics/events.d.ts.map +1 -0
- package/lib/typescript/src/connect/testUtils.d.ts +45 -0
- package/lib/typescript/src/connect/testUtils.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +2 -0
- package/lib/typescript/src/events.d.ts.map +1 -1
- package/lib/typescript/src/functions.d.ts +13 -1
- package/lib/typescript/src/functions.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useStripe.d.ts +2 -1
- package/lib/typescript/src/hooks/useStripe.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativePaymentMethodMessagingElement.d.ts +16 -0
- package/lib/typescript/src/specs/NativePaymentMethodMessagingElement.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts +16 -1
- package/lib/typescript/src/specs/NativeStripeSdkModule.d.ts.map +1 -1
- package/lib/typescript/src/types/CustomerSheet.d.ts +5 -0
- package/lib/typescript/src/types/CustomerSheet.d.ts.map +1 -1
- package/lib/typescript/src/types/EmbeddedPaymentElement.d.ts +5 -0
- package/lib/typescript/src/types/EmbeddedPaymentElement.d.ts.map +1 -1
- package/lib/typescript/src/types/Errors.d.ts +4 -0
- package/lib/typescript/src/types/Errors.d.ts.map +1 -1
- package/lib/typescript/src/types/PaymentSheet.d.ts +5 -0
- package/lib/typescript/src/types/PaymentSheet.d.ts.map +1 -1
- package/lib/typescript/src/types/components/PaymentMethodMessagingElementComponent.d.ts +69 -0
- package/lib/typescript/src/types/components/PaymentMethodMessagingElementComponent.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +8 -1
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/connect/Components.tsx +91 -0
- package/src/connect/ConnectComponentsProvider.tsx +69 -2
- package/src/connect/EmbeddedComponent.tsx +254 -30
- package/src/connect/analytics/AnalyticsClient.ts +75 -0
- package/src/connect/analytics/ComponentAnalyticsClient.ts +315 -0
- package/src/connect/analytics/events.ts +253 -0
- package/src/connect/testUtils.ts +37 -0
- package/src/events.ts +2 -0
- package/src/functions.ts +10 -0
- package/src/hooks/useStripe.tsx +8 -0
- package/src/specs/NativePaymentMethodMessagingElement.ts +25 -0
- package/src/specs/NativeStripeSdkModule.ts +21 -1
- package/src/types/CustomerSheet.ts +5 -0
- package/src/types/EmbeddedPaymentElement.tsx +5 -0
- package/src/types/Errors.ts +5 -0
- package/src/types/PaymentSheet.ts +6 -1
- package/src/types/components/PaymentMethodMessagingElementComponent.tsx +74 -0
- 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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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 (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
data
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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(
|
|
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
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
}
|