@glomopay/react-native-sdk 4.0.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 (127) 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 +9 -1
  29. package/lib/glomo-checkout.d.ts.map +1 -1
  30. package/lib/glomo-checkout.js +14 -5
  31. package/lib/glomo-lrs-checkout.d.ts +3 -0
  32. package/lib/glomo-lrs-checkout.d.ts.map +1 -1
  33. package/lib/glomo-lrs-checkout.js +34 -11
  34. package/lib/glomo-standard-checkout.d.ts +4 -1
  35. package/lib/glomo-standard-checkout.d.ts.map +1 -1
  36. package/lib/glomo-standard-checkout.js +21 -13
  37. package/lib/glomo-subscriptions-checkout.d.ts.map +1 -1
  38. package/lib/glomo-subscriptions-checkout.js +1 -1
  39. package/lib/index.d.ts +2 -2
  40. package/lib/index.d.ts.map +1 -1
  41. package/lib/index.js +4 -3
  42. package/lib/injections/index.d.ts +24 -0
  43. package/lib/injections/index.d.ts.map +1 -1
  44. package/lib/injections/index.js +41 -0
  45. package/lib/injections/webview-flow.injection.d.ts.map +1 -1
  46. package/lib/injections/webview-flow.injection.js +76 -0
  47. package/lib/injections/webview-main.injection.d.ts.map +1 -1
  48. package/lib/injections/webview-main.injection.js +31 -0
  49. package/lib/injections/webview-standard.injection.d.ts.map +1 -1
  50. package/lib/injections/webview-standard.injection.js +31 -0
  51. package/lib/services/file-saver.d.ts +109 -0
  52. package/lib/services/file-saver.d.ts.map +1 -0
  53. package/lib/services/file-saver.js +416 -0
  54. package/lib/types/checkout.d.ts +67 -6
  55. package/lib/types/checkout.d.ts.map +1 -1
  56. package/lib/types/checkout.js +24 -2
  57. package/lib/types/standard-checkout.d.ts +6 -6
  58. package/lib/types/standard-checkout.d.ts.map +1 -1
  59. package/lib/types/subscriptions-checkout.d.ts +5 -3
  60. package/lib/types/subscriptions-checkout.d.ts.map +1 -1
  61. package/lib/use-glomo-checkout.d.ts +2 -2
  62. package/lib/use-glomo-checkout.d.ts.map +1 -1
  63. package/lib/use-glomo-checkout.js +25 -4
  64. package/lib/use-lrs-checkout.d.ts +18 -6
  65. package/lib/use-lrs-checkout.d.ts.map +1 -1
  66. package/lib/use-lrs-checkout.js +330 -171
  67. package/lib/use-standard-checkout.d.ts +18 -13
  68. package/lib/use-standard-checkout.d.ts.map +1 -1
  69. package/lib/use-standard-checkout.js +282 -206
  70. package/lib/utils/analytics.d.ts +177 -51
  71. package/lib/utils/analytics.d.ts.map +1 -1
  72. package/lib/utils/analytics.js +299 -81
  73. package/lib/utils/device-compliance.d.ts +57 -12
  74. package/lib/utils/device-compliance.d.ts.map +1 -1
  75. package/lib/utils/device-compliance.js +101 -37
  76. package/lib/utils/event-projections.d.ts +56 -0
  77. package/lib/utils/event-projections.d.ts.map +1 -0
  78. package/lib/utils/event-projections.js +42 -0
  79. package/lib/utils/file-metadata.d.ts +52 -0
  80. package/lib/utils/file-metadata.d.ts.map +1 -0
  81. package/lib/utils/file-metadata.js +134 -0
  82. package/lib/utils/use-sdk-session.d.ts +12 -0
  83. package/lib/utils/use-sdk-session.d.ts.map +1 -0
  84. package/lib/utils/use-sdk-session.js +37 -0
  85. package/lib/utils/validation.d.ts +37 -3
  86. package/lib/utils/validation.d.ts.map +1 -1
  87. package/lib/utils/validation.js +64 -8
  88. package/lib/webview/capability-props.d.ts +27 -0
  89. package/lib/webview/capability-props.d.ts.map +1 -0
  90. package/lib/webview/capability-props.js +61 -0
  91. package/lib/webview/use-permission-request.d.ts +33 -0
  92. package/lib/webview/use-permission-request.d.ts.map +1 -0
  93. package/lib/webview/use-permission-request.js +89 -0
  94. package/package.json +13 -3
  95. package/src/bridge/file-input-handler.ts +66 -0
  96. package/src/bridge/file-save-handler.ts +141 -0
  97. package/src/bridge/message-router.ts +92 -0
  98. package/src/bridge/payment-payload.ts +31 -0
  99. package/src/capabilities/file-save.ts +124 -0
  100. package/src/capabilities/index.ts +34 -0
  101. package/src/capabilities/use-capability-reporting.ts +55 -0
  102. package/src/config/base.ts +13 -0
  103. package/src/glomo-checkout.tsx +28 -11
  104. package/src/glomo-lrs-checkout.tsx +49 -27
  105. package/src/glomo-standard-checkout.tsx +33 -34
  106. package/src/glomo-subscriptions-checkout.tsx +1 -0
  107. package/src/index.ts +4 -2
  108. package/src/injections/index.ts +43 -0
  109. package/src/injections/webview-flow.injection.ts +76 -0
  110. package/src/injections/webview-main.injection.ts +31 -0
  111. package/src/injections/webview-standard.injection.ts +31 -0
  112. package/src/services/file-saver.ts +497 -0
  113. package/src/types/checkout.ts +72 -6
  114. package/src/types/standard-checkout.ts +6 -6
  115. package/src/types/subscriptions-checkout.ts +9 -3
  116. package/src/use-glomo-checkout.tsx +32 -7
  117. package/src/use-lrs-checkout.tsx +684 -366
  118. package/src/use-standard-checkout.tsx +731 -465
  119. package/src/utils/analytics.ts +422 -117
  120. package/src/utils/device-compliance.ts +148 -38
  121. package/src/utils/event-projections.ts +74 -0
  122. package/src/utils/file-metadata.ts +140 -0
  123. package/src/utils/use-sdk-session.ts +41 -0
  124. package/src/utils/validation.ts +70 -10
  125. package/src/webview/capability-props.ts +86 -0
  126. package/src/webview/use-permission-request.ts +114 -0
  127. 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,7 +94,21 @@ 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;
