@vio-live/web-sdk 0.9.1 → 0.11.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.
@@ -1805,6 +1805,23 @@ var CartManager = class extends EventTarget {
1805
1805
  const targetItemId = item.cartItemId;
1806
1806
  if (quantity <= 0) {
1807
1807
  cart.items = cart.items.filter((i) => i !== item);
1808
+ if (typeof document !== "undefined") {
1809
+ document.dispatchEvent(
1810
+ new CustomEvent("vio:removed-from-cart", {
1811
+ bubbles: true,
1812
+ detail: {
1813
+ productId: item.productId,
1814
+ variantId: item.variantId,
1815
+ name: item.name,
1816
+ brand: item.brand,
1817
+ price: item.unitPrice,
1818
+ quantity: item.quantity,
1819
+ currency: item.currency,
1820
+ sponsorId
1821
+ }
1822
+ })
1823
+ );
1824
+ }
1808
1825
  } else {
1809
1826
  item.quantity = quantity;
1810
1827
  }
@@ -2554,10 +2571,33 @@ function renderKustomSnippet(container, htmlSnippet) {
2554
2571
  }
2555
2572
 
2556
2573
  // src/core/checkout/payments/qliro.ts
2574
+ function readVioTheme() {
2575
+ if (typeof globalThis === "undefined") return void 0;
2576
+ const doc = globalThis.document;
2577
+ const getComputed = globalThis.getComputedStyle;
2578
+ if (!doc?.documentElement || typeof getComputed !== "function") return void 0;
2579
+ let style;
2580
+ try {
2581
+ style = getComputed(doc.documentElement);
2582
+ } catch {
2583
+ return void 0;
2584
+ }
2585
+ const read = (name) => {
2586
+ const value = style?.getPropertyValue?.(name)?.trim();
2587
+ return value || void 0;
2588
+ };
2589
+ const theme = {
2590
+ accent: read("--vio-color-accent"),
2591
+ surface: read("--vio-color-surface"),
2592
+ radiusMd: read("--vio-radius-md"),
2593
+ radiusLg: read("--vio-radius-lg")
2594
+ };
2595
+ return Object.values(theme).some(Boolean) ? theme : void 0;
2596
+ }
2557
2597
  var CREATE_PAYMENT_QLIRO_MUTATION = `
2558
- mutation CreatePaymentQliro($checkoutId: String!, $countryCode: String!, $href: String!, $email: String) {
2598
+ mutation CreatePaymentQliro($checkoutId: String!, $countryCode: String!, $href: String!, $email: String, $theme: QliroThemeInput) {
2559
2599
  Payment {
2560
- CreatePaymentQliro(checkout_id: $checkoutId, country_code: $countryCode, href: $href, email: $email) {
2600
+ CreatePaymentQliro(checkout_id: $checkoutId, country_code: $countryCode, href: $href, email: $email, theme: $theme) {
2561
2601
  order_id
2562
2602
  status
2563
2603
  html_snippet
@@ -2591,6 +2631,139 @@ async function getQliroOrder(checkoutId, options) {
2591
2631
  return json?.data?.Payment?.GetQliroOrder ?? null;
2592
2632
  }
2593
2633
 
2634
+ // src/core/checkout/payments/qliro-sync.ts
2635
+ var SYNC_PAYMENT_QLIRO_MUTATION = `
2636
+ mutation SyncPaymentQliro($checkoutId: String!) {
2637
+ Payment {
2638
+ SyncPaymentQliro(checkout_id: $checkoutId) {
2639
+ order_id
2640
+ update_version
2641
+ total_price
2642
+ }
2643
+ }
2644
+ }
2645
+ `;
2646
+ async function syncPaymentQliro(checkoutId, options) {
2647
+ const json = await executeCartGraphQL(SYNC_PAYMENT_QLIRO_MUTATION, { checkoutId }, options);
2648
+ return json?.data?.Payment?.SyncPaymentQliro ?? null;
2649
+ }
2650
+ var DEFAULT_UNLOCK_TIMEOUT_MS = 15e3;
2651
+ function installQliroListeners(options) {
2652
+ const { checkoutId, queryOptions, onEvent } = options;
2653
+ const unlockTimeoutMs = options.unlockTimeoutMs ?? DEFAULT_UNLOCK_TIMEOUT_MS;
2654
+ const win = globalThis;
2655
+ let q1 = null;
2656
+ let destroyed = false;
2657
+ let pending = null;
2658
+ const emit = (event) => {
2659
+ try {
2660
+ onEvent?.(event);
2661
+ } catch {
2662
+ }
2663
+ };
2664
+ const settle = () => {
2665
+ if (!pending) return;
2666
+ clearTimeout(pending.timer);
2667
+ const { resolve } = pending;
2668
+ pending = null;
2669
+ try {
2670
+ q1?.unlock();
2671
+ } catch {
2672
+ }
2673
+ resolve();
2674
+ };
2675
+ const acknowledges = (echo, version) => {
2676
+ if (version === null) return false;
2677
+ return !echo || echo === version;
2678
+ };
2679
+ const settleIfAcknowledged = () => {
2680
+ if (!pending) return;
2681
+ if (pending.echoes.some((echo) => acknowledges(echo, pending.version))) {
2682
+ settle();
2683
+ }
2684
+ };
2685
+ const previousQ1Ready = win.q1Ready;
2686
+ win.q1Ready = (instance) => {
2687
+ try {
2688
+ previousQ1Ready?.(instance);
2689
+ } catch {
2690
+ }
2691
+ if (destroyed) return;
2692
+ q1 = instance;
2693
+ instance.onCheckoutLoaded(() => emit({ type: "loaded" }));
2694
+ instance.onOrderUpdated((order) => {
2695
+ if (!pending) return;
2696
+ const echoed = order?.merchantUpdateVersion ?? order?.MerchantUpdateVersion ?? null;
2697
+ pending.echoes.push(echoed);
2698
+ settleIfAcknowledged();
2699
+ });
2700
+ instance.onShippingMethodChanged(
2701
+ (shipping) => emit({ type: "shipping-changed", shipping })
2702
+ );
2703
+ instance.onShippingPriceChanged(
2704
+ (price, totalPrice) => emit({ type: "shipping-price-changed", price, totalPrice })
2705
+ );
2706
+ instance.onCustomerInfoChanged(
2707
+ (customer) => emit({ type: "customer-changed", customer })
2708
+ );
2709
+ instance.onPaymentMethodChanged(
2710
+ (paymentMethod) => emit({ type: "payment-method-changed", paymentMethod })
2711
+ );
2712
+ instance.onPaymentDeclined(
2713
+ (reason, message) => emit({ type: "payment-declined", reason, message })
2714
+ );
2715
+ instance.onSessionExpired(() => {
2716
+ emit({ type: "session-expired" });
2717
+ void controller.sync();
2718
+ });
2719
+ };
2720
+ const controller = {
2721
+ async sync() {
2722
+ if (destroyed) return null;
2723
+ if (pending) settle();
2724
+ try {
2725
+ q1?.lock();
2726
+ } catch {
2727
+ }
2728
+ const settled = new Promise((resolve) => {
2729
+ pending = {
2730
+ version: null,
2731
+ echoes: [],
2732
+ resolve,
2733
+ timer: setTimeout(settle, unlockTimeoutMs)
2734
+ };
2735
+ });
2736
+ let result = null;
2737
+ try {
2738
+ result = await syncPaymentQliro(checkoutId, queryOptions);
2739
+ } catch (error) {
2740
+ emit({ type: "sync-failed", error });
2741
+ settle();
2742
+ await settled;
2743
+ return null;
2744
+ }
2745
+ if (destroyed || !result?.update_version || !q1) {
2746
+ settle();
2747
+ await settled;
2748
+ return result;
2749
+ }
2750
+ if (pending) {
2751
+ pending.version = result.update_version;
2752
+ settleIfAcknowledged();
2753
+ }
2754
+ await settled;
2755
+ return result;
2756
+ },
2757
+ destroy() {
2758
+ destroyed = true;
2759
+ settle();
2760
+ if (win.q1Ready) win.q1Ready = previousQ1Ready;
2761
+ q1 = null;
2762
+ }
2763
+ };
2764
+ return controller;
2765
+ }
2766
+
2594
2767
  // src/core/checkout/payments/walley.ts
2595
2768
  var CREATE_PAYMENT_WALLEY_MUTATION = `
2596
2769
  mutation CreatePaymentWalley($checkoutId: String!, $countryCode: String!, $href: String!, $email: String) {
@@ -2866,6 +3039,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
2866
3039
  this.applePayPublishableKey = null;
2867
3040
  this.applePayConnectedAccount = null;
2868
3041
  this.klarnaAvailableCache = null;
3042
+ /** Live q1 listeners while a Qliro widget is mounted. */
3043
+ this.qliroController = null;
2869
3044
  this.klarnaOrderInFlight = false;
2870
3045
  /** Last backend shippings fetched (per-supplier), for UI reuse. */
2871
3046
  this.lastFetchedShippings = [];
@@ -3275,16 +3450,56 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3275
3450
  checkoutId,
3276
3451
  countryCode: getGlobalCountryCode(),
3277
3452
  href,
3278
- email: this.state?.address?.email || void 0
3453
+ email: this.state?.address?.email || void 0,
3454
+ // Qliro cannot be restyled once rendered, so the host page's theme has
3455
+ // to travel with the order.
3456
+ theme: readVioTheme()
3279
3457
  },
3280
3458
  opts
3281
3459
  );
