@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.
- package/CHANGELOG.md +29 -0
- package/MIGRATION.md +112 -0
- package/README.md +173 -36
- package/lib/bridge/file-input-handler.d.ts +25 -0
- package/lib/bridge/file-input-handler.d.ts.map +1 -0
- package/lib/bridge/file-input-handler.js +38 -0
- package/lib/bridge/file-save-handler.d.ts +32 -0
- package/lib/bridge/file-save-handler.d.ts.map +1 -0
- package/lib/bridge/file-save-handler.js +92 -0
- package/lib/bridge/message-router.d.ts +44 -0
- package/lib/bridge/message-router.d.ts.map +1 -0
- package/lib/bridge/message-router.js +48 -0
- package/lib/bridge/payment-payload.d.ts +18 -0
- package/lib/bridge/payment-payload.d.ts.map +1 -0
- package/lib/bridge/payment-payload.js +31 -0
- package/lib/capabilities/file-save.d.ts +40 -0
- package/lib/capabilities/file-save.d.ts.map +1 -0
- package/lib/capabilities/file-save.js +101 -0
- package/lib/capabilities/index.d.ts +22 -0
- package/lib/capabilities/index.d.ts.map +1 -0
- package/lib/capabilities/index.js +34 -0
- package/lib/capabilities/use-capability-reporting.d.ts +24 -0
- package/lib/capabilities/use-capability-reporting.d.ts.map +1 -0
- package/lib/capabilities/use-capability-reporting.js +44 -0
- package/lib/config/base.d.ts +12 -0
- package/lib/config/base.d.ts.map +1 -1
- package/lib/config/base.js +13 -1
- package/lib/glomo-checkout.d.ts +6 -1
- package/lib/glomo-checkout.d.ts.map +1 -1
- package/lib/glomo-checkout.js +11 -5
- package/lib/glomo-lrs-checkout.d.ts.map +1 -1
- package/lib/glomo-lrs-checkout.js +33 -10
- package/lib/glomo-standard-checkout.d.ts +3 -1
- package/lib/glomo-standard-checkout.d.ts.map +1 -1
- package/lib/glomo-standard-checkout.js +21 -13
- package/lib/glomo-subscriptions-checkout.d.ts.map +1 -1
- package/lib/glomo-subscriptions-checkout.js +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -3
- package/lib/injections/index.d.ts +24 -0
- package/lib/injections/index.d.ts.map +1 -1
- package/lib/injections/index.js +41 -0
- package/lib/injections/webview-flow.injection.d.ts.map +1 -1
- package/lib/injections/webview-flow.injection.js +76 -0
- package/lib/injections/webview-main.injection.d.ts.map +1 -1
- package/lib/injections/webview-main.injection.js +31 -0
- package/lib/injections/webview-standard.injection.d.ts.map +1 -1
- package/lib/injections/webview-standard.injection.js +31 -0
- package/lib/services/file-saver.d.ts +109 -0
- package/lib/services/file-saver.d.ts.map +1 -0
- package/lib/services/file-saver.js +416 -0
- package/lib/types/checkout.d.ts +58 -6
- package/lib/types/checkout.d.ts.map +1 -1
- package/lib/types/checkout.js +24 -2
- package/lib/types/standard-checkout.d.ts +6 -6
- package/lib/types/standard-checkout.d.ts.map +1 -1
- package/lib/types/subscriptions-checkout.d.ts +3 -3
- package/lib/types/subscriptions-checkout.d.ts.map +1 -1
- package/lib/use-glomo-checkout.js +2 -2
- package/lib/use-lrs-checkout.d.ts +9 -5
- package/lib/use-lrs-checkout.d.ts.map +1 -1
- package/lib/use-lrs-checkout.js +317 -171
- package/lib/use-standard-checkout.d.ts +17 -13
- package/lib/use-standard-checkout.d.ts.map +1 -1
- package/lib/use-standard-checkout.js +281 -207
- package/lib/utils/analytics.d.ts +177 -51
- package/lib/utils/analytics.d.ts.map +1 -1
- package/lib/utils/analytics.js +299 -81
- package/lib/utils/device-compliance.d.ts +57 -12
- package/lib/utils/device-compliance.d.ts.map +1 -1
- package/lib/utils/device-compliance.js +101 -37
- package/lib/utils/event-projections.d.ts +56 -0
- package/lib/utils/event-projections.d.ts.map +1 -0
- package/lib/utils/event-projections.js +42 -0
- package/lib/utils/file-metadata.d.ts +52 -0
- package/lib/utils/file-metadata.d.ts.map +1 -0
- package/lib/utils/file-metadata.js +134 -0
- package/lib/utils/use-sdk-session.d.ts +12 -0
- package/lib/utils/use-sdk-session.d.ts.map +1 -0
- package/lib/utils/use-sdk-session.js +37 -0
- package/lib/utils/validation.d.ts +37 -3
- package/lib/utils/validation.d.ts.map +1 -1
- package/lib/utils/validation.js +64 -8
- package/lib/webview/capability-props.d.ts +27 -0
- package/lib/webview/capability-props.d.ts.map +1 -0
- package/lib/webview/capability-props.js +61 -0
- package/lib/webview/use-permission-request.d.ts +33 -0
- package/lib/webview/use-permission-request.d.ts.map +1 -0
- package/lib/webview/use-permission-request.js +89 -0
- package/package.json +10 -2
- package/src/bridge/file-input-handler.ts +66 -0
- package/src/bridge/file-save-handler.ts +141 -0
- package/src/bridge/message-router.ts +92 -0
- package/src/bridge/payment-payload.ts +31 -0
- package/src/capabilities/file-save.ts +124 -0
- package/src/capabilities/index.ts +34 -0
- package/src/capabilities/use-capability-reporting.ts +55 -0
- package/src/config/base.ts +13 -0
- package/src/glomo-checkout.tsx +11 -7
- package/src/glomo-lrs-checkout.tsx +41 -26
- package/src/glomo-standard-checkout.tsx +31 -34
- package/src/glomo-subscriptions-checkout.tsx +1 -0
- package/src/index.ts +4 -2
- package/src/injections/index.ts +43 -0
- package/src/injections/webview-flow.injection.ts +76 -0
- package/src/injections/webview-main.injection.ts +31 -0
- package/src/injections/webview-standard.injection.ts +31 -0
- package/src/services/file-saver.ts +497 -0
- package/src/types/checkout.ts +62 -6
- package/src/types/standard-checkout.ts +6 -6
- package/src/types/subscriptions-checkout.ts +7 -3
- package/src/use-glomo-checkout.tsx +2 -2
- package/src/use-lrs-checkout.tsx +656 -365
- package/src/use-standard-checkout.tsx +728 -466
- package/src/utils/analytics.ts +422 -117
- package/src/utils/device-compliance.ts +148 -38
- package/src/utils/event-projections.ts +74 -0
- package/src/utils/file-metadata.ts +140 -0
- package/src/utils/use-sdk-session.ts +41 -0
- package/src/utils/validation.ts +70 -10
- package/src/webview/capability-props.ts +86 -0
- package/src/webview/use-permission-request.ts +114 -0
- 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
|
+
}
|
package/src/config/base.ts
CHANGED
|
@@ -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;
|
package/src/glomo-checkout.tsx
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The unified GlomoPay Checkout Component
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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<
|
|
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?:
|
|
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
|
-
[
|
|
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
|
-
|
|
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,
|