111
+ _testInjectionScript?: string;
72
112
  }
73
113
 
74
114
  /** The main SDK hook for the GlomoPay Standard Checkout flow */
@@ -83,7 +123,7 @@ export function useStandardCheckout(
83
123
  onPaymentTerminate,
84
124
  onSdkError,
85
125
  onUserJourneyCompleted,
86
- onUserRefusedCameraPermissions,
126
+ onUserRefusedDevicePermissions,
87
127
  devMode = false,
88
128
  }: UseStandardCheckoutOptions,
89
129
  mockMode: boolean,
@@ -91,6 +131,7 @@ export function useStandardCheckout(
91
131
  ): UseStandardCheckoutReturn {
92
132
  // Resolving analytics trackers - defaults to standard, subscription flow overrides
93
133
  const trackers = internalOptions?.analyticsTrackers ?? standardCheckoutTrackers;
134
+ const flowType = internalOptions?.flowType ?? "standard";
94
135
  // Refs for the WebViews
95
136
  const mainWebViewRef = useRef<WebView>(null);
96
137
  const flowWebViewRef = useRef<WebView>(null);
@@ -112,7 +153,9 @@ export function useStandardCheckout(
112
153
  const mode = mockMode ? "mock" : "live";
113
154
  const params = baseParams.concat(
114
155
  `mode=${encodeURIComponent(mode)}`,
115
- `publicKey=${encodeURIComponent(publicKey)}`
156
+ `publicKey=${encodeURIComponent(publicKey)}`,
157
+ `product=${encodeURIComponent(ANALYTICS_PRODUCT)}`,
158
+ `surface=${encodeURIComponent(ANALYTICS_SURFACE)}`
116
159
  );
117
160
  const queryString = params.join("&");
118
161
 
@@ -236,22 +279,65 @@ export function useStandardCheckout(
236
279
  [devMode]
237
280
  );
238
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
+
239
328
  /**
240
329
  * Handler for the main WebView messages.
241
330
  * The main WebView hosts the checkout page. Messages arrive as JSON strings from the injected
242
331
  * JavaScript bridge and include console logs, window.open/close interceptions, and payment
243
332
  * lifecycle events (success, failure, terminate, bank transfer, pay via bank).
244
333
  */
245
- const handleMainWebViewMessage = useCallback(
246
- (event: WebViewMessageEvent) => {
247
- try {
248
- // Extracting and validating the raw message data from the WebView event
249
- const data = event.nativeEvent?.data;
250
- if (!data || typeof data !== "string") {
251
- if (devMode) {
252
- console.warn("[Glomo-RN-SDK Standard] Invalid message data from MainWebView");
253
- }
254
- // Tracking invalid message received
334
+ const handleMainWebViewMessage = useMemo(
335
+ () =>
336
+ createMessageRouter({
337
+ logPrefix: LOG_PREFIX,
338
+ source: "MainWebView",
339
+ devMode,
340
+ onInvalidMessage: (data) => {
255
341
  trackers.trackInvalidMessageReceived(
256
342
  "main",
257
343
  data,
@@ -262,216 +348,295 @@ export function useStandardCheckout(
262
348
  mockMode,
263
349
  checkoutUrl
264
350
  );
265
- return;
266
- }
351
+ },
352
+ handlers: {
353
+ "file.save": fileSaveHandler,
354
+ "file.input": fileInputHandler,
267
355
 
268
- const message = JSON.parse(data);
269
- if (devMode) {
270
- console.log("[Glomo-RN-SDK Standard] MainWebView Message received:", message.type, message);
271
- }
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
+ },
272
363
 
273
- // Forwarding console messages from the injected script to the React Native console
274
- if (message.type === "console") {
275
- if (devMode) {
276
- const level = message.level || "log";
277
- console.log(`[Glomo-RN-SDK Standard] [WebView ${level.toUpperCase()}]`, message.message);
278
- }
279
- return;
280
- }
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
+ },
281
385
 
282
- // Intercepting window.open calls - opening the target URL in the flow WebView overlay
283
- if (message.type === "window.open") {
284
- const url = message.url;
285
- if (url && typeof url === "string" && isValidUrl(url)) {
386
+ // Intercepting window.close calls - closing the flow WebView overlay
387
+ "window.close": () => {
286
388
  if (devMode) {
287
- console.log(`[Glomo-RN-SDK Standard] window.open called from MainWebView: ${url}`);
389
+ console.log(`${LOG_PREFIX} Child window close requested from MainWebView`);
288
390
  }
289
- setFlowWebViewUrl(url);
290
- setShowFlowWebView(true);
291
- // Tracking window.open interception
292
- trackers.trackWindowOpen("main", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
293
- } else {
294
- if (devMode) {
295
- console.log(
296
- "[Glomo-RN-SDK Standard] Invalid URL in window.open from MainWebView:",
297
- url,
298
- typeof url === "undefined" ? "undefined" : ""
299
- );
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;
300
473
  }
301
- }
302
- return;
303
- }
304
-
305
- // Intercepting window.close calls - closing the flow WebView overlay
306
- if (message.type === "window.close") {
307
- if (devMode) {
308
- console.log("[Glomo-RN-SDK Standard] Child window close requested from MainWebView");
309
- }
310
- setShowFlowWebView(false);
311
- setFlowWebViewUrl("");
312
- // Tracking window.close interception
313
- trackers.trackWindowClose("main", orderId, publicKey, devMode, mockMode, checkoutUrl);
314
- return;
315
- }
316
474
 
317
- // Handling merchant JS callbacks on flow completion (payment events, lifecycle events)
318
- if (message.type === "message" && message.message) {
319
- const messageData = message.message;
320
- const messageType = messageData?.type;
321
-
322
- // Handling payment success - closing checkout and notifying the merchant
323
- if (messageType === "payment.success" || messageType === "payment.complete") {
324
- const paymentMessage = messageData;
325
- 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
+ }
326
544
 
327
- 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;
328
548
  if (devMode) {
329
- console.log("[Glomo-RN-SDK Standard] Payment successful via merchant callback!");
549
+ console.log("[Glomo-RN-SDK Standard] Bank transfer submitted:", payload);
330
550
  }
331
551
  setShowCheckout(false);
332
552
  setShowFlowWebView(false);
333
553
  setFlowWebViewUrl("");
334
- setStatus("payment_successful");
335
- safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
336
- // Tracking payment success callback
337
- trackers.trackPaymentSuccess(
338
- payload.orderId,
339
- 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),
340
565
  publicKey,
341
566
  devMode,
342
567
  mockMode,
343
568
  checkoutUrl
344
569
  );
345
- } else {
346
- if (devMode) {
347
- console.error("[Glomo-RN-SDK Standard] Invalid payment success payload:", payload);
348
- }
570
+ return;
349
571
  }
350
- return;
351
- }
352
572
 
353
- // Handling payment failure - closing checkout and notifying the merchant
354
- if (messageType === "payment.failure" || messageType === "payment.error") {
355
- const paymentMessage = messageData;
356
- 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;
357
590
 
358
- if (isValidPaymentPayload(payload)) {
591
+ const payload = messageData?.payload;
359
592
  if (devMode) {
360
- 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);
361
594
  }
362
- setShowCheckout(false);
363
595
  setShowFlowWebView(false);
596
+ setShowCheckout(false);
364
597
  setFlowWebViewUrl("");
365
- setStatus("payment_failed");
366
- safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
367
- // Tracking payment failure callback
368
- trackers.trackPaymentFailure(
369
- payload.orderId,
370
- 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),
371
608
  publicKey,
372
609
  devMode,
373
610
  mockMode,
374
611
  checkoutUrl
375
612
  );
376
- } else {
377
- if (devMode) {
378
- console.error("[Glomo-RN-SDK Standard] Invalid payment failure payload:", payload);
379
- }
380
- }
381
- return;
382
- }
383
-
384
- // Handling bank transfer submission - user has submitted transfer details
385
- if (messageType === "payment.bank_transfer_submitted") {
386
- const payload = messageData?.payload;
387
- if (devMode) {
388
- console.log("[Glomo-RN-SDK Standard] Bank transfer submitted:", payload);
613
+ return;
389
614
  }
390
- setShowCheckout(false);
391
- setShowFlowWebView(false);
392
- setFlowWebViewUrl("");
393
- setStatus("bank_transfer_submitted");
394
- const journeyPayload: GlomoUserJourneyCompletedPayload = {
395
- journeyType: ASYNC_PAYMENT_EVENTS.BANK_TRANSFER_SUBMITTED,
396
- orderId: payload?.orderId,
397
- senderAccountNumber: payload?.senderAccountNumber,
398
- transactionReference: payload?.transactionReference,
399
- };
400
- safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
401
- trackers.trackBankTransferSubmitted(orderId, publicKey, devMode, mockMode, checkoutUrl);
402
- return;
403
- }
404
615
 
405
- /**
406
- * Handling the generic glomoCheckoutJourneyTerminate event.
407
- * This event is fired by the checkout page when the pay via bank (lean/open finance)
408
- * flow completes. The deduplication ref prevents firing the callback twice when both
409
- * main and flow WebViews emit this event.
410
- */
411
- if (messageType === "glomoCheckoutJourneyTerminate") {
412
- // Deduplication - both main and flow WebViews can emit this event
413
- if (payViaBankCompletedHandledRef.current) {
616
+ // Handling checkout.closed events
617
+ if (messageType === "checkout.closed") {
414
618
  if (devMode) {
415
- console.log(
416
- "[Glomo-RN-SDK Standard] Pay via bank completed already handled, ignoring duplicate"
417
- );
619
+ console.log("[Glomo-RN-SDK Standard] Checkout closed event received");
418
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
+ );
419
635
  return;
420
636
  }
421
- payViaBankCompletedHandledRef.current = true;
422
-
423
- const payload = messageData?.payload;
424
- if (devMode) {
425
- console.log("[Glomo-RN-SDK Standard] Pay via bank completed via MainWebView:", payload);
426
- }
427
- setShowFlowWebView(false);
428
- setShowCheckout(false);
429
- setFlowWebViewUrl("");
430
- setStatus("pay_via_bank_completed");
431
- const journeyPayload: GlomoUserJourneyCompletedPayload = {
432
- journeyType: ASYNC_PAYMENT_EVENTS.PAY_VIA_BANK_COMPLETED,
433
- orderId: payload?.orderId,
434
- status: payload?.status ?? "unknown",
435
- };
436
- safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
437
- trackers.trackPayViaBankCompleted(
438
- orderId,
439
- publicKey,
440
- payload?.status ?? "unknown",
441
- devMode,
442
- mockMode,
443
- checkoutUrl
444
- );
445
- return;
446
- }
447
-
448
- // Handling checkout.closed events
449
- if (messageType === "checkout.closed") {
450
- if (devMode) {
451
- console.log("[Glomo-RN-SDK Standard] Checkout closed event received");
452
- }
453
- setShowCheckout(false);
454
- setShowFlowWebView(false);
455
- setFlowWebViewUrl("");
456
- safeCallback(onPaymentTerminate, [], "onPaymentTerminate", devMode);
457
- // Tracking payment terminate callback
458
- trackers.trackPaymentTerminate(
459
- orderId,
460
- "checkout_closed",
461
- publicKey,
462
- devMode,
463
- mockMode,
464
- checkoutUrl
465
- );
466
- return;
467
- }
468
- }
469
- } catch (error) {
470
- if (devMode) {
471
- console.error("[Glomo-RN-SDK Standard] Error parsing MainWebView message:", error);
472
- }
473
- }
474
- },
637
+ },
638
+ },
639
+ }),
475
640
  [
476
641
  onPaymentSuccess,
477
642
  onPaymentFailure,
@@ -483,6 +648,10 @@ export function useStandardCheckout(
483
648
  publicKey,
484
649
  checkoutUrl,
485
650
  trackers,
651
+ fileSaveHandler,
652
+ flowType,
653
+ onSdkError,
654
+ fileInputHandler,
486
655
  ]
487
656
  );
