@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
@@ -5,11 +5,27 @@ 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";
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 } from "./utils/event-projections";
21
+ import { resolveDeviceCompliance, blocksCheckout, isComplianceCheckAvailable } from "./utils/device-compliance";
22
+ import { usePermissionRequest, type WebViewPermissionRequest } from "./webview/use-permission-request";
23
+ import { createMessageRouter } from "./bridge/message-router";
24
+ import { createFileSaveHandler } from "./bridge/file-save-handler";
25
+ import { createFileInputHandler } from "./bridge/file-input-handler";
26
+ import { resolveCapabilities, resolveFileSaveAvailability } from "./capabilities/index";
27
+ import { useCapabilityReporting } from "./capabilities/use-capability-reporting";
28
+ import { useSdkSession } from "./utils/use-sdk-session";
13
29
  import {
14
30
  trackStartAttempt,
15
31
  trackStartSuccess,
@@ -24,8 +40,16 @@ import {
24
40
  trackEducationStepsFailedToShow,
25
41
  trackSdkError,
26
42
  trackInvalidMessageReceived,
43
+ trackIncompletePaymentPayload,
44
+ trackDroppedPaymentPayload,
45
+ trackDevicePermissionRefused,
46
+ trackBackNavigationViaSdkBackButton,
27
47
  type SdkError,
28
48
  } from "./utils/analytics";
49
+ import { TERMINATION_SOURCES, type GlomoTerminationPayload } from "./types/checkout";
50
+
51
+ /** Log prefix for everything this flow emits */
52
+ const LOG_PREFIX = "[Glomo-RN-SDK LRS]";
29
53
 
30
54
  /** LRS checkout statuses */
31
55
  export type LrsCheckoutStatus =
@@ -58,11 +82,11 @@ export type LrsCheckoutStatus =
58
82
  */
59
83
  export type EducationCarouselState = "pending" | "hasContent";
60
84
 
61
- /** The payload for a successful or failed payment */
85
+ /** The payload for a successful or failed payment. See GlomoCheckoutPayload on the optional fields. */
62
86
  export interface GlomoLrsCheckoutPayload {
63
87
  orderId: string;
64
- paymentId: string;
65
- signature: string;
88
+ paymentId?: string;
89
+ signature?: string;
66
90
  }
67
91
 
68
92
  /** The options for the useLrsCheckout hook */
@@ -73,8 +97,9 @@ export interface UseLrsCheckoutOptions {
73
97
  onPaymentSuccess: (payload: GlomoLrsCheckoutPayload) => void;
74
98
  onPaymentFailure: (payload: GlomoLrsCheckoutPayload) => void;
75
99
  onConnectionError?: (error: unknown) => void;
76
- onPaymentTerminate?: () => void;
100
+ onPaymentTerminate?: (payload: GlomoTerminationPayload) => void;
77
101
  onSdkError: (error: Array<SdkError>) => void;
102
+ onUserRefusedDevicePermissions?: () => void;
78
103
  devMode?: boolean;
79
104
  }
80
105
 
@@ -101,6 +126,7 @@ export interface UseLrsCheckoutReturn {
101
126
  handleNavigationStateChange: (navState: WebViewNavigation) => void;
102
127
  handleModalBackButton: () => void;
103
128
  handleFlowBack: () => void;
129
+ handlePermissionRequest: (request: WebViewPermissionRequest) => void;
104
130
  injectedMain?: string;
105
131
  injectedFlow?: string;
106
132
  injectedEducationCarousel?: string;
@@ -126,6 +152,7 @@ export function useLrsCheckout(
126
152
  onConnectionError,
127
153
  onPaymentTerminate,
128
154
  onSdkError,
155
+ onUserRefusedDevicePermissions,
129
156
  devMode = false,
130
157
  }: UseLrsCheckoutOptions,
131
158
  mockMode: boolean,
@@ -151,7 +178,9 @@ export function useLrsCheckout(
151
178
  const mode = mockMode ? "mock" : "live";
152
179
  const params = baseParams.concat(
153
180
  `mode=${encodeURIComponent(mode)}`,
154
- `publicKey=${encodeURIComponent(publicKey)}`
181
+ `publicKey=${encodeURIComponent(publicKey)}`,
182
+ `product=${encodeURIComponent(ANALYTICS_PRODUCT)}`,
183
+ `surface=${encodeURIComponent(ANALYTICS_SURFACE)}`
155
184
  );
156
185
  const queryString = params.join("&");
157
186
 
@@ -189,7 +218,9 @@ export function useLrsCheckout(
189
218
  const separator = baseEducationCarouselUrl.includes("?") ? "&" : "?";
190
219
  const finalUrl =
191
220
  `${baseEducationCarouselUrl}${separator}orderId=${encodeURIComponent(orderId)}` +
192
- `&publicKey=${encodeURIComponent(publicKey)}`;
221
+ `&publicKey=${encodeURIComponent(publicKey)}` +
222
+ `&product=${encodeURIComponent(ANALYTICS_PRODUCT)}` +
223
+ `&surface=${encodeURIComponent(ANALYTICS_SURFACE)}`;
193
224
 
194
225
  if (!isValidUrl(finalUrl)) {
195
226
  if (devMode) {
@@ -319,16 +350,60 @@ export function useLrsCheckout(
319
350
  [devMode]
320
351
  );
321
352
 
353
+ // Correlates every event from this mount, and keeps checkoutStatus current on all of them
354
+ useSdkSession(() => status);
355
+
356
+ // Missing optional dependencies reach the host here; start() warns on the console separately
357
+ useCapabilityReporting({ onSdkError, devMode });
358
+
359
+ /**
360
+ * Handler for `file.input` - the user tapping a file input on a bank's page.
361
+ *
362
+ * Registered beside `file.save` on the same WebViews, for the same reason: uploads are not a
363
+ * property of one flow, and until now this message had no handler anywhere.
364
+ */
365
+ const fileInputHandler = useMemo(
366
+ () =>
367
+ createFileInputHandler({
368
+ flowType: "lrs",
369
+ orderId,
370
+ publicKey,
371
+ checkoutUrl,
372
+ mockMode,
373
+ devMode,
374
+ }),
375
+ [orderId, publicKey, checkoutUrl, mockMode, devMode]
376
+ );
377
+
378
+ /**
379
+ * Handler for `file.save` - the page asking the SDK to put a document on the device.
380
+ *
381
+ * One handler, registered on the main and flow WebViews both. Not on the education carousel:
382
+ * that renders our own content, which has nothing to gate, and the capability is not advertised
383
+ * there either.
384
+ */
385
+ const fileSaveHandler = useMemo(
386
+ () =>
387
+ createFileSaveHandler({
388
+ flowType: "lrs",
389
+ orderId,
390
+ publicKey,
391
+ checkoutUrl,
392
+ mockMode,
393
+ devMode,
394
+ onSdkError,
395
+ }),
396
+ [orderId, publicKey, checkoutUrl, mockMode, devMode, onSdkError]
397
+ );
398
+
322
399
  // Handler for the main WebView messages
323
- const handleMainWebViewMessage = useCallback(
324
- (event: WebViewMessageEvent) => {
325
- try {
326
- const data = event.nativeEvent?.data;
327
- if (!data || typeof data !== "string") {
328
- if (devMode) {
329
- console.warn("[Glomo-RN-SDK LRS] Invalid message data from MainWebView");
330
- }
331
- // Tracking invalid message received
400
+ const handleMainWebViewMessage = useMemo(
401
+ () =>
402
+ createMessageRouter({
403
+ logPrefix: LOG_PREFIX,
404
+ source: "MainWebView",
405
+ devMode,
406
+ onInvalidMessage: (data) => {
332
407
  trackInvalidMessageReceived(
333
408
  "main",
334
409
  data,
@@ -339,158 +414,242 @@ export function useLrsCheckout(
339
414
  mockMode,
340
415
  checkoutUrl
341
416
  );
342
- return;
343
- }
344
-
345
- const message = JSON.parse(data);
346
- if (devMode) {
347
- console.log("[Glomo-RN-SDK LRS] MainWebView Message received:", message.type, message);
348
- }
349
-
350
- if (message.type === "console") {
351
- if (devMode) {
352
- const level = message.level || "log";
353
- console.log(`[Glomo-RN-SDK LRS] [WebView ${level.toUpperCase()}]`, message.message);
354
- }
355
- return;
356
- }
417
+ },
418
+ handlers: {
419
+ "file.save": fileSaveHandler,
420
+ "file.input": fileInputHandler,
357
421
 
358
- if (message.type === "window.open") {
359
- const url = message.url;
360
- if (url && typeof url === "string" && isValidUrl(url)) {
422
+ console: (message) => {
361
423
  if (devMode) {
362
- console.log(`[Glomo-RN-SDK LRS] window.open called from MainWebView: ${url}`);
363
- }
364
- setFlowWebViewUrl(url);
365
- setShowFlowWebView(true);
366
- setEducationCarouselState("pending");
367
- // Tracking window.open interception
368
- trackWindowOpen("main", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
369
- } else {
370
- if (devMode) {
371
- console.log(
372
- "[Glomo-RN-SDK LRS] Invalid URL in window.open from MainWebView:",
373
- url,
374
- typeof url === "undefined" ? "undefined" : ""
375
- );
424
+ const level = (typeof message.level === "string" && message.level) || "log";
425
+ console.log(`${LOG_PREFIX} [WebView ${level.toUpperCase()}]`, message.message);
376
426
  }
377
- }
378
- return;
379
- }
380
-
381
- if (message.type === "window.close") {
382
- if (devMode) {
383
- console.log("[Glomo-RN-SDK LRS] Child window close requested from MainWebView");
384
- }
385
- setShowFlowWebView(false);
386
- setFlowWebViewUrl("");
387
- setEducationCarouselState("pending");
388
- // Tracking window.close interception
389
- trackWindowClose("main", orderId, publicKey, devMode, mockMode, checkoutUrl);
390
- return;
391
- }
392
-
393
- // Handling merchant JS callbacks on flow completion
394
- if (message.type === "message" && message.message) {
395
- const messageData = message.message;
396
- const messageType = messageData?.type;
397
-
398
- if (messageType === "payment.success" || messageType === "payment.complete") {
399
- const paymentMessage = messageData;
400
- const payload = paymentMessage?.payload;
427
+ },
401
428
 
402
- if (isValidPaymentPayload(payload)) {
429
+ "window.open": (message) => {
430
+ const url = message.url;
431
+ if (url && typeof url === "string" && isValidUrl(url)) {
403
432
  if (devMode) {
404
- console.log("[Glomo-RN-SDK LRS] Payment successful via merchant callback!");
433
+ console.log(`${LOG_PREFIX} window.open called from MainWebView: ${url}`);
405
434
  }
406
- setShowCheckout(false);
407
- setShowFlowWebView(false);
408
- setFlowWebViewUrl("");
435
+ setFlowWebViewUrl(url);
436
+ setShowFlowWebView(true);
409
437
  setEducationCarouselState("pending");
410
- setStatus("payment_successful");
411
- safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
412
- // Tracking payment success callback
413
- trackPaymentSuccess(
414
- payload.orderId,
415
- payload.paymentId,
416
- publicKey,
417
- devMode,
418
- mockMode,
419
- checkoutUrl
420
- );
438
+ // Tracking window.open interception
439
+ trackWindowOpen("main", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
421
440
  } else {
422
441
  if (devMode) {
423
- console.error("[Glomo-RN-SDK LRS] Invalid payment success payload:", payload);
442
+ console.log(
443
+ `${LOG_PREFIX} Invalid URL in window.open from MainWebView:`,
444
+ url,
445
+ typeof url === "undefined" ? "undefined" : ""
446
+ );
424
447
  }
425
448
  }
426
- return;
427
- }
449
+ },
450
+
451
+ "window.close": () => {
452
+ if (devMode) {
453
+ console.log(`${LOG_PREFIX} Child window close requested from MainWebView`);
454
+ }
455
+ setShowFlowWebView(false);
456
+ setFlowWebViewUrl("");
457
+ setEducationCarouselState("pending");
458
+ // Tracking window.close interception
459
+ trackWindowClose("main", orderId, publicKey, devMode, mockMode, checkoutUrl);
460
+ },
461
+
462
+ // Handling merchant JS callbacks on flow completion
463
+ message: (message) => {
464
+ const messageData = message.message as { type?: string; payload?: unknown } | undefined;
465
+ if (!messageData) return;
466
+ const messageType = messageData.type;
467
+
468
+ if (messageType === "payment.success" || messageType === "payment.complete") {
469
+ const payload = messageData.payload;
470
+
471
+ if (isValidPaymentPayload(payload)) {
472
+ if (devMode) {
473
+ console.log(`${LOG_PREFIX} Payment successful via merchant callback!`);
474
+ }
475
+ setShowCheckout(false);
476
+ setShowFlowWebView(false);
477
+ setFlowWebViewUrl("");
478
+ setEducationCarouselState("pending");
479
+ setStatus("payment_successful");
480
+ safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
481
+ // Tracking payment success callback
482
+ trackPaymentSuccess(
483
+ toPaymentEventData(payload),
484
+ publicKey,
485
+ devMode,
486
+ mockMode,
487
+ checkoutUrl
488
+ );
489
+ // Fires alongside the callback, not instead of it - the outcome is real, the payload is thin
490
+ const missingFields = missingPaymentFields(payload);
491
+ if (missingFields.length > 0) {
492
+ trackIncompletePaymentPayload(
493
+ "payment.success",
494
+ missingFields,
495
+ "main",
496
+ payload.orderId,
497
+ publicKey,
498
+ devMode,
499
+ mockMode,
500
+ checkoutUrl,
501
+ "lrs"
502
+ );
503
+ }
504
+ } else {
505
+ /**
506
+ * Reported, not dropped. Before v5 this branch was a devMode-only console.error, so in a
507
+ * release build a terminal payment outcome the SDK could not route left no trace
508
+ * anywhere - no callback, no telemetry, nothing for support to find.
509
+ */
510
+ if (devMode) {
511
+ console.error(
512
+ `${LOG_PREFIX} Unroutable payment.success payload from main:`,
513
+ payload
514
+ );
515
+ }
516
+ trackDroppedPaymentPayload(
517
+ "payment.success",
518
+ payloadKeysOf(payload),
519
+ "main",
520
+ orderId,
521
+ publicKey,
522
+ devMode,
523
+ mockMode,
524
+ checkoutUrl,
525
+ "lrs"
526
+ );
527
+ safeCallback(
528
+ onSdkError,
529
+ [[droppedPayloadSdkError("payment.success")]],
530
+ "onSdkError",
531
+ devMode
532
+ );
533
+ }
534
+ return;
535
+ }
428
536
 
429
- if (messageType === "payment.failure" || messageType === "payment.error") {
430
- const paymentMessage = messageData;
431
- const payload = paymentMessage?.payload;
537
+ if (messageType === "payment.failure" || messageType === "payment.error") {
538
+ const payload = messageData.payload;
539
+
540
+ if (isValidPaymentPayload(payload)) {
541
+ if (devMode) {
542
+ console.error(`${LOG_PREFIX} Payment failed via merchant callback`);
543
+ }
544
+ setShowCheckout(false);
545
+ setShowFlowWebView(false);
546
+ setFlowWebViewUrl("");
547
+ setEducationCarouselState("pending");
548
+ setStatus("payment_failed");
549
+ safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
550
+ // Tracking payment failure callback
551
+ trackPaymentFailure(
552
+ toPaymentEventData(payload),
553
+ publicKey,
554
+ devMode,
555
+ mockMode,
556
+ checkoutUrl
557
+ );
558
+ // Fires alongside the callback, not instead of it - the outcome is real, the payload is thin
559
+ const missingFields = missingPaymentFields(payload);
560
+ if (missingFields.length > 0) {
561
+ trackIncompletePaymentPayload(
562
+ "payment.failure",
563
+ missingFields,
564
+ "main",
565
+ payload.orderId,
566
+ publicKey,
567
+ devMode,
568
+ mockMode,
569
+ checkoutUrl,
570
+ "lrs"
571
+ );
572
+ }
573
+ } else {
574
+ /**
575
+ * Reported, not dropped. Before v5 this branch was a devMode-only console.error, so in a
576
+ * release build a terminal payment outcome the SDK could not route left no trace
577
+ * anywhere - no callback, no telemetry, nothing for support to find.
578
+ */
579
+ if (devMode) {
580
+ console.error(
581
+ `${LOG_PREFIX} Unroutable payment.failure payload from main:`,
582
+ payload
583
+ );
584
+ }
585
+ trackDroppedPaymentPayload(
586
+ "payment.failure",
587
+ payloadKeysOf(payload),
588
+ "main",
589
+ orderId,
590
+ publicKey,
591
+ devMode,
592
+ mockMode,
593
+ checkoutUrl,
594
+ "lrs"
595
+ );
596
+ safeCallback(
597
+ onSdkError,
598
+ [[droppedPayloadSdkError("payment.failure")]],
599
+ "onSdkError",
600
+ devMode
601
+ );
602
+ }
603
+ return;
604
+ }
432
605
 
433
- if (isValidPaymentPayload(payload)) {
606
+ // Handling checkout.closed events
607
+ if (messageType === "checkout.closed") {
434
608
  if (devMode) {
435
- console.error("[Glomo-RN-SDK LRS] Payment failed via merchant callback");
609
+ console.log(`${LOG_PREFIX} Checkout closed event received`);
436
610
  }
437
611
  setShowCheckout(false);
438
612
  setShowFlowWebView(false);
439
613
  setFlowWebViewUrl("");
440
614
  setEducationCarouselState("pending");
441
- setStatus("payment_failed");
442
- safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
443
- // Tracking payment failure callback
444
- trackPaymentFailure(
445
- payload.orderId,
446
- payload.paymentId,
615
+ const termination = { source: TERMINATION_SOURCES.CHECKOUT_CLOSED };
616
+ safeCallback(onPaymentTerminate, [termination], "onPaymentTerminate", devMode);
617
+ // One object to the host and to telemetry, so the two cannot drift
618
+ trackPaymentTerminate(
619
+ orderId,
620
+ toTerminationEventData(termination),
447
621
  publicKey,
448
622
  devMode,
449
623
  mockMode,
450
624
  checkoutUrl
451
625
  );
452
- } else {
453
- if (devMode) {
454
- console.error("[Glomo-RN-SDK LRS] Invalid payment failure payload:", payload);
455
- }
456
626
  }
457
- return;
458
- }
459
-
460
- // Handling checkout.closed events
461
- if (messageType === "checkout.closed") {
462
- if (devMode) {
463
- console.log("[Glomo-RN-SDK LRS] Checkout closed event received");
464
- }
465
- setShowCheckout(false);
466
- setShowFlowWebView(false);
467
- setFlowWebViewUrl("");
468
- setEducationCarouselState("pending");
469
- safeCallback(onPaymentTerminate, [], "onPaymentTerminate", devMode);
470
- // Tracking payment terminate callback
471
- trackPaymentTerminate(orderId, "checkout_closed", publicKey, devMode, mockMode, checkoutUrl);
472
- return;
473
- }
474
- }
475
- } catch (error) {
476
- if (devMode) {
477
- console.error("[Glomo-RN-SDK LRS] Error parsing MainWebView message:", error);
478
- }
479
- }
480
- },
481
- [onPaymentSuccess, onPaymentFailure, onPaymentTerminate, devMode, mockMode, orderId, publicKey, checkoutUrl]
627
+ },
628
+ },
629
+ }),
630
+ [
631
+ onPaymentSuccess,
632
+ onPaymentFailure,
633
+ onPaymentTerminate,
634
+ devMode,
635
+ mockMode,
636
+ orderId,
637
+ publicKey,
638
+ checkoutUrl,
639
+ fileSaveHandler,
640
+ onSdkError,
641
+ fileInputHandler,
642
+ ]
482
643
  );
483
644
 
484
645
  // Handler for the flow WebView messages
485
- const handleFlowWebViewMessage = useCallback(
486
- (event: WebViewMessageEvent) => {
487
- try {
488
- const data = event.nativeEvent?.data;
489
- if (!data || typeof data !== "string") {
490
- if (devMode) {
491
- console.warn("[Glomo-RN-SDK LRS] Invalid message data from FlowWebView");
492
- }
493
- // Tracking invalid message received
646
+ const handleFlowWebViewMessage = useMemo(
647
+ () =>
648
+ createMessageRouter({
649
+ logPrefix: LOG_PREFIX,
650
+ source: "FlowWebView",
651
+ devMode,
652
+ onInvalidMessage: (data) => {
494
653
  trackInvalidMessageReceived(
495
654
  "flow",
496
655
  data,
@@ -501,179 +660,261 @@ export function useLrsCheckout(
501
660
  mockMode,
502
661
  checkoutUrl
503
662
  );
504
- return;
505
- }
506
-
507
- const message = JSON.parse(data);
508
- if (devMode) {
509
- console.log("[Glomo-RN-SDK LRS] FlowWebView Message received:", message.type, message);
510
- }
663
+ },
664
+ handlers: {
665
+ /**
666
+ * Telemetry only, and deliberately not wired to onUserRefusedDevicePermissions.
667
+ * The flow injection sees this by watching getUserMedia, so coverage depends on
668
+ * whether a given bank uses it - partial coverage on a merchant callback is
669
+ * worse than none, because silence would stop being unambiguous.
670
+ */
671
+ "permission.denied": (message) => {
672
+ trackDevicePermissionRefused(
673
+ typeof message.resource === "string" ? message.resource : "camera",
674
+ "get_user_media",
675
+ orderId,
676
+ publicKey,
677
+ devMode,
678
+ mockMode,
679
+ checkoutUrl,
680
+ "lrs"
681
+ );
682
+ },
511
683
 
512
- if (message.type === "console") {
513
- if (devMode) {
514
- console.log(`[Glomo-RN-SDK LRS] [Flow] ${message.message}`);
515
- }
516
- return;
517
- }
684
+ "file.save": fileSaveHandler,
685
+ "file.input": fileInputHandler,
518
686
 
519
- if (message.type === "window.open") {
520
- const url = message.url;
521
- if (url && typeof url === "string" && isValidUrl(url)) {
687
+ console: (message) => {
522
688
  if (devMode) {
523
- console.log(`[Glomo-RN-SDK LRS] window.open called from FlowWebView: ${url}`);
524
- }
525
- setFlowWebViewUrl(url);
526
- setEducationCarouselState("pending");
527
- // Tracking window.open interception
528
- trackWindowOpen("flow", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
529
- } else {
530
- if (devMode) {
531
- console.log(
532
- "[Glomo-RN-SDK LRS] Invalid URL in window.open from FlowWebView:",
533
- url,
534
- typeof url === "undefined" ? "undefined" : ""
535
- );
689
+ console.log(`${LOG_PREFIX} [Flow] ${message.message}`);
536
690
  }
537
- }
538
- return;
539
- }
540
-
541
- if (message.type === "window.close") {
542
- if (devMode) {
543
- console.log("[Glomo-RN-SDK LRS] FlowWebView close requested");
544
- }
545
- setShowFlowWebView(false);
546
- setFlowWebViewUrl("");
547
- setEducationCarouselState("pending");
548
- // Tracking window.close interception
549
- trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
550
- return;
551
- }
552
-
553
- if (message.type === "message" && message.message) {
554
- const messageData = message.message;
555
- const messageType = messageData?.type;
556
-
557
- if (messageType === "payment.success" || messageType === "payment.complete") {
558
- const paymentMessage = messageData;
559
- const payload = paymentMessage?.payload;
691
+ },
560
692
 
561
- if (isValidPaymentPayload(payload)) {
693
+ "window.open": (message) => {
694
+ const url = message.url;
695
+ if (url && typeof url === "string" && isValidUrl(url)) {
562
696
  if (devMode) {
563
- console.log("[Glomo-RN-SDK LRS] Payment successful via FlowWebView callback!");
697
+ console.log(`${LOG_PREFIX} window.open called from FlowWebView: ${url}`);
564
698
  }
565
- setShowFlowWebView(false);
566
- setShowCheckout(false);
567
- setFlowWebViewUrl("");
699
+ setFlowWebViewUrl(url);
568
700
  setEducationCarouselState("pending");
569
- setStatus("payment_successful");
570
- safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
571
- // Tracking payment success callback
572
- trackPaymentSuccess(
573
- payload.orderId,
574
- payload.paymentId,
575
- publicKey,
576
- devMode,
577
- mockMode,
578
- checkoutUrl
579
- );
701
+ // Tracking window.open interception
702
+ trackWindowOpen("flow", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
580
703
  } else {
581
704
  if (devMode) {
582
- console.error(
583
- "[Glomo-RN-SDK LRS] Invalid payment success payload from FlowWebView:",
584
- payload
705
+ console.log(
706
+ `${LOG_PREFIX} Invalid URL in window.open from FlowWebView:`,
707
+ url,
708
+ typeof url === "undefined" ? "undefined" : ""
585
709
  );
586
710
  }
587
711
  }
588
- return;
589
- }
712
+ },
590
713
 
591
- if (messageType === "payment.failure" || messageType === "payment.error") {
592
- const paymentMessage = messageData;
593
- const payload = paymentMessage?.payload;
594
-
595
- if (isValidPaymentPayload(payload)) {
596
- if (devMode) {
597
- console.error("[Glomo-RN-SDK LRS] Payment failed via FlowWebView callback");
714
+ "window.close": () => {
715
+ if (devMode) {
716
+ console.log(`${LOG_PREFIX} FlowWebView close requested`);
717
+ }
718
+ setShowFlowWebView(false);
719
+ setFlowWebViewUrl("");
720
+ setEducationCarouselState("pending");
721
+ // Tracking window.close interception
722
+ trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
723
+ },
724
+
725
+ // Handling merchant JS callbacks on flow completion
726
+ message: (message) => {
727
+ const messageData = message.message as { type?: string; payload?: unknown } | undefined;
728
+ if (!messageData) return;
729
+ const messageType = messageData.type;
730
+
731
+ if (messageType === "payment.success" || messageType === "payment.complete") {
732
+ const payload = messageData.payload;
733
+
734
+ if (isValidPaymentPayload(payload)) {
735
+ if (devMode) {
736
+ console.log(`${LOG_PREFIX} Payment successful via FlowWebView callback!`);
737
+ }
738
+ setShowFlowWebView(false);
739
+ setShowCheckout(false);
740
+ setFlowWebViewUrl("");
741
+ setEducationCarouselState("pending");
742
+ setStatus("payment_successful");
743
+ safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
744
+ // Tracking payment success callback
745
+ trackPaymentSuccess(
746
+ toPaymentEventData(payload),
747
+ publicKey,
748
+ devMode,
749
+ mockMode,
750
+ checkoutUrl
751
+ );
752
+ // Fires alongside the callback, not instead of it - the outcome is real, the payload is thin
753
+ const missingFields = missingPaymentFields(payload);
754
+ if (missingFields.length > 0) {
755
+ trackIncompletePaymentPayload(
756
+ "payment.success",
757
+ missingFields,
758
+ "flow",
759
+ payload.orderId,
760
+ publicKey,
761
+ devMode,
762
+ mockMode,
763
+ checkoutUrl,
764
+ "lrs"
765
+ );
766
+ }
767
+ } else {
768
+ /**
769
+ * Reported, not dropped. Before v5 this branch was a devMode-only console.error, so in a
770
+ * release build a terminal payment outcome the SDK could not route left no trace
771
+ * anywhere - no callback, no telemetry, nothing for support to find.
772
+ */
773
+ if (devMode) {
774
+ console.error(
775
+ `${LOG_PREFIX} Unroutable payment.success payload from flow:`,
776
+ payload
777
+ );
778
+ }
779
+ trackDroppedPaymentPayload(
780
+ "payment.success",
781
+ payloadKeysOf(payload),
782
+ "flow",
783
+ orderId,
784
+ publicKey,
785
+ devMode,
786
+ mockMode,
787
+ checkoutUrl,
788
+ "lrs"
789
+ );
790
+ safeCallback(
791
+ onSdkError,
792
+ [[droppedPayloadSdkError("payment.success")]],
793
+ "onSdkError",
794
+ devMode
795
+ );
598
796
  }
599
- setShowFlowWebView(false);
600
- setShowCheckout(false);
601
- setFlowWebViewUrl("");
602
- setEducationCarouselState("pending");
603
- setStatus("payment_failed");
604
- safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
605
- // Tracking payment failure callback
606
- trackPaymentFailure(
607
- payload.orderId,
608
- payload.paymentId,
609
- publicKey,
610
- devMode,
611
- mockMode,
612
- checkoutUrl
613
- );
614
- } else {
615
- if (devMode) {
616
- console.error(
617
- "[Glomo-RN-SDK LRS] Invalid payment failure payload from FlowWebView:",
618
- payload
797
+ return;
798
+ }
799
+
800
+ if (messageType === "payment.failure" || messageType === "payment.error") {
801
+ const payload = messageData.payload;
802
+
803
+ if (isValidPaymentPayload(payload)) {
804
+ if (devMode) {
805
+ console.error(`${LOG_PREFIX} Payment failed via FlowWebView callback`);
806
+ }
807
+ setShowFlowWebView(false);
808
+ setShowCheckout(false);
809
+ setFlowWebViewUrl("");
810
+ setEducationCarouselState("pending");
811
+ setStatus("payment_failed");
812
+ safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
813
+ // Tracking payment failure callback
814
+ trackPaymentFailure(
815
+ toPaymentEventData(payload),
816
+ publicKey,
817
+ devMode,
818
+ mockMode,
819
+ checkoutUrl
820
+ );
821
+ // Fires alongside the callback, not instead of it - the outcome is real, the payload is thin
822
+ const missingFields = missingPaymentFields(payload);
823
+ if (missingFields.length > 0) {
824
+ trackIncompletePaymentPayload(
825
+ "payment.failure",
826
+ missingFields,
827
+ "flow",
828
+ payload.orderId,
829
+ publicKey,
830
+ devMode,
831
+ mockMode,
832
+ checkoutUrl,
833
+ "lrs"
834
+ );
835
+ }
836
+ } else {
837
+ /**
838
+ * Reported, not dropped. Before v5 this branch was a devMode-only console.error, so in a
839
+ * release build a terminal payment outcome the SDK could not route left no trace
840
+ * anywhere - no callback, no telemetry, nothing for support to find.
841
+ */
842
+ if (devMode) {
843
+ console.error(
844
+ `${LOG_PREFIX} Unroutable payment.failure payload from flow:`,
845
+ payload
846
+ );
847
+ }
848
+ trackDroppedPaymentPayload(
849
+ "payment.failure",
850
+ payloadKeysOf(payload),
851
+ "flow",
852
+ orderId,
853
+ publicKey,
854
+ devMode,
855
+ mockMode,
856
+ checkoutUrl,
857
+ "lrs"
858
+ );
859
+ safeCallback(
860
+ onSdkError,
861
+ [[droppedPayloadSdkError("payment.failure")]],
862
+ "onSdkError",
863
+ devMode
619
864
  );
620
865
  }
621
866
  }
622
- return;
623
- }
624
- }
625
- } catch (error) {
626
- if (devMode) {
627
- console.error("[Glomo-RN-SDK LRS] Error parsing FlowWebView message:", error);
628
- }
629
- }
630
- },
631
- [onPaymentSuccess, onPaymentFailure, devMode, mockMode, orderId, publicKey, flowWebViewUrl, checkoutUrl]
867
+ },
868
+ },
869
+ }),
870
+ [
871
+ onPaymentSuccess,
872
+ onPaymentFailure,
873
+ devMode,
874
+ mockMode,
875
+ orderId,
876
+ publicKey,
877
+ flowWebViewUrl,
878
+ checkoutUrl,
879
+ fileSaveHandler,
880
+ onSdkError,
881
+ fileInputHandler,
882
+ ]
632
883
  );
633
884
 
634
885
  // Handler for the education carousel WebView messages
635
- const handleEducationCarouselWebViewMessage = useCallback(
636
- (event: WebViewMessageEvent) => {
637
- try {
638
- const data = event.nativeEvent?.data;
639
- if (!data || typeof data !== "string") {
640
- if (devMode) {
641
- console.warn("[Glomo-RN-SDK LRS] Invalid message data from EducationCarouselWebView");
642
- }
643
- return;
644
- }
645
-
646
- const message = JSON.parse(data);
647
- if (devMode) {
648
- console.log("[Glomo-RN-SDK LRS] EducationCarouselWebView Message received:", message.type, message);
649
- }
650
-
651
- if (message.type === "console") {
652
- if (devMode) {
653
- console.log(`[Glomo-RN-SDK LRS] [EducationCarousel] ${message.message}`);
654
- }
655
- return;
656
- }
886
+ const handleEducationCarouselWebViewMessage = useMemo(
887
+ () =>
888
+ createMessageRouter({
889
+ logPrefix: LOG_PREFIX,
890
+ source: "EducationCarouselWebView",
891
+ devMode,
892
+ handlers: {
893
+ console: (message) => {
894
+ if (devMode) {
895
+ console.log(`${LOG_PREFIX} [EducationCarousel] ${message.message}`);
896
+ }
897
+ },
657
898
 
658
- if (message.type === "lrs.has_education_steps") {
659
- const nextState: EducationCarouselState = "hasContent";
899
+ "lrs.has_education_steps": (message) => {
900
+ if (devMode) {
901
+ console.log(
902
+ `${LOG_PREFIX} Education carousel signal received: value=${String(message.value)} source=${message.source || "unknown"} payload=${JSON.stringify(message.payload || {})}`
903
+ );
904
+ }
660
905
 
661
- if (devMode) {
662
- console.log(
663
- `[Glomo-RN-SDK LRS] Education carousel signal received: value=${String(message.value)} source=${message.source || "unknown"} payload=${JSON.stringify(message.payload || {})}`
906
+ trackEducationStepsShown(
907
+ orderId,
908
+ publicKey,
909
+ devMode,
910
+ mockMode,
911
+ checkoutUrl,
912
+ typeof message.source === "string" ? message.source : undefined
664
913
  );
665
- }
666
-
667
- trackEducationStepsShown(orderId, publicKey, devMode, mockMode, checkoutUrl, message.source);
668
- setEducationCarouselState(nextState);
669
- return;
670
- }
671
- } catch (error) {
672
- if (devMode) {
673
- console.error("[Glomo-RN-SDK LRS] Error parsing EducationCarouselWebView message:", error);
674
- }
675
- }
676
- },
914
+ setEducationCarouselState("hasContent");
915
+ },
916
+ },
917
+ }),
677
918
  [checkoutUrl, devMode, mockMode, orderId, publicKey]
678
919
  );
679
920
 
@@ -710,10 +951,26 @@ export function useLrsCheckout(
710
951
  console.log("[Glomo-RN-SDK LRS] Ignoring checkout status update for: " + status);
711
952
  }
712
953
  }
713
- safeCallback(onPaymentTerminate, [], "onPaymentTerminate", devMode);
714
- // Tracking payment terminate callback (user dismiss/back button)
715
- const terminateSource = Platform.OS === "ios" ? "user_dismiss" : "back_button";
716
- trackPaymentTerminate(orderId, terminateSource, publicKey, devMode, mockMode, checkoutUrl);
954
+ /**
955
+ * Inferred from the platform rather than read off an event, because `onRequestClose` is
956
+ * the only wiring here and the two platforms raise it for different gestures: Android
957
+ * for hardware back and the back gesture, iOS for swipe-down on the pageSheet. Nothing
958
+ * else routes through it, so the inference is sound - but it is an inference.
959
+ */
960
+ const termination = {
961
+ source:
962
+ Platform.OS === "ios" ? TERMINATION_SOURCES.MODAL_DISMISS : TERMINATION_SOURCES.DEVICE_BACK_BUTTON,
963
+ };
964
+ safeCallback(onPaymentTerminate, [termination], "onPaymentTerminate", devMode);
965
+ // One object to the host and to telemetry, so the two cannot drift
966
+ trackPaymentTerminate(
967
+ orderId,
968
+ toTerminationEventData(termination),
969
+ publicKey,
970
+ devMode,
971
+ mockMode,
972
+ checkoutUrl
973
+ );
717
974
  } catch (error) {
718
975
  if (devMode) {
719
976
  console.error("[Glomo-RN-SDK LRS] Error in handleModalBackButton:", error);
@@ -721,6 +978,39 @@ export function useLrsCheckout(
721
978
  }
722
979
  }, [showFlowWebView, devMode, onPaymentTerminate, status, orderId, publicKey, mockMode, checkoutUrl]);
723
980
 
981
+ /**
982
+ * Closing the checkout when the user refuses camera access, since the bank page cannot
983
+ * proceed without it. Kept here rather than inside usePermissionRequest because moving the
984
+ * status and notifying the merchant are flow concerns.
985
+ */
986
+ const handleDevicePermissionDenied = useCallback(() => {
987
+ setShowCheckout(false);
988
+ setShowFlowWebView(false);
989
+ setFlowWebViewUrl("");
990
+ setEducationCarouselState("pending");
991
+ if (status === "payment_in_progress") {
992
+ setStatus("payment_cancelled");
993
+ }
994
+ safeCallback(onUserRefusedDevicePermissions, [], "onUserRefusedDevicePermissions", devMode);
995
+ // The permission hook only reaches here for camera; anything else is denied without notifying
996
+ trackDevicePermissionRefused(
997
+ "camera",
998
+ "permission_request",
999
+ orderId,
1000
+ publicKey,
1001
+ devMode,
1002
+ mockMode,
1003
+ checkoutUrl,
1004
+ "lrs"
1005
+ );
1006
+ }, [status, onUserRefusedDevicePermissions, devMode, orderId, publicKey, mockMode, checkoutUrl]);
1007
+
1008
+ const handlePermissionRequest = usePermissionRequest({
1009
+ logPrefix: "[Glomo-RN-SDK LRS]",
1010
+ onDenied: handleDevicePermissionDenied,
1011
+ devMode,
1012
+ });
1013
+
724
1014
  /** Closes the flow WebView and returns to the main checkout */
725
1015
  const handleFlowBack = useCallback(() => {
726
1016
  if (devMode) {
@@ -729,7 +1019,7 @@ export function useLrsCheckout(
729
1019
  setShowFlowWebView(false);
730
1020
  setFlowWebViewUrl("");
731
1021
  setEducationCarouselState("pending");
732
- trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
1022
+ trackBackNavigationViaSdkBackButton(orderId, publicKey, devMode, mockMode, checkoutUrl, "lrs");
733
1023
  }, [devMode, orderId, publicKey, mockMode, checkoutUrl]);
734
1024
 
735
1025
  /**
@@ -749,11 +1039,16 @@ export function useLrsCheckout(
749
1039
  * Checking device security compliance (if peerDependencies are available)
750
1040
  * A null value indicates that recommended security dependencies were not installed and compliance checks will be skipped in this case
751
1041
  */
752
- const deviceComplianceCheck = checkDeviceCompliance(devMode);
753
- const jailbreakDetectionLibraryInstalled = isComplianceCheckAvailable();
754
- const jailbreakDetectionInfo = {
755
- jailbreakDetectionLibraryInstalled,
756
- jailbreakDetected: deviceComplianceCheck,
1042
+ const compliance = resolveDeviceCompliance(devMode);
1043
+ const complianceLibraryInstalled = isComplianceCheckAvailable();
1044
+ const fileSave = resolveFileSaveAvailability();
1045
+ const dependencyInfo = {
1046
+ jailbreakDetectionLibraryInstalled: complianceLibraryInstalled,
1047
+ jailbreakDetected: compliance.compromised,
1048
+ complianceSource: compliance.source,
1049
+ complianceSignals: compliance.trippedSignals,
1050
+ fileSaveAvailable: fileSave.available,
1051
+ missingFileSavePackages: fileSave.missing,
757
1052
  };
758
1053
 
759
1054
  if (devMode) {
@@ -761,37 +1056,64 @@ export function useLrsCheckout(
761
1056
  }
762
1057
 
763
1058
  // Tracking start attempt with current status
764
- trackStartAttempt(status, orderId, publicKey, devMode, mockMode, checkoutUrl, jailbreakDetectionInfo);
1059
+ trackStartAttempt(status, orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
765
1060
 
766
- if (deviceComplianceCheck === true) {
767
- // Device detected as compromised
1061
+ /**
1062
+ * Ungated on purpose, both of them. An absent optional dependency degrades silently by
1063
+ * design, so a console warning is the only thing that reaches a merchant who has not
1064
+ * noticed - and the compliance one now precedes a hard block on live keys.
1065
+ */
1066
+ if (!complianceLibraryInstalled) {
1067
+ console.warn(
1068
+ "[Glomo-RN-SDK LRS] jail-monkey is not installed or its native module is not linked, so " +
1069
+ "rooted and jailbroken devices cannot be detected. As of v5 this BLOCKS live checkouts. " +
1070
+ "Install jail-monkey and rebuild the app - see the README."
1071
+ );
1072
+ }
1073
+ if (!fileSave.available) {
1074
+ console.warn(
1075
+ `[Glomo-RN-SDK LRS] Document downloads are disabled: ${fileSave.missing.join(" and ")} ` +
1076
+ "could not be loaded. Install the package(s) and rebuild the app, or ignore this if your " +
1077
+ "checkout does not offer downloads - the page falls back to its own behaviour."
1078
+ );
1079
+ }
1080
+
1081
+ if (blocksCheckout(compliance, publicKey)) {
1082
+ /**
1083
+ * Two conditions, one refusal. `compromised === true` is a rooted or jailbroken device;
1084
+ * `null` is a device we could not check, which on RN overwhelmingly means jail-monkey is
1085
+ * absent rather than that a platform probe failed. Both fail closed on a live key, and
1086
+ * neither fires on a test or mock key.
1087
+ */
1088
+ const unknown = compliance.compromised === null;
768
1089
  if (devMode) {
769
- console.error("[Glomo-RN-SDK LRS] Cannot start checkout. Device is rooted or jailbroken.");
1090
+ console.error(
1091
+ `[Glomo-RN-SDK LRS] Cannot start checkout. ${
1092
+ unknown ? "Device security could not be verified." : "Device is rooted or jailbroken."
1093
+ }`
1094
+ );
770
1095
  }
771
1096
  const errors: SdkError[] = [
772
1097
  {
773
1098
  type: "device_forbidden",
774
- message: "LRS checkout is not allowed on rooted or jailbroken devices.",
1099
+ message: unknown
1100
+ ? "LRS checkout could not verify this device's security. Install jail-monkey and rebuild the app."
1101
+ : "LRS checkout is not allowed on rooted or jailbroken devices.",
775
1102
  },
776
1103
  ];
777
1104
  safeCallback(onSdkError, [errors], "onSdkError", devMode);
778
1105
  // Tracking start() failure
779
1106
  trackStartFailure(
780
- "device_forbidden",
1107
+ unknown ? "device_compliance_unknown" : "device_forbidden",
781
1108
  orderId,
782
1109
  publicKey,
783
1110
  devMode,
784
1111
  mockMode,
785
1112
  checkoutUrl,
786
- jailbreakDetectionInfo
1113
+ dependencyInfo
787
1114
  );
788
1115
  setShowCheckout(false);
789
1116
  return false;
790
- } else if (deviceComplianceCheck === null) {
791
- // 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.
792
- console.warn(
793
- "[Glomo-RN-SDK LRS] 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 LRS checkouts from compromised devices."
794
- );
795
1117
  }
796
1118
 
797
1119
  // Enforcing devMode requirement for development servers
@@ -805,15 +1127,7 @@ export function useLrsCheckout(
805
1127
  ];
806
1128
  safeCallback(onSdkError, [errors], "onSdkError", devMode);
807
1129
  trackSdkError(errors, orderId, publicKey, devMode, mockMode, checkoutUrl);
808
- trackStartFailure(
809
- "validation_error",
810
- orderId,
811
- publicKey,
812
- devMode,
813
- mockMode,
814
- checkoutUrl,
815
- jailbreakDetectionInfo
816
- );
1130
+ trackStartFailure("validation_error", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
817
1131
  setShowCheckout(false);
818
1132
  return false;
819
1133
  }
@@ -861,15 +1175,7 @@ export function useLrsCheckout(
861
1175
  safeCallback(onSdkError, [errors], "onSdkError", devMode);
862
1176
  // Tracking start() failure
863
1177
  trackSdkError(errors, orderId, publicKey, devMode, mockMode, checkoutUrl);
864
- trackStartFailure(
865
- "validation_error",
866
- orderId,
867
- publicKey,
868
- devMode,
869
- mockMode,
870
- checkoutUrl,
871
- jailbreakDetectionInfo
872
- );
1178
+ trackStartFailure("validation_error", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
873
1179
  setShowCheckout(false);
874
1180
  return false;
875
1181
  }
@@ -881,15 +1187,7 @@ export function useLrsCheckout(
881
1187
  );
882
1188
  }
883
1189
  // Tracking start() failure
884
- trackStartFailure(
885
- "payment_successful",
886
- orderId,
887
- publicKey,
888
- devMode,
889
- mockMode,
890
- checkoutUrl,
891
- jailbreakDetectionInfo
892
- );
1190
+ trackStartFailure("payment_successful", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
893
1191
  setShowCheckout(false);
894
1192
  return false;
895
1193
  }
@@ -906,15 +1204,7 @@ export function useLrsCheckout(
906
1204
  console.log(`[Glomo-RN-SDK LRS] Cannot start checkout. Unknown status: ${status}.`);
907
1205
  }
908
1206
  // Tracking start() failure
909
- trackStartFailure(
910
- "invalid_status",
911
- orderId,
912
- publicKey,
913
- devMode,
914
- mockMode,
915
- checkoutUrl,
916
- jailbreakDetectionInfo
917
- );
1207
+ trackStartFailure("invalid_status", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
918
1208
  setShowCheckout(false);
919
1209
  return false;
920
1210
  }
@@ -924,15 +1214,7 @@ export function useLrsCheckout(
924
1214
  console.error("[Glomo-RN-SDK LRS] Cannot start checkout. Invalid checkout URL.");
925
1215
  }
926
1216
  // Tracking start() failure
927
- trackStartFailure(
928
- "invalid_url",
929
- orderId,
930
- publicKey,
931
- devMode,
932
- mockMode,
933
- checkoutUrl,
934
- jailbreakDetectionInfo
935
- );
1217
+ trackStartFailure("invalid_url", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
936
1218
  setShowCheckout(false);
937
1219
  return false;
938
1220
  }
@@ -947,7 +1229,7 @@ export function useLrsCheckout(
947
1229
  setStatus("payment_in_progress");
948
1230
 
949
1231
  // Tracking successful start
950
- trackStartSuccess(orderId, publicKey, devMode, mockMode, checkoutUrl, jailbreakDetectionInfo);
1232
+ trackStartSuccess(orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
951
1233
 
952
1234
  setShowCheckout(true);
953
1235
  return true;
@@ -991,6 +1273,14 @@ export function useLrsCheckout(
991
1273
  };
992
1274
  }, []);
993
1275
 
1276
+ /**
1277
+ * Scripts injected into each WebView, carrying what this build can advertise.
1278
+ *
1279
+ * Resolved once per mount rather than at module load, so a merchant who never opens a
1280
+ * checkout never pays for the dependency probe.
1281
+ */
1282
+ const injectionScripts = useMemo(() => buildInjectionScripts(resolveCapabilities()), []);
1283
+
994
1284
  return {
995
1285
  start,
996
1286
  status,
@@ -1011,14 +1301,15 @@ export function useLrsCheckout(
1011
1301
  handleNavigationStateChange,
1012
1302
  handleModalBackButton,
1013
1303
  handleFlowBack,
1304
+ handlePermissionRequest,
1014
1305
  injectedMain:
1015
1306
  __DEV__ && internalOptions?._testInjectionScript
1016
- ? InjectionScripts.main + internalOptions._testInjectionScript
1017
- : InjectionScripts.main,
1018
- injectedFlow: InjectionScripts.flow,
1307
+ ? injectionScripts.main + internalOptions._testInjectionScript
1308
+ : injectionScripts.main,
1309
+ injectedFlow: injectionScripts.flow,
1019
1310
  injectedEducationCarousel:
1020
1311
  __DEV__ && internalOptions?._testCarouselInjectionScript
1021
- ? InjectionScripts.educationCarousel + internalOptions._testCarouselInjectionScript
1022
- : InjectionScripts.educationCarousel,
1312
+ ? injectionScripts.educationCarousel + internalOptions._testCarouselInjectionScript
1313
+ : injectionScripts.educationCarousel,
1023
1314
  };
1024
1315
  }