@glomopay/react-native-sdk 4.1.0 → 5.0.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 (124) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/MIGRATION.md +112 -0
  3. package/README.md +173 -36
  4. package/lib/bridge/file-input-handler.d.ts +25 -0
  5. package/lib/bridge/file-input-handler.d.ts.map +1 -0
  6. package/lib/bridge/file-input-handler.js +38 -0
  7. package/lib/bridge/file-save-handler.d.ts +32 -0
  8. package/lib/bridge/file-save-handler.d.ts.map +1 -0
  9. package/lib/bridge/file-save-handler.js +92 -0
  10. package/lib/bridge/message-router.d.ts +44 -0
  11. package/lib/bridge/message-router.d.ts.map +1 -0
  12. package/lib/bridge/message-router.js +48 -0
  13. package/lib/bridge/payment-payload.d.ts +18 -0
  14. package/lib/bridge/payment-payload.d.ts.map +1 -0
  15. package/lib/bridge/payment-payload.js +31 -0
  16. package/lib/capabilities/file-save.d.ts +40 -0
  17. package/lib/capabilities/file-save.d.ts.map +1 -0
  18. package/lib/capabilities/file-save.js +101 -0
  19. package/lib/capabilities/index.d.ts +22 -0
  20. package/lib/capabilities/index.d.ts.map +1 -0
  21. package/lib/capabilities/index.js +34 -0
  22. package/lib/capabilities/use-capability-reporting.d.ts +24 -0
  23. package/lib/capabilities/use-capability-reporting.d.ts.map +1 -0
  24. package/lib/capabilities/use-capability-reporting.js +44 -0
  25. package/lib/config/base.d.ts +12 -0
  26. package/lib/config/base.d.ts.map +1 -1
  27. package/lib/config/base.js +13 -1
  28. package/lib/glomo-checkout.d.ts +6 -1
  29. package/lib/glomo-checkout.d.ts.map +1 -1
  30. package/lib/glomo-checkout.js +11 -5
  31. package/lib/glomo-lrs-checkout.d.ts.map +1 -1
  32. package/lib/glomo-lrs-checkout.js +33 -10
  33. package/lib/glomo-standard-checkout.d.ts +3 -1
  34. package/lib/glomo-standard-checkout.d.ts.map +1 -1
  35. package/lib/glomo-standard-checkout.js +21 -13
  36. package/lib/glomo-subscriptions-checkout.d.ts.map +1 -1
  37. package/lib/glomo-subscriptions-checkout.js +1 -1
  38. package/lib/index.d.ts +2 -2
  39. package/lib/index.d.ts.map +1 -1
  40. package/lib/index.js +4 -3
  41. package/lib/injections/index.d.ts +24 -0
  42. package/lib/injections/index.d.ts.map +1 -1
  43. package/lib/injections/index.js +41 -0
  44. package/lib/injections/webview-flow.injection.d.ts.map +1 -1
  45. package/lib/injections/webview-flow.injection.js +76 -0
  46. package/lib/injections/webview-main.injection.d.ts.map +1 -1
  47. package/lib/injections/webview-main.injection.js +31 -0
  48. package/lib/injections/webview-standard.injection.d.ts.map +1 -1
  49. package/lib/injections/webview-standard.injection.js +31 -0
  50. package/lib/services/file-saver.d.ts +109 -0
  51. package/lib/services/file-saver.d.ts.map +1 -0
  52. package/lib/services/file-saver.js +416 -0
  53. package/lib/types/checkout.d.ts +58 -6
  54. package/lib/types/checkout.d.ts.map +1 -1
  55. package/lib/types/checkout.js +24 -2
  56. package/lib/types/standard-checkout.d.ts +6 -6
  57. package/lib/types/standard-checkout.d.ts.map +1 -1
  58. package/lib/types/subscriptions-checkout.d.ts +3 -3
  59. package/lib/types/subscriptions-checkout.d.ts.map +1 -1
  60. package/lib/use-glomo-checkout.js +2 -2
  61. package/lib/use-lrs-checkout.d.ts +9 -5
  62. package/lib/use-lrs-checkout.d.ts.map +1 -1
  63. package/lib/use-lrs-checkout.js +317 -171
  64. package/lib/use-standard-checkout.d.ts +17 -13
  65. package/lib/use-standard-checkout.d.ts.map +1 -1
  66. package/lib/use-standard-checkout.js +281 -207
  67. package/lib/utils/analytics.d.ts +177 -51
  68. package/lib/utils/analytics.d.ts.map +1 -1
  69. package/lib/utils/analytics.js +299 -81
  70. package/lib/utils/device-compliance.d.ts +57 -12
  71. package/lib/utils/device-compliance.d.ts.map +1 -1
  72. package/lib/utils/device-compliance.js +101 -37
  73. package/lib/utils/event-projections.d.ts +56 -0
  74. package/lib/utils/event-projections.d.ts.map +1 -0
  75. package/lib/utils/event-projections.js +42 -0
  76. package/lib/utils/file-metadata.d.ts +52 -0
  77. package/lib/utils/file-metadata.d.ts.map +1 -0
  78. package/lib/utils/file-metadata.js +134 -0
  79. package/lib/utils/use-sdk-session.d.ts +12 -0
  80. package/lib/utils/use-sdk-session.d.ts.map +1 -0
  81. package/lib/utils/use-sdk-session.js +37 -0
  82. package/lib/utils/validation.d.ts +37 -3
  83. package/lib/utils/validation.d.ts.map +1 -1
  84. package/lib/utils/validation.js +64 -8
  85. package/lib/webview/capability-props.d.ts +27 -0
  86. package/lib/webview/capability-props.d.ts.map +1 -0
  87. package/lib/webview/capability-props.js +61 -0
  88. package/lib/webview/use-permission-request.d.ts +33 -0
  89. package/lib/webview/use-permission-request.d.ts.map +1 -0
  90. package/lib/webview/use-permission-request.js +89 -0
  91. package/package.json +10 -2
  92. package/src/bridge/file-input-handler.ts +66 -0
  93. package/src/bridge/file-save-handler.ts +141 -0
  94. package/src/bridge/message-router.ts +92 -0
  95. package/src/bridge/payment-payload.ts +31 -0
  96. package/src/capabilities/file-save.ts +124 -0
  97. package/src/capabilities/index.ts +34 -0
  98. package/src/capabilities/use-capability-reporting.ts +55 -0
  99. package/src/config/base.ts +13 -0
  100. package/src/glomo-checkout.tsx +11 -7
  101. package/src/glomo-lrs-checkout.tsx +41 -26
  102. package/src/glomo-standard-checkout.tsx +31 -34
  103. package/src/glomo-subscriptions-checkout.tsx +1 -0
  104. package/src/index.ts +4 -2
  105. package/src/injections/index.ts +43 -0
  106. package/src/injections/webview-flow.injection.ts +76 -0
  107. package/src/injections/webview-main.injection.ts +31 -0
  108. package/src/injections/webview-standard.injection.ts +31 -0
  109. package/src/services/file-saver.ts +497 -0
  110. package/src/types/checkout.ts +62 -6
  111. package/src/types/standard-checkout.ts +6 -6
  112. package/src/types/subscriptions-checkout.ts +7 -3
  113. package/src/use-glomo-checkout.tsx +2 -2
  114. package/src/use-lrs-checkout.tsx +656 -365
  115. package/src/use-standard-checkout.tsx +728 -466
  116. package/src/utils/analytics.ts +422 -117
  117. package/src/utils/device-compliance.ts +148 -38
  118. package/src/utils/event-projections.ts +74 -0
  119. package/src/utils/file-metadata.ts +140 -0
  120. package/src/utils/use-sdk-session.ts +41 -0
  121. package/src/utils/validation.ts +70 -10
  122. package/src/webview/capability-props.ts +86 -0
  123. package/src/webview/use-permission-request.ts +114 -0
  124. package/src/jail-monkey.d.ts +0 -19
@@ -1,26 +1,52 @@
1
1
  /** Main SDK hook for the GlomoPay Standard Checkout flow */
2
2
 
3
3
  import React, { useState, useRef, useMemo, useCallback, useEffect } from "react";
4
- import { Platform, PermissionsAndroid, type NativeSyntheticEvent } from "react-native";
4
+ import { Platform, type NativeSyntheticEvent } from "react-native";
5
5
 
6
6
  import { type WebViewNavigation, type WebViewMessageEvent, type WebView } from "react-native-webview";
7
7
 
8
- import { InjectionScripts } from "./injections/index";
8
+ import { buildInjectionScripts } from "./injections/index";
9
9
  import { initializeSegment } from "./config/segment";