488
657
 
@@ -491,16 +660,13 @@ export function useStandardCheckout(
491
660
  * The flow WebView hosts child pages opened via window.open (e.g. 3DS, bank redirects).
492
661
  * Message structure mirrors the main WebView handler but operates on the overlay WebView.
493
662
  */
494
- const handleFlowWebViewMessage = useCallback(
495
- (event: WebViewMessageEvent) => {
496
- try {
497
- // Extracting and validating the raw message data from the flow WebView event
498
- const data = event.nativeEvent?.data;
499
- if (!data || typeof data !== "string") {
500
- if (devMode) {
501
- console.warn("[Glomo-RN-SDK Standard] Invalid message data from FlowWebView");
502
- }
503
- // Tracking invalid message received
663
+ const handleFlowWebViewMessage = useMemo(
664
+ () =>
665
+ createMessageRouter({
666
+ logPrefix: LOG_PREFIX,
667
+ source: "FlowWebView",
668
+ devMode,
669
+ onInvalidMessage: (data) => {
504
670
  trackers.trackInvalidMessageReceived(
505
671
  "flow",
506
672
  data,
@@ -511,173 +677,257 @@ export function useStandardCheckout(
511
677
  mockMode,
512
678
  checkoutUrl
513
679
  );
514
- return;
515
- }
516
-
517
- const message = JSON.parse(data);
518
- if (devMode) {
519
- console.log("[Glomo-RN-SDK Standard] FlowWebView Message received:", message.type, message);
520
- }
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
+ },
521
700
 
522
- // Forwarding console messages from the flow WebView's injected script
523
- if (message.type === "console") {
524
- if (devMode) {
525
- console.log(`[Glomo-RN-SDK Standard] [Flow] ${message.message}`);
526
- }
527
- return;
528
- }
701
+ "file.save": fileSaveHandler,
702
+ "file.input": fileInputHandler,
529
703
 
530
- // Intercepting window.open calls from the flow WebView - navigating within the same overlay
531
- if (message.type === "window.open") {
532
- const url = message.url;
533
- if (url && typeof url === "string" && isValidUrl(url)) {
534
- if (devMode) {
535
- console.log(`[Glomo-RN-SDK Standard] window.open called from FlowWebView: ${url}`);
536
- }
537
- setFlowWebViewUrl(url);
538
- // Tracking window.open interception
539
- trackers.trackWindowOpen("flow", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
540
- } else {
704
+ // Forwarding console messages from the flow WebView's injected script
705
+ console: (message) => {
541
706
  if (devMode) {
542
- console.log(
543
- "[Glomo-RN-SDK Standard] Invalid URL in window.open from FlowWebView:",
544
- url,
545
- typeof url === "undefined" ? "undefined" : ""
546
- );
707
+ console.log(`${LOG_PREFIX} [Flow] ${message.message}`);
547
708
  }
548
- }
549
- return;
550
- }
551
-
552
- // Intercepting window.close calls from the flow WebView - closing the overlay
553
- if (message.type === "window.close") {
554
- if (devMode) {
555
- console.log("[Glomo-RN-SDK Standard] FlowWebView close requested");
556
- }
557
- setShowFlowWebView(false);
558
- setFlowWebViewUrl("");
559
- // Tracking window.close interception
560
- trackers.trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
561
- return;
562
- }
563
-
564
- // Handling merchant JS callbacks from the flow WebView (payment events, lifecycle events)
565
- if (message.type === "message" && message.message) {
566
- const messageData = message.message;
567
- const messageType = messageData?.type;
568
-
569
- // Handling payment success from the flow WebView
570
- if (messageType === "payment.success" || messageType === "payment.complete") {
571
- const paymentMessage = messageData;
572
- const payload = paymentMessage?.payload;
709
+ },
573
710
 
574
- 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)) {
575
715
  if (devMode) {
576
- console.log("[Glomo-RN-SDK Standard] Payment successful via FlowWebView callback!");
716
+ console.log(`${LOG_PREFIX} window.open called from FlowWebView: ${url}`);
577
717
  }
578
- setShowFlowWebView(false);
579
- setShowCheckout(false);
580
- setFlowWebViewUrl("");
581
- setStatus("payment_successful");
582
- safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
583
- // Tracking payment success callback
584
- trackers.trackPaymentSuccess(
585
- payload.orderId,
586
- payload.paymentId,
587
- publicKey,
588
- devMode,
589
- mockMode,
590
- checkoutUrl
591
- );
718
+ setFlowWebViewUrl(url);
719
+ // Tracking window.open interception
720
+ trackers.trackWindowOpen("flow", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
592
721
  } else {
593
722
  if (devMode) {
594
- console.error(
595
- "[Glomo-RN-SDK Standard] Invalid payment success payload from FlowWebView:",
596
- payload
723
+ console.log(
724
+ `${LOG_PREFIX} Invalid URL in window.open from FlowWebView:`,
725
+ url,
726
+ typeof url === "undefined" ? "undefined" : ""
597
727
  );
598
728
  }
599
729
  }
600
- return;
601
- }
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
+ }
602
890
 
603
- // Handling payment failure from the flow WebView
604
- if (messageType === "payment.failure" || messageType === "payment.error") {
605
- const paymentMessage = messageData;
606
- 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;
607
903
 
608
- if (isValidPaymentPayload(payload)) {
904
+ const payload = messageData?.payload;
609
905
  if (devMode) {
610
- 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);
611
907
  }
612
908
  setShowFlowWebView(false);
613
909
  setShowCheckout(false);
614
910
  setFlowWebViewUrl("");
615
- setStatus("payment_failed");
616
- safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
617
- // Tracking payment failure callback
618
- trackers.trackPaymentFailure(
619
- payload.orderId,
620
- 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),
621
921
  publicKey,
622
922
  devMode,
623
923
  mockMode,
624
924
  checkoutUrl
625
925
  );
626
- } else {
627
- if (devMode) {
628
- console.error(
629
- "[Glomo-RN-SDK Standard] Invalid payment failure payload from FlowWebView:",
630
- payload
631
- );
632
- }
633
- }
634
- return;
635
- }
636
-
637
- // Handling glomoCheckoutJourneyTerminate from the flow WebView (pay via bank completion)
638
- if (messageType === "glomoCheckoutJourneyTerminate") {
639
- // Deduplication - both main and flow WebViews can emit this event
640
- if (payViaBankCompletedHandledRef.current) {
641
- if (devMode) {
642
- console.log(
643
- "[Glomo-RN-SDK Standard] Pay via bank completed already handled, ignoring duplicate"
644
- );
645
- }
646
926
  return;
647
927
  }
648
- payViaBankCompletedHandledRef.current = true;
649
-
650
- const payload = messageData?.payload;
651
- if (devMode) {
652
- console.log("[Glomo-RN-SDK Standard] Pay via bank completed via FlowWebView:", payload);
653
- }
654
- setShowFlowWebView(false);
655
- setShowCheckout(false);
656
- setFlowWebViewUrl("");
657
- setStatus("pay_via_bank_completed");
658
- const journeyPayload: GlomoUserJourneyCompletedPayload = {
659
- journeyType: ASYNC_PAYMENT_EVENTS.PAY_VIA_BANK_COMPLETED,
660
- orderId: payload?.orderId,
661
- status: payload?.status ?? "unknown",
662
- };
663
- safeCallback(onUserJourneyCompleted, [journeyPayload], "onUserJourneyCompleted", devMode);
664
- trackers.trackPayViaBankCompleted(
665
- orderId,
666
- publicKey,
667
- payload?.status ?? "unknown",
668
- devMode,
669
- mockMode,
670
- checkoutUrl
671
- );
672
- return;
673
- }
674
- }
675
- } catch (error) {
676
- if (devMode) {
677
- console.error("[Glomo-RN-SDK Standard] Error parsing FlowWebView message:", error);
678
- }
679
- }
680
- },
928
+ },
929
+ },
930
+ }),
681
931
  [
682
932
  onPaymentSuccess,
683
933
  onPaymentFailure,
@@ -689,6 +939,10 @@ export function useStandardCheckout(
689
939
  flowWebViewUrl,
690
940
  checkoutUrl,
691
941
  trackers,
942
+ fileSaveHandler,
943
+ flowType,
944
+ onSdkError,
945
+ fileInputHandler,
692
946
  ]
693
947
  );
