@tagadapay/headless-sdk 1.8.0-rc.0 → 1.9.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/README.md +216 -1
- package/dist/core/client.d.ts +68 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +73 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/errors.d.ts +53 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +58 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/helpers/autocomplete.d.ts +89 -0
- package/dist/core/helpers/autocomplete.d.ts.map +1 -0
- package/dist/core/helpers/autocomplete.js +186 -0
- package/dist/core/helpers/autocomplete.js.map +1 -0
- package/dist/core/helpers/countries.d.ts +45 -0
- package/dist/core/helpers/countries.d.ts.map +1 -0
- package/dist/core/helpers/countries.js +65 -0
- package/dist/core/helpers/countries.js.map +1 -0
- package/dist/core/helpers/currency.d.ts +26 -0
- package/dist/core/helpers/currency.d.ts.map +1 -0
- package/dist/core/helpers/currency.js +72 -0
- package/dist/core/helpers/currency.js.map +1 -0
- package/dist/core/helpers/index.d.ts +6 -0
- package/dist/core/helpers/index.d.ts.map +1 -0
- package/dist/core/helpers/index.js +6 -0
- package/dist/core/helpers/index.js.map +1 -0
- package/dist/core/helpers/iso3166.d.ts +28 -0
- package/dist/core/helpers/iso3166.d.ts.map +1 -0
- package/dist/core/helpers/iso3166.js +84 -0
- package/dist/core/helpers/iso3166.js.map +1 -0
- package/dist/core/helpers/walletContact.d.ts +67 -0
- package/dist/core/helpers/walletContact.d.ts.map +1 -0
- package/dist/core/helpers/walletContact.js +71 -0
- package/dist/core/helpers/walletContact.js.map +1 -0
- package/dist/core/http.d.ts +27 -0
- package/dist/core/http.d.ts.map +1 -0
- package/dist/core/http.js +125 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +13 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/modules/catalog.d.ts +27 -0
- package/dist/core/modules/catalog.d.ts.map +1 -0
- package/dist/core/modules/catalog.js +38 -0
- package/dist/core/modules/catalog.js.map +1 -0
- package/dist/core/modules/checkout.d.ts +268 -0
- package/dist/core/modules/checkout.d.ts.map +1 -0
- package/dist/core/modules/checkout.js +470 -0
- package/dist/core/modules/checkout.js.map +1 -0
- package/dist/core/modules/customer.d.ts +33 -0
- package/dist/core/modules/customer.d.ts.map +1 -0
- package/dist/core/modules/customer.js +53 -0
- package/dist/core/modules/customer.js.map +1 -0
- package/dist/core/modules/funnel.d.ts +38 -0
- package/dist/core/modules/funnel.d.ts.map +1 -0
- package/dist/core/modules/funnel.js +143 -0
- package/dist/core/modules/funnel.js.map +1 -0
- package/dist/core/modules/offers.d.ts +137 -0
- package/dist/core/modules/offers.d.ts.map +1 -0
- package/dist/core/modules/offers.js +141 -0
- package/dist/core/modules/offers.js.map +1 -0
- package/dist/core/modules/payment.d.ts +353 -0
- package/dist/core/modules/payment.d.ts.map +1 -0
- package/dist/core/modules/payment.js +784 -0
- package/dist/core/modules/payment.js.map +1 -0
- package/dist/core/modules/rx.d.ts +162 -0
- package/dist/core/modules/rx.d.ts.map +1 -0
- package/dist/core/modules/rx.js +46 -0
- package/dist/core/modules/rx.js.map +1 -0
- package/dist/core/types.d.ts +532 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +5 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/react/hooks/useCatalog.d.ts +25 -0
- package/dist/react/hooks/useCatalog.d.ts.map +1 -0
- package/dist/react/hooks/useCatalog.js +58 -0
- package/dist/react/hooks/useCatalog.js.map +1 -0
- package/dist/react/hooks/useCheckout.d.ts +58 -0
- package/dist/react/hooks/useCheckout.d.ts.map +1 -0
- package/dist/react/hooks/useCheckout.js +177 -0
- package/dist/react/hooks/useCheckout.js.map +1 -0
- package/dist/react/hooks/useCustomer.d.ts +28 -0
- package/dist/react/hooks/useCustomer.d.ts.map +1 -0
- package/dist/react/hooks/useCustomer.js +77 -0
- package/dist/react/hooks/useCustomer.js.map +1 -0
- package/dist/react/hooks/useGoogleAutocomplete.d.ts +26 -0
- package/dist/react/hooks/useGoogleAutocomplete.d.ts.map +1 -0
- package/dist/react/hooks/useGoogleAutocomplete.js +70 -0
- package/dist/react/hooks/useGoogleAutocomplete.js.map +1 -0
- package/dist/react/hooks/useISOData.d.ts +23 -0
- package/dist/react/hooks/useISOData.d.ts.map +1 -0
- package/dist/react/hooks/useISOData.js +63 -0
- package/dist/react/hooks/useISOData.js.map +1 -0
- package/dist/react/hooks/useOffers.d.ts +134 -0
- package/dist/react/hooks/useOffers.d.ts.map +1 -0
- package/dist/react/hooks/useOffers.js +171 -0
- package/dist/react/hooks/useOffers.js.map +1 -0
- package/dist/react/hooks/usePayment.d.ts +117 -0
- package/dist/react/hooks/usePayment.d.ts.map +1 -0
- package/dist/react/hooks/usePayment.js +233 -0
- package/dist/react/hooks/usePayment.js.map +1 -0
- package/dist/react/hooks/useRx.d.ts +64 -0
- package/dist/react/hooks/useRx.d.ts.map +1 -0
- package/dist/react/hooks/useRx.js +145 -0
- package/dist/react/hooks/useRx.js.map +1 -0
- package/dist/react/index.d.ts +22 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +12 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/internal/redirectGuard.d.ts +9 -0
- package/dist/react/internal/redirectGuard.d.ts.map +1 -0
- package/dist/react/internal/redirectGuard.js +25 -0
- package/dist/react/internal/redirectGuard.js.map +1 -0
- package/dist/react/provider.d.ts +31 -0
- package/dist/react/provider.d.ts.map +1 -0
- package/dist/react/provider.js +32 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/stripe/StripeExpressButton.d.ts +30 -0
- package/dist/stripe/StripeExpressButton.d.ts.map +1 -0
- package/dist/stripe/StripeExpressButton.js +32 -0
- package/dist/stripe/StripeExpressButton.js.map +1 -0
- package/dist/stripe/StripeExpressCheckout.d.ts +109 -0
- package/dist/stripe/StripeExpressCheckout.d.ts.map +1 -0
- package/dist/stripe/StripeExpressCheckout.js +242 -0
- package/dist/stripe/StripeExpressCheckout.js.map +1 -0
- package/dist/stripe/index.d.ts +8 -0
- package/dist/stripe/index.d.ts.map +1 -0
- package/dist/stripe/index.js +15 -0
- package/dist/stripe/index.js.map +1 -0
- package/dist/stripe/useStripeExpress.d.ts +54 -0
- package/dist/stripe/useStripeExpress.d.ts.map +1 -0
- package/dist/stripe/useStripeExpress.js +125 -0
- package/dist/stripe/useStripeExpress.js.map +1 -0
- package/dist/tagada-headless-react.min.js +8 -0
- package/dist/tagada-headless-react.min.js.map +7 -0
- package/dist/tagada-headless.js +2423 -0
- package/dist/tagada-headless.min.js +8 -0
- package/dist/tagada-headless.min.js.map +7 -0
- package/package.json +79 -73
|
@@ -0,0 +1,2423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tagadapay/headless-sdk v1.9.0
|
|
3
|
+
* Headless checkout, payments, offers, and CRM
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
"use strict";
|
|
7
|
+
var TagadaHeadlessBundle = (() => {
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
15
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
16
|
+
}) : x)(function(x) {
|
|
17
|
+
if (typeof require !== "undefined")
|
|
18
|
+
return require.apply(this, arguments);
|
|
19
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
20
|
+
});
|
|
21
|
+
var __export = (target, all) => {
|
|
22
|
+
for (var name in all)
|
|
23
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
24
|
+
};
|
|
25
|
+
var __copyProps = (to, from, except, desc) => {
|
|
26
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
27
|
+
for (let key of __getOwnPropNames(from))
|
|
28
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
29
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
30
|
+
}
|
|
31
|
+
return to;
|
|
32
|
+
};
|
|
33
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
34
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
35
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
36
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
37
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
38
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
39
|
+
mod
|
|
40
|
+
));
|
|
41
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
42
|
+
|
|
43
|
+
// src/core/index.ts
|
|
44
|
+
var core_exports = {};
|
|
45
|
+
__export(core_exports, {
|
|
46
|
+
AVAILABLE_LANGUAGES: () => AVAILABLE_LANGUAGES,
|
|
47
|
+
CatalogModule: () => CatalogModule,
|
|
48
|
+
CheckoutModule: () => CheckoutModule,
|
|
49
|
+
CurrencyUtils: () => CurrencyUtils,
|
|
50
|
+
CustomerModule: () => CustomerModule,
|
|
51
|
+
FunnelModule: () => FunnelModule,
|
|
52
|
+
GoogleAutocompleteCore: () => GoogleAutocompleteCore,
|
|
53
|
+
HeadlessClient: () => HeadlessClient,
|
|
54
|
+
HttpClient: () => HttpClient,
|
|
55
|
+
ISODataCore: () => ISODataCore,
|
|
56
|
+
OffersModule: () => OffersModule,
|
|
57
|
+
PaymentModule: () => PaymentModule,
|
|
58
|
+
RxModule: () => RxModule,
|
|
59
|
+
TagadaAuthError: () => TagadaAuthError,
|
|
60
|
+
TagadaError: () => TagadaError,
|
|
61
|
+
TagadaNetworkError: () => TagadaNetworkError,
|
|
62
|
+
TagadaNotFoundError: () => TagadaNotFoundError,
|
|
63
|
+
TagadaPaymentError: () => TagadaPaymentError,
|
|
64
|
+
TagadaRateLimitError: () => TagadaRateLimitError,
|
|
65
|
+
TagadaValidationError: () => TagadaValidationError,
|
|
66
|
+
createHeadlessClient: () => createHeadlessClient,
|
|
67
|
+
ensureGeoDataLoaded: () => ensureGeoDataLoaded,
|
|
68
|
+
formatMoney: () => formatMoney,
|
|
69
|
+
getCountries: () => getCountries,
|
|
70
|
+
getStatesForCountry: () => getStatesForCountry,
|
|
71
|
+
isLanguageLoaded: () => isLanguageLoaded,
|
|
72
|
+
loadGoogleMapsScript: () => loadGoogleMapsScript
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// src/core/errors.ts
|
|
76
|
+
var TagadaError = class extends Error {
|
|
77
|
+
constructor(message, code, statusCode, details) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "TagadaError";
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.statusCode = statusCode;
|
|
82
|
+
this.details = details;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var TagadaAuthError = class extends TagadaError {
|
|
86
|
+
constructor(message = "Authentication failed. Check your API key or checkout token.") {
|
|
87
|
+
super(message, "auth_error", 401);
|
|
88
|
+
this.name = "TagadaAuthError";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var TagadaValidationError = class extends TagadaError {
|
|
92
|
+
constructor(message, errors = [], appCode, details) {
|
|
93
|
+
super(message, "validation_error", 422, { errors, ...details });
|
|
94
|
+
this.name = "TagadaValidationError";
|
|
95
|
+
this.errors = errors;
|
|
96
|
+
this.appCode = appCode;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var TagadaNotFoundError = class extends TagadaError {
|
|
100
|
+
constructor(resource, id) {
|
|
101
|
+
super(`${resource} "${id}" not found.`, "not_found", 404);
|
|
102
|
+
this.name = "TagadaNotFoundError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var TagadaRateLimitError = class extends TagadaError {
|
|
106
|
+
constructor(retryAfterMs = 1e3) {
|
|
107
|
+
super(`Rate limited. Retry after ${retryAfterMs}ms.`, "rate_limit", 429);
|
|
108
|
+
this.name = "TagadaRateLimitError";
|
|
109
|
+
this.retryAfterMs = retryAfterMs;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var TagadaPaymentError = class extends TagadaError {
|
|
113
|
+
constructor(message, opts) {
|
|
114
|
+
super(message, "payment_error", void 0, opts == null ? void 0 : opts.details);
|
|
115
|
+
this.name = "TagadaPaymentError";
|
|
116
|
+
this.paymentId = opts == null ? void 0 : opts.paymentId;
|
|
117
|
+
this.requireAction = opts == null ? void 0 : opts.requireAction;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var TagadaNetworkError = class extends TagadaError {
|
|
121
|
+
constructor(message = "Network request failed. Check your connection.") {
|
|
122
|
+
super(message, "network_error");
|
|
123
|
+
this.name = "TagadaNetworkError";
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/core/http.ts
|
|
128
|
+
var BASE_URLS = {
|
|
129
|
+
production: "https://api.tagada.io",
|
|
130
|
+
development: "https://api.tagada.dev",
|
|
131
|
+
local: "http://localhost:3000"
|
|
132
|
+
};
|
|
133
|
+
var HttpClient = class {
|
|
134
|
+
constructor(config) {
|
|
135
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
136
|
+
this.timeout = config.timeout;
|
|
137
|
+
this.apiKey = config.apiKey;
|
|
138
|
+
this.getToken = config.getToken;
|
|
139
|
+
}
|
|
140
|
+
static resolveBaseUrl(environment, override) {
|
|
141
|
+
return override ?? BASE_URLS[environment] ?? BASE_URLS.production;
|
|
142
|
+
}
|
|
143
|
+
async request(path, options = {}) {
|
|
144
|
+
var _a;
|
|
145
|
+
const { method = "GET", body, headers = {}, params, token, signal } = options;
|
|
146
|
+
let url = `${this.baseUrl}${path}`;
|
|
147
|
+
if (params) {
|
|
148
|
+
const qs = new URLSearchParams();
|
|
149
|
+
for (const [k, v] of Object.entries(params)) {
|
|
150
|
+
if (v !== void 0)
|
|
151
|
+
qs.set(k, String(v));
|
|
152
|
+
}
|
|
153
|
+
const str = qs.toString();
|
|
154
|
+
if (str)
|
|
155
|
+
url += `?${str}`;
|
|
156
|
+
}
|
|
157
|
+
const reqHeaders = {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
...headers
|
|
160
|
+
};
|
|
161
|
+
const authToken = token ?? this.apiKey ?? ((_a = this.getToken) == null ? void 0 : _a.call(this));
|
|
162
|
+
if (authToken) {
|
|
163
|
+
reqHeaders["x-cms-token"] = authToken;
|
|
164
|
+
}
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(url, {
|
|
169
|
+
method,
|
|
170
|
+
headers: reqHeaders,
|
|
171
|
+
body: body != null ? JSON.stringify(body) : void 0,
|
|
172
|
+
signal: signal ?? controller.signal
|
|
173
|
+
});
|
|
174
|
+
clearTimeout(timeoutId);
|
|
175
|
+
if (res.ok) {
|
|
176
|
+
const text = await res.text();
|
|
177
|
+
return text ? JSON.parse(text) : {};
|
|
178
|
+
}
|
|
179
|
+
await this.handleErrorResponse(res, path);
|
|
180
|
+
throw new TagadaError("Unexpected error", "unknown", res.status);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
clearTimeout(timeoutId);
|
|
183
|
+
if (err instanceof TagadaError)
|
|
184
|
+
throw err;
|
|
185
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
186
|
+
throw new TagadaNetworkError(`Request to ${path} timed out after ${this.timeout}ms.`);
|
|
187
|
+
}
|
|
188
|
+
throw new TagadaNetworkError(
|
|
189
|
+
err instanceof Error ? err.message : "Network request failed."
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
get(path, options) {
|
|
194
|
+
return this.request(path, { ...options, method: "GET" });
|
|
195
|
+
}
|
|
196
|
+
post(path, body, options) {
|
|
197
|
+
return this.request(path, { ...options, method: "POST", body });
|
|
198
|
+
}
|
|
199
|
+
patch(path, body, options) {
|
|
200
|
+
return this.request(path, { ...options, method: "PATCH", body });
|
|
201
|
+
}
|
|
202
|
+
put(path, body, options) {
|
|
203
|
+
return this.request(path, { ...options, method: "PUT", body });
|
|
204
|
+
}
|
|
205
|
+
delete(path, options) {
|
|
206
|
+
return this.request(path, { ...options, method: "DELETE" });
|
|
207
|
+
}
|
|
208
|
+
async handleErrorResponse(res, path) {
|
|
209
|
+
let parsed = {};
|
|
210
|
+
try {
|
|
211
|
+
parsed = await res.json();
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
const data = parsed.data ?? {};
|
|
215
|
+
const msg = parsed.message ?? parsed.error ?? (typeof data.message === "string" ? data.message : void 0) ?? (typeof parsed.data === "string" ? parsed.data : void 0) ?? `Request failed: ${res.status} ${res.statusText} on ${path}`;
|
|
216
|
+
const appCode = typeof data.code === "string" ? data.code : void 0;
|
|
217
|
+
switch (res.status) {
|
|
218
|
+
case 401:
|
|
219
|
+
case 403:
|
|
220
|
+
throw new TagadaAuthError(msg);
|
|
221
|
+
case 404:
|
|
222
|
+
throw new TagadaNotFoundError(path, "");
|
|
223
|
+
case 422: {
|
|
224
|
+
const fieldErrors = Array.isArray(parsed.errors) ? parsed.errors : [];
|
|
225
|
+
throw new TagadaValidationError(msg, fieldErrors, appCode, parsed);
|
|
226
|
+
}
|
|
227
|
+
case 429: {
|
|
228
|
+
const retryAfter = Number(res.headers.get("Retry-After") ?? "1") * 1e3;
|
|
229
|
+
throw new TagadaRateLimitError(retryAfter);
|
|
230
|
+
}
|
|
231
|
+
default:
|
|
232
|
+
throw new TagadaError(msg, "api_error", res.status, parsed);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/core/modules/checkout.ts
|
|
238
|
+
var CheckoutModule = class {
|
|
239
|
+
constructor(http, storeId, onTokenChange) {
|
|
240
|
+
this.http = http;
|
|
241
|
+
this.storeId = storeId;
|
|
242
|
+
this.onTokenChange = onTokenChange;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Returns the most recently loaded `CheckoutSession`, if any.
|
|
246
|
+
*
|
|
247
|
+
* Populated by `loadSession()`. Returns `undefined` if the SDK has not
|
|
248
|
+
* loaded a session yet. Used by `funnel.navigate()` to source the cart.
|
|
249
|
+
*/
|
|
250
|
+
getCurrentSession() {
|
|
251
|
+
return this._currentSession;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Load an existing checkout session by its checkout token.
|
|
255
|
+
*
|
|
256
|
+
* @param checkoutToken The checkout token (from URL param `checkoutToken`).
|
|
257
|
+
* @param sessionToken The JWT session token (from URL param `token`). Required for auth.
|
|
258
|
+
* @param opts.currency Optional presentment currency override (e.g. 'EUR').
|
|
259
|
+
* Only applied when the session has no currency set yet (first load).
|
|
260
|
+
* Once set, currency is locked to the session and this param is ignored.
|
|
261
|
+
*
|
|
262
|
+
* Both `checkoutToken` and `sessionToken` come from the redirect URL returned by `createSession()`.
|
|
263
|
+
* Example URL: `/checkout?checkoutToken=abc123&token=eyJ...`
|
|
264
|
+
*/
|
|
265
|
+
async loadSession(checkoutToken, sessionToken, opts) {
|
|
266
|
+
var _a;
|
|
267
|
+
if (sessionToken) {
|
|
268
|
+
this.sessionToken = sessionToken;
|
|
269
|
+
(_a = this.onTokenChange) == null ? void 0 : _a.call(this, sessionToken);
|
|
270
|
+
}
|
|
271
|
+
const params = { skipAsyncWait: "false" };
|
|
272
|
+
if (opts == null ? void 0 : opts.currency) {
|
|
273
|
+
params.currency = opts.currency;
|
|
274
|
+
}
|
|
275
|
+
const raw = await this.http.get(
|
|
276
|
+
`/api/v1/checkout-sessions/${checkoutToken}/v2`,
|
|
277
|
+
{
|
|
278
|
+
params,
|
|
279
|
+
token: this.sessionToken
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
const session = this.normalizeSession(raw);
|
|
283
|
+
this._currentSession = session;
|
|
284
|
+
return session;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Create a new checkout session.
|
|
288
|
+
*
|
|
289
|
+
* Returns a `checkoutToken` and `sessionToken` (JWT) needed for all subsequent calls.
|
|
290
|
+
* Also returns a `checkoutUrl` (the full Tagada-hosted checkout URL, for reference).
|
|
291
|
+
*/
|
|
292
|
+
async createSession(opts) {
|
|
293
|
+
var _a;
|
|
294
|
+
const lineItems = opts.items.map((i) => ({
|
|
295
|
+
variantId: i.variantId,
|
|
296
|
+
priceId: i.priceId,
|
|
297
|
+
quantity: i.quantity
|
|
298
|
+
}));
|
|
299
|
+
const body = {
|
|
300
|
+
storeId: this.storeId,
|
|
301
|
+
lineItems
|
|
302
|
+
};
|
|
303
|
+
if (opts.returnUrl)
|
|
304
|
+
body.returnUrl = opts.returnUrl;
|
|
305
|
+
const customerData = {};
|
|
306
|
+
if (opts.currency)
|
|
307
|
+
customerData.currency = opts.currency;
|
|
308
|
+
if (opts.customerEmail) {
|
|
309
|
+
customerData.email = opts.customerEmail;
|
|
310
|
+
customerData.firstName = opts.customerFirstName;
|
|
311
|
+
customerData.lastName = opts.customerLastName;
|
|
312
|
+
}
|
|
313
|
+
if (Object.keys(customerData).length > 0) {
|
|
314
|
+
body.customer = customerData;
|
|
315
|
+
}
|
|
316
|
+
const res = await this.http.post("/api/v1/checkout/session/init", body);
|
|
317
|
+
const url = new URL(res.checkoutUrl);
|
|
318
|
+
const sessionToken = url.searchParams.get("token") ?? "";
|
|
319
|
+
this.sessionToken = sessionToken;
|
|
320
|
+
if (sessionToken)
|
|
321
|
+
(_a = this.onTokenChange) == null ? void 0 : _a.call(this, sessionToken);
|
|
322
|
+
return {
|
|
323
|
+
checkoutUrl: res.checkoutUrl,
|
|
324
|
+
checkoutToken: res.checkoutToken,
|
|
325
|
+
sessionToken,
|
|
326
|
+
customerId: res.customerId
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Create a session and produce a self-hosted redirect URL.
|
|
331
|
+
*
|
|
332
|
+
* Convenience wrapper around `createSession()`. Builds a URL of the
|
|
333
|
+
* form `${origin}${checkoutPath}?checkoutToken=...&sessionToken=...`
|
|
334
|
+
* that you can navigate to from a Cart page. The receiving page reads
|
|
335
|
+
* the params via {@link CheckoutModule.parseTokensFromUrl} and calls
|
|
336
|
+
* {@link CheckoutModule.loadSession}.
|
|
337
|
+
*
|
|
338
|
+
* For Tagada-hosted checkouts use the `checkoutUrl` returned by
|
|
339
|
+
* `createSession()` directly — this helper is for **self-hosted**
|
|
340
|
+
* checkout pages.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```ts
|
|
344
|
+
* const { url } = await tagada.checkout.createSessionUrl({
|
|
345
|
+
* items: [{ variantId, quantity: 1 }],
|
|
346
|
+
* checkoutPath: '/checkout',
|
|
347
|
+
* });
|
|
348
|
+
* window.location.href = url;
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
async createSessionUrl(opts) {
|
|
352
|
+
const session = await this.createSession({
|
|
353
|
+
items: opts.items,
|
|
354
|
+
currency: opts.currency,
|
|
355
|
+
returnUrl: opts.returnUrl,
|
|
356
|
+
customerEmail: opts.customerEmail,
|
|
357
|
+
customerFirstName: opts.customerFirstName,
|
|
358
|
+
customerLastName: opts.customerLastName
|
|
359
|
+
});
|
|
360
|
+
const origin = opts.origin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
361
|
+
const checkoutPath = opts.checkoutPath ?? "/checkout";
|
|
362
|
+
const params = new URLSearchParams({
|
|
363
|
+
checkoutToken: session.checkoutToken,
|
|
364
|
+
sessionToken: session.sessionToken
|
|
365
|
+
});
|
|
366
|
+
const url = `${origin}${checkoutPath}?${params.toString()}`;
|
|
367
|
+
return { ...session, url };
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Read `checkoutToken` and session token from a URL.
|
|
371
|
+
*
|
|
372
|
+
* Recognizes both the SDK convention (`?sessionToken=`) and the
|
|
373
|
+
* Tagada-hosted convention (`?token=`), so the same checkout page
|
|
374
|
+
* works whether the user came from `createSessionUrl()` or directly
|
|
375
|
+
* from a Tagada-hosted redirect.
|
|
376
|
+
*
|
|
377
|
+
* Returns `null` if `checkoutToken` is missing.
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```ts
|
|
381
|
+
* const tokens = CheckoutModule.parseTokensFromUrl();
|
|
382
|
+
* if (!tokens) {
|
|
383
|
+
* window.location.href = '/cart';
|
|
384
|
+
* return;
|
|
385
|
+
* }
|
|
386
|
+
* const session = await tagada.checkout.loadSession(
|
|
387
|
+
* tokens.checkoutToken,
|
|
388
|
+
* tokens.sessionToken,
|
|
389
|
+
* );
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
static parseTokensFromUrl(url) {
|
|
393
|
+
if (typeof url === "undefined" && typeof window === "undefined")
|
|
394
|
+
return null;
|
|
395
|
+
const href = url ?? window.location.href;
|
|
396
|
+
let parsed;
|
|
397
|
+
try {
|
|
398
|
+
parsed = new URL(href);
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const checkoutToken = parsed.searchParams.get("checkoutToken");
|
|
403
|
+
if (!checkoutToken)
|
|
404
|
+
return null;
|
|
405
|
+
const sessionToken = parsed.searchParams.get("sessionToken") ?? parsed.searchParams.get("token") ?? "";
|
|
406
|
+
return { checkoutToken, sessionToken };
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Add or update cart items on an existing session (upsert by product).
|
|
410
|
+
*
|
|
411
|
+
* Sends each item as `lineItems` to `POST /checkout-sessions/:id/line-items`.
|
|
412
|
+
* The endpoint UPSERTs by product: it adds new products and updates the
|
|
413
|
+
* variant/quantity of products already in the cart, but it does NOT remove
|
|
414
|
+
* products omitted from `items`, and an empty array is a no-op (it does not
|
|
415
|
+
* clear the cart). Each `quantity` must be >= 1 — removal is not supported by
|
|
416
|
+
* this endpoint.
|
|
417
|
+
*
|
|
418
|
+
* Returns a server-side mutation summary (counts of created/updated rows).
|
|
419
|
+
* It does NOT return the updated `CheckoutSession` — the underlying endpoint
|
|
420
|
+
* never has and the previous typing was a lie that quietly assigned
|
|
421
|
+
* `{ success, updated, ... }` into a `CheckoutSession` slot in `useCheckout`.
|
|
422
|
+
*
|
|
423
|
+
* Call `loadSession()` after this if you need the refreshed session
|
|
424
|
+
* (the React `useCheckout` hook does this for you).
|
|
425
|
+
*/
|
|
426
|
+
async updateCart(sessionId, items) {
|
|
427
|
+
const lineItems = items.map((i) => ({
|
|
428
|
+
variantId: i.variantId,
|
|
429
|
+
priceId: i.priceId,
|
|
430
|
+
quantity: i.quantity
|
|
431
|
+
}));
|
|
432
|
+
return this.http.post(
|
|
433
|
+
`/api/v1/checkout-sessions/${sessionId}/line-items`,
|
|
434
|
+
{ lineItems },
|
|
435
|
+
{ token: this.sessionToken }
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Update customer identity (email, name, phone).
|
|
440
|
+
*
|
|
441
|
+
* Call this before `updateAddress()` — the SDK remembers the values and
|
|
442
|
+
* includes them automatically when addresses are submitted.
|
|
443
|
+
*
|
|
444
|
+
* If you prefer a single call, use `updateCustomerAndAddress()` instead.
|
|
445
|
+
*/
|
|
446
|
+
async updateCustomer(sessionId, customer) {
|
|
447
|
+
if (customer.email)
|
|
448
|
+
this.customerEmail = customer.email;
|
|
449
|
+
if (customer.firstName)
|
|
450
|
+
this.customerFirstName = customer.firstName;
|
|
451
|
+
if (customer.lastName)
|
|
452
|
+
this.customerLastName = customer.lastName;
|
|
453
|
+
if (customer.phone)
|
|
454
|
+
this.customerPhone = customer.phone;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Update shipping and/or billing address on the session.
|
|
458
|
+
*
|
|
459
|
+
* Automatically includes customer name/phone stored from `updateCustomer()`.
|
|
460
|
+
* Uses the same atomic API call as the production checkout, so email,
|
|
461
|
+
* customer name, and addresses are all persisted together.
|
|
462
|
+
*
|
|
463
|
+
* **Shipped products**: pass `shippingAddress`. If `billingAddress` is omitted,
|
|
464
|
+
* the shipping address is automatically used as billing.
|
|
465
|
+
*
|
|
466
|
+
* **Digital products**: pass only `billingAddress` (no shipping needed).
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* // Shipped product — billing = shipping automatically
|
|
470
|
+
* await checkout.updateAddress(sessionId, {
|
|
471
|
+
* shippingAddress: { line1: '10 Rue de la Paix', city: 'Paris', postalCode: '75002', country: 'FR' },
|
|
472
|
+
* });
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* // Digital product — billing only
|
|
476
|
+
* await checkout.updateAddress(sessionId, {
|
|
477
|
+
* billingAddress: { country: 'FR' },
|
|
478
|
+
* });
|
|
479
|
+
*/
|
|
480
|
+
async updateAddress(sessionId, addresses) {
|
|
481
|
+
const shipping = this.mapAddress(addresses.shippingAddress);
|
|
482
|
+
const billing = this.mapAddress(addresses.billingAddress);
|
|
483
|
+
const effectiveShipping = shipping ?? billing ?? {};
|
|
484
|
+
const effectiveBilling = billing ?? shipping;
|
|
485
|
+
const differentBilling = !!(addresses.billingAddress && addresses.shippingAddress);
|
|
486
|
+
await this.http.post(
|
|
487
|
+
`/api/v1/checkout-sessions/${sessionId}/customer-and-session-info`,
|
|
488
|
+
{
|
|
489
|
+
checkoutSessionId: sessionId,
|
|
490
|
+
customerData: {
|
|
491
|
+
email: this.customerEmail
|
|
492
|
+
},
|
|
493
|
+
shippingAddress: effectiveShipping,
|
|
494
|
+
billingAddress: effectiveBilling ?? {},
|
|
495
|
+
differentBillingAddress: differentBilling
|
|
496
|
+
},
|
|
497
|
+
{ token: this.sessionToken }
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Set customer info + addresses in one call (mirrors production checkout).
|
|
502
|
+
*
|
|
503
|
+
* Equivalent to calling `updateCustomer()` then `updateAddress()`, but
|
|
504
|
+
* in a single network request.
|
|
505
|
+
*/
|
|
506
|
+
async updateCustomerAndAddress(sessionId, data) {
|
|
507
|
+
if (data.customer.email)
|
|
508
|
+
this.customerEmail = data.customer.email;
|
|
509
|
+
if (data.customer.firstName)
|
|
510
|
+
this.customerFirstName = data.customer.firstName;
|
|
511
|
+
if (data.customer.lastName)
|
|
512
|
+
this.customerLastName = data.customer.lastName;
|
|
513
|
+
if (data.customer.phone)
|
|
514
|
+
this.customerPhone = data.customer.phone;
|
|
515
|
+
const shipping = this.mapAddress(data.shippingAddress);
|
|
516
|
+
const billing = this.mapAddress(data.billingAddress);
|
|
517
|
+
const effectiveShipping = shipping ?? billing ?? {};
|
|
518
|
+
const effectiveBilling = billing ?? shipping;
|
|
519
|
+
const differentBilling = !!(data.billingAddress && data.shippingAddress);
|
|
520
|
+
await this.http.post(
|
|
521
|
+
`/api/v1/checkout-sessions/${sessionId}/customer-and-session-info`,
|
|
522
|
+
{
|
|
523
|
+
checkoutSessionId: sessionId,
|
|
524
|
+
customerData: {
|
|
525
|
+
email: this.customerEmail
|
|
526
|
+
},
|
|
527
|
+
shippingAddress: effectiveShipping,
|
|
528
|
+
billingAddress: effectiveBilling ?? {},
|
|
529
|
+
differentBillingAddress: differentBilling
|
|
530
|
+
},
|
|
531
|
+
{ token: this.sessionToken }
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
/** Map an SDK Address to the API shape, injecting stored customer fields. */
|
|
535
|
+
mapAddress(a) {
|
|
536
|
+
if (!a)
|
|
537
|
+
return void 0;
|
|
538
|
+
return stripEmpty({
|
|
539
|
+
firstName: a.firstName ?? this.customerFirstName,
|
|
540
|
+
lastName: a.lastName ?? this.customerLastName,
|
|
541
|
+
phone: a.phone ?? this.customerPhone,
|
|
542
|
+
address1: a.line1,
|
|
543
|
+
address2: a.line2,
|
|
544
|
+
city: a.city,
|
|
545
|
+
state: a.state,
|
|
546
|
+
postal: a.postalCode,
|
|
547
|
+
country: a.country
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
/** Apply a promotion code to the session. */
|
|
551
|
+
async applyPromoCode(sessionId, code) {
|
|
552
|
+
await this.http.post(
|
|
553
|
+
`/api/v1/checkout-sessions/${sessionId}/promotions/apply`,
|
|
554
|
+
{ checkoutSessionId: sessionId, code },
|
|
555
|
+
{ token: this.sessionToken }
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
/** Remove a promotion from the session by promotion ID. */
|
|
559
|
+
async removePromoCode(sessionId, promotionId) {
|
|
560
|
+
const promoId = promotionId ?? "default";
|
|
561
|
+
await this.http.delete(
|
|
562
|
+
`/api/v1/checkout-sessions/${sessionId}/promotions/${promoId}`,
|
|
563
|
+
{ token: this.sessionToken }
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Fetch available shipping rates for the session's address.
|
|
568
|
+
* Only relevant for **shipped products** — digital products can skip this step.
|
|
569
|
+
*
|
|
570
|
+
* Requires a shipping address to be set first via `updateAddress()`.
|
|
571
|
+
* Returns an empty array if no rates are configured or address is missing.
|
|
572
|
+
*/
|
|
573
|
+
async getShippingRates(sessionId) {
|
|
574
|
+
const res = await this.http.get(
|
|
575
|
+
`/api/v1/checkout-sessions/${sessionId}/shipping-rates`,
|
|
576
|
+
{ token: this.sessionToken }
|
|
577
|
+
);
|
|
578
|
+
return (res.rates ?? []).map((r) => this.normalizeShippingRate(r));
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Normalize the v1 rates payload (`shippingRateName`, `estimatedDeliveryTime`)
|
|
582
|
+
* to the SDK's `ShippingRate` shape (`name`, `estimatedDays`). Without
|
|
583
|
+
* this the consumer's UI shows blank rate names — the demo's "Delivery"
|
|
584
|
+
* radios were rendering only the price, no label.
|
|
585
|
+
*/
|
|
586
|
+
normalizeShippingRate(r) {
|
|
587
|
+
return {
|
|
588
|
+
id: r.id,
|
|
589
|
+
name: r.name ?? r.shippingRateName ?? "",
|
|
590
|
+
description: r.description ?? void 0,
|
|
591
|
+
amount: r.amount ?? 0,
|
|
592
|
+
currency: r.currency ?? "USD",
|
|
593
|
+
estimatedDays: r.estimatedDays ?? r.estimatedDeliveryTime ?? void 0
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Select a shipping rate for the session.
|
|
598
|
+
* Only relevant for **shipped products** — digital products can skip this step.
|
|
599
|
+
*/
|
|
600
|
+
async selectShippingRate(sessionId, rateId) {
|
|
601
|
+
await this.http.post(
|
|
602
|
+
`/api/v1/checkout-sessions/${sessionId}/shipping-rate`,
|
|
603
|
+
{ checkoutSessionId: sessionId, shippingRateId: rateId },
|
|
604
|
+
{ token: this.sessionToken }
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Normalize the raw v2 API response into a cleaner CheckoutSession shape.
|
|
609
|
+
* The raw response has nested `checkoutSession` + `summary` -- we flatten it.
|
|
610
|
+
*/
|
|
611
|
+
normalizeSession(raw) {
|
|
612
|
+
var _a;
|
|
613
|
+
const cs = raw.checkoutSession;
|
|
614
|
+
const orderSummaries = cs.orderSummaries ?? [];
|
|
615
|
+
const summary = raw.summary ?? cs.summary ?? orderSummaries[0] ?? {};
|
|
616
|
+
const lineItems = cs.sessionLineItems ?? [];
|
|
617
|
+
const customer = cs.customer;
|
|
618
|
+
const sessionCurrency = (_a = summary.currency ?? cs.selectedPresentmentCurrency) == null ? void 0 : _a.toUpperCase();
|
|
619
|
+
const items = lineItems.map((li) => {
|
|
620
|
+
const price = li.price;
|
|
621
|
+
const variant = li.variant ?? (price == null ? void 0 : price.variant) ?? {};
|
|
622
|
+
const product = li.product ?? variant.product ?? {};
|
|
623
|
+
const currencyOpts = (price == null ? void 0 : price.currencyOptions) ?? {};
|
|
624
|
+
const priceEntry = (sessionCurrency ? currencyOpts[sessionCurrency] : void 0) ?? currencyOpts[Object.keys(currencyOpts)[0]];
|
|
625
|
+
const amount = (priceEntry == null ? void 0 : priceEntry.amount) ?? 0;
|
|
626
|
+
return {
|
|
627
|
+
id: li.id,
|
|
628
|
+
variantId: li.variantId,
|
|
629
|
+
priceId: li.priceId,
|
|
630
|
+
productName: product.name ?? "",
|
|
631
|
+
variantName: variant.name ?? "",
|
|
632
|
+
quantity: li.quantity ?? 1,
|
|
633
|
+
unitAmount: amount,
|
|
634
|
+
totalAmount: amount * (li.quantity ?? 1),
|
|
635
|
+
currency: sessionCurrency ?? "USD",
|
|
636
|
+
imageUrl: variant.imageUrl ?? product.imageUrl ?? ""
|
|
637
|
+
};
|
|
638
|
+
});
|
|
639
|
+
const summaryItems = summary.items ?? [];
|
|
640
|
+
return {
|
|
641
|
+
id: cs.id,
|
|
642
|
+
storeId: cs.storeId,
|
|
643
|
+
checkoutToken: cs.checkoutToken,
|
|
644
|
+
currency: sessionCurrency ?? "USD",
|
|
645
|
+
items,
|
|
646
|
+
totals: {
|
|
647
|
+
subtotal: summary.subtotalAmount ?? 0,
|
|
648
|
+
discount: summary.totalPromotionAmount ?? 0,
|
|
649
|
+
shipping: summary.shippingCost ?? 0,
|
|
650
|
+
tax: summary.totalTaxAmount ?? 0,
|
|
651
|
+
total: summary.totalAmount ?? summaryItems.reduce((s, i) => s + (i.amount ?? 0), 0),
|
|
652
|
+
currency: sessionCurrency ?? "USD"
|
|
653
|
+
},
|
|
654
|
+
customer: customer ? {
|
|
655
|
+
id: customer.id,
|
|
656
|
+
email: customer.email,
|
|
657
|
+
firstName: customer.firstName,
|
|
658
|
+
lastName: customer.lastName,
|
|
659
|
+
phone: customer.phone
|
|
660
|
+
} : void 0,
|
|
661
|
+
shippingAddress: this.normalizeAddress(cs.shippingAddress),
|
|
662
|
+
billingAddress: this.normalizeAddress(cs.billingAddress),
|
|
663
|
+
selectedShippingRateId: cs.shippingRateId ?? null,
|
|
664
|
+
metadata: cs.metadata
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Map the raw API address shape (`address1`, `address2`, `postal`) onto
|
|
669
|
+
* the SDK's `Address` shape (`line1`, `line2`, `postalCode`). Without
|
|
670
|
+
* this, `session.shippingAddress.line1` is silently undefined after a
|
|
671
|
+
* reload — the type was lying about the actual key names — which is
|
|
672
|
+
* what makes the demo lose form state on refresh.
|
|
673
|
+
*/
|
|
674
|
+
normalizeAddress(raw) {
|
|
675
|
+
if (!raw || typeof raw !== "object")
|
|
676
|
+
return void 0;
|
|
677
|
+
const a = raw;
|
|
678
|
+
const out = {
|
|
679
|
+
firstName: a.firstName,
|
|
680
|
+
lastName: a.lastName,
|
|
681
|
+
line1: a.line1 ?? a.address1,
|
|
682
|
+
line2: a.line2 ?? a.address2,
|
|
683
|
+
city: a.city,
|
|
684
|
+
state: a.state,
|
|
685
|
+
postalCode: a.postalCode ?? a.postal,
|
|
686
|
+
country: a.country,
|
|
687
|
+
phone: a.phone
|
|
688
|
+
};
|
|
689
|
+
return Object.values(out).some((v) => v !== void 0 && v !== "") ? out : void 0;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
function stripEmpty(obj) {
|
|
693
|
+
const result = {};
|
|
694
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
695
|
+
if (v !== void 0 && v !== null && v !== "") {
|
|
696
|
+
result[k] = v;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/core/helpers/walletContact.ts
|
|
703
|
+
function applePayContactToAddress(contact) {
|
|
704
|
+
var _a, _b;
|
|
705
|
+
if (!contact)
|
|
706
|
+
return void 0;
|
|
707
|
+
const address = {
|
|
708
|
+
firstName: contact.givenName,
|
|
709
|
+
lastName: contact.familyName,
|
|
710
|
+
line1: (_a = contact.addressLines) == null ? void 0 : _a[0],
|
|
711
|
+
line2: (_b = contact.addressLines) == null ? void 0 : _b[1],
|
|
712
|
+
city: contact.locality,
|
|
713
|
+
state: contact.administrativeArea,
|
|
714
|
+
postalCode: contact.postalCode,
|
|
715
|
+
country: contact.countryCode,
|
|
716
|
+
phone: contact.phoneNumber
|
|
717
|
+
};
|
|
718
|
+
return isUsable(address) ? address : void 0;
|
|
719
|
+
}
|
|
720
|
+
function googlePayContactToAddress(contact) {
|
|
721
|
+
if (!contact)
|
|
722
|
+
return void 0;
|
|
723
|
+
const [firstName, ...rest] = (contact.name ?? "").trim().split(/\s+/);
|
|
724
|
+
const address = {
|
|
725
|
+
firstName: firstName || void 0,
|
|
726
|
+
lastName: rest.length ? rest.join(" ") : void 0,
|
|
727
|
+
line1: contact.address1,
|
|
728
|
+
line2: [contact.address2, contact.address3].filter(Boolean).join(" ") || void 0,
|
|
729
|
+
city: contact.locality,
|
|
730
|
+
state: contact.administrativeArea,
|
|
731
|
+
postalCode: contact.postalCode,
|
|
732
|
+
country: contact.countryCode,
|
|
733
|
+
phone: contact.phoneNumber
|
|
734
|
+
};
|
|
735
|
+
return isUsable(address) ? address : void 0;
|
|
736
|
+
}
|
|
737
|
+
function isUsable(address) {
|
|
738
|
+
var _a, _b;
|
|
739
|
+
return Boolean(((_a = address.line1) == null ? void 0 : _a.trim()) && ((_b = address.country) == null ? void 0 : _b.trim()));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/core/modules/payment.ts
|
|
743
|
+
var PaymentModule = class {
|
|
744
|
+
constructor(http, storeId, environment, checkout) {
|
|
745
|
+
this.http = http;
|
|
746
|
+
this.storeId = storeId;
|
|
747
|
+
this.environment = environment;
|
|
748
|
+
this.checkout = checkout;
|
|
749
|
+
}
|
|
750
|
+
// ---------------------------------------------------------------------------
|
|
751
|
+
// Discovery: what payment methods are available?
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
/**
|
|
754
|
+
* Get the full payment setup config for a checkout session.
|
|
755
|
+
* Returns a map of payment methods with their configuration
|
|
756
|
+
* (card, apple_pay, google_pay, klarna, etc.).
|
|
757
|
+
*
|
|
758
|
+
* Priority chain:
|
|
759
|
+
* 1. Explicit `paymentSetupConfig` parameter (developer override)
|
|
760
|
+
* 2. `window.__TGD_STEP_CONFIG__` injection (Tagada routing infra)
|
|
761
|
+
* 3. API fallback (`GET /api/v1/payment-methods`)
|
|
762
|
+
*/
|
|
763
|
+
async getPaymentSetup(checkoutSessionId, opts) {
|
|
764
|
+
if (opts == null ? void 0 : opts.paymentSetupConfig) {
|
|
765
|
+
return opts.paymentSetupConfig;
|
|
766
|
+
}
|
|
767
|
+
const injected = this.readInjectedStepConfig();
|
|
768
|
+
if (injected) {
|
|
769
|
+
return injected;
|
|
770
|
+
}
|
|
771
|
+
const methods = await this.http.get(
|
|
772
|
+
"/api/v1/payment-methods",
|
|
773
|
+
{ params: { checkoutSessionId } }
|
|
774
|
+
);
|
|
775
|
+
const config = {};
|
|
776
|
+
for (const m of methods.methods ?? []) {
|
|
777
|
+
const key = m.provider ? `${m.method}:${m.provider}` : m.method;
|
|
778
|
+
config[key] = m;
|
|
779
|
+
}
|
|
780
|
+
return config;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Get express payment methods (Apple Pay, Google Pay, PayPal, etc.)
|
|
784
|
+
* filtered to only those available in the current browser.
|
|
785
|
+
*/
|
|
786
|
+
async getExpressMethods(checkoutSessionId, opts) {
|
|
787
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
788
|
+
const setup = await this.getPaymentSetup(checkoutSessionId, opts);
|
|
789
|
+
const result = {};
|
|
790
|
+
for (const [key, config] of Object.entries(setup)) {
|
|
791
|
+
if (!config.enabled)
|
|
792
|
+
continue;
|
|
793
|
+
if (key.startsWith("express_checkout:") && config.methods) {
|
|
794
|
+
const base2 = {
|
|
795
|
+
available: true,
|
|
796
|
+
processorId: config.processorId,
|
|
797
|
+
publishableKey: config.publishableKey,
|
|
798
|
+
metadata: config.metadata
|
|
799
|
+
};
|
|
800
|
+
if ((_a = config.methods.apple_pay) == null ? void 0 : _a.enabled) {
|
|
801
|
+
const hasApplePay = typeof window !== "undefined" && "ApplePaySession" in window && ((_c = (_b = window.ApplePaySession) == null ? void 0 : _b.canMakePayments) == null ? void 0 : _c.call(_b));
|
|
802
|
+
result.applePay = { ...base2, type: "apple_pay", available: !!hasApplePay };
|
|
803
|
+
}
|
|
804
|
+
if ((_d = config.methods.google_pay) == null ? void 0 : _d.enabled) {
|
|
805
|
+
result.googlePay = { ...base2, type: "google_pay" };
|
|
806
|
+
}
|
|
807
|
+
if ((_e = config.methods.link) == null ? void 0 : _e.enabled) {
|
|
808
|
+
result.link = { ...base2, type: "link" };
|
|
809
|
+
}
|
|
810
|
+
if ((_f = config.methods.klarna_express) == null ? void 0 : _f.enabled) {
|
|
811
|
+
result.klarna = { ...base2, type: "klarna_express" };
|
|
812
|
+
}
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
if (!config.express)
|
|
816
|
+
continue;
|
|
817
|
+
const method = config.method;
|
|
818
|
+
const base = {
|
|
819
|
+
type: method,
|
|
820
|
+
available: true,
|
|
821
|
+
processorId: config.processorId,
|
|
822
|
+
publishableKey: config.publishableKey,
|
|
823
|
+
metadata: config.metadata
|
|
824
|
+
};
|
|
825
|
+
if (method === "apple_pay" || config.type === "apple_pay") {
|
|
826
|
+
const hasApplePay = typeof window !== "undefined" && "ApplePaySession" in window && ((_h = (_g = window.ApplePaySession) == null ? void 0 : _g.canMakePayments) == null ? void 0 : _h.call(_g));
|
|
827
|
+
result.applePay = { ...base, available: !!hasApplePay };
|
|
828
|
+
} else if (method === "google_pay" || config.type === "google_pay") {
|
|
829
|
+
result.googlePay = base;
|
|
830
|
+
} else if (method === "paypal") {
|
|
831
|
+
result.paypal = base;
|
|
832
|
+
} else if (method === "klarna" || method === "klarna_express") {
|
|
833
|
+
result.klarna = base;
|
|
834
|
+
} else if (method === "link") {
|
|
835
|
+
result.link = base;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return result;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Get store-level payment config (3DS flags, default flow ID, etc.).
|
|
842
|
+
*/
|
|
843
|
+
async getStoreConfig() {
|
|
844
|
+
const res = await this.http.get(
|
|
845
|
+
"/api/v1/checkout/config",
|
|
846
|
+
{ params: { storeId: this.storeId } }
|
|
847
|
+
);
|
|
848
|
+
return res.config ?? res;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Convenience: return only enabled payment method keys.
|
|
852
|
+
*/
|
|
853
|
+
async getEnabledMethods(checkoutSessionId, opts) {
|
|
854
|
+
const setup = await this.getPaymentSetup(checkoutSessionId, opts);
|
|
855
|
+
return Object.entries(setup).filter(([, c]) => c.enabled).map(([key]) => key);
|
|
856
|
+
}
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
// Tokenization (delegates to @tagadapay/core-js if available)
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
/**
|
|
861
|
+
* Tokenize a card using @tagadapay/core-js.
|
|
862
|
+
* Requires `@tagadapay/core-js` to be installed as a peer dependency.
|
|
863
|
+
*
|
|
864
|
+
* Card data is sent directly to Basis Theory for tokenization and never
|
|
865
|
+
* reaches TagadaPay servers. The returned `tagadaToken` is safe to pass
|
|
866
|
+
* to the TagadaPay API.
|
|
867
|
+
*
|
|
868
|
+
* **PCI note:** Using this method means card data passes through your
|
|
869
|
+
* JavaScript environment. You are responsible for PCI DSS SAQ-A-EP
|
|
870
|
+
* compliance. See https://docs.tagada.io/developer-tools/headless-sdk/pci-compliance
|
|
871
|
+
*
|
|
872
|
+
* @returns tagadaToken (base64 string) and rawToken with SCA metadata.
|
|
873
|
+
*/
|
|
874
|
+
async tokenizeCard(card) {
|
|
875
|
+
const coreJs = await this.loadCoreJs();
|
|
876
|
+
const tokenizer = new coreJs.Tokenizer({ environment: this.environment });
|
|
877
|
+
await tokenizer.initialize();
|
|
878
|
+
const sanitized = {
|
|
879
|
+
cardNumber: card.cardNumber,
|
|
880
|
+
expiryDate: card.expiryDate,
|
|
881
|
+
cvc: card.cvc
|
|
882
|
+
};
|
|
883
|
+
if (card.cardholderName)
|
|
884
|
+
sanitized.cardholderName = card.cardholderName;
|
|
885
|
+
const rawToken = await tokenizer.tokenizeCardRaw(sanitized);
|
|
886
|
+
const tagadaToken = coreJs.createTagadaToken(
|
|
887
|
+
rawToken,
|
|
888
|
+
tokenizer.getProvider().getProviderName()
|
|
889
|
+
);
|
|
890
|
+
return { tagadaToken, rawToken };
|
|
891
|
+
}
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// Instrument creation
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
/**
|
|
896
|
+
* Create a reusable payment instrument from a TagadaToken.
|
|
897
|
+
* Session-authenticated (uses the checkout session JWT).
|
|
898
|
+
*/
|
|
899
|
+
async createInstrument(opts) {
|
|
900
|
+
return this.http.post("/api/v1/payment/create-instrument-from-token", {
|
|
901
|
+
tagadaToken: opts.tagadaToken
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
// ---------------------------------------------------------------------------
|
|
905
|
+
// 3DS
|
|
906
|
+
// ---------------------------------------------------------------------------
|
|
907
|
+
/**
|
|
908
|
+
* Create a 3DS session on the TagadaPay backend.
|
|
909
|
+
* This persists the session so the processor can use it during payment.
|
|
910
|
+
*/
|
|
911
|
+
async create3dsSession(opts) {
|
|
912
|
+
return this.http.post("/api/public/v1/threeds/create-session", {
|
|
913
|
+
provider: opts.provider ?? "basis_theory",
|
|
914
|
+
storeId: this.storeId,
|
|
915
|
+
paymentInstrumentId: opts.paymentInstrumentId,
|
|
916
|
+
sessionData: opts.sessionData
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
// ---------------------------------------------------------------------------
|
|
920
|
+
// Payment processing
|
|
921
|
+
// ---------------------------------------------------------------------------
|
|
922
|
+
/**
|
|
923
|
+
* High-level pay: single API call that decodes tagadaToken server-side,
|
|
924
|
+
* creates the payment instrument, and processes payment.
|
|
925
|
+
*
|
|
926
|
+
* Also accepts `paymentInstrumentId` directly for pre-created instruments.
|
|
927
|
+
*/
|
|
928
|
+
async pay(opts) {
|
|
929
|
+
if (opts.tagadaToken && !opts.paymentInstrumentId) {
|
|
930
|
+
return this.http.post("/api/v1/payment/pay-with-token", {
|
|
931
|
+
tagadaToken: opts.tagadaToken,
|
|
932
|
+
checkoutSessionId: opts.checkoutSessionId,
|
|
933
|
+
returnUrl: opts.returnUrl,
|
|
934
|
+
paymentFlowId: opts.paymentFlowId,
|
|
935
|
+
processorId: opts.processorId,
|
|
936
|
+
paymentMethod: opts.paymentMethod,
|
|
937
|
+
isExpress: opts.isExpress,
|
|
938
|
+
metadata: opts.metadata
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return this.http.post("/api/public/v1/checkout/pay-v2", {
|
|
942
|
+
checkoutSessionId: opts.checkoutSessionId,
|
|
943
|
+
paymentInstrumentId: opts.paymentInstrumentId,
|
|
944
|
+
storeId: this.storeId,
|
|
945
|
+
paymentFlowId: opts.paymentFlowId,
|
|
946
|
+
threedsSessionId: opts.threedsSessionId,
|
|
947
|
+
processorId: opts.processorId,
|
|
948
|
+
paymentMethod: opts.paymentMethod,
|
|
949
|
+
isExpress: opts.isExpress,
|
|
950
|
+
metadata: opts.metadata,
|
|
951
|
+
returnUrl: opts.returnUrl
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Low-level pay: process payment with an existing instrument or for APM/express flows
|
|
956
|
+
* where no instrument is needed (redirect-based methods).
|
|
957
|
+
*/
|
|
958
|
+
async payDirect(opts) {
|
|
959
|
+
return this.http.post("/api/public/v1/checkout/pay-v2", {
|
|
960
|
+
checkoutSessionId: opts.checkoutSessionId,
|
|
961
|
+
paymentInstrumentId: opts.paymentInstrumentId ?? "",
|
|
962
|
+
storeId: this.storeId,
|
|
963
|
+
paymentFlowId: opts.paymentFlowId,
|
|
964
|
+
processorId: opts.processorId,
|
|
965
|
+
paymentMethod: opts.paymentMethod,
|
|
966
|
+
isExpress: opts.isExpress,
|
|
967
|
+
threedsSessionId: opts.threedsSessionId,
|
|
968
|
+
returnUrl: opts.returnUrl
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Persist a wallet contact onto the session before the charge.
|
|
973
|
+
*
|
|
974
|
+
* Express wallets are the one checkout where the buyer never fills an address
|
|
975
|
+
* form: they approve one inside the sheet, and it comes back on the
|
|
976
|
+
* authorization event. Studio's `ExpressPaymentWidget` writes it to the
|
|
977
|
+
* session; the headless path used to drop it, which is how a year of orders
|
|
978
|
+
* reached merchants with NULL shipping, billing and customer addresses
|
|
979
|
+
* (#TAG-1989).
|
|
980
|
+
*
|
|
981
|
+
* Never invents data: a contact with no street line or no country maps to
|
|
982
|
+
* `undefined` and the session is left exactly as it was. Failures are NOT
|
|
983
|
+
* swallowed — if we hold an address and cannot store it, the charge must not
|
|
984
|
+
* proceed, or we are back to the bug this fixes.
|
|
985
|
+
*/
|
|
986
|
+
async syncWalletContact(checkoutSessionId, contact) {
|
|
987
|
+
const { shippingAddress, billingAddress } = contact;
|
|
988
|
+
if (!shippingAddress && !billingAddress)
|
|
989
|
+
return;
|
|
990
|
+
if (!this.checkout) {
|
|
991
|
+
throw new Error(
|
|
992
|
+
"PaymentModule cannot persist the wallet contact: no CheckoutModule was provided. Build the SDK through createTagadaClient() rather than constructing PaymentModule directly."
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
const customer = {};
|
|
996
|
+
if (contact.email)
|
|
997
|
+
customer.email = contact.email;
|
|
998
|
+
if (contact.phone)
|
|
999
|
+
customer.phone = contact.phone;
|
|
1000
|
+
if (contact.firstName)
|
|
1001
|
+
customer.firstName = contact.firstName;
|
|
1002
|
+
if (contact.lastName)
|
|
1003
|
+
customer.lastName = contact.lastName;
|
|
1004
|
+
await this.checkout.updateCustomerAndAddress(checkoutSessionId, {
|
|
1005
|
+
customer,
|
|
1006
|
+
shippingAddress,
|
|
1007
|
+
billingAddress
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Process an Apple Pay payment.
|
|
1012
|
+
* Tokenizes the Apple Pay token and processes payment in one call.
|
|
1013
|
+
*
|
|
1014
|
+
* Pass the contacts from `onpaymentauthorized` — `event.payment.billingContact`
|
|
1015
|
+
* and `event.payment.shippingContact` — and they are written to the session
|
|
1016
|
+
* before the charge, so the address the buyer approved in the wallet sheet
|
|
1017
|
+
* lands on the order (#TAG-1989).
|
|
1018
|
+
*
|
|
1019
|
+
* Apple only returns them when the payment request asked for them, so the
|
|
1020
|
+
* `ApplePaySession` request needs:
|
|
1021
|
+
*
|
|
1022
|
+
* ```js
|
|
1023
|
+
* requiredBillingContactFields: ['postalAddress'],
|
|
1024
|
+
* requiredShippingContactFields: ['name', 'phone', 'email', 'postalAddress'],
|
|
1025
|
+
* ```
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* session.onpaymentauthorized = async (event) => {
|
|
1029
|
+
* await tagada.payment.processApplePay({
|
|
1030
|
+
* checkoutSessionId,
|
|
1031
|
+
* applePayToken: event.payment.token,
|
|
1032
|
+
* billingContact: event.payment.billingContact,
|
|
1033
|
+
* shippingContact: event.payment.shippingContact,
|
|
1034
|
+
* });
|
|
1035
|
+
* };
|
|
1036
|
+
*/
|
|
1037
|
+
async processApplePay(opts) {
|
|
1038
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1039
|
+
await this.syncWalletContact(opts.checkoutSessionId, {
|
|
1040
|
+
shippingAddress: applePayContactToAddress(opts.shippingContact),
|
|
1041
|
+
billingAddress: applePayContactToAddress(opts.billingContact),
|
|
1042
|
+
email: ((_a = opts.shippingContact) == null ? void 0 : _a.emailAddress) ?? ((_b = opts.billingContact) == null ? void 0 : _b.emailAddress),
|
|
1043
|
+
phone: ((_c = opts.shippingContact) == null ? void 0 : _c.phoneNumber) ?? ((_d = opts.billingContact) == null ? void 0 : _d.phoneNumber),
|
|
1044
|
+
firstName: ((_e = opts.shippingContact) == null ? void 0 : _e.givenName) ?? ((_f = opts.billingContact) == null ? void 0 : _f.givenName),
|
|
1045
|
+
lastName: ((_g = opts.shippingContact) == null ? void 0 : _g.familyName) ?? ((_h = opts.billingContact) == null ? void 0 : _h.familyName)
|
|
1046
|
+
});
|
|
1047
|
+
const coreJs = await this.loadCoreJs();
|
|
1048
|
+
const tokenizer = new coreJs.Tokenizer({ environment: this.environment });
|
|
1049
|
+
await tokenizer.initialize();
|
|
1050
|
+
const { tagadaToken } = await tokenizer.tokenizeApplePay(opts.applePayToken);
|
|
1051
|
+
return this.pay({
|
|
1052
|
+
checkoutSessionId: opts.checkoutSessionId,
|
|
1053
|
+
tagadaToken,
|
|
1054
|
+
paymentFlowId: opts.paymentFlowId,
|
|
1055
|
+
processorId: opts.processorId,
|
|
1056
|
+
paymentMethod: "apple_pay",
|
|
1057
|
+
isExpress: true
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Process a Google Pay payment.
|
|
1062
|
+
* Tokenizes the Google Pay token and processes payment in one call.
|
|
1063
|
+
*
|
|
1064
|
+
* Pass `paymentData.shippingAddress` / the billing address off
|
|
1065
|
+
* `paymentMethodData.info.billingAddress` and they are persisted to the
|
|
1066
|
+
* session before the charge (#TAG-1989). Google only returns them when the
|
|
1067
|
+
* request sets `shippingAddressRequired` / `billingAddressRequired`.
|
|
1068
|
+
*/
|
|
1069
|
+
async processGooglePay(opts) {
|
|
1070
|
+
var _a, _b, _c, _d;
|
|
1071
|
+
await this.syncWalletContact(opts.checkoutSessionId, {
|
|
1072
|
+
shippingAddress: googlePayContactToAddress(opts.shippingAddress),
|
|
1073
|
+
billingAddress: googlePayContactToAddress(opts.billingAddress),
|
|
1074
|
+
email: opts.email ?? ((_a = opts.shippingAddress) == null ? void 0 : _a.emailAddress) ?? ((_b = opts.billingAddress) == null ? void 0 : _b.emailAddress),
|
|
1075
|
+
phone: ((_c = opts.shippingAddress) == null ? void 0 : _c.phoneNumber) ?? ((_d = opts.billingAddress) == null ? void 0 : _d.phoneNumber)
|
|
1076
|
+
});
|
|
1077
|
+
const coreJs = await this.loadCoreJs();
|
|
1078
|
+
const tokenizer = new coreJs.Tokenizer({ environment: this.environment });
|
|
1079
|
+
await tokenizer.initialize();
|
|
1080
|
+
const { tagadaToken } = await tokenizer.tokenizeGooglePay(opts.googlePayToken);
|
|
1081
|
+
return this.pay({
|
|
1082
|
+
checkoutSessionId: opts.checkoutSessionId,
|
|
1083
|
+
tagadaToken,
|
|
1084
|
+
paymentFlowId: opts.paymentFlowId,
|
|
1085
|
+
processorId: opts.processorId,
|
|
1086
|
+
paymentMethod: "google_pay",
|
|
1087
|
+
isExpress: true
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Process an APM payment (Klarna, iDEAL, etc.) via redirect.
|
|
1092
|
+
* These typically don't require a payment instrument -- the processor handles the redirect.
|
|
1093
|
+
*/
|
|
1094
|
+
async processApm(opts) {
|
|
1095
|
+
return this.payDirect({
|
|
1096
|
+
checkoutSessionId: opts.checkoutSessionId,
|
|
1097
|
+
paymentMethod: opts.paymentMethod,
|
|
1098
|
+
processorId: opts.processorId,
|
|
1099
|
+
paymentFlowId: opts.paymentFlowId
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Initiate a Stripe Express Checkout payment (Apple Pay, Google Pay, Link, etc.).
|
|
1104
|
+
*
|
|
1105
|
+
* Calls `pay-v2` with `isExpress: true`, which returns a `clientSecret` in
|
|
1106
|
+
* `payment.requireActionData.metadata.stripeExpressCheckout.clientSecret`.
|
|
1107
|
+
* The caller must then call `stripe.confirmPayment()` with that secret.
|
|
1108
|
+
*
|
|
1109
|
+
* Used by the `StripeExpressCheckout` class and `useStripeExpress` hook.
|
|
1110
|
+
* Not typically called directly — use the Stripe integration classes instead.
|
|
1111
|
+
*/
|
|
1112
|
+
async processStripeExpress(opts) {
|
|
1113
|
+
return this.payDirect({
|
|
1114
|
+
checkoutSessionId: opts.checkoutSessionId,
|
|
1115
|
+
processorId: opts.processorId,
|
|
1116
|
+
paymentMethod: opts.paymentMethod,
|
|
1117
|
+
isExpress: true
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
// ---------------------------------------------------------------------------
|
|
1121
|
+
// High-level orchestrator — the ONE function beginners should call
|
|
1122
|
+
// ---------------------------------------------------------------------------
|
|
1123
|
+
/**
|
|
1124
|
+
* Process a payment end-to-end: pay, handle 3DS / redirects, poll until done.
|
|
1125
|
+
*
|
|
1126
|
+
* This is the **recommended** way to process payments. It wraps `pay()`, detects
|
|
1127
|
+
* `requireAction`, runs 3DS challenges (if `@tagadapay/core-js` is installed),
|
|
1128
|
+
* and polls until a terminal status.
|
|
1129
|
+
*
|
|
1130
|
+
* Returns a discriminated union so you can pattern-match on `result.status`:
|
|
1131
|
+
*
|
|
1132
|
+
* @example
|
|
1133
|
+
* ```ts
|
|
1134
|
+
* const result = await payment.processPayment({
|
|
1135
|
+
* checkoutSessionId: session.id,
|
|
1136
|
+
* tagadaToken,
|
|
1137
|
+
* });
|
|
1138
|
+
*
|
|
1139
|
+
* switch (result.status) {
|
|
1140
|
+
* case 'succeeded':
|
|
1141
|
+
* console.log('Paid!', result.payment.id);
|
|
1142
|
+
* break;
|
|
1143
|
+
* case 'requires_redirect':
|
|
1144
|
+
* // In browser: window.location.href = result.redirectUrl
|
|
1145
|
+
* // The React hook handles this automatically.
|
|
1146
|
+
* break;
|
|
1147
|
+
* case 'failed':
|
|
1148
|
+
* console.error(result.error);
|
|
1149
|
+
* break;
|
|
1150
|
+
* }
|
|
1151
|
+
* ```
|
|
1152
|
+
*/
|
|
1153
|
+
async processPayment(opts) {
|
|
1154
|
+
if (!opts.returnUrl && typeof window !== "undefined") {
|
|
1155
|
+
opts = { ...opts, returnUrl: window.location.href };
|
|
1156
|
+
}
|
|
1157
|
+
const payResult = await this.pay(opts);
|
|
1158
|
+
return this.finalize(payResult, { returnUrl: opts.returnUrl });
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Resolve a raw `PayResult` (from any pay endpoint — `pay()`, `payDirect()`,
|
|
1162
|
+
* `processApplePay()`, `offers.payPreviewedOffer()`, …) into a typed
|
|
1163
|
+
* `ProcessPaymentResult` (`succeeded` / `requires_redirect` / `failed`
|
|
1164
|
+
* / `pending`).
|
|
1165
|
+
*
|
|
1166
|
+
* Handles 3DS routing (modal challenge via core-js, or bank redirect)
|
|
1167
|
+
* and async polling until the payment reaches a terminal status.
|
|
1168
|
+
*
|
|
1169
|
+
* This is the same logic `processPayment()` runs after `pay()` — exposed
|
|
1170
|
+
* so callers (and the offers module) that already have a raw `PayResult`
|
|
1171
|
+
* don't need to re-pay.
|
|
1172
|
+
*/
|
|
1173
|
+
async finalize(input, opts) {
|
|
1174
|
+
var _a, _b;
|
|
1175
|
+
const payment = input.payment;
|
|
1176
|
+
const returnUrl = (opts == null ? void 0 : opts.returnUrl) ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
1177
|
+
if (payment.status === "succeeded") {
|
|
1178
|
+
return { status: "succeeded", payment, order: input.order };
|
|
1179
|
+
}
|
|
1180
|
+
if (payment.status === "failed" || payment.status === "declined") {
|
|
1181
|
+
const msg = ((_a = payment.requireActionData) == null ? void 0 : _a.message) || ((_b = payment.requireActionData) == null ? void 0 : _b.errorCode) || `Payment ${payment.status}`;
|
|
1182
|
+
return { status: "failed", error: msg, payment };
|
|
1183
|
+
}
|
|
1184
|
+
if (payment.requireAction && payment.requireAction !== "none" && payment.requireActionData) {
|
|
1185
|
+
return this.handlePaymentAction(payment, { returnUrl });
|
|
1186
|
+
}
|
|
1187
|
+
return this.pollUntilDone(payment.id);
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Resume a payment after the user returns from a redirect (3DS, processor auth, APM).
|
|
1191
|
+
*
|
|
1192
|
+
* Called automatically by `usePayment()` in React when it detects return-URL params.
|
|
1193
|
+
* You can also call it manually in non-React apps.
|
|
1194
|
+
*/
|
|
1195
|
+
async resumeAfterRedirect(paymentId) {
|
|
1196
|
+
return this.pollUntilDone(paymentId);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Detect a return from a 3DS / processor redirect by inspecting the
|
|
1200
|
+
* current page URL, then poll the payment to completion.
|
|
1201
|
+
*
|
|
1202
|
+
* The post-3DS URL convention used by Tagada-routed processors is
|
|
1203
|
+
* `?paymentAction=requireAction&paymentActionStatus=completed&paymentId=pay_xxx`.
|
|
1204
|
+
* If those params are present, this method polls the payment, cleans the
|
|
1205
|
+
* URL (so a refresh doesn't re-trigger), and returns the resolved
|
|
1206
|
+
* `ProcessPaymentResult`. Otherwise it returns `null`.
|
|
1207
|
+
*
|
|
1208
|
+
* Use this in vanilla apps as the first thing on page load. The React
|
|
1209
|
+
* `usePayment` hook does the same automatically — don't call this when
|
|
1210
|
+
* also using `usePayment`, you'll double-poll.
|
|
1211
|
+
*
|
|
1212
|
+
* @example
|
|
1213
|
+
* ```ts
|
|
1214
|
+
* const tagada = createHeadlessClient({ storeId, environment });
|
|
1215
|
+
*
|
|
1216
|
+
* const resumed = await tagada.payment.maybeResumeFromUrl();
|
|
1217
|
+
* if (resumed?.status === 'succeeded') {
|
|
1218
|
+
* window.location.href = `/thank-you?orderId=${resumed.order?.id}`;
|
|
1219
|
+
* return;
|
|
1220
|
+
* }
|
|
1221
|
+
* if (resumed?.status === 'failed') {
|
|
1222
|
+
* showError(resumed.error);
|
|
1223
|
+
* return;
|
|
1224
|
+
* }
|
|
1225
|
+
* // No 3DS return — render the normal page.
|
|
1226
|
+
* ```
|
|
1227
|
+
*/
|
|
1228
|
+
async maybeResumeFromUrl(opts) {
|
|
1229
|
+
if (typeof window === "undefined" && !(opts == null ? void 0 : opts.url))
|
|
1230
|
+
return null;
|
|
1231
|
+
const href = (opts == null ? void 0 : opts.url) ?? window.location.href;
|
|
1232
|
+
let url;
|
|
1233
|
+
try {
|
|
1234
|
+
url = new URL(href);
|
|
1235
|
+
} catch {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
const action = url.searchParams.get("paymentAction");
|
|
1239
|
+
const actionStatus = url.searchParams.get("paymentActionStatus");
|
|
1240
|
+
const paymentId = url.searchParams.get("paymentId");
|
|
1241
|
+
if (action !== "requireAction" || actionStatus !== "completed" || !paymentId) {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
const result = await this.resumeAfterRedirect(paymentId);
|
|
1245
|
+
if ((opts == null ? void 0 : opts.cleanUrl) !== false && typeof window !== "undefined" && !(opts == null ? void 0 : opts.url)) {
|
|
1246
|
+
const cleanParams = new URLSearchParams(window.location.search);
|
|
1247
|
+
[
|
|
1248
|
+
"paymentAction",
|
|
1249
|
+
"paymentActionStatus",
|
|
1250
|
+
"paymentId",
|
|
1251
|
+
"payment_intent",
|
|
1252
|
+
"payment_intent_client_secret",
|
|
1253
|
+
"source_type",
|
|
1254
|
+
"redirect_status",
|
|
1255
|
+
"sessionId"
|
|
1256
|
+
].forEach((k) => cleanParams.delete(k));
|
|
1257
|
+
const search = cleanParams.toString();
|
|
1258
|
+
try {
|
|
1259
|
+
window.history.replaceState(
|
|
1260
|
+
{},
|
|
1261
|
+
document.title,
|
|
1262
|
+
window.location.pathname + (search ? "?" + search : "")
|
|
1263
|
+
);
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return result;
|
|
1268
|
+
}
|
|
1269
|
+
// ---------------------------------------------------------------------------
|
|
1270
|
+
// Status polling
|
|
1271
|
+
// ---------------------------------------------------------------------------
|
|
1272
|
+
/** Get current status of a payment. */
|
|
1273
|
+
async getPaymentStatus(paymentId) {
|
|
1274
|
+
return this.http.get(`/api/v1/payments/${paymentId}`);
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Poll a payment until it reaches a terminal status.
|
|
1278
|
+
* Returns the final status or throws on timeout.
|
|
1279
|
+
*/
|
|
1280
|
+
async pollPaymentStatus(paymentId, opts) {
|
|
1281
|
+
const interval = (opts == null ? void 0 : opts.intervalMs) ?? 1500;
|
|
1282
|
+
const timeout = (opts == null ? void 0 : opts.timeoutMs) ?? 6e4;
|
|
1283
|
+
const start = Date.now();
|
|
1284
|
+
while (Date.now() - start < timeout) {
|
|
1285
|
+
const status = await this.getPaymentStatus(paymentId);
|
|
1286
|
+
if (status.status === "succeeded" || status.status === "failed" || status.status === "declined") {
|
|
1287
|
+
return status;
|
|
1288
|
+
}
|
|
1289
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
1290
|
+
}
|
|
1291
|
+
throw new Error(`Payment ${paymentId} did not reach terminal status within ${timeout}ms.`);
|
|
1292
|
+
}
|
|
1293
|
+
/** Finalize a payment after a 3DS challenge completes. */
|
|
1294
|
+
async completeAfter3ds(paymentId) {
|
|
1295
|
+
return this.http.post("/api/v1/payments/complete-after-3ds", {
|
|
1296
|
+
paymentId
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
/** Tell the server this action has been seen / is being processed. */
|
|
1300
|
+
async markActionProcessed(paymentId) {
|
|
1301
|
+
try {
|
|
1302
|
+
await this.http.post("/api/v1/payments/require-action/processed", {
|
|
1303
|
+
paymentId
|
|
1304
|
+
});
|
|
1305
|
+
} catch {
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
// ---------------------------------------------------------------------------
|
|
1309
|
+
// Internal: action routing
|
|
1310
|
+
// ---------------------------------------------------------------------------
|
|
1311
|
+
/**
|
|
1312
|
+
* Route a payment action to the appropriate handler.
|
|
1313
|
+
* Returns `requires_redirect` for browser-level redirects,
|
|
1314
|
+
* or attempts 3DS inline and polls to completion.
|
|
1315
|
+
*/
|
|
1316
|
+
async handlePaymentAction(payment, opts) {
|
|
1317
|
+
const actionData = payment.requireActionData;
|
|
1318
|
+
await this.markActionProcessed(payment.id);
|
|
1319
|
+
const actionType = actionData.type;
|
|
1320
|
+
switch (actionType) {
|
|
1321
|
+
case "threeds_auth":
|
|
1322
|
+
return this.handleThreedsAuth(payment, opts);
|
|
1323
|
+
case "processor_auth":
|
|
1324
|
+
case "redirect_to_payment":
|
|
1325
|
+
return this.extractRedirect(payment, actionData);
|
|
1326
|
+
case "redirect":
|
|
1327
|
+
return this.extractRedirect(payment, actionData);
|
|
1328
|
+
case "error":
|
|
1329
|
+
return {
|
|
1330
|
+
status: "failed",
|
|
1331
|
+
error: actionData.message || actionData.errorCode || "Payment failed",
|
|
1332
|
+
payment
|
|
1333
|
+
};
|
|
1334
|
+
default:
|
|
1335
|
+
return this.extractRedirect(payment, actionData);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Extract redirect info from actionData.
|
|
1340
|
+
* Works for processor_auth (GET or POST), redirect, and any unknown type with a URL.
|
|
1341
|
+
*/
|
|
1342
|
+
extractRedirect(payment, actionData) {
|
|
1343
|
+
var _a, _b;
|
|
1344
|
+
const redirect = (_a = actionData.metadata) == null ? void 0 : _a.redirect;
|
|
1345
|
+
const url = (redirect == null ? void 0 : redirect.redirectUrl) || actionData.redirectUrl;
|
|
1346
|
+
if (!url) {
|
|
1347
|
+
return {
|
|
1348
|
+
status: "failed",
|
|
1349
|
+
error: `Payment requires action (${actionData.type}) but no redirect URL was provided`,
|
|
1350
|
+
payment
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
status: "requires_redirect",
|
|
1355
|
+
redirectUrl: url,
|
|
1356
|
+
method: ((_b = redirect == null ? void 0 : redirect.method) == null ? void 0 : _b.toUpperCase()) || "GET",
|
|
1357
|
+
postData: redirect == null ? void 0 : redirect.data,
|
|
1358
|
+
paymentId: payment.id
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Run a 3DS challenge using @tagadapay/core-js.
|
|
1363
|
+
* If core-js is not installed, falls back to redirect.
|
|
1364
|
+
*/
|
|
1365
|
+
async handleThreedsAuth(payment, opts) {
|
|
1366
|
+
var _a, _b;
|
|
1367
|
+
const session = (_b = (_a = payment.requireActionData) == null ? void 0 : _a.metadata) == null ? void 0 : _b.threedsSession;
|
|
1368
|
+
if (!(session == null ? void 0 : session.externalSessionId)) {
|
|
1369
|
+
return this.extractRedirect(payment, payment.requireActionData);
|
|
1370
|
+
}
|
|
1371
|
+
let coreJs;
|
|
1372
|
+
try {
|
|
1373
|
+
coreJs = await import("@tagadapay/core-js");
|
|
1374
|
+
} catch {
|
|
1375
|
+
if (session.acsChallengeUrl) {
|
|
1376
|
+
return {
|
|
1377
|
+
status: "requires_redirect",
|
|
1378
|
+
redirectUrl: session.acsChallengeUrl,
|
|
1379
|
+
paymentId: payment.id
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
return this.extractRedirect(payment, payment.requireActionData);
|
|
1383
|
+
}
|
|
1384
|
+
try {
|
|
1385
|
+
const manager = new coreJs.ThreedsManager({ provider: "basis_theory" });
|
|
1386
|
+
await manager.initialize({ apiKey: void 0 });
|
|
1387
|
+
await manager.startChallenge({
|
|
1388
|
+
sessionId: session.externalSessionId,
|
|
1389
|
+
acsChallengeUrl: session.acsChallengeUrl,
|
|
1390
|
+
acsTransactionId: session.acsTransID,
|
|
1391
|
+
threeDSVersion: session.messageVersion
|
|
1392
|
+
});
|
|
1393
|
+
await this.completeAfter3ds(payment.id);
|
|
1394
|
+
return this.pollUntilDone(payment.id);
|
|
1395
|
+
} catch {
|
|
1396
|
+
if (session.acsChallengeUrl) {
|
|
1397
|
+
return {
|
|
1398
|
+
status: "requires_redirect",
|
|
1399
|
+
redirectUrl: session.acsChallengeUrl,
|
|
1400
|
+
paymentId: payment.id
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
return {
|
|
1404
|
+
status: "failed",
|
|
1405
|
+
error: "3D Secure authentication failed",
|
|
1406
|
+
payment
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Poll until the payment reaches a terminal status, then return a ProcessPaymentResult.
|
|
1412
|
+
* Also handles new requireAction that may appear during polling.
|
|
1413
|
+
*/
|
|
1414
|
+
async pollUntilDone(paymentId, opts) {
|
|
1415
|
+
const interval = (opts == null ? void 0 : opts.intervalMs) ?? 1500;
|
|
1416
|
+
const timeout = (opts == null ? void 0 : opts.timeoutMs) ?? 6e4;
|
|
1417
|
+
const start = Date.now();
|
|
1418
|
+
let consecutiveErrors = 0;
|
|
1419
|
+
while (Date.now() - start < timeout) {
|
|
1420
|
+
let status;
|
|
1421
|
+
try {
|
|
1422
|
+
status = await this.getPaymentStatus(paymentId);
|
|
1423
|
+
consecutiveErrors = 0;
|
|
1424
|
+
} catch {
|
|
1425
|
+
consecutiveErrors++;
|
|
1426
|
+
if (consecutiveErrors >= 3) {
|
|
1427
|
+
return { status: "failed", error: "Payment verification failed due to network errors" };
|
|
1428
|
+
}
|
|
1429
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (status.status === "succeeded" || status.status === "pending" && status.subStatus === "authorized") {
|
|
1433
|
+
return { status: "succeeded", payment: status, order: status.order };
|
|
1434
|
+
}
|
|
1435
|
+
if (status.status === "failed" || status.status === "declined") {
|
|
1436
|
+
return { status: "failed", error: status.status, payment: status };
|
|
1437
|
+
}
|
|
1438
|
+
if (status.requireAction && status.requireAction !== "none" && status.requireActionData && !status.requireActionData.processed) {
|
|
1439
|
+
return this.handlePaymentAction(
|
|
1440
|
+
status,
|
|
1441
|
+
{}
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
1445
|
+
}
|
|
1446
|
+
return { status: "pending", paymentId };
|
|
1447
|
+
}
|
|
1448
|
+
// ---------------------------------------------------------------------------
|
|
1449
|
+
// Internal helpers
|
|
1450
|
+
// ---------------------------------------------------------------------------
|
|
1451
|
+
/**
|
|
1452
|
+
* Read paymentSetupConfig from the Tagada routing injection.
|
|
1453
|
+
* Only available when running on Tagada infra (not on external hosting like Vercel).
|
|
1454
|
+
*/
|
|
1455
|
+
readInjectedStepConfig() {
|
|
1456
|
+
if (typeof window === "undefined")
|
|
1457
|
+
return null;
|
|
1458
|
+
const raw = window.__TGD_STEP_CONFIG__;
|
|
1459
|
+
if (!raw)
|
|
1460
|
+
return null;
|
|
1461
|
+
try {
|
|
1462
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1463
|
+
return (parsed == null ? void 0 : parsed.paymentSetupConfig) ?? null;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1469
|
+
async loadCoreJs() {
|
|
1470
|
+
try {
|
|
1471
|
+
return await import("@tagadapay/core-js");
|
|
1472
|
+
} catch {
|
|
1473
|
+
throw new Error(
|
|
1474
|
+
"This method requires @tagadapay/core-js. Install it: npm install @tagadapay/core-js"
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
// src/core/modules/offers.ts
|
|
1481
|
+
var OffersModule = class {
|
|
1482
|
+
constructor(http, storeId, payment) {
|
|
1483
|
+
this.http = http;
|
|
1484
|
+
this.storeId = storeId;
|
|
1485
|
+
this.payment = payment;
|
|
1486
|
+
}
|
|
1487
|
+
// ---------------------------------------------------------------------------
|
|
1488
|
+
// Offer loading
|
|
1489
|
+
// ---------------------------------------------------------------------------
|
|
1490
|
+
/** Get a single offer by ID. */
|
|
1491
|
+
async getOffer(offerId) {
|
|
1492
|
+
return this.http.get(`/api/v1/offers/${offerId}`);
|
|
1493
|
+
}
|
|
1494
|
+
/** List offers for the current store. Optionally filter by type or a set of IDs. */
|
|
1495
|
+
async listOffers(opts) {
|
|
1496
|
+
var _a;
|
|
1497
|
+
let url = `/api/v1/stores/${this.storeId}/offers`;
|
|
1498
|
+
const queryParts = [];
|
|
1499
|
+
if (opts == null ? void 0 : opts.type)
|
|
1500
|
+
queryParts.push(`type=${encodeURIComponent(opts.type)}`);
|
|
1501
|
+
if ((_a = opts == null ? void 0 : opts.ids) == null ? void 0 : _a.length) {
|
|
1502
|
+
for (const id of opts.ids)
|
|
1503
|
+
queryParts.push(`ids=${encodeURIComponent(id)}`);
|
|
1504
|
+
}
|
|
1505
|
+
if (queryParts.length)
|
|
1506
|
+
url += `?${queryParts.join("&")}`;
|
|
1507
|
+
const res = await this.http.get(url);
|
|
1508
|
+
return res.offers ?? [];
|
|
1509
|
+
}
|
|
1510
|
+
// ---------------------------------------------------------------------------
|
|
1511
|
+
// Preview & pay (one-click upsell flow from plugin-sdk)
|
|
1512
|
+
// ---------------------------------------------------------------------------
|
|
1513
|
+
/** Preview offer pricing. Returns line items with amounts and totals. */
|
|
1514
|
+
async previewOffer(opts) {
|
|
1515
|
+
return this.http.post(`/api/v1/offers/${opts.offerId}/preview`, {
|
|
1516
|
+
offerId: opts.offerId,
|
|
1517
|
+
currency: opts.currency ?? "",
|
|
1518
|
+
lineItems: opts.lineItems
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Pay a previously previewed offer (one-click charge).
|
|
1523
|
+
* Used after a main order so the stored payment instrument is reused.
|
|
1524
|
+
*
|
|
1525
|
+
* **Low-level API.** Returns the raw `OfferPayResult` — `requireAction`,
|
|
1526
|
+
* polling, and 3DS redirects are NOT handled. For most cases, use
|
|
1527
|
+
* {@link processOfferPayment} which orchestrates the full lifecycle.
|
|
1528
|
+
*/
|
|
1529
|
+
async payPreviewedOffer(opts) {
|
|
1530
|
+
return this.http.post(`/api/v1/offers/${opts.offerId}/pay-preview`, {
|
|
1531
|
+
offerId: opts.offerId,
|
|
1532
|
+
currency: opts.currency ?? "",
|
|
1533
|
+
lineItems: opts.lineItems,
|
|
1534
|
+
returnUrl: opts.returnUrl ?? (typeof window !== "undefined" ? window.location.href : ""),
|
|
1535
|
+
mainOrderId: opts.mainOrderId,
|
|
1536
|
+
paymentInstrumentId: opts.paymentInstrumentId
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* One-click upsell with full lifecycle handling.
|
|
1541
|
+
*
|
|
1542
|
+
* Like `payment.processPayment()` but for previewed offers: charges the
|
|
1543
|
+
* stored instrument from `mainOrderId`, runs any 3DS challenge or bank
|
|
1544
|
+
* redirect, polls until terminal, and returns a typed
|
|
1545
|
+
* `ProcessPaymentResult` (`succeeded` / `requires_redirect` /
|
|
1546
|
+
* `failed` / `pending`).
|
|
1547
|
+
*
|
|
1548
|
+
* Use this instead of the lower-level `payPreviewedOffer()` whenever you
|
|
1549
|
+
* don't need to inspect the raw `OfferPayResult` shape — which is most
|
|
1550
|
+
* of the time.
|
|
1551
|
+
*
|
|
1552
|
+
* @example
|
|
1553
|
+
* ```ts
|
|
1554
|
+
* const result = await tagada.offers.processOfferPayment({
|
|
1555
|
+
* offerId: 'offer_xxx',
|
|
1556
|
+
* mainOrderId: order.id,
|
|
1557
|
+
* returnUrl: window.location.href,
|
|
1558
|
+
* });
|
|
1559
|
+
*
|
|
1560
|
+
* switch (result.status) {
|
|
1561
|
+
* case 'succeeded':
|
|
1562
|
+
* window.location.href = '/upsell-2';
|
|
1563
|
+
* break;
|
|
1564
|
+
* case 'requires_redirect':
|
|
1565
|
+
* window.location.href = result.redirectUrl;
|
|
1566
|
+
* break;
|
|
1567
|
+
* case 'failed':
|
|
1568
|
+
* showError(result.error);
|
|
1569
|
+
* break;
|
|
1570
|
+
* }
|
|
1571
|
+
* ```
|
|
1572
|
+
*/
|
|
1573
|
+
async processOfferPayment(opts) {
|
|
1574
|
+
if (!this.payment) {
|
|
1575
|
+
throw new Error(
|
|
1576
|
+
"OffersModule: no PaymentModule injected. Use createHeadlessClient() instead of constructing OffersModule directly."
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
const returnUrl = opts.returnUrl ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
1580
|
+
const payResult = await this.payPreviewedOffer({ ...opts, returnUrl });
|
|
1581
|
+
return this.payment.finalize(
|
|
1582
|
+
{ payment: payResult.payment, order: payResult.order },
|
|
1583
|
+
{ returnUrl }
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
// ---------------------------------------------------------------------------
|
|
1587
|
+
// Order bumps (checkout-session scoped)
|
|
1588
|
+
// ---------------------------------------------------------------------------
|
|
1589
|
+
/** Toggle an order bump on/off within a checkout session. */
|
|
1590
|
+
async toggleOrderBump(opts) {
|
|
1591
|
+
return this.http.post(
|
|
1592
|
+
`/api/v1/checkout-sessions/${opts.checkoutSessionId}/toggle-order-bump`,
|
|
1593
|
+
{
|
|
1594
|
+
orderBumpOfferId: opts.orderBumpOfferId,
|
|
1595
|
+
selected: opts.selected
|
|
1596
|
+
}
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
// ---------------------------------------------------------------------------
|
|
1600
|
+
// Post-purchase offers (order-scoped)
|
|
1601
|
+
// ---------------------------------------------------------------------------
|
|
1602
|
+
/** List post-purchase offers for an order. */
|
|
1603
|
+
async listPostPurchaseOffers(orderId) {
|
|
1604
|
+
const res = await this.http.get(`/api/v1/post-purchase/${orderId}/offers`);
|
|
1605
|
+
return Array.isArray(res) ? res : [];
|
|
1606
|
+
}
|
|
1607
|
+
/** Accept a post-purchase offer. */
|
|
1608
|
+
async acceptPostPurchaseOffer(opts) {
|
|
1609
|
+
return this.http.post(
|
|
1610
|
+
`/api/v1/orders/${opts.orderId}/post-purchase-offers/${opts.offerId}/accept`,
|
|
1611
|
+
{ items: opts.items ?? [] }
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
/** Decline a post-purchase offer. */
|
|
1615
|
+
async declinePostPurchaseOffer(opts) {
|
|
1616
|
+
return this.http.post(
|
|
1617
|
+
`/api/v1/orders/${opts.orderId}/post-purchase-offers/${opts.offerId}/decline`
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
// src/core/modules/customer.ts
|
|
1623
|
+
var CustomerModule = class {
|
|
1624
|
+
constructor(http, storeId) {
|
|
1625
|
+
this.http = http;
|
|
1626
|
+
this.storeId = storeId;
|
|
1627
|
+
}
|
|
1628
|
+
/** Get customer profile by ID. */
|
|
1629
|
+
async getProfile(customerId) {
|
|
1630
|
+
const res = await this.http.get(
|
|
1631
|
+
`/api/v1/customers/${customerId}`,
|
|
1632
|
+
{ params: { storeId: this.storeId } }
|
|
1633
|
+
);
|
|
1634
|
+
return res.customer;
|
|
1635
|
+
}
|
|
1636
|
+
/** Get customer profile by email. */
|
|
1637
|
+
async getByEmail(email) {
|
|
1638
|
+
try {
|
|
1639
|
+
const res = await this.http.get(
|
|
1640
|
+
"/api/v1/customers/by-email",
|
|
1641
|
+
{ params: { email, storeId: this.storeId } }
|
|
1642
|
+
);
|
|
1643
|
+
return res.customer;
|
|
1644
|
+
} catch {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
/** Update customer profile fields. */
|
|
1649
|
+
async updateProfile(customerId, data) {
|
|
1650
|
+
const res = await this.http.patch(
|
|
1651
|
+
`/api/v1/customers/${customerId}`,
|
|
1652
|
+
{ storeId: this.storeId, ...data }
|
|
1653
|
+
);
|
|
1654
|
+
return res.customer;
|
|
1655
|
+
}
|
|
1656
|
+
/** List orders for a customer. */
|
|
1657
|
+
async getOrders(customerId, opts) {
|
|
1658
|
+
return this.http.get(
|
|
1659
|
+
`/api/v1/customers/${customerId}/orders`,
|
|
1660
|
+
{ params: { storeId: this.storeId, limit: opts == null ? void 0 : opts.limit, offset: opts == null ? void 0 : opts.offset } }
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
/** Get a single order by ID. */
|
|
1664
|
+
async getOrder(orderId) {
|
|
1665
|
+
const res = await this.http.get(`/api/v1/orders/${orderId}`, {
|
|
1666
|
+
params: { storeId: this.storeId }
|
|
1667
|
+
});
|
|
1668
|
+
return res.order;
|
|
1669
|
+
}
|
|
1670
|
+
/** List subscriptions for a customer. */
|
|
1671
|
+
async getSubscriptions(customerId) {
|
|
1672
|
+
const res = await this.http.get(
|
|
1673
|
+
`/api/v1/customers/${customerId}/subscriptions`,
|
|
1674
|
+
{ params: { storeId: this.storeId } }
|
|
1675
|
+
);
|
|
1676
|
+
return res.subscriptions;
|
|
1677
|
+
}
|
|
1678
|
+
/** Get a single subscription by ID. */
|
|
1679
|
+
async getSubscription(subscriptionId) {
|
|
1680
|
+
const res = await this.http.get(
|
|
1681
|
+
`/api/v1/subscriptions/${subscriptionId}`,
|
|
1682
|
+
{ params: { storeId: this.storeId } }
|
|
1683
|
+
);
|
|
1684
|
+
return res.subscription;
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
// src/core/modules/catalog.ts
|
|
1689
|
+
var CatalogModule = class {
|
|
1690
|
+
constructor(http, storeId) {
|
|
1691
|
+
this.http = http;
|
|
1692
|
+
this.storeId = storeId;
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* List all active products for the store, including variants and prices.
|
|
1696
|
+
*
|
|
1697
|
+
* @param opts.productIds Optional filter to specific product IDs.
|
|
1698
|
+
* @param opts.variantIds Optional filter to products containing these variant IDs.
|
|
1699
|
+
*/
|
|
1700
|
+
async getProducts(opts) {
|
|
1701
|
+
var _a, _b;
|
|
1702
|
+
const body = {
|
|
1703
|
+
storeId: this.storeId,
|
|
1704
|
+
includeVariants: true,
|
|
1705
|
+
includePrices: true
|
|
1706
|
+
};
|
|
1707
|
+
if ((_a = opts == null ? void 0 : opts.productIds) == null ? void 0 : _a.length)
|
|
1708
|
+
body.productIds = opts.productIds;
|
|
1709
|
+
if ((_b = opts == null ? void 0 : opts.variantIds) == null ? void 0 : _b.length)
|
|
1710
|
+
body.variantIds = opts.variantIds;
|
|
1711
|
+
const res = await this.http.post(
|
|
1712
|
+
"/api/v1/products",
|
|
1713
|
+
body
|
|
1714
|
+
);
|
|
1715
|
+
return res.items ?? [];
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Get a single product by ID.
|
|
1719
|
+
*/
|
|
1720
|
+
async getProduct(productId) {
|
|
1721
|
+
const items = await this.getProducts({ productIds: [productId] });
|
|
1722
|
+
return items[0] ?? null;
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
// src/core/modules/funnel.ts
|
|
1727
|
+
var FunnelModule = class {
|
|
1728
|
+
constructor(http, storeId, checkout, accountId) {
|
|
1729
|
+
this.http = http;
|
|
1730
|
+
this.storeId = storeId;
|
|
1731
|
+
this.checkout = checkout;
|
|
1732
|
+
this.accountId = accountId;
|
|
1733
|
+
}
|
|
1734
|
+
async navigate(input) {
|
|
1735
|
+
var _a;
|
|
1736
|
+
const session = this.checkout.getCurrentSession();
|
|
1737
|
+
if (!session) {
|
|
1738
|
+
throw new TagadaError(
|
|
1739
|
+
"No checkout session loaded. Call sdk.checkout.loadSession(...) before sdk.funnel.navigate(...).",
|
|
1740
|
+
"no_cart_loaded"
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
const accountId = input.accountId ?? this.accountId;
|
|
1744
|
+
if (!accountId) {
|
|
1745
|
+
throw new TagadaError(
|
|
1746
|
+
"accountId is required for funnel.navigate(). Set it on createHeadlessClient({ accountId }) or pass it per call.",
|
|
1747
|
+
"missing_account_id"
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
const currentUrl = input.currentUrl ?? (typeof globalThis !== "undefined" ? (_a = globalThis.location) == null ? void 0 : _a.href : void 0);
|
|
1751
|
+
await this.ensureCmsSession(session, accountId, input);
|
|
1752
|
+
const funnelSessionId = await this.initializeFunnel({
|
|
1753
|
+
funnelId: input.funnelId,
|
|
1754
|
+
stepId: input.stepId,
|
|
1755
|
+
accountId,
|
|
1756
|
+
currentUrl
|
|
1757
|
+
});
|
|
1758
|
+
const url = await this.runNavigate({
|
|
1759
|
+
funnelSessionId,
|
|
1760
|
+
stepId: input.stepId,
|
|
1761
|
+
session,
|
|
1762
|
+
input,
|
|
1763
|
+
currentUrl
|
|
1764
|
+
});
|
|
1765
|
+
return { url, funnelSessionId };
|
|
1766
|
+
}
|
|
1767
|
+
// -------------------------------------------------------------------------
|
|
1768
|
+
// Internals
|
|
1769
|
+
// -------------------------------------------------------------------------
|
|
1770
|
+
async ensureCmsSession(session, accountId, input) {
|
|
1771
|
+
var _a, _b, _c;
|
|
1772
|
+
if (this._cmsToken && this._cmsSessionId && this._cmsCustomerId)
|
|
1773
|
+
return;
|
|
1774
|
+
const anon = await this.http.post(
|
|
1775
|
+
"/api/v1/cms/session/anonymous",
|
|
1776
|
+
{ storeId: this.storeId, role: "anonymous", draft: false }
|
|
1777
|
+
);
|
|
1778
|
+
this._cmsToken = anon.token;
|
|
1779
|
+
this._cmsSessionId = anon.sessionId;
|
|
1780
|
+
const init = await this.http.post(
|
|
1781
|
+
"/api/v1/cms/session/v2/init",
|
|
1782
|
+
{
|
|
1783
|
+
storeId: this.storeId,
|
|
1784
|
+
accountId,
|
|
1785
|
+
customerId: "anon_placeholder",
|
|
1786
|
+
browserLocale: ((_a = input.customer) == null ? void 0 : _a.locale) ?? defaultLocale(),
|
|
1787
|
+
userAgent: defaultUserAgent(),
|
|
1788
|
+
queryCurrency: ((_b = input.customer) == null ? void 0 : _b.currency) ?? session.currency,
|
|
1789
|
+
queryLocale: (_c = input.customer) == null ? void 0 : _c.locale
|
|
1790
|
+
},
|
|
1791
|
+
{ token: this._cmsToken }
|
|
1792
|
+
);
|
|
1793
|
+
this._cmsCustomerId = init.customer.id;
|
|
1794
|
+
}
|
|
1795
|
+
async initializeFunnel(args) {
|
|
1796
|
+
const res = await this.http.post(
|
|
1797
|
+
"/api/v1/funnel/initialize",
|
|
1798
|
+
{
|
|
1799
|
+
cmsSession: {
|
|
1800
|
+
customerId: this._cmsCustomerId,
|
|
1801
|
+
sessionId: this._cmsSessionId,
|
|
1802
|
+
storeId: this.storeId,
|
|
1803
|
+
accountId: args.accountId
|
|
1804
|
+
},
|
|
1805
|
+
funnelId: args.funnelId,
|
|
1806
|
+
entryStepId: args.stepId,
|
|
1807
|
+
funnelStepId: args.stepId,
|
|
1808
|
+
currentUrl: args.currentUrl,
|
|
1809
|
+
draft: false
|
|
1810
|
+
},
|
|
1811
|
+
{ token: this._cmsToken }
|
|
1812
|
+
);
|
|
1813
|
+
return res.context.sessionId;
|
|
1814
|
+
}
|
|
1815
|
+
async runNavigate(args) {
|
|
1816
|
+
var _a, _b, _c, _d;
|
|
1817
|
+
const { funnelSessionId, stepId, session, input, currentUrl } = args;
|
|
1818
|
+
const lineItems = session.items.map((i) => ({
|
|
1819
|
+
variantId: i.variantId,
|
|
1820
|
+
priceId: i.priceId,
|
|
1821
|
+
quantity: i.quantity
|
|
1822
|
+
}));
|
|
1823
|
+
const discountCodes = input.discountCodes ?? (session.promotionCode ? [session.promotionCode] : void 0);
|
|
1824
|
+
const customer = {
|
|
1825
|
+
email: ((_a = input.customer) == null ? void 0 : _a.email) ?? ((_b = session.customer) == null ? void 0 : _b.email),
|
|
1826
|
+
currency: ((_c = input.customer) == null ? void 0 : _c.currency) ?? session.currency,
|
|
1827
|
+
locale: (_d = input.customer) == null ? void 0 : _d.locale
|
|
1828
|
+
};
|
|
1829
|
+
const res = await this.http.post(
|
|
1830
|
+
"/api/v1/funnel/navigate",
|
|
1831
|
+
{
|
|
1832
|
+
sessionId: funnelSessionId,
|
|
1833
|
+
currentUrl,
|
|
1834
|
+
funnelStepId: stepId,
|
|
1835
|
+
fireAndForget: false,
|
|
1836
|
+
event: {
|
|
1837
|
+
type: "INIT_CHECKOUT",
|
|
1838
|
+
data: {
|
|
1839
|
+
checkoutInit: {
|
|
1840
|
+
cartToken: session.id,
|
|
1841
|
+
lineItems,
|
|
1842
|
+
customer,
|
|
1843
|
+
returnUrl: input.returnUrl,
|
|
1844
|
+
storeId: this.storeId,
|
|
1845
|
+
metadata: input.metadata,
|
|
1846
|
+
discountCodes,
|
|
1847
|
+
// Pins the session to this exact shipping amount when the caller's
|
|
1848
|
+
// storefront already settled one (#TAG-1785). Left undefined when
|
|
1849
|
+
// absent, so it is omitted from the JSON body and the platform
|
|
1850
|
+
// auto-selects a rate exactly as before.
|
|
1851
|
+
externalShipping: input.externalShipping
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
},
|
|
1856
|
+
{ token: this._cmsToken }
|
|
1857
|
+
);
|
|
1858
|
+
return res.result.url;
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
function defaultLocale() {
|
|
1862
|
+
if (typeof navigator !== "undefined" && navigator.language)
|
|
1863
|
+
return navigator.language;
|
|
1864
|
+
return "en-US";
|
|
1865
|
+
}
|
|
1866
|
+
function defaultUserAgent() {
|
|
1867
|
+
if (typeof navigator !== "undefined" && navigator.userAgent)
|
|
1868
|
+
return navigator.userAgent;
|
|
1869
|
+
return "tagadapay-headless-sdk";
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/core/modules/rx.ts
|
|
1873
|
+
var RxModule = class {
|
|
1874
|
+
constructor(http) {
|
|
1875
|
+
this.http = http;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Check whether a product requires an Rx case. Storefronts call this
|
|
1879
|
+
* before rendering the intake form so they only ask the patient
|
|
1880
|
+
* questions when really necessary.
|
|
1881
|
+
*
|
|
1882
|
+
* Read-only / public — does not require any ownership proof.
|
|
1883
|
+
*/
|
|
1884
|
+
async isRequiredForProduct(productId, opts) {
|
|
1885
|
+
return this.http.get("/api/public/v1/rx/required-for-product", {
|
|
1886
|
+
params: {
|
|
1887
|
+
productId,
|
|
1888
|
+
clinicalNetworkId: opts == null ? void 0 : opts.clinicalNetworkId
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Look up the Rx cases attached to an order. Storefronts poll this
|
|
1894
|
+
* on the thank-you page to render "Your prescription is being
|
|
1895
|
+
* reviewed" → "Shipped" transitions.
|
|
1896
|
+
*/
|
|
1897
|
+
async getCasesForOrder(input) {
|
|
1898
|
+
return this.http.post(
|
|
1899
|
+
"/api/public/v1/rx/cases/lookup",
|
|
1900
|
+
input
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Submit a fresh clinical case for a freshly-paid order. PHI is
|
|
1905
|
+
* forwarded to the clinical network and never persisted on
|
|
1906
|
+
* Tagada-side rows.
|
|
1907
|
+
*/
|
|
1908
|
+
async submitCase(input) {
|
|
1909
|
+
return this.http.post("/api/public/v1/rx/cases/submit", input);
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Fetch the network-authored intake questions for a product.
|
|
1913
|
+
* Empty `questions` means the storefront should use its local quiz.
|
|
1914
|
+
*/
|
|
1915
|
+
async getQuestionsForProduct(productId) {
|
|
1916
|
+
return this.http.get("/api/public/v1/rx/questions-for-product", {
|
|
1917
|
+
params: { productId }
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1921
|
+
|
|
1922
|
+
// src/core/client.ts
|
|
1923
|
+
var HeadlessClient = class {
|
|
1924
|
+
constructor(config) {
|
|
1925
|
+
const environment = config.environment ?? "production";
|
|
1926
|
+
const baseUrl = HttpClient.resolveBaseUrl(environment, config.baseUrl);
|
|
1927
|
+
this.config = {
|
|
1928
|
+
...config,
|
|
1929
|
+
environment,
|
|
1930
|
+
baseUrl,
|
|
1931
|
+
timeout: config.timeout ?? 3e4
|
|
1932
|
+
};
|
|
1933
|
+
this.http = new HttpClient({
|
|
1934
|
+
baseUrl,
|
|
1935
|
+
timeout: this.config.timeout,
|
|
1936
|
+
apiKey: config.apiKey,
|
|
1937
|
+
getToken: () => this._sessionToken
|
|
1938
|
+
});
|
|
1939
|
+
const setToken = (token) => {
|
|
1940
|
+
this._sessionToken = token;
|
|
1941
|
+
};
|
|
1942
|
+
this.checkout = new CheckoutModule(this.http, this.config.storeId, setToken);
|
|
1943
|
+
this.payment = new PaymentModule(this.http, this.config.storeId, environment, this.checkout);
|
|
1944
|
+
this.offers = new OffersModule(this.http, this.config.storeId, this.payment);
|
|
1945
|
+
this.customer = new CustomerModule(this.http, this.config.storeId);
|
|
1946
|
+
this.catalog = new CatalogModule(this.http, this.config.storeId);
|
|
1947
|
+
this.funnel = new FunnelModule(
|
|
1948
|
+
this.http,
|
|
1949
|
+
this.config.storeId,
|
|
1950
|
+
this.checkout,
|
|
1951
|
+
this.config.accountId
|
|
1952
|
+
);
|
|
1953
|
+
this.rx = new RxModule(this.http);
|
|
1954
|
+
}
|
|
1955
|
+
/** Manually set the session token (e.g. from URL params on page load). */
|
|
1956
|
+
setSessionToken(token) {
|
|
1957
|
+
this._sessionToken = token;
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
function createHeadlessClient(config) {
|
|
1961
|
+
return new HeadlessClient(config);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// src/core/helpers/autocomplete.ts
|
|
1965
|
+
var scriptInjected = false;
|
|
1966
|
+
var scriptReady = false;
|
|
1967
|
+
var readyCallbacks = [];
|
|
1968
|
+
function onScriptReady() {
|
|
1969
|
+
scriptReady = true;
|
|
1970
|
+
for (const cb of readyCallbacks)
|
|
1971
|
+
cb();
|
|
1972
|
+
readyCallbacks.length = 0;
|
|
1973
|
+
}
|
|
1974
|
+
function loadGoogleMapsScript(apiKey, opts = {}) {
|
|
1975
|
+
var _a, _b;
|
|
1976
|
+
if (scriptReady || ((_b = (_a = window.google) == null ? void 0 : _a.maps) == null ? void 0 : _b.places)) {
|
|
1977
|
+
onScriptReady();
|
|
1978
|
+
return Promise.resolve();
|
|
1979
|
+
}
|
|
1980
|
+
if (scriptInjected) {
|
|
1981
|
+
return new Promise((resolve) => readyCallbacks.push(resolve));
|
|
1982
|
+
}
|
|
1983
|
+
scriptInjected = true;
|
|
1984
|
+
return new Promise((resolve, reject) => {
|
|
1985
|
+
readyCallbacks.push(resolve);
|
|
1986
|
+
const existing = document.querySelector('script[src*="maps.googleapis.com"]');
|
|
1987
|
+
if (existing) {
|
|
1988
|
+
const poll = () => {
|
|
1989
|
+
var _a2, _b2;
|
|
1990
|
+
if ((_b2 = (_a2 = window.google) == null ? void 0 : _a2.maps) == null ? void 0 : _b2.places) {
|
|
1991
|
+
onScriptReady();
|
|
1992
|
+
} else {
|
|
1993
|
+
setTimeout(poll, 100);
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
poll();
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const params = new URLSearchParams({
|
|
2000
|
+
key: apiKey,
|
|
2001
|
+
libraries: "places",
|
|
2002
|
+
v: opts.version ?? "weekly"
|
|
2003
|
+
});
|
|
2004
|
+
if (opts.language)
|
|
2005
|
+
params.set("language", opts.language);
|
|
2006
|
+
if (opts.region)
|
|
2007
|
+
params.set("region", opts.region);
|
|
2008
|
+
const script = document.createElement("script");
|
|
2009
|
+
script.src = `https://maps.googleapis.com/maps/api/js?${params}`;
|
|
2010
|
+
script.async = true;
|
|
2011
|
+
script.defer = true;
|
|
2012
|
+
script.onload = () => onScriptReady();
|
|
2013
|
+
script.onerror = () => reject(new Error("Failed to load Google Maps script"));
|
|
2014
|
+
document.head.appendChild(script);
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
var GoogleAutocompleteCore = class _GoogleAutocompleteCore {
|
|
2018
|
+
/** Search for address predictions. */
|
|
2019
|
+
static async searchPlaces(input, options = {}) {
|
|
2020
|
+
var _a, _b;
|
|
2021
|
+
if (!((_b = (_a = window.google) == null ? void 0 : _a.maps) == null ? void 0 : _b.places) || !input.trim())
|
|
2022
|
+
return [];
|
|
2023
|
+
const service = new window.google.maps.places.AutocompleteService();
|
|
2024
|
+
return new Promise((resolve) => {
|
|
2025
|
+
service.getPlacePredictions(
|
|
2026
|
+
{ input, types: options.types ?? ["address"], componentRestrictions: options.componentRestrictions },
|
|
2027
|
+
(predictions, status) => {
|
|
2028
|
+
var _a2, _b2, _c;
|
|
2029
|
+
resolve(status === ((_c = (_b2 = (_a2 = window.google) == null ? void 0 : _a2.maps) == null ? void 0 : _b2.places) == null ? void 0 : _c.PlacesServiceStatus.OK) && predictions ? predictions : []);
|
|
2030
|
+
}
|
|
2031
|
+
);
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
/** Get full place details by place_id. */
|
|
2035
|
+
static async getPlaceDetails(placeId) {
|
|
2036
|
+
var _a, _b;
|
|
2037
|
+
if (!((_b = (_a = window.google) == null ? void 0 : _a.maps) == null ? void 0 : _b.places))
|
|
2038
|
+
return null;
|
|
2039
|
+
const svc = new window.google.maps.places.PlacesService(document.createElement("div"));
|
|
2040
|
+
return new Promise((resolve) => {
|
|
2041
|
+
svc.getDetails(
|
|
2042
|
+
{ placeId, fields: ["place_id", "formatted_address", "address_components", "geometry"] },
|
|
2043
|
+
(place, status) => {
|
|
2044
|
+
var _a2, _b2, _c;
|
|
2045
|
+
resolve(status === ((_c = (_b2 = (_a2 = window.google) == null ? void 0 : _a2.maps) == null ? void 0 : _b2.places) == null ? void 0 : _c.PlacesServiceStatus.OK) && place ? place : null);
|
|
2046
|
+
}
|
|
2047
|
+
);
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
/** Extract structured address fields from a Google PlaceDetails result. */
|
|
2051
|
+
static extractAddressComponents(place) {
|
|
2052
|
+
const out = {
|
|
2053
|
+
subpremise: "",
|
|
2054
|
+
streetNumber: "",
|
|
2055
|
+
route: "",
|
|
2056
|
+
locality: "",
|
|
2057
|
+
administrativeAreaLevel1: "",
|
|
2058
|
+
administrativeAreaLevel1Long: "",
|
|
2059
|
+
administrativeAreaLevel2: "",
|
|
2060
|
+
administrativeAreaLevel2Long: "",
|
|
2061
|
+
country: "",
|
|
2062
|
+
postalCode: "",
|
|
2063
|
+
fullStreetAddress: ""
|
|
2064
|
+
};
|
|
2065
|
+
let premise = "";
|
|
2066
|
+
let neighborhood = "";
|
|
2067
|
+
let sublocality5 = "", sublocality4 = "", sublocality3 = "", sublocality2 = "", sublocality1 = "";
|
|
2068
|
+
let postalTown = "";
|
|
2069
|
+
for (const c of place.address_components ?? []) {
|
|
2070
|
+
const t = c.types;
|
|
2071
|
+
if (t.includes("subpremise"))
|
|
2072
|
+
out.subpremise = c.long_name;
|
|
2073
|
+
if (t.includes("street_number"))
|
|
2074
|
+
out.streetNumber = c.long_name;
|
|
2075
|
+
if (t.includes("premise"))
|
|
2076
|
+
premise = c.long_name;
|
|
2077
|
+
if (t.includes("route"))
|
|
2078
|
+
out.route = c.long_name;
|
|
2079
|
+
if (t.includes("neighborhood"))
|
|
2080
|
+
neighborhood = c.long_name;
|
|
2081
|
+
if (t.includes("sublocality_level_5"))
|
|
2082
|
+
sublocality5 = c.long_name;
|
|
2083
|
+
if (t.includes("sublocality_level_4"))
|
|
2084
|
+
sublocality4 = c.long_name;
|
|
2085
|
+
if (t.includes("sublocality_level_3"))
|
|
2086
|
+
sublocality3 = c.long_name;
|
|
2087
|
+
if (t.includes("sublocality_level_2"))
|
|
2088
|
+
sublocality2 = c.long_name;
|
|
2089
|
+
if (t.includes("sublocality_level_1"))
|
|
2090
|
+
sublocality1 = c.long_name;
|
|
2091
|
+
if (t.includes("locality"))
|
|
2092
|
+
out.locality = c.long_name;
|
|
2093
|
+
if (t.includes("postal_town"))
|
|
2094
|
+
postalTown = c.long_name;
|
|
2095
|
+
if (t.includes("administrative_area_level_2")) {
|
|
2096
|
+
out.administrativeAreaLevel2 = c.short_name;
|
|
2097
|
+
out.administrativeAreaLevel2Long = c.long_name;
|
|
2098
|
+
}
|
|
2099
|
+
if (t.includes("administrative_area_level_1")) {
|
|
2100
|
+
out.administrativeAreaLevel1 = c.short_name;
|
|
2101
|
+
out.administrativeAreaLevel1Long = c.long_name;
|
|
2102
|
+
}
|
|
2103
|
+
if (t.includes("country"))
|
|
2104
|
+
out.country = c.short_name;
|
|
2105
|
+
if (t.includes("postal_code"))
|
|
2106
|
+
out.postalCode = c.long_name;
|
|
2107
|
+
}
|
|
2108
|
+
if (!out.locality)
|
|
2109
|
+
out.locality = postalTown || sublocality2 || sublocality1 || "";
|
|
2110
|
+
if (!out.administrativeAreaLevel1 && out.administrativeAreaLevel2) {
|
|
2111
|
+
out.administrativeAreaLevel1 = out.administrativeAreaLevel2Long || out.administrativeAreaLevel2;
|
|
2112
|
+
out.administrativeAreaLevel1Long = out.administrativeAreaLevel2Long;
|
|
2113
|
+
}
|
|
2114
|
+
const buildingNumber = out.streetNumber || premise;
|
|
2115
|
+
const streetName = out.route || sublocality5 || sublocality4 || sublocality3 || neighborhood;
|
|
2116
|
+
if (out.subpremise && buildingNumber && streetName) {
|
|
2117
|
+
out.fullStreetAddress = `${out.subpremise}/${buildingNumber} ${streetName}`;
|
|
2118
|
+
} else if (buildingNumber && streetName) {
|
|
2119
|
+
if (premise && sublocality4 && sublocality3) {
|
|
2120
|
+
let addr = `${sublocality3}-${sublocality4}-${premise}`;
|
|
2121
|
+
if (sublocality2)
|
|
2122
|
+
addr = `${addr} ${sublocality2}`;
|
|
2123
|
+
out.fullStreetAddress = addr;
|
|
2124
|
+
} else {
|
|
2125
|
+
out.fullStreetAddress = `${buildingNumber} ${streetName}`;
|
|
2126
|
+
}
|
|
2127
|
+
} else if (streetName) {
|
|
2128
|
+
out.fullStreetAddress = streetName;
|
|
2129
|
+
} else if (buildingNumber) {
|
|
2130
|
+
out.fullStreetAddress = buildingNumber;
|
|
2131
|
+
}
|
|
2132
|
+
out.fullStreetAddress = out.fullStreetAddress.trim();
|
|
2133
|
+
return out;
|
|
2134
|
+
}
|
|
2135
|
+
/** Shorthand: extract into the flat shape used by address forms. */
|
|
2136
|
+
static extractFormattedAddress(place) {
|
|
2137
|
+
const a = _GoogleAutocompleteCore.extractAddressComponents(place);
|
|
2138
|
+
return {
|
|
2139
|
+
address1: a.fullStreetAddress,
|
|
2140
|
+
city: a.locality,
|
|
2141
|
+
country: a.country,
|
|
2142
|
+
state: a.administrativeAreaLevel1,
|
|
2143
|
+
postal: a.postalCode
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
|
|
2148
|
+
// src/core/helpers/iso3166.ts
|
|
2149
|
+
var GEODATA_CDN_BASE = "https://cvwnizdbugpz6jwk.public.blob.vercel-storage.com/geodata/v1";
|
|
2150
|
+
var AVAILABLE_LANGUAGES = ["en", "ru", "de", "da", "fr", "es", "zh", "hi", "pt", "ja", "ar", "it", "he"];
|
|
2151
|
+
var countriesCache = /* @__PURE__ */ new Map();
|
|
2152
|
+
var regionsCache = /* @__PURE__ */ new Map();
|
|
2153
|
+
var inflightCountries = /* @__PURE__ */ new Map();
|
|
2154
|
+
var inflightRegions = /* @__PURE__ */ new Map();
|
|
2155
|
+
async function fetchCountriesRaw(language) {
|
|
2156
|
+
const cached = countriesCache.get(language);
|
|
2157
|
+
if (cached)
|
|
2158
|
+
return cached;
|
|
2159
|
+
let inflight = inflightCountries.get(language);
|
|
2160
|
+
if (inflight)
|
|
2161
|
+
return inflight;
|
|
2162
|
+
inflight = (async () => {
|
|
2163
|
+
const url = `${GEODATA_CDN_BASE}/${language}/countries.json`;
|
|
2164
|
+
const res = await fetch(url);
|
|
2165
|
+
if (!res.ok)
|
|
2166
|
+
throw new Error(`Geodata fetch failed: ${url} (${res.status})`);
|
|
2167
|
+
const data = await res.json();
|
|
2168
|
+
countriesCache.set(language, data);
|
|
2169
|
+
inflightCountries.delete(language);
|
|
2170
|
+
return data;
|
|
2171
|
+
})();
|
|
2172
|
+
inflightCountries.set(language, inflight);
|
|
2173
|
+
return inflight;
|
|
2174
|
+
}
|
|
2175
|
+
async function fetchRegionsRaw(language) {
|
|
2176
|
+
const cached = regionsCache.get(language);
|
|
2177
|
+
if (cached)
|
|
2178
|
+
return cached;
|
|
2179
|
+
let inflight = inflightRegions.get(language);
|
|
2180
|
+
if (inflight)
|
|
2181
|
+
return inflight;
|
|
2182
|
+
inflight = (async () => {
|
|
2183
|
+
const url = `${GEODATA_CDN_BASE}/${language}/regions.json`;
|
|
2184
|
+
const res = await fetch(url);
|
|
2185
|
+
if (!res.ok)
|
|
2186
|
+
throw new Error(`Geodata fetch failed: ${url} (${res.status})`);
|
|
2187
|
+
const data = await res.json();
|
|
2188
|
+
regionsCache.set(language, data);
|
|
2189
|
+
inflightRegions.delete(language);
|
|
2190
|
+
return data;
|
|
2191
|
+
})();
|
|
2192
|
+
inflightRegions.set(language, inflight);
|
|
2193
|
+
return inflight;
|
|
2194
|
+
}
|
|
2195
|
+
function getCountriesCached(language) {
|
|
2196
|
+
return countriesCache.get(language) || countriesCache.get("en") || {};
|
|
2197
|
+
}
|
|
2198
|
+
function getRegionsCached(language) {
|
|
2199
|
+
return regionsCache.get(language) || regionsCache.get("en") || {};
|
|
2200
|
+
}
|
|
2201
|
+
async function ensureGeoDataLoaded(language = "en") {
|
|
2202
|
+
await Promise.all([fetchCountriesRaw(language), fetchRegionsRaw(language)]);
|
|
2203
|
+
}
|
|
2204
|
+
var getCountries = (language = "en") => {
|
|
2205
|
+
const raw = getCountriesCached(language);
|
|
2206
|
+
return Object.entries(raw).map(([code, c]) => ({ code, name: c.n, iso3: c.i3, numeric: c.nu })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2207
|
+
};
|
|
2208
|
+
var getStatesForCountry = (countryCode, language = "en") => {
|
|
2209
|
+
const raw = getRegionsCached(language);
|
|
2210
|
+
const tuples = raw[countryCode];
|
|
2211
|
+
if (!tuples)
|
|
2212
|
+
return [];
|
|
2213
|
+
return tuples.map(([iso, name]) => ({ code: iso, name, countryCode })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2214
|
+
};
|
|
2215
|
+
function isLanguageLoaded(language) {
|
|
2216
|
+
return countriesCache.has(language);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// src/core/helpers/countries.ts
|
|
2220
|
+
var ISODataCore = class _ISODataCore {
|
|
2221
|
+
/** Fetch countries + regions from CDN for a language. Call before sync getters. */
|
|
2222
|
+
static ensureLoaded(language = "en") {
|
|
2223
|
+
return ensureGeoDataLoaded(language);
|
|
2224
|
+
}
|
|
2225
|
+
/** Get all countries as a Record keyed by ISO code. */
|
|
2226
|
+
static getCountries(language = "en") {
|
|
2227
|
+
const arr = getCountries(language);
|
|
2228
|
+
const result = {};
|
|
2229
|
+
for (const c of arr) {
|
|
2230
|
+
result[c.code] = { iso: c.code, iso3: c.iso3 ?? "", numeric: c.numeric ?? 0, name: c.name };
|
|
2231
|
+
}
|
|
2232
|
+
return result;
|
|
2233
|
+
}
|
|
2234
|
+
/** Get all countries as a sorted array of { value, label } for select inputs. */
|
|
2235
|
+
static getCountryOptions(language = "en") {
|
|
2236
|
+
return getCountries(language).map((c) => ({ value: c.code, label: c.name }));
|
|
2237
|
+
}
|
|
2238
|
+
/** Get regions/states for a country. */
|
|
2239
|
+
static getRegions(countryCode, language = "en") {
|
|
2240
|
+
return getStatesForCountry(countryCode, language).map((s) => ({ iso: s.code, name: s.name }));
|
|
2241
|
+
}
|
|
2242
|
+
/** Get regions as a sorted array of { value, label } for select inputs. */
|
|
2243
|
+
static getRegionOptions(countryCode, language = "en") {
|
|
2244
|
+
return _ISODataCore.getRegions(countryCode, language).map((r) => ({ value: r.iso, label: r.name }));
|
|
2245
|
+
}
|
|
2246
|
+
/** Find a specific region by ISO code. */
|
|
2247
|
+
static findRegion(countryCode, regionCode, language = "en") {
|
|
2248
|
+
return _ISODataCore.getRegions(countryCode, language).find((r) => r.iso === regionCode) ?? null;
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Map a Google Places administrative_area to an ISO region.
|
|
2252
|
+
* Tries exact ISO match, then name match, then partial match.
|
|
2253
|
+
*/
|
|
2254
|
+
static mapGoogleToISO(googleState, googleStateLong, countryCode, language = "en") {
|
|
2255
|
+
const regions = _ISODataCore.getRegions(countryCode, language);
|
|
2256
|
+
if (regions.length === 0)
|
|
2257
|
+
return null;
|
|
2258
|
+
let match = regions.find((r) => r.iso === googleState);
|
|
2259
|
+
if (match)
|
|
2260
|
+
return match;
|
|
2261
|
+
match = regions.find((r) => r.name.toLowerCase() === googleState.toLowerCase());
|
|
2262
|
+
if (match)
|
|
2263
|
+
return match;
|
|
2264
|
+
match = regions.find((r) => r.name.toLowerCase() === googleStateLong.toLowerCase());
|
|
2265
|
+
if (match)
|
|
2266
|
+
return match;
|
|
2267
|
+
match = regions.find(
|
|
2268
|
+
(r) => r.name.toLowerCase().includes(googleStateLong.toLowerCase()) || googleStateLong.toLowerCase().includes(r.name.toLowerCase())
|
|
2269
|
+
);
|
|
2270
|
+
return match ?? null;
|
|
2271
|
+
}
|
|
2272
|
+
static isLanguageLoaded(language) {
|
|
2273
|
+
return isLanguageLoaded(language);
|
|
2274
|
+
}
|
|
2275
|
+
static getAvailableLanguages() {
|
|
2276
|
+
return AVAILABLE_LANGUAGES;
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
|
|
2280
|
+
// src/core/helpers/currency.ts
|
|
2281
|
+
var SYMBOLS = {
|
|
2282
|
+
USD: "$",
|
|
2283
|
+
EUR: "\u20AC",
|
|
2284
|
+
GBP: "\xA3",
|
|
2285
|
+
JPY: "\xA5",
|
|
2286
|
+
CAD: "C$",
|
|
2287
|
+
AUD: "A$",
|
|
2288
|
+
CHF: "CHF",
|
|
2289
|
+
CNY: "\xA5",
|
|
2290
|
+
SEK: "kr",
|
|
2291
|
+
NOK: "kr",
|
|
2292
|
+
DKK: "kr",
|
|
2293
|
+
PLN: "z\u0142",
|
|
2294
|
+
CZK: "K\u010D",
|
|
2295
|
+
HUF: "Ft",
|
|
2296
|
+
RON: "lei",
|
|
2297
|
+
BGN: "\u043B\u0432",
|
|
2298
|
+
HRK: "kn",
|
|
2299
|
+
RUB: "\u20BD",
|
|
2300
|
+
UAH: "\u20B4",
|
|
2301
|
+
TRY: "\u20BA",
|
|
2302
|
+
BRL: "R$",
|
|
2303
|
+
MXN: "$",
|
|
2304
|
+
ZAR: "R",
|
|
2305
|
+
INR: "\u20B9",
|
|
2306
|
+
KRW: "\u20A9",
|
|
2307
|
+
THB: "\u0E3F",
|
|
2308
|
+
VND: "\u20AB",
|
|
2309
|
+
IDR: "Rp",
|
|
2310
|
+
MYR: "RM",
|
|
2311
|
+
SGD: "S$",
|
|
2312
|
+
PHP: "\u20B1",
|
|
2313
|
+
TWD: "NT$",
|
|
2314
|
+
HKD: "HK$",
|
|
2315
|
+
ILS: "\u20AA",
|
|
2316
|
+
AED: "\u062F.\u0625",
|
|
2317
|
+
SAR: "\u0631.\u0633",
|
|
2318
|
+
QAR: "\u0631.\u0642",
|
|
2319
|
+
KWD: "\u062F.\u0643",
|
|
2320
|
+
BHD: "\u062F.\u0628",
|
|
2321
|
+
OMR: "\u0631.\u0639.",
|
|
2322
|
+
EGP: "\xA3",
|
|
2323
|
+
NGN: "\u20A6",
|
|
2324
|
+
KES: "KSh",
|
|
2325
|
+
GHS: "\u20B5",
|
|
2326
|
+
COP: "$",
|
|
2327
|
+
ARS: "$",
|
|
2328
|
+
CLP: "$",
|
|
2329
|
+
PEN: "S/",
|
|
2330
|
+
CRC: "\u20A1",
|
|
2331
|
+
DOP: "RD$",
|
|
2332
|
+
JMD: "J$",
|
|
2333
|
+
BOB: "Bs",
|
|
2334
|
+
PYG: "\u20B2",
|
|
2335
|
+
GTQ: "Q",
|
|
2336
|
+
NIO: "C$",
|
|
2337
|
+
PAB: "B/.",
|
|
2338
|
+
PKR: "\u20A8",
|
|
2339
|
+
BDT: "\u09F3",
|
|
2340
|
+
LKR: "\u20A8",
|
|
2341
|
+
NPR: "\u20A8",
|
|
2342
|
+
GEL: "\u20BE",
|
|
2343
|
+
AMD: "\u058F",
|
|
2344
|
+
AZN: "\u20BC",
|
|
2345
|
+
KZT: "\u20B8"
|
|
2346
|
+
};
|
|
2347
|
+
var NAMES = {
|
|
2348
|
+
USD: "US Dollar",
|
|
2349
|
+
EUR: "Euro",
|
|
2350
|
+
GBP: "British Pound",
|
|
2351
|
+
JPY: "Japanese Yen",
|
|
2352
|
+
CAD: "Canadian Dollar",
|
|
2353
|
+
AUD: "Australian Dollar",
|
|
2354
|
+
CHF: "Swiss Franc",
|
|
2355
|
+
CNY: "Chinese Yuan",
|
|
2356
|
+
SEK: "Swedish Krona",
|
|
2357
|
+
NOK: "Norwegian Krone",
|
|
2358
|
+
DKK: "Danish Krone",
|
|
2359
|
+
PLN: "Polish Zloty",
|
|
2360
|
+
CZK: "Czech Koruna",
|
|
2361
|
+
HUF: "Hungarian Forint",
|
|
2362
|
+
BRL: "Brazilian Real",
|
|
2363
|
+
MXN: "Mexican Peso",
|
|
2364
|
+
INR: "Indian Rupee",
|
|
2365
|
+
KRW: "South Korean Won",
|
|
2366
|
+
THB: "Thai Baht",
|
|
2367
|
+
SGD: "Singapore Dollar",
|
|
2368
|
+
HKD: "Hong Kong Dollar",
|
|
2369
|
+
TWD: "Taiwan Dollar",
|
|
2370
|
+
ILS: "Israeli Shekel",
|
|
2371
|
+
AED: "UAE Dirham",
|
|
2372
|
+
SAR: "Saudi Riyal",
|
|
2373
|
+
ZAR: "South African Rand",
|
|
2374
|
+
TRY: "Turkish Lira",
|
|
2375
|
+
RUB: "Russian Ruble",
|
|
2376
|
+
NGN: "Nigerian Naira"
|
|
2377
|
+
};
|
|
2378
|
+
var ZERO_DECIMAL = /* @__PURE__ */ new Set([
|
|
2379
|
+
"JPY",
|
|
2380
|
+
"KRW",
|
|
2381
|
+
"VND",
|
|
2382
|
+
"IDR",
|
|
2383
|
+
"LAK",
|
|
2384
|
+
"KHR",
|
|
2385
|
+
"MMK",
|
|
2386
|
+
"PYG"
|
|
2387
|
+
]);
|
|
2388
|
+
var CurrencyUtils = class _CurrencyUtils {
|
|
2389
|
+
static getSymbol(code) {
|
|
2390
|
+
return SYMBOLS[code] ?? code;
|
|
2391
|
+
}
|
|
2392
|
+
static getName(code) {
|
|
2393
|
+
return NAMES[code] ?? code;
|
|
2394
|
+
}
|
|
2395
|
+
static getDecimalPlaces(code) {
|
|
2396
|
+
return ZERO_DECIMAL.has(code) ? 0 : 2;
|
|
2397
|
+
}
|
|
2398
|
+
static getCurrency(code) {
|
|
2399
|
+
return {
|
|
2400
|
+
code,
|
|
2401
|
+
symbol: _CurrencyUtils.getSymbol(code),
|
|
2402
|
+
name: _CurrencyUtils.getName(code),
|
|
2403
|
+
decimalPlaces: _CurrencyUtils.getDecimalPlaces(code)
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
};
|
|
2407
|
+
function formatMoney(amountMinorUnits, currencyCode = "USD", locale = "en-US") {
|
|
2408
|
+
const decimals = CurrencyUtils.getDecimalPlaces(currencyCode);
|
|
2409
|
+
const value = amountMinorUnits / (decimals === 0 ? 100 : Math.pow(10, decimals));
|
|
2410
|
+
try {
|
|
2411
|
+
return new Intl.NumberFormat(locale, {
|
|
2412
|
+
style: "currency",
|
|
2413
|
+
currency: currencyCode,
|
|
2414
|
+
minimumFractionDigits: decimals,
|
|
2415
|
+
maximumFractionDigits: decimals
|
|
2416
|
+
}).format(value);
|
|
2417
|
+
} catch {
|
|
2418
|
+
return `${CurrencyUtils.getSymbol(currencyCode)}${value.toFixed(decimals)}`;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return __toCommonJS(core_exports);
|
|
2422
|
+
})();
|
|
2423
|
+
if(typeof window!=="undefined"&&TagadaHeadlessBundle){ window.TagadaHeadless=TagadaHeadlessBundle; window.TagadaHeadless.create=TagadaHeadlessBundle.createHeadlessClient;}
|