3282
3460
  if (!order?.html_snippet) {
3283
3461
  throw new Error("Qliro order creation failed: missing html_snippet");
3284
3462
  }
3463
+ this.qliroController?.destroy();
3464
+ this.qliroController = installQliroListeners({
3465
+ checkoutId,
3466
+ queryOptions: opts,
3467
+ onEvent: (event) => this.onQliroEvent(event)
3468
+ });
3285
3469
  renderKustomSnippet(container, order.html_snippet);
3286
3470
  return order;
3287
3471
  }
3472
+ /**
3473
+ * Everything the customer does inside the Qliro iframe, re-published as a
3474
+ * DOM event so a host page can follow a checkout it cannot see into.
3475
+ */
3476
+ onQliroEvent(event) {
3477
+ this.dispatchEvent(
3478
+ new CustomEvent("qliro-event", {
3479
+ detail: event,
3480
+ bubbles: true,
3481
+ composed: true
3482
+ })
3483
+ );
3484
+ if (event.type === "shipping-changed" || event.type === "shipping-price-changed") {
3485
+ this.emit();
3486
+ }
3487
+ }
3488
+ /**
3489
+ * Push the current cart onto the open Qliro order.
3490
+ *
3491
+ * Call it after changing the cart while the widget is open: it holds
3492
+ * `q1.lock()` across the round trip and unlocks once Qliro confirms it is
3493
+ * showing the new version. A no-op when no Qliro widget is mounted.
3494
+ */
3495
+ async syncQliroOrder() {
3496
+ return await this.qliroController?.sync() ?? null;
3497
+ }
3498
+ /** Drop the Qliro listeners and release any lock still held. */
3499
+ destroyQliroListeners() {
3500
+ this.qliroController?.destroy();
3501
+ this.qliroController = null;
3502
+ }
3288
3503
  /** Re-read the Qliro order owned by a checkout — the confirmation trip. */