694
948
 
@@ -723,10 +977,26 @@ export function useStandardCheckout(
723
977
  console.log("[Glomo-RN-SDK Standard] Ignoring checkout status update for: " + status);
724
978
  }
725
979
  }
726
- safeCallback(onPaymentTerminate, [], "onPaymentTerminate", devMode);
727
- // Tracking payment terminate callback (user dismiss/back button)
728
- const terminateSource = Platform.OS === "ios" ? "user_dismiss" : "back_button";
729
- 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
+ );
730
1000
  } catch (error) {
731
1001
  if (devMode) {
732
1002
  console.error("[Glomo-RN-SDK Standard] Error in handleModalBackButton:", error);
@@ -741,93 +1011,46 @@ export function useStandardCheckout(
741
1011
  }
742
1012
  setShowFlowWebView(false);
743
1013
  setFlowWebViewUrl("");
744
- trackers.trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
745
- }, [devMode, orderId, publicKey, mockMode, checkoutUrl, trackers]);
1014
+ trackBackNavigationViaSdkBackButton(orderId, publicKey, devMode, mockMode, checkoutUrl, flowType);
1015
+ }, [devMode, orderId, publicKey, mockMode, checkoutUrl, flowType]);
746
1016
 
747
1017
  /**
748
- * Handler for WebView permission requests (e.g. camera access for bank authentication).
749
- * On Android, prompts the user for camera permission via PermissionsAndroid.
750
- * On iOS, grants camera access automatically (iOS handles its own permission prompt).
751
- * 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.
752
1027
  */