10
- import { resolveServerConfig, type GlomoServer } from "./config/base";
11
- import { isValidPublicKey, isValidOrderId, isValidUrl, isValidPaymentPayload, safeCallback } from "./utils/validation";
12
- import { checkDeviceCompliance, isComplianceCheckAvailable } from "./utils/device-compliance";
13
- import { type SdkError, type CheckoutAnalyticsTrackers, standardCheckoutTrackers } from "./utils/analytics";
14
-
15
- import { ASYNC_PAYMENT_EVENTS, type GlomoUserJourneyCompletedPayload } from "./types/checkout";
10
+ import { resolveServerConfig, ANALYTICS_PRODUCT, ANALYTICS_SURFACE, type GlomoServer } from "./config/base";
11
+ import {
12
+ isValidPublicKey,
13
+ isValidOrderId,
14
+ isValidUrl,
15
+ isValidPaymentPayload,
16
+ missingPaymentFields,
17
+ safeCallback,
18
+ } from "./utils/validation";
19
+ import { droppedPayloadSdkError, payloadKeysOf } from "./bridge/payment-payload";
20
+ import { toPaymentEventData, toTerminationEventData, toJourneyEventData } from "./utils/event-projections";
21
+ import { resolveDeviceCompliance, blocksCheckout, isComplianceCheckAvailable } from "./utils/device-compliance";
22
+ import { createMessageRouter } from "./bridge/message-router";
23
+ import { createFileSaveHandler } from "./bridge/file-save-handler";
24
+ import { createFileInputHandler } from "./bridge/file-input-handler";
25
+ import { resolveCapabilities, resolveFileSaveAvailability } from "./capabilities/index";
26
+ import { useCapabilityReporting } from "./capabilities/use-capability-reporting";
27
+ import { useSdkSession } from "./utils/use-sdk-session";
28
+ import { usePermissionRequest, type WebViewPermissionRequest } from "./webview/use-permission-request";
29
+ import {
30
+ trackIncompletePaymentPayload,
31
+ trackDroppedPaymentPayload,
32
+ trackDevicePermissionRefused,
33
+ trackBackNavigationViaSdkBackButton,
34
+ type SdkError,
35
+ type FlowType,
36
+ type CheckoutAnalyticsTrackers,
37
+ standardCheckoutTrackers,
38
+ } from "./utils/analytics";
39
+
40
+ import {
41
+ ASYNC_PAYMENT_EVENTS,
42
+ TERMINATION_SOURCES,
43
+ type GlomoTerminationPayload,
44
+ type GlomoUserJourneyCompletedPayload,
45
+ } from "./types/checkout";
16
46
  import { type StandardCheckoutStatus, type GlomoStandardCheckoutPayload } from "./types/standard-checkout";
17
47
 
18
- /** Android-only WebView permission request, not yet in react-native-webview type definitions */
19
- export interface WebViewPermissionRequest {
20
- nativeEvent: { resources: string[] };
21
- grant: (resources: string[]) => void;
22
- deny: () => void;
23
- }
48
+ /** Log prefix for everything this flow emits */
49
+ const LOG_PREFIX = "[Glomo-RN-SDK Standard]";
24
50
 
25
51
  /** The options for the useStandardCheckout hook */