3289
3504
  async getQliroOrder(checkoutId, sponsorId) {
3290
3505
  const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
@@ -4154,7 +4369,7 @@ function toSmallestUnit(amount, currency) {
4154
4369
  }
4155
4370
 
4156
4371
  // src/core/version.ts
4157
- var SDK_VERSION = "0.9.1";
4372
+ var SDK_VERSION = "0.11.0";
4158
4373
 
4159
4374
  // src/core/analytics/analytics-manager.ts
4160
4375
  var SESSION_TTL_MS = 30 * 60 * 1e3;
@@ -4176,6 +4391,9 @@ var AnalyticsManager = class extends EventTarget {
4176
4391
  this.retryAttempt = 0;
4177
4392
  this.retryNotBefore = 0;
4178
4393
  this.sessionId = null;
4394
+ this.requireConsent = false;
4395
+ this.consentGranted = false;
4396
+ this.anonIdCache = null;
4179
4397
  /** Set via start({ sessionId }) — the host owns the session, we follow. */
4180
4398
  this.externalSessionId = null;
4181
4399
  this.sessionStartedAt = 0;
@@ -4191,7 +4409,29 @@ var AnalyticsManager = class extends EventTarget {
4191
4409
  * Install listeners + begin flushing. Explicit on purpose (no module
4192
4410
  * side-effects). Safe to call twice (second call only updates options).
4193
4411
  */
4412
+ /**
4413
+ * Consentimiento (ePrivacy/GDPR). Por defecto el SDK asume que el host ya
4414
+ * lo gestionó — su CMP decide si nos carga. Con `requireConsent: true`
4415
+ * arranca en modo **cookieless**: mide la visita con ids efímeros en
4416
+ * memoria y NO escribe nada en localStorage hasta que el host llame
4417
+ * `setConsent(true)`. Retirarlo borra lo persistido.
4418
+ */
4419
+ setConsent(granted) {
4420
+ this.consentGranted = granted;
4421
+ if (granted) {
4422
+ if (this.anonIdCache) safeStorageSet(ANON_KEY, this.anonIdCache);
4423
+ if (this.sessionId) this.persistSession();
4424
+ return;
4425
+ }
4426
+ safeStorageRemove(ANON_KEY);
4427
+ safeStorageRemove(SESSION_KEY);
4428
+ }
4429
+ /** ¿Podemos escribir en el dispositivo? */
4430
+ get canPersist() {
4431
+ return !this.requireConsent || this.consentGranted;
4432
+ }
4194
4433
  start(options = {}) {
4434
+ this.requireConsent = options.requireConsent === true;
4195
4435
  this.options = {
4196
4436
  host: options.host ?? this.options.host,
4197
4437
  collector: options.collector ?? this.options.collector,
@@ -4341,6 +4581,7 @@ var AnalyticsManager = class extends EventTarget {
4341
4581
  content_id: ctx.contentId,
4342
4582
  // Deletion-proof reporting: sources have no trash bin — the title
4343
4583
  // snapshotted at event time keeps reports legible if content dies.
4584
+ error_code: ctx.errorCode,
4344
4585
  content_title: ctx.contentTitle ?? (typeof document !== "undefined" && document.title ? document.title.slice(0, 255) : void 0),
4345
4586
  variant: ctx.variant
4346
4587
  });
@@ -4376,18 +4617,35 @@ var AnalyticsManager = class extends EventTarget {
4376
4617
  ...opts.props ? { props: opts.props } : {}
4377
4618
  };
4378
4619
  }
4620
+ /** Guarda la sesión solo si podemos escribir en el dispositivo. */
4621
+ persistSession(now = Date.now()) {
4622
+ if (!this.canPersist || !this.sessionId) return;
4623
+ safeStorageSet(
4624
+ SESSION_KEY,
4625
+ JSON.stringify({
4626
+ id: this.sessionId,
4627
+ ts: now,
4628
+ startedAt: this.sessionStartedAt ?? now
4629
+ })
4630
+ );
4631
+ }
4379
4632
  anonId() {
4380
- const stored = safeStorageGet(ANON_KEY);
4381
- if (stored) return stored;
4633
+ if (this.anonIdCache) return this.anonIdCache;
4634
+ const stored = this.canPersist ? safeStorageGet(ANON_KEY) : null;
4635
+ if (stored) {
4636
+ this.anonIdCache = stored;
4637
+ return stored;
4638
+ }
4382
4639
  const id = `a-${uuid()}`;
4383
- safeStorageSet(ANON_KEY, id);
4384
- return safeStorageGet(ANON_KEY) ?? id;
4640
+ if (this.canPersist) safeStorageSet(ANON_KEY, id);
4641
+ this.anonIdCache = (this.canPersist ? safeStorageGet(ANON_KEY) : null) ?? id;
4642
+ return this.anonIdCache;
4385
4643
  }
4386
4644
  /** Rolling session: renewed by activity, rotated after 30 min idle. */
4387
4645
  ensureSession() {
4388
4646
  const now = Date.now();
4389
4647
  if (!this.sessionId) {
4390
- const stored = safeStorageGet(SESSION_KEY);
4648
+ const stored = this.canPersist ? safeStorageGet(SESSION_KEY) : null;
4391
4649
  if (stored) {
4392
4650
  try {
4393
4651
  const parsed = JSON.parse(stored);
@@ -4399,7 +4657,7 @@ var AnalyticsManager = class extends EventTarget {
4399
4657
  }
4400
4658
  }
4401
4659
  } else {
4402
- const stored = safeStorageGet(SESSION_KEY);
4660
+ const stored = this.canPersist ? safeStorageGet(SESSION_KEY) : null;
4403
4661
  if (stored) {
4404
4662
  try {
4405
4663
  if (now - JSON.parse(stored).ts >= SESSION_TTL_MS) {
@@ -4421,16 +4679,10 @@ var AnalyticsManager = class extends EventTarget {
4421
4679
  this.sessionId = `s-${uuid()}`;
4422
4680
  this.sessionStartedAt = now;
4423
4681
  this.impressedComponents.clear();
4424
- safeStorageSet(
4425
- SESSION_KEY,
4426
- JSON.stringify({ id: this.sessionId, ts: now, startedAt: now })
4427
- );
4682
+ this.persistSession(now);
4428
4683
  this.track("session_start");
4429
4684
  } else {
4430
- safeStorageSet(
4431
- SESSION_KEY,
4432
- JSON.stringify({ id: this.sessionId, ts: now, startedAt: this.sessionStartedAt })
4433
- );
4685
+ this.persistSession(now);
4434
4686
  }
4435
4687
  }
4436
4688
  trackSessionEnd() {
@@ -4495,6 +4747,34 @@ var AnalyticsManager = class extends EventTarget {
4495
4747
  }
4496
4748
  });
4497
4749
  });
4750
+ on("vio:removed-from-cart", (ev) => {
4751
+ const d = ev.detail ?? {};
4752
+ if (d.productId === void 0) return;
4753
+ this.track("remove_from_cart", {
4754
+ context: { sponsorId: d.sponsorId },
4755
+ commerce: {
4756
+ items: [
4757
+ {
4758
+ productId: d.productId,
4759
+ variantId: d.variantId,
4760
+ name: d.name,
4761
+ brand: d.brand,
4762
+ price: d.price,
4763
+ quantity: d.quantity
4764
+ }
4765
+ ],
4766
+ value: (d.price ?? 0) * (d.quantity ?? 1),
4767
+ currency: d.currency
4768
+ }
4769
+ });
4770
+ });
4771
+ on("vio:cart-error", (ev) => {
4772
+ const d = ev.detail ?? {};
4773
+ this.track("sdk_error", {
4774
+ context: { errorCode: "cart_error" },
4775
+ props: d.message ? { message: String(d.message).slice(0, 500) } : void 0
4776
+ });
4777
+ });
4498
4778
  on("vio:product-click", (ev) => {
4499
4779
  const d = ev.detail ?? {};
4500
4780
  if (d.productId === void 0) return;
@@ -4595,6 +4875,12 @@ function safeStorageGet(key) {
4595
4875
  return null;
4596
4876
  }
4597
4877
  }
4878
+ function safeStorageRemove(key) {
4879
+ try {
4880
+ window.localStorage.removeItem(key);
4881
+ } catch {
4882
+ }
4883
+ }
4598
4884
  function safeStorageSet(key, value) {
4599
4885
  try {
4600
4886
  window.localStorage.setItem(key, value);
@@ -4780,5 +5066,5 @@ var _vioGlobal = globalThis;
4780
5066
  var Vio = _vioGlobal.__VIO_FACADE__ ?? (_vioGlobal.__VIO_FACADE__ = new VioFacade());
4781
5067
 
4782
5068
  export { ADD_ITEM_MUTATION, AnalyticsManager, 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, SDK_VERSION, 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 };
4783
- //# sourceMappingURL=chunk-WKJRYYNI.js.map
4784
- //# sourceMappingURL=chunk-WKJRYYNI.js.map
5069
+ //# sourceMappingURL=chunk-U2QG4IVJ.js.map
5070
+ //# sourceMappingURL=chunk-U2QG4IVJ.js.map