@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
@@ -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,19 +1,28 @@
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";
8
13
 
9
14
  import { useGlomoCheckout, type UseGlomoCheckoutInternalReturn } from "./use-glomo-checkout";
10
- import { type GlomoCheckoutRef, type GlomoCheckoutProps } from "./types/checkout";
15
+ import { type GlomoCheckoutRef, type GlomoCheckoutProps, type GlomoCheckoutInternalProps } from "./types/checkout";
11
16
  import { trackUseOfUnsupportedFunctionality } from "./utils/analytics";
12
17
  import { GlomoLrsCheckout } from "./glomo-lrs-checkout";
13
- import { GlomoStandardCheckout } from "./glomo-standard-checkout";
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
- function GlomoCheckoutComponent(props: GlomoCheckoutProps, ref: React.ForwardedRef<GlomoCheckoutRef>) {
22
+ function GlomoCheckoutComponent(
23
+ props: GlomoCheckoutProps & GlomoCheckoutInternalProps,
24
+ ref: React.ForwardedRef<GlomoCheckoutRef>
25
+ ) {
17
26
  // useGlomoCheckout handles order type detection and exposes start/getStatus for the ref
18
27
  const {
19
28
  orderType,
@@ -34,10 +43,7 @@ function GlomoCheckoutComponent(props: GlomoCheckoutProps, ref: React.ForwardedR
34
43
  // Tracking unsupported callback props on subscription flows (fires once per mount)
35
44
  const hasTrackedUnsupportedRef = useRef(false);
36
45
  const devMode = passedProps.devMode ?? false;
37
- const mockMode = useMemo(
38
- () => passedProps.publicKey?.toLowerCase().startsWith("test_") ?? false,
39
- [passedProps.publicKey]
40
- );
46
+ const mockMode = useMemo(() => isMockModeKey(passedProps.publicKey), [passedProps.publicKey]);
41
47
 
42
48
  // Extracting stable boolean for the tracking effect to avoid depending on passedProps object
43
49
  const hasUserJourneyCallback = !!passedProps.onUserJourneyCompleted;
@@ -71,7 +77,11 @@ function GlomoCheckoutComponent(props: GlomoCheckoutProps, ref: React.ForwardedR
71
77
  onConnectionError={passedProps.onConnectionError}
72
78
  onPaymentTerminate={passedProps.onPaymentTerminate}
73
79
  onSdkError={passedProps.onSdkError}
80
+ onUserRefusedDevicePermissions={passedProps.onUserRefusedDevicePermissions}
74
81
  devMode={passedProps.devMode}
82
+ _testInjectionScript={props._testInjectionScript}
83
+ _testCarouselInjectionScript={props._testCarouselInjectionScript}
84
+ _onEducationCarouselStateChange={props._onEducationCarouselStateChange}
75
85
  />
76
86
  );
77
87
  }
@@ -90,8 +100,9 @@ function GlomoCheckoutComponent(props: GlomoCheckoutProps, ref: React.ForwardedR
90
100
  onPaymentTerminate={passedProps.onPaymentTerminate}
91
101
  onSdkError={passedProps.onSdkError}
92
102
  onUserJourneyCompleted={passedProps.onUserJourneyCompleted}
93
- onUserRefusedCameraPermissions={passedProps.onUserRefusedCameraPermissions}
103
+ onUserRefusedDevicePermissions={passedProps.onUserRefusedDevicePermissions}
94
104
  devMode={passedProps.devMode}
105
+ _testInjectionScript={props._testInjectionScript}
95
106
  />
96
107
  );
97
108
  }
@@ -110,8 +121,9 @@ function GlomoCheckoutComponent(props: GlomoCheckoutProps, ref: React.ForwardedR
110
121
  onPaymentTerminate={passedProps.onPaymentTerminate}
111
122
  onSdkError={passedProps.onSdkError}
112
123
  onUserJourneyCompleted={passedProps.onUserJourneyCompleted}
113
- onUserRefusedCameraPermissions={passedProps.onUserRefusedCameraPermissions}
124
+ onUserRefusedDevicePermissions={passedProps.onUserRefusedDevicePermissions}
114
125
  devMode={passedProps.devMode}
126
+ _testInjectionScript={props._testInjectionScript}
115
127
  />
116
128
  );
117
129
  }
@@ -124,6 +136,11 @@ function GlomoCheckoutComponent(props: GlomoCheckoutProps, ref: React.ForwardedR
124
136
  return null;
125
137
  }
126
138
 
127
- export const GlomoCheckout = forwardRef<GlomoCheckoutRef, GlomoCheckoutProps>(GlomoCheckoutComponent);
139
+ /**
140
+ * Internal props if provided, should be used for automated testing only
141
+ */
142
+ export const GlomoCheckout = forwardRef(GlomoCheckoutComponent) as React.ForwardRefExoticComponent<
143
+ GlomoCheckoutProps & React.RefAttributes<GlomoCheckoutRef>
144
+ >;
128
145
 
129
146
  GlomoCheckout.displayName = "GlomoCheckout";
@@ -2,15 +2,19 @@
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;
13
14
  onSdkError: (error: Array<SdkError>) => void;
15
+ _testInjectionScript?: string;
16
+ _testCarouselInjectionScript?: string;
17
+ _onEducationCarouselStateChange?: (state: string) => void;
14
18
  }