26
52
  export interface UseStandardCheckoutOptions {
@@ -30,10 +56,10 @@ export interface UseStandardCheckoutOptions {
30
56
  onPaymentSuccess: (payload: GlomoStandardCheckoutPayload) => void;
31
57
  onPaymentFailure: (payload: GlomoStandardCheckoutPayload) => void;
32
58
  onConnectionError?: (error: unknown) => void;
33
- onPaymentTerminate?: () => void;
59
+ onPaymentTerminate?: (payload: GlomoTerminationPayload) => void;
34
60
  onSdkError: (error: Array<SdkError>) => void;
35
61
  onUserJourneyCompleted?: (payload: GlomoUserJourneyCompletedPayload) => void;
36
- onUserRefusedCameraPermissions?: () => void;
62
+ onUserRefusedDevicePermissions?: () => void;
37
63
  devMode?: boolean;
38
64
  }
39
65
 
@@ -68,6 +94,19 @@ export interface UseStandardCheckoutReturn {
68
94
  */
69
95
  export interface UseStandardCheckoutInternalOptions {
70
96
  analyticsTrackers?: CheckoutAnalyticsTrackers;
97
+
98
+ /**
99
+ * Which flow this hook is serving, for the telemetry that is not routed through
100
+ * analyticsTrackers.
101
+ *
102
+ * The tracker map cannot answer this: its whole purpose is that the caller picks a set of
103
+ * flow-specific functions, so a flow-indifferent event has nothing to look up there. File saves
104
+ * are the first of those - the capability is advertised to every WebView regardless of flow -
105
+ * and putting them in the map would re-couple a capability to a flow, which is the thing v5
106
+ * exists to undo.
107
+ */
108
+ flowType?: FlowType;
109
+
71
110
  _skipOrderIdValidation?: boolean;
72
111
  _testInjectionScript?: string;
73
112
  }
@@ -84,7 +123,7 @@ export function useStandardCheckout(
84
123
  onPaymentTerminate,
85
124
  onSdkError,
86
125
  onUserJourneyCompleted,
87
- onUserRefusedCameraPermissions,
126
+ onUserRefusedDevicePermissions,
88
127
  devMode = false,
89
128
  }: UseStandardCheckoutOptions,
90
129
  mockMode: boolean,
@@ -92,6 +131,7 @@ export function useStandardCheckout(
92
131
  ): UseStandardCheckoutReturn {
93
132
  // Resolving analytics trackers - defaults to standard, subscription flow overrides
94
133
  const trackers = internalOptions?.analyticsTrackers ?? standardCheckoutTrackers;
134
+ const flowType = internalOptions?.flowType ?? "standard";
95
135
  // Refs for the WebViews
96
136
  const mainWebViewRef = useRef<WebView>(null);
97
137
  const flowWebViewRef = useRef<WebView>(null);
@@ -113,7 +153,9 @@ export function useStandardCheckout(
113
153
  const mode = mockMode ? "mock" : "live";
114
154
  const params = baseParams.concat(
115
155
  `mode=${encodeURIComponent(mode)}`,
116
- `publicKey=${encodeURIComponent(publicKey)}`
156
+ `publicKey=${encodeURIComponent(publicKey)}`,
157
+ `product=${encodeURIComponent(ANALYTICS_PRODUCT)}`,
158
+ `surface=${encodeURIComponent(ANALYTICS_SURFACE)}`
117
159
  );
118
160
  const queryString = params.join("&");
119
161
 
@@ -237,22 +279,65 @@ export function useStandardCheckout(
237
279
  [devMode]
238
280
  );
239
281
 
282
+ // Correlates every event from this mount, and keeps checkoutStatus current on all of them
283
+ useSdkSession(() => status);
284
+
285
+ // Missing optional dependencies reach the host here; start() warns on the console separately
286
+ useCapabilityReporting({ onSdkError, devMode });
287
+
288
+ /**
289
+ * Handler for `file.input` - the user tapping a file input on a bank's page.
290
+ *
291
+ * Registered beside `file.save` on the same WebViews, for the same reason: uploads are not a
292
+ * property of one flow, and until now this message had no handler anywhere.
293
+ */
294
+ const fileInputHandler = useMemo(
295
+ () =>
296
+ createFileInputHandler({
297
+ flowType: flowType,
298
+ orderId,
299
+ publicKey,
300
+ checkoutUrl,
301
+ mockMode,
302
+ devMode,
303
+ }),
304
+ [orderId, publicKey, checkoutUrl, mockMode, devMode, flowType]
305
+ );
306
+
307
+ /**
308
+ * Handler for `file.save` - the page asking the SDK to put a document on the device.
309
+ *
310
+ * One handler, registered on the main and flow WebViews both. Not on the education carousel:
311
+ * that renders our own content, which has nothing to gate, and the capability is not advertised
312
+ * there either.
313
+ */
314
+ const fileSaveHandler = useMemo(
315
+ () =>
316
+ createFileSaveHandler({
317
+ flowType,
318
+ orderId,
319
+ publicKey,
320
+ checkoutUrl,
321
+ mockMode,
322
+ devMode,
323
+ onSdkError,
324
+ }),
325
+ [flowType, orderId, publicKey, checkoutUrl, mockMode, devMode, onSdkError]
326
+ );
327
+
240
328
  /**
241
329
  * Handler for the main WebView messages.
242
330
  * The main WebView hosts the checkout page. Messages arrive as JSON strings from the injected
243
331
  * JavaScript bridge and include console logs, window.open/close interceptions, and payment
244
332
  * lifecycle events (success, failure, terminate, bank transfer, pay via bank).
245
333
  */
246
- const handleMainWebViewMessage = useCallback(
247
- (event: WebViewMessageEvent) => {
248
- try {
249
- // Extracting and validating the raw message data from the WebView event
250
- const data = event.nativeEvent?.data;
251
- if (!data || typeof data !== "string") {
252
- if (devMode) {
253
- console.warn("[Glomo-RN-SDK Standard] Invalid message data from MainWebView");
254
- }
255
- // Tracking invalid message received
334
+ const handleMainWebViewMessage = useMemo(
335
+ () =>
336
+ createMessageRouter({
337
+ logPrefix: LOG_PREFIX,
338
+ source: "MainWebView",
339
+ devMode,
340
+ onInvalidMessage: (data) => {
256
341
  trackers.trackInvalidMessageReceived(
257
342
  "main",
258
343
  data,
@@ -263,216 +348,295 @@ export function useStandardCheckout(
263
348
  mockMode,
264
349
  checkoutUrl
265
350
  );
266
- return;
267
- }
351
+ },
352
+ handlers: {
353
+ "file.save": fileSaveHandler,
354
+ "file.input": fileInputHandler,
268
355
 
269
- const message = JSON.parse(data);
270
- if (devMode) {
271
- console.log("[Glomo-RN-SDK Standard] MainWebView Message received:", message.type, message);
272
- }
356
+ // Forwarding console messages from the injected script to the React Native console
357
+ console: (message) => {
358
+ if (devMode) {
359
+ const level = (typeof message.level === "string" && message.level) || "log";
360
+ console.log(`${LOG_PREFIX} [WebView ${level.toUpperCase()}]`, message.message);
361
+ }
362
+ },
273
363
 
274
- // Forwarding console messages from the injected script to the React Native console
275
- if (message.type === "console") {
276
- if (devMode) {
277
- const level = message.level || "log";
278
- console.log(`[Glomo-RN-SDK Standard] [WebView ${level.toUpperCase()}]`, message.message);
279
- }
280
- return;
281
- }
364
+ // Intercepting window.open calls - opening the target URL in the flow WebView overlay
365
+ "window.open": (message) => {
366
+ const url = message.url;
367
+ if (url && typeof url === "string" && isValidUrl(url)) {
368
+ if (devMode) {
369
+ console.log(`${LOG_PREFIX} window.open called from MainWebView: ${url}`);
370
+ }
371
+ setFlowWebViewUrl(url);
372
+ setShowFlowWebView(true);
373
+ // Tracking window.open interception
374
+ trackers.trackWindowOpen("main", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
375
+ } else {
376
+ if (devMode) {
377
+ console.log(
378
+ `${LOG_PREFIX} Invalid URL in window.open from MainWebView:`,
379
+ url,
380
+ typeof url === "undefined" ? "undefined" : ""
381
+ );
382
+ }
383
+ }
384
+ },
282
385
 
283
- // Intercepting window.open calls - opening the target URL in the flow WebView overlay
284
- if (message.type === "window.open") {
285
- const url = message.url;
286
- if (url && typeof url === "string" && isValidUrl(url)) {
386
+ // Intercepting window.close calls - closing the flow WebView overlay
387
+ "window.close": () => {
287
388
  if (devMode) {
288
- console.log(`[Glomo-RN-SDK Standard] window.open called from MainWebView: ${url}`);
389
+ console.log(`${LOG_PREFIX} Child window close requested from MainWebView`);
289
390
  }
290
- setFlowWebViewUrl(url);
291
- setShowFlowWebView(true);
292
- // Tracking window.open interception
293
- trackers.trackWindowOpen("main", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
294
- } else {
295
- if (devMode) {
296
- console.log(
297
- "[Glomo-RN-SDK Standard] Invalid URL in window.open from MainWebView:",
298
- url,
299
- typeof url === "undefined" ? "undefined" : ""
300
- );
391
+ setShowFlowWebView(false);
392
+ setFlowWebViewUrl("");
393
+ // Tracking window.close interception
394
+ trackers.trackWindowClose("main", orderId, publicKey, devMode, mockMode, checkoutUrl);
395
+ },
396
+
397
+ // Handling merchant JS callbacks on flow completion (payment events, lifecycle events)
398
+ message: (message) => {
399
+ const messageData = message.message as
400
+ | { type?: string; payload?: Record<string, string | undefined> }
401
+ | undefined;
402
+ if (!messageData) return;
403
+ const messageType = messageData.type;
404
+
405
+ // Handling payment success - closing checkout and notifying the merchant
406
+ if (messageType === "payment.success" || messageType === "payment.complete") {
407
+ const paymentMessage = messageData;
408
+ const payload = paymentMessage?.payload;
409
+
410
+ if (isValidPaymentPayload(payload)) {
411
+ if (devMode) {
412
+ console.log("[Glomo-RN-SDK Standard] Payment successful via merchant callback!");
413
+ }
414
+ setShowCheckout(false);
415
+ setShowFlowWebView(false);
416
+ setFlowWebViewUrl("");
417
+ setStatus("payment_successful");
418
+ safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
419
+ // Tracking payment success callback
420
+ trackers.trackPaymentSuccess(
421
+ toPaymentEventData(payload),
422
+ publicKey,
423
+ devMode,
424
+ mockMode,
425
+ checkoutUrl
426
+ );
427
+ // Fires alongside the callback, not instead of it - the outcome is real, the payload is thin
428
+ const missingFields = missingPaymentFields(payload);
429
+ if (missingFields.length > 0) {
430
+ trackIncompletePaymentPayload(
431
+ "payment.success",
432
+ missingFields,
433
+ "main",
434
+ payload.orderId,
435
+ publicKey,
436
+ devMode,
437
+ mockMode,
438
+ checkoutUrl,
439
+ flowType
440
+ );
441
+ }
442
+ } else {
443
+ /**
444
+ * Reported, not dropped. Before v5 this branch was a devMode-only console.error, so in a
445
+ * release build a terminal payment outcome the SDK could not route left no trace
446
+ * anywhere - no callback, no telemetry, nothing for support to find.
447
+ */
448
+ if (devMode) {
449
+ console.error(
450
+ `${LOG_PREFIX} Unroutable payment.success payload from main:`,
451
+ payload
452
+ );
453
+ }
454
+ trackDroppedPaymentPayload(
455
+ "payment.success",
456
+ payloadKeysOf(payload),
457
+ "main",
458
+ orderId,
459
+ publicKey,
460
+ devMode,
461
+ mockMode,
462
+ checkoutUrl,
463
+ flowType
464
+ );
465
+ safeCallback(
466
+ onSdkError,
467
+ [[droppedPayloadSdkError("payment.success")]],
468
+ "onSdkError",
469
+ devMode
470
+ );
471
+ }
472
+ return;
301
473
  }
302
- }
303
- return;
304
- }
305
-
306
- // Intercepting window.close calls - closing the flow WebView overlay
307
- if (message.type === "window.close") {
308
- if (devMode) {
309
- console.log("[Glomo-RN-SDK Standard] Child window close requested from MainWebView");
310
- }
311
- setShowFlowWebView(false);
312
- setFlowWebViewUrl("");
313
- // Tracking window.close interception
314
- trackers.trackWindowClose("main", orderId, publicKey, devMode, mockMode, checkoutUrl);
315
- return;
316
- }
317
474
 
318
- // Handling merchant JS callbacks on flow completion (payment events, lifecycle events)
319
- if (message.type === "message" && message.message) {
320
- const messageData = message.message;
321
- const messageType = messageData?.type;
322
-
323
- // Handling payment success - closing checkout and notifying the merchant
324
- if (messageType === "payment.success" || messageType === "payment.complete") {
325
- const paymentMessage = messageData;
326
- const payload = paymentMessage?.payload;
475
+ // Handling payment failure - closing checkout and notifying the merchant
476
+ if (messageType === "payment.failure" || messageType === "payment.error") {
477
+ const paymentMessage = messageData;
478
+ const payload = paymentMessage?.payload;
479
+
480
+ if (isValidPaymentPayload(payload)) {
481
+ if (devMode) {
482
+ console.error("[Glomo-RN-SDK Standard] Payment failed via merchant callback");
483
+ }
484
+ setShowCheckout(false);
485
+ setShowFlowWebView(false);
486
+ setFlowWebViewUrl("");
487
+ setStatus("payment_failed");
488
+ safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
489
+ // Tracking payment failure callback
490
+ trackers.trackPaymentFailure(
491
+ toPaymentEventData(payload),
492
+ publicKey,
493
+ devMode,
494
+ mockMode,
495
+ checkoutUrl
496
+ );
497
+ // Fires alongside the callback, not instead of it - the outcome is real, the payload is thin
498
+ const missingFields = missingPaymentFields(payload);
499
+ if (missingFields.length > 0) {
500
+ trackIncompletePaymentPayload(
501
+ "payment.failure",
502
+ missingFields,
503
+ "main",
504
+ payload.orderId,
505
+ publicKey,
506
+ devMode,
507
+ mockMode,
508
+ checkoutUrl,
509
+ flowType
510
+ );
511
+ }
512
+ } else {
513
+ /**
514
+ * Reported, not dropped. Before v5 this branch was a devMode-only console.error, so in a
515
+ * release build a terminal payment outcome the SDK could not route left no trace
516
+ * anywhere - no callback, no telemetry, nothing for support to find.
517
+ */
518
+ if (devMode) {
519
+ console.error(
520
+ `${LOG_PREFIX} Unroutable payment.failure payload from main:`,
521
+ payload
522
+ );
523
+ }
524
+ trackDroppedPaymentPayload(
525
+ "payment.failure",
526
+ payloadKeysOf(payload),
527
+ "main",
528
+ orderId,
529
+ publicKey,
530
+ devMode,
531
+ mockMode,
532
+ checkoutUrl,
533
+ flowType
534
+ );
535
+ safeCallback(
536
+ onSdkError,
537
+ [[droppedPayloadSdkError("payment.failure")]],
538
+ "onSdkError",
539
+ devMode
540
+ );
541
+ }
542
+ return;
543
+ }
327
544
 
328
- if (isValidPaymentPayload(payload)) {
545
+ // Handling bank transfer submission - user has submitted transfer details
546
+ if (messageType === "payment.bank_transfer_submitted") {
547
+ const payload = messageData?.payload;
329
548
  if (devMode) {
330
- console.log("[Glomo-RN-SDK Standard] Payment successful via merchant callback!");
549
+ console.log("[Glomo-RN-SDK Standard] Bank transfer submitted:", payload);
331
550
  }
332
551
  setShowCheckout(false);
333
552
  setShowFlowWebView(false);
334
553
  setFlowWebViewUrl("");
335
- setStatus("payment_successful");
336
- safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
337
- // Tracking payment success callback
338
- trackers.trackPaymentSuccess(
339
- payload.orderId,
340
- payload.paymentId,
554
+ setStatus("bank_transfer_submitted");
555
+ const journeyPayload: GlomoUserJourneyCompletedPayload = {
556
+ journeyType: ASYNC_PAYMENT_EVENTS.BANK_TRANSFER_SUBMITTED,
557
+ orderId: payload?.orderId,
558
+ senderAccountNumber: payload?.senderAccountNumber,
559
+ transactionReference: payload?.transactionReference,
560
+ };
561
+ safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
562
+ trackers.trackBankTransferSubmitted(
563
+ orderId,
564
+ toJourneyEventData(journeyPayload),
341
565
  publicKey,
342
566
  devMode,
343
567
  mockMode,
344
568
  checkoutUrl
345
569
  );
346
- } else {
347
- if (devMode) {
348
- console.error("[Glomo-RN-SDK Standard] Invalid payment success payload:", payload);
349
- }
570
+ return;
350
571
  }
351
- return;
352
- }
353
572
 
354
- // Handling payment failure - closing checkout and notifying the merchant
355
- if (messageType === "payment.failure" || messageType === "payment.error") {
356
- const paymentMessage = messageData;
357
- const payload = paymentMessage?.payload;
573
+ /**
574
+ * Handling the generic glomoCheckoutJourneyTerminate event.
575
+ * This event is fired by the checkout page when the pay via bank (lean/open finance)
576
+ * flow completes. The deduplication ref prevents firing the callback twice when both
577
+ * main and flow WebViews emit this event.
578
+ */
579
+ if (messageType === "glomoCheckoutJourneyTerminate") {
580
+ // Deduplication - both main and flow WebViews can emit this event
581
+ if (payViaBankCompletedHandledRef.current) {
582
+ if (devMode) {
583
+ console.log(
584
+ "[Glomo-RN-SDK Standard] Pay via bank completed already handled, ignoring duplicate"
585
+ );
586
+ }
587
+ return;
588
+ }
589
+ payViaBankCompletedHandledRef.current = true;
358
590
 
359
- if (isValidPaymentPayload(payload)) {
591
+ const payload = messageData?.payload;
360
592
  if (devMode) {
361
- console.error("[Glomo-RN-SDK Standard] Payment failed via merchant callback");
593
+ console.log("[Glomo-RN-SDK Standard] Pay via bank completed via MainWebView:", payload);
362
594
  }
363
- setShowCheckout(false);
364
595
  setShowFlowWebView(false);
596
+ setShowCheckout(false);
365
597
  setFlowWebViewUrl("");
366
- setStatus("payment_failed");
367
- safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
368
- // Tracking payment failure callback
369
- trackers.trackPaymentFailure(
370
- payload.orderId,
371
- payload.paymentId,
598
+ setStatus("pay_via_bank_completed");
599
+ const journeyPayload: GlomoUserJourneyCompletedPayload = {
600
+ journeyType: ASYNC_PAYMENT_EVENTS.PAY_VIA_BANK_COMPLETED,
601
+ orderId: payload?.orderId,
602
+ status: payload?.status ?? "unknown",
603
+ };
604
+ safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
605
+ trackers.trackPayViaBankCompleted(
606
+ orderId,
607
+ toJourneyEventData(journeyPayload),
372
608
  publicKey,
373
609
  devMode,
374
610
  mockMode,
375
611
  checkoutUrl
376
612
  );
377
- } else {
378
- if (devMode) {
379
- console.error("[Glomo-RN-SDK Standard] Invalid payment failure payload:", payload);
380
- }
381
- }
382
- return;
383
- }
384
-
385
- // Handling bank transfer submission - user has submitted transfer details
386
- if (messageType === "payment.bank_transfer_submitted") {
387
- const payload = messageData?.payload;
388
- if (devMode) {
389
- console.log("[Glomo-RN-SDK Standard] Bank transfer submitted:", payload);
613
+ return;
390
614
  }
391
- setShowCheckout(false);
392
- setShowFlowWebView(false);
393
- setFlowWebViewUrl("");
394
- setStatus("bank_transfer_submitted");
395
- const journeyPayload: GlomoUserJourneyCompletedPayload = {
396
- journeyType: ASYNC_PAYMENT_EVENTS.BANK_TRANSFER_SUBMITTED,
397
- orderId: payload?.orderId,
398
- senderAccountNumber: payload?.senderAccountNumber,
399
- transactionReference: payload?.transactionReference,
400
- };
401
- safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
402
- trackers.trackBankTransferSubmitted(orderId, publicKey, devMode, mockMode, checkoutUrl);
403
- return;
404
- }
405
615
 
406
- /**
407
- * Handling the generic glomoCheckoutJourneyTerminate event.
408
- * This event is fired by the checkout page when the pay via bank (lean/open finance)
409
- * flow completes. The deduplication ref prevents firing the callback twice when both
410
- * main and flow WebViews emit this event.
411
- */
412
- if (messageType === "glomoCheckoutJourneyTerminate") {
413
- // Deduplication - both main and flow WebViews can emit this event
414
- if (payViaBankCompletedHandledRef.current) {
616
+ // Handling checkout.closed events
617
+ if (messageType === "checkout.closed") {
415
618
  if (devMode) {
416
- console.log(
417
- "[Glomo-RN-SDK Standard] Pay via bank completed already handled, ignoring duplicate"
418
- );
619
+ console.log("[Glomo-RN-SDK Standard] Checkout closed event received");
419
620
  }
621
+ setShowCheckout(false);
622
+ setShowFlowWebView(false);
623
+ setFlowWebViewUrl("");
624
+ const termination = { source: TERMINATION_SOURCES.CHECKOUT_CLOSED };
625
+ safeCallback(onPaymentTerminate, [termination], "onPaymentTerminate", devMode);
626
+ // One object to the host and to telemetry, so the two cannot drift
627
+ trackers.trackPaymentTerminate(
628
+ orderId,
629
+ toTerminationEventData(termination),
630
+ publicKey,
631
+ devMode,
632
+ mockMode,
633
+ checkoutUrl
634
+ );
420
635
  return;
421
636
  }
422
- payViaBankCompletedHandledRef.current = true;
423
-
424
- const payload = messageData?.payload;
425
- if (devMode) {
426
- console.log("[Glomo-RN-SDK Standard] Pay via bank completed via MainWebView:", payload);
427
- }
428
- setShowFlowWebView(false);
429
- setShowCheckout(false);
430
- setFlowWebViewUrl("");
431
- setStatus("pay_via_bank_completed");
432
- const journeyPayload: GlomoUserJourneyCompletedPayload = {
433
- journeyType: ASYNC_PAYMENT_EVENTS.PAY_VIA_BANK_COMPLETED,
434
- orderId: payload?.orderId,
435
- status: payload?.status ?? "unknown",
436
- };
437
- safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
438
- trackers.trackPayViaBankCompleted(
439
- orderId,
440
- publicKey,
441
- payload?.status ?? "unknown",
442
- devMode,
443
- mockMode,
444
- checkoutUrl
445
- );
446
- return;
447
- }
448
-
449
- // Handling checkout.closed events
450
- if (messageType === "checkout.closed") {
451
- if (devMode) {
452
- console.log("[Glomo-RN-SDK Standard] Checkout closed event received");
453
- }
454
- setShowCheckout(false);
455
- setShowFlowWebView(false);
456
- setFlowWebViewUrl("");
457
- safeCallback(onPaymentTerminate, [], "onPaymentTerminate", devMode);
458
- // Tracking payment terminate callback
459
- trackers.trackPaymentTerminate(
460
- orderId,
461
- "checkout_closed",
462
- publicKey,
463
- devMode,
464
- mockMode,
465
- checkoutUrl
466
- );
467
- return;
468
- }
469
- }
470
- } catch (error) {
471
- if (devMode) {
472
- console.error("[Glomo-RN-SDK Standard] Error parsing MainWebView message:", error);
473
- }
474
- }
475
- },
637
+ },
638
+ },
639
+ }),
476
640
  [
477
641
  onPaymentSuccess,
478
642
  onPaymentFailure,
@@ -484,6 +648,10 @@ export function useStandardCheckout(
484
648
  publicKey,
485
649
  checkoutUrl,
486
650
  trackers,
651
+ fileSaveHandler,
652
+ flowType,
653
+ onSdkError,
654
+ fileInputHandler,
487
655
  ]
488
656
  );
489
657
 
@@ -492,16 +660,13 @@ export function useStandardCheckout(
492
660
  * The flow WebView hosts child pages opened via window.open (e.g. 3DS, bank redirects).
493
661
  * Message structure mirrors the main WebView handler but operates on the overlay WebView.
494
662
  */
495
- const handleFlowWebViewMessage = useCallback(
496
- (event: WebViewMessageEvent) => {
497
- try {
498
- // Extracting and validating the raw message data from the flow WebView event
499
- const data = event.nativeEvent?.data;
500
- if (!data || typeof data !== "string") {
501
- if (devMode) {
502
- console.warn("[Glomo-RN-SDK Standard] Invalid message data from FlowWebView");
503
- }
504
- // Tracking invalid message received
663
+ const handleFlowWebViewMessage = useMemo(
664
+ () =>
665
+ createMessageRouter({
666
+ logPrefix: LOG_PREFIX,
667
+ source: "FlowWebView",
668
+ devMode,
669
+ onInvalidMessage: (data) => {
505
670
  trackers.trackInvalidMessageReceived(
506
671
  "flow",
507
672
  data,
@@ -512,173 +677,257 @@ export function useStandardCheckout(
512
677
  mockMode,
513
678
  checkoutUrl
514
679
  );
515
- return;
516
- }
517
-
518
- const message = JSON.parse(data);
519
- if (devMode) {
520
- console.log("[Glomo-RN-SDK Standard] FlowWebView Message received:", message.type, message);
521
- }
680
+ },
681
+ handlers: {
682
+ /**
683
+ * Telemetry only, and deliberately not wired to onUserRefusedDevicePermissions.
684
+ * The flow injection sees this by watching getUserMedia, so coverage depends on
685
+ * whether a given bank uses it - partial coverage on a merchant callback is
686
+ * worse than none, because silence would stop being unambiguous.
687
+ */
688
+ "permission.denied": (message) => {
689
+ trackDevicePermissionRefused(
690
+ typeof message.resource === "string" ? message.resource : "camera",
691
+ "get_user_media",
692
+ orderId,
693
+ publicKey,
694
+ devMode,
695
+ mockMode,
696
+ checkoutUrl,
697
+ flowType
698
+ );
699
+ },
522
700
 
523
- // Forwarding console messages from the flow WebView's injected script
524
- if (message.type === "console") {
525
- if (devMode) {
526
- console.log(`[Glomo-RN-SDK Standard] [Flow] ${message.message}`);
527
- }
528
- return;
529
- }
701
+ "file.save": fileSaveHandler,
702
+ "file.input": fileInputHandler,
530
703
 
531
- // Intercepting window.open calls from the flow WebView - navigating within the same overlay
532
- if (message.type === "window.open") {
533
- const url = message.url;
534
- if (url && typeof url === "string" && isValidUrl(url)) {
535
- if (devMode) {
536
- console.log(`[Glomo-RN-SDK Standard] window.open called from FlowWebView: ${url}`);
537
- }
538
- setFlowWebViewUrl(url);
539
- // Tracking window.open interception
540
- trackers.trackWindowOpen("flow", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
541
- } else {
704
+ // Forwarding console messages from the flow WebView's injected script
705
+ console: (message) => {
542
706
  if (devMode) {
543
- console.log(
544
- "[Glomo-RN-SDK Standard] Invalid URL in window.open from FlowWebView:",
545
- url,
546
- typeof url === "undefined" ? "undefined" : ""
547
- );
707
+ console.log(`${LOG_PREFIX} [Flow] ${message.message}`);
548
708
  }
549
- }
550
- return;
551
- }
552
-
553
- // Intercepting window.close calls from the flow WebView - closing the overlay
554
- if (message.type === "window.close") {
555
- if (devMode) {
556
- console.log("[Glomo-RN-SDK Standard] FlowWebView close requested");
557
- }
558
- setShowFlowWebView(false);
559
- setFlowWebViewUrl("");
560
- // Tracking window.close interception
561
- trackers.trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
562
- return;
563
- }
564
-
565
- // Handling merchant JS callbacks from the flow WebView (payment events, lifecycle events)
566
- if (message.type === "message" && message.message) {
567
- const messageData = message.message;
568
- const messageType = messageData?.type;
569
-
570
- // Handling payment success from the flow WebView
571
- if (messageType === "payment.success" || messageType === "payment.complete") {
572
- const paymentMessage = messageData;
573
- const payload = paymentMessage?.payload;
709
+ },
574
710
 
575
- if (isValidPaymentPayload(payload)) {
711
+ // Intercepting window.open calls from the flow WebView - navigating within the same overlay
712
+ "window.open": (message) => {
713
+ const url = message.url;
714
+ if (url && typeof url === "string" && isValidUrl(url)) {
576
715
  if (devMode) {
577
- console.log("[Glomo-RN-SDK Standard] Payment successful via FlowWebView callback!");
716
+ console.log(`${LOG_PREFIX} window.open called from FlowWebView: ${url}`);
578
717
  }
579
- setShowFlowWebView(false);
580
- setShowCheckout(false);
581
- setFlowWebViewUrl("");
582
- setStatus("payment_successful");
583
- safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
584
- // Tracking payment success callback
585
- trackers.trackPaymentSuccess(
586
- payload.orderId,
587
- payload.paymentId,
588
- publicKey,
589
- devMode,
590
- mockMode,
591
- checkoutUrl
592
- );
718
+ setFlowWebViewUrl(url);
719
+ // Tracking window.open interception
720
+ trackers.trackWindowOpen("flow", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
593
721
  } else {
594
722
  if (devMode) {
595
- console.error(
596
- "[Glomo-RN-SDK Standard] Invalid payment success payload from FlowWebView:",
597
- payload
723
+ console.log(
724
+ `${LOG_PREFIX} Invalid URL in window.open from FlowWebView:`,
725
+ url,
726
+ typeof url === "undefined" ? "undefined" : ""
598
727
  );
599
728
  }
600
729
  }
601
- return;
602
- }
730
+ },
731
+
732
+ // Intercepting window.close calls from the flow WebView - closing the overlay
733
+ "window.close": () => {
734
+ if (devMode) {
735
+ console.log(`${LOG_PREFIX} FlowWebView close requested`);
736
+ }
737
+ setShowFlowWebView(false);
738
+ setFlowWebViewUrl("");
739
+ // Tracking window.close interception
740
+ trackers.trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
741
+ },
742
+
743
+ // Handling merchant JS callbacks from the flow WebView (payment events, lifecycle events)
744
+ message: (message) => {
745
+ const messageData = message.message as
746
+ | { type?: string; payload?: Record<string, string | undefined> }
747
+ | undefined;
748
+ if (!messageData) return;
749
+ const messageType = messageData.type;
750
+
751
+ // Handling payment success from the flow WebView
752
+ if (messageType === "payment.success" || messageType === "payment.complete") {
753
+ const paymentMessage = messageData;
754
+ const payload = paymentMessage?.payload;
755
+
756
+ if (isValidPaymentPayload(payload)) {
757
+ if (devMode) {
758
+ console.log("[Glomo-RN-SDK Standard] Payment successful via FlowWebView callback!");
759
+ }
760
+ setShowFlowWebView(false);
761
+ setShowCheckout(false);
762
+ setFlowWebViewUrl("");
763
+ setStatus("payment_successful");
764
+ safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
765
+ // Tracking payment success callback
766
+ trackers.trackPaymentSuccess(
767
+ toPaymentEventData(payload),
768
+ publicKey,
769
+ devMode,
770
+ mockMode,
771
+ checkoutUrl
772
+ );
773
+ // Fires alongside the callback, not instead of it - the outcome is real, the payload is thin
774
+ const missingFields = missingPaymentFields(payload);
775
+ if (missingFields.length > 0) {
776
+ trackIncompletePaymentPayload(
777
+ "payment.success",
778
+ missingFields,
779
+ "flow",
780
+ payload.orderId,
781
+ publicKey,
782
+ devMode,
783
+ mockMode,
784
+ checkoutUrl,
785
+ flowType
786
+ );
787
+ }
788
+ } else {
789
+ /**
790
+ * Reported, not dropped. Before v5 this branch was a devMode-only console.error, so in a
791
+ * release build a terminal payment outcome the SDK could not route left no trace
792
+ * anywhere - no callback, no telemetry, nothing for support to find.
793
+ */
794
+ if (devMode) {
795
+ console.error(
796
+ `${LOG_PREFIX} Unroutable payment.success payload from flow:`,
797
+ payload
798
+ );
799
+ }
800
+ trackDroppedPaymentPayload(
801
+ "payment.success",
802
+ payloadKeysOf(payload),
803
+ "flow",
804
+ orderId,
805
+ publicKey,
806
+ devMode,
807
+ mockMode,
808
+ checkoutUrl,
809
+ flowType
810
+ );
811
+ safeCallback(
812
+ onSdkError,
813
+ [[droppedPayloadSdkError("payment.success")]],
814
+ "onSdkError",
815
+ devMode
816
+ );
817
+ }
818
+ return;
819
+ }
820
+
821
+ // Handling payment failure from the flow WebView
822
+ if (messageType === "payment.failure" || messageType === "payment.error") {
823
+ const paymentMessage = messageData;
824
+ const payload = paymentMessage?.payload;
825
+
826
+ if (isValidPaymentPayload(payload)) {
827
+ if (devMode) {
828
+ console.error("[Glomo-RN-SDK Standard] Payment failed via FlowWebView callback");
829
+ }
830
+ setShowFlowWebView(false);
831
+ setShowCheckout(false);
832
+ setFlowWebViewUrl("");
833
+ setStatus("payment_failed");
834
+ safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
835
+ // Tracking payment failure callback
836
+ trackers.trackPaymentFailure(
837
+ toPaymentEventData(payload),
838
+ publicKey,
839
+ devMode,
840
+ mockMode,
841
+ checkoutUrl
842
+ );
843
+ // Fires alongside the callback, not instead of it - the outcome is real, the payload is thin
844
+ const missingFields = missingPaymentFields(payload);
845
+ if (missingFields.length > 0) {
846
+ trackIncompletePaymentPayload(
847
+ "payment.failure",
848
+ missingFields,
849
+ "flow",
850
+ payload.orderId,
851
+ publicKey,
852
+ devMode,
853
+ mockMode,
854
+ checkoutUrl,
855
+ flowType
856
+ );
857
+ }
858
+ } else {
859
+ /**
860
+ * Reported, not dropped. Before v5 this branch was a devMode-only console.error, so in a
861
+ * release build a terminal payment outcome the SDK could not route left no trace
862
+ * anywhere - no callback, no telemetry, nothing for support to find.
863
+ */
864
+ if (devMode) {
865
+ console.error(
866
+ `${LOG_PREFIX} Unroutable payment.failure payload from flow:`,
867
+ payload
868
+ );
869
+ }
870
+ trackDroppedPaymentPayload(
871
+ "payment.failure",
872
+ payloadKeysOf(payload),
873
+ "flow",
874
+ orderId,
875
+ publicKey,
876
+ devMode,
877
+ mockMode,
878
+ checkoutUrl,
879
+ flowType
880
+ );
881
+ safeCallback(
882
+ onSdkError,
883
+ [[droppedPayloadSdkError("payment.failure")]],
884
+ "onSdkError",
885
+ devMode
886
+ );
887
+ }
888
+ return;
889
+ }
603
890
 
604
- // Handling payment failure from the flow WebView
605
- if (messageType === "payment.failure" || messageType === "payment.error") {
606
- const paymentMessage = messageData;
607
- const payload = paymentMessage?.payload;
891
+ // Handling glomoCheckoutJourneyTerminate from the flow WebView (pay via bank completion)
892
+ if (messageType === "glomoCheckoutJourneyTerminate") {
893
+ // Deduplication - both main and flow WebViews can emit this event
894
+ if (payViaBankCompletedHandledRef.current) {
895
+ if (devMode) {
896
+ console.log(
897
+ "[Glomo-RN-SDK Standard] Pay via bank completed already handled, ignoring duplicate"
898
+ );
899
+ }
900
+ return;
901
+ }
902
+ payViaBankCompletedHandledRef.current = true;
608
903
 
609
- if (isValidPaymentPayload(payload)) {
904
+ const payload = messageData?.payload;
610
905
  if (devMode) {
611
- console.error("[Glomo-RN-SDK Standard] Payment failed via FlowWebView callback");
906
+ console.log("[Glomo-RN-SDK Standard] Pay via bank completed via FlowWebView:", payload);
612
907
  }
613
908
  setShowFlowWebView(false);
614
909
  setShowCheckout(false);
615
910
  setFlowWebViewUrl("");
616
- setStatus("payment_failed");
617
- safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
618
- // Tracking payment failure callback
619
- trackers.trackPaymentFailure(
620
- payload.orderId,
621
- payload.paymentId,
911
+ setStatus("pay_via_bank_completed");
912
+ const journeyPayload: GlomoUserJourneyCompletedPayload = {
913
+ journeyType: ASYNC_PAYMENT_EVENTS.PAY_VIA_BANK_COMPLETED,
914
+ orderId: payload?.orderId,
915
+ status: payload?.status ?? "unknown",
916
+ };
917
+ safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
918
+ trackers.trackPayViaBankCompleted(
919
+ orderId,
920
+ toJourneyEventData(journeyPayload),
622
921
  publicKey,
623
922
  devMode,
624
923
  mockMode,
625
924
  checkoutUrl
626
925
  );
627
- } else {
628
- if (devMode) {
629
- console.error(
630
- "[Glomo-RN-SDK Standard] Invalid payment failure payload from FlowWebView:",
631
- payload
632
- );
633
- }
634
- }
635
- return;
636
- }
637
-
638
- // Handling glomoCheckoutJourneyTerminate from the flow WebView (pay via bank completion)
639
- if (messageType === "glomoCheckoutJourneyTerminate") {
640
- // Deduplication - both main and flow WebViews can emit this event
641
- if (payViaBankCompletedHandledRef.current) {
642
- if (devMode) {
643
- console.log(
644
- "[Glomo-RN-SDK Standard] Pay via bank completed already handled, ignoring duplicate"
645
- );
646
- }
647
926
  return;
648
927
  }
649
- payViaBankCompletedHandledRef.current = true;
650
-
651
- const payload = messageData?.payload;
652
- if (devMode) {
653
- console.log("[Glomo-RN-SDK Standard] Pay via bank completed via FlowWebView:", payload);
654
- }
655
- setShowFlowWebView(false);
656
- setShowCheckout(false);
657
- setFlowWebViewUrl("");
658
- setStatus("pay_via_bank_completed");
659
- const journeyPayload: GlomoUserJourneyCompletedPayload = {
660
- journeyType: ASYNC_PAYMENT_EVENTS.PAY_VIA_BANK_COMPLETED,
661
- orderId: payload?.orderId,
662
- status: payload?.status ?? "unknown",
663
- };
664
- safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
665
- trackers.trackPayViaBankCompleted(
666
- orderId,
667
- publicKey,
668
- payload?.status ?? "unknown",
669
- devMode,
670
- mockMode,
671
- checkoutUrl
672
- );
673
- return;
674
- }
675
- }
676
- } catch (error) {
677
- if (devMode) {
678
- console.error("[Glomo-RN-SDK Standard] Error parsing FlowWebView message:", error);
679
- }
680
- }
681
- },
928
+ },
929
+ },
930
+ }),
682
931
  [
683
932
  onPaymentSuccess,
684
933
  onPaymentFailure,
@@ -690,6 +939,10 @@ export function useStandardCheckout(
690
939
  flowWebViewUrl,
691
940
  checkoutUrl,
692
941
  trackers,
942
+ fileSaveHandler,
943
+ flowType,
944
+ onSdkError,
945
+ fileInputHandler,
693
946
  ]
694
947
  );
695
948
 
@@ -724,10 +977,26 @@ export function useStandardCheckout(
724
977
  console.log("[Glomo-RN-SDK Standard] Ignoring checkout status update for: " + status);
725
978
  }
726
979
  }
727
- safeCallback(onPaymentTerminate, [], "onPaymentTerminate", devMode);
728
- // Tracking payment terminate callback (user dismiss/back button)
729
- const terminateSource = Platform.OS === "ios" ? "user_dismiss" : "back_button";
730
- trackers.trackPaymentTerminate(orderId, terminateSource, publicKey, devMode, mockMode, checkoutUrl);
980
+ /**
981
+ * Inferred from the platform rather than read off an event, because `onRequestClose` is
982
+ * the only wiring here and the two platforms raise it for different gestures: Android
983
+ * for hardware back and the back gesture, iOS for swipe-down on the pageSheet. Nothing
984
+ * else routes through it, so the inference is sound - but it is an inference.
985
+ */
986
+ const termination = {
987
+ source:
988
+ Platform.OS === "ios" ? TERMINATION_SOURCES.MODAL_DISMISS : TERMINATION_SOURCES.DEVICE_BACK_BUTTON,
989
+ };
990
+ safeCallback(onPaymentTerminate, [termination], "onPaymentTerminate", devMode);
991
+ // One object to the host and to telemetry, so the two cannot drift
992
+ trackers.trackPaymentTerminate(
993
+ orderId,
994
+ toTerminationEventData(termination),
995
+ publicKey,
996
+ devMode,
997
+ mockMode,
998
+ checkoutUrl
999
+ );
731
1000
  } catch (error) {
732
1001
  if (devMode) {
733
1002
  console.error("[Glomo-RN-SDK Standard] Error in handleModalBackButton:", error);
@@ -742,93 +1011,46 @@ export function useStandardCheckout(
742
1011
  }
743
1012
  setShowFlowWebView(false);
744
1013
  setFlowWebViewUrl("");
745
- trackers.trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
746
- }, [devMode, orderId, publicKey, mockMode, checkoutUrl, trackers]);
1014
+ trackBackNavigationViaSdkBackButton(orderId, publicKey, devMode, mockMode, checkoutUrl, flowType);
1015
+ }, [devMode, orderId, publicKey, mockMode, checkoutUrl, flowType]);
747
1016
 
748
1017
  /**
749
- * Handler for WebView permission requests (e.g. camera access for bank authentication).
750
- * On Android, prompts the user for camera permission via PermissionsAndroid.
751
- * On iOS, grants camera access automatically (iOS handles its own permission prompt).
752
- * Non-camera permissions are denied by default.
1018
+ * Closing the checkout when the user refuses camera access, since the bank page cannot
1019
+ * proceed without it. Kept here rather than inside usePermissionRequest because moving the
1020
+ * status and notifying the merchant are flow concerns.
1021
+ *
1022
+ * Resets the flow overlay as well as the modal, matching the LRS handler. The prompt is raised
1023
+ * from a bank page inside that overlay, so this is the one close path where the flow state is
1024
+ * guaranteed non-default. Nothing visible depends on it today - the overlay renders inside the
1025
+ * Modal and unmounts with it - but "every close path resets derived state" should not hold in
1026
+ * one flow and not the other.
753
1027
  */
754
- const handlePermissionRequest = useCallback(
755
- async (request: WebViewPermissionRequest) => {
756
- try {
757
- const { resources } = request.nativeEvent;
758
- // Checking if the requested permission is camera-related
759
- const isCameraPermissionRequest = resources.some(
760
- (resource: string) => resource === "camera" || resource.toLowerCase().includes("camera")
761
- );
762
-
763
- if (!isCameraPermissionRequest) {
764
- if (devMode) {
765
- console.log("[Glomo-RN-SDK Standard] Non-camera permission requested, denying:", resources);
766
- }
767
- request.deny();
768
- return;
769
- }
770
-
771
- if (devMode) {
772
- console.log("[Glomo-RN-SDK Standard] Camera permission requested by webpage");
773
- }
1028
+ const handleCameraPermissionDenied = useCallback(() => {
1029
+ setShowCheckout(false);
1030
+ setShowFlowWebView(false);
1031
+ setFlowWebViewUrl("");
1032
+ if (status === "payment_in_progress") {
1033
+ setStatus("payment_cancelled");
1034
+ }
1035
+ safeCallback(onUserRefusedDevicePermissions, [], "onUserRefusedDevicePermissions", devMode);
1036
+ // The permission hook only reaches here for camera; anything else is denied without notifying
1037
+ trackDevicePermissionRefused(
1038
+ "camera",
1039
+ "permission_request",
1040
+ orderId,
1041
+ publicKey,
1042
+ devMode,
1043
+ mockMode,
1044
+ checkoutUrl,
1045
+ flowType
1046
+ );
1047
+ }, [status, onUserRefusedDevicePermissions, devMode, orderId, publicKey, mockMode, checkoutUrl, flowType]);
774
1048
 
775
- if (Platform.OS === "android") {
776
- try {
777
- const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA, {
778
- title: "Camera Permission",
779
- message: "This app needs camera access for identity verification during checkout",
780
- buttonNeutral: "Ask Me Later",
781
- buttonNegative: "Cancel",
782
- buttonPositive: "OK",
783
- });
784
-
785
- if (granted === PermissionsAndroid.RESULTS.GRANTED) {
786
- if (devMode) {
787
- console.log("[Glomo-RN-SDK Standard] Camera permission granted");
788
- }
789
- request.grant(resources);
790
- } else {
791
- if (devMode) {
792
- console.log("[Glomo-RN-SDK Standard] Camera permission denied by user");
793
- }
794
- setShowCheckout(false);
795
- if (status === "payment_in_progress") {
796
- setStatus("payment_cancelled");
797
- }
798
- safeCallback(onUserRefusedCameraPermissions, [], "onUserRefusedCameraPermissions", devMode);
799
- request.deny();
800
- }
801
- } catch (err) {
802
- if (devMode) {
803
- console.error("[Glomo-RN-SDK Standard] Error requesting camera permission:", err);
804
- }
805
- setShowCheckout(false);
806
- if (status === "payment_in_progress") {
807
- setStatus("payment_cancelled");
808
- }
809
- safeCallback(onUserRefusedCameraPermissions, [], "onUserRefusedCameraPermissions", devMode);
810
- request.deny();
811
- }
812
- } else if (Platform.OS === "ios") {
813
- if (devMode) {
814
- console.log("[Glomo-RN-SDK Standard] Granting camera permission request on iOS");
815
- }
816
- request.grant(resources);
817
- } else {
818
- if (devMode) {
819
- console.warn("[Glomo-RN-SDK Standard] Unsupported platform for camera permissions");
820
- }
821
- request.deny();
822
- }
823
- } catch (error) {
824
- if (devMode) {
825
- console.error("[Glomo-RN-SDK Standard] Error in handlePermissionRequest:", error);
826
- }
827
- request.deny();
828
- }
829
- },
830
- [devMode, status, onUserRefusedCameraPermissions]
831
- );
1049
+ const handlePermissionRequest = usePermissionRequest({
1050
+ logPrefix: "[Glomo-RN-SDK Standard]",
1051
+ onDenied: handleCameraPermissionDenied,
1052
+ devMode,
1053
+ });
832
1054
 
833
1055
  /**
834
1056
  * Initiator method for the Standard Checkout flow.
@@ -847,11 +1069,16 @@ export function useStandardCheckout(
847
1069
  * Checking device security compliance (if peerDependencies are available)
848
1070
  * A null value indicates that recommended security dependencies were not installed and compliance checks will be skipped in this case
849
1071
  */
850
- const deviceComplianceCheck = checkDeviceCompliance(devMode);
851
- const jailbreakDetectionLibraryInstalled = isComplianceCheckAvailable();
852
- const jailbreakDetectionInfo = {
853
- jailbreakDetectionLibraryInstalled,
854
- jailbreakDetected: deviceComplianceCheck,
1072
+ const compliance = resolveDeviceCompliance(devMode);
1073
+ const complianceLibraryInstalled = isComplianceCheckAvailable();
1074
+ const fileSave = resolveFileSaveAvailability();
1075
+ const dependencyInfo = {
1076
+ jailbreakDetectionLibraryInstalled: complianceLibraryInstalled,
1077
+ jailbreakDetected: compliance.compromised,
1078
+ complianceSource: compliance.source,
1079
+ complianceSignals: compliance.trippedSignals,
1080
+ fileSaveAvailable: fileSave.available,
1081
+ missingFileSavePackages: fileSave.missing,
855
1082
  };
856
1083
 
857
1084
  if (devMode) {
@@ -861,37 +1088,64 @@ export function useStandardCheckout(
861
1088
  }
862
1089
 
863
1090
  // Tracking start attempt with current status
864
- trackers.trackStartAttempt(status, orderId, publicKey, devMode, mockMode, checkoutUrl, jailbreakDetectionInfo);
1091
+ trackers.trackStartAttempt(status, orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
1092
+
1093
+ /**
1094
+ * Ungated on purpose, both of them. An absent optional dependency degrades silently by
1095
+ * design, so a console warning is the only thing that reaches a merchant who has not
1096
+ * noticed - and the compliance one now precedes a hard block on live keys.
1097
+ */
1098
+ if (!complianceLibraryInstalled) {
1099
+ console.warn(
1100
+ "[Glomo-RN-SDK Standard] jail-monkey is not installed or its native module is not linked, so " +
1101
+ "rooted and jailbroken devices cannot be detected. As of v5 this BLOCKS live checkouts. " +
1102
+ "Install jail-monkey and rebuild the app - see the README."
1103
+ );
1104
+ }
1105
+ if (!fileSave.available) {
1106
+ console.warn(
1107
+ `[Glomo-RN-SDK Standard] Document downloads are disabled: ${fileSave.missing.join(" and ")} ` +
1108
+ "could not be loaded. Install the package(s) and rebuild the app, or ignore this if your " +
1109
+ "checkout does not offer downloads - the page falls back to its own behaviour."
1110
+ );
1111
+ }
865
1112
 
866
- if (deviceComplianceCheck === true) {
867
- // Device detected as compromised
1113
+ if (blocksCheckout(compliance, publicKey)) {
1114
+ /**
1115
+ * Two conditions, one refusal. `compromised === true` is a rooted or jailbroken device;
1116
+ * `null` is a device we could not check, which on RN overwhelmingly means jail-monkey is
1117
+ * absent rather than that a platform probe failed. Both fail closed on a live key, and
1118
+ * neither fires on a test or mock key.
1119
+ */
1120
+ const unknown = compliance.compromised === null;
868
1121
  if (devMode) {
869
- console.error("[Glomo-RN-SDK Standard] Cannot start checkout. Device is rooted or jailbroken.");
1122
+ console.error(
1123
+ `[Glomo-RN-SDK Standard] Cannot start checkout. ${
1124
+ unknown ? "Device security could not be verified." : "Device is rooted or jailbroken."
1125
+ }`
1126
+ );
870
1127
  }
871
1128
  const errors: SdkError[] = [
872
1129
  {
873
1130
  type: "device_forbidden",
874
- message: "Standard checkout is not allowed on rooted or jailbroken devices.",
1131
+ message: unknown
1132
+ ? "Standard checkout could not verify this device's security. Install jail-monkey and rebuild the app."
1133
+ : "Standard checkout is not allowed on rooted or jailbroken devices.",
875
1134
  },
876
1135
  ];
877
1136
  safeCallback(onSdkError, [errors], "onSdkError", devMode);
878
1137
  // Tracking start() failure
879
1138
  trackers.trackStartFailure(
880
- "device_forbidden",
1139
+ unknown ? "device_compliance_unknown" : "device_forbidden",
881
1140
  orderId,
882
1141
  publicKey,
883
1142
  devMode,
884
1143
  mockMode,
885
1144
  checkoutUrl,
886
- jailbreakDetectionInfo
1145
+ dependencyInfo
887
1146
  );
888
1147
  setShowCheckout(false);
889
1148
  return false;
890
- } else if (deviceComplianceCheck === null) {
891
- // This warning was intentionally not gated behind a devMode check to ensure merchants are aware of the strong recommendation to install jail-monkey for enhanced security compliance.
892
- console.warn(
893
- "[Glomo-RN-SDK Standard] Device security library not installed - the SDK won't be able to automatically detect rooted/jailbroken devices. It is HIGHLY recommended to install jail-monkey as per the README, and prevent standard checkouts from compromised devices."
894
- );
895
1149
  }
896
1150
 
897
1151
  // Enforcing devMode requirement for development servers
@@ -912,7 +1166,7 @@ export function useStandardCheckout(
912
1166
  devMode,
913
1167
  mockMode,
914
1168
  checkoutUrl,
915
- jailbreakDetectionInfo
1169
+ dependencyInfo
916
1170
  );
917
1171
  setShowCheckout(false);
918
1172
  return false;
@@ -968,7 +1222,7 @@ export function useStandardCheckout(
968
1222
  devMode,
969
1223
  mockMode,
970
1224
  checkoutUrl,
971
- jailbreakDetectionInfo
1225
+ dependencyInfo
972
1226
  );
973
1227
  setShowCheckout(false);
974
1228
  return false;
@@ -988,7 +1242,7 @@ export function useStandardCheckout(
988
1242
  devMode,
989
1243
  mockMode,
990
1244
  checkoutUrl,
991
- jailbreakDetectionInfo
1245
+ dependencyInfo
992
1246
  );
993
1247
  setShowCheckout(false);
994
1248
  return false;
@@ -1013,7 +1267,7 @@ export function useStandardCheckout(
1013
1267
  devMode,
1014
1268
  mockMode,
1015
1269
  checkoutUrl,
1016
- jailbreakDetectionInfo
1270
+ dependencyInfo
1017
1271
  );
1018
1272
  setShowCheckout(false);
1019
1273
  return false;
@@ -1031,7 +1285,7 @@ export function useStandardCheckout(
1031
1285
  devMode,
1032
1286
  mockMode,
1033
1287
  checkoutUrl,
1034
- jailbreakDetectionInfo
1288
+ dependencyInfo
1035
1289
  );
1036
1290
  setShowCheckout(false);
1037
1291
  return false;
@@ -1048,7 +1302,7 @@ export function useStandardCheckout(
1048
1302
  setStatus("payment_in_progress");
1049
1303
 
1050
1304
  // Tracking successful start
1051
- trackers.trackStartSuccess(orderId, publicKey, devMode, mockMode, checkoutUrl, jailbreakDetectionInfo);
1305
+ trackers.trackStartSuccess(orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
1052
1306
 
1053
1307
  // Opening the checkout modal
1054
1308
  setShowCheckout(true);
@@ -1104,6 +1358,14 @@ export function useStandardCheckout(
1104
1358
  };
1105
1359
  }, []);
1106
1360
 
1361
+ /**
1362
+ * Scripts injected into each WebView, carrying what this build can advertise.
1363
+ *
1364
+ * Resolved once per mount rather than at module load, so a merchant who never opens a
1365
+ * checkout never pays for the dependency probe.
1366
+ */
1367
+ const injectionScripts = useMemo(() => buildInjectionScripts(resolveCapabilities()), []);
1368
+
1107
1369
  return {
1108
1370
  start,
1109
1371
  status,
@@ -1123,8 +1385,8 @@ export function useStandardCheckout(
1123
1385
  handleFlowBack,
1124
1386
  injectedMain:
1125
1387
  __DEV__ && internalOptions?._testInjectionScript
1126
- ? InjectionScripts.standard + internalOptions._testInjectionScript
1127
- : InjectionScripts.standard,
1128
- injectedFlow: InjectionScripts.flow,
1388
+ ? injectionScripts.standard + internalOptions._testInjectionScript
1389
+ : injectionScripts.standard,
1390
+ injectedFlow: injectionScripts.flow,
1129
1391
  };
1130
1392
  }