753
- const handlePermissionRequest = useCallback(
754
- async (request: WebViewPermissionRequest) => {
755
- try {
756
- const { resources } = request.nativeEvent;
757
- // Checking if the requested permission is camera-related
758
- const isCameraPermissionRequest = resources.some(
759
- (resource: string) => resource === "camera" || resource.toLowerCase().includes("camera")
760
- );
761
-
762
- if (!isCameraPermissionRequest) {
763
- if (devMode) {
764
- console.log("[Glomo-RN-SDK Standard] Non-camera permission requested, denying:", resources);
765
- }
766
- request.deny();
767
- return;
768
- }
769
-
770
- if (devMode) {
771
- console.log("[Glomo-RN-SDK Standard] Camera permission requested by webpage");
772
- }
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]);
773
1048
 
774
- if (Platform.OS === "android") {
775
- try {
776
- const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA, {
777
- title: "Camera Permission",
778
- message: "This app needs camera access for identity verification during checkout",
779
- buttonNeutral: "Ask Me Later",
780
- buttonNegative: "Cancel",
781
- buttonPositive: "OK",
782
- });
783
-
784
- if (granted === PermissionsAndroid.RESULTS.GRANTED) {
785
- if (devMode) {
786
- console.log("[Glomo-RN-SDK Standard] Camera permission granted");
787
- }
788
- request.grant(resources);
789
- } else {
790
- if (devMode) {
791
- console.log("[Glomo-RN-SDK Standard] Camera permission denied by user");
792
- }
793
- setShowCheckout(false);
794
- if (status === "payment_in_progress") {
795
- setStatus("payment_cancelled");
796
- }
797
- safeCallback(onUserRefusedCameraPermissions, [], "onUserRefusedCameraPermissions", devMode);
798
- request.deny();
799
- }
800
- } catch (err) {
801
- if (devMode) {
802
- console.error("[Glomo-RN-SDK Standard] Error requesting camera permission:", err);
803
- }
804
- setShowCheckout(false);
805
- if (status === "payment_in_progress") {
806
- setStatus("payment_cancelled");
807
- }
808
- safeCallback(onUserRefusedCameraPermissions, [], "onUserRefusedCameraPermissions", devMode);
809
- request.deny();
810
- }
811
- } else if (Platform.OS === "ios") {
812
- if (devMode) {
813
- console.log("[Glomo-RN-SDK Standard] Granting camera permission request on iOS");
814
- }
815
- request.grant(resources);
816
- } else {
817
- if (devMode) {
818
- console.warn("[Glomo-RN-SDK Standard] Unsupported platform for camera permissions");
819
- }
820
- request.deny();
821
- }
822
- } catch (error) {
823
- if (devMode) {
824
- console.error("[Glomo-RN-SDK Standard] Error in handlePermissionRequest:", error);
825
- }
826
- request.deny();
827
- }
828
- },
829
- [devMode, status, onUserRefusedCameraPermissions]
830
- );
1049
+ const handlePermissionRequest = usePermissionRequest({
1050
+ logPrefix: "[Glomo-RN-SDK Standard]",
1051
+ onDenied: handleCameraPermissionDenied,
1052
+ devMode,
1053
+ });
831
1054
 
