@orqex/checkout-js 0.0.1
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/chunk-4Q2JETLA.js +37 -0
- package/dist/chunk-4Q2JETLA.js.map +1 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +853 -0
- package/dist/index.js.map +1 -0
- package/dist/orqex.global.js +3 -0
- package/dist/orqex.global.js.map +1 -0
- package/dist/protocol.d.ts +43 -0
- package/dist/protocol.js +3 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
import { successMessage, errorMessage, closeMessage, __publicField, isEmbedMessage } from './chunk-4Q2JETLA.js';
|
|
2
|
+
export { ORQEX_PROTOCOL_VERSION, closeMessage, errorMessage, isEmbedMessage, readyMessage, redirectMessage, resizeMessage, successMessage } from './chunk-4Q2JETLA.js';
|
|
3
|
+
|
|
4
|
+
// src/key.ts
|
|
5
|
+
var PK_PATTERN = /^pk_(live|test)_[A-Za-z0-9]+$/;
|
|
6
|
+
function parsePublishableKey(key) {
|
|
7
|
+
if (typeof key !== "string" || !PK_PATTERN.test(key)) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
'Orqex: a publishable key ("pk_live_\u2026" or "pk_test_\u2026") is required. Never pass a secret key to the browser.'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return { key, livemode: key.startsWith("pk_live_") };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
var DEFAULT_CHECKOUT_BASE_URL = "https://checkout.orqex.com";
|
|
17
|
+
var DEFAULT_API_BASE_URL = "https://api.orqex.com";
|
|
18
|
+
function resolveConfig(publishableKey, options = {}) {
|
|
19
|
+
const { key, livemode } = parsePublishableKey(publishableKey);
|
|
20
|
+
const raw = options.checkoutBaseUrl ?? DEFAULT_CHECKOUT_BASE_URL;
|
|
21
|
+
const checkoutBaseUrl = raw.replace(/\/+$/, "");
|
|
22
|
+
let checkoutOrigin;
|
|
23
|
+
try {
|
|
24
|
+
checkoutOrigin = new URL(checkoutBaseUrl).origin;
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(`Orqex: invalid checkoutBaseUrl "${raw}".`);
|
|
27
|
+
}
|
|
28
|
+
const apiBaseUrl = (options.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
29
|
+
return { publishableKey: key, livemode, checkoutBaseUrl, checkoutOrigin, apiBaseUrl };
|
|
30
|
+
}
|
|
31
|
+
function buildEmbedUrl(config, publicId, parentOrigin) {
|
|
32
|
+
const id = encodeURIComponent(publicId);
|
|
33
|
+
return `${config.checkoutBaseUrl}/${id}?embed=1&origin=${encodeURIComponent(parentOrigin)}`;
|
|
34
|
+
}
|
|
35
|
+
function buildRedirectUrl(config, publicId) {
|
|
36
|
+
return `${config.checkoutBaseUrl}/${encodeURIComponent(publicId)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/embedded.ts
|
|
40
|
+
var SANDBOX = "allow-scripts allow-same-origin allow-forms allow-popups";
|
|
41
|
+
var EmbeddedController = class {
|
|
42
|
+
constructor(deps) {
|
|
43
|
+
__publicField(this, "config");
|
|
44
|
+
__publicField(this, "bus");
|
|
45
|
+
__publicField(this, "win");
|
|
46
|
+
__publicField(this, "navigate");
|
|
47
|
+
__publicField(this, "iframe", null);
|
|
48
|
+
__publicField(this, "listener", null);
|
|
49
|
+
this.config = deps.config;
|
|
50
|
+
this.bus = deps.bus;
|
|
51
|
+
this.win = deps.win ?? window;
|
|
52
|
+
this.navigate = deps.navigate ?? ((url) => this.win.location.assign(url));
|
|
53
|
+
}
|
|
54
|
+
mount(target, options) {
|
|
55
|
+
if (!options.publicId) throw new Error("Orqex: mount requires a publicId.");
|
|
56
|
+
const host = this.resolveHost(target);
|
|
57
|
+
this.unmount();
|
|
58
|
+
const iframe = this.win.document.createElement("iframe");
|
|
59
|
+
iframe.src = buildEmbedUrl(this.config, options.publicId, this.win.location.origin);
|
|
60
|
+
iframe.setAttribute("sandbox", SANDBOX);
|
|
61
|
+
iframe.setAttribute("allow", "payment");
|
|
62
|
+
iframe.setAttribute("title", "Orqex Checkout");
|
|
63
|
+
iframe.style.width = "100%";
|
|
64
|
+
iframe.style.border = "none";
|
|
65
|
+
iframe.style.height = "150px";
|
|
66
|
+
host.appendChild(iframe);
|
|
67
|
+
this.iframe = iframe;
|
|
68
|
+
const listener = (event) => {
|
|
69
|
+
if (event.origin !== this.config.checkoutOrigin) return;
|
|
70
|
+
if (event.source !== iframe.contentWindow) return;
|
|
71
|
+
if (!isEmbedMessage(event.data)) return;
|
|
72
|
+
this.handle(event.data);
|
|
73
|
+
};
|
|
74
|
+
this.win.addEventListener("message", listener);
|
|
75
|
+
this.listener = listener;
|
|
76
|
+
}
|
|
77
|
+
unmount() {
|
|
78
|
+
if (this.listener) {
|
|
79
|
+
this.win.removeEventListener("message", this.listener);
|
|
80
|
+
this.listener = null;
|
|
81
|
+
}
|
|
82
|
+
if (this.iframe) {
|
|
83
|
+
this.iframe.remove();
|
|
84
|
+
this.iframe = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
handle(message) {
|
|
88
|
+
switch (message.type) {
|
|
89
|
+
case "resize":
|
|
90
|
+
if (this.iframe) this.iframe.style.height = `${message.height}px`;
|
|
91
|
+
break;
|
|
92
|
+
case "success":
|
|
93
|
+
this.bus.emit("success", { status: message.status });
|
|
94
|
+
break;
|
|
95
|
+
case "error":
|
|
96
|
+
this.bus.emit("error", { code: message.code, message: message.message });
|
|
97
|
+
break;
|
|
98
|
+
case "close":
|
|
99
|
+
this.bus.emit("close", { status: message.status });
|
|
100
|
+
break;
|
|
101
|
+
case "redirect":
|
|
102
|
+
this.bus.emit("redirect", { url: message.url });
|
|
103
|
+
this.navigate(message.url);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
resolveHost(target) {
|
|
108
|
+
const host = typeof target === "string" ? this.win.document.querySelector(target) : target;
|
|
109
|
+
if (!host) throw new Error(`Orqex: mount target "${String(target)}" not found.`);
|
|
110
|
+
return host;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/event-bus.ts
|
|
115
|
+
var EventBus = class {
|
|
116
|
+
constructor() {
|
|
117
|
+
__publicField(this, "handlers", {
|
|
118
|
+
success: /* @__PURE__ */ new Set(),
|
|
119
|
+
error: /* @__PURE__ */ new Set(),
|
|
120
|
+
redirect: /* @__PURE__ */ new Set(),
|
|
121
|
+
close: /* @__PURE__ */ new Set()
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
on(event, handler) {
|
|
125
|
+
this.handlers[event].add(handler);
|
|
126
|
+
return () => this.off(event, handler);
|
|
127
|
+
}
|
|
128
|
+
off(event, handler) {
|
|
129
|
+
this.handlers[event].delete(handler);
|
|
130
|
+
}
|
|
131
|
+
emit(event, payload) {
|
|
132
|
+
for (const handler of this.handlers[event]) {
|
|
133
|
+
try {
|
|
134
|
+
handler(payload);
|
|
135
|
+
} catch (cause) {
|
|
136
|
+
console.error(`Orqex: "${event}" event handler threw`, cause);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ../engine/src/types/errors.ts
|
|
143
|
+
var CheckoutError = class extends Error {
|
|
144
|
+
constructor(code, message, httpStatus) {
|
|
145
|
+
super(message);
|
|
146
|
+
__publicField(this, "code");
|
|
147
|
+
__publicField(this, "httpStatus");
|
|
148
|
+
this.name = "CheckoutError";
|
|
149
|
+
this.code = code;
|
|
150
|
+
this.httpStatus = httpStatus;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function errorCodeFromStatus(status) {
|
|
154
|
+
switch (status) {
|
|
155
|
+
case 400:
|
|
156
|
+
return "not_open";
|
|
157
|
+
case 404:
|
|
158
|
+
return "not_found";
|
|
159
|
+
case 422:
|
|
160
|
+
return "unprocessable";
|
|
161
|
+
case 429:
|
|
162
|
+
return "rate_limited";
|
|
163
|
+
default:
|
|
164
|
+
return status >= 500 ? "network" : "unknown";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ../engine/src/transport/http-transport.ts
|
|
169
|
+
var HttpTransport = class {
|
|
170
|
+
constructor(options) {
|
|
171
|
+
__publicField(this, "baseUrl");
|
|
172
|
+
__publicField(this, "auth");
|
|
173
|
+
__publicField(this, "locale");
|
|
174
|
+
__publicField(this, "fetchImpl");
|
|
175
|
+
__publicField(this, "timeoutMs");
|
|
176
|
+
const fetchImpl = options.fetchImpl ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
|
|
177
|
+
if (!fetchImpl) {
|
|
178
|
+
throw new CheckoutError("unknown", "No fetch implementation available; pass options.fetchImpl.");
|
|
179
|
+
}
|
|
180
|
+
if (!options.auth?.publishableKey || !options.auth?.publicId) {
|
|
181
|
+
throw new CheckoutError("unknown", "HttpTransport requires `auth` with publishableKey and publicId.");
|
|
182
|
+
}
|
|
183
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
184
|
+
this.auth = options.auth;
|
|
185
|
+
this.locale = options.locale;
|
|
186
|
+
this.fetchImpl = fetchImpl;
|
|
187
|
+
this.timeoutMs = options.timeoutMs ?? 2e4;
|
|
188
|
+
}
|
|
189
|
+
show(query) {
|
|
190
|
+
return this.request("GET", this.path(""), {
|
|
191
|
+
query: { country: query?.country, currency: query?.currency }
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
countries() {
|
|
195
|
+
return this.request("GET", this.path("/countries"));
|
|
196
|
+
}
|
|
197
|
+
pay(body) {
|
|
198
|
+
return this.request("POST", this.path("/pay"), { body });
|
|
199
|
+
}
|
|
200
|
+
authorize(body) {
|
|
201
|
+
return this.request("POST", this.path("/authorize"), { body });
|
|
202
|
+
}
|
|
203
|
+
poll() {
|
|
204
|
+
return this.request("GET", this.path("/poll"));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Build the resource path: `/checkout/{publicId}{suffix}`.
|
|
208
|
+
*
|
|
209
|
+
* One surface shared by the hosted page and the SDK (backend PR #105):
|
|
210
|
+
* `GET ''` (show), `POST /pay`, `POST /authorize`, `GET /poll`,
|
|
211
|
+
* `GET /countries`. Scoped to the pk_'s project — a public_id from another
|
|
212
|
+
* project returns 404 (the engine maps that to a terminal not_found).
|
|
213
|
+
*/
|
|
214
|
+
path(suffix) {
|
|
215
|
+
return `/checkout/${encodeURIComponent(this.auth.publicId)}${suffix}`;
|
|
216
|
+
}
|
|
217
|
+
async request(method, path, opts) {
|
|
218
|
+
const url = this.baseUrl + path + this.queryString(opts?.query);
|
|
219
|
+
const headers = { Accept: "application/json" };
|
|
220
|
+
if (this.locale) headers["Accept-Language"] = this.locale;
|
|
221
|
+
headers.Authorization = `Bearer ${this.auth.publishableKey}`;
|
|
222
|
+
if (opts?.body !== void 0) headers["Content-Type"] = "application/json";
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
225
|
+
let response;
|
|
226
|
+
try {
|
|
227
|
+
response = await this.fetchImpl(url, {
|
|
228
|
+
method,
|
|
229
|
+
headers,
|
|
230
|
+
body: opts?.body === void 0 ? void 0 : JSON.stringify(opts.body),
|
|
231
|
+
signal: controller.signal
|
|
232
|
+
});
|
|
233
|
+
} catch (cause) {
|
|
234
|
+
throw new CheckoutError("network", describeNetworkError(cause));
|
|
235
|
+
} finally {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
}
|
|
238
|
+
return this.parse(response);
|
|
239
|
+
}
|
|
240
|
+
async parse(response) {
|
|
241
|
+
const text = await response.text().catch(() => "");
|
|
242
|
+
let json;
|
|
243
|
+
if (text) {
|
|
244
|
+
try {
|
|
245
|
+
json = JSON.parse(text);
|
|
246
|
+
} catch {
|
|
247
|
+
json = void 0;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
const message = extractMessage(json) ?? `Request failed with status ${response.status}`;
|
|
252
|
+
throw new CheckoutError(errorCodeFromStatus(response.status), message, response.status);
|
|
253
|
+
}
|
|
254
|
+
if (json === void 0) {
|
|
255
|
+
throw new CheckoutError("unknown", "Empty or non-JSON response body.", response.status);
|
|
256
|
+
}
|
|
257
|
+
return json;
|
|
258
|
+
}
|
|
259
|
+
queryString(query) {
|
|
260
|
+
if (!query) return "";
|
|
261
|
+
const params = new URLSearchParams();
|
|
262
|
+
for (const [key, value] of Object.entries(query)) {
|
|
263
|
+
if (value !== void 0 && value !== "") params.set(key, value);
|
|
264
|
+
}
|
|
265
|
+
const qs = params.toString();
|
|
266
|
+
return qs ? `?${qs}` : "";
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
function extractMessage(json) {
|
|
270
|
+
if (json && typeof json === "object" && "message" in json) {
|
|
271
|
+
const message = json.message;
|
|
272
|
+
if (typeof message === "string") return message;
|
|
273
|
+
}
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
function describeNetworkError(cause) {
|
|
277
|
+
if (cause instanceof Error && cause.name === "AbortError") return "Request timed out.";
|
|
278
|
+
if (cause instanceof Error) return cause.message;
|
|
279
|
+
return "Network request failed.";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ../engine/src/engine/phase.ts
|
|
283
|
+
function derivePhase(checkout) {
|
|
284
|
+
if (!checkout) return "loading";
|
|
285
|
+
const session = checkout.session.status;
|
|
286
|
+
const intent = checkout.payment?.status;
|
|
287
|
+
const attempt = checkout.attempt?.status;
|
|
288
|
+
if (intent === "completed") return "succeeded";
|
|
289
|
+
if (intent === "failed" || intent === "expired") return "failed";
|
|
290
|
+
if (session !== "open") {
|
|
291
|
+
return session === "completed" ? "succeeded" : "closed";
|
|
292
|
+
}
|
|
293
|
+
if (attempt === "action_required") return "action_required";
|
|
294
|
+
if (attempt === "processing") return "processing";
|
|
295
|
+
return "collecting";
|
|
296
|
+
}
|
|
297
|
+
function isTerminalPhase(phase) {
|
|
298
|
+
return phase === "succeeded" || phase === "failed" || phase === "closed";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ../engine/src/engine/project.ts
|
|
302
|
+
function actionCompletion(action) {
|
|
303
|
+
switch (action.type) {
|
|
304
|
+
case "collect_otp":
|
|
305
|
+
return "submit";
|
|
306
|
+
case "complete_with_sdk":
|
|
307
|
+
return "sdk";
|
|
308
|
+
case "redirect_to_url":
|
|
309
|
+
return "redirect";
|
|
310
|
+
case "embed_iframe":
|
|
311
|
+
case "approve_on_phone":
|
|
312
|
+
case "scan_qr_code":
|
|
313
|
+
case "display_payment_instructions":
|
|
314
|
+
return "poll";
|
|
315
|
+
case "none":
|
|
316
|
+
return "poll";
|
|
317
|
+
default:
|
|
318
|
+
return "poll";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function isActionable(action) {
|
|
322
|
+
return action !== null && action.type !== "none";
|
|
323
|
+
}
|
|
324
|
+
function projectView(state) {
|
|
325
|
+
const { phase, checkout, catalogue } = state;
|
|
326
|
+
if (phase === "loading") {
|
|
327
|
+
return { kind: "loading" };
|
|
328
|
+
}
|
|
329
|
+
if (!checkout) {
|
|
330
|
+
const kind = phase === "succeeded" || phase === "failed" ? phase : "closed";
|
|
331
|
+
return { kind, ctx: result(state, kind) };
|
|
332
|
+
}
|
|
333
|
+
const attempt = checkout.attempt;
|
|
334
|
+
const appearance = state.appearance;
|
|
335
|
+
switch (phase) {
|
|
336
|
+
case "collecting": {
|
|
337
|
+
if (!catalogue) return { kind: "loading" };
|
|
338
|
+
const ctx = {
|
|
339
|
+
amount: catalogue.amount,
|
|
340
|
+
methods: catalogue.methods,
|
|
341
|
+
country: catalogue.country,
|
|
342
|
+
currencies: catalogue.currencies,
|
|
343
|
+
supportsAnyCountry: catalogue.supportsAnyCountry,
|
|
344
|
+
previousPhones: checkout.previous_used_phones ?? []
|
|
345
|
+
};
|
|
346
|
+
if (checkout.restriction) ctx.restriction = checkout.restriction;
|
|
347
|
+
if (checkout.customer) ctx.customer = checkout.customer;
|
|
348
|
+
if (checkout.project) ctx.project = checkout.project;
|
|
349
|
+
if (appearance) ctx.appearance = appearance;
|
|
350
|
+
if (!state.dismissedError) {
|
|
351
|
+
if (state.error) ctx.lastError = state.error;
|
|
352
|
+
if (isFailedAttempt(attempt)) ctx.retryOf = attempt;
|
|
353
|
+
}
|
|
354
|
+
return { kind: "form", ctx };
|
|
355
|
+
}
|
|
356
|
+
case "action_required": {
|
|
357
|
+
const action = attempt?.next_action ?? null;
|
|
358
|
+
if (!attempt || !isActionable(action)) {
|
|
359
|
+
return attempt ? { kind: "processing", ctx: waiting(state, attempt) } : { kind: "loading" };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
kind: "action",
|
|
363
|
+
ctx: {
|
|
364
|
+
action,
|
|
365
|
+
attempt,
|
|
366
|
+
completion: actionCompletion(action),
|
|
367
|
+
...catalogue ? { amount: catalogue.amount } : {},
|
|
368
|
+
...checkout.project ? { project: checkout.project } : {},
|
|
369
|
+
...appearance ? { appearance } : {}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
case "processing": {
|
|
374
|
+
return attempt ? { kind: "processing", ctx: waiting(state, attempt) } : { kind: "loading" };
|
|
375
|
+
}
|
|
376
|
+
case "succeeded":
|
|
377
|
+
return { kind: "succeeded", ctx: result(state, "succeeded") };
|
|
378
|
+
case "failed":
|
|
379
|
+
return { kind: "failed", ctx: result(state, "failed") };
|
|
380
|
+
case "closed":
|
|
381
|
+
return { kind: "closed", ctx: result(state, "closed") };
|
|
382
|
+
default:
|
|
383
|
+
return { kind: "loading" };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function isFailedAttempt(attempt) {
|
|
387
|
+
return attempt != null && (attempt.status === "failed" || attempt.status === "cancelled");
|
|
388
|
+
}
|
|
389
|
+
function waiting(state, attempt) {
|
|
390
|
+
const ctx = { attempt };
|
|
391
|
+
if (state.catalogue) ctx.amount = state.catalogue.amount;
|
|
392
|
+
if (state.checkout?.project) ctx.project = state.checkout.project;
|
|
393
|
+
if (state.appearance) ctx.appearance = state.appearance;
|
|
394
|
+
return ctx;
|
|
395
|
+
}
|
|
396
|
+
function result(state, kind) {
|
|
397
|
+
const checkout = state.checkout;
|
|
398
|
+
const status = kind === "closed" ? checkout?.session.status ?? "expired" : checkout?.payment?.status ?? checkout?.session.status ?? "expired";
|
|
399
|
+
const failureMessage = kind === "failed" ? checkout?.attempt?.failure.message : void 0;
|
|
400
|
+
let message = "";
|
|
401
|
+
if (kind === "failed") message = failureMessage ?? "";
|
|
402
|
+
else if (kind === "succeeded") message = state.message;
|
|
403
|
+
const ctx = { status, message };
|
|
404
|
+
if (checkout?.payment) ctx.payment = checkout.payment;
|
|
405
|
+
if (state.catalogue) ctx.amount = state.catalogue.amount;
|
|
406
|
+
if (checkout?.project) ctx.project = checkout.project;
|
|
407
|
+
if (state.appearance) ctx.appearance = state.appearance;
|
|
408
|
+
if (state.returnUrl) ctx.returnUrl = state.returnUrl;
|
|
409
|
+
return ctx;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ../engine/src/engine/state.ts
|
|
413
|
+
function initialState() {
|
|
414
|
+
return {
|
|
415
|
+
catalogue: null,
|
|
416
|
+
checkout: null,
|
|
417
|
+
message: "",
|
|
418
|
+
phase: "loading",
|
|
419
|
+
error: null,
|
|
420
|
+
dismissedError: false,
|
|
421
|
+
appearance: null,
|
|
422
|
+
returnUrl: null,
|
|
423
|
+
polling: false
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function extractCatalogue(checkout) {
|
|
427
|
+
const { country, currencies, amount, methods } = checkout;
|
|
428
|
+
if (!country || !currencies || !amount || !methods) return null;
|
|
429
|
+
return {
|
|
430
|
+
country,
|
|
431
|
+
currencies,
|
|
432
|
+
amount,
|
|
433
|
+
methods,
|
|
434
|
+
supportsAnyCountry: checkout.supports_any_country ?? false
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ../engine/src/engine/engine.ts
|
|
439
|
+
var CheckoutEngine = class {
|
|
440
|
+
constructor(transport, options = {}) {
|
|
441
|
+
__publicField(this, "transport", transport);
|
|
442
|
+
__publicField(this, "state", initialState());
|
|
443
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Set());
|
|
444
|
+
__publicField(this, "autoPoll");
|
|
445
|
+
__publicField(this, "intervals");
|
|
446
|
+
__publicField(this, "steady");
|
|
447
|
+
__publicField(this, "now");
|
|
448
|
+
__publicField(this, "setTimeoutImpl");
|
|
449
|
+
__publicField(this, "clearTimeoutImpl");
|
|
450
|
+
__publicField(this, "pollHandle", null);
|
|
451
|
+
__publicField(this, "pollTick", 0);
|
|
452
|
+
__publicField(this, "pollInFlight", false);
|
|
453
|
+
__publicField(this, "destroyed", false);
|
|
454
|
+
this.autoPoll = options.autoPoll ?? true;
|
|
455
|
+
this.intervals = options.pollIntervalsMs ?? [2e3, 2e3, 3e3, 5e3];
|
|
456
|
+
this.steady = options.steadyIntervalMs ?? 5e3;
|
|
457
|
+
this.now = options.now ?? Date.now;
|
|
458
|
+
this.setTimeoutImpl = options.setTimeoutImpl ?? ((cb, ms) => setTimeout(cb, ms));
|
|
459
|
+
this.clearTimeoutImpl = options.clearTimeoutImpl ?? ((handle) => clearTimeout(handle));
|
|
460
|
+
}
|
|
461
|
+
// --- read surface ---
|
|
462
|
+
getView() {
|
|
463
|
+
return projectView(this.state);
|
|
464
|
+
}
|
|
465
|
+
/** Subscribe to view changes. Fires immediately with the current view. */
|
|
466
|
+
subscribe(fn) {
|
|
467
|
+
this.listeners.add(fn);
|
|
468
|
+
fn(this.getView());
|
|
469
|
+
return () => {
|
|
470
|
+
this.listeners.delete(fn);
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
// --- actions ---
|
|
474
|
+
/**
|
|
475
|
+
* Seed state from a server-fetched `show` payload (SSR) so the first paint is
|
|
476
|
+
* the form, not the loading screen — no transport call. The host passes the
|
|
477
|
+
* `GET /checkout/{public_id}` payload the SSR layer fetched after bootstrap.
|
|
478
|
+
* Treated exactly like a `show`, so the resolved catalogue is cached. The
|
|
479
|
+
* engine may still `poll()`/`load()` afterwards to refresh.
|
|
480
|
+
*/
|
|
481
|
+
hydrate(checkout, message = "") {
|
|
482
|
+
this.applyResponse(checkout, message, true);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Seed session-scoped chrome from the SSR bootstrap payload. `appearance` and
|
|
486
|
+
* `return_url` no longer ride on show/pay/poll responses, so the host passes
|
|
487
|
+
* them once here and the engine projects them onto every view. Call before the
|
|
488
|
+
* first render (alongside `hydrate`) so the theme paints branded from frame 1.
|
|
489
|
+
*/
|
|
490
|
+
seedBootstrap(input) {
|
|
491
|
+
const next = {};
|
|
492
|
+
if (input.appearance) next.appearance = input.appearance;
|
|
493
|
+
if (input.returnUrl !== void 0) next.returnUrl = input.returnUrl;
|
|
494
|
+
if (Object.keys(next).length > 0) this.setState(next);
|
|
495
|
+
}
|
|
496
|
+
async load() {
|
|
497
|
+
await this.runShow(void 0);
|
|
498
|
+
}
|
|
499
|
+
async selectCountry(country) {
|
|
500
|
+
await this.runShow({ country });
|
|
501
|
+
}
|
|
502
|
+
async selectCurrency(currency) {
|
|
503
|
+
const country = this.state.catalogue?.country.code;
|
|
504
|
+
await this.runShow(country ? { country, currency } : { currency });
|
|
505
|
+
}
|
|
506
|
+
/** Full selectable country catalogue for the picker (not part of the view). */
|
|
507
|
+
async listCountries() {
|
|
508
|
+
try {
|
|
509
|
+
const res = await this.transport.countries();
|
|
510
|
+
return res.data;
|
|
511
|
+
} catch (cause) {
|
|
512
|
+
this.setError(cause);
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async pay(input) {
|
|
517
|
+
await this.runMutation(() => this.transport.pay(this.withCatalogueDefaults(input)), false);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Dismiss the current error banner (both `lastError` and a failed-attempt
|
|
521
|
+
* `retryOf`). Themes call this when the customer selects a method again so a
|
|
522
|
+
* stale error doesn't linger; the next server response clears the dismissal.
|
|
523
|
+
*/
|
|
524
|
+
clearError() {
|
|
525
|
+
if (this.state.error === null && this.state.dismissedError) return;
|
|
526
|
+
this.setState({ error: null, dismissedError: true });
|
|
527
|
+
}
|
|
528
|
+
async authorize(input) {
|
|
529
|
+
await this.runMutation(() => this.transport.authorize(input), false);
|
|
530
|
+
}
|
|
531
|
+
async poll() {
|
|
532
|
+
await this.runMutation(() => this.transport.poll(), false);
|
|
533
|
+
}
|
|
534
|
+
/** Called when the customer returns from an external redirect. Just polls. */
|
|
535
|
+
async resumeFromRedirect() {
|
|
536
|
+
await this.poll();
|
|
537
|
+
}
|
|
538
|
+
// --- polling control (host wires visibility/focus to these) ---
|
|
539
|
+
startPolling() {
|
|
540
|
+
if (this.destroyed || this.state.polling) return;
|
|
541
|
+
this.setState({ polling: true });
|
|
542
|
+
this.pollTick = 0;
|
|
543
|
+
this.scheduleNextPoll();
|
|
544
|
+
}
|
|
545
|
+
stopPolling() {
|
|
546
|
+
if (this.pollHandle !== null) {
|
|
547
|
+
this.clearTimeoutImpl(this.pollHandle);
|
|
548
|
+
this.pollHandle = null;
|
|
549
|
+
}
|
|
550
|
+
if (this.state.polling) this.setState({ polling: false });
|
|
551
|
+
}
|
|
552
|
+
destroy() {
|
|
553
|
+
this.destroyed = true;
|
|
554
|
+
this.stopPolling();
|
|
555
|
+
this.listeners.clear();
|
|
556
|
+
}
|
|
557
|
+
// --- internals ---
|
|
558
|
+
async runShow(query) {
|
|
559
|
+
try {
|
|
560
|
+
const res = await this.transport.show(query);
|
|
561
|
+
this.applyResponse(res.data, res.message ?? "", true);
|
|
562
|
+
} catch (cause) {
|
|
563
|
+
this.setError(cause);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async runMutation(call, isShow) {
|
|
567
|
+
try {
|
|
568
|
+
const res = await call();
|
|
569
|
+
this.applyResponse(res.data, res.message ?? "", isShow);
|
|
570
|
+
} catch (cause) {
|
|
571
|
+
this.setError(cause);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Stamp the pay payload with the currently-resolved country/currency from the
|
|
576
|
+
* cached catalogue. Themes call `selectCountry`/`selectCurrency` (which refetch
|
|
577
|
+
* the catalogue) but never thread that selection back into `pay` — without this
|
|
578
|
+
* the server falls back to its default country and rejects methods that aren't
|
|
579
|
+
* available there. A value already on the input always wins; currency is only
|
|
580
|
+
* stamped when DCC is in effect (the only time the server expects it).
|
|
581
|
+
*/
|
|
582
|
+
withCatalogueDefaults(input) {
|
|
583
|
+
const catalogue = this.state.catalogue;
|
|
584
|
+
if (!catalogue) return input;
|
|
585
|
+
const next = { ...input };
|
|
586
|
+
if (next.country === void 0) next.country = catalogue.country.code;
|
|
587
|
+
if (next.currency === void 0 && catalogue.amount.is_dcc) {
|
|
588
|
+
const currency = catalogue.amount.final.currency;
|
|
589
|
+
if (currency) next.currency = currency;
|
|
590
|
+
}
|
|
591
|
+
return next;
|
|
592
|
+
}
|
|
593
|
+
applyResponse(checkout, message, isShow) {
|
|
594
|
+
const next = {
|
|
595
|
+
checkout,
|
|
596
|
+
message,
|
|
597
|
+
phase: derivePhase(checkout),
|
|
598
|
+
error: null,
|
|
599
|
+
// A fresh response (incl. a new failed attempt) is shown, not suppressed.
|
|
600
|
+
dismissedError: false
|
|
601
|
+
};
|
|
602
|
+
if (isShow) {
|
|
603
|
+
const catalogue = extractCatalogue(checkout);
|
|
604
|
+
if (catalogue) next.catalogue = catalogue;
|
|
605
|
+
}
|
|
606
|
+
this.setState(next);
|
|
607
|
+
this.reconcilePolling();
|
|
608
|
+
}
|
|
609
|
+
setError(cause) {
|
|
610
|
+
const error = cause instanceof CheckoutError ? cause : new CheckoutError("unknown", toMessage(cause));
|
|
611
|
+
const isTerminalError = error.code === "not_found" || error.code === "not_open";
|
|
612
|
+
const keepPhase = !isTerminalError && this.state.checkout;
|
|
613
|
+
const phase = keepPhase ? this.state.phase : "closed";
|
|
614
|
+
this.setState({ error, phase, message: error.message, dismissedError: false });
|
|
615
|
+
this.reconcilePolling();
|
|
616
|
+
}
|
|
617
|
+
reconcilePolling() {
|
|
618
|
+
if (!this.autoPoll || this.destroyed) return;
|
|
619
|
+
if (isTerminalPhase(this.state.phase)) {
|
|
620
|
+
this.stopPolling();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (this.shouldPoll()) {
|
|
624
|
+
this.startPolling();
|
|
625
|
+
} else {
|
|
626
|
+
this.stopPolling();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Poll while an attempt is `processing`, or while it is `action_required`
|
|
631
|
+
* with a poll-completion action (approve_on_phone, scan_qr_code,
|
|
632
|
+
* display_payment_instructions, embed_iframe). Do NOT poll while waiting on
|
|
633
|
+
* the customer to submit (collect_otp), redirect, or run an SDK.
|
|
634
|
+
*/
|
|
635
|
+
shouldPoll() {
|
|
636
|
+
if (this.state.phase === "processing") return true;
|
|
637
|
+
if (this.state.phase === "action_required") {
|
|
638
|
+
const action = this.state.checkout?.attempt?.next_action;
|
|
639
|
+
return action != null && actionCompletion(action) === "poll";
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
scheduleNextPoll() {
|
|
644
|
+
if (this.destroyed || !this.state.polling) return;
|
|
645
|
+
const delay = this.intervals[this.pollTick] ?? this.steady;
|
|
646
|
+
this.pollTick++;
|
|
647
|
+
const expiresAt = this.expiresAtMs();
|
|
648
|
+
if (expiresAt !== null && this.now() >= expiresAt) {
|
|
649
|
+
this.stopPolling();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
this.pollHandle = this.setTimeoutImpl(() => {
|
|
653
|
+
this.tickPoll().catch(() => void 0);
|
|
654
|
+
}, delay);
|
|
655
|
+
}
|
|
656
|
+
async tickPoll() {
|
|
657
|
+
if (this.destroyed || !this.state.polling || this.pollInFlight) return;
|
|
658
|
+
this.pollInFlight = true;
|
|
659
|
+
try {
|
|
660
|
+
await this.poll();
|
|
661
|
+
} finally {
|
|
662
|
+
this.pollInFlight = false;
|
|
663
|
+
}
|
|
664
|
+
if (this.state.polling && !isTerminalPhase(this.state.phase)) {
|
|
665
|
+
this.scheduleNextPoll();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
expiresAtMs() {
|
|
669
|
+
const raw = this.state.checkout?.session.expires_at;
|
|
670
|
+
if (!raw) return null;
|
|
671
|
+
const ms = Date.parse(raw);
|
|
672
|
+
return Number.isNaN(ms) ? null : ms;
|
|
673
|
+
}
|
|
674
|
+
setState(partial) {
|
|
675
|
+
this.state = { ...this.state, ...partial };
|
|
676
|
+
this.emit();
|
|
677
|
+
}
|
|
678
|
+
emit() {
|
|
679
|
+
const view = this.getView();
|
|
680
|
+
for (const listener of this.listeners) listener(view);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
function toMessage(cause) {
|
|
684
|
+
if (cause instanceof Error) return cause.message;
|
|
685
|
+
return "Unexpected error.";
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/redirect-effect.ts
|
|
689
|
+
function performActionRedirect(action, deps = {}) {
|
|
690
|
+
const doc = deps.doc ?? document;
|
|
691
|
+
const navigate = deps.navigate ?? ((url) => window.location.assign(url));
|
|
692
|
+
if (!isHttps(action.url)) {
|
|
693
|
+
console.error(`Orqex: refused redirect to non-https URL "${action.url}".`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (action.method === "POST") {
|
|
697
|
+
const form = doc.createElement("form");
|
|
698
|
+
form.method = "POST";
|
|
699
|
+
form.action = action.url;
|
|
700
|
+
form.style.display = "none";
|
|
701
|
+
for (const [name, value] of Object.entries(action.post_data ?? {})) {
|
|
702
|
+
const input = doc.createElement("input");
|
|
703
|
+
input.type = "hidden";
|
|
704
|
+
input.name = name;
|
|
705
|
+
input.value = typeof value === "string" ? value : JSON.stringify(value);
|
|
706
|
+
form.appendChild(input);
|
|
707
|
+
}
|
|
708
|
+
doc.body.appendChild(form);
|
|
709
|
+
form.submit();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
navigate(action.url);
|
|
713
|
+
}
|
|
714
|
+
function isHttps(url) {
|
|
715
|
+
try {
|
|
716
|
+
return new URL(url).protocol === "https:";
|
|
717
|
+
} catch {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/headless.ts
|
|
723
|
+
function redirectActionOf(view) {
|
|
724
|
+
if (view.kind === "action" && view.ctx.completion === "redirect" && view.ctx.action.type === "redirect_to_url") {
|
|
725
|
+
return view.ctx.action;
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
var markerKey = (publicId) => `orqex:redirect:${publicId}`;
|
|
730
|
+
function wireAutoResume(engine, publicId, ctx) {
|
|
731
|
+
const key = markerKey(publicId);
|
|
732
|
+
if (ctx.storage.getItem(key) !== null) {
|
|
733
|
+
ctx.storage.removeItem(key);
|
|
734
|
+
engine.resumeFromRedirect();
|
|
735
|
+
}
|
|
736
|
+
let handled = null;
|
|
737
|
+
const unsubscribe = engine.subscribe((view) => {
|
|
738
|
+
const action = redirectActionOf(view);
|
|
739
|
+
if (!action || !action.can_auto_redirect || view.kind !== "action") return;
|
|
740
|
+
const dedupe = `${view.ctx.attempt.id}:${action.type}`;
|
|
741
|
+
if (handled === dedupe) return;
|
|
742
|
+
handled = dedupe;
|
|
743
|
+
ctx.storage.setItem(key, view.ctx.attempt.id);
|
|
744
|
+
performActionRedirect(action, { navigate: (url) => ctx.win.location.assign(url), doc: ctx.win.document });
|
|
745
|
+
});
|
|
746
|
+
const onPageShow = () => {
|
|
747
|
+
const kind = engine.getView().kind;
|
|
748
|
+
if (kind === "action" || kind === "processing") engine.resumeFromRedirect();
|
|
749
|
+
};
|
|
750
|
+
ctx.win.addEventListener("pageshow", onPageShow);
|
|
751
|
+
return () => {
|
|
752
|
+
unsubscribe();
|
|
753
|
+
ctx.win.removeEventListener("pageshow", onPageShow);
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function createHeadlessCheckout(config, options, deps = {}) {
|
|
757
|
+
const win = deps.win ?? window;
|
|
758
|
+
const engine = deps.engine ?? new CheckoutEngine(
|
|
759
|
+
new HttpTransport({
|
|
760
|
+
baseUrl: config.apiBaseUrl,
|
|
761
|
+
auth: { publishableKey: config.publishableKey, publicId: options.publicId },
|
|
762
|
+
locale: options.locale ?? (typeof navigator === "undefined" ? void 0 : navigator.language),
|
|
763
|
+
fetchImpl: deps.fetchImpl
|
|
764
|
+
})
|
|
765
|
+
);
|
|
766
|
+
let autoResumeTeardown = () => {
|
|
767
|
+
};
|
|
768
|
+
const facade = {
|
|
769
|
+
subscribe: (fn) => engine.subscribe(fn),
|
|
770
|
+
getView: () => engine.getView(),
|
|
771
|
+
pay: (input) => engine.pay(input),
|
|
772
|
+
authorize: (input) => engine.authorize(input),
|
|
773
|
+
poll: () => engine.poll(),
|
|
774
|
+
selectCountry: (country) => engine.selectCountry(country),
|
|
775
|
+
selectCurrency: (currency) => engine.selectCurrency(currency),
|
|
776
|
+
listCountries: () => engine.listCountries(),
|
|
777
|
+
resumeFromRedirect: () => engine.resumeFromRedirect(),
|
|
778
|
+
startPolling: () => engine.startPolling(),
|
|
779
|
+
stopPolling: () => engine.stopPolling(),
|
|
780
|
+
performRedirect: () => {
|
|
781
|
+
const action = redirectActionOf(engine.getView());
|
|
782
|
+
if (action) performActionRedirect(action, { navigate: (url) => win.location.assign(url), doc: win.document });
|
|
783
|
+
},
|
|
784
|
+
destroy: () => {
|
|
785
|
+
autoResumeTeardown();
|
|
786
|
+
engine.destroy();
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
if (options.autoResume) {
|
|
790
|
+
autoResumeTeardown = wireAutoResume(engine, options.publicId, { storage: deps.storage ?? win.sessionStorage, win });
|
|
791
|
+
}
|
|
792
|
+
engine.load();
|
|
793
|
+
return facade;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/redirect.ts
|
|
797
|
+
var defaultNavigate = (url) => {
|
|
798
|
+
window.location.assign(url);
|
|
799
|
+
};
|
|
800
|
+
function redirectToCheckout(config, options, navigate = defaultNavigate) {
|
|
801
|
+
if (!options.publicId) {
|
|
802
|
+
throw new Error("Orqex: redirectToCheckout requires a publicId.");
|
|
803
|
+
}
|
|
804
|
+
navigate(buildRedirectUrl(config, options.publicId));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/orqex.ts
|
|
808
|
+
function Orqex(publishableKey, options) {
|
|
809
|
+
const config = resolveConfig(publishableKey, options);
|
|
810
|
+
const bus = new EventBus();
|
|
811
|
+
const embedded = new EmbeddedController({ config, bus });
|
|
812
|
+
return {
|
|
813
|
+
mount(target, mountOptions) {
|
|
814
|
+
embedded.mount(target, mountOptions);
|
|
815
|
+
},
|
|
816
|
+
redirectToCheckout(redirectOptions) {
|
|
817
|
+
redirectToCheckout(config, redirectOptions);
|
|
818
|
+
},
|
|
819
|
+
on(event, handler) {
|
|
820
|
+
return bus.on(event, handler);
|
|
821
|
+
},
|
|
822
|
+
unmount() {
|
|
823
|
+
embedded.unmount();
|
|
824
|
+
},
|
|
825
|
+
createEngine(engineOptions) {
|
|
826
|
+
return createHeadlessCheckout(config, engineOptions);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/embed-projection.ts
|
|
832
|
+
function projectEmbedMessage(view, lastKey) {
|
|
833
|
+
let projection = null;
|
|
834
|
+
if (view.kind === "succeeded") {
|
|
835
|
+
const status = String(view.ctx.status);
|
|
836
|
+
projection = { message: successMessage(status), key: `succeeded:${status}` };
|
|
837
|
+
} else if (view.kind === "failed") {
|
|
838
|
+
const status = String(view.ctx.status);
|
|
839
|
+
projection = { message: errorMessage(status, view.ctx.message), key: `failed:${status}` };
|
|
840
|
+
} else if (view.kind === "closed") {
|
|
841
|
+
const status = String(view.ctx.status);
|
|
842
|
+
projection = { message: closeMessage(status), key: `closed:${status}` };
|
|
843
|
+
}
|
|
844
|
+
if (!projection || projection.key === lastKey) return null;
|
|
845
|
+
return projection;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/index.ts
|
|
849
|
+
var SDK_VERSION = "0.0.1";
|
|
850
|
+
|
|
851
|
+
export { Orqex, SDK_VERSION, projectEmbedMessage };
|
|
852
|
+
//# sourceMappingURL=index.js.map
|
|
853
|
+
//# sourceMappingURL=index.js.map
|