@vio-live/web-sdk 0.2.0 → 0.4.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 +29 -2
- package/dist/chunk-NP2KCM7X.js +3859 -0
- package/dist/chunk-NP2KCM7X.js.map +1 -0
- package/dist/chunk-PBQSHIID.js +3 -0
- package/dist/{chunk-GGM2AIK5.js.map → chunk-PBQSHIID.js.map} +1 -1
- package/dist/{chunk-7JSSYPRN.js → chunk-V5ZQG2H5.js} +1419 -169
- package/dist/chunk-V5ZQG2H5.js.map +1 -0
- package/dist/core/index.d.ts +314 -12
- package/dist/core/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +3 -2
- package/dist/react/index.js.map +1 -1
- package/dist/tokens-CvqQYURw.d.ts +555 -0
- package/dist/ui/index.d.ts +3 -83
- package/dist/ui/index.js +5 -132
- package/dist/ui/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7JSSYPRN.js.map +0 -1
- package/dist/chunk-GGM2AIK5.js +0 -3
- package/dist/chunk-SOO2G6CF.js +0 -1618
- package/dist/chunk-SOO2G6CF.js.map +0 -1
- package/dist/vio-checkout-CrC5tvmh.d.ts +0 -300
|
@@ -0,0 +1,3859 @@
|
|
|
1
|
+
import { GraphQLClient } from 'graphql-request';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
6
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
7
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
8
|
+
if (decorator = decorators[i])
|
|
9
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
10
|
+
if (kind && result) __defProp(target, key, result);
|
|
11
|
+
return result;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/core/configuration.ts
|
|
15
|
+
var DEFAULT_URLS = {
|
|
16
|
+
development: {
|
|
17
|
+
apiBase: "https://api-dev.vio.live",
|
|
18
|
+
graphQLBase: "https://graph-ql-dev.vio.live"
|
|
19
|
+
},
|
|
20
|
+
// No separate staging GraphQL deployment yet — shares dev's.
|
|
21
|
+
testing: {
|
|
22
|
+
apiBase: "https://api-staging.vio.live",
|
|
23
|
+
graphQLBase: "https://graph-ql-dev.vio.live"
|
|
24
|
+
},
|
|
25
|
+
production: {
|
|
26
|
+
apiBase: "https://api.vio.live",
|
|
27
|
+
graphQLBase: "https://graph-ql.vio.live"
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var ConfigurationSingleton = class {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.resolved = null;
|
|
33
|
+
this.readyWaiters = [];
|
|
34
|
+
}
|
|
35
|
+
init(input) {
|
|
36
|
+
if (!input.apiKey || typeof input.apiKey !== "string") {
|
|
37
|
+
throw new Error("[Vio] apiKey is required in Vio.init({ apiKey })");
|
|
38
|
+
}
|
|
39
|
+
const env = input.environment ?? "development";
|
|
40
|
+
const defaults = DEFAULT_URLS[env];
|
|
41
|
+
this.resolved = {
|
|
42
|
+
apiKey: input.apiKey,
|
|
43
|
+
environment: env,
|
|
44
|
+
apiBase: input.apiBase ?? defaults.apiBase,
|
|
45
|
+
graphQLBase: input.graphQLBase ?? defaults.graphQLBase,
|
|
46
|
+
stripePublishableKey: input.stripePublishableKey,
|
|
47
|
+
klarnaClientId: input.klarnaClientId,
|
|
48
|
+
// Sandbox by default — live Klarna must be opted into explicitly.
|
|
49
|
+
klarnaEnvironment: input.klarnaEnvironment ?? "playground"
|
|
50
|
+
};
|
|
51
|
+
if (typeof console !== "undefined") {
|
|
52
|
+
const masked = input.apiKey.length > 4 ? "*".repeat(Math.max(0, input.apiKey.length - 4)) + input.apiKey.slice(-4) : "****";
|
|
53
|
+
console.info("[Vio] Initialized", {
|
|
54
|
+
environment: env,
|
|
55
|
+
apiBase: this.resolved.apiBase,
|
|
56
|
+
apiKey: masked,
|
|
57
|
+
stripeConfigured: !!input.stripePublishableKey,
|
|
58
|
+
klarnaConfigured: !!input.klarnaClientId,
|
|
59
|
+
klarnaEnvironment: this.resolved.klarnaEnvironment
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const waiters = this.readyWaiters;
|
|
63
|
+
this.readyWaiters = [];
|
|
64
|
+
for (const waiter of waiters) waiter();
|
|
65
|
+
}
|
|
66
|
+
get() {
|
|
67
|
+
if (!this.resolved) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
"[Vio] Not initialized. Call Vio.init({ apiKey }) before using the SDK."
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return this.resolved;
|
|
73
|
+
}
|
|
74
|
+
get isInitialized() {
|
|
75
|
+
return this.resolved !== null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolves once `Vio.init()` has run, or after `timeoutMs` — whichever
|
|
79
|
+
* comes first. Hosts that mount several independent components (e.g. Vev
|
|
80
|
+
* blocks, which mount in no guaranteed order) may have one fire a GraphQL
|
|
81
|
+
* call before another has called `Vio.init()`. Awaiting this briefly lets
|
|
82
|
+
* that race resolve instead of silently sending an unauthenticated
|
|
83
|
+
* request. Resolves `true` if init happened in time, `false` on timeout —
|
|
84
|
+
* callers should still degrade gracefully either way.
|
|
85
|
+
*/
|
|
86
|
+
whenReady(timeoutMs = 2e3) {
|
|
87
|
+
if (this.resolved) return Promise.resolve(true);
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
90
|
+
this.readyWaiters.push(() => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** For tests only. */
|
|
97
|
+
reset() {
|
|
98
|
+
this.resolved = null;
|
|
99
|
+
this.readyWaiters = [];
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var Configuration = /* @__PURE__ */ new ConfigurationSingleton();
|
|
103
|
+
|
|
104
|
+
// src/core/api/vio.ts
|
|
105
|
+
var VioApi = class {
|
|
106
|
+
constructor(options) {
|
|
107
|
+
if (!options.apiBase) throw new Error("[VioApi] apiBase is required");
|
|
108
|
+
if (!options.apiKey) throw new Error("[VioApi] apiKey is required");
|
|
109
|
+
this.apiBase = options.apiBase.replace(/\/+$/, "");
|
|
110
|
+
this.apiKey = options.apiKey;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Bootstrap — fetches client app config (sponsors with commerce keys,
|
|
114
|
+
* default market, campaign id). Called once at SDK init.
|
|
115
|
+
*/
|
|
116
|
+
async bootstrap() {
|
|
117
|
+
return this.get("/v2/mobile/config");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get placement components for a campaign. Used by the carousel auto-fetch
|
|
121
|
+
* mode (`<vio-product-carousel campaign-id="36" location="news-favorites">`).
|
|
122
|
+
*/
|
|
123
|
+
async getComponents(campaignId) {
|
|
124
|
+
const path = `/v2/mobile/campaigns/${encodeURIComponent(String(campaignId))}/components`;
|
|
125
|
+
const res = await this.get(path);
|
|
126
|
+
return Array.isArray(res) ? res : res?.components ?? [];
|
|
127
|
+
}
|
|
128
|
+
// MARK: - HTTP plumbing
|
|
129
|
+
async get(path) {
|
|
130
|
+
const url = `${this.apiBase}${path}`;
|
|
131
|
+
const res = await fetch(url, {
|
|
132
|
+
method: "GET",
|
|
133
|
+
headers: {
|
|
134
|
+
"X-API-Key": this.apiKey,
|
|
135
|
+
Accept: "application/json"
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
throw new VioApiError(res.status, await safeText(res), `GET ${path}`);
|
|
140
|
+
}
|
|
141
|
+
return await res.json();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var VioApiError = class extends Error {
|
|
145
|
+
constructor(status, body, endpoint) {
|
|
146
|
+
super(`[VioApi] ${endpoint} \u2192 HTTP ${status}: ${body.slice(0, 200)}`);
|
|
147
|
+
this.status = status;
|
|
148
|
+
this.body = body;
|
|
149
|
+
this.endpoint = endpoint;
|
|
150
|
+
this.name = "VioApiError";
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
async function safeText(res) {
|
|
154
|
+
try {
|
|
155
|
+
return await res.text();
|
|
156
|
+
} catch {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function createVioApi(options) {
|
|
161
|
+
return new VioApi(options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/core/types.ts
|
|
165
|
+
function displayPrice(product) {
|
|
166
|
+
const p = product.price;
|
|
167
|
+
const amount = p.amount_incl_taxes ?? p.amount;
|
|
168
|
+
return formatPrice(amount, p.currency_code);
|
|
169
|
+
}
|
|
170
|
+
function getGlobalCountryCode() {
|
|
171
|
+
const g = globalThis;
|
|
172
|
+
if (g.__VIO_COUNTRY__) return g.__VIO_COUNTRY__;
|
|
173
|
+
if (typeof localStorage !== "undefined") {
|
|
174
|
+
try {
|
|
175
|
+
const v = localStorage.getItem("vio.country");
|
|
176
|
+
if (v) return v;
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return "NO";
|
|
181
|
+
}
|
|
182
|
+
function getGlobalLocale() {
|
|
183
|
+
const g = globalThis;
|
|
184
|
+
if (g.__VIO_LOCALE__) return g.__VIO_LOCALE__;
|
|
185
|
+
if (typeof localStorage !== "undefined") {
|
|
186
|
+
try {
|
|
187
|
+
const v = localStorage.getItem("vio.locale");
|
|
188
|
+
if (v) return v;
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return "nb-NO";
|
|
193
|
+
}
|
|
194
|
+
function getGlobalSymbol(currency) {
|
|
195
|
+
const activeCurrency = currency || getGlobalCurrency();
|
|
196
|
+
const g = globalThis;
|
|
197
|
+
if (!currency && g.__VIO_SYMBOL__) return g.__VIO_SYMBOL__;
|
|
198
|
+
if (!currency && typeof localStorage !== "undefined") {
|
|
199
|
+
try {
|
|
200
|
+
const v = localStorage.getItem("vio.symbol");
|
|
201
|
+
if (v) return v;
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const curr = String(activeCurrency).toUpperCase();
|
|
206
|
+
if (curr === "NOK" || curr === "DKK") return "Kr";
|
|
207
|
+
if (curr === "SEK") return "SEK";
|
|
208
|
+
if (curr === "EUR") return "\u20AC";
|
|
209
|
+
if (curr === "USD" || curr === "CAD") return "$";
|
|
210
|
+
if (curr === "GBP") return "\xA3";
|
|
211
|
+
if (curr === "CHF") return "CHF";
|
|
212
|
+
return activeCurrency;
|
|
213
|
+
}
|
|
214
|
+
function getGlobalCurrency() {
|
|
215
|
+
const g = globalThis;
|
|
216
|
+
if (g.__VIO_CURRENCY__) return g.__VIO_CURRENCY__;
|
|
217
|
+
if (typeof localStorage !== "undefined") {
|
|
218
|
+
try {
|
|
219
|
+
const v = localStorage.getItem("vio.currency");
|
|
220
|
+
if (v) return v;
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return "NOK";
|
|
225
|
+
}
|
|
226
|
+
var validatedLocales = /* @__PURE__ */ new Map();
|
|
227
|
+
function safeLocale(locale) {
|
|
228
|
+
const cached = validatedLocales.get(locale);
|
|
229
|
+
if (cached) return cached;
|
|
230
|
+
let resolved = "nb-NO";
|
|
231
|
+
try {
|
|
232
|
+
if (Intl.NumberFormat.supportedLocalesOf([locale]).length > 0) resolved = locale;
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
validatedLocales.set(locale, resolved);
|
|
236
|
+
return resolved;
|
|
237
|
+
}
|
|
238
|
+
function formatPrice(amount, currency) {
|
|
239
|
+
const activeCurrency = currency || getGlobalCurrency();
|
|
240
|
+
const activeLocale = safeLocale(getGlobalLocale());
|
|
241
|
+
const activeSymbol = getGlobalSymbol(activeCurrency);
|
|
242
|
+
const formatted = amount.toLocaleString(activeLocale, { minimumFractionDigits: 0, maximumFractionDigits: 2 }).replace(/ /g, " ");
|
|
243
|
+
return `${formatted} ${activeSymbol}`;
|
|
244
|
+
}
|
|
245
|
+
function primaryImageUrl(product) {
|
|
246
|
+
const p = product;
|
|
247
|
+
if (!p || typeof p !== "object") return void 0;
|
|
248
|
+
if (Array.isArray(p.variants) && p.variants.length > 0) {
|
|
249
|
+
const vImages = p.variants[0]?.images;
|
|
250
|
+
if (Array.isArray(vImages) && vImages.length > 0) {
|
|
251
|
+
const vUrl = typeof vImages[0] === "string" ? vImages[0] : vImages[0]?.url;
|
|
252
|
+
if (vUrl) return vUrl;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(p.images) && p.images.length > 0) {
|
|
256
|
+
const img0 = p.images[0];
|
|
257
|
+
const url = typeof img0 === "string" ? img0 : img0?.url;
|
|
258
|
+
if (url) return url;
|
|
259
|
+
}
|
|
260
|
+
if (typeof p.image === "string" && p.image) return p.image;
|
|
261
|
+
if (p.image && typeof p.image === "object" && p.image.url) return p.image.url;
|
|
262
|
+
if (typeof p.imageUrl === "string" && p.imageUrl) return p.imageUrl;
|
|
263
|
+
if (typeof p.image_url === "string" && p.image_url) return p.image_url;
|
|
264
|
+
if (typeof p.primaryImage === "string" && p.primaryImage) return p.primaryImage;
|
|
265
|
+
if (p.primaryImage && typeof p.primaryImage === "object" && p.primaryImage.url) {
|
|
266
|
+
return p.primaryImage.url;
|
|
267
|
+
}
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
var GET_PRODUCTS_BY_IDS = (
|
|
271
|
+
/* GraphQL */
|
|
272
|
+
`
|
|
273
|
+
query GetProductsByIds(
|
|
274
|
+
$currency: String
|
|
275
|
+
$imageSize: ImageSize
|
|
276
|
+
$productIds: [Int!]
|
|
277
|
+
$useCache: Boolean!
|
|
278
|
+
$shippingCountryCode: String
|
|
279
|
+
) {
|
|
280
|
+
Channel {
|
|
281
|
+
GetProductsByIds(
|
|
282
|
+
currency: $currency
|
|
283
|
+
image_size: $imageSize
|
|
284
|
+
product_ids: $productIds
|
|
285
|
+
useCache: $useCache
|
|
286
|
+
shipping_country_code: $shippingCountryCode
|
|
287
|
+
) {
|
|
288
|
+
id
|
|
289
|
+
brand
|
|
290
|
+
title
|
|
291
|
+
description
|
|
292
|
+
tags
|
|
293
|
+
sku
|
|
294
|
+
quantity
|
|
295
|
+
price {
|
|
296
|
+
amount
|
|
297
|
+
currency_code
|
|
298
|
+
compare_at
|
|
299
|
+
amount_incl_taxes
|
|
300
|
+
compare_at_incl_taxes
|
|
301
|
+
tax_amount
|
|
302
|
+
tax_rate
|
|
303
|
+
}
|
|
304
|
+
variants {
|
|
305
|
+
id
|
|
306
|
+
barcode
|
|
307
|
+
quantity
|
|
308
|
+
sku
|
|
309
|
+
title
|
|
310
|
+
price {
|
|
311
|
+
amount
|
|
312
|
+
currency_code
|
|
313
|
+
compare_at
|
|
314
|
+
amount_incl_taxes
|
|
315
|
+
compare_at_incl_taxes
|
|
316
|
+
tax_amount
|
|
317
|
+
tax_rate
|
|
318
|
+
}
|
|
319
|
+
images {
|
|
320
|
+
id
|
|
321
|
+
url
|
|
322
|
+
width
|
|
323
|
+
height
|
|
324
|
+
order
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
barcode
|
|
328
|
+
options {
|
|
329
|
+
id
|
|
330
|
+
name
|
|
331
|
+
order
|
|
332
|
+
values
|
|
333
|
+
}
|
|
334
|
+
categories {
|
|
335
|
+
id
|
|
336
|
+
name
|
|
337
|
+
}
|
|
338
|
+
images {
|
|
339
|
+
id
|
|
340
|
+
url
|
|
341
|
+
width
|
|
342
|
+
height
|
|
343
|
+
order
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
`
|
|
349
|
+
);
|
|
350
|
+
var ChannelProduct = class {
|
|
351
|
+
constructor(client) {
|
|
352
|
+
this.client = client;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Fetch a batch of products by their numeric ids.
|
|
356
|
+
* Returns an empty array if no ids are provided.
|
|
357
|
+
*/
|
|
358
|
+
async getByIds(options) {
|
|
359
|
+
if (!options.product_ids || options.product_ids.length === 0) return [];
|
|
360
|
+
const variables = {
|
|
361
|
+
productIds: options.product_ids,
|
|
362
|
+
// Page-level currency/country defaults (multi-market support).
|
|
363
|
+
currency: options.currency ?? getGlobalCurrency(),
|
|
364
|
+
imageSize: options.image_size ?? "large",
|
|
365
|
+
useCache: options.useCache ?? true,
|
|
366
|
+
shippingCountryCode: options.shipping_country_code ?? getGlobalCountryCode()
|
|
367
|
+
};
|
|
368
|
+
const data = await this.client.request(GET_PRODUCTS_BY_IDS, variables);
|
|
369
|
+
return data.Channel?.GetProductsByIds ?? [];
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var Channel = class {
|
|
373
|
+
constructor(client) {
|
|
374
|
+
this.product = new ChannelProduct(client);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var CommerceClient = class {
|
|
378
|
+
constructor(options) {
|
|
379
|
+
if (!options.endpoint) throw new Error("[VioCommerce] endpoint is required");
|
|
380
|
+
if (!options.apiKey) throw new Error("[VioCommerce] apiKey is required");
|
|
381
|
+
this.raw = new GraphQLClient(options.endpoint, {
|
|
382
|
+
headers: { Authorization: options.apiKey }
|
|
383
|
+
});
|
|
384
|
+
this.channel = new Channel(this.raw);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
function createCommerceClient(options) {
|
|
388
|
+
return new CommerceClient(options);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/core/api/cart-queries.ts
|
|
392
|
+
var CREATE_CART_MUTATION = `
|
|
393
|
+
mutation CreateCart($customerSessionId: String!, $currency: String!, $shippingCountry: String) {
|
|
394
|
+
Cart {
|
|
395
|
+
CreateCart(customer_session_id: $customerSessionId, currency: $currency, shipping_country: $shippingCountry) {
|
|
396
|
+
cart_id
|
|
397
|
+
customer_session_id
|
|
398
|
+
shipping_country
|
|
399
|
+
line_items {
|
|
400
|
+
id
|
|
401
|
+
supplier
|
|
402
|
+
image {
|
|
403
|
+
id
|
|
404
|
+
url
|
|
405
|
+
width
|
|
406
|
+
height
|
|
407
|
+
}
|
|
408
|
+
sku
|
|
409
|
+
barcode
|
|
410
|
+
brand
|
|
411
|
+
product_id
|
|
412
|
+
title
|
|
413
|
+
variant_id
|
|
414
|
+
variant_title
|
|
415
|
+
variant {
|
|
416
|
+
option
|
|
417
|
+
value
|
|
418
|
+
}
|
|
419
|
+
quantity
|
|
420
|
+
price {
|
|
421
|
+
amount
|
|
422
|
+
currency_code
|
|
423
|
+
discount
|
|
424
|
+
compare_at
|
|
425
|
+
compare_at_incl_taxes
|
|
426
|
+
amount_incl_taxes
|
|
427
|
+
tax_amount
|
|
428
|
+
tax_rate
|
|
429
|
+
}
|
|
430
|
+
shipping {
|
|
431
|
+
id
|
|
432
|
+
name
|
|
433
|
+
description
|
|
434
|
+
price {
|
|
435
|
+
amount
|
|
436
|
+
currency_code
|
|
437
|
+
amount_incl_taxes
|
|
438
|
+
tax_amount
|
|
439
|
+
tax_rate
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
available_shippings {
|
|
443
|
+
id
|
|
444
|
+
name
|
|
445
|
+
description
|
|
446
|
+
country_code
|
|
447
|
+
price {
|
|
448
|
+
amount
|
|
449
|
+
currency_code
|
|
450
|
+
amount_incl_taxes
|
|
451
|
+
tax_amount
|
|
452
|
+
tax_rate
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
subtotal
|
|
457
|
+
shipping
|
|
458
|
+
currency
|
|
459
|
+
available_shipping_countries
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
`;
|
|
464
|
+
var ADD_ITEM_MUTATION = `
|
|
465
|
+
mutation AddItem($cartId: String!, $lineItems: [LineItemInput!]!) {
|
|
466
|
+
Cart {
|
|
467
|
+
AddItem(cart_id: $cartId, line_items: $lineItems) {
|
|
468
|
+
cart_id
|
|
469
|
+
customer_session_id
|
|
470
|
+
shipping_country
|
|
471
|
+
line_items {
|
|
472
|
+
id
|
|
473
|
+
supplier
|
|
474
|
+
image {
|
|
475
|
+
id
|
|
476
|
+
url
|
|
477
|
+
width
|
|
478
|
+
height
|
|
479
|
+
}
|
|
480
|
+
sku
|
|
481
|
+
barcode
|
|
482
|
+
brand
|
|
483
|
+
product_id
|
|
484
|
+
title
|
|
485
|
+
variant_id
|
|
486
|
+
variant_title
|
|
487
|
+
variant {
|
|
488
|
+
option
|
|
489
|
+
value
|
|
490
|
+
}
|
|
491
|
+
quantity
|
|
492
|
+
price {
|
|
493
|
+
amount
|
|
494
|
+
currency_code
|
|
495
|
+
discount
|
|
496
|
+
compare_at
|
|
497
|
+
compare_at_incl_taxes
|
|
498
|
+
amount_incl_taxes
|
|
499
|
+
tax_amount
|
|
500
|
+
tax_rate
|
|
501
|
+
}
|
|
502
|
+
shipping {
|
|
503
|
+
id
|
|
504
|
+
name
|
|
505
|
+
description
|
|
506
|
+
price {
|
|
507
|
+
amount
|
|
508
|
+
currency_code
|
|
509
|
+
amount_incl_taxes
|
|
510
|
+
tax_amount
|
|
511
|
+
tax_rate
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
available_shippings {
|
|
515
|
+
id
|
|
516
|
+
name
|
|
517
|
+
description
|
|
518
|
+
country_code
|
|
519
|
+
price {
|
|
520
|
+
amount
|
|
521
|
+
currency_code
|
|
522
|
+
amount_incl_taxes
|
|
523
|
+
tax_amount
|
|
524
|
+
tax_rate
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
subtotal
|
|
529
|
+
shipping
|
|
530
|
+
currency
|
|
531
|
+
available_shipping_countries
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
`;
|
|
536
|
+
var DELETE_ITEM_MUTATION = `
|
|
537
|
+
mutation DeleteItem($cartId: String!, $cartItemId: String!) {
|
|
538
|
+
Cart {
|
|
539
|
+
DeleteItem(cart_id: $cartId, cart_item_id: $cartItemId) {
|
|
540
|
+
cart_id
|
|
541
|
+
customer_session_id
|
|
542
|
+
shipping_country
|
|
543
|
+
line_items {
|
|
544
|
+
id
|
|
545
|
+
supplier
|
|
546
|
+
image {
|
|
547
|
+
id
|
|
548
|
+
url
|
|
549
|
+
width
|
|
550
|
+
height
|
|
551
|
+
}
|
|
552
|
+
sku
|
|
553
|
+
barcode
|
|
554
|
+
brand
|
|
555
|
+
product_id
|
|
556
|
+
title
|
|
557
|
+
variant_id
|
|
558
|
+
variant_title
|
|
559
|
+
variant {
|
|
560
|
+
option
|
|
561
|
+
value
|
|
562
|
+
}
|
|
563
|
+
quantity
|
|
564
|
+
price {
|
|
565
|
+
amount
|
|
566
|
+
currency_code
|
|
567
|
+
discount
|
|
568
|
+
compare_at
|
|
569
|
+
compare_at_incl_taxes
|
|
570
|
+
amount_incl_taxes
|
|
571
|
+
tax_amount
|
|
572
|
+
tax_rate
|
|
573
|
+
}
|
|
574
|
+
shipping {
|
|
575
|
+
id
|
|
576
|
+
name
|
|
577
|
+
description
|
|
578
|
+
price {
|
|
579
|
+
amount
|
|
580
|
+
currency_code
|
|
581
|
+
amount_incl_taxes
|
|
582
|
+
tax_amount
|
|
583
|
+
tax_rate
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
available_shippings {
|
|
587
|
+
id
|
|
588
|
+
name
|
|
589
|
+
description
|
|
590
|
+
country_code
|
|
591
|
+
price {
|
|
592
|
+
amount
|
|
593
|
+
currency_code
|
|
594
|
+
amount_incl_taxes
|
|
595
|
+
tax_amount
|
|
596
|
+
tax_rate
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
subtotal
|
|
601
|
+
shipping
|
|
602
|
+
currency
|
|
603
|
+
available_shipping_countries
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
`;
|
|
608
|
+
var UPDATE_ITEM_MUTATION = `
|
|
609
|
+
mutation UpdateItem($cartId: String!, $cartItemId: String!, $shippingId: String, $qty: Int) {
|
|
610
|
+
Cart {
|
|
611
|
+
UpdateItem(cart_id: $cartId, cart_item_id: $cartItemId, shipping_id: $shippingId, qty: $qty) {
|
|
612
|
+
cart_id
|
|
613
|
+
customer_session_id
|
|
614
|
+
shipping_country
|
|
615
|
+
line_items {
|
|
616
|
+
id
|
|
617
|
+
supplier
|
|
618
|
+
image {
|
|
619
|
+
id
|
|
620
|
+
url
|
|
621
|
+
width
|
|
622
|
+
height
|
|
623
|
+
}
|
|
624
|
+
sku
|
|
625
|
+
barcode
|
|
626
|
+
brand
|
|
627
|
+
product_id
|
|
628
|
+
title
|
|
629
|
+
variant_id
|
|
630
|
+
variant_title
|
|
631
|
+
variant {
|
|
632
|
+
option
|
|
633
|
+
value
|
|
634
|
+
}
|
|
635
|
+
quantity
|
|
636
|
+
price {
|
|
637
|
+
amount
|
|
638
|
+
currency_code
|
|
639
|
+
discount
|
|
640
|
+
compare_at
|
|
641
|
+
compare_at_incl_taxes
|
|
642
|
+
amount_incl_taxes
|
|
643
|
+
tax_amount
|
|
644
|
+
tax_rate
|
|
645
|
+
}
|
|
646
|
+
shipping {
|
|
647
|
+
id
|
|
648
|
+
name
|
|
649
|
+
description
|
|
650
|
+
price {
|
|
651
|
+
amount
|
|
652
|
+
currency_code
|
|
653
|
+
amount_incl_taxes
|
|
654
|
+
tax_amount
|
|
655
|
+
tax_rate
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
available_shippings {
|
|
659
|
+
id
|
|
660
|
+
name
|
|
661
|
+
description
|
|
662
|
+
country_code
|
|
663
|
+
price {
|
|
664
|
+
amount
|
|
665
|
+
currency_code
|
|
666
|
+
amount_incl_taxes
|
|
667
|
+
tax_amount
|
|
668
|
+
tax_rate
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
subtotal
|
|
673
|
+
shipping
|
|
674
|
+
currency
|
|
675
|
+
available_shipping_countries
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
`;
|
|
680
|
+
var GET_CART_QUERY = `
|
|
681
|
+
query GetCart($cartId: String!) {
|
|
682
|
+
Cart {
|
|
683
|
+
GetCart(cart_id: $cartId) {
|
|
684
|
+
cart_id
|
|
685
|
+
customer_session_id
|
|
686
|
+
shipping_country
|
|
687
|
+
line_items {
|
|
688
|
+
id
|
|
689
|
+
supplier
|
|
690
|
+
image {
|
|
691
|
+
id
|
|
692
|
+
url
|
|
693
|
+
width
|
|
694
|
+
height
|
|
695
|
+
}
|
|
696
|
+
sku
|
|
697
|
+
barcode
|
|
698
|
+
brand
|
|
699
|
+
product_id
|
|
700
|
+
title
|
|
701
|
+
variant_id
|
|
702
|
+
variant_title
|
|
703
|
+
variant {
|
|
704
|
+
option
|
|
705
|
+
value
|
|
706
|
+
}
|
|
707
|
+
quantity
|
|
708
|
+
price {
|
|
709
|
+
amount
|
|
710
|
+
currency_code
|
|
711
|
+
discount
|
|
712
|
+
compare_at
|
|
713
|
+
compare_at_incl_taxes
|
|
714
|
+
amount_incl_taxes
|
|
715
|
+
tax_amount
|
|
716
|
+
tax_rate
|
|
717
|
+
}
|
|
718
|
+
shipping {
|
|
719
|
+
id
|
|
720
|
+
name
|
|
721
|
+
description
|
|
722
|
+
price {
|
|
723
|
+
amount
|
|
724
|
+
currency_code
|
|
725
|
+
amount_incl_taxes
|
|
726
|
+
tax_amount
|
|
727
|
+
tax_rate
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
available_shippings {
|
|
731
|
+
id
|
|
732
|
+
name
|
|
733
|
+
description
|
|
734
|
+
country_code
|
|
735
|
+
price {
|
|
736
|
+
amount
|
|
737
|
+
currency_code
|
|
738
|
+
amount_incl_taxes
|
|
739
|
+
tax_amount
|
|
740
|
+
tax_rate
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
subtotal
|
|
745
|
+
shipping
|
|
746
|
+
currency
|
|
747
|
+
available_shipping_countries
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
`;
|
|
752
|
+
var GET_LINE_ITEMS_BY_SUPPLIER_QUERY = `
|
|
753
|
+
query GetLineItemsBySupplier($cartId: String!) {
|
|
754
|
+
Cart {
|
|
755
|
+
GetLineItemsBySupplier(cart_id: $cartId) {
|
|
756
|
+
supplier {
|
|
757
|
+
id
|
|
758
|
+
name
|
|
759
|
+
}
|
|
760
|
+
available_shippings {
|
|
761
|
+
id
|
|
762
|
+
name
|
|
763
|
+
description
|
|
764
|
+
country_code
|
|
765
|
+
price {
|
|
766
|
+
amount
|
|
767
|
+
currency_code
|
|
768
|
+
amount_incl_taxes
|
|
769
|
+
tax_amount
|
|
770
|
+
tax_rate
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
line_items {
|
|
774
|
+
id
|
|
775
|
+
supplier
|
|
776
|
+
image {
|
|
777
|
+
id
|
|
778
|
+
url
|
|
779
|
+
width
|
|
780
|
+
height
|
|
781
|
+
}
|
|
782
|
+
sku
|
|
783
|
+
barcode
|
|
784
|
+
brand
|
|
785
|
+
product_id
|
|
786
|
+
title
|
|
787
|
+
variant_id
|
|
788
|
+
variant_title
|
|
789
|
+
variant {
|
|
790
|
+
option
|
|
791
|
+
value
|
|
792
|
+
}
|
|
793
|
+
quantity
|
|
794
|
+
price {
|
|
795
|
+
amount
|
|
796
|
+
currency_code
|
|
797
|
+
discount
|
|
798
|
+
compare_at
|
|
799
|
+
compare_at_incl_taxes
|
|
800
|
+
amount_incl_taxes
|
|
801
|
+
tax_amount
|
|
802
|
+
tax_rate
|
|
803
|
+
}
|
|
804
|
+
shipping {
|
|
805
|
+
id
|
|
806
|
+
name
|
|
807
|
+
description
|
|
808
|
+
price {
|
|
809
|
+
amount
|
|
810
|
+
currency_code
|
|
811
|
+
amount_incl_taxes
|
|
812
|
+
tax_amount
|
|
813
|
+
tax_rate
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
`;
|
|
821
|
+
var UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION = `
|
|
822
|
+
mutation UpdateShippingsBySupplier($cartId: String!, $data: [DataUpdateShippingsBySupplier!]!) {
|
|
823
|
+
Cart {
|
|
824
|
+
UpdateShippingsBySupplier(cart_id: $cartId, data: $data) {
|
|
825
|
+
cart_id
|
|
826
|
+
customer_session_id
|
|
827
|
+
shipping_country
|
|
828
|
+
line_items {
|
|
829
|
+
id
|
|
830
|
+
supplier
|
|
831
|
+
image {
|
|
832
|
+
id
|
|
833
|
+
url
|
|
834
|
+
width
|
|
835
|
+
height
|
|
836
|
+
}
|
|
837
|
+
sku
|
|
838
|
+
barcode
|
|
839
|
+
brand
|
|
840
|
+
product_id
|
|
841
|
+
title
|
|
842
|
+
variant_id
|
|
843
|
+
variant_title
|
|
844
|
+
variant {
|
|
845
|
+
option
|
|
846
|
+
value
|
|
847
|
+
}
|
|
848
|
+
quantity
|
|
849
|
+
price {
|
|
850
|
+
amount
|
|
851
|
+
currency_code
|
|
852
|
+
discount
|
|
853
|
+
compare_at
|
|
854
|
+
compare_at_incl_taxes
|
|
855
|
+
amount_incl_taxes
|
|
856
|
+
tax_amount
|
|
857
|
+
tax_rate
|
|
858
|
+
}
|
|
859
|
+
shipping {
|
|
860
|
+
id
|
|
861
|
+
name
|
|
862
|
+
description
|
|
863
|
+
price {
|
|
864
|
+
amount
|
|
865
|
+
currency_code
|
|
866
|
+
amount_incl_taxes
|
|
867
|
+
tax_amount
|
|
868
|
+
tax_rate
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
available_shippings {
|
|
872
|
+
id
|
|
873
|
+
name
|
|
874
|
+
description
|
|
875
|
+
country_code
|
|
876
|
+
price {
|
|
877
|
+
amount
|
|
878
|
+
currency_code
|
|
879
|
+
amount_incl_taxes
|
|
880
|
+
tax_amount
|
|
881
|
+
tax_rate
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
subtotal
|
|
886
|
+
shipping
|
|
887
|
+
currency
|
|
888
|
+
available_shipping_countries
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
`;
|
|
893
|
+
var CREATE_CHECKOUT_MUTATION = `
|
|
894
|
+
mutation CreateCheckout($cartId: String!) {
|
|
895
|
+
Checkout {
|
|
896
|
+
CreateCheckout(cart_id: $cartId) {
|
|
897
|
+
buyer_accepts_purchase_conditions
|
|
898
|
+
buyer_accepts_terms_conditions
|
|
899
|
+
created_at
|
|
900
|
+
updated_at
|
|
901
|
+
id
|
|
902
|
+
deleted_at
|
|
903
|
+
success_url
|
|
904
|
+
cancel_url
|
|
905
|
+
payment_method
|
|
906
|
+
email
|
|
907
|
+
status
|
|
908
|
+
checkout_url
|
|
909
|
+
origin_payment_id
|
|
910
|
+
billing_address {
|
|
911
|
+
id
|
|
912
|
+
first_name
|
|
913
|
+
last_name
|
|
914
|
+
phone_code
|
|
915
|
+
phone
|
|
916
|
+
company
|
|
917
|
+
address1
|
|
918
|
+
address2
|
|
919
|
+
city
|
|
920
|
+
province
|
|
921
|
+
province_code
|
|
922
|
+
country
|
|
923
|
+
country_code
|
|
924
|
+
zip
|
|
925
|
+
}
|
|
926
|
+
shipping_address {
|
|
927
|
+
id
|
|
928
|
+
first_name
|
|
929
|
+
last_name
|
|
930
|
+
phone_code
|
|
931
|
+
phone
|
|
932
|
+
company
|
|
933
|
+
address1
|
|
934
|
+
address2
|
|
935
|
+
city
|
|
936
|
+
province
|
|
937
|
+
province_code
|
|
938
|
+
country
|
|
939
|
+
country_code
|
|
940
|
+
zip
|
|
941
|
+
}
|
|
942
|
+
available_payment_methods {
|
|
943
|
+
name
|
|
944
|
+
}
|
|
945
|
+
discount_code
|
|
946
|
+
cart {
|
|
947
|
+
cart_id
|
|
948
|
+
customer_session_id
|
|
949
|
+
shipping_country
|
|
950
|
+
line_items {
|
|
951
|
+
id
|
|
952
|
+
supplier
|
|
953
|
+
image {
|
|
954
|
+
id
|
|
955
|
+
url
|
|
956
|
+
width
|
|
957
|
+
height
|
|
958
|
+
}
|
|
959
|
+
sku
|
|
960
|
+
barcode
|
|
961
|
+
brand
|
|
962
|
+
product_id
|
|
963
|
+
title
|
|
964
|
+
variant_id
|
|
965
|
+
variant_title
|
|
966
|
+
variant {
|
|
967
|
+
option
|
|
968
|
+
value
|
|
969
|
+
}
|
|
970
|
+
quantity
|
|
971
|
+
price {
|
|
972
|
+
amount
|
|
973
|
+
currency_code
|
|
974
|
+
discount
|
|
975
|
+
compare_at
|
|
976
|
+
compare_at_incl_taxes
|
|
977
|
+
amount_incl_taxes
|
|
978
|
+
tax_amount
|
|
979
|
+
tax_rate
|
|
980
|
+
}
|
|
981
|
+
shipping {
|
|
982
|
+
id
|
|
983
|
+
name
|
|
984
|
+
description
|
|
985
|
+
price {
|
|
986
|
+
amount
|
|
987
|
+
currency_code
|
|
988
|
+
amount_incl_taxes
|
|
989
|
+
tax_amount
|
|
990
|
+
tax_rate
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
available_shippings {
|
|
994
|
+
id
|
|
995
|
+
name
|
|
996
|
+
description
|
|
997
|
+
country_code
|
|
998
|
+
price {
|
|
999
|
+
amount
|
|
1000
|
+
currency_code
|
|
1001
|
+
amount_incl_taxes
|
|
1002
|
+
tax_amount
|
|
1003
|
+
tax_rate
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
subtotal
|
|
1008
|
+
shipping
|
|
1009
|
+
currency
|
|
1010
|
+
available_shipping_countries
|
|
1011
|
+
}
|
|
1012
|
+
totals {
|
|
1013
|
+
currency_code
|
|
1014
|
+
subtotal
|
|
1015
|
+
shipping
|
|
1016
|
+
total
|
|
1017
|
+
taxes
|
|
1018
|
+
tax_rate
|
|
1019
|
+
discounts
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
`;
|
|
1025
|
+
var UPDATE_CHECKOUT_MUTATION = `
|
|
1026
|
+
mutation UpdateCheckout($checkoutId: String!, $status: String, $successUrl: String, $email: String, $cancelUrl: String, $paymentMethod: String, $shippingAddress: AddressArgs, $billingAddress: AddressArgs, $buyerAcceptsPurchaseConditions: Boolean, $buyerAcceptsTermsConditions: Boolean) {
|
|
1027
|
+
Checkout {
|
|
1028
|
+
UpdateCheckout(checkout_id: $checkoutId, status: $status, success_url: $successUrl, email: $email, cancel_url: $cancelUrl, payment_method: $paymentMethod, shipping_address: $shippingAddress, billing_address: $billingAddress, buyer_accepts_purchase_conditions: $buyerAcceptsPurchaseConditions, buyer_accepts_terms_conditions: $buyerAcceptsTermsConditions) {
|
|
1029
|
+
buyer_accepts_purchase_conditions
|
|
1030
|
+
buyer_accepts_terms_conditions
|
|
1031
|
+
created_at
|
|
1032
|
+
updated_at
|
|
1033
|
+
id
|
|
1034
|
+
deleted_at
|
|
1035
|
+
success_url
|
|
1036
|
+
cancel_url
|
|
1037
|
+
payment_method
|
|
1038
|
+
email
|
|
1039
|
+
status
|
|
1040
|
+
checkout_url
|
|
1041
|
+
origin_payment_id
|
|
1042
|
+
billing_address {
|
|
1043
|
+
id
|
|
1044
|
+
first_name
|
|
1045
|
+
last_name
|
|
1046
|
+
phone_code
|
|
1047
|
+
phone
|
|
1048
|
+
company
|
|
1049
|
+
address1
|
|
1050
|
+
address2
|
|
1051
|
+
city
|
|
1052
|
+
province
|
|
1053
|
+
province_code
|
|
1054
|
+
country
|
|
1055
|
+
country_code
|
|
1056
|
+
zip
|
|
1057
|
+
}
|
|
1058
|
+
shipping_address {
|
|
1059
|
+
id
|
|
1060
|
+
first_name
|
|
1061
|
+
last_name
|
|
1062
|
+
phone_code
|
|
1063
|
+
phone
|
|
1064
|
+
company
|
|
1065
|
+
address1
|
|
1066
|
+
address2
|
|
1067
|
+
city
|
|
1068
|
+
province
|
|
1069
|
+
province_code
|
|
1070
|
+
country
|
|
1071
|
+
country_code
|
|
1072
|
+
zip
|
|
1073
|
+
}
|
|
1074
|
+
available_payment_methods {
|
|
1075
|
+
name
|
|
1076
|
+
}
|
|
1077
|
+
discount_code
|
|
1078
|
+
cart {
|
|
1079
|
+
cart_id
|
|
1080
|
+
customer_session_id
|
|
1081
|
+
shipping_country
|
|
1082
|
+
line_items {
|
|
1083
|
+
id
|
|
1084
|
+
supplier
|
|
1085
|
+
image {
|
|
1086
|
+
id
|
|
1087
|
+
url
|
|
1088
|
+
width
|
|
1089
|
+
height
|
|
1090
|
+
}
|
|
1091
|
+
sku
|
|
1092
|
+
barcode
|
|
1093
|
+
brand
|
|
1094
|
+
product_id
|
|
1095
|
+
title
|
|
1096
|
+
variant_id
|
|
1097
|
+
variant_title
|
|
1098
|
+
variant {
|
|
1099
|
+
option
|
|
1100
|
+
value
|
|
1101
|
+
}
|
|
1102
|
+
quantity
|
|
1103
|
+
price {
|
|
1104
|
+
amount
|
|
1105
|
+
currency_code
|
|
1106
|
+
discount
|
|
1107
|
+
compare_at
|
|
1108
|
+
compare_at_incl_taxes
|
|
1109
|
+
amount_incl_taxes
|
|
1110
|
+
tax_amount
|
|
1111
|
+
tax_rate
|
|
1112
|
+
}
|
|
1113
|
+
shipping {
|
|
1114
|
+
id
|
|
1115
|
+
name
|
|
1116
|
+
description
|
|
1117
|
+
price {
|
|
1118
|
+
amount
|
|
1119
|
+
currency_code
|
|
1120
|
+
amount_incl_taxes
|
|
1121
|
+
tax_amount
|
|
1122
|
+
tax_rate
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
available_shippings {
|
|
1126
|
+
id
|
|
1127
|
+
name
|
|
1128
|
+
description
|
|
1129
|
+
country_code
|
|
1130
|
+
price {
|
|
1131
|
+
amount
|
|
1132
|
+
currency_code
|
|
1133
|
+
amount_incl_taxes
|
|
1134
|
+
tax_amount
|
|
1135
|
+
tax_rate
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
subtotal
|
|
1140
|
+
shipping
|
|
1141
|
+
currency
|
|
1142
|
+
available_shipping_countries
|
|
1143
|
+
}
|
|
1144
|
+
totals {
|
|
1145
|
+
currency_code
|
|
1146
|
+
subtotal
|
|
1147
|
+
shipping
|
|
1148
|
+
total
|
|
1149
|
+
taxes
|
|
1150
|
+
tax_rate
|
|
1151
|
+
discounts
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
`;
|
|
1157
|
+
var GET_CHECKOUT_QUERY = `
|
|
1158
|
+
query GetCheckout($checkoutId: String!) {
|
|
1159
|
+
Checkout {
|
|
1160
|
+
GetCheckout(checkout_id: $checkoutId) {
|
|
1161
|
+
buyer_accepts_purchase_conditions
|
|
1162
|
+
buyer_accepts_terms_conditions
|
|
1163
|
+
created_at
|
|
1164
|
+
updated_at
|
|
1165
|
+
id
|
|
1166
|
+
deleted_at
|
|
1167
|
+
success_url
|
|
1168
|
+
cancel_url
|
|
1169
|
+
payment_method
|
|
1170
|
+
email
|
|
1171
|
+
status
|
|
1172
|
+
checkout_url
|
|
1173
|
+
origin_payment_id
|
|
1174
|
+
billing_address {
|
|
1175
|
+
id
|
|
1176
|
+
first_name
|
|
1177
|
+
last_name
|
|
1178
|
+
phone_code
|
|
1179
|
+
phone
|
|
1180
|
+
company
|
|
1181
|
+
address1
|
|
1182
|
+
address2
|
|
1183
|
+
city
|
|
1184
|
+
province
|
|
1185
|
+
province_code
|
|
1186
|
+
country
|
|
1187
|
+
country_code
|
|
1188
|
+
zip
|
|
1189
|
+
}
|
|
1190
|
+
shipping_address {
|
|
1191
|
+
id
|
|
1192
|
+
first_name
|
|
1193
|
+
last_name
|
|
1194
|
+
phone_code
|
|
1195
|
+
phone
|
|
1196
|
+
company
|
|
1197
|
+
address1
|
|
1198
|
+
address2
|
|
1199
|
+
city
|
|
1200
|
+
province
|
|
1201
|
+
province_code
|
|
1202
|
+
country
|
|
1203
|
+
country_code
|
|
1204
|
+
zip
|
|
1205
|
+
}
|
|
1206
|
+
available_payment_methods {
|
|
1207
|
+
name
|
|
1208
|
+
}
|
|
1209
|
+
discount_code
|
|
1210
|
+
cart {
|
|
1211
|
+
cart_id
|
|
1212
|
+
customer_session_id
|
|
1213
|
+
shipping_country
|
|
1214
|
+
line_items {
|
|
1215
|
+
id
|
|
1216
|
+
supplier
|
|
1217
|
+
image {
|
|
1218
|
+
id
|
|
1219
|
+
url
|
|
1220
|
+
width
|
|
1221
|
+
height
|
|
1222
|
+
}
|
|
1223
|
+
sku
|
|
1224
|
+
barcode
|
|
1225
|
+
brand
|
|
1226
|
+
product_id
|
|
1227
|
+
title
|
|
1228
|
+
variant_id
|
|
1229
|
+
variant_title
|
|
1230
|
+
variant {
|
|
1231
|
+
option
|
|
1232
|
+
value
|
|
1233
|
+
}
|
|
1234
|
+
quantity
|
|
1235
|
+
price {
|
|
1236
|
+
amount
|
|
1237
|
+
currency_code
|
|
1238
|
+
discount
|
|
1239
|
+
compare_at
|
|
1240
|
+
compare_at_incl_taxes
|
|
1241
|
+
amount_incl_taxes
|
|
1242
|
+
tax_amount
|
|
1243
|
+
tax_rate
|
|
1244
|
+
}
|
|
1245
|
+
shipping {
|
|
1246
|
+
id
|
|
1247
|
+
name
|
|
1248
|
+
description
|
|
1249
|
+
price {
|
|
1250
|
+
amount
|
|
1251
|
+
currency_code
|
|
1252
|
+
amount_incl_taxes
|
|
1253
|
+
tax_amount
|
|
1254
|
+
tax_rate
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
available_shippings {
|
|
1258
|
+
id
|
|
1259
|
+
name
|
|
1260
|
+
description
|
|
1261
|
+
country_code
|
|
1262
|
+
price {
|
|
1263
|
+
amount
|
|
1264
|
+
currency_code
|
|
1265
|
+
amount_incl_taxes
|
|
1266
|
+
tax_amount
|
|
1267
|
+
tax_rate
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
subtotal
|
|
1272
|
+
shipping
|
|
1273
|
+
currency
|
|
1274
|
+
available_shipping_countries
|
|
1275
|
+
}
|
|
1276
|
+
totals {
|
|
1277
|
+
currency_code
|
|
1278
|
+
subtotal
|
|
1279
|
+
shipping
|
|
1280
|
+
total
|
|
1281
|
+
taxes
|
|
1282
|
+
tax_rate
|
|
1283
|
+
discounts
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
`;
|
|
1289
|
+
var GET_AVAILABLE_PAYMENT_METHODS_QUERY = `
|
|
1290
|
+
query Config {
|
|
1291
|
+
Payment {
|
|
1292
|
+
GetAvailablePaymentMethods {
|
|
1293
|
+
config {
|
|
1294
|
+
name
|
|
1295
|
+
type
|
|
1296
|
+
value
|
|
1297
|
+
}
|
|
1298
|
+
name
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
`;
|
|
1303
|
+
async function getCartGraphQLOptions(sponsorId) {
|
|
1304
|
+
const facade = globalThis.__VIO_FACADE__;
|
|
1305
|
+
if (facade && typeof facade.bootstrap === "function" && !facade.bootstrapCache) {
|
|
1306
|
+
try {
|
|
1307
|
+
await facade.bootstrap();
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
if (typeof console !== "undefined") {
|
|
1310
|
+
console.warn("[Vio] getCartGraphQLOptions bootstrap await failed:", err);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (!Configuration.isInitialized) {
|
|
1315
|
+
await Configuration.whenReady();
|
|
1316
|
+
}
|
|
1317
|
+
const cfg = Configuration.isInitialized ? Configuration.get() : { apiKey: void 0, graphQLBase: "https://graph-ql-dev.vio.live" };
|
|
1318
|
+
const anyCfg = cfg;
|
|
1319
|
+
let commerceKey = anyCfg.apiKey;
|
|
1320
|
+
let sponsorResolved = false;
|
|
1321
|
+
if (sponsorId && typeof facade?.findSponsor === "function") {
|
|
1322
|
+
const sponsor = facade.findSponsor(sponsorId);
|
|
1323
|
+
if (sponsor?.commerce?.apiKey) {
|
|
1324
|
+
commerceKey = sponsor.commerce.apiKey;
|
|
1325
|
+
sponsorResolved = true;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
endpoint: anyCfg.graphQLBase || "https://graph-ql-dev.vio.live",
|
|
1330
|
+
apiKey: anyCfg.apiKey,
|
|
1331
|
+
commerceKey,
|
|
1332
|
+
sponsorResolved
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
var CUSTOMER_SESSION_KEY = "vio.customer_session_id.v1";
|
|
1336
|
+
function getCustomerSessionId() {
|
|
1337
|
+
if (typeof localStorage === "undefined") return "cs_session_default";
|
|
1338
|
+
let id = localStorage.getItem(CUSTOMER_SESSION_KEY);
|
|
1339
|
+
if (!id) {
|
|
1340
|
+
id = "cs_" + Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
|
|
1341
|
+
localStorage.setItem(CUSTOMER_SESSION_KEY, id);
|
|
1342
|
+
}
|
|
1343
|
+
return id;
|
|
1344
|
+
}
|
|
1345
|
+
async function executeCartGraphQL(query, variables, options = {}) {
|
|
1346
|
+
const resolvedOptions = typeof options === "number" ? await getCartGraphQLOptions(options) : !options.apiKey && !options.commerceKey ? { ...await getCartGraphQLOptions(), ...options } : options;
|
|
1347
|
+
const { endpoint = "https://graph-ql-dev.vio.live", apiKey, commerceKey } = resolvedOptions;
|
|
1348
|
+
const headers = { "Content-Type": "application/json" };
|
|
1349
|
+
if (apiKey) headers["X-API-Key"] = apiKey;
|
|
1350
|
+
if (commerceKey || apiKey) headers["Authorization"] = commerceKey || apiKey;
|
|
1351
|
+
const res = await fetch(endpoint, {
|
|
1352
|
+
method: "POST",
|
|
1353
|
+
headers,
|
|
1354
|
+
body: JSON.stringify({ query, variables })
|
|
1355
|
+
});
|
|
1356
|
+
const json = await res.json().catch(() => ({}));
|
|
1357
|
+
if (!res.ok || json.errors) {
|
|
1358
|
+
const errMsg = json.errors?.[0]?.message || `GraphQL request failed (HTTP ${res.status})`;
|
|
1359
|
+
throw new Error(errMsg);
|
|
1360
|
+
}
|
|
1361
|
+
return json;
|
|
1362
|
+
}
|
|
1363
|
+
async function createCart(customerSessionId, currency, shippingCountry, options) {
|
|
1364
|
+
const json = await executeCartGraphQL(
|
|
1365
|
+
CREATE_CART_MUTATION,
|
|
1366
|
+
{
|
|
1367
|
+
customerSessionId,
|
|
1368
|
+
currency: currency || getGlobalCurrency(),
|
|
1369
|
+
shippingCountry: shippingCountry || getGlobalCountryCode()
|
|
1370
|
+
},
|
|
1371
|
+
options
|
|
1372
|
+
);
|
|
1373
|
+
return json?.data?.Cart?.CreateCart;
|
|
1374
|
+
}
|
|
1375
|
+
async function addItem(cartId, lineItems, options) {
|
|
1376
|
+
const json = await executeCartGraphQL(ADD_ITEM_MUTATION, { cartId, lineItems }, options);
|
|
1377
|
+
return json?.data?.Cart?.AddItem;
|
|
1378
|
+
}
|
|
1379
|
+
async function deleteItem(cartId, cartItemId, options) {
|
|
1380
|
+
const json = await executeCartGraphQL(DELETE_ITEM_MUTATION, { cartId, cartItemId }, options);
|
|
1381
|
+
return json?.data?.Cart?.DeleteItem;
|
|
1382
|
+
}
|
|
1383
|
+
async function updateItem(cartId, cartItemId, shippingId, qty, options) {
|
|
1384
|
+
const json = await executeCartGraphQL(
|
|
1385
|
+
UPDATE_ITEM_MUTATION,
|
|
1386
|
+
{ cartId, cartItemId, shippingId: shippingId || null, qty: qty != null ? qty : null },
|
|
1387
|
+
options
|
|
1388
|
+
);
|
|
1389
|
+
return json?.data?.Cart?.UpdateItem;
|
|
1390
|
+
}
|
|
1391
|
+
async function getCart(cartId, options) {
|
|
1392
|
+
const json = await executeCartGraphQL(GET_CART_QUERY, { cartId }, options);
|
|
1393
|
+
return json?.data?.Cart?.GetCart;
|
|
1394
|
+
}
|
|
1395
|
+
async function getLineItemsBySupplier(cartId, options) {
|
|
1396
|
+
const json = await executeCartGraphQL(GET_LINE_ITEMS_BY_SUPPLIER_QUERY, { cartId }, options);
|
|
1397
|
+
return json?.data?.Cart?.GetLineItemsBySupplier;
|
|
1398
|
+
}
|
|
1399
|
+
async function updateShippingsBySupplier(cartId, data, options) {
|
|
1400
|
+
const json = await executeCartGraphQL(
|
|
1401
|
+
UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION,
|
|
1402
|
+
{ cartId, data },
|
|
1403
|
+
options
|
|
1404
|
+
);
|
|
1405
|
+
return json?.data?.Cart?.UpdateShippingsBySupplier;
|
|
1406
|
+
}
|
|
1407
|
+
async function createCheckout(cartId, options) {
|
|
1408
|
+
const json = await executeCartGraphQL(CREATE_CHECKOUT_MUTATION, { cartId }, options);
|
|
1409
|
+
return json?.data?.Checkout?.CreateCheckout;
|
|
1410
|
+
}
|
|
1411
|
+
async function updateCheckout(variables, options) {
|
|
1412
|
+
const json = await executeCartGraphQL(UPDATE_CHECKOUT_MUTATION, variables, options);
|
|
1413
|
+
return json?.data?.Checkout?.UpdateCheckout;
|
|
1414
|
+
}
|
|
1415
|
+
async function getCheckout(checkoutId, options) {
|
|
1416
|
+
const json = await executeCartGraphQL(GET_CHECKOUT_QUERY, { checkoutId }, options);
|
|
1417
|
+
return json?.data?.Checkout?.GetCheckout;
|
|
1418
|
+
}
|
|
1419
|
+
async function getAvailablePaymentMethods(options) {
|
|
1420
|
+
const json = await executeCartGraphQL(GET_AVAILABLE_PAYMENT_METHODS_QUERY, {}, options);
|
|
1421
|
+
return json?.data?.Payment?.GetAvailablePaymentMethods;
|
|
1422
|
+
}
|
|
1423
|
+
var CREATE_PAYMENT_STRIPE_MUTATION = `
|
|
1424
|
+
mutation CreatePaymentStripe($checkoutId: String!, $successUrl: String!, $paymentMethod: String!, $email: String!) {
|
|
1425
|
+
Payment {
|
|
1426
|
+
CreatePaymentStripe(checkout_id: $checkoutId, success_url: $successUrl, payment_method: $paymentMethod, email: $email) {
|
|
1427
|
+
order_id
|
|
1428
|
+
checkout_url
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
`;
|
|
1433
|
+
async function createPaymentStripe(variables, options) {
|
|
1434
|
+
const json = await executeCartGraphQL(CREATE_PAYMENT_STRIPE_MUTATION, variables, options);
|
|
1435
|
+
return json?.data?.Payment?.CreatePaymentStripe;
|
|
1436
|
+
}
|
|
1437
|
+
var CREATE_PAYMENT_APPLE_PAY_MUTATION = `
|
|
1438
|
+
mutation CreatePaymentApplePay($checkoutId: String!) {
|
|
1439
|
+
Payment {
|
|
1440
|
+
CreatePaymentApplePay(checkout_id: $checkoutId) {
|
|
1441
|
+
gateway
|
|
1442
|
+
gateway_merchant_id
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
`;
|
|
1447
|
+
async function createPaymentApplePay(variables, options) {
|
|
1448
|
+
const json = await executeCartGraphQL(CREATE_PAYMENT_APPLE_PAY_MUTATION, variables, options);
|
|
1449
|
+
return json?.data?.Payment?.CreatePaymentApplePay;
|
|
1450
|
+
}
|
|
1451
|
+
var CONFIRM_PAYMENT_APPLE_PAY_MUTATION = `
|
|
1452
|
+
mutation ConfirmPaymentApplePay($checkoutId: String!, $applePayToken: String!, $email: String, $shippingAddress: ApplePayAddressInput) {
|
|
1453
|
+
Payment {
|
|
1454
|
+
ConfirmPaymentApplePay(checkout_id: $checkoutId, apple_pay_token: $applePayToken, email: $email, shipping_address: $shippingAddress) {
|
|
1455
|
+
order_id
|
|
1456
|
+
status
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
`;
|
|
1461
|
+
async function confirmPaymentApplePay(variables, options) {
|
|
1462
|
+
const json = await executeCartGraphQL(CONFIRM_PAYMENT_APPLE_PAY_MUTATION, variables, options);
|
|
1463
|
+
return json?.data?.Payment?.ConfirmPaymentApplePay;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/core/checkout/payments/vipps.ts
|
|
1467
|
+
var CREATE_PAYMENT_VIPPS_MUTATION = `
|
|
1468
|
+
mutation CreatePaymentVipps($checkoutId: String!, $email: String!, $returnUrl: String!) {
|
|
1469
|
+
Payment {
|
|
1470
|
+
CreatePaymentVipps(checkout_id: $checkoutId, email: $email, return_url: $returnUrl) {
|
|
1471
|
+
payment_url
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
`;
|
|
1476
|
+
var GET_VIPPS_STATUS_QUERY = `
|
|
1477
|
+
query GetVippsStatus($checkoutId: String!) {
|
|
1478
|
+
Payment {
|
|
1479
|
+
GetVippsStatus(checkout_id: $checkoutId) {
|
|
1480
|
+
state
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
`;
|
|
1485
|
+
async function createPaymentVipps(variables, options) {
|
|
1486
|
+
const json = await executeCartGraphQL(CREATE_PAYMENT_VIPPS_MUTATION, variables, options);
|
|
1487
|
+
return json?.data?.Payment?.CreatePaymentVipps ?? null;
|
|
1488
|
+
}
|
|
1489
|
+
async function getVippsStatus(variables, options) {
|
|
1490
|
+
const json = await executeCartGraphQL(GET_VIPPS_STATUS_QUERY, variables, options);
|
|
1491
|
+
return json?.data?.Payment?.GetVippsStatus ?? null;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/core/cart/cart-manager.ts
|
|
1495
|
+
var STORAGE_KEY = "vio.cart.v1";
|
|
1496
|
+
function lineId() {
|
|
1497
|
+
return "li_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
1498
|
+
}
|
|
1499
|
+
function normId(val) {
|
|
1500
|
+
if (val === null || val === void 0 || val === 0 || val === "0" || val === "null" || val === "undefined") {
|
|
1501
|
+
return "";
|
|
1502
|
+
}
|
|
1503
|
+
return String(val).trim();
|
|
1504
|
+
}
|
|
1505
|
+
var CartManager = class extends EventTarget {
|
|
1506
|
+
constructor() {
|
|
1507
|
+
super();
|
|
1508
|
+
/** In-flight CreateCart per sponsor — two quick adds must share ONE backend
|
|
1509
|
+
* cart (two CreateCarts split the items and one silently disappears). */
|
|
1510
|
+
this.cartIdPromises = /* @__PURE__ */ new Map();
|
|
1511
|
+
/** Monotonic mutation counter per sponsor + last applied response, so a
|
|
1512
|
+
* slow response can't overwrite the state of a newer one. */
|
|
1513
|
+
this.mutationSeq = /* @__PURE__ */ new Map();
|
|
1514
|
+
this.appliedSeq = /* @__PURE__ */ new Map();
|
|
1515
|
+
/** Quantity intents for lines whose backend id hasn't arrived yet — replayed
|
|
1516
|
+
* when the AddItem response reconciles (keyed by product::variant). */
|
|
1517
|
+
this.pendingQtyIntents = /* @__PURE__ */ new Map();
|
|
1518
|
+
this.cartsBySponsor = this.loadFromStorage();
|
|
1519
|
+
}
|
|
1520
|
+
nextSeq(sponsorId) {
|
|
1521
|
+
const n = (this.mutationSeq.get(sponsorId) ?? 0) + 1;
|
|
1522
|
+
this.mutationSeq.set(sponsorId, n);
|
|
1523
|
+
return n;
|
|
1524
|
+
}
|
|
1525
|
+
/** Apply a backend response only if no newer response was applied already. */
|
|
1526
|
+
applyBackendCart(sponsorId, backendCart, seq) {
|
|
1527
|
+
if (seq < (this.appliedSeq.get(sponsorId) ?? 0)) return;
|
|
1528
|
+
this.appliedSeq.set(sponsorId, seq);
|
|
1529
|
+
this.updateFromBackendCart(sponsorId, backendCart);
|
|
1530
|
+
}
|
|
1531
|
+
/** Surface a cart sync problem to hosts/UI (and the console). */
|
|
1532
|
+
emitError(message, err) {
|
|
1533
|
+
if (typeof console !== "undefined") console.warn("[CartManager]", message, err ?? "");
|
|
1534
|
+
this.dispatchEvent(new CustomEvent("error", { detail: { message } }));
|
|
1535
|
+
if (typeof document !== "undefined") {
|
|
1536
|
+
document.dispatchEvent(
|
|
1537
|
+
new CustomEvent("vio:cart-error", { bubbles: true, detail: { message } })
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
// MARK: - Backend cart sync (Vio Commerce GraphQL)
|
|
1542
|
+
/** Ensure the sponsor cart exists server-side; returns its backend cart id. */
|
|
1543
|
+
async ensureCartId(sponsorId, currency, shippingCountry) {
|
|
1544
|
+
const activeCurrency = currency || getGlobalCurrency();
|
|
1545
|
+
const activeCountry = shippingCountry || getGlobalCountryCode();
|
|
1546
|
+
let cart = this.cartsBySponsor.get(sponsorId);
|
|
1547
|
+
if (!cart) {
|
|
1548
|
+
cart = { sponsorId, items: [], currency: activeCurrency, shippingCountry: activeCountry };
|
|
1549
|
+
this.cartsBySponsor.set(sponsorId, cart);
|
|
1550
|
+
} else {
|
|
1551
|
+
cart.shippingCountry = activeCountry;
|
|
1552
|
+
}
|
|
1553
|
+
if (cart.cartId) return cart.cartId;
|
|
1554
|
+
const inFlight = this.cartIdPromises.get(sponsorId);
|
|
1555
|
+
if (inFlight) return inFlight;
|
|
1556
|
+
const creation = (async () => {
|
|
1557
|
+
const opts = await getCartGraphQLOptions(sponsorId);
|
|
1558
|
+
if (!opts.sponsorResolved) {
|
|
1559
|
+
this.emitError(`no commerce key resolved for sponsor ${sponsorId} \u2014 cart not synced`);
|
|
1560
|
+
return void 0;
|
|
1561
|
+
}
|
|
1562
|
+
const customerSessionId = getCustomerSessionId();
|
|
1563
|
+
try {
|
|
1564
|
+
const backendCart = await createCart(
|
|
1565
|
+
customerSessionId,
|
|
1566
|
+
activeCurrency,
|
|
1567
|
+
activeCountry,
|
|
1568
|
+
opts
|
|
1569
|
+
);
|
|
1570
|
+
if (backendCart?.cart_id) {
|
|
1571
|
+
cart.cartId = backendCart.cart_id;
|
|
1572
|
+
if (Array.isArray(backendCart.line_items) && backendCart.line_items.length > 0) {
|
|
1573
|
+
this.updateFromBackendCart(sponsorId, backendCart);
|
|
1574
|
+
} else {
|
|
1575
|
+
if (backendCart.currency) cart.currency = backendCart.currency;
|
|
1576
|
+
if (backendCart.shipping_country) cart.shippingCountry = backendCart.shipping_country;
|
|
1577
|
+
if (backendCart.subtotal != null) cart.subtotal = backendCart.subtotal;
|
|
1578
|
+
if (backendCart.shipping != null) cart.shipping = backendCart.shipping;
|
|
1579
|
+
if (backendCart.available_shipping_countries) {
|
|
1580
|
+
cart.availableShippingCountries = backendCart.available_shipping_countries;
|
|
1581
|
+
}
|
|
1582
|
+
this.persist();
|
|
1583
|
+
this.emit();
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
this.emitError("createCart failed", err);
|
|
1588
|
+
}
|
|
1589
|
+
return cart.cartId;
|
|
1590
|
+
})().finally(() => {
|
|
1591
|
+
this.cartIdPromises.delete(sponsorId);
|
|
1592
|
+
});
|
|
1593
|
+
this.cartIdPromises.set(sponsorId, creation);
|
|
1594
|
+
return creation;
|
|
1595
|
+
}
|
|
1596
|
+
/** Overwrite local cart state with the backend cart response (source of truth). */
|
|
1597
|
+
updateFromBackendCart(sponsorId, backendCart) {
|
|
1598
|
+
const cart = this.cartsBySponsor.get(sponsorId);
|
|
1599
|
+
if (!cart) return;
|
|
1600
|
+
if (backendCart.cart_id) cart.cartId = backendCart.cart_id;
|
|
1601
|
+
if (backendCart.currency) cart.currency = backendCart.currency;
|
|
1602
|
+
if (backendCart.shipping_country) cart.shippingCountry = backendCart.shipping_country;
|
|
1603
|
+
if (backendCart.subtotal != null) cart.subtotal = backendCart.subtotal;
|
|
1604
|
+
if (backendCart.shipping != null) cart.shipping = backendCart.shipping;
|
|
1605
|
+
if (backendCart.available_shipping_countries) {
|
|
1606
|
+
cart.availableShippingCountries = backendCart.available_shipping_countries;
|
|
1607
|
+
}
|
|
1608
|
+
if (Array.isArray(backendCart.line_items)) {
|
|
1609
|
+
const existingItems = cart.items || [];
|
|
1610
|
+
if (backendCart.line_items.length === 0 && existingItems.length > 0) {
|
|
1611
|
+
this.persist();
|
|
1612
|
+
this.emit();
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
cart.items = backendCart.line_items.map((li) => {
|
|
1616
|
+
const urlFromBackend = (typeof li.image === "string" ? li.image : li.image?.url) || li.imageUrl || li.image_url || "";
|
|
1617
|
+
const matchedLocal = existingItems.find((existing) => {
|
|
1618
|
+
const p1 = normId(existing.productId);
|
|
1619
|
+
const p2 = normId(li.product_id);
|
|
1620
|
+
const v1 = normId(existing.variantId);
|
|
1621
|
+
const v2 = normId(li.variant_id);
|
|
1622
|
+
if (p1 && p2 && p1 === p2 && v1 === v2) return true;
|
|
1623
|
+
if (v1 && v2 && v1 === v2) return true;
|
|
1624
|
+
if (p1 && p2 && p1 === p2 && !v1 && !v2) return true;
|
|
1625
|
+
const n1 = (existing.name || "").trim().toLowerCase();
|
|
1626
|
+
const n2 = (li.title || "").trim().toLowerCase();
|
|
1627
|
+
if (n1 && n2 && n1 === n2 && v1 === v2) return true;
|
|
1628
|
+
return false;
|
|
1629
|
+
});
|
|
1630
|
+
const imageUrl = urlFromBackend || matchedLocal?.imageUrl || "";
|
|
1631
|
+
return {
|
|
1632
|
+
id: li.id,
|
|
1633
|
+
cartItemId: li.id,
|
|
1634
|
+
productId: li.product_id,
|
|
1635
|
+
sponsorId,
|
|
1636
|
+
variantId: li.variant_id != null ? Number(li.variant_id) : void 0,
|
|
1637
|
+
brand: li.brand || matchedLocal?.brand || "",
|
|
1638
|
+
name: li.title ? li.variant_title ? `${li.title} \u2014 ${li.variant_title}` : li.title : matchedLocal?.name || "",
|
|
1639
|
+
unitPrice: li.price?.amount_incl_taxes ?? li.price?.amount ?? matchedLocal?.unitPrice ?? 0,
|
|
1640
|
+
currency: li.price?.currency_code || backendCart.currency || matchedLocal?.currency || "NOK",
|
|
1641
|
+
imageUrl,
|
|
1642
|
+
quantity: li.quantity,
|
|
1643
|
+
shipping: li.shipping,
|
|
1644
|
+
availableShippings: li.available_shippings
|
|
1645
|
+
};
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
this.persist();
|
|
1649
|
+
this.emit();
|
|
1650
|
+
if (this.pendingQtyIntents.size > 0) {
|
|
1651
|
+
const currentCart = this.cartsBySponsor.get(sponsorId);
|
|
1652
|
+
for (const [key, qty] of [...this.pendingQtyIntents]) {
|
|
1653
|
+
const line = currentCart?.items.find(
|
|
1654
|
+
(i) => this.intentKey(i.productId, i.variantId) === key && i.cartItemId
|
|
1655
|
+
);
|
|
1656
|
+
if (line) {
|
|
1657
|
+
this.pendingQtyIntents.delete(key);
|
|
1658
|
+
if (line.quantity !== qty) {
|
|
1659
|
+
this.updateQuantity(line.cartItemId, sponsorId, qty);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
// MARK: - Mutations
|
|
1666
|
+
/** Add a Product DTO to a sponsor's cart. Bumps quantity if already present.
|
|
1667
|
+
* When a variantId is given, the line uses the VARIANT's price / image / title
|
|
1668
|
+
* (not the product base), so different combinations are priced correctly. */
|
|
1669
|
+
addProduct(opts) {
|
|
1670
|
+
const variant = opts.variantId != null ? opts.product.variants?.find((v) => v.id === opts.variantId) : void 0;
|
|
1671
|
+
const price = variant?.price ?? opts.product.price;
|
|
1672
|
+
const unitPrice = price.amount_incl_taxes ?? price.amount ?? 0;
|
|
1673
|
+
const imageUrl = variant?.images?.[0]?.url || primaryImageUrl(opts.product) || "";
|
|
1674
|
+
const name = variant?.title ? `${opts.product.title} \u2014 ${variant.title}` : opts.product.title;
|
|
1675
|
+
return this.addManual({
|
|
1676
|
+
productId: opts.product.id,
|
|
1677
|
+
sponsorId: opts.sponsorId,
|
|
1678
|
+
sponsorName: opts.sponsorName,
|
|
1679
|
+
brand: opts.product.brand ?? "",
|
|
1680
|
+
name,
|
|
1681
|
+
unitPrice,
|
|
1682
|
+
currency: price.currency_code,
|
|
1683
|
+
imageUrl,
|
|
1684
|
+
quantity: opts.quantity,
|
|
1685
|
+
variantId: opts.variantId
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
/** Add a manually-specified line item (no Product DTO). */
|
|
1689
|
+
addManual(opts) {
|
|
1690
|
+
const qty = Math.max(1, opts.quantity ?? 1);
|
|
1691
|
+
let cart = this.cartsBySponsor.get(opts.sponsorId);
|
|
1692
|
+
if (!cart) {
|
|
1693
|
+
cart = {
|
|
1694
|
+
sponsorId: opts.sponsorId,
|
|
1695
|
+
sponsorName: opts.sponsorName,
|
|
1696
|
+
items: [],
|
|
1697
|
+
currency: opts.currency
|
|
1698
|
+
};
|
|
1699
|
+
this.cartsBySponsor.set(opts.sponsorId, cart);
|
|
1700
|
+
} else if (opts.sponsorName && !cart.sponsorName) {
|
|
1701
|
+
cart.sponsorName = opts.sponsorName;
|
|
1702
|
+
}
|
|
1703
|
+
const existing = cart.items.find((i) => {
|
|
1704
|
+
const p1 = normId(i.productId);
|
|
1705
|
+
const p2 = normId(opts.productId);
|
|
1706
|
+
const v1 = normId(i.variantId);
|
|
1707
|
+
const v2 = normId(opts.variantId);
|
|
1708
|
+
if (p1 && p2 && p1 === p2 && v1 === v2) return true;
|
|
1709
|
+
if (v1 && v2 && v1 === v2) return true;
|
|
1710
|
+
if (p1 && p2 && p1 === p2 && !v1 && !v2) return true;
|
|
1711
|
+
const n1 = (i.name || "").trim().toLowerCase();
|
|
1712
|
+
const n2 = (opts.name || "").trim().toLowerCase();
|
|
1713
|
+
if (n1 && n2 && n1 === n2 && v1 === v2) return true;
|
|
1714
|
+
return false;
|
|
1715
|
+
});
|
|
1716
|
+
if (existing) {
|
|
1717
|
+
existing.quantity += qty;
|
|
1718
|
+
} else {
|
|
1719
|
+
const item = {
|
|
1720
|
+
id: lineId(),
|
|
1721
|
+
productId: opts.productId,
|
|
1722
|
+
sponsorId: opts.sponsorId,
|
|
1723
|
+
variantId: opts.variantId,
|
|
1724
|
+
brand: opts.brand,
|
|
1725
|
+
name: opts.name,
|
|
1726
|
+
unitPrice: opts.unitPrice,
|
|
1727
|
+
currency: opts.currency,
|
|
1728
|
+
imageUrl: opts.imageUrl,
|
|
1729
|
+
quantity: qty
|
|
1730
|
+
};
|
|
1731
|
+
cart.items.push(item);
|
|
1732
|
+
}
|
|
1733
|
+
this.persist();
|
|
1734
|
+
this.emit();
|
|
1735
|
+
const addedItem = existing ?? cart.items[cart.items.length - 1];
|
|
1736
|
+
const addedLineId = addedItem.id;
|
|
1737
|
+
const seq = this.nextSeq(opts.sponsorId);
|
|
1738
|
+
void (async () => {
|
|
1739
|
+
try {
|
|
1740
|
+
const cartId = await this.ensureCartId(opts.sponsorId, opts.currency || void 0);
|
|
1741
|
+
if (cartId) {
|
|
1742
|
+
const gqlOpts = await getCartGraphQLOptions(opts.sponsorId);
|
|
1743
|
+
const lineItemsInput = [
|
|
1744
|
+
{
|
|
1745
|
+
product_id: Number(opts.productId || 0),
|
|
1746
|
+
quantity: qty,
|
|
1747
|
+
variant_id: opts.variantId != null ? Number(opts.variantId) : null
|
|
1748
|
+
}
|
|
1749
|
+
];
|
|
1750
|
+
const resCart = await addItem(cartId, lineItemsInput, gqlOpts);
|
|
1751
|
+
if (resCart && Array.isArray(resCart.line_items)) {
|
|
1752
|
+
this.applyBackendCart(opts.sponsorId, resCart, seq);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
const c = this.cartsBySponsor.get(opts.sponsorId);
|
|
1757
|
+
const line = c?.items.find((i) => i.id === addedLineId);
|
|
1758
|
+
if (c && line) {
|
|
1759
|
+
line.quantity -= qty;
|
|
1760
|
+
if (line.quantity <= 0) c.items = c.items.filter((i) => i !== line);
|
|
1761
|
+
if (c.items.length === 0) this.cartsBySponsor.delete(opts.sponsorId);
|
|
1762
|
+
this.persist();
|
|
1763
|
+
this.emit();
|
|
1764
|
+
}
|
|
1765
|
+
this.emitError("AddItem rejected by the backend \u2014 item removed from the cart", err);
|
|
1766
|
+
}
|
|
1767
|
+
})();
|
|
1768
|
+
return addedItem;
|
|
1769
|
+
}
|
|
1770
|
+
intentKey(productId, variantId) {
|
|
1771
|
+
return `${normId(productId)}::${normId(variantId)}`;
|
|
1772
|
+
}
|
|
1773
|
+
updateQuantity(lineItemId, sponsorId, quantity) {
|
|
1774
|
+
const cart = this.cartsBySponsor.get(sponsorId);
|
|
1775
|
+
if (!cart) return;
|
|
1776
|
+
const item = cart.items.find((i) => i.id === lineItemId || i.cartItemId === lineItemId);
|
|
1777
|
+
if (!item) return;
|
|
1778
|
+
const targetItemId = item.cartItemId;
|
|
1779
|
+
if (quantity <= 0) {
|
|
1780
|
+
cart.items = cart.items.filter((i) => i !== item);
|
|
1781
|
+
} else {
|
|
1782
|
+
item.quantity = quantity;
|
|
1783
|
+
}
|
|
1784
|
+
if (cart.items.length === 0) this.cartsBySponsor.delete(sponsorId);
|
|
1785
|
+
this.persist();
|
|
1786
|
+
this.emit();
|
|
1787
|
+
if (!cart.cartId) return;
|
|
1788
|
+
if (!targetItemId) {
|
|
1789
|
+
this.pendingQtyIntents.set(this.intentKey(item.productId, item.variantId), quantity);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const backendCartId = cart.cartId;
|
|
1793
|
+
const seq = this.nextSeq(sponsorId);
|
|
1794
|
+
void (async () => {
|
|
1795
|
+
try {
|
|
1796
|
+
const gqlOpts = await getCartGraphQLOptions(sponsorId);
|
|
1797
|
+
const resCart = quantity <= 0 ? await deleteItem(backendCartId, targetItemId, gqlOpts) : await updateItem(backendCartId, targetItemId, null, quantity, gqlOpts);
|
|
1798
|
+
if (resCart && Array.isArray(resCart.line_items)) {
|
|
1799
|
+
this.applyBackendCart(sponsorId, resCart, seq);
|
|
1800
|
+
}
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
this.emitError("Update/DeleteItem failed \u2014 cart re-synced from backend", err);
|
|
1803
|
+
void this.fetchCartFromBackend(sponsorId);
|
|
1804
|
+
}
|
|
1805
|
+
})();
|
|
1806
|
+
}
|
|
1807
|
+
removeItem(lineItemId, sponsorId) {
|
|
1808
|
+
this.updateQuantity(lineItemId, sponsorId, 0);
|
|
1809
|
+
}
|
|
1810
|
+
clearSponsorCart(sponsorId) {
|
|
1811
|
+
if (this.cartsBySponsor.delete(sponsorId)) {
|
|
1812
|
+
this.persist();
|
|
1813
|
+
this.emit();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
clearAllCarts() {
|
|
1817
|
+
if (this.cartsBySponsor.size === 0) return;
|
|
1818
|
+
this.cartsBySponsor.clear();
|
|
1819
|
+
this.persist();
|
|
1820
|
+
this.emit();
|
|
1821
|
+
}
|
|
1822
|
+
/** Refresh the local cart from the backend (source of truth). */
|
|
1823
|
+
async fetchCartFromBackend(sponsorId) {
|
|
1824
|
+
const cart = this.cartsBySponsor.get(sponsorId);
|
|
1825
|
+
if (!cart?.cartId) return null;
|
|
1826
|
+
const gqlOpts = await getCartGraphQLOptions(sponsorId);
|
|
1827
|
+
const resCart = await getCart(cart.cartId, gqlOpts);
|
|
1828
|
+
if (resCart) this.updateFromBackendCart(sponsorId, resCart);
|
|
1829
|
+
return resCart;
|
|
1830
|
+
}
|
|
1831
|
+
/** Line items grouped by supplier (shipping is per-supplier server-side). */
|
|
1832
|
+
async fetchLineItemsBySupplier(sponsorId) {
|
|
1833
|
+
const cart = this.cartsBySponsor.get(sponsorId);
|
|
1834
|
+
if (!cart?.cartId) return null;
|
|
1835
|
+
const gqlOpts = await getCartGraphQLOptions(sponsorId);
|
|
1836
|
+
return getLineItemsBySupplier(cart.cartId, gqlOpts);
|
|
1837
|
+
}
|
|
1838
|
+
/** Set the chosen shipping option per supplier on the backend cart. */
|
|
1839
|
+
async updateShippingsBySupplier(sponsorId, data) {
|
|
1840
|
+
const cart = this.cartsBySponsor.get(sponsorId);
|
|
1841
|
+
if (!cart?.cartId) return null;
|
|
1842
|
+
const gqlOpts = await getCartGraphQLOptions(sponsorId);
|
|
1843
|
+
const resCart = await updateShippingsBySupplier(cart.cartId, data, gqlOpts);
|
|
1844
|
+
if (resCart && Array.isArray(resCart.line_items)) {
|
|
1845
|
+
this.updateFromBackendCart(sponsorId, resCart);
|
|
1846
|
+
}
|
|
1847
|
+
return resCart;
|
|
1848
|
+
}
|
|
1849
|
+
// MARK: - Read accessors
|
|
1850
|
+
getAllCarts() {
|
|
1851
|
+
return new Map(this.cartsBySponsor);
|
|
1852
|
+
}
|
|
1853
|
+
getCart(sponsorId) {
|
|
1854
|
+
return this.cartsBySponsor.get(sponsorId);
|
|
1855
|
+
}
|
|
1856
|
+
get itemCount() {
|
|
1857
|
+
let count = 0;
|
|
1858
|
+
for (const cart of this.cartsBySponsor.values()) {
|
|
1859
|
+
for (const item of cart.items) count += item.quantity;
|
|
1860
|
+
}
|
|
1861
|
+
return count;
|
|
1862
|
+
}
|
|
1863
|
+
subtotalForSponsor(sponsorId) {
|
|
1864
|
+
const cart = this.cartsBySponsor.get(sponsorId);
|
|
1865
|
+
if (!cart) return 0;
|
|
1866
|
+
return cart.items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
1867
|
+
}
|
|
1868
|
+
get totalAcrossSponsors() {
|
|
1869
|
+
let sum = 0;
|
|
1870
|
+
for (const cart of this.cartsBySponsor.values()) {
|
|
1871
|
+
for (const item of cart.items) sum += item.unitPrice * item.quantity;
|
|
1872
|
+
}
|
|
1873
|
+
return sum;
|
|
1874
|
+
}
|
|
1875
|
+
get primaryCurrency() {
|
|
1876
|
+
for (const cart of this.cartsBySponsor.values()) return cart.currency;
|
|
1877
|
+
return "NOK";
|
|
1878
|
+
}
|
|
1879
|
+
formatTotal() {
|
|
1880
|
+
return formatPrice(this.totalAcrossSponsors, this.primaryCurrency);
|
|
1881
|
+
}
|
|
1882
|
+
// MARK: - Persistence
|
|
1883
|
+
persist() {
|
|
1884
|
+
if (typeof localStorage === "undefined") return;
|
|
1885
|
+
try {
|
|
1886
|
+
const obj = {};
|
|
1887
|
+
for (const [k, v] of this.cartsBySponsor) obj[String(k)] = v;
|
|
1888
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
|
|
1889
|
+
} catch {
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
loadFromStorage() {
|
|
1893
|
+
if (typeof localStorage === "undefined") return /* @__PURE__ */ new Map();
|
|
1894
|
+
try {
|
|
1895
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
1896
|
+
if (!raw) return /* @__PURE__ */ new Map();
|
|
1897
|
+
const obj = JSON.parse(raw);
|
|
1898
|
+
const map = /* @__PURE__ */ new Map();
|
|
1899
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1900
|
+
const id = parseInt(k, 10);
|
|
1901
|
+
if (!Number.isNaN(id) && v && Array.isArray(v.items)) map.set(id, v);
|
|
1902
|
+
}
|
|
1903
|
+
return map;
|
|
1904
|
+
} catch {
|
|
1905
|
+
return /* @__PURE__ */ new Map();
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
emit() {
|
|
1909
|
+
const detail = {
|
|
1910
|
+
cartsBySponsor: this.getAllCarts(),
|
|
1911
|
+
itemCount: this.itemCount
|
|
1912
|
+
};
|
|
1913
|
+
this.dispatchEvent(new CustomEvent("change", { detail }));
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
// src/core/checkout/payments/apple-pay.ts
|
|
1918
|
+
var STRIPE_JS_URL = "https://js.stripe.com/v3";
|
|
1919
|
+
var stripeJsPromise = null;
|
|
1920
|
+
function preloadStripeJs() {
|
|
1921
|
+
if (typeof window === "undefined") return;
|
|
1922
|
+
void loadStripeJs().catch(() => {
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
function loadStripeJs() {
|
|
1926
|
+
if (stripeJsPromise) return stripeJsPromise;
|
|
1927
|
+
if (typeof document === "undefined") {
|
|
1928
|
+
return Promise.reject(new Error("[VioApplePay] Stripe.js requires a browser environment"));
|
|
1929
|
+
}
|
|
1930
|
+
stripeJsPromise = new Promise((resolve, reject) => {
|
|
1931
|
+
const w = window;
|
|
1932
|
+
if (w.Stripe) {
|
|
1933
|
+
resolve(w.Stripe);
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const existing = document.querySelector(
|
|
1937
|
+
`script[src^="${STRIPE_JS_URL}"]`
|
|
1938
|
+
);
|
|
1939
|
+
if (existing) {
|
|
1940
|
+
existing.addEventListener("load", () => resolve(w.Stripe));
|
|
1941
|
+
existing.addEventListener(
|
|
1942
|
+
"error",
|
|
1943
|
+
() => reject(new Error("[VioApplePay] Failed to load Stripe.js"))
|
|
1944
|
+
);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
const script = document.createElement("script");
|
|
1948
|
+
script.src = STRIPE_JS_URL;
|
|
1949
|
+
script.async = true;
|
|
1950
|
+
script.addEventListener("load", () => resolve(w.Stripe));
|
|
1951
|
+
script.addEventListener(
|
|
1952
|
+
"error",
|
|
1953
|
+
() => reject(new Error("[VioApplePay] Failed to load Stripe.js"))
|
|
1954
|
+
);
|
|
1955
|
+
document.head.appendChild(script);
|
|
1956
|
+
});
|
|
1957
|
+
return stripeJsPromise;
|
|
1958
|
+
}
|
|
1959
|
+
async function getStripe(config) {
|
|
1960
|
+
const Stripe = await loadStripeJs();
|
|
1961
|
+
return Stripe(
|
|
1962
|
+
config.publishableKey,
|
|
1963
|
+
config.connectedAccount ? { stripeAccount: config.connectedAccount } : void 0
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
function buildRequestOptions(config) {
|
|
1967
|
+
const ship = config.shippingOptions;
|
|
1968
|
+
const preselectedShipping = ship && ship.length > 0 ? ship[0].amount : 0;
|
|
1969
|
+
return {
|
|
1970
|
+
country: config.country,
|
|
1971
|
+
currency: config.currency.toLowerCase(),
|
|
1972
|
+
total: { label: config.label, amount: config.amount + preselectedShipping },
|
|
1973
|
+
requestPayerName: config.requestPayerName ?? true,
|
|
1974
|
+
requestPayerEmail: config.requestPayerEmail ?? true,
|
|
1975
|
+
requestShipping: ship && ship.length > 0 ? true : config.requestShipping ?? false,
|
|
1976
|
+
...ship && ship.length > 0 ? { shippingOptions: ship } : {},
|
|
1977
|
+
disableWallets: ["link", "googlePay"]
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
async function checkApplePayAvailability(config) {
|
|
1981
|
+
if (typeof window === "undefined") return false;
|
|
1982
|
+
if (typeof window.ApplePaySession === "undefined") {
|
|
1983
|
+
return false;
|
|
1984
|
+
}
|
|
1985
|
+
if (!config.publishableKey) return false;
|
|
1986
|
+
try {
|
|
1987
|
+
const stripe = await getStripe(config);
|
|
1988
|
+
const pr = stripe.paymentRequest(buildRequestOptions(config));
|
|
1989
|
+
const result = await pr.canMakePayment();
|
|
1990
|
+
return !!result?.applePay;
|
|
1991
|
+
} catch (err) {
|
|
1992
|
+
if (typeof console !== "undefined") {
|
|
1993
|
+
console.warn("[VioApplePay] availability check failed:", err);
|
|
1994
|
+
}
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async function showApplePaySheet(config) {
|
|
1999
|
+
if (!config.publishableKey) {
|
|
2000
|
+
throw new Error("[VioApplePay] publishableKey is required");
|
|
2001
|
+
}
|
|
2002
|
+
const stripe = await getStripe(config);
|
|
2003
|
+
const pr = stripe.paymentRequest(buildRequestOptions(config));
|
|
2004
|
+
const canMake = await pr.canMakePayment();
|
|
2005
|
+
if (!canMake?.applePay) {
|
|
2006
|
+
throw new Error("[VioApplePay] Apple Pay is not available on this device");
|
|
2007
|
+
}
|
|
2008
|
+
return new Promise((resolve, reject) => {
|
|
2009
|
+
let settled = false;
|
|
2010
|
+
pr.on("shippingaddresschange", (raw) => {
|
|
2011
|
+
const ev = raw;
|
|
2012
|
+
ev.updateWith({ status: "success" });
|
|
2013
|
+
});
|
|
2014
|
+
pr.on("paymentmethod", (raw) => {
|
|
2015
|
+
const ev = raw;
|
|
2016
|
+
settled = true;
|
|
2017
|
+
const result = {
|
|
2018
|
+
paymentMethodId: ev.paymentMethod.id,
|
|
2019
|
+
payerName: ev.payerName,
|
|
2020
|
+
payerEmail: ev.payerEmail,
|
|
2021
|
+
payerPhone: ev.payerPhone,
|
|
2022
|
+
shippingAddress: ev.shippingAddress
|
|
2023
|
+
};
|
|
2024
|
+
void (async () => {
|
|
2025
|
+
try {
|
|
2026
|
+
const confirmation = config.onAuthorize ? await config.onAuthorize(result) : void 0;
|
|
2027
|
+
ev.complete("success");
|
|
2028
|
+
resolve({ ...result, confirmation });
|
|
2029
|
+
} catch (err) {
|
|
2030
|
+
ev.complete("fail");
|
|
2031
|
+
reject(err);
|
|
2032
|
+
}
|
|
2033
|
+
})();
|
|
2034
|
+
});
|
|
2035
|
+
pr.on("cancel", () => {
|
|
2036
|
+
if (!settled) resolve(null);
|
|
2037
|
+
});
|
|
2038
|
+
pr.show();
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
async function prepareApplePay(config) {
|
|
2042
|
+
const unavailable = (msg) => ({
|
|
2043
|
+
available: false,
|
|
2044
|
+
show: () => Promise.reject(new Error(msg))
|
|
2045
|
+
});
|
|
2046
|
+
if (typeof window === "undefined" || !config.publishableKey) {
|
|
2047
|
+
return unavailable("[VioApplePay] unavailable");
|
|
2048
|
+
}
|
|
2049
|
+
if (typeof window.ApplePaySession === "undefined") {
|
|
2050
|
+
return unavailable("[VioApplePay] ApplePaySession not available");
|
|
2051
|
+
}
|
|
2052
|
+
try {
|
|
2053
|
+
const stripe = await getStripe(config);
|
|
2054
|
+
const pr = stripe.paymentRequest(buildRequestOptions(config));
|
|
2055
|
+
if (config.shippingOptions && config.shippingOptions.length > 0) {
|
|
2056
|
+
pr.on("shippingoptionchange", (raw) => {
|
|
2057
|
+
const ev = raw;
|
|
2058
|
+
const opt = config.shippingOptions?.find((o) => o.id === ev.shippingOption?.id);
|
|
2059
|
+
ev.updateWith({
|
|
2060
|
+
status: "success",
|
|
2061
|
+
total: { label: config.label, amount: config.amount + (opt?.amount ?? 0) }
|
|
2062
|
+
});
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
pr.on("shippingaddresschange", (raw) => {
|
|
2066
|
+
const ev = raw;
|
|
2067
|
+
ev.updateWith({ status: "success" });
|
|
2068
|
+
});
|
|
2069
|
+
const result = await pr.canMakePayment();
|
|
2070
|
+
if (!result?.applePay) return unavailable("[VioApplePay] Apple Pay not available");
|
|
2071
|
+
return {
|
|
2072
|
+
available: true,
|
|
2073
|
+
show() {
|
|
2074
|
+
return new Promise((resolve, reject) => {
|
|
2075
|
+
let settled = false;
|
|
2076
|
+
pr.on("paymentmethod", (raw) => {
|
|
2077
|
+
const ev = raw;
|
|
2078
|
+
settled = true;
|
|
2079
|
+
const res = {
|
|
2080
|
+
paymentMethodId: ev.paymentMethod.id,
|
|
2081
|
+
payerName: ev.payerName,
|
|
2082
|
+
payerEmail: ev.payerEmail,
|
|
2083
|
+
payerPhone: ev.payerPhone,
|
|
2084
|
+
shippingAddress: ev.shippingAddress
|
|
2085
|
+
};
|
|
2086
|
+
void (async () => {
|
|
2087
|
+
try {
|
|
2088
|
+
const confirmation = config.onAuthorize ? await config.onAuthorize(res) : void 0;
|
|
2089
|
+
ev.complete("success");
|
|
2090
|
+
resolve({ ...res, confirmation });
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
ev.complete("fail");
|
|
2093
|
+
reject(err);
|
|
2094
|
+
}
|
|
2095
|
+
})();
|
|
2096
|
+
});
|
|
2097
|
+
pr.on("cancel", () => {
|
|
2098
|
+
if (!settled) reject(new Error("[VioApplePay] User cancelled Apple Pay"));
|
|
2099
|
+
});
|
|
2100
|
+
pr.show();
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
if (typeof console !== "undefined") {
|
|
2106
|
+
console.warn("[VioApplePay] prepare failed:", err);
|
|
2107
|
+
}
|
|
2108
|
+
return unavailable("[VioApplePay] prepare failed");
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/core/checkout/payments/klarna.ts
|
|
2113
|
+
var KLARNA_SDK_URL = "https://js.klarna.com/web-sdk/v2/klarna.mjs";
|
|
2114
|
+
var sdkCache = /* @__PURE__ */ new Map();
|
|
2115
|
+
function loadKlarnaSdk(config) {
|
|
2116
|
+
if (typeof window === "undefined") {
|
|
2117
|
+
return Promise.reject(new Error("[VioKlarna] requires a browser environment"));
|
|
2118
|
+
}
|
|
2119
|
+
if (!config.clientId) {
|
|
2120
|
+
return Promise.reject(new Error("[VioKlarna] clientId is required"));
|
|
2121
|
+
}
|
|
2122
|
+
const cacheKey = `${config.clientId}:${config.environment ?? "default"}`;
|
|
2123
|
+
const cached = sdkCache.get(cacheKey);
|
|
2124
|
+
if (cached) return cached;
|
|
2125
|
+
const url = KLARNA_SDK_URL;
|
|
2126
|
+
const promise = import(
|
|
2127
|
+
/* @vite-ignore */
|
|
2128
|
+
url
|
|
2129
|
+
).then((mod) => {
|
|
2130
|
+
const factory = mod.KlarnaSDK ?? mod.default;
|
|
2131
|
+
if (typeof factory !== "function") {
|
|
2132
|
+
throw new Error("[VioKlarna] KlarnaSDK factory not found in module");
|
|
2133
|
+
}
|
|
2134
|
+
return factory({
|
|
2135
|
+
clientId: config.clientId,
|
|
2136
|
+
products: ["PAYMENT"],
|
|
2137
|
+
...config.locale ? { locale: config.locale } : {},
|
|
2138
|
+
...config.environment ? { environment: config.environment } : {}
|
|
2139
|
+
});
|
|
2140
|
+
}).catch((err) => {
|
|
2141
|
+
sdkCache.delete(cacheKey);
|
|
2142
|
+
throw err;
|
|
2143
|
+
});
|
|
2144
|
+
sdkCache.set(cacheKey, promise);
|
|
2145
|
+
return promise;
|
|
2146
|
+
}
|
|
2147
|
+
async function isKlarnaAvailable(config) {
|
|
2148
|
+
if (typeof window === "undefined" || !config.clientId) return false;
|
|
2149
|
+
try {
|
|
2150
|
+
await loadKlarnaSdk(config);
|
|
2151
|
+
return true;
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
if (typeof console !== "undefined") {
|
|
2154
|
+
console.warn("[VioKlarna] availability check failed:", err);
|
|
2155
|
+
}
|
|
2156
|
+
return false;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
function findAuthToken(value, depth = 0) {
|
|
2160
|
+
if (!value || depth > 4 || typeof value !== "object") return void 0;
|
|
2161
|
+
const o = value;
|
|
2162
|
+
for (const key of ["authorizationToken", "authorization_token", "authToken"]) {
|
|
2163
|
+
if (typeof o[key] === "string" && o[key].length > 0) return o[key];
|
|
2164
|
+
}
|
|
2165
|
+
for (const k of Object.keys(o)) {
|
|
2166
|
+
const found = findAuthToken(o[k], depth + 1);
|
|
2167
|
+
if (found) return found;
|
|
2168
|
+
}
|
|
2169
|
+
return void 0;
|
|
2170
|
+
}
|
|
2171
|
+
async function mountKlarnaExpressButton(opts) {
|
|
2172
|
+
const sdk = await loadKlarnaSdk(opts.config);
|
|
2173
|
+
const button = sdk.Payment.button({
|
|
2174
|
+
id: "vio-klarna-express",
|
|
2175
|
+
// `PAY` = one-time purchase (vs BUY/SUBSCRIBE). Must be an array.
|
|
2176
|
+
intents: ["PAY"],
|
|
2177
|
+
// initiationMode belongs on the BUTTON config (per Klarna Web SDK docs),
|
|
2178
|
+
// NOT inside the initiate() data — when set only in initiate() the SDK
|
|
2179
|
+
// falls back to its default (DEVICE_BEST → desktop popup), which opened
|
|
2180
|
+
// and closed without completing. Forcing REDIRECT here makes the click do
|
|
2181
|
+
// a full-page navigation to Klarna's hosted journey, returning via
|
|
2182
|
+
// `returnUrl` (recovered by resumeKlarnaReturn()).
|
|
2183
|
+
initiationMode: "REDIRECT"
|
|
2184
|
+
});
|
|
2185
|
+
const returnUrl = opts.returnUrl ?? (typeof window !== "undefined" ? window.location.origin + window.location.pathname : "");
|
|
2186
|
+
let settled = false;
|
|
2187
|
+
button.on("click", () => {
|
|
2188
|
+
if (opts.pendingKey && opts.pendingContext && typeof sessionStorage !== "undefined") {
|
|
2189
|
+
try {
|
|
2190
|
+
sessionStorage.setItem(opts.pendingKey, JSON.stringify(opts.pendingContext));
|
|
2191
|
+
} catch {
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
const promise = sdk.Payment.initiate(() => ({
|
|
2195
|
+
currency: opts.paymentRequest.currency,
|
|
2196
|
+
paymentAmount: opts.paymentRequest.amount,
|
|
2197
|
+
intents: ["PAY"],
|
|
2198
|
+
locale: getGlobalLocale(),
|
|
2199
|
+
// REDIRECT = full-page navigation to Klarna's hosted journey, then back
|
|
2200
|
+
// to `returnUrl` with the result. We force this (vs DEVICE_BEST's desktop
|
|
2201
|
+
// popup) because the popup proved unreliable here — it opened and closed
|
|
2202
|
+
// without completing. The redirect flow has no popup-blocker / window-
|
|
2203
|
+
// messaging failure modes; the return is recovered via resumeKlarnaReturn().
|
|
2204
|
+
initiationMode: "REDIRECT",
|
|
2205
|
+
returnUrl,
|
|
2206
|
+
...opts.paymentRequest.supplementaryPurchaseData ? { supplementaryPurchaseData: opts.paymentRequest.supplementaryPurchaseData } : {}
|
|
2207
|
+
}));
|
|
2208
|
+
Promise.resolve(promise).then((result) => {
|
|
2209
|
+
if (settled) return;
|
|
2210
|
+
const token = findAuthToken(result);
|
|
2211
|
+
if (token) {
|
|
2212
|
+
settled = true;
|
|
2213
|
+
opts.onAuthorize({ authorizationToken: token, raw: result });
|
|
2214
|
+
} else if (typeof console !== "undefined") {
|
|
2215
|
+
console.warn("[VioKlarna] authorize resolved without a token:", result);
|
|
2216
|
+
}
|
|
2217
|
+
}).catch((err) => {
|
|
2218
|
+
opts.onError?.(err);
|
|
2219
|
+
});
|
|
2220
|
+
return promise;
|
|
2221
|
+
});
|
|
2222
|
+
button.mount(opts.container);
|
|
2223
|
+
return {
|
|
2224
|
+
unmount: () => {
|
|
2225
|
+
try {
|
|
2226
|
+
button.unmount();
|
|
2227
|
+
} catch {
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
function readKlarnaTokenFromUrl() {
|
|
2233
|
+
if (typeof window === "undefined") return null;
|
|
2234
|
+
const params = new URLSearchParams(window.location.search);
|
|
2235
|
+
for (const name of [
|
|
2236
|
+
"authorization_token",
|
|
2237
|
+
"authorizationToken",
|
|
2238
|
+
"klarna_authorization_token",
|
|
2239
|
+
"vio_klarna_token"
|
|
2240
|
+
]) {
|
|
2241
|
+
const v = params.get(name);
|
|
2242
|
+
if (v && v.length > 0 && v !== "{klarna.authorization_token}") {
|
|
2243
|
+
return { token: v, param: name };
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
return null;
|
|
2247
|
+
}
|
|
2248
|
+
function cleanUrlParams(params) {
|
|
2249
|
+
if (typeof window === "undefined" || !window.history?.replaceState) return;
|
|
2250
|
+
const url = new URL(window.location.href);
|
|
2251
|
+
let changed = false;
|
|
2252
|
+
for (const p of params) {
|
|
2253
|
+
if (url.searchParams.has(p)) {
|
|
2254
|
+
url.searchParams.delete(p);
|
|
2255
|
+
changed = true;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (changed) window.history.replaceState({}, "", url.toString());
|
|
2259
|
+
}
|
|
2260
|
+
async function listenForKlarnaCompletion(opts) {
|
|
2261
|
+
let sdk;
|
|
2262
|
+
try {
|
|
2263
|
+
sdk = await loadKlarnaSdk(opts.config);
|
|
2264
|
+
} catch (err) {
|
|
2265
|
+
if (typeof console !== "undefined") {
|
|
2266
|
+
console.warn("[VioKlarna] resume: SDK load failed:", err);
|
|
2267
|
+
}
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
let settled = false;
|
|
2271
|
+
const onComplete = (...args) => {
|
|
2272
|
+
if (settled) return;
|
|
2273
|
+
for (const a of args) {
|
|
2274
|
+
const token = findAuthToken(a);
|
|
2275
|
+
if (token) {
|
|
2276
|
+
settled = true;
|
|
2277
|
+
opts.onComplete({ authorizationToken: token, raw: a });
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
try {
|
|
2283
|
+
sdk.Payment.on?.("complete", onComplete);
|
|
2284
|
+
} catch {
|
|
2285
|
+
}
|
|
2286
|
+
try {
|
|
2287
|
+
sdk.Payment.on?.("error", (...a) => opts.onError?.(a[0]));
|
|
2288
|
+
} catch {
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// src/core/checkout/payments/klarna-payments.ts
|
|
2293
|
+
var KLARNA_KP_LIB_URL = "https://x.klarnacdn.net/kp/lib/v1/api.js";
|
|
2294
|
+
var kpLoad = null;
|
|
2295
|
+
function loadKpLib() {
|
|
2296
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
2297
|
+
return Promise.reject(new Error("[VioKlarnaKP] requires a browser environment"));
|
|
2298
|
+
}
|
|
2299
|
+
if (window.Klarna?.Payments) return Promise.resolve(window.Klarna);
|
|
2300
|
+
if (kpLoad) return kpLoad;
|
|
2301
|
+
kpLoad = new Promise((resolve, reject) => {
|
|
2302
|
+
const timer = setTimeout(
|
|
2303
|
+
() => reject(new Error("[VioKlarnaKP] kp/lib/v1/api.js load timed out")),
|
|
2304
|
+
12e3
|
|
2305
|
+
);
|
|
2306
|
+
const ready = () => {
|
|
2307
|
+
clearTimeout(timer);
|
|
2308
|
+
if (window.Klarna?.Payments) resolve(window.Klarna);
|
|
2309
|
+
else reject(new Error("[VioKlarnaKP] Klarna global missing after ready"));
|
|
2310
|
+
};
|
|
2311
|
+
const prev = window.klarnaAsyncCallback;
|
|
2312
|
+
window.klarnaAsyncCallback = () => {
|
|
2313
|
+
if (typeof prev === "function") {
|
|
2314
|
+
try {
|
|
2315
|
+
prev();
|
|
2316
|
+
} catch {
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
ready();
|
|
2320
|
+
};
|
|
2321
|
+
const existing = document.querySelector(
|
|
2322
|
+
`script[src="${KLARNA_KP_LIB_URL}"]`
|
|
2323
|
+
);
|
|
2324
|
+
if (existing) {
|
|
2325
|
+
if (window.Klarna?.Payments) ready();
|
|
2326
|
+
} else {
|
|
2327
|
+
const s = document.createElement("script");
|
|
2328
|
+
s.src = KLARNA_KP_LIB_URL;
|
|
2329
|
+
s.async = true;
|
|
2330
|
+
s.onerror = () => {
|
|
2331
|
+
clearTimeout(timer);
|
|
2332
|
+
kpLoad = null;
|
|
2333
|
+
reject(new Error("[VioKlarnaKP] failed to load kp/lib/v1/api.js"));
|
|
2334
|
+
};
|
|
2335
|
+
document.head.appendChild(s);
|
|
2336
|
+
}
|
|
2337
|
+
}).catch((err) => {
|
|
2338
|
+
kpLoad = null;
|
|
2339
|
+
throw err;
|
|
2340
|
+
});
|
|
2341
|
+
return kpLoad;
|
|
2342
|
+
}
|
|
2343
|
+
async function isKlarnaPaymentsAvailable() {
|
|
2344
|
+
if (typeof window === "undefined") return false;
|
|
2345
|
+
try {
|
|
2346
|
+
await loadKpLib();
|
|
2347
|
+
return true;
|
|
2348
|
+
} catch (err) {
|
|
2349
|
+
if (typeof console !== "undefined") {
|
|
2350
|
+
console.warn("[VioKlarnaKP] availability check failed:", err);
|
|
2351
|
+
}
|
|
2352
|
+
return false;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
function findPayNowCategory(categories) {
|
|
2356
|
+
if (!Array.isArray(categories) || categories.length === 0) return null;
|
|
2357
|
+
const byId = (re) => categories.find((c) => c?.identifier && re.test(c.identifier));
|
|
2358
|
+
const byName = (re) => categories.find((c) => c?.name && re.test(c.name));
|
|
2359
|
+
const cat = categories.find((c) => c?.identifier === "pay_now") ?? byName(/betal\s*n[åa]|pay\s*now/i) ?? byId(/pay_now|direct_debit|direct_bank_transfer|pay_in_full|klarna_pay_now/i) ?? byName(/direkte|bank|kort|straks|sofort/i) ?? categories.find((c) => c?.identifier && !/pay_later|pay_over_time|slice_it/.test(c.identifier));
|
|
2360
|
+
return cat?.identifier ?? null;
|
|
2361
|
+
}
|
|
2362
|
+
async function createKlarnaPaymentsWidget(input) {
|
|
2363
|
+
const Klarna = await loadKpLib();
|
|
2364
|
+
Klarna.Payments.init({ client_token: input.clientToken });
|
|
2365
|
+
let selected = input.category ?? findPayNowCategory(input.categories) ?? input.categories[0]?.identifier ?? "pay_now";
|
|
2366
|
+
return {
|
|
2367
|
+
categories: input.categories,
|
|
2368
|
+
get selected() {
|
|
2369
|
+
return selected;
|
|
2370
|
+
},
|
|
2371
|
+
load(category) {
|
|
2372
|
+
if (category) selected = category;
|
|
2373
|
+
return new Promise((resolve, reject) => {
|
|
2374
|
+
try {
|
|
2375
|
+
Klarna.Payments.load(
|
|
2376
|
+
{ container: input.container, payment_method_category: selected },
|
|
2377
|
+
{},
|
|
2378
|
+
(res) => {
|
|
2379
|
+
if (res?.error) reject(res.error);
|
|
2380
|
+
else resolve({ showForm: res?.show_form !== false });
|
|
2381
|
+
}
|
|
2382
|
+
);
|
|
2383
|
+
} catch (err) {
|
|
2384
|
+
reject(err);
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
},
|
|
2388
|
+
authorize(data) {
|
|
2389
|
+
return new Promise((resolve, reject) => {
|
|
2390
|
+
try {
|
|
2391
|
+
Klarna.Payments.authorize(
|
|
2392
|
+
{ payment_method_category: selected, auto_finalize: true },
|
|
2393
|
+
data ?? {},
|
|
2394
|
+
(res) => {
|
|
2395
|
+
if (res?.approved && res.authorization_token) {
|
|
2396
|
+
resolve(res.authorization_token);
|
|
2397
|
+
} else if (res?.error) {
|
|
2398
|
+
reject(res.error);
|
|
2399
|
+
} else if (res?.finalize_required) {
|
|
2400
|
+
reject(new Error("Klarna requires a finalize step (multistep)"));
|
|
2401
|
+
} else {
|
|
2402
|
+
reject(new Error("Klarna authorization was not approved"));
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
);
|
|
2406
|
+
} catch (err) {
|
|
2407
|
+
reject(err);
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// src/core/checkout/checkout-manager.ts
|
|
2415
|
+
function describeError(err) {
|
|
2416
|
+
if (err instanceof Error) return err.message;
|
|
2417
|
+
if (typeof err === "string") return err;
|
|
2418
|
+
if (err && typeof err === "object") {
|
|
2419
|
+
const o = err;
|
|
2420
|
+
for (const k of ["message", "error", "reason", "description"]) {
|
|
2421
|
+
if (typeof o[k] === "string" && o[k]) return o[k];
|
|
2422
|
+
}
|
|
2423
|
+
try {
|
|
2424
|
+
return JSON.stringify(err);
|
|
2425
|
+
} catch {
|
|
2426
|
+
return String(err);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return String(err);
|
|
2430
|
+
}
|
|
2431
|
+
var KLARNA_PENDING_KEY = "vio.klarna.pending.v1";
|
|
2432
|
+
var KLARNA_SHIPPING_OPTIONS = [
|
|
2433
|
+
{
|
|
2434
|
+
id: "standard",
|
|
2435
|
+
method: "Standard",
|
|
2436
|
+
description: "3\u20135 virkedager",
|
|
2437
|
+
price: 4900,
|
|
2438
|
+
tax_amount: 0,
|
|
2439
|
+
tax_rate: 0,
|
|
2440
|
+
preselected: true
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
id: "express",
|
|
2444
|
+
method: "Express",
|
|
2445
|
+
description: "1\u20132 virkedager",
|
|
2446
|
+
price: 9900,
|
|
2447
|
+
tax_amount: 0,
|
|
2448
|
+
tax_rate: 0,
|
|
2449
|
+
preselected: false
|
|
2450
|
+
}
|
|
2451
|
+
];
|
|
2452
|
+
var CREATE_PAYMENT_KLARNA_NATIVE_MUTATION = `
|
|
2453
|
+
mutation CreatePaymentKlarnaNative($checkoutId: String!, $countryCode: String, $currency: String, $locale: String, $returnUrl: String, $intent: String, $customer: KlarnaNativeCustomerInput, $billingAddress: KlarnaNativeAddressInput, $shippingAddress: KlarnaNativeAddressInput, $autoCapture: Boolean) {
|
|
2454
|
+
Payment {
|
|
2455
|
+
CreatePaymentKlarnaNative(checkout_id: $checkoutId, country_code: $countryCode, currency: $currency, locale: $locale, return_url: $returnUrl, intent: $intent, customer: $customer, billing_address: $billingAddress, shipping_address: $shippingAddress, auto_capture: $autoCapture) {
|
|
2456
|
+
client_token
|
|
2457
|
+
session_id
|
|
2458
|
+
purchase_country
|
|
2459
|
+
purchase_currency
|
|
2460
|
+
cart_id
|
|
2461
|
+
checkout_id
|
|
2462
|
+
payment_method_categories {
|
|
2463
|
+
identifier
|
|
2464
|
+
name
|
|
2465
|
+
asset_urls {
|
|
2466
|
+
descriptive
|
|
2467
|
+
standard
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
`;
|
|
2474
|
+
var CONFIRM_PAYMENT_KLARNA_NATIVE_MUTATION = `
|
|
2475
|
+
mutation ConfirmPaymentKlarnaNative($checkoutId: String!, $authorizationToken: String!, $autoCapture: Boolean, $billingAddress: KlarnaNativeAddressInput, $shippingAddress: KlarnaNativeAddressInput, $customer: KlarnaNativeCustomerInput) {
|
|
2476
|
+
Payment {
|
|
2477
|
+
ConfirmPaymentKlarnaNative(checkout_id: $checkoutId, authorization_token: $authorizationToken, auto_capture: $autoCapture, billing_address: $billingAddress, shipping_address: $shippingAddress, customer: $customer) {
|
|
2478
|
+
order_id
|
|
2479
|
+
checkout_id
|
|
2480
|
+
fraud_status
|
|
2481
|
+
order {
|
|
2482
|
+
order_id
|
|
2483
|
+
status
|
|
2484
|
+
purchase_country
|
|
2485
|
+
purchase_currency
|
|
2486
|
+
locale
|
|
2487
|
+
billing_address {
|
|
2488
|
+
given_name
|
|
2489
|
+
family_name
|
|
2490
|
+
email
|
|
2491
|
+
street_address
|
|
2492
|
+
postal_code
|
|
2493
|
+
city
|
|
2494
|
+
country
|
|
2495
|
+
}
|
|
2496
|
+
shipping_address {
|
|
2497
|
+
given_name
|
|
2498
|
+
family_name
|
|
2499
|
+
email
|
|
2500
|
+
street_address
|
|
2501
|
+
postal_code
|
|
2502
|
+
city
|
|
2503
|
+
country
|
|
2504
|
+
}
|
|
2505
|
+
order_amount
|
|
2506
|
+
order_tax_amount
|
|
2507
|
+
total_line_items_price
|
|
2508
|
+
order_lines {
|
|
2509
|
+
type
|
|
2510
|
+
name
|
|
2511
|
+
quantity
|
|
2512
|
+
unit_price
|
|
2513
|
+
tax_rate
|
|
2514
|
+
total_amount
|
|
2515
|
+
total_discount_amount
|
|
2516
|
+
total_tax_amount
|
|
2517
|
+
merchant_data
|
|
2518
|
+
}
|
|
2519
|
+
merchant_urls {
|
|
2520
|
+
terms
|
|
2521
|
+
checkout
|
|
2522
|
+
confirmation
|
|
2523
|
+
push
|
|
2524
|
+
}
|
|
2525
|
+
html_snippet
|
|
2526
|
+
started_at
|
|
2527
|
+
last_modified_at
|
|
2528
|
+
options {
|
|
2529
|
+
allow_separate_shipping_address
|
|
2530
|
+
date_of_birth_mandatory
|
|
2531
|
+
require_validate_callback_success
|
|
2532
|
+
phone_mandatory
|
|
2533
|
+
auto_capture
|
|
2534
|
+
}
|
|
2535
|
+
shipping_options {
|
|
2536
|
+
id
|
|
2537
|
+
name
|
|
2538
|
+
price
|
|
2539
|
+
tax_amount
|
|
2540
|
+
tax_rate
|
|
2541
|
+
preselected
|
|
2542
|
+
}
|
|
2543
|
+
merchant_data
|
|
2544
|
+
selected_shipping_option {
|
|
2545
|
+
id
|
|
2546
|
+
name
|
|
2547
|
+
price
|
|
2548
|
+
tax_amount
|
|
2549
|
+
tax_rate
|
|
2550
|
+
preselected
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
`;
|
|
2557
|
+
function buildKlarnaCustomer(address) {
|
|
2558
|
+
if (!address) return null;
|
|
2559
|
+
return {
|
|
2560
|
+
dob: address.dob || address.date_of_birth || null,
|
|
2561
|
+
email: address.email || null,
|
|
2562
|
+
organization_registration_id: address.organization_registration_id || address.org_id || null,
|
|
2563
|
+
phone: address.phone || address.telephone || null,
|
|
2564
|
+
type: address.type || "person"
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
function buildKlarnaAddress(address) {
|
|
2568
|
+
if (!address) return null;
|
|
2569
|
+
return {
|
|
2570
|
+
city: address.city || null,
|
|
2571
|
+
country: address.country || address.countryCode || getGlobalCountryCode(),
|
|
2572
|
+
email: address.email || null,
|
|
2573
|
+
family_name: address.familyName || address.family_name || address.last_name || address.lastName || null,
|
|
2574
|
+
given_name: address.givenName || address.given_name || address.first_name || address.firstName || null,
|
|
2575
|
+
phone: address.phone || null,
|
|
2576
|
+
postal_code: address.postalCode || address.postal_code || address.zip || null,
|
|
2577
|
+
region: address.region || address.state || null,
|
|
2578
|
+
street_address: address.streetAddress || address.street_address || address.address1 || address.street || address.address || // CheckoutAddress uses plain `address` for the street
|
|
2579
|
+
null
|
|
2580
|
+
// street_address2 intentionally omitted — the KlarnaNativeAddressInput
|
|
2581
|
+
// backend type rejects it.
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
function buildCheckoutAddress(address) {
|
|
2585
|
+
if (!address) return null;
|
|
2586
|
+
const firstName = address.firstName || address.first_name || address.givenName || address.given_name || null;
|
|
2587
|
+
const lastName = address.lastName || address.last_name || address.familyName || address.family_name || null;
|
|
2588
|
+
const address1 = address.address || address.address1 || address.street || address.street_address || null;
|
|
2589
|
+
const city = address.city || null;
|
|
2590
|
+
const zip = address.postalCode || address.postal_code || address.zip || null;
|
|
2591
|
+
const country_code = address.countryCode || address.country_code || address.country || getGlobalCountryCode();
|
|
2592
|
+
if (!firstName && !lastName && !address1 && !city && !zip) return null;
|
|
2593
|
+
return {
|
|
2594
|
+
first_name: firstName,
|
|
2595
|
+
last_name: lastName,
|
|
2596
|
+
address1,
|
|
2597
|
+
city,
|
|
2598
|
+
zip,
|
|
2599
|
+
country_code
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
function resolveHttpsReturnUrl(overrideUrl, params = null) {
|
|
2603
|
+
let url = void 0;
|
|
2604
|
+
if (!url && typeof window !== "undefined" && window.location) {
|
|
2605
|
+
url = window.location.href;
|
|
2606
|
+
}
|
|
2607
|
+
if (!url || typeof url !== "string" || url.startsWith("about:") || url.startsWith("file:")) {
|
|
2608
|
+
url = "https://vev.design/checkout/confirmation";
|
|
2609
|
+
}
|
|
2610
|
+
if (url.startsWith("http://")) {
|
|
2611
|
+
url = "https://" + url.slice(7);
|
|
2612
|
+
} else if (!url.startsWith("https://")) {
|
|
2613
|
+
url = "https://" + url.replace(/^[a-zA-Z]+:\/\//, "");
|
|
2614
|
+
}
|
|
2615
|
+
if (params && typeof params === "object") {
|
|
2616
|
+
try {
|
|
2617
|
+
const parsed = new URL(url);
|
|
2618
|
+
Object.keys(params).forEach((k) => {
|
|
2619
|
+
if (params[k] !== void 0 && params[k] !== null) {
|
|
2620
|
+
parsed.searchParams.set(k, String(params[k]));
|
|
2621
|
+
}
|
|
2622
|
+
});
|
|
2623
|
+
return parsed.toString();
|
|
2624
|
+
} catch {
|
|
2625
|
+
const queryStr = Object.keys(params).filter((k) => params[k] !== void 0 && params[k] !== null).map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(String(params[k]))}`).join("&");
|
|
2626
|
+
if (queryStr) {
|
|
2627
|
+
url += (url.includes("?") ? "&" : "?") + queryStr;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return url;
|
|
2632
|
+
}
|
|
2633
|
+
async function executeKlarnaGraphQL(query, variables, sponsorId) {
|
|
2634
|
+
return executeCartGraphQL(query, variables, await getCartGraphQLOptions(sponsorId));
|
|
2635
|
+
}
|
|
2636
|
+
var CheckoutManager = class _CheckoutManager extends EventTarget {
|
|
2637
|
+
constructor(cartManager) {
|
|
2638
|
+
super();
|
|
2639
|
+
this.cartManager = cartManager;
|
|
2640
|
+
this.state = null;
|
|
2641
|
+
this.applePayAvailableCache = null;
|
|
2642
|
+
/** Gateway hints learned from CreatePaymentApplePay (per session). The
|
|
2643
|
+
* backend's gateway_merchant_id may be a publishable key (pk_…) or a Stripe
|
|
2644
|
+
* Connect account (acct_…) — stored defensively by prefix. */
|
|
2645
|
+
this.applePayPublishableKey = null;
|
|
2646
|
+
this.applePayConnectedAccount = null;
|
|
2647
|
+
this.klarnaAvailableCache = null;
|
|
2648
|
+
this.klarnaOrderInFlight = false;
|
|
2649
|
+
/** Last backend shippings fetched (per-supplier), for UI reuse. */
|
|
2650
|
+
this.lastFetchedShippings = [];
|
|
2651
|
+
/** Per-sponsor payment methods cache + in-flight dedupe. */
|
|
2652
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2653
|
+
this.paymentMethodsCache = /* @__PURE__ */ new Map();
|
|
2654
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2655
|
+
this.paymentMethodsPromises = /* @__PURE__ */ new Map();
|
|
2656
|
+
preloadStripeJs();
|
|
2657
|
+
}
|
|
2658
|
+
open(sponsorId) {
|
|
2659
|
+
const cart = this.cartManager.getCart(sponsorId);
|
|
2660
|
+
if (!cart || cart.items.length === 0) {
|
|
2661
|
+
throw new Error(`[VioCheckout] sponsor ${sponsorId} has no cart items`);
|
|
2662
|
+
}
|
|
2663
|
+
this.state = {
|
|
2664
|
+
sponsorId,
|
|
2665
|
+
subtotal: this.cartManager.subtotalForSponsor(sponsorId),
|
|
2666
|
+
// Page-level currency wins over whatever the persisted cart carries —
|
|
2667
|
+
// the host may have switched currency since the cart was created.
|
|
2668
|
+
currency: getGlobalCurrency()
|
|
2669
|
+
};
|
|
2670
|
+
this.emit();
|
|
2671
|
+
return this.state;
|
|
2672
|
+
}
|
|
2673
|
+
close() {
|
|
2674
|
+
this.state = null;
|
|
2675
|
+
this.applePayAvailableCache = null;
|
|
2676
|
+
this.klarnaAvailableCache = null;
|
|
2677
|
+
this.emit();
|
|
2678
|
+
}
|
|
2679
|
+
// MARK: - Backend checkout (Vio Commerce GraphQL)
|
|
2680
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2681
|
+
/** Create the backend checkout for the sponsor's cart (auto-accepts terms). */
|
|
2682
|
+
async createCheckout(sponsorId) {
|
|
2683
|
+
const cart = this.cartManager.getCart(sponsorId);
|
|
2684
|
+
let cartId = cart?.cartId;
|
|
2685
|
+
if (!cartId) {
|
|
2686
|
+
cartId = await this.cartManager.ensureCartId(sponsorId, cart?.currency);
|
|
2687
|
+
if (!cartId) {
|
|
2688
|
+
throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${sponsorId}`);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
const opts = await getCartGraphQLOptions(sponsorId);
|
|
2692
|
+
const checkout = await createCheckout(cartId, opts);
|
|
2693
|
+
if (checkout?.id) {
|
|
2694
|
+
this.state = {
|
|
2695
|
+
...this.state ?? { sponsorId, subtotal: 0, currency: getGlobalCurrency() },
|
|
2696
|
+
sponsorId,
|
|
2697
|
+
checkoutId: checkout.id,
|
|
2698
|
+
checkout
|
|
2699
|
+
};
|
|
2700
|
+
this.emit();
|
|
2701
|
+
try {
|
|
2702
|
+
const updated = await updateCheckout(
|
|
2703
|
+
{
|
|
2704
|
+
checkoutId: checkout.id,
|
|
2705
|
+
buyerAcceptsPurchaseConditions: true,
|
|
2706
|
+
buyerAcceptsTermsConditions: true
|
|
2707
|
+
},
|
|
2708
|
+
opts
|
|
2709
|
+
);
|
|
2710
|
+
if (updated?.id) {
|
|
2711
|
+
this.state = { ...this.state, checkout: updated };
|
|
2712
|
+
this.emit();
|
|
2713
|
+
return updated;
|
|
2714
|
+
}
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
if (typeof console !== "undefined") {
|
|
2717
|
+
console.warn("[CheckoutManager] Failed to auto-accept terms & purchase conditions:", err);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
return checkout;
|
|
2722
|
+
}
|
|
2723
|
+
/** Update the backend checkout (terms are re-asserted unless overridden). */
|
|
2724
|
+
async updateCheckout(variables, sponsorId) {
|
|
2725
|
+
const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
|
|
2726
|
+
const fullVars = {
|
|
2727
|
+
buyerAcceptsPurchaseConditions: true,
|
|
2728
|
+
buyerAcceptsTermsConditions: true,
|
|
2729
|
+
...variables
|
|
2730
|
+
};
|
|
2731
|
+
const updated = await updateCheckout(fullVars, opts);
|
|
2732
|
+
if (updated?.id && this.state) {
|
|
2733
|
+
this.state = { ...this.state, checkoutId: updated.id, checkout: updated };
|
|
2734
|
+
this.emit();
|
|
2735
|
+
}
|
|
2736
|
+
return updated;
|
|
2737
|
+
}
|
|
2738
|
+
async getCheckout(checkoutId, sponsorId) {
|
|
2739
|
+
const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
|
|
2740
|
+
return getCheckout(checkoutId, opts);
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Payment methods enabled for the sponsor (backend-configured). Memoised per
|
|
2744
|
+
* sponsor with in-flight dedupe — several UI components ask on every open.
|
|
2745
|
+
*/
|
|
2746
|
+
async getAvailablePaymentMethods(sponsorId) {
|
|
2747
|
+
const spId = sponsorId ?? this.state?.sponsorId;
|
|
2748
|
+
if (!spId) return null;
|
|
2749
|
+
if (this.paymentMethodsCache.has(spId)) {
|
|
2750
|
+
return this.paymentMethodsCache.get(spId);
|
|
2751
|
+
}
|
|
2752
|
+
const inFlight = this.paymentMethodsPromises.get(spId);
|
|
2753
|
+
if (inFlight) return inFlight;
|
|
2754
|
+
const promise = (async () => {
|
|
2755
|
+
try {
|
|
2756
|
+
const opts = await getCartGraphQLOptions(spId);
|
|
2757
|
+
const res = await getAvailablePaymentMethods(opts);
|
|
2758
|
+
if (res) this.paymentMethodsCache.set(spId, res);
|
|
2759
|
+
return res;
|
|
2760
|
+
} finally {
|
|
2761
|
+
this.paymentMethodsPromises.delete(spId);
|
|
2762
|
+
}
|
|
2763
|
+
})();
|
|
2764
|
+
this.paymentMethodsPromises.set(spId, promise);
|
|
2765
|
+
return promise;
|
|
2766
|
+
}
|
|
2767
|
+
clearPaymentMethodsCache() {
|
|
2768
|
+
this.paymentMethodsCache.clear();
|
|
2769
|
+
this.paymentMethodsPromises.clear();
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Real shipping options from the backend cart (per supplier), mapped to the
|
|
2773
|
+
* KlarnaShippingOption shape (minor units). Empty array on failure — callers
|
|
2774
|
+
* fall back to the static KLARNA_SHIPPING_OPTIONS.
|
|
2775
|
+
*/
|
|
2776
|
+
async fetchAvailableShippings(sponsorId) {
|
|
2777
|
+
try {
|
|
2778
|
+
const spId = sponsorId ?? this.state?.sponsorId;
|
|
2779
|
+
if (!spId) return [];
|
|
2780
|
+
const cart = this.cartManager.getCart(spId);
|
|
2781
|
+
let cartId = cart?.cartId;
|
|
2782
|
+
if (!cartId) {
|
|
2783
|
+
cartId = await this.cartManager.ensureCartId(spId);
|
|
2784
|
+
}
|
|
2785
|
+
if (!cartId) return [];
|
|
2786
|
+
const opts = await getCartGraphQLOptions(spId);
|
|
2787
|
+
const supplierGroups = await getLineItemsBySupplier(cartId, opts);
|
|
2788
|
+
const firstGroup = Array.isArray(supplierGroups) ? supplierGroups[0] : null;
|
|
2789
|
+
const shippings = firstGroup?.available_shippings ?? [];
|
|
2790
|
+
if (Array.isArray(shippings) && shippings.length > 0) {
|
|
2791
|
+
const supplierId = firstGroup?.supplier?.id;
|
|
2792
|
+
const mapped = shippings.map((s, idx) => {
|
|
2793
|
+
const priceObj = s.price ?? {};
|
|
2794
|
+
const amount = typeof priceObj.amount_incl_taxes === "number" ? priceObj.amount_incl_taxes : typeof priceObj.amount === "number" ? priceObj.amount : 0;
|
|
2795
|
+
return {
|
|
2796
|
+
id: String(s.id),
|
|
2797
|
+
supplierId: supplierId != null ? String(supplierId) : void 0,
|
|
2798
|
+
method: s.name || "Default Shipping",
|
|
2799
|
+
name: s.name || "Default Shipping",
|
|
2800
|
+
description: s.description || "",
|
|
2801
|
+
country_code: s.country_code || "NO",
|
|
2802
|
+
price: Math.round(amount * 100),
|
|
2803
|
+
priceMajor: amount,
|
|
2804
|
+
currency: priceObj.currency_code || getGlobalCurrency(),
|
|
2805
|
+
tax_amount: Math.round((priceObj.tax_amount ?? 0) * 100),
|
|
2806
|
+
tax_rate: Math.round((priceObj.tax_rate ?? 0) * 100),
|
|
2807
|
+
preselected: idx === 0
|
|
2808
|
+
};
|
|
2809
|
+
});
|
|
2810
|
+
this.lastFetchedShippings = mapped;
|
|
2811
|
+
return mapped;
|
|
2812
|
+
}
|
|
2813
|
+
} catch (err) {
|
|
2814
|
+
if (typeof console !== "undefined") {
|
|
2815
|
+
console.warn("[CheckoutManager] fetchAvailableShippings failed:", err);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return [];
|
|
2819
|
+
}
|
|
2820
|
+
/** Set the chosen shipping per supplier on the backend cart. */
|
|
2821
|
+
async updateShippingsBySupplier(data, sponsorId) {
|
|
2822
|
+
const spId = sponsorId ?? this.state?.sponsorId;
|
|
2823
|
+
if (!spId) return null;
|
|
2824
|
+
const cart = this.cartManager.getCart(spId);
|
|
2825
|
+
if (!cart?.cartId) return null;
|
|
2826
|
+
const opts = await getCartGraphQLOptions(spId);
|
|
2827
|
+
const updated = await updateShippingsBySupplier(cart.cartId, data, opts);
|
|
2828
|
+
if (updated) {
|
|
2829
|
+
this.cartManager.updateFromBackendCart(spId, updated);
|
|
2830
|
+
}
|
|
2831
|
+
return updated;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Shared setup for the redirect payment links (Stripe / Vipps): ensure the
|
|
2835
|
+
* backend cart + checkout exist, push email/address onto the checkout, and
|
|
2836
|
+
* return everything the payment mutation needs.
|
|
2837
|
+
*/
|
|
2838
|
+
async prepareRedirectPayment(sponsorId, formData, extraUpdateVars = {}) {
|
|
2839
|
+
const spId = sponsorId ?? this.state?.sponsorId;
|
|
2840
|
+
if (!spId) throw new Error("[CheckoutManager] no sponsor for payment");
|
|
2841
|
+
const cart = this.cartManager.getCart(spId);
|
|
2842
|
+
let cartId = cart?.cartId;
|
|
2843
|
+
if (!cartId) {
|
|
2844
|
+
cartId = await this.cartManager.ensureCartId(spId);
|
|
2845
|
+
}
|
|
2846
|
+
if (!cartId) {
|
|
2847
|
+
throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
|
|
2848
|
+
}
|
|
2849
|
+
const opts = await getCartGraphQLOptions(spId);
|
|
2850
|
+
let checkoutId = this.state?.checkoutId;
|
|
2851
|
+
if (!checkoutId) {
|
|
2852
|
+
const checkoutRes = await createCheckout(cartId, opts);
|
|
2853
|
+
checkoutId = checkoutRes?.id;
|
|
2854
|
+
}
|
|
2855
|
+
if (!checkoutId) {
|
|
2856
|
+
throw new Error("[CheckoutManager] no backend checkout \u2014 cannot start the payment");
|
|
2857
|
+
}
|
|
2858
|
+
const isVipps = extraUpdateVars?.paymentMethod === "Vipps";
|
|
2859
|
+
const addr = typeof formData === "object" && formData !== null ? formData : this.state?.address;
|
|
2860
|
+
const addressObj = !isVipps ? buildCheckoutAddress(addr) : null;
|
|
2861
|
+
const emailVal = (typeof formData === "string" ? formData : formData?.email) || this.state?.address?.email || "";
|
|
2862
|
+
if (!emailVal || !emailVal.includes("@")) {
|
|
2863
|
+
throw new Error("[CheckoutManager] email is required to start the payment");
|
|
2864
|
+
}
|
|
2865
|
+
const updateVars = {
|
|
2866
|
+
checkoutId,
|
|
2867
|
+
email: emailVal,
|
|
2868
|
+
buyerAcceptsPurchaseConditions: true,
|
|
2869
|
+
buyerAcceptsTermsConditions: true,
|
|
2870
|
+
...extraUpdateVars
|
|
2871
|
+
};
|
|
2872
|
+
if (addressObj) {
|
|
2873
|
+
updateVars.shippingAddress = addressObj;
|
|
2874
|
+
updateVars.billingAddress = addressObj;
|
|
2875
|
+
}
|
|
2876
|
+
try {
|
|
2877
|
+
const updated = await updateCheckout(updateVars, opts);
|
|
2878
|
+
if (updated?.id) {
|
|
2879
|
+
this.state = {
|
|
2880
|
+
...this.state ?? { sponsorId: spId, subtotal: 0, currency: getGlobalCurrency() },
|
|
2881
|
+
sponsorId: spId,
|
|
2882
|
+
checkoutId: updated.id,
|
|
2883
|
+
checkout: updated
|
|
2884
|
+
};
|
|
2885
|
+
this.emit();
|
|
2886
|
+
checkoutId = updated.id;
|
|
2887
|
+
}
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
throw new Error(
|
|
2890
|
+
`[CheckoutManager] could not persist checkout details: ${describeError(err)}`
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
if (!checkoutId) {
|
|
2894
|
+
throw new Error("[CheckoutManager] no backend checkout \u2014 cannot start the payment");
|
|
2895
|
+
}
|
|
2896
|
+
return { spId, checkoutId, emailVal, opts };
|
|
2897
|
+
}
|
|
2898
|
+
/** Redirect the top window to a payment provider URL. */
|
|
2899
|
+
static redirectTo(url) {
|
|
2900
|
+
if (typeof window === "undefined") return;
|
|
2901
|
+
try {
|
|
2902
|
+
if (window.top && window.top !== window) {
|
|
2903
|
+
window.top.location.href = url;
|
|
2904
|
+
} else {
|
|
2905
|
+
window.location.href = url;
|
|
2906
|
+
}
|
|
2907
|
+
} catch {
|
|
2908
|
+
window.open(url, "_top") || window.open(url, "_blank");
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Stripe payment link: create/refresh the backend checkout, mint the Stripe
|
|
2913
|
+
* Checkout URL (CreatePaymentStripe) and redirect. The return URL carries
|
|
2914
|
+
* vio_payment/vio_method/checkout_id so the page can show the confirmation.
|
|
2915
|
+
*/
|
|
2916
|
+
async startStripePayment(sponsorId, formData) {
|
|
2917
|
+
const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
|
|
2918
|
+
sponsorId,
|
|
2919
|
+
formData,
|
|
2920
|
+
{ paymentMethod: "Stripe" }
|
|
2921
|
+
);
|
|
2922
|
+
const successUrl = resolveHttpsReturnUrl(null, {
|
|
2923
|
+
vio_payment: "success",
|
|
2924
|
+
vio_method: "stripe",
|
|
2925
|
+
vio_sponsor: spId,
|
|
2926
|
+
checkout_id: checkoutId
|
|
2927
|
+
});
|
|
2928
|
+
const stripeRes = await createPaymentStripe(
|
|
2929
|
+
{ checkoutId, successUrl, paymentMethod: "Stripe", email: emailVal },
|
|
2930
|
+
opts
|
|
2931
|
+
);
|
|
2932
|
+
if (stripeRes?.checkout_url) {
|
|
2933
|
+
_CheckoutManager.redirectTo(stripeRes.checkout_url);
|
|
2934
|
+
return stripeRes;
|
|
2935
|
+
}
|
|
2936
|
+
throw new Error("Stripe payment link generation failed: missing checkout_url");
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Vipps payment link: same flow as Stripe — CreatePaymentVipps → redirect to
|
|
2940
|
+
* payment_url.
|
|
2941
|
+
*/
|
|
2942
|
+
async startVippsPayment(sponsorId, formData) {
|
|
2943
|
+
const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
|
|
2944
|
+
sponsorId,
|
|
2945
|
+
formData,
|
|
2946
|
+
{ paymentMethod: "Vipps" }
|
|
2947
|
+
);
|
|
2948
|
+
const returnUrl = resolveHttpsReturnUrl(null, {
|
|
2949
|
+
vio_payment: "success",
|
|
2950
|
+
vio_method: "vipps",
|
|
2951
|
+
vio_sponsor: spId,
|
|
2952
|
+
checkout_id: checkoutId
|
|
2953
|
+
});
|
|
2954
|
+
const vippsRes = await createPaymentVipps(
|
|
2955
|
+
{ checkoutId, email: emailVal, returnUrl },
|
|
2956
|
+
opts
|
|
2957
|
+
);
|
|
2958
|
+
if (vippsRes?.payment_url) {
|
|
2959
|
+
_CheckoutManager.redirectTo(vippsRes.payment_url);
|
|
2960
|
+
return vippsRes;
|
|
2961
|
+
}
|
|
2962
|
+
throw new Error("Vipps payment link generation failed: missing payment_url");
|
|
2963
|
+
}
|
|
2964
|
+
/** Vipps' own payment state for the checkout — see getVippsStatus in
|
|
2965
|
+
* cart-queries.ts for why this exists instead of trusting GetCheckout. */
|
|
2966
|
+
async getVippsStatus(checkoutId, sponsorId) {
|
|
2967
|
+
const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
|
|
2968
|
+
return getVippsStatus({ checkoutId }, opts);
|
|
2969
|
+
}
|
|
2970
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
2971
|
+
setAddress(address) {
|
|
2972
|
+
if (!this.state) return;
|
|
2973
|
+
this.state = { ...this.state, address };
|
|
2974
|
+
this.emit();
|
|
2975
|
+
}
|
|
2976
|
+
selectPaymentMethod(method) {
|
|
2977
|
+
if (!this.state) return;
|
|
2978
|
+
this.state = { ...this.state, paymentMethod: method };
|
|
2979
|
+
this.emit();
|
|
2980
|
+
this.dispatchEvent(
|
|
2981
|
+
new CustomEvent("payment-select", {
|
|
2982
|
+
detail: { method, state: this.state }
|
|
2983
|
+
})
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
// MARK: - Apple Pay (Stripe Payment Request)
|
|
2987
|
+
/**
|
|
2988
|
+
* Returns true if Apple Pay can be shown on this device + Stripe config.
|
|
2989
|
+
* Result is memoised — call `clearApplePayCache()` if config changes.
|
|
2990
|
+
*/
|
|
2991
|
+
async isApplePayAvailable(country = "NO") {
|
|
2992
|
+
if (this.applePayAvailableCache !== null) return this.applePayAvailableCache;
|
|
2993
|
+
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
2994
|
+
const publishableKey = cfg?.stripePublishableKey ?? "";
|
|
2995
|
+
if (!publishableKey || !this.state) {
|
|
2996
|
+
this.applePayAvailableCache = false;
|
|
2997
|
+
return false;
|
|
2998
|
+
}
|
|
2999
|
+
const amount = toSmallestUnit(this.state.subtotal, this.state.currency);
|
|
3000
|
+
const available = await checkApplePayAvailability({
|
|
3001
|
+
publishableKey,
|
|
3002
|
+
connectedAccount: this.findStripeConnectAccount() ?? void 0,
|
|
3003
|
+
amount,
|
|
3004
|
+
currency: this.state.currency,
|
|
3005
|
+
country,
|
|
3006
|
+
label: "Vio"
|
|
3007
|
+
});
|
|
3008
|
+
this.applePayAvailableCache = available;
|
|
3009
|
+
return available;
|
|
3010
|
+
}
|
|
3011
|
+
clearApplePayCache() {
|
|
3012
|
+
this.applePayAvailableCache = null;
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Prepare Apple Pay for the product-page express button (no active checkout
|
|
3016
|
+
* needed). Pre-creates the Stripe PaymentRequest + resolves canMakePayment so
|
|
3017
|
+
* the returned `show()` can fire synchronously inside the tap (Apple blocks
|
|
3018
|
+
* the sheet if `show()` is reached after an await). `show()` resolves the
|
|
3019
|
+
* sheet, then dispatches `payment-complete` (→ confirmation) or
|
|
3020
|
+
* `payment-error`. `available` is true only when Apple Pay will actually work.
|
|
3021
|
+
*/
|
|
3022
|
+
async prepareApplePayFor(amount, currency, country = "NO") {
|
|
3023
|
+
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
3024
|
+
const publishableKey = this.applePayPublishableKey || cfg?.stripePublishableKey || "";
|
|
3025
|
+
if (!publishableKey) return { available: false, show: async () => {
|
|
3026
|
+
} };
|
|
3027
|
+
const prepared = await prepareApplePay({
|
|
3028
|
+
publishableKey,
|
|
3029
|
+
connectedAccount: this.applePayConnectedAccount ?? this.findStripeConnectAccount() ?? void 0,
|
|
3030
|
+
amount: toSmallestUnit(amount, currency),
|
|
3031
|
+
currency,
|
|
3032
|
+
country,
|
|
3033
|
+
label: "Vio",
|
|
3034
|
+
// Native Apple Pay shipping — same Standard/Express options as Klarna.
|
|
3035
|
+
shippingOptions: KLARNA_SHIPPING_OPTIONS.map((o) => ({
|
|
3036
|
+
id: o.id,
|
|
3037
|
+
label: o.method,
|
|
3038
|
+
detail: o.description,
|
|
3039
|
+
amount: o.price
|
|
3040
|
+
})),
|
|
3041
|
+
// The real backend charge — sponsor resolved at authorize-time (the
|
|
3042
|
+
// product-detail button calls checkout.open() before show()).
|
|
3043
|
+
onAuthorize: (raw) => {
|
|
3044
|
+
const spId = this.state?.sponsorId ?? [...this.cartManager.getAllCarts().keys()][0];
|
|
3045
|
+
if (spId == null) {
|
|
3046
|
+
return Promise.reject(new Error("[VioCheckout] no sponsor for Apple Pay"));
|
|
3047
|
+
}
|
|
3048
|
+
return this.buildApplePayAuthorize(spId)(raw);
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
return {
|
|
3052
|
+
available: prepared.available,
|
|
3053
|
+
show: async () => {
|
|
3054
|
+
try {
|
|
3055
|
+
const result = await prepared.show();
|
|
3056
|
+
this.selectPaymentMethod("apple-pay");
|
|
3057
|
+
const state = this.state ?? { sponsorId: 0, subtotal: amount, currency };
|
|
3058
|
+
this.dispatchEvent(
|
|
3059
|
+
new CustomEvent("payment-complete", {
|
|
3060
|
+
detail: { method: "apple-pay", state, result }
|
|
3061
|
+
})
|
|
3062
|
+
);
|
|
3063
|
+
} catch (err) {
|
|
3064
|
+
if (typeof console !== "undefined") {
|
|
3065
|
+
console.warn("[VioCheckout] Apple Pay express failed:", err);
|
|
3066
|
+
}
|
|
3067
|
+
this.dispatchEvent(
|
|
3068
|
+
new CustomEvent("payment-error", {
|
|
3069
|
+
detail: { method: "apple-pay", error: describeError(err) }
|
|
3070
|
+
})
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* Prepare an Apple Pay express charge for an arbitrary amount (e.g. the whole
|
|
3078
|
+
* cart) with NO active checkout. Unlike `prepareApplePayFor`, the returned
|
|
3079
|
+
* `show()` RESOLVES with the result (or `null` on cancel/failure) and does
|
|
3080
|
+
* NOT dispatch the global `payment-complete` — so no checkout overlay appears.
|
|
3081
|
+
* The caller (the cart drawer) renders its own confirmation. `show()` must be
|
|
3082
|
+
* called synchronously inside the tap (Apple's user-gesture rule).
|
|
3083
|
+
*/
|
|
3084
|
+
async prepareApplePayExpress(amount, currency, country = "NO", sponsorId) {
|
|
3085
|
+
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
3086
|
+
const publishableKey = this.applePayPublishableKey || cfg?.stripePublishableKey || "";
|
|
3087
|
+
if (!publishableKey) return { available: false, show: async () => null };
|
|
3088
|
+
const spId = sponsorId ?? this.state?.sponsorId ?? [...this.cartManager.getAllCarts().keys()][0];
|
|
3089
|
+
const prepared = await prepareApplePay({
|
|
3090
|
+
publishableKey,
|
|
3091
|
+
connectedAccount: this.applePayConnectedAccount ?? this.findStripeConnectAccount() ?? void 0,
|
|
3092
|
+
amount: toSmallestUnit(amount, currency),
|
|
3093
|
+
currency,
|
|
3094
|
+
country,
|
|
3095
|
+
label: "Vio",
|
|
3096
|
+
// Native Apple Pay shipping (same Standard/Express options as Klarna) —
|
|
3097
|
+
// selected inside the sheet, so it's not a separate checkout step.
|
|
3098
|
+
shippingOptions: KLARNA_SHIPPING_OPTIONS.map((o) => ({
|
|
3099
|
+
id: o.id,
|
|
3100
|
+
label: o.method,
|
|
3101
|
+
detail: o.description,
|
|
3102
|
+
amount: o.price
|
|
3103
|
+
})),
|
|
3104
|
+
// The real backend charge, run inside the sheet's authorize event.
|
|
3105
|
+
...spId != null ? { onAuthorize: this.buildApplePayAuthorize(spId) } : {}
|
|
3106
|
+
});
|
|
3107
|
+
return {
|
|
3108
|
+
available: prepared.available,
|
|
3109
|
+
show: async () => {
|
|
3110
|
+
try {
|
|
3111
|
+
return await prepared.show();
|
|
3112
|
+
} catch (err) {
|
|
3113
|
+
if (typeof console !== "undefined") {
|
|
3114
|
+
console.warn("[VioCheckout] cart Apple Pay express failed:", err);
|
|
3115
|
+
}
|
|
3116
|
+
return null;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
/** The charge closure run inside the Apple Pay sheet's authorize event —
|
|
3122
|
+
* shared by the checkout button and the cart's express button. Fail-closed:
|
|
3123
|
+
* any failure throws and the sheet closes with failure, nothing confirmed. */
|
|
3124
|
+
buildApplePayAuthorize(spId) {
|
|
3125
|
+
return async (raw) => {
|
|
3126
|
+
let checkoutId = this.state?.checkoutId;
|
|
3127
|
+
if (!checkoutId) {
|
|
3128
|
+
const created = await this.createCheckout(spId);
|
|
3129
|
+
checkoutId = created?.id ?? this.state?.checkoutId;
|
|
3130
|
+
}
|
|
3131
|
+
if (!checkoutId) {
|
|
3132
|
+
throw new Error("[VioCheckout] could not obtain a checkout for Apple Pay");
|
|
3133
|
+
}
|
|
3134
|
+
const email = raw.payerEmail || this.state?.address?.email || "";
|
|
3135
|
+
try {
|
|
3136
|
+
await this.updateCheckout(
|
|
3137
|
+
{ checkoutId, paymentMethod: "Apple Pay", ...email ? { email } : {} },
|
|
3138
|
+
spId
|
|
3139
|
+
);
|
|
3140
|
+
} catch (err) {
|
|
3141
|
+
if (typeof console !== "undefined") {
|
|
3142
|
+
console.warn("[VioCheckout] updateCheckout before Apple Pay confirm failed:", err);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
const gqlOpts = await getCartGraphQLOptions(spId);
|
|
3146
|
+
const createRes = await createPaymentApplePay({ checkoutId }, gqlOpts);
|
|
3147
|
+
const gatewayId = String(createRes?.gateway_merchant_id ?? "");
|
|
3148
|
+
if (gatewayId.startsWith("pk_")) this.applePayPublishableKey = gatewayId;
|
|
3149
|
+
else if (gatewayId.startsWith("acct_")) this.applePayConnectedAccount = gatewayId;
|
|
3150
|
+
const addr = raw.shippingAddress ?? {};
|
|
3151
|
+
const str = (v) => typeof v === "string" ? v : "";
|
|
3152
|
+
const lines = Array.isArray(addr.addressLine) ? addr.addressLine : [];
|
|
3153
|
+
const fullName = str(addr.name) || raw.payerName || "";
|
|
3154
|
+
const shippingAddress = {
|
|
3155
|
+
address1: lines[0] || str(addr.line1) || "",
|
|
3156
|
+
address2: lines[1] || str(addr.line2) || "",
|
|
3157
|
+
city: str(addr.locality) || str(addr.city) || "",
|
|
3158
|
+
company: "",
|
|
3159
|
+
country: str(addr.country) || "",
|
|
3160
|
+
country_code: str(addr.countryCode) || getGlobalCountryCode(),
|
|
3161
|
+
first_name: str(addr.givenName) || fullName.split(" ")[0] || "",
|
|
3162
|
+
last_name: str(addr.familyName) || fullName.split(" ").slice(1).join(" ") || "",
|
|
3163
|
+
phone: str(addr.phone) || str(addr.phoneNumber) || raw.payerPhone || "",
|
|
3164
|
+
phone_code: "",
|
|
3165
|
+
province: str(addr.administrativeArea) || str(addr.state) || "",
|
|
3166
|
+
province_code: "",
|
|
3167
|
+
zip: str(addr.postalCode) || str(addr.postal_code) || ""
|
|
3168
|
+
};
|
|
3169
|
+
const confirmRes = await confirmPaymentApplePay(
|
|
3170
|
+
{ checkoutId, applePayToken: raw.paymentMethodId, email, shippingAddress },
|
|
3171
|
+
gqlOpts
|
|
3172
|
+
);
|
|
3173
|
+
const status = String(confirmRes?.status ?? "").toLowerCase();
|
|
3174
|
+
if (!confirmRes || _CheckoutManager.APPLE_PAY_FAILED_STATUSES.has(status)) {
|
|
3175
|
+
throw new Error(
|
|
3176
|
+
`[VioCheckout] Apple Pay confirmation failed (status: ${confirmRes?.status ?? "none"})`
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
return { orderId: confirmRes?.order_id, status: confirmRes?.status };
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
static {
|
|
3183
|
+
/** Backend statuses from ConfirmPaymentApplePay that mean the charge did
|
|
3184
|
+
* NOT go through — anything here fails the sheet (fail-closed). */
|
|
3185
|
+
this.APPLE_PAY_FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
3186
|
+
"failed",
|
|
3187
|
+
"declined",
|
|
3188
|
+
"rejected",
|
|
3189
|
+
"cancelled",
|
|
3190
|
+
"canceled",
|
|
3191
|
+
"error",
|
|
3192
|
+
"expired"
|
|
3193
|
+
]);
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Launches the Apple Pay sheet and charges server-side INSIDE the sheet's
|
|
3197
|
+
* authorize event (round-5 wiring, contract from the commerce backend):
|
|
3198
|
+
* 1. ensure a backend checkout exists (create if missing)
|
|
3199
|
+
* 2. UpdateCheckout(paymentMethod: "Apple Pay" + payer email)
|
|
3200
|
+
* 3. CreatePaymentApplePay → gateway hints (pk_/acct_ stored by prefix)
|
|
3201
|
+
* 4. ConfirmPaymentApplePay(token, email, shipping) → the REAL charge;
|
|
3202
|
+
* a failed/absent confirmation throws → the sheet closes with failure
|
|
3203
|
+
* Resolves null if the user closes the sheet. On success, dispatches
|
|
3204
|
+
* `payment-complete` with the backend order id in `result.confirmation`.
|
|
3205
|
+
*/
|
|
3206
|
+
async startApplePay(options = {}) {
|
|
3207
|
+
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
3208
|
+
const spId = options.sponsorId ?? this.state.sponsorId;
|
|
3209
|
+
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
3210
|
+
const publishableKey = this.applePayPublishableKey || cfg?.stripePublishableKey || "";
|
|
3211
|
+
if (!publishableKey) {
|
|
3212
|
+
throw new Error(
|
|
3213
|
+
"[VioCheckout] No Stripe publishable key configured. Pass `stripePublishableKey` to Vio.init({ ... })."
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
const amount = toSmallestUnit(this.state.subtotal, this.state.currency);
|
|
3217
|
+
const onAuthorize = this.buildApplePayAuthorize(spId);
|
|
3218
|
+
const result = await showApplePaySheet({
|
|
3219
|
+
publishableKey,
|
|
3220
|
+
connectedAccount: this.applePayConnectedAccount ?? this.findStripeConnectAccount() ?? void 0,
|
|
3221
|
+
amount,
|
|
3222
|
+
currency: this.state.currency,
|
|
3223
|
+
country: options.country ?? "NO",
|
|
3224
|
+
label: options.label ?? "Vio",
|
|
3225
|
+
onAuthorize
|
|
3226
|
+
});
|
|
3227
|
+
if (!result) return null;
|
|
3228
|
+
this.selectPaymentMethod("apple-pay");
|
|
3229
|
+
this.dispatchEvent(
|
|
3230
|
+
new CustomEvent("payment-complete", {
|
|
3231
|
+
detail: { method: "apple-pay", state: this.state, result }
|
|
3232
|
+
})
|
|
3233
|
+
);
|
|
3234
|
+
return result;
|
|
3235
|
+
}
|
|
3236
|
+
// MARK: - Klarna Express Checkout (Klarna Web SDK v2)
|
|
3237
|
+
/**
|
|
3238
|
+
* True if the Klarna Express button can be offered (clientId configured +
|
|
3239
|
+
* SDK loads). Memoised per active checkout. Never throws.
|
|
3240
|
+
*/
|
|
3241
|
+
async isKlarnaExpressAvailable() {
|
|
3242
|
+
if (this.klarnaAvailableCache !== null) return this.klarnaAvailableCache;
|
|
3243
|
+
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
3244
|
+
const clientId = cfg?.klarnaClientId ?? "";
|
|
3245
|
+
if (!clientId || !this.state) {
|
|
3246
|
+
this.klarnaAvailableCache = false;
|
|
3247
|
+
return false;
|
|
3248
|
+
}
|
|
3249
|
+
const available = await isKlarnaAvailable({
|
|
3250
|
+
clientId,
|
|
3251
|
+
locale: getGlobalLocale(),
|
|
3252
|
+
environment: cfg?.klarnaEnvironment
|
|
3253
|
+
});
|
|
3254
|
+
this.klarnaAvailableCache = available;
|
|
3255
|
+
return available;
|
|
3256
|
+
}
|
|
3257
|
+
clearKlarnaCache() {
|
|
3258
|
+
this.klarnaAvailableCache = null;
|
|
3259
|
+
}
|
|
3260
|
+
/**
|
|
3261
|
+
* Build the Klarna payment request from the active cart — amounts in the
|
|
3262
|
+
* currency's minor unit (øre/cents), one line item per cart line.
|
|
3263
|
+
*/
|
|
3264
|
+
buildKlarnaPaymentRequest() {
|
|
3265
|
+
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
3266
|
+
const currency = this.state.currency;
|
|
3267
|
+
const lineItems = this.items.map((item) => {
|
|
3268
|
+
const unitPrice = toSmallestUnit(item.unitPrice, currency);
|
|
3269
|
+
return {
|
|
3270
|
+
name: item.name || item.brand || "Produkt",
|
|
3271
|
+
quantity: item.quantity,
|
|
3272
|
+
unitPrice,
|
|
3273
|
+
totalAmount: unitPrice * item.quantity,
|
|
3274
|
+
reference: String(item.productId),
|
|
3275
|
+
...item.imageUrl ? { imageUrl: item.imageUrl } : {}
|
|
3276
|
+
};
|
|
3277
|
+
});
|
|
3278
|
+
return {
|
|
3279
|
+
currency,
|
|
3280
|
+
amount: toSmallestUnit(this.state.subtotal, currency),
|
|
3281
|
+
supplementaryPurchaseData: { lineItems }
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Mount the Klarna Express button into `container`. On a successful
|
|
3286
|
+
* authorization, fires `payment-complete` with the authorization token —
|
|
3287
|
+
* the host (or vio-backend) then creates the Klarna order server-side.
|
|
3288
|
+
*
|
|
3289
|
+
* Returns the button handle so the UI can unmount on teardown.
|
|
3290
|
+
*/
|
|
3291
|
+
async mountKlarnaExpress(container) {
|
|
3292
|
+
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
3293
|
+
const cfg = Configuration.get();
|
|
3294
|
+
if (!cfg.klarnaClientId) {
|
|
3295
|
+
throw new Error(
|
|
3296
|
+
"[VioCheckout] No Klarna client id configured. Pass `klarnaClientId` to Vio.init({ ... })."
|
|
3297
|
+
);
|
|
3298
|
+
}
|
|
3299
|
+
const pendingContext = this.buildKlarnaPendingContext();
|
|
3300
|
+
return mountKlarnaExpressButton({
|
|
3301
|
+
config: {
|
|
3302
|
+
clientId: cfg.klarnaClientId,
|
|
3303
|
+
locale: getGlobalLocale(),
|
|
3304
|
+
environment: cfg.klarnaEnvironment
|
|
3305
|
+
},
|
|
3306
|
+
container,
|
|
3307
|
+
paymentRequest: this.buildKlarnaPaymentRequest(),
|
|
3308
|
+
pendingKey: KLARNA_PENDING_KEY,
|
|
3309
|
+
pendingContext,
|
|
3310
|
+
// POPUP path: the initiate() promise resolves in-page with the token.
|
|
3311
|
+
onAuthorize: (result) => {
|
|
3312
|
+
void this.completeKlarnaOrder(result.authorizationToken, pendingContext, result);
|
|
3313
|
+
},
|
|
3314
|
+
onError: (err) => {
|
|
3315
|
+
if (typeof console !== "undefined") {
|
|
3316
|
+
console.warn("[VioCheckout] Klarna Express cancelled/error:", err);
|
|
3317
|
+
}
|
|
3318
|
+
this.dispatchEvent(
|
|
3319
|
+
new CustomEvent("payment-error", {
|
|
3320
|
+
detail: { method: "klarna", error: describeError(err) }
|
|
3321
|
+
})
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Klarna Payments — classic widget flow (server client_token). Creates a
|
|
3328
|
+
* session on vio-backend, then mounts Klarna's inline widget into
|
|
3329
|
+
* `container`. Returns a handle to switch category and to authorize → which
|
|
3330
|
+
* on approval creates the order via the same backend /orders route as KEC.
|
|
3331
|
+
*
|
|
3332
|
+
* Unlike `mountKlarnaExpress`, this needs no public `klarnaClientId` and no
|
|
3333
|
+
* origin handshake — auth is the server-side API key.
|
|
3334
|
+
*/
|
|
3335
|
+
async mountKlarnaPayments(container, opts) {
|
|
3336
|
+
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
3337
|
+
const sponsorId = this.state.sponsorId;
|
|
3338
|
+
const fetchedShippings = await this.fetchAvailableShippings(sponsorId);
|
|
3339
|
+
const shippings = fetchedShippings.length > 0 ? fetchedShippings : KLARNA_SHIPPING_OPTIONS;
|
|
3340
|
+
const { ctx } = opts?.withShipping ? this.buildKlarnaInstantContext(opts.shippingId, shippings) : { ctx: this.buildKlarnaPendingContext() };
|
|
3341
|
+
const selectedOpt = shippings.find((s) => s.id === opts?.shippingId) ?? shippings[0];
|
|
3342
|
+
if (selectedOpt?.id && selectedOpt?.supplierId) {
|
|
3343
|
+
try {
|
|
3344
|
+
await this.updateShippingsBySupplier(
|
|
3345
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
3346
|
+
sponsorId
|
|
3347
|
+
);
|
|
3348
|
+
} catch (err) {
|
|
3349
|
+
if (typeof console !== "undefined") {
|
|
3350
|
+
console.warn("[CheckoutManager] updateShippingsBySupplier failed during mount:", err);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
let checkoutId = ctx.checkoutId ?? this.state.checkoutId;
|
|
3355
|
+
try {
|
|
3356
|
+
const checkoutRes = await this.createCheckout(sponsorId);
|
|
3357
|
+
if (checkoutRes?.id) {
|
|
3358
|
+
checkoutId = checkoutRes.id;
|
|
3359
|
+
ctx.checkoutId = checkoutRes.id;
|
|
3360
|
+
}
|
|
3361
|
+
} catch (err) {
|
|
3362
|
+
if (typeof console !== "undefined") {
|
|
3363
|
+
console.warn("[CheckoutManager] CreateCheckout before Klarna init failed:", err);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
if (!checkoutId) {
|
|
3367
|
+
throw new Error("[CheckoutManager] no backend checkout for the Klarna session");
|
|
3368
|
+
}
|
|
3369
|
+
const address = this.state.address;
|
|
3370
|
+
const activeCurrency = getGlobalCurrency();
|
|
3371
|
+
if (this.state) this.state = { ...this.state, currency: activeCurrency };
|
|
3372
|
+
ctx.currency = activeCurrency;
|
|
3373
|
+
const variables = {
|
|
3374
|
+
checkoutId,
|
|
3375
|
+
countryCode: getGlobalCountryCode(),
|
|
3376
|
+
currency: activeCurrency,
|
|
3377
|
+
locale: getGlobalLocale(),
|
|
3378
|
+
returnUrl: resolveHttpsReturnUrl(null, {
|
|
3379
|
+
vio_payment: "success",
|
|
3380
|
+
vio_method: "klarna",
|
|
3381
|
+
vio_sponsor: sponsorId,
|
|
3382
|
+
checkout_id: checkoutId
|
|
3383
|
+
}),
|
|
3384
|
+
intent: "buy",
|
|
3385
|
+
customer: buildKlarnaCustomer(address),
|
|
3386
|
+
billingAddress: buildKlarnaAddress(address),
|
|
3387
|
+
shippingAddress: buildKlarnaAddress(address),
|
|
3388
|
+
autoCapture: true
|
|
3389
|
+
};
|
|
3390
|
+
const json = await executeKlarnaGraphQL(
|
|
3391
|
+
CREATE_PAYMENT_KLARNA_NATIVE_MUTATION,
|
|
3392
|
+
variables,
|
|
3393
|
+
sponsorId
|
|
3394
|
+
);
|
|
3395
|
+
const nativeRes = json?.data?.Payment?.CreatePaymentKlarnaNative;
|
|
3396
|
+
if (!nativeRes || !nativeRes.client_token) {
|
|
3397
|
+
throw new Error("Klarna session creation failed: missing client_token");
|
|
3398
|
+
}
|
|
3399
|
+
if (nativeRes.checkout_id) {
|
|
3400
|
+
ctx.checkoutId = nativeRes.checkout_id;
|
|
3401
|
+
this.state = { ...this.state, checkoutId: nativeRes.checkout_id };
|
|
3402
|
+
}
|
|
3403
|
+
if (nativeRes.session_id) {
|
|
3404
|
+
ctx.sessionId = nativeRes.session_id;
|
|
3405
|
+
this.state = { ...this.state, sessionId: nativeRes.session_id };
|
|
3406
|
+
}
|
|
3407
|
+
ctx.clientToken = nativeRes.client_token;
|
|
3408
|
+
this.state = { ...this.state, clientToken: nativeRes.client_token };
|
|
3409
|
+
const widget = await createKlarnaPaymentsWidget({
|
|
3410
|
+
clientToken: nativeRes.client_token,
|
|
3411
|
+
sessionId: nativeRes.session_id,
|
|
3412
|
+
categories: nativeRes.payment_method_categories ?? [],
|
|
3413
|
+
category: opts?.category,
|
|
3414
|
+
container
|
|
3415
|
+
});
|
|
3416
|
+
await widget.load();
|
|
3417
|
+
return {
|
|
3418
|
+
categories: widget.categories,
|
|
3419
|
+
shippings,
|
|
3420
|
+
get selected() {
|
|
3421
|
+
return widget.selected;
|
|
3422
|
+
},
|
|
3423
|
+
load: (category) => widget.load(category),
|
|
3424
|
+
authorize: async () => {
|
|
3425
|
+
try {
|
|
3426
|
+
const token = await widget.authorize();
|
|
3427
|
+
await this.completeKlarnaOrder(token, ctx, { authorizationToken: token });
|
|
3428
|
+
} catch (err) {
|
|
3429
|
+
if (typeof console !== "undefined") {
|
|
3430
|
+
console.warn("[VioCheckout] Klarna Payments authorize failed:", err);
|
|
3431
|
+
}
|
|
3432
|
+
this.dispatchEvent(
|
|
3433
|
+
new CustomEvent("payment-error", {
|
|
3434
|
+
detail: { method: "klarna", error: describeError(err) }
|
|
3435
|
+
})
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
},
|
|
3439
|
+
unmount: () => {
|
|
3440
|
+
try {
|
|
3441
|
+
container.innerHTML = "";
|
|
3442
|
+
} catch {
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
/** True if the Klarna Payments widget lib can load (UI gate). */
|
|
3448
|
+
async klarnaPaymentsAvailable() {
|
|
3449
|
+
return isKlarnaPaymentsAvailable();
|
|
3450
|
+
}
|
|
3451
|
+
/** Cart snapshot + chosen shipping line/amount, for express purchases. */
|
|
3452
|
+
buildKlarnaInstantContext(shippingId, availableShippings = []) {
|
|
3453
|
+
const base = this.buildKlarnaPendingContext();
|
|
3454
|
+
const options = availableShippings.length > 0 ? availableShippings : KLARNA_SHIPPING_OPTIONS;
|
|
3455
|
+
const shipping = (shippingId ? options.find((o) => o.id === shippingId) : void 0) ?? options.find((o) => o.preselected) ?? options[0];
|
|
3456
|
+
const orderLines = [...base.orderLines];
|
|
3457
|
+
if (shipping) {
|
|
3458
|
+
orderLines.push({
|
|
3459
|
+
name: `Frakt \u2013 ${shipping.method || shipping.name}`,
|
|
3460
|
+
quantity: 1,
|
|
3461
|
+
unit_price: shipping.price,
|
|
3462
|
+
total_amount: shipping.price,
|
|
3463
|
+
reference: `shipping:${shipping.id}`,
|
|
3464
|
+
type: "shipping_fee"
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
return {
|
|
3468
|
+
ctx: {
|
|
3469
|
+
...base,
|
|
3470
|
+
orderLines,
|
|
3471
|
+
orderAmount: base.orderAmount + (shipping?.price ?? 0)
|
|
3472
|
+
},
|
|
3473
|
+
shippingOptions: options
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Express instant purchase — go straight to Klarna's own modal, no checkout
|
|
3478
|
+
* overlay. Creates a session (with shipping options), loads the widget into a
|
|
3479
|
+
* hidden container, then authorizes (which opens Klarna's confirm modal). On
|
|
3480
|
+
* approval creates the order → `payment-complete` (host shows confirmation);
|
|
3481
|
+
* on failure → `payment-error`. Call `open(sponsorId)` first.
|
|
3482
|
+
*/
|
|
3483
|
+
async startKlarnaInstant() {
|
|
3484
|
+
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
3485
|
+
const sponsorId = this.state.sponsorId;
|
|
3486
|
+
const fetchedShippings = await this.fetchAvailableShippings(sponsorId);
|
|
3487
|
+
const shippings = fetchedShippings.length > 0 ? fetchedShippings : KLARNA_SHIPPING_OPTIONS;
|
|
3488
|
+
const { ctx } = this.buildKlarnaInstantContext(void 0, shippings);
|
|
3489
|
+
let clientToken;
|
|
3490
|
+
let categories;
|
|
3491
|
+
try {
|
|
3492
|
+
let checkoutId = ctx.checkoutId ?? this.state.checkoutId;
|
|
3493
|
+
try {
|
|
3494
|
+
const checkoutRes = await this.createCheckout(sponsorId);
|
|
3495
|
+
if (checkoutRes?.id) {
|
|
3496
|
+
checkoutId = checkoutRes.id;
|
|
3497
|
+
ctx.checkoutId = checkoutRes.id;
|
|
3498
|
+
}
|
|
3499
|
+
} catch (err) {
|
|
3500
|
+
if (typeof console !== "undefined") {
|
|
3501
|
+
console.warn("[CheckoutManager] CreateCheckout before Klarna instant failed:", err);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
if (!checkoutId) {
|
|
3505
|
+
throw new Error("[CheckoutManager] no backend checkout for the Klarna session");
|
|
3506
|
+
}
|
|
3507
|
+
const address = this.state.address;
|
|
3508
|
+
const activeCurrency = getGlobalCurrency();
|
|
3509
|
+
this.state = { ...this.state, currency: activeCurrency };
|
|
3510
|
+
ctx.currency = activeCurrency;
|
|
3511
|
+
const variables = {
|
|
3512
|
+
checkoutId,
|
|
3513
|
+
countryCode: getGlobalCountryCode(),
|
|
3514
|
+
currency: activeCurrency,
|
|
3515
|
+
locale: getGlobalLocale(),
|
|
3516
|
+
returnUrl: resolveHttpsReturnUrl(null, {
|
|
3517
|
+
vio_payment: "success",
|
|
3518
|
+
vio_method: "klarna",
|
|
3519
|
+
vio_sponsor: sponsorId,
|
|
3520
|
+
checkout_id: checkoutId
|
|
3521
|
+
}),
|
|
3522
|
+
intent: "buy",
|
|
3523
|
+
customer: buildKlarnaCustomer(address),
|
|
3524
|
+
billingAddress: buildKlarnaAddress(address),
|
|
3525
|
+
shippingAddress: buildKlarnaAddress(address),
|
|
3526
|
+
autoCapture: true
|
|
3527
|
+
};
|
|
3528
|
+
const json = await executeKlarnaGraphQL(
|
|
3529
|
+
CREATE_PAYMENT_KLARNA_NATIVE_MUTATION,
|
|
3530
|
+
variables,
|
|
3531
|
+
sponsorId
|
|
3532
|
+
);
|
|
3533
|
+
const nativeRes = json?.data?.Payment?.CreatePaymentKlarnaNative;
|
|
3534
|
+
if (!nativeRes || !nativeRes.client_token) {
|
|
3535
|
+
throw new Error("Klarna instant session failed: missing client_token");
|
|
3536
|
+
}
|
|
3537
|
+
if (nativeRes.checkout_id) {
|
|
3538
|
+
ctx.checkoutId = nativeRes.checkout_id;
|
|
3539
|
+
this.state = { ...this.state, checkoutId: nativeRes.checkout_id };
|
|
3540
|
+
}
|
|
3541
|
+
if (nativeRes.session_id) {
|
|
3542
|
+
ctx.sessionId = nativeRes.session_id;
|
|
3543
|
+
this.state = { ...this.state, sessionId: nativeRes.session_id };
|
|
3544
|
+
}
|
|
3545
|
+
ctx.clientToken = nativeRes.client_token;
|
|
3546
|
+
this.state = { ...this.state, clientToken: nativeRes.client_token };
|
|
3547
|
+
clientToken = nativeRes.client_token;
|
|
3548
|
+
categories = nativeRes.payment_method_categories ?? [];
|
|
3549
|
+
} catch (err) {
|
|
3550
|
+
this.dispatchEvent(
|
|
3551
|
+
new CustomEvent("payment-error", {
|
|
3552
|
+
detail: { method: "klarna", error: describeError(err) }
|
|
3553
|
+
})
|
|
3554
|
+
);
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
const container = document.createElement("div");
|
|
3558
|
+
container.setAttribute("aria-hidden", "true");
|
|
3559
|
+
container.style.cssText = "position:fixed;left:-9999px;top:0;width:1px;height:1px;overflow:hidden;";
|
|
3560
|
+
document.body.appendChild(container);
|
|
3561
|
+
try {
|
|
3562
|
+
const widget = await createKlarnaPaymentsWidget({
|
|
3563
|
+
clientToken,
|
|
3564
|
+
sessionId: ctx.sessionId,
|
|
3565
|
+
categories,
|
|
3566
|
+
container
|
|
3567
|
+
});
|
|
3568
|
+
await widget.load();
|
|
3569
|
+
const token = await widget.authorize();
|
|
3570
|
+
await this.completeKlarnaOrder(token, ctx, { authorizationToken: token });
|
|
3571
|
+
} catch (err) {
|
|
3572
|
+
if (typeof console !== "undefined") {
|
|
3573
|
+
console.warn("[VioCheckout] Klarna instant failed:", err);
|
|
3574
|
+
}
|
|
3575
|
+
this.dispatchEvent(
|
|
3576
|
+
new CustomEvent("payment-error", {
|
|
3577
|
+
detail: { method: "klarna", error: describeError(err) }
|
|
3578
|
+
})
|
|
3579
|
+
);
|
|
3580
|
+
} finally {
|
|
3581
|
+
try {
|
|
3582
|
+
container.remove();
|
|
3583
|
+
} catch {
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
/**
|
|
3588
|
+
* REDIRECT path: call once on app load. If a Klarna purchase was launched
|
|
3589
|
+
* (pending context persisted) and we're back with a token — from the URL or
|
|
3590
|
+
* the SDK's resumed `complete` event — finish creating the order + confirm.
|
|
3591
|
+
*/
|
|
3592
|
+
async resumeKlarnaReturn() {
|
|
3593
|
+
if (typeof sessionStorage === "undefined") return;
|
|
3594
|
+
const raw = sessionStorage.getItem(KLARNA_PENDING_KEY);
|
|
3595
|
+
if (!raw) return;
|
|
3596
|
+
let pending;
|
|
3597
|
+
try {
|
|
3598
|
+
pending = JSON.parse(raw);
|
|
3599
|
+
} catch {
|
|
3600
|
+
sessionStorage.removeItem(KLARNA_PENDING_KEY);
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3603
|
+
const fromUrl = readKlarnaTokenFromUrl();
|
|
3604
|
+
if (fromUrl) {
|
|
3605
|
+
cleanUrlParams([fromUrl.param]);
|
|
3606
|
+
await this.completeKlarnaOrder(fromUrl.token, pending, { authorizationToken: fromUrl.token });
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
if (typeof window !== "undefined" && typeof console !== "undefined" && window.location.search) {
|
|
3610
|
+
console.warn(
|
|
3611
|
+
"[VioCheckout] Klarna return: no known token param. URL was:",
|
|
3612
|
+
window.location.search
|
|
3613
|
+
);
|
|
3614
|
+
}
|
|
3615
|
+
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
3616
|
+
if (!cfg?.klarnaClientId) return;
|
|
3617
|
+
await listenForKlarnaCompletion({
|
|
3618
|
+
config: { clientId: cfg.klarnaClientId, locale: getGlobalLocale(), environment: cfg.klarnaEnvironment },
|
|
3619
|
+
onComplete: (result) => {
|
|
3620
|
+
void this.completeKlarnaOrder(result.authorizationToken, pending, result);
|
|
3621
|
+
},
|
|
3622
|
+
onError: (err) => {
|
|
3623
|
+
if (typeof console !== "undefined") {
|
|
3624
|
+
console.warn("[VioCheckout] Klarna resume error:", err);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
/** Snapshot of the active cart for redirect recovery (minor units). */
|
|
3630
|
+
buildKlarnaPendingContext() {
|
|
3631
|
+
if (!this.state) throw new Error("[VioCheckout] no active checkout");
|
|
3632
|
+
const currency = this.state.currency;
|
|
3633
|
+
return {
|
|
3634
|
+
sponsorId: this.state.sponsorId,
|
|
3635
|
+
currency,
|
|
3636
|
+
orderAmount: toSmallestUnit(this.state.subtotal, currency),
|
|
3637
|
+
orderLines: this.items.map((item) => {
|
|
3638
|
+
const unitPrice = toSmallestUnit(item.unitPrice, currency);
|
|
3639
|
+
return {
|
|
3640
|
+
name: item.name || item.brand || "Produkt",
|
|
3641
|
+
quantity: item.quantity,
|
|
3642
|
+
unit_price: unitPrice,
|
|
3643
|
+
total_amount: unitPrice * item.quantity,
|
|
3644
|
+
reference: String(item.productId)
|
|
3645
|
+
};
|
|
3646
|
+
})
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* POST the token + order lines to vio-backend (it holds the secret API key
|
|
3651
|
+
* and creates the real Klarna order). On success → `payment-complete`; on
|
|
3652
|
+
* failure → `payment-error`. Idempotent: clears the pending key so the
|
|
3653
|
+
* popup and redirect paths can't double-fire.
|
|
3654
|
+
*/
|
|
3655
|
+
async completeKlarnaOrder(authorizationToken, ctx, result) {
|
|
3656
|
+
if (this.klarnaOrderInFlight) return;
|
|
3657
|
+
this.klarnaOrderInFlight = true;
|
|
3658
|
+
if (typeof sessionStorage !== "undefined" && sessionStorage.getItem("vio.klarna.confirmed.v1") === authorizationToken) {
|
|
3659
|
+
this.klarnaOrderInFlight = false;
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
try {
|
|
3663
|
+
const checkoutId = ctx?.checkoutId ?? this.state?.checkoutId ?? String(ctx?.sponsorId ?? this.state?.sponsorId ?? "");
|
|
3664
|
+
const address = this.state?.address;
|
|
3665
|
+
const variables = {
|
|
3666
|
+
checkoutId,
|
|
3667
|
+
authorizationToken,
|
|
3668
|
+
autoCapture: true,
|
|
3669
|
+
billingAddress: buildKlarnaAddress(address),
|
|
3670
|
+
shippingAddress: buildKlarnaAddress(address),
|
|
3671
|
+
customer: buildKlarnaCustomer(address)
|
|
3672
|
+
};
|
|
3673
|
+
const json = await executeKlarnaGraphQL(
|
|
3674
|
+
CONFIRM_PAYMENT_KLARNA_NATIVE_MUTATION,
|
|
3675
|
+
variables,
|
|
3676
|
+
ctx?.sponsorId ?? this.state?.sponsorId
|
|
3677
|
+
);
|
|
3678
|
+
const confirmRes = json?.data?.Payment?.ConfirmPaymentKlarnaNative;
|
|
3679
|
+
if (!confirmRes) {
|
|
3680
|
+
throw new Error("Klarna order confirmation failed: missing response data");
|
|
3681
|
+
}
|
|
3682
|
+
const orderData = confirmRes.order || confirmRes;
|
|
3683
|
+
if (typeof sessionStorage !== "undefined") {
|
|
3684
|
+
sessionStorage.setItem("vio.klarna.confirmed.v1", authorizationToken);
|
|
3685
|
+
sessionStorage.removeItem(KLARNA_PENDING_KEY);
|
|
3686
|
+
}
|
|
3687
|
+
const state = this.state ?? { sponsorId: ctx.sponsorId, subtotal: ctx.orderAmount, currency: ctx.currency };
|
|
3688
|
+
this.dispatchEvent(
|
|
3689
|
+
new CustomEvent("payment-complete", {
|
|
3690
|
+
detail: {
|
|
3691
|
+
method: "klarna",
|
|
3692
|
+
state,
|
|
3693
|
+
// chargedTotal in MAJOR units (incl. shipping for express) for the
|
|
3694
|
+
// confirmation screen. ctx.orderAmount is minor (øre) → /100.
|
|
3695
|
+
// Prefer the amount Klarna actually charged (includes shipping in
|
|
3696
|
+
// every flow); the local ctx misses shipping outside express.
|
|
3697
|
+
result: {
|
|
3698
|
+
...result,
|
|
3699
|
+
order: orderData,
|
|
3700
|
+
chargedTotal: typeof orderData?.order_amount === "number" ? orderData.order_amount / 100 : ctx.orderAmount / 100
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
})
|
|
3704
|
+
);
|
|
3705
|
+
} catch (err) {
|
|
3706
|
+
if (typeof console !== "undefined") {
|
|
3707
|
+
console.error("[VioCheckout] Klarna order creation failed:", err);
|
|
3708
|
+
}
|
|
3709
|
+
this.dispatchEvent(
|
|
3710
|
+
new CustomEvent("payment-error", {
|
|
3711
|
+
detail: { method: "klarna", error: err instanceof Error ? err.message : String(err) }
|
|
3712
|
+
})
|
|
3713
|
+
);
|
|
3714
|
+
} finally {
|
|
3715
|
+
this.klarnaOrderInFlight = false;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
// MARK: - Accessors
|
|
3719
|
+
get currentState() {
|
|
3720
|
+
return this.state;
|
|
3721
|
+
}
|
|
3722
|
+
get items() {
|
|
3723
|
+
if (!this.state) return [];
|
|
3724
|
+
return this.cartManager.getCart(this.state.sponsorId)?.items ?? [];
|
|
3725
|
+
}
|
|
3726
|
+
// MARK: - Internals
|
|
3727
|
+
/**
|
|
3728
|
+
* Resolve the Stripe Connect account for the active sponsor — for now
|
|
3729
|
+
* pulled from bootstrap cache if present. Returns undefined if not set
|
|
3730
|
+
* (Stripe will route to the platform account in that case).
|
|
3731
|
+
*/
|
|
3732
|
+
findStripeConnectAccount() {
|
|
3733
|
+
return void 0;
|
|
3734
|
+
}
|
|
3735
|
+
emit() {
|
|
3736
|
+
this.dispatchEvent(
|
|
3737
|
+
new CustomEvent("change", { detail: { state: this.state } })
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
3741
|
+
function toSmallestUnit(amount, currency) {
|
|
3742
|
+
const zeroDecimal = /* @__PURE__ */ new Set(["JPY", "KRW", "VND"]);
|
|
3743
|
+
if (zeroDecimal.has(currency.toUpperCase())) return Math.round(amount);
|
|
3744
|
+
return Math.round(amount * 100);
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
// src/core/client.ts
|
|
3748
|
+
var VioFacade = class {
|
|
3749
|
+
constructor() {
|
|
3750
|
+
this.apiInstance = null;
|
|
3751
|
+
this.commerceClients = /* @__PURE__ */ new Map();
|
|
3752
|
+
this.bootstrapPromise = null;
|
|
3753
|
+
this.cachedBootstrap = null;
|
|
3754
|
+
/** Last bootstrap failure — short backoff so cart mutations don't hammer a
|
|
3755
|
+
* dead endpoint on every call. */
|
|
3756
|
+
this.bootstrapFailedAt = 0;
|
|
3757
|
+
// Client-side managers — created lazily on first access. They don't
|
|
3758
|
+
// need Vio.init() to work (state is purely local).
|
|
3759
|
+
this.cartManager = null;
|
|
3760
|
+
this.checkoutManager = null;
|
|
3761
|
+
}
|
|
3762
|
+
init(config) {
|
|
3763
|
+
Configuration.init(config);
|
|
3764
|
+
this.apiInstance = null;
|
|
3765
|
+
this.commerceClients.clear();
|
|
3766
|
+
this.bootstrapPromise = null;
|
|
3767
|
+
this.cachedBootstrap = null;
|
|
3768
|
+
this.bootstrapFailedAt = 0;
|
|
3769
|
+
this.checkoutManager?.clearPaymentMethodsCache();
|
|
3770
|
+
}
|
|
3771
|
+
get isInitialized() {
|
|
3772
|
+
return Configuration.isInitialized;
|
|
3773
|
+
}
|
|
3774
|
+
get config() {
|
|
3775
|
+
return Configuration.get();
|
|
3776
|
+
}
|
|
3777
|
+
// MARK: - Client-side managers (no init required)
|
|
3778
|
+
/** Multi-sponsor cart manager. Reactive — listen via addEventListener('change', ...). */
|
|
3779
|
+
get cart() {
|
|
3780
|
+
if (!this.cartManager) this.cartManager = new CartManager();
|
|
3781
|
+
return this.cartManager;
|
|
3782
|
+
}
|
|
3783
|
+
/** Checkout flow manager. Subscribes to cart state via the CartManager. */
|
|
3784
|
+
get checkout() {
|
|
3785
|
+
if (!this.checkoutManager) this.checkoutManager = new CheckoutManager(this.cart);
|
|
3786
|
+
return this.checkoutManager;
|
|
3787
|
+
}
|
|
3788
|
+
// MARK: - Backend-bound
|
|
3789
|
+
/** Lazy VioApi for REST endpoints on `api-dev.vio.live`. */
|
|
3790
|
+
get api() {
|
|
3791
|
+
if (!this.apiInstance) {
|
|
3792
|
+
const cfg = Configuration.get();
|
|
3793
|
+
this.apiInstance = createVioApi({ apiBase: cfg.apiBase, apiKey: cfg.apiKey });
|
|
3794
|
+
}
|
|
3795
|
+
return this.apiInstance;
|
|
3796
|
+
}
|
|
3797
|
+
/**
|
|
3798
|
+
* Bootstrap — fetches client config and caches it. Dedupes in-flight
|
|
3799
|
+
* concurrent requests.
|
|
3800
|
+
*/
|
|
3801
|
+
async bootstrap() {
|
|
3802
|
+
if (this.cachedBootstrap) return this.cachedBootstrap;
|
|
3803
|
+
if (this.bootstrapPromise) return this.bootstrapPromise;
|
|
3804
|
+
if (this.bootstrapFailedAt && Date.now() - this.bootstrapFailedAt < 5e3) {
|
|
3805
|
+
throw new Error("[Vio] bootstrap recently failed \u2014 backing off");
|
|
3806
|
+
}
|
|
3807
|
+
this.bootstrapPromise = this.api.bootstrap().then((res) => {
|
|
3808
|
+
this.cachedBootstrap = res;
|
|
3809
|
+
this.bootstrapFailedAt = 0;
|
|
3810
|
+
return res;
|
|
3811
|
+
}).catch((err) => {
|
|
3812
|
+
this.bootstrapFailedAt = Date.now();
|
|
3813
|
+
throw err;
|
|
3814
|
+
}).finally(() => {
|
|
3815
|
+
this.bootstrapPromise = null;
|
|
3816
|
+
});
|
|
3817
|
+
return this.bootstrapPromise;
|
|
3818
|
+
}
|
|
3819
|
+
get bootstrapCache() {
|
|
3820
|
+
return this.cachedBootstrap;
|
|
3821
|
+
}
|
|
3822
|
+
/**
|
|
3823
|
+
* Get a CommerceClient scoped to a specific sponsor (via bootstrap).
|
|
3824
|
+
* Throws if bootstrap hasn't been called or the sponsor is unknown.
|
|
3825
|
+
*/
|
|
3826
|
+
commerceFor(sponsorId) {
|
|
3827
|
+
sponsorId = Number(sponsorId);
|
|
3828
|
+
const existing = this.commerceClients.get(sponsorId);
|
|
3829
|
+
if (existing) return existing;
|
|
3830
|
+
const sponsor = this.findSponsor(sponsorId);
|
|
3831
|
+
if (!sponsor) {
|
|
3832
|
+
throw new Error(
|
|
3833
|
+
`[Vio] Sponsor ${sponsorId} not found in bootstrap. Call Vio.bootstrap() first or pass commerce-key directly.`
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
const cfg = Configuration.get();
|
|
3837
|
+
const client = createCommerceClient({ endpoint: cfg.graphQLBase, apiKey: sponsor.commerce.apiKey });
|
|
3838
|
+
this.commerceClients.set(sponsorId, client);
|
|
3839
|
+
return client;
|
|
3840
|
+
}
|
|
3841
|
+
/** Get a CommerceClient using an explicit commerce apiKey (skip bootstrap). */
|
|
3842
|
+
commerceWithKey(commerceApiKey) {
|
|
3843
|
+
const cfg = Configuration.get();
|
|
3844
|
+
return createCommerceClient({ endpoint: cfg.graphQLBase, apiKey: commerceApiKey });
|
|
3845
|
+
}
|
|
3846
|
+
findSponsor(sponsorId) {
|
|
3847
|
+
const boot = this.cachedBootstrap;
|
|
3848
|
+
if (!boot) return void 0;
|
|
3849
|
+
const id = Number(sponsorId);
|
|
3850
|
+
if (Number(boot.primarySponsor?.id) === id) return boot.primarySponsor;
|
|
3851
|
+
return boot.secondarySponsors?.find((s) => Number(s.id) === id);
|
|
3852
|
+
}
|
|
3853
|
+
};
|
|
3854
|
+
var _vioGlobal = globalThis;
|
|
3855
|
+
var Vio = _vioGlobal.__VIO_FACADE__ ?? (_vioGlobal.__VIO_FACADE__ = new VioFacade());
|
|
3856
|
+
|
|
3857
|
+
export { ADD_ITEM_MUTATION, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_PAYMENT_STRIPE_MUTATION, CREATE_PAYMENT_VIPPS_MUTATION, CartManager, CheckoutManager, CommerceClient, Configuration, DELETE_ITEM_MUTATION, GET_AVAILABLE_PAYMENT_METHODS_QUERY, GET_CART_QUERY, GET_CHECKOUT_QUERY, GET_LINE_ITEMS_BY_SUPPLIER_QUERY, GET_VIPPS_STATUS_QUERY, KLARNA_SHIPPING_OPTIONS, UPDATE_CHECKOUT_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, Vio, VioApi, VioApiError, __decorateClass, addItem, checkApplePayAvailability, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, executeCartGraphQL, formatPrice, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCustomerSessionId, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, primaryImageUrl, showApplePaySheet, updateCheckout, updateItem, updateShippingsBySupplier };
|
|
3858
|
+
//# sourceMappingURL=chunk-NP2KCM7X.js.map
|
|
3859
|
+
//# sourceMappingURL=chunk-NP2KCM7X.js.map
|