832
1055
  /**
833
1056
  * Initiator method for the Standard Checkout flow.
@@ -846,11 +1069,16 @@ export function useStandardCheckout(
846
1069
  * Checking device security compliance (if peerDependencies are available)
847
1070
  * A null value indicates that recommended security dependencies were not installed and compliance checks will be skipped in this case
848
1071
  */
849
- const deviceComplianceCheck = checkDeviceCompliance(devMode);
850
- const jailbreakDetectionLibraryInstalled = isComplianceCheckAvailable();
851
- const jailbreakDetectionInfo = {
852
- jailbreakDetectionLibraryInstalled,
853
- 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,
854
1082
  };
855
1083
 
856
1084
  if (devMode) {
@@ -860,37 +1088,64 @@ export function useStandardCheckout(
860
1088
  }
861
1089
 
862
1090
  // Tracking start attempt with current status
863
- 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
+ }
864
1112
 
865
- if (deviceComplianceCheck === true) {
866
- // 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;
867
1121
  if (devMode) {
868
- 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
+ );
869
1127
  }
870
1128
  const errors: SdkError[] = [
871
1129
  {
872
1130
  type: "device_forbidden",
873
- 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.",
874
1134
  },
875
1135
  ];
876
1136
  safeCallback(onSdkError, [errors], "onSdkError", devMode);
