@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
@@ -0,0 +1,92 @@
1
+ /** Shared WebView message routing for every GlomoPay checkout flow */
2
+
3
+ import { type WebViewMessageEvent } from "react-native-webview";
4
+
5
+ /**
6
+ * A parsed bridge message.
7
+ *
8
+ * Deliberately loose: the page and our own injection scripts both post here, and the
9
+ * shapes differ per type. Handlers narrow what they need.
10
+ */
11
+ export interface WebViewMessage {
12
+ type?: string;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ /** Handles one message type. Registered by the flow that cares about it. */
17
+ export type WebViewMessageHandler = (message: WebViewMessage) => void;
18
+
19
+ /** The options for createMessageRouter */
20
+ export interface CreateMessageRouterOptions {
21
+ /** Log prefix of the calling flow, e.g. "[Glomo-RN-SDK LRS]" */
22
+ logPrefix: string;
23
+
24
+ /** Which WebView this router serves. Appears in logs only. */
25
+ source: string;
26
+
27
+ /**
28
+ * Called when the event carries no usable string payload, before the router gives up.
29
+ * The caller reports it, because the tracker needs flow-level context the router has no
30
+ * business knowing.
31
+ */
32
+ onInvalidMessage?: (data: unknown) => void;
33
+
34
+ /**
35
+ * Message type to handler. A type with no handler is ignored silently, which is what
36
+ * the open-coded switches did by falling through. Inherited keys are not handlers.
37
+ */
38
+ handlers: Record<string, WebViewMessageHandler>;
39
+
40
+ devMode?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Builds the onMessage handler for a WebView.
45
+ *
46
+ * Exists because all four message handlers opened with the same twenty-five lines - guard the
47
+ * payload, parse it, log it, then dispatch on `message.type`. Only the dispatch differed. New
48
+ * message types now register in one place per flow instead of being added to each switch, which
49
+ * is what let `file.input` telemetry and the education carousel's invalid-message tracking go
50
+ * missing on some WebViews and not others.
51
+ */
52
+ export function createMessageRouter({
53
+ logPrefix,
54
+ source,
55
+ onInvalidMessage,
56
+ handlers,
57
+ devMode = false,
58
+ }: CreateMessageRouterOptions) {
59
+ return (event: WebViewMessageEvent): void => {
60
+ try {
61
+ const data = event.nativeEvent?.data;
62
+ if (!data || typeof data !== "string") {
63
+ if (devMode) {
64
+ console.warn(`${logPrefix} Invalid message data from ${source}`);
65
+ }
66
+ onInvalidMessage?.(data);
67
+ return;
68
+ }
69
+
70
+ const message: WebViewMessage = JSON.parse(data);
71
+ if (devMode) {
72
+ console.log(`${logPrefix} ${source} Message received:`, message.type, message);
73
+ }
74
+
75
+ /**
76
+ * Own-property lookup, not a bare index. `message.type` is page-controlled, and this is
77
+ * now the single dispatch point for every flow - a bare index resolves `__proto__` to
78
+ * `Object.prototype` and throws into the catch below, and `constructor`/`toString` to
79
+ * real functions that then get called. The switches this replaced had neither case.
80
+ */
81
+ const type = message.type;
82
+ const handler = type && Object.prototype.hasOwnProperty.call(handlers, type) ? handlers[type] : undefined;
83
+ if (typeof handler === "function") {
84
+ handler(message);
85
+ }
86
+ } catch (error) {
87
+ if (devMode) {
88
+ console.error(`${logPrefix} Error parsing ${source} message:`, error);
89
+ }
90
+ }
91
+ };
92
+ }
@@ -0,0 +1,31 @@
1
+ /** What the SDK says about a payment payload it could not use */
2
+
3
+ import { type SdkError } from "../utils/analytics";
4
+
5
+ /**
6
+ * The key names on a bridge payload, for telemetry.
7
+ *
8
+ * Names only. The values are payment identifiers and a signature, which is why the dropped-payload
9
+ * event reports the shape of what arrived rather than what it contained.
10
+ */
11
+ export function payloadKeysOf(payload: unknown): string[] {
12
+ if (!payload || typeof payload !== "object") {
13
+ return [];
14
+ }
15
+ return Object.keys(payload as Record<string, unknown>);
16
+ }
17
+
18
+ /**
19
+ * The host-facing form of a payment payload the SDK could not route.
20
+ *
21
+ * Surfaced rather than swallowed: the page reported a terminal outcome and the merchant would
22
+ * otherwise never learn that one happened. `generatedCheckoutUrl` is the nearest existing `field`
23
+ * value - this is a fault in what the checkout page sent, not in anything the merchant configured.
24
+ */
25
+ export function droppedPayloadSdkError(event: string): SdkError {
26
+ return {
27
+ type: "validation_error",
28
+ message: `Received a ${event} event with a payload the SDK could not route. The payment outcome was not reported.`,
29
+ field: "generatedCheckoutUrl",
30
+ };
31
+ }
@@ -0,0 +1,124 @@
1
+ /** Loads the optional dependencies that back document saving, and reports whether they are there */
2
+
3
+ /**
4
+ * The picker surface this SDK uses, as a type only.
5
+ *
6
+ * `typeof import(...)` is erased at compile time, so this gives full typechecking against the real
7
+ * package without emitting an import that would make an optional dependency mandatory.
8
+ */
9
+ type DocumentPickerModule = {
10
+ saveDocuments: typeof import("@react-native-documents/picker").saveDocuments;
11
+ errorCodes: typeof import("@react-native-documents/picker").errorCodes;
12
+ isErrorWithCode: typeof import("@react-native-documents/picker").isErrorWithCode;
13
+ };
14
+
15
+ type BlobUtilModule = import("react-native-blob-util").ReactNativeBlobUtil;
16
+
17
+ /** Package names, so a missing one can be named in a warning and in telemetry */
18
+ export const FILE_SAVE_PACKAGES = ["react-native-blob-util", "@react-native-documents/picker"] as const;
19
+
20
+ /** What the probe found. Shared by the capability list, the start() warning and telemetry. */
21
+ export interface FileSaveAvailability {
22
+ /** True only when every package loaded */
23
+ available: boolean;
24
+
25
+ /** Packages that could not be loaded, in FILE_SAVE_PACKAGES order */
26
+ missing: string[];
27
+ }
28
+
29
+ /**
30
+ * Metro's interop puts an ES default export on `.default`; the picker uses named exports.
31
+ *
32
+ * Widened rather than narrowed because these two packages are loaded through the same seam and
33
+ * disagree on export style.
34
+ */
35
+ function unwrap<T>(loaded: unknown): T | null {
36
+ const module = loaded as ({ default?: T } & T) | null | undefined;
37
+ return module?.default ?? module ?? null;
38
+ }
39
+
40
+ /**
41
+ * Streams a document to disk. Null when the package is absent or its native module is not linked.
42
+ *
43
+ * Metro requires a string literal here, and requires the call to sit directly inside a `try` block.
44
+ *
45
+ * Both halves are load-bearing and neither is obvious:
46
+ *
47
+ * - A `require()` whose argument Metro cannot evaluate statically is rejected at bundle time in
48
+ * project code, and compiled to a runtime throw inside `node_modules` - which is where this file
49
+ * lives in a merchant app. So a loop over FILE_SAVE_PACKAGES calling `require(name)` would not
50
+ * fail loudly; it would report every package missing in every merchant build and silently
51
+ * withdraw the capability.
52
+ * - Metro only tolerates an unresolvable literal when the call is lexically inside a `try` block
53
+ * (`collectDependencies.isOptionalDependency`), and only when `allowOptionalDependencies` is on.
54
+ * `@react-native/metro-config` sets it, which is what makes the optional-peer-dependency pattern
55
+ * work at all - the same mechanism the existing `jail-monkey` require in utils/device-compliance
56
+ * already depends on.
57
+ *
58
+ * - The require must run while this module is still initialising, which is why it sits in an IIFE
59
+ * at module scope rather than inside the exported function. Metro's `guardedLoadModule` reports a
60
+ * failed module load through `ErrorUtils.reportFatalError` and then swallows it, but only when it
61
+ * is not already loading something (`inGuard`). A lazy first `require` - from a hook, a render, or
62
+ * anything after startup - is outside that guard, so an absent package is reported as a *fatal*
63
+ * error and raises a full-screen LogBox in every dev build, even though our `catch` here means the
64
+ * capability degrades correctly and nothing actually breaks. Resolving at module init keeps the
65
+ * throw inside the guard, where it propagates to our `catch` silently.
66
+ *
67
+ * The consequence for structure: these two loaders cannot be collapsed into one parameterised
68
+ * helper, the `try` cannot be hoisted into a caller, and the resolution cannot be made lazy.
69
+ *
70
+ * Deliberately treats "not installed" and "installed but will not load" alike. Both packages ship
71
+ * native modules, so a merchant who installs them without rebuilding the app has them present in
72
+ * node_modules and still unusable - and either way the SDK cannot save a file. The distinction
73
+ * matters only to the warning text, which names both causes rather than guessing between them.
74
+ */
75
+ const blobUtilModule: BlobUtilModule | null = (() => {
76
+ try {
77
+ return unwrap<BlobUtilModule>(require("react-native-blob-util"));
78
+ } catch {
79
+ return null;
80
+ }
81
+ })();
82
+
83
+ export function loadBlobUtil(): BlobUtilModule | null {
84
+ return blobUtilModule;
85
+ }
86
+
87
+ /** Presents the system save dialog. Null, and shaped, on the same terms as loadBlobUtil above. */
88
+ const documentPickerModule: DocumentPickerModule | null = (() => {
89
+ try {
90
+ return unwrap<DocumentPickerModule>(require("@react-native-documents/picker"));
91
+ } catch {
92
+ return null;
93
+ }
94
+ })();
95
+
96
+ export function loadDocumentPicker(): DocumentPickerModule | null {
97
+ return documentPickerModule;
98
+ }
99
+
100
+ /**
101
+ * Whether the SDK can put a document on the device.
102
+ *
103
+ * Both packages are needed and neither substitutes for the other - blob-util streams the download
104
+ * to a temp file, the picker presents the system save dialog for the user to choose a destination.
105
+ * A build missing either one cannot complete a save, so the capability is not advertised and the
106
+ * checkout page keeps its own download path.
107
+ *
108
+ * Reads through the same loaders the saver calls, so the answer here and the behaviour there cannot
109
+ * disagree. Both resolved once at module init and returned from there - see loadBlobUtil on why that
110
+ * is required rather than an optimisation. Safe regardless, since a native module cannot appear
111
+ * mid-session.
112
+ */
113
+ export function resolveFileSaveAvailability(): FileSaveAvailability {
114
+ const missing: string[] = [];
115
+
116
+ if (!loadBlobUtil()) {
117
+ missing.push(FILE_SAVE_PACKAGES[0]);
118
+ }
119
+ if (!loadDocumentPicker()) {
120
+ missing.push(FILE_SAVE_PACKAGES[1]);
121
+ }
122
+
123
+ return { available: missing.length === 0, missing };
124
+ }
@@ -0,0 +1,34 @@
1
+ /** What this SDK build can do, resolved at runtime */
2
+
3
+ import { resolveFileSaveAvailability } from "./file-save";
4
+
5
+ /**
6
+ * Capability strings the checkout page gates on.
7
+ *
8
+ * The page reads these from `window.__glomoSdkCapabilities__` at the moment it needs one, and
9
+ * falls back to its own behaviour when a string is absent. It deliberately does not infer support
10
+ * from the bridge existing - every Glomo SDK has a bridge, so a build without the handler would
11
+ * take delivery of the message into a router that ignores it, and the user's tap would vanish.
12
+ */
13
+ export const CAPABILITIES = {
14
+ FILE_SAVE: "file.save",
15
+ } as const;
16
+
17
+ /**
18
+ * The capabilities this build can honour.
19
+ *
20
+ * Append only, and only when the handler actually exists in this build - removing a string
21
+ * silently reverts the page to its own path, which is the correct degradation but a confusing
22
+ * one to debug if it happens by accident.
23
+ */
24
+ export function resolveCapabilities(): string[] {
25
+ const capabilities: string[] = [];
26
+
27
+ if (resolveFileSaveAvailability().available) {
28
+ capabilities.push(CAPABILITIES.FILE_SAVE);
29
+ }
30
+
31
+ return capabilities;
32
+ }
33
+
34
+ export { resolveFileSaveAvailability, FILE_SAVE_PACKAGES, type FileSaveAvailability } from "./file-save";
@@ -0,0 +1,55 @@
1
+ /** Tells the host, once per mount, about capabilities this build cannot offer */
2
+
3
+ import { useEffect, useRef } from "react";
4
+
5
+ import { resolveFileSaveAvailability } from "./file-save";
6
+ import { safeCallback } from "../utils/validation";
7
+ import { type SdkError } from "../utils/analytics";
8
+
9
+ /** The options for useCapabilityReporting */
10
+ export interface UseCapabilityReportingOptions {
11
+ onSdkError: (errors: Array<SdkError>) => void;
12
+ devMode?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Reports missing optional dependencies to the host on mount.
17
+ *
18
+ * Paired with the ungated `console.warn` in `start()`, and deliberately not merged with it: the
19
+ * warning reaches a developer watching a console, this reaches a merchant's error handling, and a
20
+ * merchant who has wired up logging should not have to read logs to discover that a feature they
21
+ * were told exists is inert in their build.
22
+ *
23
+ * Not an error the SDK raises about itself - the checkout still works, and the page falls back to
24
+ * its own download path. `field` is set so it can be filtered apart from a configuration fault on
25
+ * the same callback.
26
+ *
27
+ * Fires once per mounted component rather than once per process, and never on a build where the
28
+ * dependencies are present.
29
+ */
30
+ export function useCapabilityReporting({ onSdkError, devMode = false }: UseCapabilityReportingOptions): void {
31
+ const reportedRef = useRef(false);
32
+
33
+ useEffect(() => {
34
+ if (reportedRef.current) {
35
+ return;
36
+ }
37
+ reportedRef.current = true;
38
+
39
+ const fileSave = resolveFileSaveAvailability();
40
+ if (fileSave.available) {
41
+ return;
42
+ }
43
+
44
+ const errors: Array<SdkError> = [
45
+ {
46
+ type: "validation_error",
47
+ message:
48
+ `Document downloads are unavailable: ${fileSave.missing.join(" and ")} could not be loaded. ` +
49
+ "Install the package(s) and rebuild the app to enable them.",
50
+ field: "file.save",
51
+ },
52
+ ];
53
+ safeCallback(onSdkError, [errors], "onSdkError", devMode);
54
+ }, [onSdkError, devMode]);
55
+ }
@@ -51,6 +51,19 @@ export const SERVER_CONFIGS: Record<InternalGlomoServer, GlomoServerConfig> = {
51
51
  },
52
52
  };
53
53
 
54
+ /**
55
+ * Identifies this SDK to the checkout page, which forwards both onto the order it creates.
56
+ *
57
+ * Exists because nothing in `payments`, `payment_sessions`, `terminals` or `api_audits` carries a
58
+ * surface dimension, so an SDK-originated order is indistinguishable from a dashboard one
59
+ * (KAN-6551). The Flutter SDK sends the analogous pair (`checkout-flutter-sdk` / `flutter`).
60
+ *
61
+ * Inert until the checkout page is redeployed to read them - neither `apps/checkout` nor
62
+ * `lrs-checkout-page` allow-lists query params, so unknown ones are ignored rather than rejected.
63
+ */
64
+ export const ANALYTICS_PRODUCT = "checkout-react-native-sdk";
65
+ export const ANALYTICS_SURFACE = "react_native";
66
+
54
67
  /** Returns the full server config for the given environment, falling back to prod */
55
68
  export function resolveServerConfig(server?: InternalGlomoServer): GlomoServerConfig {
56
69
  const resolvedServer = server && server in SERVER_CONFIGS ? server : DEFAULT_SERVER;
@@ -1,7 +1,12 @@
1
1
  /**
2
- * The unified GlomoPay Checkout Component (v4).
2
+ * The unified GlomoPay Checkout Component.
3
3
  * Detects order type (LRS vs Standard) and delegates rendering to the appropriate inner component.
4
4
  * Enforces error handling via `onSdkError()` as a mandatory prop.
5
+ *
6
+ * Its props are a union over both flows, so a prop that only one flow uses is still accepted here.
7
+ * `onUserJourneyCompleted` is the one case where that matters: it is forwarded to the standard and
8
+ * subscriptions flows and silently ignored on LRS orders, because LRS never produces an
9
+ * asynchronous journey for it to report. Every other callback reaches every flow.
5
10
  */
6
11
 
7
12
  import React, { useImperativeHandle, useRef, useEffect, useMemo, forwardRef } from "react";
@@ -12,6 +17,7 @@ import { trackUseOfUnsupportedFunctionality } from "./utils/analytics";
12
17
  import { GlomoLrsCheckout } from "./glomo-lrs-checkout";
13
18
  import { GlomoStandardCheckoutInternal as GlomoStandardCheckout } from "./glomo-standard-checkout";
14
19
  import { GlomoSubscriptionsCheckout } from "./glomo-subscriptions-checkout";
20
+ import { isMockModeKey } from "./utils/validation";
15
21
 
16
22
  function GlomoCheckoutComponent(
17
23
  props: GlomoCheckoutProps & GlomoCheckoutInternalProps,
@@ -37,10 +43,7 @@ function GlomoCheckoutComponent(
37
43
  // Tracking unsupported callback props on subscription flows (fires once per mount)
38
44
  const hasTrackedUnsupportedRef = useRef(false);
39
45
  const devMode = passedProps.devMode ?? false;
40
- const mockMode = useMemo(
41
- () => passedProps.publicKey?.toLowerCase().startsWith("test_") ?? false,
42
- [passedProps.publicKey]
43
- );
46
+ const mockMode = useMemo(() => isMockModeKey(passedProps.publicKey), [passedProps.publicKey]);
44
47
 
45
48
  // Extracting stable boolean for the tracking effect to avoid depending on passedProps object
46
49
  const hasUserJourneyCallback = !!passedProps.onUserJourneyCompleted;
@@ -74,6 +77,7 @@ function GlomoCheckoutComponent(
74
77
  onConnectionError={passedProps.onConnectionError}
75
78
  onPaymentTerminate={passedProps.onPaymentTerminate}
76
79
  onSdkError={passedProps.onSdkError}
80
+ onUserRefusedDevicePermissions={passedProps.onUserRefusedDevicePermissions}
77
81
  devMode={passedProps.devMode}
78
82
  _testInjectionScript={props._testInjectionScript}
79
83
  _testCarouselInjectionScript={props._testCarouselInjectionScript}
@@ -96,7 +100,7 @@ function GlomoCheckoutComponent(
96
100
  onPaymentTerminate={passedProps.onPaymentTerminate}
97
101
  onSdkError={passedProps.onSdkError}
98
102
  onUserJourneyCompleted={passedProps.onUserJourneyCompleted}
99
- onUserRefusedCameraPermissions={passedProps.onUserRefusedCameraPermissions}
103
+ onUserRefusedDevicePermissions={passedProps.onUserRefusedDevicePermissions}
100
104
  devMode={passedProps.devMode}
101
105
  _testInjectionScript={props._testInjectionScript}
102
106
  />
@@ -117,7 +121,7 @@ function GlomoCheckoutComponent(
117
121
  onPaymentTerminate={passedProps.onPaymentTerminate}
118
122
  onSdkError={passedProps.onSdkError}
119
123
  onUserJourneyCompleted={passedProps.onUserJourneyCompleted}
120
- onUserRefusedCameraPermissions={passedProps.onUserRefusedCameraPermissions}
124
+ onUserRefusedDevicePermissions={passedProps.onUserRefusedDevicePermissions}
121
125
  devMode={passedProps.devMode}
122
126
  _testInjectionScript={props._testInjectionScript}
123
127
  />
@@ -2,11 +2,12 @@
2
2
 
3
3
  import React, { useMemo, useImperativeHandle, forwardRef } from "react";
4
4
  import { StyleSheet, View, Modal, Platform, SafeAreaView, TouchableOpacity, Text } from "react-native";
5
- import { WebView, type WebViewMessageEvent, type WebViewProps } from "react-native-webview";
5
+ import { WebView, type WebViewMessageEvent } from "react-native-webview";
6
6
 
7
7
  import { useLrsCheckout, type UseLrsCheckoutOptions, type LrsCheckoutStatus } from "./use-lrs-checkout";
8
8
  import { type SdkError } from "./utils/analytics";
9
- import { isValidUrl } from "./utils/validation";
9
+ import { isValidUrl, isMockModeKey } from "./utils/validation";
10
+ import { buildCapabilityProps, type CapabilityWebViewProps } from "./webview/capability-props";
10
11
 
11
12
  export interface GlomoLrsCheckoutProps extends UseLrsCheckoutOptions {
12
13
  visible?: boolean;
@@ -30,7 +31,8 @@ export interface GlomoLrsCheckoutRef {
30
31
  *
31
32
  * @param onPaymentSuccess - The success callback with payload, for a successful payment
32
33
  * @param onPaymentFailure - The failure callback with payload, for a failed payment
33
- * @param onPaymentTerminate - Optional callback called when the checkout is terminated either from the LRS webpage (back button or close button) or mobile component (user dismissing the modal on iOS or user pressing the back button on Android)
34
+ * @param onPaymentTerminate - Optional callback called when the checkout ends without a payment outcome. Receives a payload whose `source` is a TERMINATION_SOURCES value naming how it ended: the page's own close control, the Android back button/gesture, or an iOS swipe-down on the modal. The back chevron over a bank flow is deliberately not a termination - it returns the user to the checkout with the session intact.
35
+ * @param onUserRefusedDevicePermissions - Optional callback invoked when the user refuses a device permission the bank's page asked for - camera today. Upload-related permissions are requested by react-native-webview itself and do not reach this callback. Currently Android only: due to platform limitations on iOS, the WebView raises its own prompt and never reports the answer back, so this cannot fire there.
34
36
  *
35
37
  * @param onConnectionError - Optional callback for connection/network errors
36
38
  * @param onSdkError - Optional callback for handling SDK validation errors and configuration issues
@@ -50,6 +52,7 @@ function GlomoLrsCheckoutComponent(
50
52
  onConnectionError,
51
53
  onPaymentTerminate,
52
54
  onSdkError,
55
+ onUserRefusedDevicePermissions,
53
56
  devMode = false,
54
57
  visible: controlledVisible,
55
58
  _testInjectionScript,
@@ -60,7 +63,7 @@ function GlomoLrsCheckoutComponent(
60
63
  ) {
61
64
  // Computing mockMode based on the publicKey prefix
62
65
  const mockMode = useMemo(() => {
63
- const resolvedMockMode = publicKey?.toLowerCase().startsWith("test_") ?? false;
66
+ const resolvedMockMode = isMockModeKey(publicKey);
64
67
  if (devMode) {
65
68
  console.log(
66
69
  `[Glomo-RN-SDK LRS] ${resolvedMockMode ? "Using" : "Not using"} Mock mode for publicKey: ${publicKey}`
@@ -89,6 +92,7 @@ function GlomoLrsCheckoutComponent(
89
92
  handleNavigationStateChange,
90
93
  handleModalBackButton,
91
94
  handleFlowBack,
95
+ handlePermissionRequest,
92
96
  injectedMain,
93
97
  injectedFlow,
94
98
  injectedEducationCarousel,
@@ -102,6 +106,7 @@ function GlomoLrsCheckoutComponent(
102
106
  onConnectionError,
103
107
  onPaymentTerminate,
104
108
  onSdkError,
109
+ onUserRefusedDevicePermissions,
105
110
  devMode,
106
111
  },
107
112
  mockMode,
@@ -161,8 +166,14 @@ function GlomoLrsCheckoutComponent(
161
166
  * setSupportZoom/scalesPageToFit prevent user-initiated and system-initiated zoom;
162
167
  * textZoom=100 on Android normalises the system font-size setting so the carousel isn't affected.
163
168
  */
164
- const carouselWebViewProps = useMemo<Partial<WebViewProps>>(
169
+ const carouselWebViewProps = useMemo<CapabilityWebViewProps>(
165
170
  () => ({
171
+ /**
172
+ * The carousel renders our own content, which never asks for a device permission.
173
+ * Capabilities are advertised only where something can exercise them, so it opts out
174
+ * of the handler the other two WebViews share.
175
+ */
176
+ onPermissionRequest: undefined,
166
177
  setSupportZoom: false,
167
178
  scalesPageToFit: false,
168
179
  overScrollMode: "never",
@@ -171,6 +182,12 @@ function GlomoLrsCheckoutComponent(
171
182
  []
172
183
  );
173
184
 
185
+ // Configuration shared by every WebView the SDK renders, regardless of flow
186
+ const capabilityProps = useMemo(
187
+ () => buildCapabilityProps({ devMode, onPermissionRequest: handlePermissionRequest }),
188
+ [devMode, handlePermissionRequest]
189
+ );
190
+
174
191
  // Making the WebView component
175
192
  const makeWebView = React.useCallback(
176
193
  (
@@ -180,7 +197,7 @@ function GlomoLrsCheckoutComponent(
180
197
  identifier: "main" | "flow" | "educationCarousel",
181
198
  injectedJavaScript?: string,
182
199
  injectedJavaScriptBeforeContentLoaded?: string,
183
- additionalProps?: Partial<WebViewProps>
200
+ additionalProps?: CapabilityWebViewProps
184
201
  ) => {
185
202
  // Preventing a render if no injection script is provided
186
203
  if (!injectedJavaScript) {
@@ -212,10 +229,8 @@ function GlomoLrsCheckoutComponent(
212
229
 
213
230
  return (
214
231
  <WebView
232
+ {...capabilityProps}
215
233
  ref={ref}
216
- webviewDebuggingEnabled={__DEV__ && devMode}
217
- javaScriptEnabled={true}
218
- domStorageEnabled={true}
219
234
  onMessage={messageHandler}
220
235
  onError={handleError}
221
236
  onHttpError={handleHttpError}
@@ -241,29 +256,21 @@ function GlomoLrsCheckoutComponent(
241
256
  }
242
257
  }}
243
258
  source={{ uri: url }}
244
- javaScriptCanOpenWindowsAutomatically={true}
245
- setSupportMultipleWindows={true}
246
- incognito={true}
247
- cacheEnabled={false}
248
- cacheMode='LOAD_NO_CACHE'
249
- pullToRefreshEnabled={false}
250
- limitsNavigationsToAppBoundDomains={false}
251
- menuItems={[]}
252
259
  injectedJavaScript={injectedJavaScript}
253
260
  injectedJavaScriptBeforeContentLoaded={injectedJavaScriptBeforeContentLoaded}
254
261
  style={styles.webview}
255
- showsHorizontalScrollIndicator={false}
256
- showsVerticalScrollIndicator={false}
257
- mixedContentMode='always'
258
- thirdPartyCookiesEnabled={true}
259
- sharedCookiesEnabled={true}
260
- allowsInlineMediaPlayback={true}
261
- mediaPlaybackRequiresUserAction={false}
262
262
  {...additionalProps}
263
263
  />
264
264
  );
265
265
  },
266
- [handleError, handleHttpError, handleNavigationStateChange, devMode, carouselViewportGuardScript]
266
+ [
267
+ handleError,
268
+ handleHttpError,
269
+ handleNavigationStateChange,
270
+ devMode,
271
+ carouselViewportGuardScript,
272
+ capabilityProps,
273
+ ]
267
274
  );
268
275
 
269
276
  const MainWebViewComponent = useMemo(
@@ -352,6 +359,8 @@ function GlomoLrsCheckoutComponent(
352
359
  <TouchableOpacity
353
360
  onPress={handleFlowBack}
354
361
  style={styles.flowBackButton}
362
+ accessibilityRole='button'
363
+ accessibilityLabel='Go back to checkout'
355
364
  >
356
365
  <Text style={styles.flowBackChevron}>‹</Text>
357
366
  </TouchableOpacity>
@@ -419,8 +428,14 @@ const styles = StyleSheet.create({
419
428
  borderBottomColor: "#e0e0e0",
420
429
  },
421
430
  flowBackButton: {
422
- paddingVertical: 0,
431
+ /**
432
+ * Sized to the 44dp / 44pt minimum touch target both platforms specify. The glyph alone is
433
+ * 42px tall and the hit area used to hug it horizontally, which is the only SDK-drawn
434
+ * control in this package and the one a user reaches mid-payment.
435
+ */
436
+ paddingVertical: 6,
423
437
  paddingLeft: 20,
438
+ paddingRight: 20,
424
439
  },
425
440
  flowBackChevron: {
426
441
  fontSize: 42,