@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
@@ -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,12 +126,22 @@ 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;
107
133
  }
108
134
 
109
- /** The main SDK hook for the GlomoPay LRS Checkout flow */
135
+ /**
136
+ * The main SDK hook for the GlomoPay LRS Checkout flow
137
+ * Internal options meant to simulate an LRS checkout are for automated tests only.
138
+ */
139
+ export interface UseLrsCheckoutInternalOptions {
140
+ _testInjectionScript?: string;
141
+ _testCarouselInjectionScript?: string;
142
+ _onEducationCarouselStateChange?: (state: string) => void;
143
+ }
144
+
110
145
  export function useLrsCheckout(
111
146
  {
112
147
  server,
@@ -117,9 +152,11 @@ export function useLrsCheckout(
117
152
  onConnectionError,
118
153
  onPaymentTerminate,
119
154
  onSdkError,
155
+ onUserRefusedDevicePermissions,
120
156
  devMode = false,
121
157
  }: UseLrsCheckoutOptions,
122
- mockMode: boolean
158
+ mockMode: boolean,
159
+ internalOptions?: UseLrsCheckoutInternalOptions
123
160
  ): UseLrsCheckoutReturn {
124
161
  // Refs for the WebViews
125
162
  const mainWebViewRef = useRef<WebView>(null);
@@ -141,7 +178,9 @@ export function useLrsCheckout(
141
178
  const mode = mockMode ? "mock" : "live";
142
179
  const params = baseParams.concat(
143
180
  `mode=${encodeURIComponent(mode)}`,
144
- `publicKey=${encodeURIComponent(publicKey)}`
181
+ `publicKey=${encodeURIComponent(publicKey)}`,
182
+ `product=${encodeURIComponent(ANALYTICS_PRODUCT)}`,
183
+ `surface=${encodeURIComponent(ANALYTICS_SURFACE)}`
145
184
  );
146
185
  const queryString = params.join("&");
147
186
 
@@ -179,7 +218,9 @@ export function useLrsCheckout(
179
218
  const separator = baseEducationCarouselUrl.includes("?") ? "&" : "?";
180
219
  const finalUrl =
181
220
  `${baseEducationCarouselUrl}${separator}orderId=${encodeURIComponent(orderId)}` +
182
- `&publicKey=${encodeURIComponent(publicKey)}`;
221
+ `&publicKey=${encodeURIComponent(publicKey)}` +
222
+ `&product=${encodeURIComponent(ANALYTICS_PRODUCT)}` +
223
+ `&surface=${encodeURIComponent(ANALYTICS_SURFACE)}`;
183
224
 
184
225
  if (!isValidUrl(finalUrl)) {
185
226
  if (devMode) {
@@ -208,7 +249,18 @@ export function useLrsCheckout(
208
249
 
209
250
  const [showFlowWebView, setShowFlowWebView] = useState(false);
210
251
  const [flowWebViewUrl, setFlowWebViewUrl] = useState<string>("");
211
- const [educationCarouselState, setEducationCarouselState] = useState<EducationCarouselState>("pending");
252
+ const [educationCarouselState, setEducationCarouselStateRaw] = useState<EducationCarouselState>("pending");
253
+
254
+ /** Wrapping the raw state setter to also fire the internal test callback */
255
+ const testCarouselCallbackRef = useRef(internalOptions?._onEducationCarouselStateChange);
256
+ testCarouselCallbackRef.current = internalOptions?._onEducationCarouselStateChange;
257
+
258
+ const setEducationCarouselState = useCallback((nextState: EducationCarouselState) => {
259
+ setEducationCarouselStateRaw(nextState);
260
+ if (__DEV__) {
261
+ testCarouselCallbackRef.current?.(nextState);
262
+ }
263
+ }, []);
212
264
 
213
265
  useEffect(() => {
214
266
  if (devMode) {
@@ -298,16 +350,60 @@ export function useLrsCheckout(
298
350
  [devMode]
299
351
  );
300
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
+
301
399
  // Handler for the main WebView messages
302
- const handleMainWebViewMessage = useCallback(
303
- (event: WebViewMessageEvent) => {
304
- try {
305
- const data = event.nativeEvent?.data;
306
- if (!data || typeof data !== "string") {
307
- if (devMode) {
308
- console.warn("[Glomo-RN-SDK LRS] Invalid message data from MainWebView");
309
- }
310
- // Tracking invalid message received
400
+ const handleMainWebViewMessage = useMemo(
401
+ () =>
402
+ createMessageRouter({
403
+ logPrefix: LOG_PREFIX,
404
+ source: "MainWebView",
405
+ devMode,
406
+ onInvalidMessage: (data) => {
311
407
  trackInvalidMessageReceived(
312
408
  "main",
313
409
  data,
@@ -318,158 +414,242 @@ export function useLrsCheckout(
318
414
  mockMode,
319
415
  checkoutUrl
320
416
  );
321
- return;
322
- }
323
-
324
- const message = JSON.parse(data);
325
- if (devMode) {
326
- console.log("[Glomo-RN-SDK LRS] MainWebView Message received:", message.type, message);
327
- }
328
-
329
- if (message.type === "console") {
330
- if (devMode) {
331
- const level = message.level || "log";
332
- console.log(`[Glomo-RN-SDK LRS] [WebView ${level.toUpperCase()}]`, message.message);
333
- }
334
- return;
335
- }
417
+ },
418
+ handlers: {
419
+ "file.save": fileSaveHandler,
420
+ "file.input": fileInputHandler,
336
421
 
337
- if (message.type === "window.open") {
338
- const url = message.url;
339
- if (url && typeof url === "string" && isValidUrl(url)) {
340
- if (devMode) {
341
- console.log(`[Glomo-RN-SDK LRS] window.open called from MainWebView: ${url}`);
342
- }
343
- setFlowWebViewUrl(url);
344
- setShowFlowWebView(true);
345
- setEducationCarouselState("pending");
346
- // Tracking window.open interception
347
- trackWindowOpen("main", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
348
- } else {
422
+ console: (message) => {
349
423
  if (devMode) {
350
- console.log(
351
- "[Glomo-RN-SDK LRS] Invalid URL in window.open from MainWebView:",
352
- url,
353
- typeof url === "undefined" ? "undefined" : ""
354
- );
424
+ const level = (typeof message.level === "string" && message.level) || "log";
425
+ console.log(`${LOG_PREFIX} [WebView ${level.toUpperCase()}]`, message.message);
355
426
  }
356
- }
357
- return;
358
- }
359
-
360
- if (message.type === "window.close") {
361
- if (devMode) {
362
- console.log("[Glomo-RN-SDK LRS] Child window close requested from MainWebView");
363
- }
364
- setShowFlowWebView(false);
365
- setFlowWebViewUrl("");
366
- setEducationCarouselState("pending");
367
- // Tracking window.close interception
368
- trackWindowClose("main", orderId, publicKey, devMode, mockMode, checkoutUrl);
369
- return;
370
- }
427
+ },
371
428
 
372
- // Handling merchant JS callbacks on flow completion
373
- if (message.type === "message" && message.message) {
374
- const messageData = message.message;
375
- const messageType = messageData?.type;
376
-
377
- if (messageType === "payment.success" || messageType === "payment.complete") {
378
- const paymentMessage = messageData;
379
- const payload = paymentMessage?.payload;
380
-
381
- if (isValidPaymentPayload(payload)) {
429
+ "window.open": (message) => {
430
+ const url = message.url;
431
+ if (url && typeof url === "string" && isValidUrl(url)) {
382
432
  if (devMode) {
383
- console.log("[Glomo-RN-SDK LRS] Payment successful via merchant callback!");
433
+ console.log(`${LOG_PREFIX} window.open called from MainWebView: ${url}`);
384
434
  }
385
- setShowCheckout(false);
386
- setShowFlowWebView(false);
387
- setFlowWebViewUrl("");
435
+ setFlowWebViewUrl(url);
436
+ setShowFlowWebView(true);
388
437
  setEducationCarouselState("pending");
389
- setStatus("payment_successful");
390
- safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
391
- // Tracking payment success callback
392
- trackPaymentSuccess(
393
- payload.orderId,
394
- payload.paymentId,
395
- publicKey,
396
- devMode,
397
- mockMode,
398
- checkoutUrl
399
- );
438
+ // Tracking window.open interception
439
+ trackWindowOpen("main", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
400
440
  } else {
401
441
  if (devMode) {
402
- 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
+ );
403
447
  }
404
448
  }
405
- return;
406
- }
449
+ },
407
450
 
408
- if (messageType === "payment.failure" || messageType === "payment.error") {
409
- const paymentMessage = messageData;
410
- const payload = paymentMessage?.payload;
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
+ }
536
+
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
+ }
411
605
 
412
- if (isValidPaymentPayload(payload)) {
606
+ // Handling checkout.closed events
607
+ if (messageType === "checkout.closed") {
413
608
  if (devMode) {
414
- console.error("[Glomo-RN-SDK LRS] Payment failed via merchant callback");
609
+ console.log(`${LOG_PREFIX} Checkout closed event received`);
415
610
  }
416
611
  setShowCheckout(false);
417
612
  setShowFlowWebView(false);
418
613
  setFlowWebViewUrl("");
419
614
  setEducationCarouselState("pending");
420
- setStatus("payment_failed");
421
- safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
422
- // Tracking payment failure callback
423
- trackPaymentFailure(
424
- payload.orderId,
425
- 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),
426
621
  publicKey,
427
622
  devMode,
428
623
  mockMode,
429
624
  checkoutUrl
430
625
  );
431
- } else {
432
- if (devMode) {
433
- console.error("[Glomo-RN-SDK LRS] Invalid payment failure payload:", payload);
434
- }
435
626
  }
436
- return;
437
- }
438
-
439
- // Handling checkout.closed events
440
- if (messageType === "checkout.closed") {
441
- if (devMode) {
442
- console.log("[Glomo-RN-SDK LRS] Checkout closed event received");
443
- }
444
- setShowCheckout(false);
445
- setShowFlowWebView(false);
446
- setFlowWebViewUrl("");
447
- setEducationCarouselState("pending");
448
- safeCallback(onPaymentTerminate, [], "onPaymentTerminate", devMode);
449
- // Tracking payment terminate callback
450
- trackPaymentTerminate(orderId, "checkout_closed", publicKey, devMode, mockMode, checkoutUrl);
451
- return;
452
- }
453
- }
454
- } catch (error) {
455
- if (devMode) {
456
- console.error("[Glomo-RN-SDK LRS] Error parsing MainWebView message:", error);
457
- }
458
- }
459
- },
460
- [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
+ ]
461
643
  );
462
644
 
463
645
  // Handler for the flow WebView messages
464
- const handleFlowWebViewMessage = useCallback(
465
- (event: WebViewMessageEvent) => {
466
- try {
467
- const data = event.nativeEvent?.data;
468
- if (!data || typeof data !== "string") {
469
- if (devMode) {
470
- console.warn("[Glomo-RN-SDK LRS] Invalid message data from FlowWebView");
471
- }
472
- // Tracking invalid message received
646
+ const handleFlowWebViewMessage = useMemo(
647
+ () =>
648
+ createMessageRouter({
649
+ logPrefix: LOG_PREFIX,
650
+ source: "FlowWebView",
651
+ devMode,
652
+ onInvalidMessage: (data) => {
473
653
  trackInvalidMessageReceived(
474
654
  "flow",
475
655
  data,
@@ -480,179 +660,261 @@ export function useLrsCheckout(
480
660
  mockMode,
481
661
  checkoutUrl
482
662
  );
483
- return;
484
- }
485
-
486
- const message = JSON.parse(data);
487
- if (devMode) {
488
- console.log("[Glomo-RN-SDK LRS] FlowWebView Message received:", message.type, message);
489
- }
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
+ },
490
683
 
491
- if (message.type === "console") {
492
- if (devMode) {
493
- console.log(`[Glomo-RN-SDK LRS] [Flow] ${message.message}`);
494
- }
495
- return;
496
- }
684
+ "file.save": fileSaveHandler,
685
+ "file.input": fileInputHandler,
497
686
 
498
- if (message.type === "window.open") {
499
- const url = message.url;
500
- if (url && typeof url === "string" && isValidUrl(url)) {
687
+ console: (message) => {
501
688
  if (devMode) {
502
- console.log(`[Glomo-RN-SDK LRS] window.open called from FlowWebView: ${url}`);
689
+ console.log(`${LOG_PREFIX} [Flow] ${message.message}`);
503
690
  }
504
- setFlowWebViewUrl(url);
505
- setEducationCarouselState("pending");
506
- // Tracking window.open interception
507
- trackWindowOpen("flow", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
508
- } else {
509
- if (devMode) {
510
- console.log(
511
- "[Glomo-RN-SDK LRS] Invalid URL in window.open from FlowWebView:",
512
- url,
513
- typeof url === "undefined" ? "undefined" : ""
514
- );
515
- }
516
- }
517
- return;
518
- }
519
-
520
- if (message.type === "window.close") {
521
- if (devMode) {
522
- console.log("[Glomo-RN-SDK LRS] FlowWebView close requested");
523
- }
524
- setShowFlowWebView(false);
525
- setFlowWebViewUrl("");
526
- setEducationCarouselState("pending");
527
- // Tracking window.close interception
528
- trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
529
- return;
530
- }
531
-
532
- if (message.type === "message" && message.message) {
533
- const messageData = message.message;
534
- const messageType = messageData?.type;
535
-
536
- if (messageType === "payment.success" || messageType === "payment.complete") {
537
- const paymentMessage = messageData;
538
- const payload = paymentMessage?.payload;
691
+ },
539
692
 
540
- if (isValidPaymentPayload(payload)) {
693
+ "window.open": (message) => {
694
+ const url = message.url;
695
+ if (url && typeof url === "string" && isValidUrl(url)) {
541
696
  if (devMode) {
542
- console.log("[Glomo-RN-SDK LRS] Payment successful via FlowWebView callback!");
697
+ console.log(`${LOG_PREFIX} window.open called from FlowWebView: ${url}`);
543
698
  }
544
- setShowFlowWebView(false);
545
- setShowCheckout(false);
546
- setFlowWebViewUrl("");
699
+ setFlowWebViewUrl(url);
547
700
  setEducationCarouselState("pending");
548
- setStatus("payment_successful");
549
- safeCallback(onPaymentSuccess, [payload], "onPaymentSuccess", devMode);
550
- // Tracking payment success callback
551
- trackPaymentSuccess(
552
- payload.orderId,
553
- payload.paymentId,
554
- publicKey,
555
- devMode,
556
- mockMode,
557
- checkoutUrl
558
- );
701
+ // Tracking window.open interception
702
+ trackWindowOpen("flow", url, orderId, publicKey, devMode, mockMode, checkoutUrl);
559
703
  } else {
560
704
  if (devMode) {
561
- console.error(
562
- "[Glomo-RN-SDK LRS] Invalid payment success payload from FlowWebView:",
563
- payload
705
+ console.log(
706
+ `${LOG_PREFIX} Invalid URL in window.open from FlowWebView:`,
707
+ url,
708
+ typeof url === "undefined" ? "undefined" : ""
564
709
  );
565
710
  }
566
711
  }
567
- return;
568
- }
712
+ },
569
713
 
570
- if (messageType === "payment.failure" || messageType === "payment.error") {
571
- const paymentMessage = messageData;
572
- const payload = paymentMessage?.payload;
573
-
574
- if (isValidPaymentPayload(payload)) {
575
- if (devMode) {
576
- 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
+ );
577
796
  }
578
- setShowFlowWebView(false);
579
- setShowCheckout(false);
580
- setFlowWebViewUrl("");
581
- setEducationCarouselState("pending");
582
- setStatus("payment_failed");
583
- safeCallback(onPaymentFailure, [payload], "onPaymentFailure", devMode);
584
- // Tracking payment failure callback
585
- trackPaymentFailure(
586
- payload.orderId,
587
- payload.paymentId,
588
- publicKey,
589
- devMode,
590
- mockMode,
591
- checkoutUrl
592
- );
593
- } else {
594
- if (devMode) {
595
- console.error(
596
- "[Glomo-RN-SDK LRS] Invalid payment failure payload from FlowWebView:",
597
- 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
598
864
  );
599
865
  }
600
866
  }
601
- return;
602
- }
603
- }
604
- } catch (error) {
605
- if (devMode) {
606
- console.error("[Glomo-RN-SDK LRS] Error parsing FlowWebView message:", error);
607
- }
608
- }
609
- },
610
- [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
+ ]
611
883
  );
612
884
 
613
885
  // Handler for the education carousel WebView messages
614
- const handleEducationCarouselWebViewMessage = useCallback(
615
- (event: WebViewMessageEvent) => {
616
- try {
617
- const data = event.nativeEvent?.data;
618
- if (!data || typeof data !== "string") {
619
- if (devMode) {
620
- console.warn("[Glomo-RN-SDK LRS] Invalid message data from EducationCarouselWebView");
621
- }
622
- return;
623
- }
624
-
625
- const message = JSON.parse(data);
626
- if (devMode) {
627
- console.log("[Glomo-RN-SDK LRS] EducationCarouselWebView Message received:", message.type, message);
628
- }
629
-
630
- if (message.type === "console") {
631
- if (devMode) {
632
- console.log(`[Glomo-RN-SDK LRS] [EducationCarousel] ${message.message}`);
633
- }
634
- return;
635
- }
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
+ },
636
898
 
637
- if (message.type === "lrs.has_education_steps") {
638
- 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
+ }
639
905
 
640
- if (devMode) {
641
- console.log(
642
- `[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
643
913
  );
644
- }
645
-
646
- trackEducationStepsShown(orderId, publicKey, devMode, mockMode, checkoutUrl, message.source);
647
- setEducationCarouselState(nextState);
648
- return;
649
- }
650
- } catch (error) {
651
- if (devMode) {
652
- console.error("[Glomo-RN-SDK LRS] Error parsing EducationCarouselWebView message:", error);
653
- }
654
- }
655
- },
914
+ setEducationCarouselState("hasContent");
915
+ },
916
+ },
917
+ }),
656
918
  [checkoutUrl, devMode, mockMode, orderId, publicKey]
657
919
  );
658
920
 
@@ -689,10 +951,26 @@ export function useLrsCheckout(
689
951
  console.log("[Glomo-RN-SDK LRS] Ignoring checkout status update for: " + status);
690
952
  }
691
953
  }
692
- safeCallback(onPaymentTerminate, [], "onPaymentTerminate", devMode);
693
- // Tracking payment terminate callback (user dismiss/back button)
694
- const terminateSource = Platform.OS === "ios" ? "user_dismiss" : "back_button";
695
- 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
+ );
696
974
  } catch (error) {
697
975
  if (devMode) {
698
976
  console.error("[Glomo-RN-SDK LRS] Error in handleModalBackButton:", error);
@@ -700,6 +978,39 @@ export function useLrsCheckout(
700
978
  }
701
979
  }, [showFlowWebView, devMode, onPaymentTerminate, status, orderId, publicKey, mockMode, checkoutUrl]);
702
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
+
703
1014
  /** Closes the flow WebView and returns to the main checkout */
704
1015
  const handleFlowBack = useCallback(() => {
705
1016
  if (devMode) {
@@ -708,7 +1019,7 @@ export function useLrsCheckout(
708
1019
  setShowFlowWebView(false);
709
1020
  setFlowWebViewUrl("");
710
1021
  setEducationCarouselState("pending");
711
- trackWindowClose("flow", orderId, publicKey, devMode, mockMode, checkoutUrl);
1022
+ trackBackNavigationViaSdkBackButton(orderId, publicKey, devMode, mockMode, checkoutUrl, "lrs");
712
1023
  }, [devMode, orderId, publicKey, mockMode, checkoutUrl]);
713
1024
 
714
1025
  /**
@@ -728,11 +1039,16 @@ export function useLrsCheckout(
728
1039
  * Checking device security compliance (if peerDependencies are available)
729
1040
  * A null value indicates that recommended security dependencies were not installed and compliance checks will be skipped in this case
730
1041
  */
731
- const deviceComplianceCheck = checkDeviceCompliance(devMode);
732
- const jailbreakDetectionLibraryInstalled = isComplianceCheckAvailable();
733
- const jailbreakDetectionInfo = {
734
- jailbreakDetectionLibraryInstalled,
735
- 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,
736
1052
  };
737
1053
 
738
1054
  if (devMode) {
@@ -740,37 +1056,64 @@ export function useLrsCheckout(
740
1056
  }
741
1057
 
742
1058
  // Tracking start attempt with current status
743
- trackStartAttempt(status, orderId, publicKey, devMode, mockMode, checkoutUrl, jailbreakDetectionInfo);
1059
+ trackStartAttempt(status, orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
1060
+
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
+ }
744
1080
 
745
- if (deviceComplianceCheck === true) {
746
- // Device detected as compromised
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;
747
1089
  if (devMode) {
748
- 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
+ );
749
1095
  }
750
1096
  const errors: SdkError[] = [
751
1097
  {
752
1098
  type: "device_forbidden",
753
- 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.",
754
1102
  },
755
1103
  ];
756
1104
  safeCallback(onSdkError, [errors], "onSdkError", devMode);
757
1105
  // Tracking start() failure
758
1106
  trackStartFailure(
759
- "device_forbidden",
1107
+ unknown ? "device_compliance_unknown" : "device_forbidden",
760
1108
  orderId,
761
1109
  publicKey,
762
1110
  devMode,
763
1111
  mockMode,
764
1112
  checkoutUrl,
765
- jailbreakDetectionInfo
1113
+ dependencyInfo
766
1114
  );
767
1115
  setShowCheckout(false);
768
1116
  return false;
769
- } else if (deviceComplianceCheck === null) {
770
- // 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.
771
- console.warn(
772
- "[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."
773
- );
774
1117
  }
775
1118
 
776
1119
  // Enforcing devMode requirement for development servers
@@ -784,15 +1127,7 @@ export function useLrsCheckout(
784
1127
  ];
785
1128
  safeCallback(onSdkError, [errors], "onSdkError", devMode);
786
1129
  trackSdkError(errors, orderId, publicKey, devMode, mockMode, checkoutUrl);
787
- trackStartFailure(
788
- "validation_error",
789
- orderId,
790
- publicKey,
791
- devMode,
792
- mockMode,
793
- checkoutUrl,
794
- jailbreakDetectionInfo
795
- );
1130
+ trackStartFailure("validation_error", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
796
1131
  setShowCheckout(false);
797
1132
  return false;
798
1133
  }
@@ -840,15 +1175,7 @@ export function useLrsCheckout(
840
1175
  safeCallback(onSdkError, [errors], "onSdkError", devMode);
841
1176
  // Tracking start() failure
842
1177
  trackSdkError(errors, orderId, publicKey, devMode, mockMode, checkoutUrl);
843
- trackStartFailure(
844
- "validation_error",
845
- orderId,
846
- publicKey,
847
- devMode,
848
- mockMode,
849
- checkoutUrl,
850
- jailbreakDetectionInfo
851
- );
1178
+ trackStartFailure("validation_error", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
852
1179
  setShowCheckout(false);
853
1180
  return false;
854
1181
  }
@@ -860,15 +1187,7 @@ export function useLrsCheckout(
860
1187
  );
861
1188
  }
862
1189
  // Tracking start() failure
863
- trackStartFailure(
864
- "payment_successful",
865
- orderId,
866
- publicKey,
867
- devMode,
868
- mockMode,
869
- checkoutUrl,
870
- jailbreakDetectionInfo
871
- );
1190
+ trackStartFailure("payment_successful", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
872
1191
  setShowCheckout(false);
873
1192
  return false;
874
1193
  }
@@ -885,15 +1204,7 @@ export function useLrsCheckout(
885
1204
  console.log(`[Glomo-RN-SDK LRS] Cannot start checkout. Unknown status: ${status}.`);
886
1205
  }
887
1206
  // Tracking start() failure
888
- trackStartFailure(
889
- "invalid_status",
890
- orderId,
891
- publicKey,
892
- devMode,
893
- mockMode,
894
- checkoutUrl,
895
- jailbreakDetectionInfo
896
- );
1207
+ trackStartFailure("invalid_status", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
897
1208
  setShowCheckout(false);
898
1209
  return false;
899
1210
  }
@@ -903,15 +1214,7 @@ export function useLrsCheckout(
903
1214
  console.error("[Glomo-RN-SDK LRS] Cannot start checkout. Invalid checkout URL.");
904
1215
  }
905
1216
  // Tracking start() failure
906
- trackStartFailure(
907
- "invalid_url",
908
- orderId,
909
- publicKey,
910
- devMode,
911
- mockMode,
912
- checkoutUrl,
913
- jailbreakDetectionInfo
914
- );
1217
+ trackStartFailure("invalid_url", orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
915
1218
  setShowCheckout(false);
916
1219
  return false;
917
1220
  }
@@ -926,7 +1229,7 @@ export function useLrsCheckout(
926
1229
  setStatus("payment_in_progress");
927
1230
 
928
1231
  // Tracking successful start
929
- trackStartSuccess(orderId, publicKey, devMode, mockMode, checkoutUrl, jailbreakDetectionInfo);
1232
+ trackStartSuccess(orderId, publicKey, devMode, mockMode, checkoutUrl, dependencyInfo);
930
1233
 
931
1234
  setShowCheckout(true);
932
1235
  return true;
@@ -970,6 +1273,14 @@ export function useLrsCheckout(
970
1273
  };
971
1274
  }, []);
972
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
+
973
1284
  return {
974
1285
  start,
975
1286
  status,
@@ -990,8 +1301,15 @@ export function useLrsCheckout(
990
1301
  handleNavigationStateChange,
991
1302
  handleModalBackButton,
992
1303
  handleFlowBack,
993
- injectedMain: InjectionScripts.main,
994
- injectedFlow: InjectionScripts.flow,
995
- injectedEducationCarousel: InjectionScripts.educationCarousel,
1304
+ handlePermissionRequest,
1305
+ injectedMain:
1306
+ __DEV__ && internalOptions?._testInjectionScript
1307
+ ? injectionScripts.main + internalOptions._testInjectionScript
1308
+ : injectionScripts.main,
1309
+ injectedFlow: injectionScripts.flow,
1310
+ injectedEducationCarousel:
1311
+ __DEV__ && internalOptions?._testCarouselInjectionScript
1312
+ ? injectionScripts.educationCarousel + internalOptions._testCarouselInjectionScript
1313
+ : injectionScripts.educationCarousel,
996
1314
  };
997
1315
  }