15
19
 
16
20
  export interface GlomoLrsCheckoutRef {
@@ -27,7 +31,8 @@ export interface GlomoLrsCheckoutRef {
27
31
  *
28
32
  * @param onPaymentSuccess - The success callback with payload, for a successful payment
29
33
  * @param onPaymentFailure - The failure callback with payload, for a failed payment
30
- * @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.
31
36
  *
32
37
  * @param onConnectionError - Optional callback for connection/network errors
33
38
  * @param onSdkError - Optional callback for handling SDK validation errors and configuration issues
@@ -47,14 +52,18 @@ function GlomoLrsCheckoutComponent(
47
52
  onConnectionError,
48
53
  onPaymentTerminate,
49
54
  onSdkError,
55
+ onUserRefusedDevicePermissions,
50
56
  devMode = false,
51
57
  visible: controlledVisible,
58
+ _testInjectionScript,
59
+ _testCarouselInjectionScript,
60
+ _onEducationCarouselStateChange,
52
61
  }: GlomoLrsCheckoutProps,
53
62
  ref: React.ForwardedRef<GlomoLrsCheckoutRef>
54
63
  ) {
55
64
  // Computing mockMode based on the publicKey prefix
56
65
  const mockMode = useMemo(() => {
57
- const resolvedMockMode = publicKey?.toLowerCase().startsWith("test_") ?? false;
66
+ const resolvedMockMode = isMockModeKey(publicKey);
58
67
  if (devMode) {
59
68
  console.log(
60
69
  `[Glomo-RN-SDK LRS] ${resolvedMockMode ? "Using" : "Not using"} Mock mode for publicKey: ${publicKey}`
@@ -83,6 +92,7 @@ function GlomoLrsCheckoutComponent(
83
92
  handleNavigationStateChange,
84
93
  handleModalBackButton,
85
94
  handleFlowBack,
95
+ handlePermissionRequest,
86
96
  injectedMain,
87
97
  injectedFlow,
88
98
  injectedEducationCarousel,
@@ -96,9 +106,11 @@ function GlomoLrsCheckoutComponent(
96
106
  onConnectionError,
97
107
  onPaymentTerminate,
98
108
  onSdkError,
109
+ onUserRefusedDevicePermissions,
99
110
  devMode,
100
111
  },
101
- mockMode
112
+ mockMode,
113
+ { _testInjectionScript, _testCarouselInjectionScript, _onEducationCarouselStateChange }
102
114
  );
103
115
 
104
116
  // Exposing the start() and getStatus() methods via the ref
@@ -154,8 +166,14 @@ function GlomoLrsCheckoutComponent(
154
166
  * setSupportZoom/scalesPageToFit prevent user-initiated and system-initiated zoom;
155
167
  * textZoom=100 on Android normalises the system font-size setting so the carousel isn't affected.
156
168
  */
157
- const carouselWebViewProps = useMemo<Partial<WebViewProps>>(
169
+ const carouselWebViewProps = useMemo<CapabilityWebViewProps>(
158
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,
159
177
  setSupportZoom: false,
160
178
  scalesPageToFit: false,
161
179
  overScrollMode: "never",
@@ -164,6 +182,12 @@ function GlomoLrsCheckoutComponent(
164
182
  []
165
183
  );
166
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
+
167
191
  // Making the WebView component
168
192
  const makeWebView = React.useCallback(
169
193
  (
@@ -173,7 +197,7 @@ function GlomoLrsCheckoutComponent(
173
197
  identifier: "main" | "flow" | "educationCarousel",
174
198
  injectedJavaScript?: string,
175
199
  injectedJavaScriptBeforeContentLoaded?: string,
176
- additionalProps?: Partial<WebViewProps>
200
+ additionalProps?: CapabilityWebViewProps
177
201
  ) => {
178
202
  // Preventing a render if no injection script is provided
179
203
  if (!injectedJavaScript) {
@@ -205,10 +229,8 @@ function GlomoLrsCheckoutComponent(
205
229
 
206
230
  return (
207
231
  <WebView
232
+ {...capabilityProps}
208
233
  ref={ref}
209
- webviewDebuggingEnabled={__DEV__ && devMode}
210
- javaScriptEnabled={true}
211
- domStorageEnabled={true}
212
234
  onMessage={messageHandler}
213
235
  onError={handleError}
214
236
  onHttpError={handleHttpError}
@@ -234,29 +256,21 @@ function GlomoLrsCheckoutComponent(
234
256
  }
235
257
  }}
236
258
  source={{ uri: url }}
237
- javaScriptCanOpenWindowsAutomatically={true}
238
- setSupportMultipleWindows={true}
239
- incognito={true}
240
- cacheEnabled={false}
241
- cacheMode='LOAD_NO_CACHE'
242
- pullToRefreshEnabled={false}
243
- limitsNavigationsToAppBoundDomains={false}
244
- menuItems={[]}
245
259
  injectedJavaScript={injectedJavaScript}
246
260
  injectedJavaScriptBeforeContentLoaded={injectedJavaScriptBeforeContentLoaded}
247
261
  style={styles.webview}
248
- showsHorizontalScrollIndicator={false}
249
- showsVerticalScrollIndicator={false}
250
- mixedContentMode='always'
251
- thirdPartyCookiesEnabled={true}
252
- sharedCookiesEnabled={true}
253
- allowsInlineMediaPlayback={true}
254
- mediaPlaybackRequiresUserAction={false}
255
262
  {...additionalProps}
256
263
  />
257
264
  );
258
265
  },
259
- [handleError, handleHttpError, handleNavigationStateChange, devMode, carouselViewportGuardScript]
266
+ [
267
+ handleError,
268
+ handleHttpError,
269
+ handleNavigationStateChange,
270
+ devMode,
271
+ carouselViewportGuardScript,
272
+ capabilityProps,
273
+ ]
260
274
  );
261
275
 
262
276
  const MainWebViewComponent = useMemo(
@@ -345,6 +359,8 @@ function GlomoLrsCheckoutComponent(
345
359
  <TouchableOpacity
346
360
  onPress={handleFlowBack}
347
361
  style={styles.flowBackButton}
362
+ accessibilityRole='button'
363
+ accessibilityLabel='Go back to checkout'
348
364
  >
349
365
  <Text style={styles.flowBackChevron}>‹</Text>
350
366
  </TouchableOpacity>
@@ -412,8 +428,14 @@ const styles = StyleSheet.create({
412
428
  borderBottomColor: "#e0e0e0",
413
429
  },
414
430
  flowBackButton: {
415
- 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,
416
437
  paddingLeft: 20,
438
+ paddingRight: 20,
417
439
  },
418
440
  flowBackChevron: {
419
441
  fontSize: 42,