877
1137
  // Tracking start() failure
878
1138
  trackers.trackStartFailure(
879
- "device_forbidden",
1139
+ unknown ? "device_compliance_unknown" : "device_forbidden",
880
1140
  orderId,
881
1141
  publicKey,
882
1142
  devMode,
883
1143
  mockMode,
884
1144
  checkoutUrl,
885
- jailbreakDetectionInfo
1145
+ dependencyInfo
886
1146
  );
887
1147
  setShowCheckout(false);
888
1148
  return false;
889
- } else if (deviceComplianceCheck === null) {
890
- // 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.
891
- console.warn(
892
- "[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."
893
- );
894
1149
  }
895
1150
 
896
1151
  // Enforcing devMode requirement for development servers
@@ -911,7 +1166,7 @@ export function useStandardCheckout(
911
1166
  devMode,
912
1167
  mockMode,
913
1168
  checkoutUrl,
914
- jailbreakDetectionInfo
1169
+ dependencyInfo
915
1170
  );
916
1171
  setShowCheckout(false);
917
1172
  return false;
@@ -967,7 +1222,7 @@ export function useStandardCheckout(
967
1222
  devMode,
968
1223
  mockMode,
969
1224
  checkoutUrl,
970
- jailbreakDetectionInfo
1225
+ dependencyInfo
971
1226
  );
