@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.
- 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 +9 -1
- package/lib/glomo-checkout.d.ts.map +1 -1
- package/lib/glomo-checkout.js +14 -5
- package/lib/glomo-lrs-checkout.d.ts +3 -0
- package/lib/glomo-lrs-checkout.d.ts.map +1 -1
- package/lib/glomo-lrs-checkout.js +34 -11
- package/lib/glomo-standard-checkout.d.ts +4 -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 +67 -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 +5 -3
- package/lib/types/subscriptions-checkout.d.ts.map +1 -1
- package/lib/use-glomo-checkout.d.ts +2 -2
- package/lib/use-glomo-checkout.d.ts.map +1 -1
- package/lib/use-glomo-checkout.js +25 -4
- package/lib/use-lrs-checkout.d.ts +18 -6
- package/lib/use-lrs-checkout.d.ts.map +1 -1
- package/lib/use-lrs-checkout.js +330 -171
- package/lib/use-standard-checkout.d.ts +18 -13
- package/lib/use-standard-checkout.d.ts.map +1 -1
- package/lib/use-standard-checkout.js +282 -206
- 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 +13 -3
- 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 +28 -11
- package/src/glomo-lrs-checkout.tsx +49 -27
- package/src/glomo-standard-checkout.tsx +33 -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 +72 -6
- package/src/types/standard-checkout.ts +6 -6
- package/src/types/subscriptions-checkout.ts +9 -3
- package/src/use-glomo-checkout.tsx +32 -7
- package/src/use-lrs-checkout.tsx +684 -366
- package/src/use-standard-checkout.tsx +731 -465
- 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
|
@@ -206,6 +206,37 @@ exports.injectedScript = `
|
|
|
206
206
|
});
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* File-input click detection.
|
|
212
|
+
*
|
|
213
|
+
* Does not open the picker - react-native-webview does that natively through
|
|
214
|
+
* onShowFileChooser, and the SDK never sees the rest of it. This carries the only record
|
|
215
|
+
* that an upload was attempted, and on iOS it is the only file signal at all, since
|
|
216
|
+
* WKWebView runs its own picker and reports nothing back.
|
|
217
|
+
*
|
|
218
|
+
* Capture phase, so a page that stops propagation on its own handler cannot silence it.
|
|
219
|
+
*
|
|
220
|
+
* Posts the shape of the input and nothing that identifies it. The id and name of a KYC
|
|
221
|
+
* upload control are third-party identity, and unconsumed page data sitting on the bridge is
|
|
222
|
+
* how identity ends up spread into a tracker by someone who never read this.
|
|
223
|
+
*/
|
|
224
|
+
document.addEventListener('click', function(event) {
|
|
225
|
+
try {
|
|
226
|
+
const target = event.target;
|
|
227
|
+
if (!target || target.tagName !== 'INPUT' || target.type !== 'file') {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
safePostMessage({
|
|
231
|
+
type: 'file.input',
|
|
232
|
+
accept: target.getAttribute('accept') || '',
|
|
233
|
+
hasCapture: target.hasAttribute('capture')
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Never let this break a bank's own upload handling
|
|
237
|
+
}
|
|
238
|
+
}, true);
|
|
239
|
+
|
|
209
240
|
// Logging that injection is complete
|
|
210
241
|
console.log(LOG_PREFIX + 'GlomoPay WebView JavaScript injection complete');
|
|
211
242
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches a document and offers it to the user through the platform save dialog
|
|
3
|
+
*
|
|
4
|
+
* This is the whole of the SDK's answer to a `file.save` bridge message. Nothing here is reachable
|
|
5
|
+
* from the merchant API - the bridge handler is the only caller.
|
|
6
|
+
*
|
|
7
|
+
* Exists because there is no web-only way to put a file on the device from inside a WebView:
|
|
8
|
+
* Android drops `anchor[download]` on a `blob:` URL unless the host registers a DownloadListener,
|
|
9
|
+
* and the Web Share API is absent from Android WebView entirely. The page sends the message only
|
|
10
|
+
* after finding `file.save` in the capability list the injection scripts publish.
|
|
11
|
+
*/
|
|
12
|
+
import { type SdkError } from "../utils/analytics";
|
|
13
|
+
/**
|
|
14
|
+
* How a save attempt ended.
|
|
15
|
+
*
|
|
16
|
+
* Named for the condition rather than carrying a message, for the same reason FileSaveResult
|
|
17
|
+
* carries no filename: every value here reaches telemetry.
|
|
18
|
+
*
|
|
19
|
+
* Values are camelCase where this repo's other string enums are snake_case, and the deviation is
|
|
20
|
+
* deliberate. ASYNC_PAYMENT_EVENTS is merchant-facing API surface, so it follows the SDK's own
|
|
21
|
+
* convention; this one is telemetry that has to join against the Flutter SDK's already-shipped
|
|
22
|
+
* `file.save_completed`, where the binding constraint is that `outcome` reads the same on both.
|
|
23
|
+
*/
|
|
24
|
+
export declare enum FILE_SAVE_OUTCOMES {
|
|
25
|
+
/** The user chose a destination and the bytes were written to it. */
|
|
26
|
+
SAVED = "saved",
|
|
27
|
+
/** The save dialog was dismissed without choosing one. A normal user action, not a failure. */
|
|
28
|
+
DISMISSED = "dismissed",
|
|
29
|
+
/** The URL failed the scheme check, so no request was made. */
|
|
30
|
+
REJECTED = "rejected",
|
|
31
|
+
/** The document could not be fetched: stall, transport, or non-200. */
|
|
32
|
+
FETCH_FAILED = "fetchFailed",
|
|
33
|
+
/** Larger than FILE_SAVE_MAX_BYTES. Refused mid-stream, so the excess never reached disk. */
|
|
34
|
+
TOO_LARGE = "tooLarge",
|
|
35
|
+
/** The platform refused to present the save dialog, or failed writing to the chosen target. */
|
|
36
|
+
SAVE_FAILED = "saveFailed",
|
|
37
|
+
/**
|
|
38
|
+
* The optional save packages are not usable in this build, so the message should not have been
|
|
39
|
+
* sent at all.
|
|
40
|
+
*
|
|
41
|
+
* No Flutter counterpart - there the two packages are hard dependencies and this state cannot
|
|
42
|
+
* exist. It is reachable here whenever a page sends `file.save` without gating on the capability
|
|
43
|
+
* list, which is the failure this outcome exists to make visible rather than silent.
|
|
44
|
+
*/
|
|
45
|
+
UNAVAILABLE = "unavailable"
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Outcome of one save, shaped for telemetry.
|
|
49
|
+
*
|
|
50
|
+
* Carries no filename and no URL. The agreement URLs are signed links - a signed link is a bearer
|
|
51
|
+
* credential, and the filename is the same class of data file-metadata exists to keep out of events.
|
|
52
|
+
* Extension and byte count are what a field investigation actually needs.
|
|
53
|
+
*/
|
|
54
|
+
export interface FileSaveResult {
|
|
55
|
+
outcome: FILE_SAVE_OUTCOMES;
|
|
56
|
+
/** From extensionOf, so it is a known extension or a sentinel. */
|
|
57
|
+
fileExtension: string;
|
|
58
|
+
/** SIZE_UNAVAILABLE when the document was never fetched. */
|
|
59
|
+
fileSize: number;
|
|
60
|
+
/** Error name or code, never a message - RN network errors interpolate the full URL. */
|
|
61
|
+
failureType?: string;
|
|
62
|
+
}
|
|
63
|
+
/** The options for saveDocumentToDevice */
|
|
64
|
+
export interface SaveDocumentOptions {
|
|
65
|
+
url: string;
|
|
66
|
+
filename?: string;
|
|
67
|
+
devMode?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The largest document this will put on the device.
|
|
71
|
+
*
|
|
72
|
+
* Held at the Flutter SDK's figure so a merchant does not see one platform refuse what the other
|
|
73
|
+
* accepts, but the reason differs and that matters if either is ever revisited. Flutter is bounded
|
|
74
|
+
* by memory: `FilePicker.saveFile` takes bytes, so the whole document is materialised before the
|
|
75
|
+
* dialog. Here nothing is ever held in memory - blob-util streams to disk and the picker takes a
|
|
76
|
+
* URI - so this bounds device storage and the user's patience instead.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately not justified by agreement PDFs being small. `file.save` is generic device-save
|
|
79
|
+
* capability, reached by any bank or segment, so a response far past a typical agreement is an
|
|
80
|
+
* ordinary case rather than evidence of a misrouted URL.
|
|
81
|
+
*/
|
|
82
|
+
export declare const FILE_SAVE_MAX_BYTES: number;
|
|
83
|
+
/**
|
|
84
|
+
* True for outcomes that mean the user did not get the document.
|
|
85
|
+
*
|
|
86
|
+
* Dismissal is excluded: the user was offered the file and declined it.
|
|
87
|
+
*/
|
|
88
|
+
export declare function isFileSaveFailure(result: FileSaveResult): boolean;
|
|
89
|
+
/** The telemetry projection of a result. Everything here is safe to send; the inputs are not. */
|
|
90
|
+
export declare function toFileSaveEventData(result: FileSaveResult): Record<string, unknown>;
|
|
91
|
+
/**
|
|
92
|
+
* The host-facing form of a failure.
|
|
93
|
+
*
|
|
94
|
+
* `validation_error` rather than anything terminal because onSdkError is also the bridge's generic
|
|
95
|
+
* catch - without a type these arrive mixed with parse failures and a merchant cannot filter them
|
|
96
|
+
* apart. `field` names the message that produced it for the same reason.
|
|
97
|
+
*
|
|
98
|
+
* Must never be routed anywhere that sets checkout status: a document failing to save must not end
|
|
99
|
+
* the customer's payment session.
|
|
100
|
+
*/
|
|
101
|
+
export declare function toFileSaveSdkError(result: FileSaveResult): SdkError;
|
|
102
|
+
/**
|
|
103
|
+
* Fetches the document at `url` and offers it to the user.
|
|
104
|
+
*
|
|
105
|
+
* Never throws: every failure is an outcome, because the caller is a bridge message handler whose
|
|
106
|
+
* only other option is to drop the user's tap silently.
|
|
107
|
+
*/
|
|
108
|
+
export declare function saveDocumentToDevice({ url, filename, devMode, }: SaveDocumentOptions): Promise<FileSaveResult>;
|
|
109
|
+
//# sourceMappingURL=file-saver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-saver.d.ts","sourceRoot":"","sources":["../../src/services/file-saver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAKnD;;;;;;;;;;GAUG;AACH,oBAAY,kBAAkB;IAC1B,qEAAqE;IACrE,KAAK,UAAU;IAEf,+FAA+F;IAC/F,SAAS,cAAc;IAEvB,+DAA+D;IAC/D,QAAQ,aAAa;IAErB,uEAAuE;IACvE,YAAY,gBAAgB;IAE5B,6FAA6F;IAC7F,SAAS,aAAa;IAEtB,+FAA+F;IAC/F,WAAW,eAAe;IAE1B;;;;;;;OAOG;IACH,WAAW,gBAAgB;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,kBAAkB,CAAC;IAE5B,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;IAEtB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IAEjB,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,QAAoB,CAAC;AAuCrD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAEjE;AAED,iGAAiG;AACjG,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CASnE;AAuED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,EACvC,GAAG,EACH,QAAQ,EACR,OAAe,GAClB,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CA+M/C"}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fetches a document and offers it to the user through the platform save dialog
|
|
4
|
+
*
|
|
5
|
+
* This is the whole of the SDK's answer to a `file.save` bridge message. Nothing here is reachable
|
|
6
|
+
* from the merchant API - the bridge handler is the only caller.
|
|
7
|
+
*
|
|
8
|
+
* Exists because there is no web-only way to put a file on the device from inside a WebView:
|
|
9
|
+
* Android drops `anchor[download]` on a `blob:` URL unless the host registers a DownloadListener,
|
|
10
|
+
* and the Web Share API is absent from Android WebView entirely. The page sends the message only
|
|
11
|
+
* after finding `file.save` in the capability list the injection scripts publish.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.FILE_SAVE_MAX_BYTES = exports.FILE_SAVE_OUTCOMES = void 0;
|
|
15
|
+
exports.isFileSaveFailure = isFileSaveFailure;
|
|
16
|
+
exports.toFileSaveEventData = toFileSaveEventData;
|
|
17
|
+
exports.toFileSaveSdkError = toFileSaveSdkError;
|
|
18
|
+
exports.saveDocumentToDevice = saveDocumentToDevice;
|
|
19
|
+
const file_save_1 = require("../capabilities/file-save");
|
|
20
|
+
const file_metadata_1 = require("../utils/file-metadata");
|
|
21
|
+
/** Log prefix. Deliberately unqualified by flow - a save is not owned by one. */
|
|
22
|
+
const LOG_PREFIX = "[Glomo-RN-SDK]";
|
|
23
|
+
/**
|
|
24
|
+
* How a save attempt ended.
|
|
25
|
+
*
|
|
26
|
+
* Named for the condition rather than carrying a message, for the same reason FileSaveResult
|
|
27
|
+
* carries no filename: every value here reaches telemetry.
|
|
28
|
+
*
|
|
29
|
+
* Values are camelCase where this repo's other string enums are snake_case, and the deviation is
|
|
30
|
+
* deliberate. ASYNC_PAYMENT_EVENTS is merchant-facing API surface, so it follows the SDK's own
|
|
31
|
+
* convention; this one is telemetry that has to join against the Flutter SDK's already-shipped
|
|
32
|
+
* `file.save_completed`, where the binding constraint is that `outcome` reads the same on both.
|
|
33
|
+
*/
|
|
34
|
+
var FILE_SAVE_OUTCOMES;
|
|
35
|
+
(function (FILE_SAVE_OUTCOMES) {
|
|
36
|
+
/** The user chose a destination and the bytes were written to it. */
|
|
37
|
+
FILE_SAVE_OUTCOMES["SAVED"] = "saved";
|
|
38
|
+
/** The save dialog was dismissed without choosing one. A normal user action, not a failure. */
|
|
39
|
+
FILE_SAVE_OUTCOMES["DISMISSED"] = "dismissed";
|
|
40
|
+
/** The URL failed the scheme check, so no request was made. */
|
|
41
|
+
FILE_SAVE_OUTCOMES["REJECTED"] = "rejected";
|
|
42
|
+
/** The document could not be fetched: stall, transport, or non-200. */
|
|
43
|
+
FILE_SAVE_OUTCOMES["FETCH_FAILED"] = "fetchFailed";
|
|
44
|
+
/** Larger than FILE_SAVE_MAX_BYTES. Refused mid-stream, so the excess never reached disk. */
|
|
45
|
+
FILE_SAVE_OUTCOMES["TOO_LARGE"] = "tooLarge";
|
|
46
|
+
/** The platform refused to present the save dialog, or failed writing to the chosen target. */
|
|
47
|
+
FILE_SAVE_OUTCOMES["SAVE_FAILED"] = "saveFailed";
|
|
48
|
+
/**
|
|
49
|
+
* The optional save packages are not usable in this build, so the message should not have been
|
|
50
|
+
* sent at all.
|
|
51
|
+
*
|
|
52
|
+
* No Flutter counterpart - there the two packages are hard dependencies and this state cannot
|
|
53
|
+
* exist. It is reachable here whenever a page sends `file.save` without gating on the capability
|
|
54
|
+
* list, which is the failure this outcome exists to make visible rather than silent.
|
|
55
|
+
*/
|
|
56
|
+
FILE_SAVE_OUTCOMES["UNAVAILABLE"] = "unavailable";
|
|
57
|
+
})(FILE_SAVE_OUTCOMES || (exports.FILE_SAVE_OUTCOMES = FILE_SAVE_OUTCOMES = {}));
|
|
58
|
+
/**
|
|
59
|
+
* The largest document this will put on the device.
|
|
60
|
+
*
|
|
61
|
+
* Held at the Flutter SDK's figure so a merchant does not see one platform refuse what the other
|
|
62
|
+
* accepts, but the reason differs and that matters if either is ever revisited. Flutter is bounded
|
|
63
|
+
* by memory: `FilePicker.saveFile` takes bytes, so the whole document is materialised before the
|
|
64
|
+
* dialog. Here nothing is ever held in memory - blob-util streams to disk and the picker takes a
|
|
65
|
+
* URI - so this bounds device storage and the user's patience instead.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately not justified by agreement PDFs being small. `file.save` is generic device-save
|
|
68
|
+
* capability, reached by any bank or segment, so a response far past a typical agreement is an
|
|
69
|
+
* ordinary case rather than evidence of a misrouted URL.
|
|
70
|
+
*/
|
|
71
|
+
exports.FILE_SAVE_MAX_BYTES = 100 * 1024 * 1024;
|
|
72
|
+
/** Matches the Flutter SDK's fetch timeout, and blob-util applies it to connect only (see below). */
|
|
73
|
+
const CONNECT_TIMEOUT_MS = 30000;
|
|
74
|
+
/**
|
|
75
|
+
* How long a started download may go without delivering bytes before it is abandoned.
|
|
76
|
+
*
|
|
77
|
+
* Needed because blob-util sets `readTimeout(0)` for file-to-disk responses on Android - deliberate
|
|
78
|
+
* on their side, so a slow but healthy transfer of a large file is not killed by OkHttp's 60s
|
|
79
|
+
* default. The consequence is that a genuinely wedged socket never resolves, and the user is left
|
|
80
|
+
* on a button that already reported success.
|
|
81
|
+
*
|
|
82
|
+
* A stall bound rather than a total bound: a total timeout large enough for FILE_SAVE_MAX_BYTES on
|
|
83
|
+
* a slow connection is too large to catch anything, and one small enough to catch a stall would
|
|
84
|
+
* fail legitimate large downloads. The Flutter SDK applies a 30s total, which is the tradeoff worth
|
|
85
|
+
* revisiting there now that its own cap is 100 MB.
|
|
86
|
+
*/
|
|
87
|
+
const STALL_TIMEOUT_MS = 30000;
|
|
88
|
+
/** Only the basename survives, and only from an allowlist */
|
|
89
|
+
const UNSAFE_CHARS = /[^A-Za-z0-9._-]/g;
|
|
90
|
+
/** Both sides of this protocol independently default to this, which is why collisions are designed out */
|
|
91
|
+
const DEFAULT_FILENAME = "document.pdf";
|
|
92
|
+
/** Reported when a thrown value carries neither a code nor a name */
|
|
93
|
+
const UNKNOWN_ERROR_TYPE = "UNKNOWN_ERROR_TYPE";
|
|
94
|
+
/** Distinguishes one save's staging directory from another's. Saves are not serialised. */
|
|
95
|
+
let stagingCounter = 0;
|
|
96
|
+
/**
|
|
97
|
+
* True for outcomes that mean the user did not get the document.
|
|
98
|
+
*
|
|
99
|
+
* Dismissal is excluded: the user was offered the file and declined it.
|
|
100
|
+
*/
|
|
101
|
+
function isFileSaveFailure(result) {
|
|
102
|
+
return result.outcome !== FILE_SAVE_OUTCOMES.SAVED && result.outcome !== FILE_SAVE_OUTCOMES.DISMISSED;
|
|
103
|
+
}
|
|
104
|
+
/** The telemetry projection of a result. Everything here is safe to send; the inputs are not. */
|
|
105
|
+
function toFileSaveEventData(result) {
|
|
106
|
+
return {
|
|
107
|
+
outcome: result.outcome,
|
|
108
|
+
fileExtension: result.fileExtension,
|
|
109
|
+
fileSize: result.fileSize,
|
|
110
|
+
...(result.failureType ? { failureType: result.failureType } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The host-facing form of a failure.
|
|
115
|
+
*
|
|
116
|
+
* `validation_error` rather than anything terminal because onSdkError is also the bridge's generic
|
|
117
|
+
* catch - without a type these arrive mixed with parse failures and a merchant cannot filter them
|
|
118
|
+
* apart. `field` names the message that produced it for the same reason.
|
|
119
|
+
*
|
|
120
|
+
* Must never be routed anywhere that sets checkout status: a document failing to save must not end
|
|
121
|
+
* the customer's payment session.
|
|
122
|
+
*/
|
|
123
|
+
function toFileSaveSdkError(result) {
|
|
124
|
+
return {
|
|
125
|
+
type: "validation_error",
|
|
126
|
+
message: result.outcome === FILE_SAVE_OUTCOMES.TOO_LARGE
|
|
127
|
+
? `Downloads above ${Math.floor(exports.FILE_SAVE_MAX_BYTES / (1024 * 1024))} MB are not supported.`
|
|
128
|
+
: "Could not save the document to your device.",
|
|
129
|
+
field: "file.save",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* https only, except on loopback.
|
|
134
|
+
*
|
|
135
|
+
* isValidUrl admits any http URL, which is right for bank redirects and wrong here: this fetch
|
|
136
|
+
* carries a signed link and its bytes become a document the user is told is theirs. The loopback
|
|
137
|
+
* exception is what lets the checkout page's dev mock server serve agreements over http.
|
|
138
|
+
*
|
|
139
|
+
* Gated on `devMode`, so a release build is https-only with no exception at all. Ungated, a page
|
|
140
|
+
* could point a production build at a service listening on the customer's own device and have the
|
|
141
|
+
* response written to their files - a dev convenience is not worth a hole in the one rule this
|
|
142
|
+
* function exists to enforce.
|
|
143
|
+
*
|
|
144
|
+
* Loopback only, and deliberately not the Android emulator's `10.0.2.2` alias for the host:
|
|
145
|
+
* reaching a dev server from an emulator is what `adb reverse` is for, which keeps the host
|
|
146
|
+
* `localhost` on both platforms. No other private address belongs in shipped code.
|
|
147
|
+
*/
|
|
148
|
+
function isFetchableUrl(url, devMode) {
|
|
149
|
+
var _a, _b, _c;
|
|
150
|
+
const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]+)/.exec(url);
|
|
151
|
+
if (!match) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const scheme = match[1].toLowerCase();
|
|
155
|
+
// Host only - a userinfo or port section must not be mistaken for one
|
|
156
|
+
const host = (_c = (_b = (_a = match[2].split("@").pop()) === null || _a === void 0 ? void 0 : _a.split(":")[0]) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : "";
|
|
157
|
+
if (scheme === "https") {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return devMode && scheme === "http" && (host === "localhost" || host === "127.0.0.1");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Basename, allowlisted, with a `.pdf` default.
|
|
164
|
+
*
|
|
165
|
+
* The page supplies this and it becomes a path segment under the staging directory, so it is
|
|
166
|
+
* untrusted input: `../../databases/x` would otherwise escape. Falls back to the URL's last
|
|
167
|
+
* segment, then to a constant.
|
|
168
|
+
*/
|
|
169
|
+
function sanitize(name) {
|
|
170
|
+
var _a, _b, _c;
|
|
171
|
+
if (!name) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const base = (_c = (_b = (_a = name.split("/").pop()) === null || _a === void 0 ? void 0 : _a.split("\\").pop()) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : "";
|
|
175
|
+
const cleaned = base.replace(UNSAFE_CHARS, "_");
|
|
176
|
+
// A name that is only dots and separators sanitises to something like `..`, still a traversal
|
|
177
|
+
if (cleaned.length === 0 || cleaned.replace(/\./g, "").length === 0) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return cleaned.length > 100 ? cleaned.slice(cleaned.length - 100) : cleaned;
|
|
181
|
+
}
|
|
182
|
+
/** Prefers what the page asked for, then what the URL implies, then a constant. */
|
|
183
|
+
function resolveFilename(supplied, url) {
|
|
184
|
+
const path = url.split("?")[0].split("#")[0];
|
|
185
|
+
const fromUrl = path.split("/").pop();
|
|
186
|
+
for (const candidate of [supplied, fromUrl]) {
|
|
187
|
+
const cleaned = sanitize(candidate);
|
|
188
|
+
if (cleaned) {
|
|
189
|
+
return cleaned;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return DEFAULT_FILENAME;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Fetches the document at `url` and offers it to the user.
|
|
196
|
+
*
|
|
197
|
+
* Never throws: every failure is an outcome, because the caller is a bridge message handler whose
|
|
198
|
+
* only other option is to drop the user's tap silently.
|
|
199
|
+
*/
|
|
200
|
+
async function saveDocumentToDevice({ url, filename, devMode = false, }) {
|
|
201
|
+
var _a, _b;
|
|
202
|
+
/**
|
|
203
|
+
* Resolved before every other check so a rejection still reports the real extension rather than
|
|
204
|
+
* the EXTRACTION_FAILED sentinel, which means something else entirely. Neither call touches the
|
|
205
|
+
* network or the filesystem.
|
|
206
|
+
*/
|
|
207
|
+
const safeName = resolveFilename(filename, url);
|
|
208
|
+
const fileExtension = (0, file_metadata_1.extensionOf)(safeName);
|
|
209
|
+
const failed = (outcome, failureType, fileSize = file_metadata_1.SIZE_UNAVAILABLE) => ({
|
|
210
|
+
outcome,
|
|
211
|
+
fileExtension,
|
|
212
|
+
fileSize,
|
|
213
|
+
...(failureType ? { failureType } : {}),
|
|
214
|
+
});
|
|
215
|
+
if (!isFetchableUrl(url, devMode)) {
|
|
216
|
+
if (devMode) {
|
|
217
|
+
console.log(`${LOG_PREFIX} file.save rejected: unsupported URL scheme`);
|
|
218
|
+
}
|
|
219
|
+
return failed(FILE_SAVE_OUTCOMES.REJECTED);
|
|
220
|
+
}
|
|
221
|
+
const blobUtil = (0, file_save_1.loadBlobUtil)();
|
|
222
|
+
const picker = (0, file_save_1.loadDocumentPicker)();
|
|
223
|
+
if (!blobUtil || !picker) {
|
|
224
|
+
// Reachable only from a page that sent the message without gating on the capability list
|
|
225
|
+
if (devMode) {
|
|
226
|
+
console.warn(`${LOG_PREFIX} file.save received but the save packages are not usable in this build`);
|
|
227
|
+
}
|
|
228
|
+
return failed(FILE_SAVE_OUTCOMES.UNAVAILABLE);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A directory this SDK owns, unique per save.
|
|
232
|
+
*
|
|
233
|
+
* The Flutter SDK has to hunt for a free name because `file_picker` stages into the app
|
|
234
|
+
* documents directory under the page-chosen name and deletes whatever is already there. Nothing
|
|
235
|
+
* equivalent applies here: the destination path is ours to choose, so a page-supplied name can
|
|
236
|
+
* only ever land inside this directory, and two concurrent saves of the same name cannot meet.
|
|
237
|
+
* The name still has to survive `sanitize` because it is a path segment either way - and on iOS
|
|
238
|
+
* it is also the name the dialog pre-fills, which is taken from the source URI.
|
|
239
|
+
*/
|
|
240
|
+
const stagingDirectory = `${blobUtil.fs.dirs.CacheDir}/glomopay-file-save/${Date.now()}-${++stagingCounter}`;
|
|
241
|
+
const stagingPath = `${stagingDirectory}/${safeName}`;
|
|
242
|
+
let cancelReason = null;
|
|
243
|
+
let stallTimer;
|
|
244
|
+
try {
|
|
245
|
+
await blobUtil.fs.mkdir(stagingDirectory);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return failed(FILE_SAVE_OUTCOMES.SAVE_FAILED, errorTypeOf(error));
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const task = blobUtil
|
|
252
|
+
.config({ path: stagingPath, timeout: CONNECT_TIMEOUT_MS, overwrite: true })
|
|
253
|
+
.fetch("GET", url);
|
|
254
|
+
const cancelWith = (reason) => {
|
|
255
|
+
if (cancelReason) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
cancelReason = reason;
|
|
259
|
+
task.cancel();
|
|
260
|
+
};
|
|
261
|
+
const armStallTimer = () => {
|
|
262
|
+
if (stallTimer) {
|
|
263
|
+
clearTimeout(stallTimer);
|
|
264
|
+
}
|
|
265
|
+
stallTimer = setTimeout(() => cancelWith({ outcome: FILE_SAVE_OUTCOMES.FETCH_FAILED, failureType: "stalled" }), STALL_TIMEOUT_MS);
|
|
266
|
+
};
|
|
267
|
+
armStallTimer();
|
|
268
|
+
task.progress({ count: -1, interval: 250 }, (received, total) => {
|
|
269
|
+
armStallTimer();
|
|
270
|
+
/**
|
|
271
|
+
* Content-Length first when the server sends one, so an oversized response is refused
|
|
272
|
+
* before any of it reaches disk; the running total is the backstop for chunked
|
|
273
|
+
* responses that send none.
|
|
274
|
+
*/
|
|
275
|
+
const advertised = total > 0 ? total : file_metadata_1.SIZE_UNAVAILABLE;
|
|
276
|
+
if (advertised > exports.FILE_SAVE_MAX_BYTES || received > exports.FILE_SAVE_MAX_BYTES) {
|
|
277
|
+
cancelWith({
|
|
278
|
+
outcome: FILE_SAVE_OUTCOMES.TOO_LARGE,
|
|
279
|
+
size: advertised > 0 ? advertised : received,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
const response = await task;
|
|
284
|
+
/**
|
|
285
|
+
* Disarmed here rather than only in `finally`, because `finally` does not run until the
|
|
286
|
+
* picker has resolved and the user may sit in it for minutes choosing a folder. A timer
|
|
287
|
+
* left armed across that would fire on a settled request - harmless, since blob-util
|
|
288
|
+
* replaces `cancel` with a no-op once the promise settles, but it is a live timer holding
|
|
289
|
+
* this closure for no reason.
|
|
290
|
+
*/
|
|
291
|
+
if (stallTimer) {
|
|
292
|
+
clearTimeout(stallTimer);
|
|
293
|
+
stallTimer = undefined;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* A cancel we asked for does not always reject. blob-util makes `cancel` a no-op once the
|
|
297
|
+
* promise has settled, so a response that crosses the cap and finishes inside the 250ms
|
|
298
|
+
* before the next progress tick resolves normally with the oversized body already on disk.
|
|
299
|
+
* Checked here as well as in the catch, or the cap silently does not apply in the one case
|
|
300
|
+
* it exists for - a chunked response whose Content-Length could not refuse it up front.
|
|
301
|
+
*/
|
|
302
|
+
if (cancelReason) {
|
|
303
|
+
const reason = cancelReason;
|
|
304
|
+
return failed(reason.outcome, reason.failureType, (_a = reason.size) !== null && _a !== void 0 ? _a : file_metadata_1.SIZE_UNAVAILABLE);
|
|
305
|
+
}
|
|
306
|
+
const info = response.info();
|
|
307
|
+
/**
|
|
308
|
+
* blob-util resolves on any status and writes the body regardless, so an error page is on
|
|
309
|
+
* disk at this point rather than the document
|
|
310
|
+
*/
|
|
311
|
+
if (info.status !== 200) {
|
|
312
|
+
return failed(FILE_SAVE_OUTCOMES.FETCH_FAILED, `http_${info.status}`);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The scheme allowlist only ever saw the URL the page sent, and redirects are followed -
|
|
316
|
+
* OkHttp and NSURLSession both allow an https to http downgrade by default, so a signed
|
|
317
|
+
* link can land the document on a plaintext hop without isFetchableUrl being consulted
|
|
318
|
+
* again. Checking the chain after the fact cannot un-fetch those bytes, but it can decline
|
|
319
|
+
* to hand the user a document that travelled in the clear.
|
|
320
|
+
*
|
|
321
|
+
* The chain entries are full URLs, so they are signed links themselves: read here, never
|
|
322
|
+
* reported. The Flutter SDK has the same gap and no equivalent check.
|
|
323
|
+
*/
|
|
324
|
+
const redirects = info.redirects;
|
|
325
|
+
if (Array.isArray(redirects) &&
|
|
326
|
+
!redirects.every((hop) => typeof hop === "string" && isFetchableUrl(hop, devMode))) {
|
|
327
|
+
if (devMode) {
|
|
328
|
+
console.log(`${LOG_PREFIX} file.save rejected: redirected through an unsupported scheme`);
|
|
329
|
+
}
|
|
330
|
+
return failed(FILE_SAVE_OUTCOMES.REJECTED, "insecure_redirect");
|
|
331
|
+
}
|
|
332
|
+
const fileSize = await sizeOf(blobUtil, stagingPath);
|
|
333
|
+
try {
|
|
334
|
+
/**
|
|
335
|
+
* `copy: false` is iOS-only and is the default - the staged file is moved to where the
|
|
336
|
+
* user chose, which is what keeps a copy from being left behind in the app container.
|
|
337
|
+
* Android always copies and the cleanup below is what handles it there.
|
|
338
|
+
*
|
|
339
|
+
* `mimeType` is derived from the extension rather than forwarded from the page. Android
|
|
340
|
+
* puts it straight onto an `ACTION_CREATE_DOCUMENT` intent, and the page's field is
|
|
341
|
+
* untrusted; the Flutter SDK declines to forward the same field for the same reason.
|
|
342
|
+
*/
|
|
343
|
+
await picker.saveDocuments({
|
|
344
|
+
sourceUris: [encodeURI(`file://${stagingPath}`)],
|
|
345
|
+
fileName: safeName,
|
|
346
|
+
mimeType: (0, file_metadata_1.mimeTypeFor)(fileExtension),
|
|
347
|
+
copy: false,
|
|
348
|
+
});
|
|
349
|
+
return { outcome: FILE_SAVE_OUTCOMES.SAVED, fileExtension, fileSize };
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
if (picker.isErrorWithCode(error) && error.code === picker.errorCodes.OPERATION_CANCELED) {
|
|
353
|
+
return { outcome: FILE_SAVE_OUTCOMES.DISMISSED, fileExtension, fileSize };
|
|
354
|
+
}
|
|
355
|
+
if (devMode) {
|
|
356
|
+
console.error(`${LOG_PREFIX} file.save dialog failed:`, errorTypeOf(error));
|
|
357
|
+
}
|
|
358
|
+
return failed(FILE_SAVE_OUTCOMES.SAVE_FAILED, errorTypeOf(error), fileSize);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
// A cancel this code asked for rejects the fetch exactly as a transport failure does
|
|
363
|
+
if (cancelReason) {
|
|
364
|
+
const reason = cancelReason;
|
|
365
|
+
return failed(reason.outcome, reason.failureType, (_b = reason.size) !== null && _b !== void 0 ? _b : file_metadata_1.SIZE_UNAVAILABLE);
|
|
366
|
+
}
|
|
367
|
+
if (devMode) {
|
|
368
|
+
console.error(`${LOG_PREFIX} file.save fetch failed:`, errorTypeOf(error));
|
|
369
|
+
}
|
|
370
|
+
return failed(FILE_SAVE_OUTCOMES.FETCH_FAILED, errorTypeOf(error));
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
if (stallTimer) {
|
|
374
|
+
clearTimeout(stallTimer);
|
|
375
|
+
}
|
|
376
|
+
// Recursive on both platforms, and a no-op once iOS has moved the file out
|
|
377
|
+
try {
|
|
378
|
+
await blobUtil.fs.unlink(stagingDirectory);
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (devMode) {
|
|
382
|
+
console.error(`${LOG_PREFIX} file.save could not clear the staging directory:`, errorTypeOf(error));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Size of what was staged, or the sentinel. Never fatal - the document is already on disk. */
|
|
388
|
+
async function sizeOf(blobUtil, path) {
|
|
389
|
+
try {
|
|
390
|
+
const stat = await blobUtil.fs.stat(path);
|
|
391
|
+
const size = Number(stat.size);
|
|
392
|
+
return Number.isFinite(size) ? size : file_metadata_1.SIZE_UNAVAILABLE;
|
|
393
|
+
}
|
|
394
|
+
catch (_a) {
|
|
395
|
+
return file_metadata_1.SIZE_UNAVAILABLE;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* The name or code of a thrown value, never its message.
|
|
400
|
+
*
|
|
401
|
+
* RN's network errors and blob-util's own both interpolate the request URL into `message`, which is
|
|
402
|
+
* the signed link. A sentinel rather than an omission, so the field is always present and "could
|
|
403
|
+
* not tell" is something to filter on.
|
|
404
|
+
*/
|
|
405
|
+
function errorTypeOf(error) {
|
|
406
|
+
if (error && typeof error === "object") {
|
|
407
|
+
const candidate = error;
|
|
408
|
+
if (typeof candidate.code === "string" && candidate.code.length > 0) {
|
|
409
|
+
return candidate.code;
|
|
410
|
+
}
|
|
411
|
+
if (typeof candidate.name === "string" && candidate.name.length > 0) {
|
|
412
|
+
return candidate.name;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return UNKNOWN_ERROR_TYPE;
|
|
416
|
+
}
|
package/lib/types/checkout.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
/** Unified
|
|
1
|
+
/** Unified type definitions for GlomoCheckout */
|
|
2
2
|
import { type GlomoServer } from "../config/base";
|
|
3
3
|
import { type SdkError } from "../utils/analytics";
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The payload for a successful or failed payment (unified across LRS and standard)
|
|
6
|
+
*
|
|
7
|
+
* `paymentId` and `signature` are optional as of v5. The checkout page can report a terminal
|
|
8
|
+
* outcome before the backend has resolved them - open banking completes on the bank's side first -
|
|
9
|
+
* and the SDK used to drop those outcomes entirely rather than report an incomplete one.
|
|
10
|
+
*
|
|
11
|
+
* With `strict` on, this lands as a compile error anywhere the value is forwarded to something
|
|
12
|
+
* expecting a `string`, which is the point: the narrowing check is the guardrail, not a nuisance.
|
|
13
|
+
*/
|
|
5
14
|
export interface GlomoCheckoutPayload {
|
|
6
15
|
orderId: string;
|
|
7
|
-
paymentId
|
|
8
|
-
signature
|
|
16
|
+
paymentId?: string;
|
|
17
|
+
signature?: string;
|
|
9
18
|
}
|
|
10
19
|
/**
|
|
11
20
|
* Enum of asynchronous payment events that trigger the
|
|
@@ -24,6 +33,40 @@ export interface GlomoUserJourneyCompletedPayload {
|
|
|
24
33
|
senderAccountNumber?: string;
|
|
25
34
|
transactionReference?: string;
|
|
26
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* How a checkout ended, when it ended without a payment outcome.
|
|
38
|
+
*
|
|
39
|
+
* **Must stay exhaustive by construction.** Any future call site that terminates a checkout adds a
|
|
40
|
+
* member here rather than passing `undefined` or a catch-all - the whole value of "the payload
|
|
41
|
+
* always carries its source" is in the "always", and one lazy call site turns `source` into a field
|
|
42
|
+
* every merchant has to null-check.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately does not include the back chevron over the bank flow. That returns the user to the
|
|
45
|
+
* main checkout with the session intact and the ability to pay, so reporting it as a termination
|
|
46
|
+
* would be false - and for a merchant who cancels the order on terminate it would cancel live
|
|
47
|
+
* orders every time a user backed out of one bank to try another.
|
|
48
|
+
*/
|
|
49
|
+
export declare enum TERMINATION_SOURCES {
|
|
50
|
+
/** The checkout page's own close control, over the bridge as `checkout.closed` */
|
|
51
|
+
CHECKOUT_CLOSED = "checkout_closed",
|
|
52
|
+
/** Android hardware back or the back gesture */
|
|
53
|
+
DEVICE_BACK_BUTTON = "device_back_button",
|
|
54
|
+
/** iOS swipe-down on the pageSheet */
|
|
55
|
+
MODAL_DISMISS = "modal_dismiss"
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The payload for the onPaymentTerminate callback.
|
|
59
|
+
*
|
|
60
|
+
* An object rather than a bare enum because every other callback in this SDK takes one, and because
|
|
61
|
+
* the source alone is not extensible: a later confirmation step would otherwise have to arrive as a
|
|
62
|
+
* second positional parameter, or silently change what an existing value means.
|
|
63
|
+
*
|
|
64
|
+
* The source describes **how** the checkout ended. The **why**, where there is one, already reached
|
|
65
|
+
* the host through the callback it got first.
|
|
66
|
+
*/
|
|
67
|
+
export interface GlomoTerminationPayload {
|
|
68
|
+
source: TERMINATION_SOURCES;
|
|
69
|
+
}
|
|
27
70
|
/** Unified checkout statuses - superset of LRS + standard statuses */
|
|
28
71
|
export type CheckoutStatus = "ready" | "detecting_order_type" | "payment_in_progress" | "payment_successful" | "payment_failed" | "payment_cancelled" | "bank_transfer_submitted" | "pay_via_bank_completed";
|
|
29
72
|
/**
|
|
@@ -35,6 +78,15 @@ export interface GlomoCheckoutRef {
|
|
|
35
78
|
start: () => Promise<boolean>;
|
|
36
79
|
getStatus: () => CheckoutStatus;
|
|
37
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Internal-only props for automated testing
|
|
83
|
+
*/
|
|
84
|
+
export interface GlomoCheckoutInternalProps {
|
|
85
|
+
_testInjectionScript?: string;
|
|
86
|
+
_testCarouselInjectionScript?: string;
|
|
87
|
+
_forceOrderType?: "standard" | "lrs";
|
|
88
|
+
_onEducationCarouselStateChange?: (state: string) => void;
|
|
89
|
+
}
|
|
38
90
|
/** Shared callback and configuration props for the unified checkout component */
|
|
39
91
|
interface GlomoCheckoutBaseProps {
|
|
40
92
|
server?: GlomoServer;
|
|
@@ -42,10 +94,19 @@ interface GlomoCheckoutBaseProps {
|
|
|
42
94
|
onPaymentSuccess: (payload: GlomoCheckoutPayload) => void;
|
|
43
95
|
onPaymentFailure: (payload: GlomoCheckoutPayload) => void;
|
|
44
96
|
onConnectionError?: (error: unknown) => void;
|
|
45
|
-
onPaymentTerminate?: () => void;
|
|
97
|
+
onPaymentTerminate?: (payload: GlomoTerminationPayload) => void;
|
|
46
98
|
onSdkError: (error: Array<SdkError>) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Optional callback for asynchronous payment journeys - bank transfers and pay via bank.
|
|
101
|
+
* Carries the journey type and the transfer details the page reported, not a confirmed payment.
|
|
102
|
+
*
|
|
103
|
+
* Accepted here because this component is a union over both flows, but **silently ignored on
|
|
104
|
+
* LRS orders**: LRS never produces an asynchronous journey, so nothing forwards it. If your
|
|
105
|
+
* orders are LRS-only you can leave it out entirely - supplying it is not an error, it simply
|
|
106
|
+
* never fires. Terminal LRS outcomes arrive through onPaymentSuccess and onPaymentFailure.
|
|
107
|
+
*/
|
|
47
108
|
onUserJourneyCompleted?: (payload: GlomoUserJourneyCompletedPayload) => void;
|
|
48
|
-
|
|
109
|
+
onUserRefusedDevicePermissions?: () => void;
|
|
49
110
|
devMode?: boolean;
|
|
50
111
|
}
|
|
51
112
|
/**
|