972
1227
  setShowCheckout(false);
973
1228
  return false;
@@ -987,7 +1242,7 @@ export function useStandardCheckout(
987
1242
  devMode,
988
1243
  mockMode,
989
1244
  checkoutUrl,
990
- jailbreakDetectionInfo
1245
+ dependencyInfo
991
1246
  );
992
1247
  setShowCheckout(false);
993
1248
  return false;
@@ -1012,7 +1267,7 @@ export function useStandardCheckout(
1012
1267
  devMode,
1013
1268
  mockMode,
1014
1269
  checkoutUrl,
1015
- jailbreakDetectionInfo
1270
+ dependencyInfo
1016
1271
  );
1017
1272
  setShowCheckout(false);
1018
1273
  return false;
@@ -1030,7 +1285,7 @@ export function useStandardCheckout(
1030
1285
  devMode,
1031
1286
  mockMode,
1032
1287
  checkoutUrl,
1033
- jailbreakDetectionInfo
1288
+ dependencyInfo
1034
1289
  );
1035
1290
  setShowCheckout(false);
1036
1291
  return false;
@@ -1047,7 +1302,7 @@ export function useStandardCheckout(
1047
1302
  setStatus("payment_in_progress");
1048
1303
 
1049
1304
  // Tracking successful start
1050
- trackers.trackStartSuccess(orderId, publicKey, devMode, mockMode, checkoutUrl, jailbreakDetectionInfo);
1305
+ trackers.trackStartSuccess(orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
1051
1306
 
1052
1307
  // Opening the checkout modal
1053
1308
  setShowCheckout(true);
@@ -1103,6 +1358,14 @@ export function useStandardCheckout(
1103
1358
  };
1104
1359
  }, []);
1105
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
+
1106
1369
  return {
1107
1370
  start,
1108
1371
  status,
@@ -1120,7 +1383,10 @@ export function useStandardCheckout(
1120
1383
  handleNavigationStateChange,
1121
1384
  handleModalBackButton,
1122
1385
  handleFlowBack,
1123
- injectedMain: InjectionScripts.standard,
1124
- injectedFlow: InjectionScripts.flow,
1386
+ injectedMain:
1387
+ __DEV__ && internalOptions?._testInjectionScript
1388
+ ? injectionScripts.standard + internalOptions._testInjectionScript
1389
+ : injectionScripts.standard,
1390
+ injectedFlow: injectionScripts.flow,
1125
1391
  };
1126
1392
  }