@ekomerc/storefront 0.1.0 → 0.1.2

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { err, ok } from "neverthrow";
2
- import { ValidationError, AuthError, NotFoundError, StateError, GraphQLError, NetworkError } from "./errors.js";
2
+ import { ValidationError, AuthError, NotFoundError, StateError, NetworkError, GraphQLError } from "./errors.js";
3
3
  import { StorefrontError } from "./errors.js";
4
4
  function createQueryCache() {
5
5
  const cache = /* @__PURE__ */ new Map();
@@ -106,7 +106,7 @@ const ADDRESS_FRAGMENT = `
106
106
  addressLine1
107
107
  addressLine2
108
108
  city
109
- postalCode
109
+ zip
110
110
  phone
111
111
  isDefault
112
112
  createdAt
@@ -184,7 +184,7 @@ function mapAddressData(data) {
184
184
  addressLine1: data.addressLine1,
185
185
  addressLine2: data.addressLine2,
186
186
  city: data.city,
187
- postalCode: data.postalCode,
187
+ zip: data.zip,
188
188
  phone: data.phone,
189
189
  isDefault: data.isDefault,
190
190
  createdAt: data.createdAt
@@ -458,117 +458,6 @@ function createAuthOperations(client, storage) {
458
458
  }
459
459
  };
460
460
  }
461
- const TRACKING_SCHEMA_VERSION$1 = 1;
462
- const TRACKING_ATTRIBUTION_STORAGE_KEY = "srb_tracking_attribution";
463
- function hasBrowserContext$1() {
464
- return typeof window !== "undefined";
465
- }
466
- function normalizeParam(value) {
467
- if (value === null) return null;
468
- const trimmed = value.trim();
469
- return trimmed.length > 0 ? trimmed : null;
470
- }
471
- function hasAttributionValue(snapshot) {
472
- return [
473
- snapshot.utm.source,
474
- snapshot.utm.medium,
475
- snapshot.utm.campaign,
476
- snapshot.utm.term,
477
- snapshot.utm.content,
478
- snapshot.clickIds.gclid,
479
- snapshot.clickIds.gbraid,
480
- snapshot.clickIds.wbraid,
481
- snapshot.clickIds.ttclid,
482
- snapshot.clickIds.fbclid
483
- ].some((value) => value !== null);
484
- }
485
- function parseStoredSnapshot(raw) {
486
- if (!raw) return null;
487
- try {
488
- const parsed = JSON.parse(raw);
489
- if (parsed.schemaVersion !== TRACKING_SCHEMA_VERSION$1 || typeof parsed.capturedAt !== "string") {
490
- return null;
491
- }
492
- const utm = parsed.utm;
493
- const clickIds = parsed.clickIds;
494
- if (!utm || !clickIds) return null;
495
- return {
496
- schemaVersion: TRACKING_SCHEMA_VERSION$1,
497
- capturedAt: parsed.capturedAt,
498
- utm: {
499
- schemaVersion: TRACKING_SCHEMA_VERSION$1,
500
- source: typeof utm.source === "string" ? utm.source : null,
501
- medium: typeof utm.medium === "string" ? utm.medium : null,
502
- campaign: typeof utm.campaign === "string" ? utm.campaign : null,
503
- term: typeof utm.term === "string" ? utm.term : null,
504
- content: typeof utm.content === "string" ? utm.content : null
505
- },
506
- clickIds: {
507
- schemaVersion: TRACKING_SCHEMA_VERSION$1,
508
- capturedAt: typeof clickIds.capturedAt === "string" ? clickIds.capturedAt : null,
509
- gclid: typeof clickIds.gclid === "string" ? clickIds.gclid : null,
510
- gbraid: typeof clickIds.gbraid === "string" ? clickIds.gbraid : null,
511
- wbraid: typeof clickIds.wbraid === "string" ? clickIds.wbraid : null,
512
- ttclid: typeof clickIds.ttclid === "string" ? clickIds.ttclid : null,
513
- fbclid: typeof clickIds.fbclid === "string" ? clickIds.fbclid : null
514
- }
515
- };
516
- } catch {
517
- return null;
518
- }
519
- }
520
- function readStoredSnapshot() {
521
- if (!hasBrowserContext$1()) return null;
522
- try {
523
- return parseStoredSnapshot(window.localStorage.getItem(TRACKING_ATTRIBUTION_STORAGE_KEY));
524
- } catch {
525
- return null;
526
- }
527
- }
528
- function writeStoredSnapshot(snapshot) {
529
- if (!hasBrowserContext$1()) return;
530
- try {
531
- window.localStorage.setItem(TRACKING_ATTRIBUTION_STORAGE_KEY, JSON.stringify(snapshot));
532
- } catch {
533
- }
534
- }
535
- function buildSnapshotFromSearch(search) {
536
- const params = new URLSearchParams(search);
537
- const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
538
- return {
539
- schemaVersion: TRACKING_SCHEMA_VERSION$1,
540
- capturedAt,
541
- utm: {
542
- schemaVersion: TRACKING_SCHEMA_VERSION$1,
543
- source: normalizeParam(params.get("utm_source")),
544
- medium: normalizeParam(params.get("utm_medium")),
545
- campaign: normalizeParam(params.get("utm_campaign")),
546
- term: normalizeParam(params.get("utm_term")),
547
- content: normalizeParam(params.get("utm_content"))
548
- },
549
- clickIds: {
550
- schemaVersion: TRACKING_SCHEMA_VERSION$1,
551
- capturedAt,
552
- gclid: normalizeParam(params.get("gclid")),
553
- gbraid: normalizeParam(params.get("gbraid")),
554
- wbraid: normalizeParam(params.get("wbraid")),
555
- ttclid: normalizeParam(params.get("ttclid")),
556
- fbclid: normalizeParam(params.get("fbclid"))
557
- }
558
- };
559
- }
560
- function captureLandingTrackingAttribution() {
561
- if (!hasBrowserContext$1()) return null;
562
- const snapshot = buildSnapshotFromSearch(window.location.search);
563
- if (hasAttributionValue(snapshot)) {
564
- writeStoredSnapshot(snapshot);
565
- return snapshot;
566
- }
567
- return readStoredSnapshot();
568
- }
569
- function getTrackingAttributionSnapshot() {
570
- return readStoredSnapshot();
571
- }
572
461
  function isAbsoluteUrl(value) {
573
462
  return /^https?:\/\//i.test(value);
574
463
  }
@@ -618,7 +507,7 @@ function normalizeCollectionAssetUrls(collection, endpoint) {
618
507
  imageAsset: collection.imageAsset ? { ...collection.imageAsset, url: resolveAssetUrl(collection.imageAsset.url, endpoint) } : null
619
508
  };
620
509
  }
621
- const CART_FRAGMENT$1 = `
510
+ const CART_FRAGMENT = `
622
511
  id
623
512
  token
624
513
  status
@@ -684,114 +573,7 @@ const CART_FRAGMENT$1 = `
684
573
  createdAt
685
574
  updatedAt
686
575
  `;
687
- const CART_QUERY = `
688
- query Cart {
689
- cart {
690
- ${CART_FRAGMENT$1}
691
- }
692
- }
693
- `;
694
- const CART_CREATE_MUTATION = `
695
- mutation CartCreate {
696
- cartCreate {
697
- cart {
698
- ${CART_FRAGMENT$1}
699
- }
700
- token
701
- userErrors {
702
- field
703
- message
704
- code
705
- }
706
- }
707
- }
708
- `;
709
- const CART_ITEM_ADD_MUTATION = `
710
- mutation CartItemAdd($input: CartItemAddInput!) {
711
- cartItemAdd(input: $input) {
712
- cart {
713
- ${CART_FRAGMENT$1}
714
- }
715
- userErrors {
716
- field
717
- message
718
- code
719
- }
720
- }
721
- }
722
- `;
723
- const CART_ITEM_UPDATE_MUTATION = `
724
- mutation CartItemUpdate($input: CartItemUpdateInput!) {
725
- cartItemUpdate(input: $input) {
726
- cart {
727
- ${CART_FRAGMENT$1}
728
- }
729
- userErrors {
730
- field
731
- message
732
- code
733
- }
734
- }
735
- }
736
- `;
737
- const CART_ITEM_REMOVE_MUTATION = `
738
- mutation CartItemRemove($input: CartItemRemoveInput!) {
739
- cartItemRemove(input: $input) {
740
- cart {
741
- ${CART_FRAGMENT$1}
742
- }
743
- userErrors {
744
- field
745
- message
746
- code
747
- }
748
- }
749
- }
750
- `;
751
- const CART_CLEAR_MUTATION = `
752
- mutation CartClear {
753
- cartClear {
754
- cart {
755
- ${CART_FRAGMENT$1}
756
- }
757
- userErrors {
758
- field
759
- message
760
- code
761
- }
762
- }
763
- }
764
- `;
765
- const CART_PROMO_CODE_APPLY_MUTATION = `
766
- mutation CartPromoCodeApply($input: CartPromoCodeApplyInput!) {
767
- cartPromoCodeApply(input: $input) {
768
- cart {
769
- ${CART_FRAGMENT$1}
770
- }
771
- userErrors {
772
- field
773
- message
774
- code
775
- }
776
- }
777
- }
778
- `;
779
- const CART_PROMO_CODE_REMOVE_MUTATION = `
780
- mutation CartPromoCodeRemove {
781
- cartPromoCodeRemove {
782
- cart {
783
- ${CART_FRAGMENT$1}
784
- }
785
- userErrors {
786
- field
787
- message
788
- code
789
- }
790
- }
791
- }
792
- `;
793
- const INVALID_CART_STATES = ["checkout", "converted", "abandoned", "expired"];
794
- function mapCartData$1(data, endpoint) {
576
+ function mapCartData(data, endpoint) {
795
577
  return {
796
578
  id: data.id,
797
579
  token: data.token,
@@ -830,45 +612,319 @@ function mapCartData$1(data, endpoint) {
830
612
  updatedAt: data.updatedAt
831
613
  };
832
614
  }
833
- function checkCartState(status) {
834
- if (INVALID_CART_STATES.includes(status)) {
835
- return err(
836
- new StateError(
837
- `Cannot modify cart in '${status}' state. Cart operations are only allowed when cart is in 'active' state.`,
838
- status
839
- )
840
- );
615
+ const TRACKING_SCHEMA_VERSION$1 = 1;
616
+ const TRACKING_ATTRIBUTION_STORAGE_KEY = "ekomerc_tracking_attribution";
617
+ const DEFAULT_TRACKING_ATTRIBUTION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
618
+ const TRACKING_ATTRIBUTION_QUERY_PARAM_KEYS = [
619
+ "utm_source",
620
+ "utm_medium",
621
+ "utm_campaign",
622
+ "utm_term",
623
+ "utm_content",
624
+ "gclid",
625
+ "gbraid",
626
+ "wbraid",
627
+ "ttclid",
628
+ "fbclid"
629
+ ];
630
+ function hasBrowserContext$1() {
631
+ return typeof window !== "undefined";
632
+ }
633
+ function hasTrackingAttributionParams(search) {
634
+ const params = new URLSearchParams(search);
635
+ return TRACKING_ATTRIBUTION_QUERY_PARAM_KEYS.some((key) => {
636
+ const value = params.get(key);
637
+ return value !== null && value.trim().length > 0;
638
+ });
639
+ }
640
+ function normalizeParam(value) {
641
+ if (value === null) return null;
642
+ const trimmed = value.trim();
643
+ return trimmed.length > 0 ? trimmed : null;
644
+ }
645
+ function hasAttributionValue(snapshot) {
646
+ return [
647
+ snapshot.utm.source,
648
+ snapshot.utm.medium,
649
+ snapshot.utm.campaign,
650
+ snapshot.utm.term,
651
+ snapshot.utm.content,
652
+ snapshot.clickIds.gclid,
653
+ snapshot.clickIds.gbraid,
654
+ snapshot.clickIds.wbraid,
655
+ snapshot.clickIds.ttclid,
656
+ snapshot.clickIds.fbclid
657
+ ].some((value) => value !== null);
658
+ }
659
+ function resolveTrackingAttributionTtlMs(options) {
660
+ const ttlMs = options?.ttlMs;
661
+ if (typeof ttlMs !== "number" || !Number.isFinite(ttlMs) || ttlMs < 0) {
662
+ return DEFAULT_TRACKING_ATTRIBUTION_TTL_MS;
841
663
  }
842
- return ok(void 0);
664
+ return ttlMs;
843
665
  }
844
- function handleUserErrors$1(userErrors) {
845
- if (userErrors.length === 0) return null;
846
- const messages = userErrors.map((e) => e.message).join("; ");
847
- const stateError = userErrors.find((e) => e.code?.includes("STATE") || e.message.includes("state"));
848
- if (stateError) {
849
- return new StateError(messages, "unknown");
666
+ function isSnapshotExpired(snapshot, ttlMs) {
667
+ const capturedAtMs = Date.parse(snapshot.capturedAt);
668
+ if (Number.isNaN(capturedAtMs)) {
669
+ return true;
850
670
  }
851
- return new ValidationError(
852
- messages,
853
- userErrors.map((e) => ({ field: e.field, message: e.message }))
854
- );
671
+ return Date.now() - capturedAtMs > ttlMs;
855
672
  }
856
- function createCartOperations(client, storage) {
857
- return {
858
- async get() {
859
- const token = storage.get(CART_TOKEN_KEY);
860
- if (!token) {
861
- return ok(null);
862
- }
863
- const result = await client.query({ query: CART_QUERY }, { cache: false });
864
- if (result.isErr()) {
865
- return err(result.error);
866
- }
867
- if (!result.value.cart) {
673
+ function parseStoredSnapshot(raw) {
674
+ if (!raw) return null;
675
+ try {
676
+ const parsed = JSON.parse(raw);
677
+ if (parsed.schemaVersion !== TRACKING_SCHEMA_VERSION$1 || typeof parsed.capturedAt !== "string") {
678
+ return null;
679
+ }
680
+ const utm = parsed.utm;
681
+ const clickIds = parsed.clickIds;
682
+ if (!utm || !clickIds) return null;
683
+ return {
684
+ schemaVersion: TRACKING_SCHEMA_VERSION$1,
685
+ capturedAt: parsed.capturedAt,
686
+ utm: {
687
+ schemaVersion: TRACKING_SCHEMA_VERSION$1,
688
+ source: typeof utm.source === "string" ? utm.source : null,
689
+ medium: typeof utm.medium === "string" ? utm.medium : null,
690
+ campaign: typeof utm.campaign === "string" ? utm.campaign : null,
691
+ term: typeof utm.term === "string" ? utm.term : null,
692
+ content: typeof utm.content === "string" ? utm.content : null
693
+ },
694
+ clickIds: {
695
+ schemaVersion: TRACKING_SCHEMA_VERSION$1,
696
+ capturedAt: typeof clickIds.capturedAt === "string" ? clickIds.capturedAt : null,
697
+ gclid: typeof clickIds.gclid === "string" ? clickIds.gclid : null,
698
+ gbraid: typeof clickIds.gbraid === "string" ? clickIds.gbraid : null,
699
+ wbraid: typeof clickIds.wbraid === "string" ? clickIds.wbraid : null,
700
+ ttclid: typeof clickIds.ttclid === "string" ? clickIds.ttclid : null,
701
+ fbclid: typeof clickIds.fbclid === "string" ? clickIds.fbclid : null
702
+ }
703
+ };
704
+ } catch {
705
+ return null;
706
+ }
707
+ }
708
+ function removeStoredSnapshot() {
709
+ if (!hasBrowserContext$1()) return;
710
+ try {
711
+ window.localStorage.removeItem(TRACKING_ATTRIBUTION_STORAGE_KEY);
712
+ } catch {
713
+ }
714
+ }
715
+ function readStoredSnapshot(options) {
716
+ if (!hasBrowserContext$1()) return null;
717
+ try {
718
+ const snapshot = parseStoredSnapshot(window.localStorage.getItem(TRACKING_ATTRIBUTION_STORAGE_KEY));
719
+ if (!snapshot) {
720
+ return null;
721
+ }
722
+ if (isSnapshotExpired(snapshot, resolveTrackingAttributionTtlMs(options))) {
723
+ removeStoredSnapshot();
724
+ return null;
725
+ }
726
+ return snapshot;
727
+ } catch {
728
+ return null;
729
+ }
730
+ }
731
+ function writeStoredSnapshot(snapshot) {
732
+ if (!hasBrowserContext$1()) return;
733
+ try {
734
+ window.localStorage.setItem(TRACKING_ATTRIBUTION_STORAGE_KEY, JSON.stringify(snapshot));
735
+ } catch {
736
+ }
737
+ }
738
+ function buildSnapshotFromSearch(search) {
739
+ const params = new URLSearchParams(search);
740
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
741
+ return {
742
+ schemaVersion: TRACKING_SCHEMA_VERSION$1,
743
+ capturedAt,
744
+ utm: {
745
+ schemaVersion: TRACKING_SCHEMA_VERSION$1,
746
+ source: normalizeParam(params.get("utm_source")),
747
+ medium: normalizeParam(params.get("utm_medium")),
748
+ campaign: normalizeParam(params.get("utm_campaign")),
749
+ term: normalizeParam(params.get("utm_term")),
750
+ content: normalizeParam(params.get("utm_content"))
751
+ },
752
+ clickIds: {
753
+ schemaVersion: TRACKING_SCHEMA_VERSION$1,
754
+ capturedAt,
755
+ gclid: normalizeParam(params.get("gclid")),
756
+ gbraid: normalizeParam(params.get("gbraid")),
757
+ wbraid: normalizeParam(params.get("wbraid")),
758
+ ttclid: normalizeParam(params.get("ttclid")),
759
+ fbclid: normalizeParam(params.get("fbclid"))
760
+ }
761
+ };
762
+ }
763
+ function captureLandingTrackingAttribution(options) {
764
+ if (!hasBrowserContext$1()) return null;
765
+ const snapshot = buildSnapshotFromSearch(window.location.search);
766
+ if (hasAttributionValue(snapshot)) {
767
+ writeStoredSnapshot(snapshot);
768
+ return snapshot;
769
+ }
770
+ return readStoredSnapshot(options);
771
+ }
772
+ function getTrackingAttributionSnapshot(options) {
773
+ return readStoredSnapshot(options);
774
+ }
775
+ function stripLandingTrackingAttributionFromUrl() {
776
+ if (!hasBrowserContext$1()) return false;
777
+ if (!hasTrackingAttributionParams(window.location.search)) return false;
778
+ if (typeof window.history?.replaceState !== "function") return false;
779
+ const params = new URLSearchParams(window.location.search);
780
+ for (const key of TRACKING_ATTRIBUTION_QUERY_PARAM_KEYS) {
781
+ params.delete(key);
782
+ }
783
+ const search = params.toString();
784
+ const hash = window.location.hash ?? "";
785
+ const nextUrl = `${window.location.pathname}${search ? `?${search}` : ""}${hash}`;
786
+ window.history.replaceState(window.history.state, "", nextUrl);
787
+ return true;
788
+ }
789
+ const CART_QUERY = `
790
+ query Cart {
791
+ cart {
792
+ ${CART_FRAGMENT}
793
+ }
794
+ }
795
+ `;
796
+ const CART_CREATE_MUTATION = `
797
+ mutation CartCreate {
798
+ cartCreate {
799
+ cart {
800
+ ${CART_FRAGMENT}
801
+ }
802
+ token
803
+ userErrors {
804
+ field
805
+ message
806
+ code
807
+ }
808
+ }
809
+ }
810
+ `;
811
+ const CART_ITEM_ADD_MUTATION = `
812
+ mutation CartItemAdd($input: CartItemAddInput!) {
813
+ cartItemAdd(input: $input) {
814
+ cart {
815
+ ${CART_FRAGMENT}
816
+ }
817
+ userErrors {
818
+ field
819
+ message
820
+ code
821
+ }
822
+ }
823
+ }
824
+ `;
825
+ const CART_ITEM_UPDATE_MUTATION = `
826
+ mutation CartItemUpdate($input: CartItemUpdateInput!) {
827
+ cartItemUpdate(input: $input) {
828
+ cart {
829
+ ${CART_FRAGMENT}
830
+ }
831
+ userErrors {
832
+ field
833
+ message
834
+ code
835
+ }
836
+ }
837
+ }
838
+ `;
839
+ const CART_ITEM_REMOVE_MUTATION = `
840
+ mutation CartItemRemove($input: CartItemRemoveInput!) {
841
+ cartItemRemove(input: $input) {
842
+ cart {
843
+ ${CART_FRAGMENT}
844
+ }
845
+ userErrors {
846
+ field
847
+ message
848
+ code
849
+ }
850
+ }
851
+ }
852
+ `;
853
+ const CART_CLEAR_MUTATION = `
854
+ mutation CartClear {
855
+ cartClear {
856
+ cart {
857
+ ${CART_FRAGMENT}
858
+ }
859
+ userErrors {
860
+ field
861
+ message
862
+ code
863
+ }
864
+ }
865
+ }
866
+ `;
867
+ const CART_PROMO_CODE_APPLY_MUTATION = `
868
+ mutation CartPromoCodeApply($input: CartPromoCodeApplyInput!) {
869
+ cartPromoCodeApply(input: $input) {
870
+ cart {
871
+ ${CART_FRAGMENT}
872
+ }
873
+ userErrors {
874
+ field
875
+ message
876
+ code
877
+ }
878
+ }
879
+ }
880
+ `;
881
+ const CART_PROMO_CODE_REMOVE_MUTATION = `
882
+ mutation CartPromoCodeRemove {
883
+ cartPromoCodeRemove {
884
+ cart {
885
+ ${CART_FRAGMENT}
886
+ }
887
+ userErrors {
888
+ field
889
+ message
890
+ code
891
+ }
892
+ }
893
+ }
894
+ `;
895
+ function handleUserErrors$1(userErrors) {
896
+ if (userErrors.length === 0) return null;
897
+ const messages = userErrors.map((e) => e.message).join("; ");
898
+ const stateError = userErrors.find((e) => e.code?.includes("STATE") || e.message.includes("state"));
899
+ if (stateError) {
900
+ return new StateError(messages, "unknown");
901
+ }
902
+ return new ValidationError(
903
+ messages,
904
+ userErrors.map((e) => ({ field: e.field, message: e.message }))
905
+ );
906
+ }
907
+ function createCartOperations(client, storage) {
908
+ function getTrackingAttribution() {
909
+ return captureLandingTrackingAttribution({
910
+ ttlMs: client.config.trackingAttributionTTL
911
+ }) ?? void 0;
912
+ }
913
+ return {
914
+ async get() {
915
+ const token = storage.get(CART_TOKEN_KEY);
916
+ if (!token) {
917
+ return ok(null);
918
+ }
919
+ const result = await client.query({ query: CART_QUERY }, { cache: false });
920
+ if (result.isErr()) {
921
+ return err(result.error);
922
+ }
923
+ if (!result.value.cart) {
868
924
  storage.remove(CART_TOKEN_KEY);
869
925
  return ok(null);
870
926
  }
871
- return ok(mapCartData$1(result.value.cart, client.config.endpoint));
927
+ return ok(mapCartData(result.value.cart, client.config.endpoint));
872
928
  },
873
929
  async create() {
874
930
  const result = await client.mutate({
@@ -886,7 +942,7 @@ function createCartOperations(client, storage) {
886
942
  return err(new NotFoundError("Failed to create cart"));
887
943
  }
888
944
  storage.set(CART_TOKEN_KEY, payload.token);
889
- return ok(mapCartData$1(payload.cart, client.config.endpoint));
945
+ return ok(mapCartData(payload.cart, client.config.endpoint));
890
946
  },
891
947
  async addItem(variantId, quantity) {
892
948
  const token = storage.get(CART_TOKEN_KEY);
@@ -899,7 +955,7 @@ function createCartOperations(client, storage) {
899
955
  input: {
900
956
  variantId,
901
957
  quantity,
902
- trackingAttribution: captureLandingTrackingAttribution() ?? void 0
958
+ trackingAttribution: getTrackingAttribution()
903
959
  }
904
960
  }
905
961
  });
@@ -914,11 +970,7 @@ function createCartOperations(client, storage) {
914
970
  if (!payload.cart) {
915
971
  return err(new NotFoundError("Cart not found"));
916
972
  }
917
- const stateCheck = checkCartState(payload.cart.status);
918
- if (stateCheck.isErr()) {
919
- return err(stateCheck.error);
920
- }
921
- return ok(mapCartData$1(payload.cart, client.config.endpoint));
973
+ return ok(mapCartData(payload.cart, client.config.endpoint));
922
974
  },
923
975
  async updateItem(variantId, quantity) {
924
976
  const token = storage.get(CART_TOKEN_KEY);
@@ -931,7 +983,7 @@ function createCartOperations(client, storage) {
931
983
  input: {
932
984
  variantId,
933
985
  quantity,
934
- trackingAttribution: captureLandingTrackingAttribution() ?? void 0
986
+ trackingAttribution: getTrackingAttribution()
935
987
  }
936
988
  }
937
989
  });
@@ -946,11 +998,7 @@ function createCartOperations(client, storage) {
946
998
  if (!payload.cart) {
947
999
  return err(new NotFoundError("Cart not found"));
948
1000
  }
949
- const stateCheck = checkCartState(payload.cart.status);
950
- if (stateCheck.isErr()) {
951
- return err(stateCheck.error);
952
- }
953
- return ok(mapCartData$1(payload.cart, client.config.endpoint));
1001
+ return ok(mapCartData(payload.cart, client.config.endpoint));
954
1002
  },
955
1003
  async removeItem(variantId) {
956
1004
  const token = storage.get(CART_TOKEN_KEY);
@@ -962,7 +1010,7 @@ function createCartOperations(client, storage) {
962
1010
  variables: {
963
1011
  input: {
964
1012
  variantId,
965
- trackingAttribution: captureLandingTrackingAttribution() ?? void 0
1013
+ trackingAttribution: getTrackingAttribution()
966
1014
  }
967
1015
  }
968
1016
  });
@@ -977,11 +1025,7 @@ function createCartOperations(client, storage) {
977
1025
  if (!payload.cart) {
978
1026
  return err(new NotFoundError("Cart not found"));
979
1027
  }
980
- const stateCheck = checkCartState(payload.cart.status);
981
- if (stateCheck.isErr()) {
982
- return err(stateCheck.error);
983
- }
984
- return ok(mapCartData$1(payload.cart, client.config.endpoint));
1028
+ return ok(mapCartData(payload.cart, client.config.endpoint));
985
1029
  },
986
1030
  async clear() {
987
1031
  const token = storage.get(CART_TOKEN_KEY);
@@ -1002,11 +1046,7 @@ function createCartOperations(client, storage) {
1002
1046
  if (!payload.cart) {
1003
1047
  return err(new NotFoundError("Cart not found"));
1004
1048
  }
1005
- const stateCheck = checkCartState(payload.cart.status);
1006
- if (stateCheck.isErr()) {
1007
- return err(stateCheck.error);
1008
- }
1009
- return ok(mapCartData$1(payload.cart, client.config.endpoint));
1049
+ return ok(mapCartData(payload.cart, client.config.endpoint));
1010
1050
  },
1011
1051
  async applyPromoCode(code) {
1012
1052
  const token = storage.get(CART_TOKEN_KEY);
@@ -1028,7 +1068,7 @@ function createCartOperations(client, storage) {
1028
1068
  if (!payload.cart) {
1029
1069
  return err(new NotFoundError("Cart not found"));
1030
1070
  }
1031
- return ok(mapCartData$1(payload.cart, client.config.endpoint));
1071
+ return ok(mapCartData(payload.cart, client.config.endpoint));
1032
1072
  },
1033
1073
  async removePromoCode() {
1034
1074
  const token = storage.get(CART_TOKEN_KEY);
@@ -1049,10 +1089,21 @@ function createCartOperations(client, storage) {
1049
1089
  if (!payload.cart) {
1050
1090
  return err(new NotFoundError("Cart not found"));
1051
1091
  }
1052
- return ok(mapCartData$1(payload.cart, client.config.endpoint));
1092
+ return ok(mapCartData(payload.cart, client.config.endpoint));
1053
1093
  }
1054
1094
  };
1055
1095
  }
1096
+ function isGlobalId(value) {
1097
+ if (!value.includes(":")) {
1098
+ try {
1099
+ const decoded = atob(value);
1100
+ return decoded.includes(":");
1101
+ } catch {
1102
+ return false;
1103
+ }
1104
+ }
1105
+ return false;
1106
+ }
1056
1107
  const CATEGORY_FIELDS = `
1057
1108
  id
1058
1109
  name
@@ -1088,7 +1139,7 @@ query CategoriesTree {
1088
1139
  }
1089
1140
  `;
1090
1141
  const CATEGORY_BY_ID_QUERY = `
1091
- query CategoryById($id: ID!) {
1142
+ query CategoryById($id: GID!) {
1092
1143
  storefrontCategory(id: $id) {
1093
1144
  ${CATEGORY_FIELDS}
1094
1145
  parent {
@@ -1120,7 +1171,7 @@ query CategoryByHandle($handle: String!) {
1120
1171
  }
1121
1172
  `;
1122
1173
  const CATEGORY_PRODUCTS_BY_ID_QUERY = `
1123
- query CategoryProductsById($id: ID!, $first: Int, $after: String, $includeDescendants: Boolean) {
1174
+ query CategoryProductsById($id: GID!, $first: Int, $after: String, $includeDescendants: Boolean) {
1124
1175
  storefrontCategory(id: $id) {
1125
1176
  id
1126
1177
  products(first: $first, after: $after, includeDescendants: $includeDescendants) {
@@ -1339,17 +1390,6 @@ function flattenTree(categories, parentId, depth) {
1339
1390
  }
1340
1391
  return result;
1341
1392
  }
1342
- function isGlobalId$2(value) {
1343
- if (!value.includes(":")) {
1344
- try {
1345
- const decoded = atob(value);
1346
- return decoded.includes(":");
1347
- } catch {
1348
- return false;
1349
- }
1350
- }
1351
- return false;
1352
- }
1353
1393
  function createCategoriesOperations(client) {
1354
1394
  return {
1355
1395
  async tree() {
@@ -1374,7 +1414,7 @@ function createCategoriesOperations(client) {
1374
1414
  return ok(flattenTree(tree, null, 0));
1375
1415
  },
1376
1416
  async get(idOrHandle) {
1377
- const useId = isGlobalId$2(idOrHandle);
1417
+ const useId = isGlobalId(idOrHandle);
1378
1418
  const result = await client.query({
1379
1419
  query: useId ? CATEGORY_BY_ID_QUERY : CATEGORY_BY_HANDLE_QUERY,
1380
1420
  variables: useId ? { id: idOrHandle } : { handle: idOrHandle }
@@ -1388,7 +1428,7 @@ function createCategoriesOperations(client) {
1388
1428
  return ok(mapRawCategory(result.value.storefrontCategory, client.config.endpoint));
1389
1429
  },
1390
1430
  async getProducts(idOrHandle, options) {
1391
- const useId = isGlobalId$2(idOrHandle);
1431
+ const useId = isGlobalId(idOrHandle);
1392
1432
  const result = await client.query({
1393
1433
  query: useId ? CATEGORY_PRODUCTS_BY_ID_QUERY : CATEGORY_PRODUCTS_BY_HANDLE_QUERY,
1394
1434
  variables: {
@@ -1417,1170 +1457,1711 @@ function createCategoriesOperations(client) {
1417
1457
  }
1418
1458
  };
1419
1459
  }
1420
- const CART_FRAGMENT = `
1421
- id
1422
- token
1423
- status
1424
- items {
1425
- id
1426
- variantId
1427
- quantity
1428
- priceAtAdd
1429
- effectiveUnitPrice
1430
- lineTotal
1431
- taxAmount
1432
- variant {
1433
- id
1434
- title
1435
- sku
1436
- price
1437
- compareAtPrice
1438
- weight
1439
- weightUnit
1440
- requiresShipping
1441
- availableForSale
1442
- quantity
1443
- selectedOptions { id value position }
1444
- image {
1445
- id
1446
- url
1447
- altText
1448
- position
1460
+ const ANALYTICS_QUEUE_KEY_PREFIX = "ekomerc_aq_";
1461
+ const ANALYTICS_QUEUE_KEY_API_PREFIX_LENGTH = 12;
1462
+ const ANALYTICS_QUEUE_MAX_EVENTS = 100;
1463
+ const ANALYTICS_QUEUE_TTL_MS = 24 * 60 * 60 * 1e3;
1464
+ function isPlainObject$1(value) {
1465
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1466
+ }
1467
+ function isAnalyticsRetryQueueEntry(value) {
1468
+ if (!isPlainObject$1(value)) {
1469
+ return false;
1470
+ }
1471
+ const event = value.event;
1472
+ return isPlainObject$1(event) && typeof event.eventId === "string" && typeof value.queuedAt === "number" && (value.notBefore === void 0 || typeof value.notBefore === "number");
1473
+ }
1474
+ function getAnalyticsRetryQueueKey(apiKey) {
1475
+ return `${ANALYTICS_QUEUE_KEY_PREFIX}${apiKey.slice(0, ANALYTICS_QUEUE_KEY_API_PREFIX_LENGTH)}`;
1476
+ }
1477
+ function createAnalyticsRetryQueue(storage, apiKey, emitDiagnostic) {
1478
+ const queueKey = getAnalyticsRetryQueueKey(apiKey);
1479
+ function persist(entries) {
1480
+ try {
1481
+ if (entries.length === 0) {
1482
+ storage.remove(queueKey);
1483
+ return;
1449
1484
  }
1450
- isOnSale
1451
- quantityPricing { minQuantity price }
1485
+ storage.set(queueKey, JSON.stringify(entries));
1486
+ } catch {
1452
1487
  }
1453
1488
  }
1454
- totalItems
1455
- totalPrice
1456
- taxTotal
1457
- shippingTotal
1458
- total
1459
- discountTotal
1460
- appliedPromoCode {
1461
- code
1462
- discountType
1463
- discountAmount
1464
- description
1465
- }
1466
- appliedDiscounts {
1467
- promotionId
1468
- discountClass
1469
- discountType
1470
- discountAmount
1471
- description
1472
- isAutomatic
1473
- }
1474
- customerEmail
1475
- customerPhone
1476
- shippingAddress
1477
- billingAddress
1478
- paymentMethod
1479
- shippingRateId
1480
- notes
1481
- checkoutStartedAt
1482
- checkoutExpiresAt
1483
- createdAt
1484
- updatedAt
1485
- `;
1486
- const CHECKOUT_START_MUTATION = `
1487
- mutation CheckoutStart {
1488
- checkoutStart {
1489
- cart {
1490
- ${CART_FRAGMENT}
1489
+ function read(nowMs = Date.now()) {
1490
+ let raw;
1491
+ try {
1492
+ raw = storage.get(queueKey);
1493
+ } catch {
1494
+ return [];
1491
1495
  }
1492
- userErrors {
1493
- field
1494
- message
1495
- code
1496
+ if (!raw) {
1497
+ return [];
1496
1498
  }
1497
- }
1498
- }
1499
- `;
1500
- const CHECKOUT_UPDATE_MUTATION = `
1501
- mutation CheckoutUpdate($input: CheckoutUpdateInput!) {
1502
- checkoutUpdate(input: $input) {
1503
- cart {
1504
- ${CART_FRAGMENT}
1499
+ let parsed;
1500
+ try {
1501
+ parsed = JSON.parse(raw);
1502
+ } catch {
1503
+ return [];
1505
1504
  }
1506
- userErrors {
1507
- field
1508
- message
1509
- code
1505
+ if (!Array.isArray(parsed)) {
1506
+ return [];
1510
1507
  }
1511
- }
1512
- }
1513
- `;
1514
- const CHECKOUT_CONVERT_MUTATION = `
1515
- mutation CheckoutConvert {
1516
- checkoutConvert {
1517
- order {
1518
- id
1519
- orderNumber
1520
- status
1521
- customerEmail
1522
- customerPhone
1523
- shippingAddress
1524
- billingAddress
1525
- subtotal
1526
- total
1527
- note
1528
- items {
1529
- id
1530
- productTitle
1531
- variantTitle
1532
- sku
1533
- quantity
1534
- unitPrice
1535
- totalPrice
1508
+ const entries = parsed.filter(isAnalyticsRetryQueueEntry);
1509
+ const activeEntries = entries.filter((entry) => {
1510
+ const isExpired = nowMs - entry.queuedAt >= ANALYTICS_QUEUE_TTL_MS;
1511
+ if (isExpired) {
1512
+ emitDiagnostic({
1513
+ target: "queue",
1514
+ status: "evicted_expired",
1515
+ eventId: entry.event.eventId
1516
+ });
1536
1517
  }
1537
- createdAt
1538
- }
1539
- paymentInstructions {
1540
- bankAccount
1541
- recipientName
1542
- referenceNumber
1543
- ipsQrCodeBase64
1544
- expiresAt
1545
- }
1546
- userErrors {
1547
- field
1548
- message
1549
- code
1518
+ return !isExpired;
1519
+ });
1520
+ if (activeEntries.length !== entries.length) {
1521
+ persist(activeEntries);
1550
1522
  }
1523
+ return activeEntries;
1551
1524
  }
1552
- }
1553
- `;
1554
- const CHECKOUT_ABANDON_MUTATION = `
1555
- mutation CheckoutAbandon {
1556
- checkoutAbandon {
1557
- cart {
1558
- ${CART_FRAGMENT}
1559
- }
1560
- userErrors {
1561
- field
1562
- message
1563
- code
1525
+ function enqueue(entry) {
1526
+ const entries = read(entry.queuedAt);
1527
+ const nextEntries = [...entries];
1528
+ while (nextEntries.length >= ANALYTICS_QUEUE_MAX_EVENTS) {
1529
+ const evicted = nextEntries.shift();
1530
+ if (evicted) {
1531
+ emitDiagnostic({
1532
+ target: "queue",
1533
+ status: "evicted_full",
1534
+ event: evicted.event
1535
+ });
1536
+ }
1564
1537
  }
1538
+ nextEntries.push(entry);
1539
+ persist(nextEntries);
1540
+ emitDiagnostic({
1541
+ target: "queue",
1542
+ status: "enqueued",
1543
+ event: entry.event
1544
+ });
1545
+ return nextEntries;
1546
+ }
1547
+ function dequeue(count, nowMs = Date.now()) {
1548
+ const entries = read(nowMs);
1549
+ const dequeueCount = Math.max(0, Math.floor(count));
1550
+ const removedEntries = entries.slice(0, dequeueCount);
1551
+ persist(entries.slice(removedEntries.length));
1552
+ emitDiagnostic({
1553
+ target: "queue",
1554
+ status: "flushed",
1555
+ count: removedEntries.length
1556
+ });
1557
+ return removedEntries;
1558
+ }
1559
+ function replace(entries) {
1560
+ persist(entries);
1565
1561
  }
1566
- }
1567
- `;
1568
- function mapCartData(data, endpoint) {
1569
1562
  return {
1570
- id: data.id,
1571
- token: data.token,
1572
- status: data.status,
1573
- items: data.items.map((item) => ({
1574
- id: item.id,
1575
- variantId: item.variantId,
1576
- quantity: item.quantity,
1577
- priceAtAdd: item.priceAtAdd,
1578
- effectiveUnitPrice: item.effectiveUnitPrice,
1579
- lineTotal: item.lineTotal,
1580
- taxAmount: item.taxAmount,
1581
- variant: item.variant ? {
1582
- ...item.variant,
1583
- image: item.variant.image ? { ...item.variant.image, url: resolveAssetUrl(item.variant.image.url, endpoint) } : null
1584
- } : null
1585
- })),
1586
- totalItems: data.totalItems,
1587
- totalPrice: data.totalPrice,
1588
- taxTotal: data.taxTotal,
1589
- shippingTotal: data.shippingTotal,
1590
- total: data.total,
1591
- discountTotal: data.discountTotal,
1592
- appliedPromoCode: data.appliedPromoCode,
1593
- appliedDiscounts: data.appliedDiscounts,
1594
- customerEmail: data.customerEmail,
1595
- customerPhone: data.customerPhone,
1596
- shippingAddress: data.shippingAddress,
1597
- billingAddress: data.billingAddress,
1598
- paymentMethod: data.paymentMethod,
1599
- shippingRateId: data.shippingRateId,
1600
- notes: data.notes,
1601
- checkoutStartedAt: data.checkoutStartedAt ?? null,
1602
- checkoutExpiresAt: data.checkoutExpiresAt ?? null,
1603
- createdAt: data.createdAt,
1604
- updatedAt: data.updatedAt
1563
+ read,
1564
+ enqueue,
1565
+ dequeue,
1566
+ replace
1605
1567
  };
1606
1568
  }
1607
- function mapOrderData(data, paymentInstructions) {
1608
- return {
1609
- id: data.id,
1610
- orderNumber: data.orderNumber,
1611
- email: data.customerEmail,
1612
- phone: data.customerPhone,
1613
- status: data.status,
1614
- shippingAddress: data.shippingAddress,
1615
- subtotal: data.subtotal,
1616
- total: data.total,
1617
- note: data.note,
1618
- items: data.items.map((item) => ({
1619
- id: item.id,
1620
- productTitle: item.productTitle,
1621
- variantTitle: item.variantTitle,
1622
- sku: item.sku,
1623
- quantity: item.quantity,
1624
- unitPrice: item.unitPrice,
1625
- totalPrice: item.totalPrice
1626
- })),
1627
- paymentInstructions: paymentInstructions ?? null,
1628
- createdAt: data.createdAt
1629
- };
1569
+ const TRACKING_POLICY_EVENT_TYPES = [
1570
+ "analytics.page_view",
1571
+ "analytics.product_view",
1572
+ "analytics.collection_view",
1573
+ "analytics.search_performed",
1574
+ "analytics.add_to_cart",
1575
+ "analytics.remove_from_cart",
1576
+ "analytics.checkout_started",
1577
+ "analytics.checkout_step_completed",
1578
+ "analytics.checkout_completed",
1579
+ "analytics.custom"
1580
+ ];
1581
+ const GTM_EVENT_MAP = {
1582
+ "analytics.page_view": "page_view",
1583
+ "analytics.product_view": "view_item",
1584
+ "analytics.collection_view": "view_item_list",
1585
+ "analytics.search_performed": "search",
1586
+ "analytics.add_to_cart": "add_to_cart",
1587
+ "analytics.remove_from_cart": "remove_from_cart",
1588
+ "analytics.checkout_started": "begin_checkout",
1589
+ "analytics.checkout_step_completed": null,
1590
+ "analytics.checkout_completed": "purchase",
1591
+ "analytics.custom": null
1592
+ };
1593
+ const META_EVENT_MAP = {
1594
+ "analytics.page_view": "PageView",
1595
+ "analytics.product_view": "ViewContent",
1596
+ "analytics.collection_view": "ViewContent",
1597
+ "analytics.search_performed": "Search",
1598
+ "analytics.add_to_cart": "AddToCart",
1599
+ "analytics.remove_from_cart": null,
1600
+ "analytics.checkout_started": "InitiateCheckout",
1601
+ "analytics.checkout_step_completed": null,
1602
+ "analytics.checkout_completed": "Purchase",
1603
+ "analytics.custom": null
1604
+ };
1605
+ const TIKTOK_EVENT_MAP = {
1606
+ "analytics.page_view": "PageView",
1607
+ "analytics.product_view": "ViewContent",
1608
+ "analytics.collection_view": "ViewContent",
1609
+ "analytics.search_performed": "Search",
1610
+ "analytics.add_to_cart": "AddToCart",
1611
+ "analytics.remove_from_cart": null,
1612
+ "analytics.checkout_started": "InitiateCheckout",
1613
+ "analytics.checkout_step_completed": null,
1614
+ "analytics.checkout_completed": "CompletePayment",
1615
+ "analytics.custom": null
1616
+ };
1617
+ const TRACKING_PROVIDER_EVENT_MAPS = {
1618
+ gtm: GTM_EVENT_MAP,
1619
+ meta: META_EVENT_MAP,
1620
+ tiktok: TIKTOK_EVENT_MAP
1621
+ };
1622
+ function collectSupportedEventTypes(provider) {
1623
+ const eventMap = TRACKING_PROVIDER_EVENT_MAPS[provider];
1624
+ return TRACKING_POLICY_EVENT_TYPES.filter((eventType) => eventMap[eventType] !== null);
1630
1625
  }
1631
- function handleUserErrors(userErrors) {
1632
- if (userErrors.length === 0) return null;
1633
- const messages = userErrors.map((e) => e.message).join("; ");
1634
- const stateErrorCodes = ["CART_NOT_IN_CHECKOUT", "CHECKOUT_START_ERROR", "CHECKOUT_ABANDON_ERROR"];
1635
- const stateError = userErrors.find(
1636
- (e) => e.code?.includes("STATE") || e.message.includes("state") || e.code && stateErrorCodes.includes(e.code)
1637
- );
1638
- if (stateError) {
1639
- return new StateError(messages, "unknown");
1626
+ ({
1627
+ gtm: {
1628
+ supportedEventTypes: collectSupportedEventTypes("gtm")
1629
+ },
1630
+ meta: {
1631
+ supportedEventTypes: collectSupportedEventTypes("meta")
1632
+ },
1633
+ tiktok: {
1634
+ supportedEventTypes: collectSupportedEventTypes("tiktok")
1640
1635
  }
1641
- return new ValidationError(
1642
- messages,
1643
- userErrors.map((e) => ({ field: e.field, message: e.message }))
1644
- );
1636
+ });
1637
+ const SHARED_DEDUPE_CONVENTION = {
1638
+ canonicalEventIdField: "eventId",
1639
+ providerEventIdField: "event_id",
1640
+ confirmedPurchaseSource: "order.id"
1641
+ };
1642
+ const TRACKING_PROVIDER_DEDUPE_FIELDS = {
1643
+ gtm: SHARED_DEDUPE_CONVENTION,
1644
+ meta: SHARED_DEDUPE_CONVENTION,
1645
+ tiktok: SHARED_DEDUPE_CONVENTION
1646
+ };
1647
+ function getTrackingProviderEventName(provider, eventType) {
1648
+ return TRACKING_PROVIDER_EVENT_MAPS[provider][eventType];
1645
1649
  }
1646
- function checkCartIsInCheckout(status) {
1647
- if (status !== "checkout") {
1648
- return err(
1649
- new StateError(`Cart must be in 'checkout' state for this operation. Current state: '${status}'.`, status)
1650
- );
1650
+ function providerSupportsTrackingEvent(provider, eventType) {
1651
+ return TRACKING_PROVIDER_EVENT_MAPS[provider][eventType] !== null;
1652
+ }
1653
+ function getTrackingProviderDedupeConvention(provider) {
1654
+ return TRACKING_PROVIDER_DEDUPE_FIELDS[provider];
1655
+ }
1656
+ const TRACKING_SCHEMA_VERSION = 1;
1657
+ function shouldDispatchForTrackingConsent(input) {
1658
+ if (input.consentState === "granted") {
1659
+ return true;
1651
1660
  }
1652
- return ok(void 0);
1661
+ if (input.consentState === "denied") {
1662
+ return false;
1663
+ }
1664
+ return input.dispatchOnUnknownConsent;
1653
1665
  }
1654
- function createCheckoutOperations(client, storage) {
1655
- return {
1656
- async start() {
1657
- const token = storage.get(CART_TOKEN_KEY);
1658
- if (!token) {
1659
- return err(new NotFoundError("No cart exists. Call cart.create() first."));
1660
- }
1661
- const result = await client.mutate({
1662
- query: CHECKOUT_START_MUTATION
1663
- });
1664
- if (result.isErr()) {
1665
- return err(result.error);
1666
- }
1667
- const payload = result.value.checkoutStart;
1668
- const userError = handleUserErrors(payload.userErrors);
1669
- if (userError) {
1670
- return err(userError);
1671
- }
1672
- if (!payload.cart) {
1673
- return err(new NotFoundError("Cart not found"));
1674
- }
1675
- return ok(mapCartData(payload.cart, client.config.endpoint));
1676
- },
1677
- async update(data) {
1678
- const token = storage.get(CART_TOKEN_KEY);
1679
- if (!token) {
1680
- return err(new NotFoundError("No cart exists. Call cart.create() first."));
1681
- }
1682
- const input = {};
1683
- if (data.email !== void 0) input.customerEmail = data.email;
1684
- if (data.phone !== void 0) input.customerPhone = data.phone;
1685
- if (data.shippingAddress !== void 0) input.shippingAddress = data.shippingAddress;
1686
- if (data.billingAddress !== void 0) input.billingAddress = data.billingAddress;
1687
- if (data.notes !== void 0) input.notes = data.notes;
1688
- if (data.emailMarketingConsent !== void 0) input.emailMarketingConsent = data.emailMarketingConsent;
1689
- if (data.paymentMethod !== void 0) input.paymentMethod = data.paymentMethod;
1690
- if (data.shippingRateId !== void 0) input.shippingRateId = data.shippingRateId;
1691
- const result = await client.mutate({
1692
- query: CHECKOUT_UPDATE_MUTATION,
1693
- variables: { input }
1694
- });
1695
- if (result.isErr()) {
1696
- return err(result.error);
1697
- }
1698
- const payload = result.value.checkoutUpdate;
1699
- const userError = handleUserErrors(payload.userErrors);
1700
- if (userError) {
1701
- return err(userError);
1702
- }
1703
- if (!payload.cart) {
1704
- return err(new NotFoundError("Cart not found"));
1705
- }
1706
- const stateCheck = checkCartIsInCheckout(payload.cart.status);
1707
- if (stateCheck.isErr()) {
1708
- return err(stateCheck.error);
1709
- }
1710
- return ok(mapCartData(payload.cart, client.config.endpoint));
1711
- },
1712
- async complete() {
1713
- const token = storage.get(CART_TOKEN_KEY);
1714
- if (!token) {
1715
- return err(new NotFoundError("No cart exists. Call cart.create() first."));
1716
- }
1717
- const result = await client.mutate({
1718
- query: CHECKOUT_CONVERT_MUTATION
1719
- });
1720
- if (result.isErr()) {
1721
- return err(result.error);
1722
- }
1723
- const payload = result.value.checkoutConvert;
1724
- const userError = handleUserErrors(payload.userErrors);
1725
- if (userError) {
1726
- return err(userError);
1727
- }
1728
- if (!payload.order) {
1729
- return err(new NotFoundError("Order not found"));
1730
- }
1731
- storage.remove(CART_TOKEN_KEY);
1732
- return ok(mapOrderData(payload.order, payload.paymentInstructions));
1733
- },
1734
- async abandon() {
1735
- const token = storage.get(CART_TOKEN_KEY);
1736
- if (!token) {
1737
- return err(new NotFoundError("No cart exists. Call cart.create() first."));
1738
- }
1739
- const result = await client.mutate({
1740
- query: CHECKOUT_ABANDON_MUTATION
1741
- });
1742
- if (result.isErr()) {
1743
- return err(result.error);
1744
- }
1745
- const payload = result.value.checkoutAbandon;
1746
- const userError = handleUserErrors(payload.userErrors);
1747
- if (userError) {
1748
- return err(userError);
1749
- }
1750
- if (!payload.cart) {
1751
- return err(new NotFoundError("Cart not found"));
1666
+ function shouldDispatchBrowserAdapter(provider, eventType, consent) {
1667
+ if (!providerSupportsTrackingEvent(provider, eventType)) {
1668
+ return { allowed: false, reason: "unsupported_event" };
1669
+ }
1670
+ if (!shouldDispatchForTrackingConsent(consent)) {
1671
+ return { allowed: false, reason: "consent_blocked" };
1672
+ }
1673
+ return { allowed: true };
1674
+ }
1675
+ const ANALYTICS_PATH = "/analytics/ingest";
1676
+ const VISITOR_COOKIE_NAME = "ekomerc_vid";
1677
+ const SESSION_STORAGE_KEY = "ekomerc_sid";
1678
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
1679
+ const KEEPALIVE_MAX_BODY_BYTES = 60 * 1024;
1680
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1681
+ const ACCEPTED_UNKNOWN_RESPONSE = {
1682
+ acceptedCount: 0,
1683
+ duplicateCount: 0,
1684
+ rejectedCount: 0,
1685
+ errors: []
1686
+ };
1687
+ const ANALYTICS_PRESET_EVENT_MAP = {
1688
+ page_view: "analytics.page_view",
1689
+ product_view: "analytics.product_view",
1690
+ collection_view: "analytics.collection_view",
1691
+ search_performed: "analytics.search_performed",
1692
+ add_to_cart: "analytics.add_to_cart",
1693
+ remove_from_cart: "analytics.remove_from_cart",
1694
+ checkout_started: "analytics.checkout_started",
1695
+ checkout_step_completed: "analytics.checkout_step_completed",
1696
+ checkout_completed: "analytics.checkout_completed"
1697
+ };
1698
+ function hasBrowserContext() {
1699
+ return typeof window !== "undefined" && typeof document !== "undefined";
1700
+ }
1701
+ function getDocument() {
1702
+ return hasBrowserContext() ? document : null;
1703
+ }
1704
+ function getWindow() {
1705
+ return hasBrowserContext() ? window : null;
1706
+ }
1707
+ function isUuid(value) {
1708
+ return UUID_REGEX.test(value);
1709
+ }
1710
+ function randomUuid() {
1711
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1712
+ return crypto.randomUUID();
1713
+ }
1714
+ const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
1715
+ return template.replace(/[xy]/g, (character) => {
1716
+ const random = Math.floor(Math.random() * 16);
1717
+ const value = character === "x" ? random : random & 3 | 8;
1718
+ return value.toString(16);
1719
+ });
1720
+ }
1721
+ function getOrCreateVisitorId() {
1722
+ const doc = getDocument();
1723
+ if (doc) {
1724
+ const raw = doc.cookie;
1725
+ for (const pair of raw.split(";")) {
1726
+ const [name, ...valueParts] = pair.trim().split("=");
1727
+ if (name === VISITOR_COOKIE_NAME) {
1728
+ return valueParts.join("=") ? decodeURIComponent(valueParts.join("=")) : randomUuid();
1752
1729
  }
1753
- return ok(mapCartData(payload.cart, client.config.endpoint));
1754
1730
  }
1731
+ }
1732
+ const value = randomUuid();
1733
+ if (doc) {
1734
+ const maxAge = 60 * 60 * 24 * 365 * 2;
1735
+ doc.cookie = `${VISITOR_COOKIE_NAME}=${encodeURIComponent(value)}; Max-Age=${maxAge}; Path=/; SameSite=Lax`;
1736
+ }
1737
+ return value;
1738
+ }
1739
+ let fallbackSessionState = null;
1740
+ function getSessionStorageState() {
1741
+ const win = getWindow();
1742
+ if (!win) return fallbackSessionState;
1743
+ try {
1744
+ const raw = win.localStorage.getItem(SESSION_STORAGE_KEY);
1745
+ if (!raw) return fallbackSessionState;
1746
+ const parsed = JSON.parse(raw);
1747
+ if (typeof parsed !== "object" || parsed === null) return null;
1748
+ const id = parsed.id;
1749
+ const startedAt = parsed.startedAt;
1750
+ const lastSeenAt = parsed.lastSeenAt;
1751
+ if (typeof id !== "string" || !id) return null;
1752
+ if (typeof startedAt !== "number" || typeof lastSeenAt !== "number") return null;
1753
+ return { id, startedAt, lastSeenAt };
1754
+ } catch {
1755
+ return fallbackSessionState;
1756
+ }
1757
+ }
1758
+ function setSessionStorageState(state) {
1759
+ fallbackSessionState = state;
1760
+ const win = getWindow();
1761
+ if (!win) return;
1762
+ try {
1763
+ win.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state));
1764
+ } catch {
1765
+ }
1766
+ }
1767
+ function resolveSessionState(existing, nowMs) {
1768
+ if (!existing) {
1769
+ return { id: randomUuid(), startedAt: nowMs, lastSeenAt: nowMs };
1770
+ }
1771
+ if (nowMs - existing.lastSeenAt > SESSION_TIMEOUT_MS) {
1772
+ return { id: randomUuid(), startedAt: nowMs, lastSeenAt: nowMs };
1773
+ }
1774
+ return { id: existing.id, startedAt: existing.startedAt, lastSeenAt: nowMs };
1775
+ }
1776
+ function resolveSessionId(nowMs) {
1777
+ const existing = getSessionStorageState();
1778
+ const state = resolveSessionState(existing, nowMs);
1779
+ setSessionStorageState(state);
1780
+ return state.id;
1781
+ }
1782
+ function normalizeNumber(value) {
1783
+ return Number.isFinite(value) ? value : 0;
1784
+ }
1785
+ function toCents(amount) {
1786
+ return Math.max(0, Math.round(normalizeNumber(amount) * 100));
1787
+ }
1788
+ function base64UrlDecode(value) {
1789
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
1790
+ const padding = normalized.length % 4;
1791
+ const padded = padding === 0 ? normalized : normalized + "=".repeat(4 - padding);
1792
+ return atob(padded);
1793
+ }
1794
+ function parseJWT(payload) {
1795
+ try {
1796
+ const decoded = base64UrlDecode(payload);
1797
+ const parsed = JSON.parse(decoded);
1798
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1799
+ } catch {
1800
+ return null;
1801
+ }
1802
+ }
1803
+ function parseCustomerIdFromToken(token) {
1804
+ if (!token) return null;
1805
+ const payload = token.split(".");
1806
+ const encodedPayload = payload[1];
1807
+ if (!encodedPayload) return null;
1808
+ const parsed = parseJWT(encodedPayload);
1809
+ if (!parsed) return null;
1810
+ const customerId = parsed.customerId;
1811
+ return typeof customerId === "string" && isUuid(customerId) ? customerId : null;
1812
+ }
1813
+ function decodeAnalyticsEntityId(value) {
1814
+ if (!value) return null;
1815
+ if (isUuid(value)) return value;
1816
+ if (value.startsWith("gid://")) {
1817
+ const parts = value.split("/");
1818
+ const candidate = parts[parts.length - 1];
1819
+ return candidate && isUuid(candidate) ? candidate : null;
1820
+ }
1821
+ try {
1822
+ const decoded = atob(value);
1823
+ const parts = decoded.split(":");
1824
+ const candidate = parts[parts.length - 1];
1825
+ return candidate && isUuid(candidate) ? candidate : null;
1826
+ } catch {
1827
+ return null;
1828
+ }
1829
+ }
1830
+ function resolveDeviceInfo() {
1831
+ const win = getWindow();
1832
+ if (!win) {
1833
+ return { deviceType: "server", deviceOs: null, deviceBrowser: null };
1834
+ }
1835
+ const ua = win.navigator.userAgent.toLowerCase();
1836
+ const deviceType = /ipad|tablet|playbook|silk/.test(ua) ? "tablet" : /mobi|android|iphone|ipod|windows phone/.test(ua) ? "mobile" : "desktop";
1837
+ let deviceOs = null;
1838
+ if (ua.includes("windows")) {
1839
+ deviceOs = "Windows";
1840
+ } else if (ua.includes("mac os") || ua.includes("macintosh")) {
1841
+ deviceOs = "macOS";
1842
+ } else if (ua.includes("android")) {
1843
+ deviceOs = "Android";
1844
+ } else if (ua.includes("iphone") || ua.includes("ipad") || ua.includes("ios")) {
1845
+ deviceOs = "iOS";
1846
+ } else if (ua.includes("linux")) {
1847
+ deviceOs = "Linux";
1848
+ }
1849
+ let deviceBrowser = null;
1850
+ if (ua.includes("edg/")) {
1851
+ deviceBrowser = "Edge";
1852
+ } else if (ua.includes("firefox/")) {
1853
+ deviceBrowser = "Firefox";
1854
+ } else if (ua.includes("chrome/") && !ua.includes("edg/")) {
1855
+ deviceBrowser = "Chrome";
1856
+ } else if (ua.includes("safari/") && !ua.includes("chrome/")) {
1857
+ deviceBrowser = "Safari";
1858
+ }
1859
+ return { deviceType, deviceOs, deviceBrowser };
1860
+ }
1861
+ function normalizePath(pathname) {
1862
+ return pathname.trim().length > 0 ? pathname : "/";
1863
+ }
1864
+ function toIsoTimestamp(value) {
1865
+ if (value == null) return null;
1866
+ const date = value instanceof Date ? value : new Date(value);
1867
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
1868
+ }
1869
+ function buildUtmPayload(context, trackingAttributionTtlMs) {
1870
+ const persisted = getTrackingAttributionSnapshot({ ttlMs: trackingAttributionTtlMs });
1871
+ return {
1872
+ schemaVersion: TRACKING_SCHEMA_VERSION,
1873
+ source: context.utmSource ?? persisted?.utm.source ?? null,
1874
+ medium: context.utmMedium ?? persisted?.utm.medium ?? null,
1875
+ campaign: context.utmCampaign ?? persisted?.utm.campaign ?? null,
1876
+ term: context.utmTerm ?? persisted?.utm.term ?? null,
1877
+ content: context.utmContent ?? persisted?.utm.content ?? null
1755
1878
  };
1756
1879
  }
1757
- function mapRawCollection(raw, endpoint) {
1880
+ function buildClickIdsPayload(context, trackingAttributionTtlMs) {
1881
+ const persisted = getTrackingAttributionSnapshot({ ttlMs: trackingAttributionTtlMs });
1882
+ const clickIds = context.clickIds;
1758
1883
  return {
1759
- id: raw.id,
1760
- handle: raw.handle,
1761
- title: raw.title,
1762
- description: raw.description,
1763
- type: raw.type,
1764
- sortOrder: raw.sortOrder,
1765
- metaTitle: raw.metaTitle,
1766
- metaDescription: raw.metaDescription,
1767
- productCount: raw.productCount,
1768
- imageAsset: raw.imageAsset ? {
1769
- url: resolveAssetUrl(raw.imageAsset.url, endpoint),
1770
- altText: raw.imageAsset.altText,
1771
- width: raw.imageAsset.width,
1772
- height: raw.imageAsset.height
1773
- } : null
1884
+ schemaVersion: TRACKING_SCHEMA_VERSION,
1885
+ capturedAt: toIsoTimestamp(clickIds?.capturedAt) ?? persisted?.clickIds.capturedAt ?? null,
1886
+ gclid: clickIds?.gclid ?? persisted?.clickIds.gclid ?? null,
1887
+ gbraid: clickIds?.gbraid ?? persisted?.clickIds.gbraid ?? null,
1888
+ wbraid: clickIds?.wbraid ?? persisted?.clickIds.wbraid ?? null,
1889
+ ttclid: clickIds?.ttclid ?? persisted?.clickIds.ttclid ?? null,
1890
+ fbclid: clickIds?.fbclid ?? persisted?.clickIds.fbclid ?? null
1774
1891
  };
1775
1892
  }
1776
- const COLLECTIONS_QUERY = `
1777
- query Collections($first: Int, $after: String, $search: String) {
1778
- collections(first: $first, after: $after, search: $search) {
1779
- edges {
1780
- node {
1781
- id
1782
- handle
1783
- title
1784
- description
1785
- type
1786
- sortOrder
1787
- metaTitle
1788
- metaDescription
1789
- productCount
1790
- imageAsset {
1791
- url
1792
- altText
1793
- width
1794
- height
1893
+ function buildDefaultContext() {
1894
+ const context = {};
1895
+ const device = resolveDeviceInfo();
1896
+ const win = getWindow();
1897
+ if (win) {
1898
+ const { pathname, search } = win.location;
1899
+ context.path = pathname + search;
1900
+ context.referrer = win.document.referrer.length > 0 ? win.document.referrer : null;
1901
+ const params = new URLSearchParams(win.location.search);
1902
+ context.utmSource = params.get("utm_source");
1903
+ context.utmMedium = params.get("utm_medium");
1904
+ context.utmCampaign = params.get("utm_campaign");
1905
+ context.utmTerm = params.get("utm_term");
1906
+ context.utmContent = params.get("utm_content");
1907
+ context.deviceType = device.deviceType;
1908
+ context.deviceOs = device.deviceOs;
1909
+ context.deviceBrowser = device.deviceBrowser;
1910
+ }
1911
+ if (!context.deviceType) {
1912
+ context.deviceType = "unknown";
1913
+ }
1914
+ context.deviceOs ??= null;
1915
+ context.deviceBrowser ??= null;
1916
+ return context;
1917
+ }
1918
+ function normalizeEventContext(context) {
1919
+ const defaults = buildDefaultContext();
1920
+ return { ...defaults, ...context };
1921
+ }
1922
+ function isIngestResponse(value) {
1923
+ if (typeof value !== "object" || value === null) return false;
1924
+ const response = value;
1925
+ return typeof response.acceptedCount === "number" && typeof response.duplicateCount === "number" && typeof response.rejectedCount === "number" && Array.isArray(response.errors);
1926
+ }
1927
+ function extractErrorMessage(response) {
1928
+ return response.json().then((payload) => {
1929
+ if (typeof payload.error === "string" && payload.error.length > 0) {
1930
+ return payload.error;
1931
+ }
1932
+ const firstMessage = Array.isArray(payload.details) ? payload.details.map((error) => typeof error?.message === "string" ? error.message : null).find((message) => message !== null) : null;
1933
+ if (firstMessage) {
1934
+ return firstMessage;
1935
+ }
1936
+ return `Analytics ingest failed with status ${response.status}`;
1937
+ }).catch(() => `Analytics ingest failed with status ${response.status}`);
1938
+ }
1939
+ function parseRetryAfterHeader(response, nowMs) {
1940
+ const retryAfter = response.headers.get("retry-after");
1941
+ if (!retryAfter) {
1942
+ return null;
1943
+ }
1944
+ const trimmed = retryAfter.trim();
1945
+ if (!/^\d+$/.test(trimmed)) {
1946
+ return null;
1947
+ }
1948
+ const seconds = Number.parseInt(trimmed, 10);
1949
+ if (!Number.isFinite(seconds) || seconds <= 0) {
1950
+ return null;
1951
+ }
1952
+ return nowMs + seconds * 1e3;
1953
+ }
1954
+ function toError(error, message) {
1955
+ if (error instanceof Error) {
1956
+ return error;
1957
+ }
1958
+ return new Error(message);
1959
+ }
1960
+ function isPlainObject(value) {
1961
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1962
+ }
1963
+ async function flushAnalyticsRetryQueue(retryQueue, sendEventOutcome, emitDiagnostic, nowMs = Date.now()) {
1964
+ const queuedEntries = retryQueue.read(nowMs);
1965
+ const nextEntries = [];
1966
+ let sentCount = 0;
1967
+ for (const entry of queuedEntries) {
1968
+ if (entry.notBefore !== void 0 && entry.notBefore > nowMs) {
1969
+ nextEntries.push(entry);
1970
+ continue;
1971
+ }
1972
+ const outcome = await sendEventOutcome(entry.event);
1973
+ if (outcome.result.isOk()) {
1974
+ sentCount += 1;
1975
+ continue;
1976
+ }
1977
+ if (!outcome.retryDisposition.retryable) {
1978
+ continue;
1979
+ }
1980
+ nextEntries.push({
1981
+ event: entry.event,
1982
+ queuedAt: entry.queuedAt,
1983
+ ...outcome.retryDisposition.notBefore !== void 0 ? { notBefore: outcome.retryDisposition.notBefore } : {}
1984
+ });
1985
+ }
1986
+ retryQueue.replace(nextEntries);
1987
+ emitDiagnostic({
1988
+ target: "queue",
1989
+ status: "flushed",
1990
+ count: sentCount
1991
+ });
1992
+ return sentCount;
1993
+ }
1994
+ function resolveTrackEvent(eventName) {
1995
+ const normalized = eventName.startsWith("analytics.") ? eventName.slice("analytics.".length) : eventName;
1996
+ if (normalized in ANALYTICS_PRESET_EVENT_MAP) {
1997
+ return { eventType: ANALYTICS_PRESET_EVENT_MAP[normalized] };
1998
+ }
1999
+ return {
2000
+ eventType: "analytics.custom",
2001
+ customEventName: eventName
2002
+ };
2003
+ }
2004
+ function dispatchEventToBrowserRuntime(runtime, event, resolvedAnalytics, emitDiagnostic, scheduleBestEffort) {
2005
+ const adapters = (runtime?.adapters ?? []).filter(
2006
+ (adapter) => adapter.provider !== "gtm" || resolvedAnalytics.gtm.enabled
2007
+ );
2008
+ if (adapters.length === 0) {
2009
+ return;
2010
+ }
2011
+ const consent = {
2012
+ consentState: event.consentState,
2013
+ dispatchOnUnknownConsent: runtime?.dispatchOnUnknownConsent ?? true
2014
+ };
2015
+ scheduleBestEffort(async () => {
2016
+ for (const adapter of adapters) {
2017
+ if (adapter.updateConsent) {
2018
+ try {
2019
+ const result = await adapter.updateConsent(consent);
2020
+ if (result?.status === "skipped") {
2021
+ emitDiagnostic({
2022
+ target: "consent_bridge",
2023
+ status: "skipped",
2024
+ provider: adapter.provider,
2025
+ consent,
2026
+ reason: result.reason
2027
+ });
2028
+ emitDiagnostic({
2029
+ target: "adapter",
2030
+ status: "skipped",
2031
+ provider: adapter.provider,
2032
+ event,
2033
+ reason: result.reason
2034
+ });
2035
+ continue;
2036
+ }
2037
+ emitDiagnostic({
2038
+ target: "consent_bridge",
2039
+ status: "success",
2040
+ provider: adapter.provider,
2041
+ consent
2042
+ });
2043
+ } catch (error) {
2044
+ emitDiagnostic({
2045
+ target: "consent_bridge",
2046
+ status: "error",
2047
+ provider: adapter.provider,
2048
+ consent,
2049
+ error: toError(error, `Failed to update ${adapter.provider} consent bridge`)
2050
+ });
2051
+ emitDiagnostic({
2052
+ target: "adapter",
2053
+ status: "error",
2054
+ provider: adapter.provider,
2055
+ event,
2056
+ error: toError(error, `Skipped ${adapter.provider} browser tracking event due to consent bridge failure`)
2057
+ });
2058
+ continue;
1795
2059
  }
1796
2060
  }
1797
- cursor
2061
+ const dispatchDecision = shouldDispatchBrowserAdapter(adapter.provider, event.eventType, consent);
2062
+ if (!dispatchDecision.allowed) {
2063
+ emitDiagnostic({
2064
+ target: "adapter",
2065
+ status: "skipped",
2066
+ provider: adapter.provider,
2067
+ event,
2068
+ reason: dispatchDecision.reason
2069
+ });
2070
+ continue;
2071
+ }
2072
+ try {
2073
+ const result = await adapter.dispatch({ event, consent });
2074
+ if (result?.status === "skipped") {
2075
+ emitDiagnostic({
2076
+ target: "adapter",
2077
+ status: "skipped",
2078
+ provider: adapter.provider,
2079
+ event,
2080
+ reason: result.reason
2081
+ });
2082
+ continue;
2083
+ }
2084
+ emitDiagnostic({
2085
+ target: "adapter",
2086
+ status: "success",
2087
+ provider: adapter.provider,
2088
+ event
2089
+ });
2090
+ } catch (error) {
2091
+ emitDiagnostic({
2092
+ target: "adapter",
2093
+ status: "error",
2094
+ provider: adapter.provider,
2095
+ event,
2096
+ error: toError(error, `Failed to dispatch ${adapter.provider} browser tracking event`)
2097
+ });
2098
+ }
2099
+ }
2100
+ });
2101
+ }
2102
+ function shouldMirrorEventToBrowserRuntime(eventType) {
2103
+ return eventType !== "analytics.checkout_completed";
2104
+ }
2105
+ function isResolvedAnalyticsConfig(input) {
2106
+ return Boolean(
2107
+ input && typeof input === "object" && "enabled" in input && "dispatchOnUnknownConsent" in input && "gtm" in input
2108
+ );
2109
+ }
2110
+ function buildDefaultResolvedAnalyticsConfig(runtime) {
2111
+ const hasGtmAdapter = runtime?.adapters?.some((adapter) => adapter.provider === "gtm") ?? false;
2112
+ return {
2113
+ enabled: true,
2114
+ dispatchOnUnknownConsent: runtime?.dispatchOnUnknownConsent ?? true,
2115
+ gtm: {
2116
+ enabled: hasGtmAdapter,
2117
+ containerId: null
2118
+ }
2119
+ };
2120
+ }
2121
+ function createAnalyticsOperations(client, storage) {
2122
+ const trackingAttributionTtlMs = client.config.trackingAttributionTTL;
2123
+ const runtimeClient = client;
2124
+ let lifecycleFlushListenersRegistered = false;
2125
+ const endpoint = (() => {
2126
+ try {
2127
+ const parsedEndpoint = new URL(client.config.endpoint);
2128
+ return `${parsedEndpoint.origin}${ANALYTICS_PATH}`;
2129
+ } catch {
2130
+ return null;
2131
+ }
2132
+ })();
2133
+ const apiKey = client.config.apiKey;
2134
+ function getTrackingRuntime() {
2135
+ return runtimeClient._analyticsRuntimeConfig ?? client.config.tracking ?? null;
2136
+ }
2137
+ function getResolvedAnalyticsConfig() {
2138
+ return runtimeClient._resolvedInitConfig?.analytics ?? null;
2139
+ }
2140
+ function emitDiagnostic(diagnostic) {
2141
+ try {
2142
+ getTrackingRuntime()?.onDiagnostic?.(diagnostic);
2143
+ } catch {
1798
2144
  }
1799
- pageInfo {
1800
- hasNextPage
1801
- hasPreviousPage
1802
- startCursor
1803
- endCursor
2145
+ }
2146
+ const retryQueue = createAnalyticsRetryQueue(storage, apiKey, emitDiagnostic);
2147
+ async function resolveConsentState(explicitConsentState) {
2148
+ if (explicitConsentState) {
2149
+ return explicitConsentState;
2150
+ }
2151
+ const trackingRuntime = getTrackingRuntime();
2152
+ if (!trackingRuntime?.resolveConsentState) {
2153
+ return "unknown";
2154
+ }
2155
+ try {
2156
+ return await trackingRuntime.resolveConsentState();
2157
+ } catch (error) {
2158
+ emitDiagnostic({
2159
+ target: "consent",
2160
+ status: "error",
2161
+ error: toError(error, "Failed to resolve tracking consent state")
2162
+ });
2163
+ return "unknown";
1804
2164
  }
1805
2165
  }
1806
- }
1807
- `;
1808
- const COLLECTION_BY_ID_QUERY = `
1809
- query CollectionById($id: ID!) {
1810
- collection(id: $id) {
1811
- id
1812
- handle
1813
- title
1814
- description
1815
- type
1816
- sortOrder
1817
- metaTitle
1818
- metaDescription
1819
- productCount
1820
- imageAsset {
1821
- url
1822
- altText
1823
- width
1824
- height
2166
+ function scheduleBestEffort(task) {
2167
+ Promise.resolve().then(task).catch(() => {
2168
+ });
2169
+ }
2170
+ function flushRetryQueueInBackground() {
2171
+ scheduleBestEffort(async () => {
2172
+ await flushAnalyticsRetryQueue(retryQueue, sendEventOutcome, emitDiagnostic);
2173
+ });
2174
+ }
2175
+ function registerLifecycleFlushListeners() {
2176
+ if (!hasBrowserContext() || lifecycleFlushListenersRegistered) {
2177
+ return;
1825
2178
  }
2179
+ const win = getWindow();
2180
+ const doc = getDocument();
2181
+ if (!win || !doc) {
2182
+ return;
2183
+ }
2184
+ win.addEventListener("online", () => {
2185
+ flushRetryQueueInBackground();
2186
+ });
2187
+ doc.addEventListener("visibilitychange", () => {
2188
+ if (doc.visibilityState === "hidden") {
2189
+ flushRetryQueueInBackground();
2190
+ }
2191
+ });
2192
+ lifecycleFlushListenersRegistered = true;
1826
2193
  }
1827
- }
1828
- `;
1829
- const COLLECTION_BY_HANDLE_QUERY = `
1830
- query CollectionByHandle($handle: String!) {
1831
- collection(handle: $handle) {
1832
- id
1833
- handle
1834
- title
1835
- description
1836
- type
1837
- sortOrder
1838
- metaTitle
1839
- metaDescription
1840
- productCount
1841
- imageAsset {
1842
- url
1843
- altText
1844
- width
1845
- height
2194
+ function init(input) {
2195
+ if (isResolvedAnalyticsConfig(input)) {
2196
+ runtimeClient._resolvedInitConfig = { analytics: input };
2197
+ } else {
2198
+ runtimeClient._analyticsRuntimeConfig = {
2199
+ ...getTrackingRuntime() ?? {},
2200
+ ...input ?? {}
2201
+ };
2202
+ runtimeClient._resolvedInitConfig ??= {
2203
+ analytics: buildDefaultResolvedAnalyticsConfig(getTrackingRuntime())
2204
+ };
2205
+ }
2206
+ captureLandingTrackingAttribution({ ttlMs: trackingAttributionTtlMs });
2207
+ if (getTrackingRuntime()?.stripUrl !== false) {
2208
+ stripLandingTrackingAttributionFromUrl();
1846
2209
  }
2210
+ registerLifecycleFlushListeners();
2211
+ flushRetryQueueInBackground();
1847
2212
  }
1848
- }
1849
- `;
1850
- const COLLECTION_PRODUCTS_BY_ID_QUERY = `
1851
- query CollectionProductsById($id: ID!, $first: Int, $after: String, $sort: CollectionSortOrder) {
1852
- collection(id: $id) {
1853
- id
1854
- products(first: $first, after: $after, sort: $sort) {
1855
- edges {
1856
- node {
1857
- id
1858
- handle
1859
- title
1860
- description
1861
- vendor
1862
- productType
1863
- metaTitle
1864
- metaDescription
1865
- publishedAt
1866
- createdAt
1867
- availableForSale
1868
- media {
1869
- id
1870
- url
1871
- altText
1872
- position
2213
+ function buildCanonicalEvent(eventType, properties, eventContext, consentState) {
2214
+ const now = new Date(eventContext.occurredAt ?? Date.now());
2215
+ const nowIso = Number.isNaN(now.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : now.toISOString();
2216
+ const customerId = eventContext.customerId === void 0 ? parseCustomerIdFromToken(client.getCustomerToken()) : eventContext.customerId;
2217
+ return {
2218
+ schemaVersion: TRACKING_SCHEMA_VERSION,
2219
+ eventId: randomUuid(),
2220
+ eventType,
2221
+ occurredAt: nowIso,
2222
+ sessionId: eventContext.sessionId ?? resolveSessionId(now.getTime()),
2223
+ visitorId: eventContext.visitorId ?? getOrCreateVisitorId(),
2224
+ customerId: customerId ?? null,
2225
+ consentState,
2226
+ context: {
2227
+ schemaVersion: TRACKING_SCHEMA_VERSION,
2228
+ path: normalizePath(eventContext.path ?? "/")
2229
+ },
2230
+ referrer: eventContext.referrer ?? null,
2231
+ utm: buildUtmPayload(eventContext, trackingAttributionTtlMs),
2232
+ clickIds: buildClickIdsPayload(eventContext, trackingAttributionTtlMs),
2233
+ device: {
2234
+ deviceType: eventContext.deviceType ?? "unknown",
2235
+ deviceOs: eventContext.deviceOs ?? null,
2236
+ deviceBrowser: eventContext.deviceBrowser ?? null
2237
+ },
2238
+ properties
2239
+ };
2240
+ }
2241
+ async function sendEventOutcome(event) {
2242
+ if (!endpoint) {
2243
+ return {
2244
+ result: err(new NetworkError("Invalid storefront endpoint")),
2245
+ retryDisposition: { retryable: true }
2246
+ };
2247
+ }
2248
+ const payload = {
2249
+ events: [event]
2250
+ };
2251
+ const body = JSON.stringify(payload);
2252
+ const keepalive = new Blob([body]).size <= KEEPALIVE_MAX_BODY_BYTES;
2253
+ try {
2254
+ const response = await fetch(endpoint, {
2255
+ method: "POST",
2256
+ headers: {
2257
+ "content-type": "application/json",
2258
+ "x-storefront-key": apiKey
2259
+ },
2260
+ body,
2261
+ ...keepalive ? { keepalive: true } : {}
2262
+ });
2263
+ if (!response.ok) {
2264
+ const message = response.headers.get("content-type")?.includes("application/json") ? await extractErrorMessage(response) : `Analytics ingest failed with status ${response.status}`;
2265
+ const networkError = new NetworkError(message);
2266
+ const nowMs = Date.now();
2267
+ const retryDisposition = response.status === 429 ? (() => {
2268
+ const notBefore = parseRetryAfterHeader(response, nowMs);
2269
+ return notBefore ? { retryable: true, notBefore } : { retryable: false };
2270
+ })() : response.status >= 500 ? { retryable: true } : { retryable: false };
2271
+ emitDiagnostic({
2272
+ target: "ingest",
2273
+ status: "error",
2274
+ event,
2275
+ error: networkError
2276
+ });
2277
+ return {
2278
+ result: err(networkError),
2279
+ retryDisposition
2280
+ };
2281
+ }
2282
+ let parsed;
2283
+ try {
2284
+ parsed = await response.json();
2285
+ } catch {
2286
+ emitDiagnostic({
2287
+ target: "ingest",
2288
+ status: "accepted_unknown",
2289
+ event
2290
+ });
2291
+ return {
2292
+ result: ok(ACCEPTED_UNKNOWN_RESPONSE),
2293
+ retryDisposition: { retryable: false }
2294
+ };
2295
+ }
2296
+ if (!isIngestResponse(parsed)) {
2297
+ const networkError = new NetworkError("Analytics response shape is invalid");
2298
+ emitDiagnostic({
2299
+ target: "ingest",
2300
+ status: "error",
2301
+ event,
2302
+ error: networkError
2303
+ });
2304
+ return {
2305
+ result: err(networkError),
2306
+ retryDisposition: { retryable: false }
2307
+ };
2308
+ }
2309
+ emitDiagnostic({
2310
+ target: "ingest",
2311
+ status: "success",
2312
+ event,
2313
+ response: {
2314
+ acceptedCount: parsed.acceptedCount,
2315
+ duplicateCount: parsed.duplicateCount,
2316
+ rejectedCount: parsed.rejectedCount
2317
+ }
2318
+ });
2319
+ return {
2320
+ result: ok(parsed),
2321
+ retryDisposition: { retryable: false }
2322
+ };
2323
+ } catch (error) {
2324
+ const networkError = new NetworkError("Failed to send analytics event", {
2325
+ cause: error instanceof Error ? error : void 0
2326
+ });
2327
+ emitDiagnostic({
2328
+ target: "ingest",
2329
+ status: "error",
2330
+ event,
2331
+ error: networkError
2332
+ });
2333
+ return {
2334
+ result: err(networkError),
2335
+ retryDisposition: { retryable: true }
2336
+ };
2337
+ }
2338
+ }
2339
+ async function track(eventName, eventPayload, context) {
2340
+ let eventContext = context;
2341
+ const normalized = resolveTrackEvent(eventName);
2342
+ let eventType;
2343
+ let properties;
2344
+ if (normalized.eventType !== "analytics.custom") {
2345
+ switch (normalized.eventType) {
2346
+ case "analytics.page_view": {
2347
+ eventType = "analytics.page_view";
2348
+ properties = {};
2349
+ eventContext = eventContext ?? (isPlainObject(eventPayload) ? eventPayload : void 0);
2350
+ break;
2351
+ }
2352
+ case "analytics.product_view": {
2353
+ if (!isPlainObject(eventPayload)) {
2354
+ return err(new NetworkError("productId is required"));
1873
2355
  }
1874
- options {
1875
- id
1876
- name
1877
- position
1878
- values {
1879
- id
1880
- value
1881
- position
1882
- }
2356
+ const payload = eventPayload;
2357
+ const decodedProductId = decodeAnalyticsEntityId(payload.productId);
2358
+ if (!decodedProductId) {
2359
+ return err(new NetworkError("Invalid productId"));
1883
2360
  }
1884
- variants {
1885
- id
1886
- title
1887
- sku
1888
- price
1889
- compareAtPrice
1890
- weight
1891
- weightUnit
1892
- requiresShipping
1893
- availableForSale
1894
- quantity
1895
- selectedOptions {
1896
- id
1897
- value
1898
- position
1899
- }
1900
- image {
1901
- id
1902
- url
1903
- altText
1904
- position
1905
- }
2361
+ const decodedVariantId = decodeAnalyticsEntityId(payload.variantId);
2362
+ eventType = "analytics.product_view";
2363
+ properties = { productId: decodedProductId, variantId: decodedVariantId };
2364
+ break;
2365
+ }
2366
+ case "analytics.collection_view": {
2367
+ if (!isPlainObject(eventPayload)) {
2368
+ return err(new NetworkError("collectionId is required"));
1906
2369
  }
1907
- categories {
1908
- id
1909
- name
1910
- handle
1911
- description
2370
+ const payload = eventPayload;
2371
+ const decodedCollectionId = decodeAnalyticsEntityId(payload.collectionId);
2372
+ if (!decodedCollectionId) {
2373
+ return err(new NetworkError("Invalid collectionId"));
1912
2374
  }
1913
- primaryCategory {
1914
- id
1915
- name
1916
- handle
2375
+ eventType = "analytics.collection_view";
2376
+ properties = { collectionId: decodedCollectionId };
2377
+ break;
2378
+ }
2379
+ case "analytics.search_performed": {
2380
+ if (!isPlainObject(eventPayload)) {
2381
+ return err(new NetworkError("query is required"));
1917
2382
  }
1918
- tags {
1919
- id
1920
- name
2383
+ const payload = eventPayload;
2384
+ const trimmed = payload.query.trim();
2385
+ if (!trimmed) {
2386
+ return err(new NetworkError("query is required"));
1921
2387
  }
2388
+ eventType = "analytics.search_performed";
2389
+ properties = { query: trimmed, resultsCount: Math.max(0, Math.floor(payload.resultsCount)) };
2390
+ break;
1922
2391
  }
1923
- cursor
1924
- }
1925
- pageInfo {
1926
- hasNextPage
1927
- hasPreviousPage
1928
- startCursor
1929
- endCursor
1930
- }
1931
- }
1932
- }
1933
- }
1934
- `;
1935
- const COLLECTION_PRODUCTS_BY_HANDLE_QUERY = `
1936
- query CollectionProductsByHandle($handle: String!, $first: Int, $after: String, $sort: CollectionSortOrder) {
1937
- collection(handle: $handle) {
1938
- id
1939
- products(first: $first, after: $after, sort: $sort) {
1940
- edges {
1941
- node {
1942
- id
1943
- handle
1944
- title
1945
- description
1946
- vendor
1947
- productType
1948
- metaTitle
1949
- metaDescription
1950
- publishedAt
1951
- createdAt
1952
- availableForSale
1953
- media {
1954
- id
1955
- url
1956
- altText
1957
- position
2392
+ case "analytics.add_to_cart": {
2393
+ if (!isPlainObject(eventPayload)) {
2394
+ return err(new NetworkError("Invalid cartId"));
2395
+ }
2396
+ const payload = eventPayload;
2397
+ const cartId = decodeAnalyticsEntityId(payload.cartId);
2398
+ if (!cartId) {
2399
+ return err(new NetworkError("Invalid cartId"));
2400
+ }
2401
+ eventType = "analytics.add_to_cart";
2402
+ properties = {
2403
+ cartId,
2404
+ quantity: Math.max(1, Math.floor(payload.quantity)),
2405
+ itemsCount: Math.max(0, Math.floor(payload.itemsCount)),
2406
+ cartValueCents: toCents(payload.cartValue)
2407
+ };
2408
+ break;
2409
+ }
2410
+ case "analytics.remove_from_cart": {
2411
+ if (!isPlainObject(eventPayload)) {
2412
+ return err(new NetworkError("Invalid cartId"));
2413
+ }
2414
+ const payload = eventPayload;
2415
+ const cartId = decodeAnalyticsEntityId(payload.cartId);
2416
+ if (!cartId) {
2417
+ return err(new NetworkError("Invalid cartId"));
2418
+ }
2419
+ eventType = "analytics.remove_from_cart";
2420
+ properties = {
2421
+ cartId,
2422
+ quantity: Math.max(1, Math.floor(payload.quantity)),
2423
+ itemsCount: Math.max(0, Math.floor(payload.itemsCount)),
2424
+ cartValueCents: toCents(payload.cartValue)
2425
+ };
2426
+ break;
2427
+ }
2428
+ case "analytics.checkout_started": {
2429
+ if (!isPlainObject(eventPayload)) {
2430
+ return err(new NetworkError("Invalid cartId"));
1958
2431
  }
1959
- options {
1960
- id
1961
- name
1962
- position
1963
- values {
1964
- id
1965
- value
1966
- position
1967
- }
2432
+ const payload = eventPayload;
2433
+ const decodedCartId = decodeAnalyticsEntityId(payload.cartId);
2434
+ if (!decodedCartId) {
2435
+ return err(new NetworkError("Invalid cartId"));
1968
2436
  }
1969
- variants {
1970
- id
1971
- title
1972
- sku
1973
- price
1974
- compareAtPrice
1975
- weight
1976
- weightUnit
1977
- requiresShipping
1978
- availableForSale
1979
- quantity
1980
- selectedOptions {
1981
- id
1982
- value
1983
- position
1984
- }
1985
- image {
1986
- id
1987
- url
1988
- altText
1989
- position
1990
- }
2437
+ eventType = "analytics.checkout_started";
2438
+ properties = { cartId: decodedCartId };
2439
+ break;
2440
+ }
2441
+ case "analytics.checkout_step_completed": {
2442
+ if (!isPlainObject(eventPayload)) {
2443
+ return err(new NetworkError("Invalid cartId"));
1991
2444
  }
1992
- categories {
1993
- id
1994
- name
1995
- handle
1996
- description
2445
+ const payload = eventPayload;
2446
+ const decodedCartId = decodeAnalyticsEntityId(payload.cartId);
2447
+ if (!decodedCartId) {
2448
+ return err(new NetworkError("Invalid cartId"));
1997
2449
  }
1998
- primaryCategory {
1999
- id
2000
- name
2001
- handle
2450
+ eventType = "analytics.checkout_step_completed";
2451
+ properties = { cartId: decodedCartId, step: payload.step };
2452
+ break;
2453
+ }
2454
+ case "analytics.checkout_completed": {
2455
+ if (!isPlainObject(eventPayload)) {
2456
+ return err(new NetworkError("Invalid orderId or cartId"));
2002
2457
  }
2003
- tags {
2004
- id
2005
- name
2458
+ const payload = eventPayload;
2459
+ const orderId = decodeAnalyticsEntityId(payload.orderId);
2460
+ const cartId = decodeAnalyticsEntityId(payload.cartId);
2461
+ if (!orderId || !cartId) {
2462
+ return err(new NetworkError("Invalid orderId or cartId"));
2006
2463
  }
2464
+ eventType = "analytics.checkout_completed";
2465
+ properties = {
2466
+ orderId,
2467
+ cartId,
2468
+ orderTotalCents: toCents(payload.orderTotal)
2469
+ };
2470
+ break;
2007
2471
  }
2008
- cursor
2009
- }
2010
- pageInfo {
2011
- hasNextPage
2012
- hasPreviousPage
2013
- startCursor
2014
- endCursor
2015
2472
  }
2473
+ } else {
2474
+ eventType = "analytics.custom";
2475
+ properties = {
2476
+ ...isPlainObject(eventPayload) ? eventPayload : {},
2477
+ eventName: normalized.customEventName ?? eventName
2478
+ };
2479
+ }
2480
+ const normalizedContext = normalizeEventContext(eventContext);
2481
+ const resolvedAnalytics = getResolvedAnalyticsConfig() ?? buildDefaultResolvedAnalyticsConfig(getTrackingRuntime());
2482
+ if (!resolvedAnalytics.enabled) {
2483
+ return ok(ACCEPTED_UNKNOWN_RESPONSE);
2484
+ }
2485
+ const trackingRuntime = getTrackingRuntime();
2486
+ const consentState = await resolveConsentState(normalizedContext.consentState);
2487
+ const event = buildCanonicalEvent(eventType, properties, normalizedContext, consentState);
2488
+ if (shouldMirrorEventToBrowserRuntime(event.eventType)) {
2489
+ dispatchEventToBrowserRuntime(trackingRuntime, event, resolvedAnalytics, emitDiagnostic, scheduleBestEffort);
2490
+ }
2491
+ const outcome = await sendEventOutcome(event);
2492
+ if (outcome.result.isErr() && outcome.retryDisposition.retryable) {
2493
+ retryQueue.enqueue({
2494
+ event,
2495
+ queuedAt: Date.now(),
2496
+ ...outcome.retryDisposition.notBefore !== void 0 ? { notBefore: outcome.retryDisposition.notBefore } : {}
2497
+ });
2498
+ }
2499
+ if (outcome.result.isOk() && retryQueue.read().length > 0) {
2500
+ flushRetryQueueInBackground();
2016
2501
  }
2502
+ return outcome.result;
2017
2503
  }
2504
+ return {
2505
+ init,
2506
+ track
2507
+ };
2018
2508
  }
2019
- `;
2020
- function isGlobalId$1(value) {
2021
- if (!value.includes(":")) {
2509
+ function dispatchConfirmedPurchaseBrowserEvent(client, purchaseTracking) {
2510
+ const trackingAttributionTtlMs = client.config.trackingAttributionTTL;
2511
+ const runtimeClient = client;
2512
+ const trackingRuntime = runtimeClient._analyticsRuntimeConfig ?? client.config.tracking ?? null;
2513
+ const resolvedAnalytics = runtimeClient._resolvedInitConfig?.analytics ?? buildDefaultResolvedAnalyticsConfig(trackingRuntime);
2514
+ if (!trackingRuntime || !resolvedAnalytics.enabled || (trackingRuntime.adapters?.length ?? 0) === 0) {
2515
+ return;
2516
+ }
2517
+ const runtime = trackingRuntime;
2518
+ function emitDiagnostic(diagnostic) {
2022
2519
  try {
2023
- const decoded = atob(value);
2024
- return decoded.includes(":");
2520
+ runtime.onDiagnostic?.(diagnostic);
2025
2521
  } catch {
2026
- return false;
2027
2522
  }
2028
2523
  }
2029
- return false;
2524
+ function scheduleBestEffort(task) {
2525
+ Promise.resolve().then(task).catch(() => {
2526
+ });
2527
+ }
2528
+ async function resolveConsentState() {
2529
+ if (!runtime.resolveConsentState) {
2530
+ return "unknown";
2531
+ }
2532
+ try {
2533
+ return await runtime.resolveConsentState();
2534
+ } catch (error) {
2535
+ emitDiagnostic({
2536
+ target: "consent",
2537
+ status: "error",
2538
+ error: toError(error, "Failed to resolve tracking consent state")
2539
+ });
2540
+ return "unknown";
2541
+ }
2542
+ }
2543
+ function buildLineItemsPayload(lineItems) {
2544
+ return lineItems.map((item) => ({
2545
+ id: item.contentId,
2546
+ content_id: item.contentId,
2547
+ productId: item.productId,
2548
+ variantId: item.variantId,
2549
+ sku: item.sku,
2550
+ productTitle: item.productTitle,
2551
+ variantTitle: item.variantTitle,
2552
+ quantity: item.quantity,
2553
+ unitPrice: item.unitPrice,
2554
+ unitPriceCents: toCents(item.unitPrice),
2555
+ lineTotal: item.lineTotal,
2556
+ lineTotalCents: toCents(item.lineTotal)
2557
+ }));
2558
+ }
2559
+ scheduleBestEffort(async () => {
2560
+ const normalizedContext = normalizeEventContext(void 0);
2561
+ const consentState = await resolveConsentState();
2562
+ const now = /* @__PURE__ */ new Date();
2563
+ const event = {
2564
+ schemaVersion: TRACKING_SCHEMA_VERSION,
2565
+ eventId: purchaseTracking.eventId,
2566
+ eventType: "analytics.checkout_completed",
2567
+ occurredAt: now.toISOString(),
2568
+ sessionId: resolveSessionId(now.getTime()),
2569
+ visitorId: getOrCreateVisitorId(),
2570
+ customerId: parseCustomerIdFromToken(client.getCustomerToken()),
2571
+ consentState,
2572
+ context: {
2573
+ schemaVersion: TRACKING_SCHEMA_VERSION,
2574
+ path: normalizePath(normalizedContext.path ?? "/")
2575
+ },
2576
+ referrer: normalizedContext.referrer ?? null,
2577
+ utm: buildUtmPayload(normalizedContext, trackingAttributionTtlMs),
2578
+ clickIds: buildClickIdsPayload(normalizedContext, trackingAttributionTtlMs),
2579
+ device: {
2580
+ deviceType: normalizedContext.deviceType ?? "unknown",
2581
+ deviceOs: normalizedContext.deviceOs ?? null,
2582
+ deviceBrowser: normalizedContext.deviceBrowser ?? null
2583
+ },
2584
+ properties: {
2585
+ orderId: purchaseTracking.orderId,
2586
+ cartId: purchaseTracking.cartId,
2587
+ orderTotalCents: toCents(purchaseTracking.total),
2588
+ totalCents: toCents(purchaseTracking.total),
2589
+ currency: purchaseTracking.currency,
2590
+ numItems: purchaseTracking.numItems,
2591
+ lineItems: buildLineItemsPayload(purchaseTracking.lineItems)
2592
+ }
2593
+ };
2594
+ dispatchEventToBrowserRuntime(runtime, event, resolvedAnalytics, emitDiagnostic, scheduleBestEffort);
2595
+ });
2596
+ }
2597
+ const CHECKOUT_PURCHASE_DISPATCH_IDS_KEY = "ekomerc_checkout_purchase_dispatch_ids";
2598
+ const MAX_STORED_PURCHASE_DISPATCH_IDS = 20;
2599
+ const CHECKOUT_START_MUTATION = `
2600
+ mutation CheckoutStart {
2601
+ checkoutStart {
2602
+ cart {
2603
+ ${CART_FRAGMENT}
2604
+ }
2605
+ userErrors {
2606
+ field
2607
+ message
2608
+ code
2609
+ }
2610
+ }
2611
+ }
2612
+ `;
2613
+ const CHECKOUT_UPDATE_MUTATION = `
2614
+ mutation CheckoutUpdate($input: CheckoutUpdateInput!) {
2615
+ checkoutUpdate(input: $input) {
2616
+ cart {
2617
+ ${CART_FRAGMENT}
2618
+ }
2619
+ userErrors {
2620
+ field
2621
+ message
2622
+ code
2623
+ }
2624
+ }
2625
+ }
2626
+ `;
2627
+ const CHECKOUT_CONVERT_MUTATION = `
2628
+ mutation CheckoutConvert {
2629
+ checkoutConvert {
2630
+ order {
2631
+ id
2632
+ orderNumber
2633
+ status
2634
+ customerEmail
2635
+ customerPhone
2636
+ shippingAddress
2637
+ billingAddress
2638
+ subtotal
2639
+ total
2640
+ note
2641
+ items {
2642
+ id
2643
+ productTitle
2644
+ variantTitle
2645
+ sku
2646
+ quantity
2647
+ unitPrice
2648
+ totalPrice
2649
+ }
2650
+ createdAt
2651
+ }
2652
+ paymentInstructions {
2653
+ bankAccount
2654
+ recipientName
2655
+ referenceNumber
2656
+ ipsQrCodeBase64
2657
+ expiresAt
2658
+ }
2659
+ purchaseTracking {
2660
+ eventId
2661
+ orderId
2662
+ cartId
2663
+ total
2664
+ currency
2665
+ numItems
2666
+ lineItems {
2667
+ contentId
2668
+ productId
2669
+ variantId
2670
+ sku
2671
+ productTitle
2672
+ variantTitle
2673
+ quantity
2674
+ unitPrice
2675
+ lineTotal
2676
+ }
2677
+ }
2678
+ userErrors {
2679
+ field
2680
+ message
2681
+ code
2682
+ }
2683
+ }
2684
+ }
2685
+ `;
2686
+ const CHECKOUT_ABANDON_MUTATION = `
2687
+ mutation CheckoutAbandon {
2688
+ checkoutAbandon {
2689
+ cart {
2690
+ ${CART_FRAGMENT}
2691
+ }
2692
+ userErrors {
2693
+ field
2694
+ message
2695
+ code
2696
+ }
2697
+ }
2698
+ }
2699
+ `;
2700
+ function mapOrderData(data, paymentInstructions) {
2701
+ return {
2702
+ id: data.id,
2703
+ orderNumber: data.orderNumber,
2704
+ email: data.customerEmail,
2705
+ phone: data.customerPhone,
2706
+ status: data.status,
2707
+ shippingAddress: data.shippingAddress,
2708
+ subtotal: data.subtotal,
2709
+ total: data.total,
2710
+ note: data.note,
2711
+ items: data.items.map((item) => ({
2712
+ id: item.id,
2713
+ productTitle: item.productTitle,
2714
+ variantTitle: item.variantTitle,
2715
+ sku: item.sku,
2716
+ quantity: item.quantity,
2717
+ unitPrice: item.unitPrice,
2718
+ totalPrice: item.totalPrice
2719
+ })),
2720
+ paymentInstructions: paymentInstructions ?? null,
2721
+ createdAt: data.createdAt
2722
+ };
2723
+ }
2724
+ function handleUserErrors(userErrors) {
2725
+ if (userErrors.length === 0) return null;
2726
+ const messages = userErrors.map((e) => e.message).join("; ");
2727
+ const stateErrorCodes = ["CART_NOT_IN_CHECKOUT", "CHECKOUT_START_ERROR", "CHECKOUT_ABANDON_ERROR"];
2728
+ const stateError = userErrors.find(
2729
+ (e) => e.code?.includes("STATE") || e.message.includes("state") || e.code && stateErrorCodes.includes(e.code)
2730
+ );
2731
+ if (stateError) {
2732
+ return new StateError(messages, "unknown");
2733
+ }
2734
+ return new ValidationError(
2735
+ messages,
2736
+ userErrors.map((e) => ({ field: e.field, message: e.message }))
2737
+ );
2738
+ }
2739
+ function checkCartIsInCheckout(status) {
2740
+ if (status !== "checkout") {
2741
+ return err(
2742
+ new StateError(`Cart must be in 'checkout' state for this operation. Current state: '${status}'.`, status)
2743
+ );
2744
+ }
2745
+ return ok(void 0);
2746
+ }
2747
+ function readHandledPurchaseDispatchIds(storage) {
2748
+ const raw = storage.get(CHECKOUT_PURCHASE_DISPATCH_IDS_KEY);
2749
+ if (!raw) {
2750
+ return [];
2751
+ }
2752
+ try {
2753
+ const parsed = JSON.parse(raw);
2754
+ if (!Array.isArray(parsed)) {
2755
+ return [];
2756
+ }
2757
+ return parsed.filter((value) => typeof value === "string" && value.length > 0);
2758
+ } catch {
2759
+ return [];
2760
+ }
2030
2761
  }
2031
- function createCollectionsOperations(client) {
2762
+ function hasHandledPurchaseDispatch(storage, eventId) {
2763
+ return readHandledPurchaseDispatchIds(storage).includes(eventId);
2764
+ }
2765
+ function markPurchaseDispatchHandled(storage, eventId) {
2766
+ const next = [eventId, ...readHandledPurchaseDispatchIds(storage).filter((value) => value !== eventId)].slice(
2767
+ 0,
2768
+ MAX_STORED_PURCHASE_DISPATCH_IDS
2769
+ );
2770
+ storage.set(CHECKOUT_PURCHASE_DISPATCH_IDS_KEY, JSON.stringify(next));
2771
+ }
2772
+ function createCheckoutOperations(client, storage) {
2032
2773
  return {
2033
- async list(options) {
2034
- const result = await client.query({
2035
- query: COLLECTIONS_QUERY,
2036
- variables: {
2037
- first: options?.first,
2038
- after: options?.after,
2039
- search: options?.search
2040
- }
2041
- });
2042
- if (result.isErr()) {
2043
- return err(result.error);
2774
+ async start() {
2775
+ const token = storage.get(CART_TOKEN_KEY);
2776
+ if (!token) {
2777
+ return err(new NotFoundError("No cart exists. Call cart.create() first."));
2044
2778
  }
2045
- const connection = result.value.collections;
2046
- return ok({
2047
- items: connection.edges.map((edge) => mapRawCollection(edge.node, client.config.endpoint)),
2048
- pageInfo: {
2049
- hasNextPage: connection.pageInfo.hasNextPage,
2050
- hasPreviousPage: connection.pageInfo.hasPreviousPage,
2051
- startCursor: connection.pageInfo.startCursor,
2052
- endCursor: connection.pageInfo.endCursor
2053
- }
2054
- });
2055
- },
2056
- async get(idOrHandle) {
2057
- const useId = isGlobalId$1(idOrHandle);
2058
- const result = await client.query({
2059
- query: useId ? COLLECTION_BY_ID_QUERY : COLLECTION_BY_HANDLE_QUERY,
2060
- variables: useId ? { id: idOrHandle } : { handle: idOrHandle }
2779
+ const result = await client.mutate({
2780
+ query: CHECKOUT_START_MUTATION
2061
2781
  });
2062
2782
  if (result.isErr()) {
2063
2783
  return err(result.error);
2064
2784
  }
2065
- if (!result.value.collection) {
2066
- return err(new NotFoundError(`Collection not found: ${idOrHandle}`));
2785
+ const payload = result.value.checkoutStart;
2786
+ const userError = handleUserErrors(payload.userErrors);
2787
+ if (userError) {
2788
+ return err(userError);
2067
2789
  }
2068
- const collection = mapRawCollection(result.value.collection, client.config.endpoint);
2069
- return ok(normalizeCollectionAssetUrls(collection, client.config.endpoint));
2070
- },
2071
- async getProducts(idOrHandle, options) {
2072
- const useId = isGlobalId$1(idOrHandle);
2073
- const result = await client.query({
2074
- query: useId ? COLLECTION_PRODUCTS_BY_ID_QUERY : COLLECTION_PRODUCTS_BY_HANDLE_QUERY,
2075
- variables: {
2076
- ...useId ? { id: idOrHandle } : { handle: idOrHandle },
2077
- first: options?.first,
2078
- after: options?.after,
2079
- ...options?.sort !== void 0 && { sort: options.sort }
2080
- }
2081
- });
2082
- if (result.isErr()) {
2083
- return err(result.error);
2790
+ if (!payload.cart) {
2791
+ return err(new NotFoundError("Cart not found"));
2084
2792
  }
2085
- if (!result.value.collection) {
2086
- return err(new NotFoundError(`Collection not found: ${idOrHandle}`));
2793
+ return ok(mapCartData(payload.cart, client.config.endpoint));
2794
+ },
2795
+ async update(data) {
2796
+ const token = storage.get(CART_TOKEN_KEY);
2797
+ if (!token) {
2798
+ return err(new NotFoundError("No cart exists. Call cart.create() first."));
2087
2799
  }
2088
- const connection = result.value.collection.products;
2089
- return ok({
2090
- items: connection.edges.map((edge) => normalizeProductAssetUrls(edge.node, client.config.endpoint)),
2091
- pageInfo: {
2092
- hasNextPage: connection.pageInfo.hasNextPage,
2093
- hasPreviousPage: connection.pageInfo.hasPreviousPage,
2094
- startCursor: connection.pageInfo.startCursor,
2095
- endCursor: connection.pageInfo.endCursor
2096
- }
2097
- });
2098
- }
2099
- };
2100
- }
2101
- function mapHttpError(status, body) {
2102
- if (status === 401 || status === 403) {
2103
- let message = "Authentication failed";
2104
- try {
2105
- const parsed = JSON.parse(body);
2106
- message = parsed.error ?? parsed.message ?? message;
2107
- } catch {
2108
- }
2109
- return new AuthError(message);
2110
- }
2111
- if (status === 404) {
2112
- return new NotFoundError("Resource not found");
2113
- }
2114
- return new GraphQLError(`HTTP error ${status}`, [{ message: body }]);
2115
- }
2116
- function mapGraphQLErrors(errors) {
2117
- const messages = errors.map((e) => e.message).join("; ");
2118
- return new GraphQLError(messages, errors);
2119
- }
2120
- function getCacheKey(request) {
2121
- return JSON.stringify({
2122
- query: request.query,
2123
- variables: request.variables ?? {},
2124
- operationName: request.operationName
2125
- });
2126
- }
2127
- function createGraphQLClient(config) {
2128
- async function execute(request) {
2129
- const headers = {
2130
- "Content-Type": "application/json",
2131
- "x-storefront-key": config.apiKey
2132
- };
2133
- const cartToken = config.getCartToken();
2134
- if (cartToken) {
2135
- headers["x-cart-token"] = cartToken;
2136
- }
2137
- const customerToken = config.getCustomerToken();
2138
- if (customerToken) {
2139
- headers["Authorization"] = `Bearer ${customerToken}`;
2140
- }
2141
- let response;
2142
- try {
2143
- response = await fetch(config.endpoint, {
2144
- method: "POST",
2145
- headers,
2146
- body: JSON.stringify({
2147
- query: request.query,
2148
- variables: request.variables,
2149
- operationName: request.operationName
2150
- })
2800
+ const input = {};
2801
+ if (data.email !== void 0) input.customerEmail = data.email;
2802
+ if (data.phone !== void 0) input.customerPhone = data.phone;
2803
+ if (data.shippingAddress !== void 0) input.shippingAddress = data.shippingAddress;
2804
+ if (data.billingAddress !== void 0) input.billingAddress = data.billingAddress;
2805
+ if (data.notes !== void 0) input.notes = data.notes;
2806
+ if (data.emailMarketingConsent !== void 0) input.emailMarketingConsent = data.emailMarketingConsent;
2807
+ if (data.paymentMethod !== void 0) input.paymentMethod = data.paymentMethod;
2808
+ if (data.shippingRateId !== void 0) input.shippingRateId = data.shippingRateId;
2809
+ const result = await client.mutate({
2810
+ query: CHECKOUT_UPDATE_MUTATION,
2811
+ variables: { input }
2151
2812
  });
2152
- } catch (error) {
2153
- const message = error instanceof Error ? error.message : "Network request failed";
2154
- return err(new NetworkError(message, { cause: error instanceof Error ? error : void 0 }));
2155
- }
2156
- if (!response.ok) {
2157
- const body = await response.text().catch(() => "");
2158
- return err(mapHttpError(response.status, body));
2159
- }
2160
- let json;
2161
- try {
2162
- json = await response.json();
2163
- } catch {
2164
- return err(new GraphQLError("Invalid JSON response", [{ message: "Failed to parse response" }]));
2165
- }
2166
- return ok(json);
2167
- }
2168
- return {
2169
- async query(request, options) {
2170
- const useCache = options?.cache !== false;
2171
- const cacheKey = getCacheKey(request);
2172
- if (useCache) {
2173
- const cached = config.cache.get(cacheKey);
2174
- if (cached !== null) {
2175
- return ok(cached);
2176
- }
2177
- }
2178
- const result = await execute(request);
2179
2813
  if (result.isErr()) {
2180
2814
  return err(result.error);
2181
2815
  }
2182
- const response = result.value;
2183
- if (response.errors && response.errors.length > 0) {
2184
- return err(mapGraphQLErrors(response.errors));
2816
+ const payload = result.value.checkoutUpdate;
2817
+ const userError = handleUserErrors(payload.userErrors);
2818
+ if (userError) {
2819
+ return err(userError);
2185
2820
  }
2186
- if (!response.data) {
2187
- return err(new GraphQLError("No data in response", [{ message: "Response has no data" }]));
2821
+ if (!payload.cart) {
2822
+ return err(new NotFoundError("Cart not found"));
2188
2823
  }
2189
- if (useCache) {
2190
- config.cache.set(cacheKey, response.data, config.cacheTTL);
2824
+ const stateCheck = checkCartIsInCheckout(payload.cart.status);
2825
+ if (stateCheck.isErr()) {
2826
+ return err(stateCheck.error);
2191
2827
  }
2192
- return ok(response.data);
2828
+ return ok(mapCartData(payload.cart, client.config.endpoint));
2193
2829
  },
2194
- async mutate(request) {
2195
- const result = await execute(request);
2830
+ async complete() {
2831
+ const token = storage.get(CART_TOKEN_KEY);
2832
+ if (!token) {
2833
+ return err(new NotFoundError("No cart exists. Call cart.create() first."));
2834
+ }
2835
+ const result = await client.mutate({
2836
+ query: CHECKOUT_CONVERT_MUTATION
2837
+ });
2196
2838
  if (result.isErr()) {
2197
2839
  return err(result.error);
2198
2840
  }
2199
- const response = result.value;
2200
- if (response.errors && response.errors.length > 0) {
2201
- return err(mapGraphQLErrors(response.errors));
2841
+ const payload = result.value.checkoutConvert;
2842
+ const userError = handleUserErrors(payload.userErrors);
2843
+ if (userError) {
2844
+ return err(userError);
2202
2845
  }
2203
- if (!response.data) {
2204
- return err(new GraphQLError("No data in response", [{ message: "Response has no data" }]));
2846
+ if (!payload.order) {
2847
+ return err(new NotFoundError("Order not found"));
2205
2848
  }
2206
- return ok(response.data);
2207
- }
2208
- };
2209
- }
2210
- function extractUserErrors(data, fieldName) {
2211
- const payload = data[fieldName];
2212
- if (!payload || typeof payload !== "object") {
2213
- return err(new ValidationError("Unexpected response format", []));
2214
- }
2215
- const typedPayload = payload;
2216
- if (typedPayload.userErrors && typedPayload.userErrors.length > 0) {
2217
- const messages = typedPayload.userErrors.map((e) => e.message).join("; ");
2218
- return err(new ValidationError(messages, typedPayload.userErrors));
2219
- }
2220
- return ok(payload);
2221
- }
2222
- const AVAILABLE_PAYMENT_METHODS_QUERY = `
2223
- query AvailablePaymentMethods {
2224
- availablePaymentMethods {
2225
- method
2226
- displayName
2227
- additionalFee
2228
- }
2229
- }
2230
- `;
2231
- function mapPaymentMethod(data) {
2232
- return {
2233
- method: data.method,
2234
- displayName: data.displayName,
2235
- additionalFee: data.additionalFee
2236
- };
2237
- }
2238
- function createPaymentsOperations(client) {
2239
- return {
2240
- async getAvailableMethods() {
2241
- const result = await client.query(
2242
- { query: AVAILABLE_PAYMENT_METHODS_QUERY },
2243
- { cache: true }
2244
- );
2849
+ storage.remove(CART_TOKEN_KEY);
2850
+ if (payload.purchaseTracking) {
2851
+ const confirmedPurchaseTracking = {
2852
+ ...payload.purchaseTracking,
2853
+ eventId: payload.purchaseTracking.orderId
2854
+ };
2855
+ if (!hasHandledPurchaseDispatch(storage, confirmedPurchaseTracking.eventId)) {
2856
+ markPurchaseDispatchHandled(storage, confirmedPurchaseTracking.eventId);
2857
+ dispatchConfirmedPurchaseBrowserEvent(client, confirmedPurchaseTracking);
2858
+ }
2859
+ }
2860
+ return ok(mapOrderData(payload.order, payload.paymentInstructions));
2861
+ },
2862
+ async abandon() {
2863
+ const token = storage.get(CART_TOKEN_KEY);
2864
+ if (!token) {
2865
+ return err(new NotFoundError("No cart exists. Call cart.create() first."));
2866
+ }
2867
+ const result = await client.mutate({
2868
+ query: CHECKOUT_ABANDON_MUTATION
2869
+ });
2245
2870
  if (result.isErr()) {
2246
2871
  return err(result.error);
2247
2872
  }
2248
- if (!result.value.availablePaymentMethods) {
2249
- return err(new NotFoundError("Payment methods not available"));
2873
+ const payload = result.value.checkoutAbandon;
2874
+ const userError = handleUserErrors(payload.userErrors);
2875
+ if (userError) {
2876
+ return err(userError);
2250
2877
  }
2251
- return ok(result.value.availablePaymentMethods.map(mapPaymentMethod));
2878
+ if (!payload.cart) {
2879
+ return err(new NotFoundError("Cart not found"));
2880
+ }
2881
+ return ok(mapCartData(payload.cart, client.config.endpoint));
2252
2882
  }
2253
2883
  };
2254
2884
  }
2255
- const PRODUCTS_QUERY = `
2256
- query Products($first: Int, $after: String, $filter: ProductFilter, $sort: ProductSortKey) {
2257
- products(first: $first, after: $after, filter: $filter, sort: $sort) {
2885
+ function mapRawCollection(raw, endpoint) {
2886
+ return {
2887
+ id: raw.id,
2888
+ handle: raw.handle,
2889
+ title: raw.title,
2890
+ description: raw.description,
2891
+ type: raw.type,
2892
+ sortOrder: raw.sortOrder,
2893
+ metaTitle: raw.metaTitle,
2894
+ metaDescription: raw.metaDescription,
2895
+ productCount: raw.productCount,
2896
+ imageAsset: raw.imageAsset ? {
2897
+ url: resolveAssetUrl(raw.imageAsset.url, endpoint),
2898
+ altText: raw.imageAsset.altText,
2899
+ width: raw.imageAsset.width,
2900
+ height: raw.imageAsset.height
2901
+ } : null
2902
+ };
2903
+ }
2904
+ const COLLECTIONS_QUERY = `
2905
+ query Collections($first: Int, $after: String, $search: String) {
2906
+ collections(first: $first, after: $after, search: $search) {
2258
2907
  edges {
2259
2908
  node {
2260
2909
  id
2261
2910
  handle
2262
2911
  title
2263
2912
  description
2264
- vendor
2265
- productType
2913
+ type
2914
+ sortOrder
2266
2915
  metaTitle
2267
2916
  metaDescription
2268
- publishedAt
2269
- createdAt
2270
- availableForSale
2271
- media {
2272
- id
2917
+ productCount
2918
+ imageAsset {
2273
2919
  url
2274
2920
  altText
2275
- position
2276
- }
2277
- options {
2278
- id
2279
- name
2280
- position
2281
- values {
2282
- id
2283
- value
2284
- position
2285
- }
2286
- }
2287
- variants {
2288
- id
2289
- title
2290
- sku
2291
- price
2292
- compareAtPrice
2293
- weight
2294
- weightUnit
2295
- requiresShipping
2296
- availableForSale
2297
- quantity
2298
- selectedOptions {
2299
- id
2300
- value
2301
- position
2302
- }
2303
- image {
2304
- id
2305
- url
2306
- altText
2307
- position
2308
- }
2309
- isOnSale
2310
- quantityPricing {
2311
- minQuantity
2312
- price
2313
- }
2314
- }
2315
- categories {
2316
- id
2317
- name
2318
- handle
2319
- description
2320
- }
2321
- primaryCategory {
2322
- id
2323
- name
2324
- handle
2325
- }
2326
- tags {
2327
- id
2328
- name
2921
+ width
2922
+ height
2329
2923
  }
2330
2924
  }
2331
2925
  cursor
2332
2926
  }
2333
2927
  pageInfo {
2334
2928
  hasNextPage
2335
- hasPreviousPage
2336
- startCursor
2337
- endCursor
2338
- }
2339
- }
2340
- }
2341
- `;
2342
- const PRODUCT_BY_ID_QUERY = `
2343
- query ProductById($id: ID!) {
2344
- product(id: $id) {
2345
- id
2346
- handle
2347
- title
2348
- description
2349
- vendor
2350
- productType
2351
- metaTitle
2352
- metaDescription
2353
- publishedAt
2354
- createdAt
2355
- availableForSale
2356
- media {
2357
- id
2358
- url
2359
- altText
2360
- position
2361
- }
2362
- options {
2363
- id
2364
- name
2365
- position
2366
- values {
2367
- id
2368
- value
2369
- position
2370
- }
2371
- }
2372
- variants {
2373
- id
2374
- title
2375
- sku
2376
- price
2377
- compareAtPrice
2378
- weight
2379
- weightUnit
2380
- requiresShipping
2381
- availableForSale
2382
- quantity
2383
- selectedOptions {
2384
- id
2385
- value
2386
- position
2387
- }
2388
- image {
2389
- id
2390
- url
2391
- altText
2392
- position
2393
- }
2394
- isOnSale
2395
- quantityPricing {
2396
- minQuantity
2397
- price
2398
- }
2399
- }
2400
- categories {
2401
- id
2402
- name
2403
- handle
2404
- description
2405
- }
2406
- tags {
2407
- id
2408
- name
2409
- }
2410
- }
2411
- }
2412
- `;
2413
- const PRODUCT_BY_HANDLE_QUERY = `
2414
- query ProductByHandle($handle: String!) {
2415
- product(handle: $handle) {
2416
- id
2417
- handle
2418
- title
2419
- description
2420
- vendor
2421
- productType
2422
- metaTitle
2423
- metaDescription
2424
- publishedAt
2425
- createdAt
2426
- availableForSale
2427
- media {
2428
- id
2429
- url
2430
- altText
2431
- position
2432
- }
2433
- options {
2434
- id
2435
- name
2436
- position
2437
- values {
2438
- id
2439
- value
2440
- position
2441
- }
2442
- }
2443
- variants {
2444
- id
2445
- title
2446
- sku
2447
- price
2448
- compareAtPrice
2449
- weight
2450
- weightUnit
2451
- requiresShipping
2452
- availableForSale
2453
- quantity
2454
- selectedOptions {
2455
- id
2456
- value
2457
- position
2458
- }
2459
- image {
2460
- id
2461
- url
2462
- altText
2463
- position
2464
- }
2465
- isOnSale
2466
- quantityPricing {
2467
- minQuantity
2468
- price
2469
- }
2470
- }
2471
- categories {
2472
- id
2473
- name
2474
- handle
2475
- description
2476
- }
2477
- tags {
2478
- id
2479
- name
2929
+ hasPreviousPage
2930
+ startCursor
2931
+ endCursor
2480
2932
  }
2481
2933
  }
2482
2934
  }
2483
2935
  `;
2484
- const PRODUCTS_BY_HANDLES_QUERY = `
2485
- query ProductsByHandles($handles: [String!]!) {
2486
- productsByHandles(handles: $handles) {
2936
+ const COLLECTION_BY_ID_QUERY = `
2937
+ query CollectionById($id: GID!) {
2938
+ collection(id: $id) {
2487
2939
  id
2488
2940
  handle
2489
2941
  title
2490
2942
  description
2491
- vendor
2492
- productType
2943
+ type
2944
+ sortOrder
2493
2945
  metaTitle
2494
2946
  metaDescription
2495
- publishedAt
2496
- createdAt
2497
- availableForSale
2498
- media {
2499
- id
2947
+ productCount
2948
+ imageAsset {
2500
2949
  url
2501
2950
  altText
2502
- position
2951
+ width
2952
+ height
2503
2953
  }
2504
- options {
2505
- id
2506
- name
2507
- position
2508
- values {
2509
- id
2510
- value
2511
- position
2512
- }
2954
+ }
2955
+ }
2956
+ `;
2957
+ const COLLECTION_BY_HANDLE_QUERY = `
2958
+ query CollectionByHandle($handle: String!) {
2959
+ collection(handle: $handle) {
2960
+ id
2961
+ handle
2962
+ title
2963
+ description
2964
+ type
2965
+ sortOrder
2966
+ metaTitle
2967
+ metaDescription
2968
+ productCount
2969
+ imageAsset {
2970
+ url
2971
+ altText
2972
+ width
2973
+ height
2513
2974
  }
2514
- variants {
2515
- id
2516
- title
2517
- sku
2518
- price
2519
- compareAtPrice
2520
- weight
2521
- weightUnit
2522
- requiresShipping
2523
- availableForSale
2524
- quantity
2525
- selectedOptions {
2526
- id
2527
- value
2528
- position
2529
- }
2530
- image {
2531
- id
2532
- url
2533
- altText
2534
- position
2975
+ }
2976
+ }
2977
+ `;
2978
+ const COLLECTION_PRODUCTS_BY_ID_QUERY = `
2979
+ query CollectionProductsById($id: GID!, $first: Int, $after: String, $sort: CollectionSortOrder) {
2980
+ collection(id: $id) {
2981
+ id
2982
+ products(first: $first, after: $after, sort: $sort) {
2983
+ edges {
2984
+ node {
2985
+ id
2986
+ handle
2987
+ title
2988
+ description
2989
+ vendor
2990
+ productType
2991
+ metaTitle
2992
+ metaDescription
2993
+ publishedAt
2994
+ createdAt
2995
+ availableForSale
2996
+ media {
2997
+ id
2998
+ url
2999
+ altText
3000
+ position
3001
+ }
3002
+ options {
3003
+ id
3004
+ name
3005
+ position
3006
+ values {
3007
+ id
3008
+ value
3009
+ position
3010
+ }
3011
+ }
3012
+ variants {
3013
+ id
3014
+ title
3015
+ sku
3016
+ price
3017
+ compareAtPrice
3018
+ weight
3019
+ weightUnit
3020
+ requiresShipping
3021
+ availableForSale
3022
+ quantity
3023
+ selectedOptions {
3024
+ id
3025
+ value
3026
+ position
3027
+ }
3028
+ image {
3029
+ id
3030
+ url
3031
+ altText
3032
+ position
3033
+ }
3034
+ }
3035
+ categories {
3036
+ id
3037
+ name
3038
+ handle
3039
+ description
3040
+ }
3041
+ primaryCategory {
3042
+ id
3043
+ name
3044
+ handle
3045
+ }
3046
+ tags {
3047
+ id
3048
+ name
3049
+ }
3050
+ }
3051
+ cursor
2535
3052
  }
2536
- isOnSale
2537
- quantityPricing {
2538
- minQuantity
2539
- price
3053
+ pageInfo {
3054
+ hasNextPage
3055
+ hasPreviousPage
3056
+ startCursor
3057
+ endCursor
2540
3058
  }
2541
3059
  }
2542
- categories {
2543
- id
2544
- name
2545
- handle
2546
- description
2547
- }
2548
- tags {
2549
- id
2550
- name
2551
- }
2552
3060
  }
2553
3061
  }
2554
3062
  `;
2555
- function isGlobalId(value) {
2556
- if (!value.includes(":")) {
2557
- try {
2558
- const decoded = atob(value);
2559
- return decoded.includes(":");
2560
- } catch {
2561
- return false;
3063
+ const COLLECTION_PRODUCTS_BY_HANDLE_QUERY = `
3064
+ query CollectionProductsByHandle($handle: String!, $first: Int, $after: String, $sort: CollectionSortOrder) {
3065
+ collection(handle: $handle) {
3066
+ id
3067
+ products(first: $first, after: $after, sort: $sort) {
3068
+ edges {
3069
+ node {
3070
+ id
3071
+ handle
3072
+ title
3073
+ description
3074
+ vendor
3075
+ productType
3076
+ metaTitle
3077
+ metaDescription
3078
+ publishedAt
3079
+ createdAt
3080
+ availableForSale
3081
+ media {
3082
+ id
3083
+ url
3084
+ altText
3085
+ position
3086
+ }
3087
+ options {
3088
+ id
3089
+ name
3090
+ position
3091
+ values {
3092
+ id
3093
+ value
3094
+ position
3095
+ }
3096
+ }
3097
+ variants {
3098
+ id
3099
+ title
3100
+ sku
3101
+ price
3102
+ compareAtPrice
3103
+ weight
3104
+ weightUnit
3105
+ requiresShipping
3106
+ availableForSale
3107
+ quantity
3108
+ selectedOptions {
3109
+ id
3110
+ value
3111
+ position
3112
+ }
3113
+ image {
3114
+ id
3115
+ url
3116
+ altText
3117
+ position
3118
+ }
3119
+ }
3120
+ categories {
3121
+ id
3122
+ name
3123
+ handle
3124
+ description
3125
+ }
3126
+ primaryCategory {
3127
+ id
3128
+ name
3129
+ handle
3130
+ }
3131
+ tags {
3132
+ id
3133
+ name
3134
+ }
3135
+ }
3136
+ cursor
3137
+ }
3138
+ pageInfo {
3139
+ hasNextPage
3140
+ hasPreviousPage
3141
+ startCursor
3142
+ endCursor
3143
+ }
2562
3144
  }
2563
3145
  }
2564
- return false;
2565
3146
  }
2566
- function createProductsOperations(client) {
3147
+ `;
3148
+ function createCollectionsOperations(client) {
2567
3149
  return {
2568
3150
  async list(options) {
2569
3151
  const result = await client.query({
2570
- query: PRODUCTS_QUERY,
3152
+ query: COLLECTIONS_QUERY,
2571
3153
  variables: {
2572
3154
  first: options?.first,
2573
3155
  after: options?.after,
2574
- filter: options?.filter,
2575
- sort: options?.sort
3156
+ search: options?.search
2576
3157
  }
2577
3158
  });
2578
3159
  if (result.isErr()) {
2579
3160
  return err(result.error);
2580
3161
  }
2581
- const connection = result.value.products;
3162
+ const connection = result.value.collections;
2582
3163
  return ok({
2583
- items: connection.edges.map((edge) => normalizeProductAssetUrls(edge.node, client.config.endpoint)),
3164
+ items: connection.edges.map((edge) => mapRawCollection(edge.node, client.config.endpoint)),
2584
3165
  pageInfo: {
2585
3166
  hasNextPage: connection.pageInfo.hasNextPage,
2586
3167
  hasPreviousPage: connection.pageInfo.hasPreviousPage,
@@ -2592,547 +3173,624 @@ function createProductsOperations(client) {
2592
3173
  async get(idOrHandle) {
2593
3174
  const useId = isGlobalId(idOrHandle);
2594
3175
  const result = await client.query({
2595
- query: useId ? PRODUCT_BY_ID_QUERY : PRODUCT_BY_HANDLE_QUERY,
3176
+ query: useId ? COLLECTION_BY_ID_QUERY : COLLECTION_BY_HANDLE_QUERY,
2596
3177
  variables: useId ? { id: idOrHandle } : { handle: idOrHandle }
2597
3178
  });
2598
3179
  if (result.isErr()) {
2599
3180
  return err(result.error);
2600
3181
  }
2601
- if (!result.value.product) {
2602
- return err(new NotFoundError(`Product not found: ${idOrHandle}`));
3182
+ if (!result.value.collection) {
3183
+ return err(new NotFoundError(`Collection not found: ${idOrHandle}`));
2603
3184
  }
2604
- return ok(normalizeProductAssetUrls(result.value.product, client.config.endpoint));
3185
+ const collection = mapRawCollection(result.value.collection, client.config.endpoint);
3186
+ return ok(normalizeCollectionAssetUrls(collection, client.config.endpoint));
2605
3187
  },
2606
- async getByHandles(handles) {
2607
- if (handles.length === 0) {
2608
- return ok([]);
2609
- }
3188
+ async getProducts(idOrHandle, options) {
3189
+ const useId = isGlobalId(idOrHandle);
2610
3190
  const result = await client.query({
2611
- query: PRODUCTS_BY_HANDLES_QUERY,
2612
- variables: { handles }
3191
+ query: useId ? COLLECTION_PRODUCTS_BY_ID_QUERY : COLLECTION_PRODUCTS_BY_HANDLE_QUERY,
3192
+ variables: {
3193
+ ...useId ? { id: idOrHandle } : { handle: idOrHandle },
3194
+ first: options?.first,
3195
+ after: options?.after,
3196
+ ...options?.sort !== void 0 && { sort: options.sort }
3197
+ }
2613
3198
  });
2614
3199
  if (result.isErr()) {
2615
3200
  return err(result.error);
2616
3201
  }
2617
- return ok(
2618
- result.value.productsByHandles.map(
2619
- (product) => product ? normalizeProductAssetUrls(product, client.config.endpoint) : null
2620
- )
2621
- );
3202
+ if (!result.value.collection) {
3203
+ return err(new NotFoundError(`Collection not found: ${idOrHandle}`));
3204
+ }
3205
+ const connection = result.value.collection.products;
3206
+ return ok({
3207
+ items: connection.edges.map((edge) => normalizeProductAssetUrls(edge.node, client.config.endpoint)),
3208
+ pageInfo: {
3209
+ hasNextPage: connection.pageInfo.hasNextPage,
3210
+ hasPreviousPage: connection.pageInfo.hasPreviousPage,
3211
+ startCursor: connection.pageInfo.startCursor,
3212
+ endCursor: connection.pageInfo.endCursor
3213
+ }
3214
+ });
2622
3215
  }
2623
3216
  };
2624
3217
  }
2625
- const ANALYTICS_PATH = "/analytics/ingest";
2626
- const VISITOR_COOKIE_NAME = "srb_vid";
2627
- const SESSION_STORAGE_KEY = "srb_sid";
2628
- const SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
2629
- const TRACKING_SCHEMA_VERSION = 1;
2630
- const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2631
- const ANALYTICS_PRESET_EVENT_MAP = {
2632
- page_view: "analytics.page_view",
2633
- product_view: "analytics.product_view",
2634
- collection_view: "analytics.collection_view",
2635
- search_performed: "analytics.search_performed",
2636
- add_to_cart: "analytics.add_to_cart",
2637
- remove_from_cart: "analytics.remove_from_cart",
2638
- checkout_started: "analytics.checkout_started",
2639
- checkout_step_completed: "analytics.checkout_step_completed",
2640
- checkout_completed: "analytics.checkout_completed"
2641
- };
2642
- function hasBrowserContext() {
2643
- return typeof window !== "undefined" && typeof document !== "undefined";
2644
- }
2645
- function getDocument() {
2646
- return hasBrowserContext() ? document : null;
2647
- }
2648
- function getWindow() {
2649
- return hasBrowserContext() ? window : null;
2650
- }
2651
- function isUuid(value) {
2652
- return UUID_REGEX.test(value);
2653
- }
2654
- function randomUuid() {
2655
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
2656
- return crypto.randomUUID();
2657
- }
2658
- const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
2659
- return template.replace(/[xy]/g, (character) => {
2660
- const random = Math.floor(Math.random() * 16);
2661
- const value = character === "x" ? random : random & 3 | 8;
2662
- return value.toString(16);
2663
- });
2664
- }
2665
- function getOrCreateVisitorId() {
2666
- const doc = getDocument();
2667
- if (doc) {
2668
- const raw = doc.cookie;
2669
- for (const pair of raw.split(";")) {
2670
- const [name, ...valueParts] = pair.trim().split("=");
2671
- if (name === VISITOR_COOKIE_NAME) {
2672
- return valueParts.join("=") ? decodeURIComponent(valueParts.join("=")) : randomUuid();
2673
- }
3218
+ function mapHttpError(status, body) {
3219
+ if (status === 401 || status === 403) {
3220
+ let message = "Authentication failed";
3221
+ try {
3222
+ const parsed = JSON.parse(body);
3223
+ message = parsed.error ?? parsed.message ?? message;
3224
+ } catch {
2674
3225
  }
3226
+ return new AuthError(message);
2675
3227
  }
2676
- const value = randomUuid();
2677
- if (doc) {
2678
- const maxAge = 60 * 60 * 24 * 365 * 2;
2679
- doc.cookie = `${VISITOR_COOKIE_NAME}=${encodeURIComponent(value)}; Max-Age=${maxAge}; Path=/; SameSite=Lax`;
2680
- }
2681
- return value;
2682
- }
2683
- let fallbackSessionState = null;
2684
- function getSessionStorageState() {
2685
- const win = getWindow();
2686
- if (!win) return fallbackSessionState;
2687
- try {
2688
- const raw = win.localStorage.getItem(SESSION_STORAGE_KEY);
2689
- if (!raw) return fallbackSessionState;
2690
- const parsed = JSON.parse(raw);
2691
- if (typeof parsed !== "object" || parsed === null) return null;
2692
- const id = parsed.id;
2693
- const startedAt = parsed.startedAt;
2694
- const lastSeenAt = parsed.lastSeenAt;
2695
- if (typeof id !== "string" || !id) return null;
2696
- if (typeof startedAt !== "number" || typeof lastSeenAt !== "number") return null;
2697
- return { id, startedAt, lastSeenAt };
2698
- } catch {
2699
- return fallbackSessionState;
2700
- }
2701
- }
2702
- function setSessionStorageState(state) {
2703
- fallbackSessionState = state;
2704
- const win = getWindow();
2705
- if (!win) return;
2706
- try {
2707
- win.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state));
2708
- } catch {
2709
- }
2710
- }
2711
- function resolveSessionState(existing, nowMs) {
2712
- if (!existing) {
2713
- return { id: randomUuid(), startedAt: nowMs, lastSeenAt: nowMs };
2714
- }
2715
- if (nowMs - existing.lastSeenAt > SESSION_TIMEOUT_MS) {
2716
- return { id: randomUuid(), startedAt: nowMs, lastSeenAt: nowMs };
3228
+ if (status === 404) {
3229
+ return new NotFoundError("Resource not found");
2717
3230
  }
2718
- return { id: existing.id, startedAt: existing.startedAt, lastSeenAt: nowMs };
2719
- }
2720
- function getSessionId(nowMs) {
2721
- const existing = getSessionStorageState();
2722
- const state = resolveSessionState(existing, nowMs);
2723
- setSessionStorageState(state);
2724
- return state.id;
2725
- }
2726
- function normalizeNumber(value) {
2727
- return Number.isFinite(value) ? value : 0;
3231
+ return new GraphQLError(`HTTP error ${status}`, [{ message: body }]);
2728
3232
  }
2729
- function toCents(amount) {
2730
- return Math.max(0, Math.round(normalizeNumber(amount) * 100));
3233
+ function mapGraphQLErrors(errors) {
3234
+ const messages = errors.map((e) => e.message).join("; ");
3235
+ return new GraphQLError(messages, errors);
2731
3236
  }
2732
- function base64UrlDecode(value) {
2733
- const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
2734
- const padding = normalized.length % 4;
2735
- const padded = padding === 0 ? normalized : normalized + "=".repeat(4 - padding);
2736
- return atob(padded);
3237
+ function getCacheKey(request) {
3238
+ return JSON.stringify({
3239
+ query: request.query,
3240
+ variables: request.variables ?? {},
3241
+ operationName: request.operationName
3242
+ });
2737
3243
  }
2738
- function parseJWT(payload) {
2739
- try {
2740
- const decoded = base64UrlDecode(payload);
2741
- const parsed = JSON.parse(decoded);
2742
- return typeof parsed === "object" && parsed !== null ? parsed : null;
2743
- } catch {
2744
- return null;
3244
+ function createGraphQLClient(config) {
3245
+ async function execute(request) {
3246
+ const headers = {
3247
+ "Content-Type": "application/json",
3248
+ "x-storefront-key": config.apiKey
3249
+ };
3250
+ const cartToken = config.getCartToken();
3251
+ if (cartToken) {
3252
+ headers["x-cart-token"] = cartToken;
3253
+ }
3254
+ const customerToken = config.getCustomerToken();
3255
+ if (customerToken) {
3256
+ headers["Authorization"] = `Bearer ${customerToken}`;
3257
+ }
3258
+ let response;
3259
+ try {
3260
+ response = await fetch(config.endpoint, {
3261
+ method: "POST",
3262
+ headers,
3263
+ body: JSON.stringify({
3264
+ query: request.query,
3265
+ variables: request.variables,
3266
+ operationName: request.operationName
3267
+ })
3268
+ });
3269
+ } catch (error) {
3270
+ const message = error instanceof Error ? error.message : "Network request failed";
3271
+ return err(new NetworkError(message, { cause: error instanceof Error ? error : void 0 }));
3272
+ }
3273
+ if (!response.ok) {
3274
+ const body = await response.text().catch(() => "");
3275
+ return err(mapHttpError(response.status, body));
3276
+ }
3277
+ let json;
3278
+ try {
3279
+ json = await response.json();
3280
+ } catch {
3281
+ return err(new GraphQLError("Invalid JSON response", [{ message: "Failed to parse response" }]));
3282
+ }
3283
+ return ok(json);
2745
3284
  }
3285
+ return {
3286
+ async query(request, options) {
3287
+ const useCache = options?.cache !== false;
3288
+ const cacheKey = getCacheKey(request);
3289
+ if (useCache) {
3290
+ const cached = config.cache.get(cacheKey);
3291
+ if (cached !== null) {
3292
+ return ok(cached);
3293
+ }
3294
+ }
3295
+ const result = await execute(request);
3296
+ if (result.isErr()) {
3297
+ return err(result.error);
3298
+ }
3299
+ const response = result.value;
3300
+ if (response.errors && response.errors.length > 0) {
3301
+ return err(mapGraphQLErrors(response.errors));
3302
+ }
3303
+ if (!response.data) {
3304
+ return err(new GraphQLError("No data in response", [{ message: "Response has no data" }]));
3305
+ }
3306
+ if (useCache) {
3307
+ config.cache.set(cacheKey, response.data, config.cacheTTL);
3308
+ }
3309
+ return ok(response.data);
3310
+ },
3311
+ async mutate(request) {
3312
+ const result = await execute(request);
3313
+ if (result.isErr()) {
3314
+ return err(result.error);
3315
+ }
3316
+ const response = result.value;
3317
+ if (response.errors && response.errors.length > 0) {
3318
+ return err(mapGraphQLErrors(response.errors));
3319
+ }
3320
+ if (!response.data) {
3321
+ return err(new GraphQLError("No data in response", [{ message: "Response has no data" }]));
3322
+ }
3323
+ return ok(response.data);
3324
+ }
3325
+ };
2746
3326
  }
2747
- function parseCustomerIdFromToken(token) {
2748
- if (!token) return null;
2749
- const payload = token.split(".");
2750
- const encodedPayload = payload[1];
2751
- if (!encodedPayload) return null;
2752
- const parsed = parseJWT(encodedPayload);
2753
- if (!parsed) return null;
2754
- const customerId = parsed.customerId;
2755
- return typeof customerId === "string" && isUuid(customerId) ? customerId : null;
2756
- }
2757
- function decodeAnalyticsEntityId(value) {
2758
- if (!value) return null;
2759
- if (isUuid(value)) return value;
2760
- if (value.startsWith("gid://")) {
2761
- const parts = value.split("/");
2762
- const candidate = parts[parts.length - 1];
2763
- return candidate && isUuid(candidate) ? candidate : null;
3327
+ function extractUserErrors(data, fieldName) {
3328
+ const payload = data[fieldName];
3329
+ if (!payload || typeof payload !== "object") {
3330
+ return err(new ValidationError("Unexpected response format", []));
2764
3331
  }
2765
- try {
2766
- const decoded = atob(value);
2767
- const parts = decoded.split(":");
2768
- const candidate = parts[parts.length - 1];
2769
- return candidate && isUuid(candidate) ? candidate : null;
2770
- } catch {
2771
- return null;
3332
+ const typedPayload = payload;
3333
+ if (typedPayload.userErrors && typedPayload.userErrors.length > 0) {
3334
+ const messages = typedPayload.userErrors.map((e) => e.message).join("; ");
3335
+ return err(new ValidationError(messages, typedPayload.userErrors));
2772
3336
  }
3337
+ return ok(payload);
2773
3338
  }
2774
- function resolveDeviceInfo() {
2775
- const win = getWindow();
2776
- if (!win) {
2777
- return { deviceType: "server", deviceOs: null, deviceBrowser: null };
2778
- }
2779
- const ua = win.navigator.userAgent.toLowerCase();
2780
- const deviceType = /ipad|tablet|playbook|silk/.test(ua) ? "tablet" : /mobi|android|iphone|ipod|windows phone/.test(ua) ? "mobile" : "desktop";
2781
- let deviceOs = null;
2782
- if (ua.includes("windows")) {
2783
- deviceOs = "Windows";
2784
- } else if (ua.includes("mac os") || ua.includes("macintosh")) {
2785
- deviceOs = "macOS";
2786
- } else if (ua.includes("android")) {
2787
- deviceOs = "Android";
2788
- } else if (ua.includes("iphone") || ua.includes("ipad") || ua.includes("ios")) {
2789
- deviceOs = "iOS";
2790
- } else if (ua.includes("linux")) {
2791
- deviceOs = "Linux";
2792
- }
2793
- let deviceBrowser = null;
2794
- if (ua.includes("edg/")) {
2795
- deviceBrowser = "Edge";
2796
- } else if (ua.includes("firefox/")) {
2797
- deviceBrowser = "Firefox";
2798
- } else if (ua.includes("chrome/") && !ua.includes("edg/")) {
2799
- deviceBrowser = "Chrome";
2800
- } else if (ua.includes("safari/") && !ua.includes("chrome/")) {
2801
- deviceBrowser = "Safari";
3339
+ const AVAILABLE_PAYMENT_METHODS_QUERY = `
3340
+ query AvailablePaymentMethods {
3341
+ availablePaymentMethods {
3342
+ method
3343
+ displayName
3344
+ additionalFee
2802
3345
  }
2803
- return { deviceType, deviceOs, deviceBrowser };
2804
- }
2805
- function parseStoreSlugFromBase(basePath) {
2806
- if (!basePath) return null;
2807
- const segments = basePath.split("/").filter(Boolean);
2808
- if (segments.length < 2) return null;
2809
- const [mode, slug] = segments;
2810
- if (mode !== "preview" && mode !== "live") return null;
2811
- return slug ?? null;
2812
- }
2813
- function parseStoreSlugFromHostname(hostname) {
2814
- if (hostname === "localhost") return null;
2815
- if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) return null;
2816
- const [candidate] = hostname.split(".");
2817
- if (!candidate || candidate === "www") return null;
2818
- return candidate;
2819
- }
2820
- function normalizePath(pathname) {
2821
- return pathname.trim().length > 0 ? pathname : "/";
2822
3346
  }
2823
- function toIsoTimestamp(value) {
2824
- if (value == null) return null;
2825
- const date = value instanceof Date ? value : new Date(value);
2826
- return Number.isNaN(date.getTime()) ? null : date.toISOString();
2827
- }
2828
- function buildUtmPayload(context) {
2829
- const persisted = getTrackingAttributionSnapshot();
3347
+ `;
3348
+ function mapPaymentMethod(data) {
2830
3349
  return {
2831
- schemaVersion: TRACKING_SCHEMA_VERSION,
2832
- source: context.utmSource ?? persisted?.utm.source ?? null,
2833
- medium: context.utmMedium ?? persisted?.utm.medium ?? null,
2834
- campaign: context.utmCampaign ?? persisted?.utm.campaign ?? null,
2835
- term: context.utmTerm ?? persisted?.utm.term ?? null,
2836
- content: context.utmContent ?? persisted?.utm.content ?? null
3350
+ method: data.method,
3351
+ displayName: data.displayName,
3352
+ additionalFee: data.additionalFee
2837
3353
  };
2838
3354
  }
2839
- function buildClickIdsPayload(context) {
2840
- const persisted = getTrackingAttributionSnapshot();
2841
- const clickIds = context.clickIds;
3355
+ function createPaymentsOperations(client) {
2842
3356
  return {
2843
- schemaVersion: TRACKING_SCHEMA_VERSION,
2844
- capturedAt: toIsoTimestamp(clickIds?.capturedAt) ?? persisted?.clickIds.capturedAt ?? null,
2845
- gclid: clickIds?.gclid ?? persisted?.clickIds.gclid ?? null,
2846
- gbraid: clickIds?.gbraid ?? persisted?.clickIds.gbraid ?? null,
2847
- wbraid: clickIds?.wbraid ?? persisted?.clickIds.wbraid ?? null,
2848
- ttclid: clickIds?.ttclid ?? persisted?.clickIds.ttclid ?? null,
2849
- fbclid: clickIds?.fbclid ?? persisted?.clickIds.fbclid ?? null
3357
+ async getAvailableMethods() {
3358
+ const result = await client.query(
3359
+ { query: AVAILABLE_PAYMENT_METHODS_QUERY },
3360
+ { cache: true }
3361
+ );
3362
+ if (result.isErr()) {
3363
+ return err(result.error);
3364
+ }
3365
+ if (!result.value.availablePaymentMethods) {
3366
+ return err(new NotFoundError("Payment methods not available"));
3367
+ }
3368
+ return ok(result.value.availablePaymentMethods.map(mapPaymentMethod));
3369
+ }
2850
3370
  };
2851
3371
  }
2852
- function buildDefaultContext(storeFallback) {
2853
- const context = {};
2854
- const device = resolveDeviceInfo();
2855
- const win = getWindow();
2856
- if (win) {
2857
- const { pathname, search, hostname } = win.location;
2858
- context.path = pathname + search;
2859
- context.referrer = win.document.referrer.length > 0 ? win.document.referrer : null;
2860
- const params = new URLSearchParams(win.location.search);
2861
- context.utmSource = params.get("utm_source");
2862
- context.utmMedium = params.get("utm_medium");
2863
- context.utmCampaign = params.get("utm_campaign");
2864
- context.utmTerm = params.get("utm_term");
2865
- context.utmContent = params.get("utm_content");
2866
- context.storeSlug = parseStoreSlugFromBase(pathname) ?? parseStoreSlugFromHostname(hostname) ?? void 0;
2867
- context.deviceType = device.deviceType;
2868
- context.deviceOs = device.deviceOs;
2869
- context.deviceBrowser = device.deviceBrowser;
3372
+ function mapDisplayIntent(raw) {
3373
+ switch (raw) {
3374
+ case "ACCORDION":
3375
+ return "ACCORDION";
3376
+ case "SPECIFICATIONS":
3377
+ return "SPECIFICATIONS";
3378
+ case "BOTH":
3379
+ return "BOTH";
3380
+ default:
3381
+ return "ACCORDION";
2870
3382
  }
2871
- if (!context.deviceType) {
2872
- context.deviceType = "unknown";
3383
+ }
3384
+ function mapDetailSection(raw) {
3385
+ const base = {
3386
+ id: raw.id,
3387
+ title: raw.title,
3388
+ displayIntent: mapDisplayIntent(raw.displayIntent),
3389
+ position: raw.position
3390
+ };
3391
+ switch (raw.sectionType) {
3392
+ case "RICH_TEXT":
3393
+ return { ...base, sectionType: "RICH_TEXT", content: { html: raw.content.html } };
3394
+ case "BULLET_LIST":
3395
+ return { ...base, sectionType: "BULLET_LIST", content: { items: raw.content.items } };
3396
+ case "TABLE":
3397
+ return { ...base, sectionType: "TABLE", content: { rows: raw.content.rows } };
3398
+ default:
3399
+ return { ...base, sectionType: "RICH_TEXT", content: { html: "" } };
3400
+ }
3401
+ }
3402
+ function mapProduct(raw) {
3403
+ return { ...raw, detailSections: raw.detailSections.map(mapDetailSection) };
3404
+ }
3405
+ const PRODUCTS_QUERY = `
3406
+ query Products($first: Int, $after: String, $filter: ProductFilter, $sort: ProductSortKey) {
3407
+ products(first: $first, after: $after, filter: $filter, sort: $sort) {
3408
+ edges {
3409
+ node {
3410
+ id
3411
+ handle
3412
+ title
3413
+ description
3414
+ vendor
3415
+ productType
3416
+ metaTitle
3417
+ metaDescription
3418
+ publishedAt
3419
+ createdAt
3420
+ availableForSale
3421
+ media {
3422
+ id
3423
+ url
3424
+ altText
3425
+ position
3426
+ }
3427
+ options {
3428
+ id
3429
+ name
3430
+ position
3431
+ values {
3432
+ id
3433
+ value
3434
+ position
3435
+ }
3436
+ }
3437
+ variants {
3438
+ id
3439
+ title
3440
+ sku
3441
+ price
3442
+ compareAtPrice
3443
+ weight
3444
+ weightUnit
3445
+ requiresShipping
3446
+ availableForSale
3447
+ quantity
3448
+ selectedOptions {
3449
+ id
3450
+ value
3451
+ position
3452
+ }
3453
+ image {
3454
+ id
3455
+ url
3456
+ altText
3457
+ position
3458
+ }
3459
+ isOnSale
3460
+ quantityPricing {
3461
+ minQuantity
3462
+ price
3463
+ }
3464
+ }
3465
+ categories {
3466
+ id
3467
+ name
3468
+ handle
3469
+ description
3470
+ }
3471
+ primaryCategory {
3472
+ id
3473
+ name
3474
+ handle
3475
+ }
3476
+ tags {
3477
+ id
3478
+ name
3479
+ }
3480
+ detailSections {
3481
+ id
3482
+ title
3483
+ sectionType
3484
+ content
3485
+ displayIntent
3486
+ position
3487
+ }
3488
+ }
3489
+ cursor
3490
+ }
3491
+ pageInfo {
3492
+ hasNextPage
3493
+ hasPreviousPage
3494
+ startCursor
3495
+ endCursor
3496
+ }
2873
3497
  }
2874
- if (!context.storeSlug) {
2875
- context.storeSlug = storeFallback;
3498
+ }
3499
+ `;
3500
+ const PRODUCT_BY_ID_QUERY = `
3501
+ query ProductById($id: GID!) {
3502
+ product(id: $id) {
3503
+ id
3504
+ handle
3505
+ title
3506
+ description
3507
+ vendor
3508
+ productType
3509
+ metaTitle
3510
+ metaDescription
3511
+ publishedAt
3512
+ createdAt
3513
+ availableForSale
3514
+ media {
3515
+ id
3516
+ url
3517
+ altText
3518
+ position
3519
+ }
3520
+ options {
3521
+ id
3522
+ name
3523
+ position
3524
+ values {
3525
+ id
3526
+ value
3527
+ position
3528
+ }
3529
+ }
3530
+ variants {
3531
+ id
3532
+ title
3533
+ sku
3534
+ price
3535
+ compareAtPrice
3536
+ weight
3537
+ weightUnit
3538
+ requiresShipping
3539
+ availableForSale
3540
+ quantity
3541
+ selectedOptions {
3542
+ id
3543
+ value
3544
+ position
3545
+ }
3546
+ image {
3547
+ id
3548
+ url
3549
+ altText
3550
+ position
3551
+ }
3552
+ isOnSale
3553
+ quantityPricing {
3554
+ minQuantity
3555
+ price
3556
+ }
3557
+ }
3558
+ categories {
3559
+ id
3560
+ name
3561
+ handle
3562
+ description
3563
+ }
3564
+ tags {
3565
+ id
3566
+ name
3567
+ }
3568
+ detailSections {
3569
+ id
3570
+ title
3571
+ sectionType
3572
+ content
3573
+ displayIntent
3574
+ position
3575
+ }
2876
3576
  }
2877
- context.deviceOs ??= null;
2878
- context.deviceBrowser ??= null;
2879
- return context;
2880
- }
2881
- function normalizeEventContext(context, storeFallback) {
2882
- const defaults = buildDefaultContext(storeFallback);
2883
- const merged = { ...defaults, ...context };
2884
- return {
2885
- context: merged,
2886
- storeSlug: merged.storeSlug ?? null
2887
- };
2888
- }
2889
- function isIngestResponse(value) {
2890
- if (typeof value !== "object" || value === null) return false;
2891
- const response = value;
2892
- return typeof response.acceptedCount === "number" && typeof response.duplicateCount === "number" && typeof response.rejectedCount === "number" && Array.isArray(response.errors);
2893
3577
  }
2894
- function extractErrorMessage(response) {
2895
- return response.json().then((payload) => {
2896
- if (typeof payload.error === "string" && payload.error.length > 0) {
2897
- return payload.error;
3578
+ `;
3579
+ const PRODUCT_BY_HANDLE_QUERY = `
3580
+ query ProductByHandle($handle: String!) {
3581
+ product(handle: $handle) {
3582
+ id
3583
+ handle
3584
+ title
3585
+ description
3586
+ vendor
3587
+ productType
3588
+ metaTitle
3589
+ metaDescription
3590
+ publishedAt
3591
+ createdAt
3592
+ availableForSale
3593
+ media {
3594
+ id
3595
+ url
3596
+ altText
3597
+ position
2898
3598
  }
2899
- const firstMessage = Array.isArray(payload.details) ? payload.details.map((error) => typeof error?.message === "string" ? error.message : null).find((message) => message !== null) : null;
2900
- if (firstMessage) {
2901
- return firstMessage;
3599
+ options {
3600
+ id
3601
+ name
3602
+ position
3603
+ values {
3604
+ id
3605
+ value
3606
+ position
3607
+ }
3608
+ }
3609
+ variants {
3610
+ id
3611
+ title
3612
+ sku
3613
+ price
3614
+ compareAtPrice
3615
+ weight
3616
+ weightUnit
3617
+ requiresShipping
3618
+ availableForSale
3619
+ quantity
3620
+ selectedOptions {
3621
+ id
3622
+ value
3623
+ position
3624
+ }
3625
+ image {
3626
+ id
3627
+ url
3628
+ altText
3629
+ position
3630
+ }
3631
+ isOnSale
3632
+ quantityPricing {
3633
+ minQuantity
3634
+ price
3635
+ }
3636
+ }
3637
+ categories {
3638
+ id
3639
+ name
3640
+ handle
3641
+ description
3642
+ }
3643
+ tags {
3644
+ id
3645
+ name
3646
+ }
3647
+ detailSections {
3648
+ id
3649
+ title
3650
+ sectionType
3651
+ content
3652
+ displayIntent
3653
+ position
2902
3654
  }
2903
- return `Analytics ingest failed with status ${response.status}`;
2904
- }).catch(() => `Analytics ingest failed with status ${response.status}`);
2905
- }
2906
- function isPlainObject(value) {
2907
- return typeof value === "object" && value !== null && !Array.isArray(value);
2908
- }
2909
- function resolveTrackEvent(eventName) {
2910
- const normalized = eventName.startsWith("analytics.") ? eventName.slice("analytics.".length) : eventName;
2911
- if (normalized in ANALYTICS_PRESET_EVENT_MAP) {
2912
- return { eventType: ANALYTICS_PRESET_EVENT_MAP[normalized] };
2913
3655
  }
2914
- return {
2915
- eventType: "analytics.custom",
2916
- customEventName: eventName
2917
- };
2918
3656
  }
2919
- function createAnalyticsOperations(client) {
2920
- captureLandingTrackingAttribution();
2921
- const endpoint = (() => {
2922
- try {
2923
- const parsedEndpoint = new URL(client.config.endpoint);
2924
- return `${parsedEndpoint.origin}${ANALYTICS_PATH}`;
2925
- } catch {
2926
- return null;
2927
- }
2928
- })();
2929
- const configStoreSlug = client.config.storeSlug;
2930
- async function sendEvent(eventType, properties, context) {
2931
- if (!endpoint) {
2932
- return err(new NetworkError("Invalid storefront endpoint"));
3657
+ `;
3658
+ const PRODUCTS_BY_HANDLES_QUERY = `
3659
+ query ProductsByHandles($handles: [String!]!) {
3660
+ productsByHandles(handles: $handles) {
3661
+ id
3662
+ handle
3663
+ title
3664
+ description
3665
+ vendor
3666
+ productType
3667
+ metaTitle
3668
+ metaDescription
3669
+ publishedAt
3670
+ createdAt
3671
+ availableForSale
3672
+ media {
3673
+ id
3674
+ url
3675
+ altText
3676
+ position
2933
3677
  }
2934
- const resolved = normalizeEventContext(context, configStoreSlug);
2935
- const storeSlug = resolved.storeSlug;
2936
- const eventContext = resolved.context;
2937
- if (!storeSlug) {
2938
- return err(new NetworkError("Missing storeSlug. Provide storeSlug in client config or event context."));
3678
+ options {
3679
+ id
3680
+ name
3681
+ position
3682
+ values {
3683
+ id
3684
+ value
3685
+ position
3686
+ }
2939
3687
  }
2940
- const now = new Date(eventContext.occurredAt ?? Date.now());
2941
- const nowIso = Number.isNaN(now.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : now.toISOString();
2942
- const customerId = eventContext.customerId === void 0 ? parseCustomerIdFromToken(client.getCustomerToken()) : eventContext.customerId;
2943
- const event = {
2944
- schemaVersion: TRACKING_SCHEMA_VERSION,
2945
- eventId: randomUuid(),
2946
- eventType,
2947
- occurredAt: nowIso,
2948
- sessionId: eventContext.sessionId ?? getSessionId(now.getTime()),
2949
- visitorId: eventContext.visitorId ?? getOrCreateVisitorId(),
2950
- customerId: customerId ?? null,
2951
- consentState: eventContext.consentState ?? "unknown",
2952
- context: {
2953
- schemaVersion: TRACKING_SCHEMA_VERSION,
2954
- path: normalizePath(eventContext.path ?? "/")
2955
- },
2956
- referrer: eventContext.referrer ?? null,
2957
- utm: buildUtmPayload(eventContext),
2958
- clickIds: buildClickIdsPayload(eventContext),
2959
- device: {
2960
- deviceType: eventContext.deviceType ?? "unknown",
2961
- deviceOs: eventContext.deviceOs ?? null,
2962
- deviceBrowser: eventContext.deviceBrowser ?? null
2963
- },
2964
- properties
2965
- };
2966
- const payload = {
2967
- storeSlug,
2968
- events: [event]
2969
- };
2970
- try {
2971
- const response = await fetch(endpoint, {
2972
- method: "POST",
2973
- headers: {
2974
- "content-type": "application/json"
2975
- },
2976
- body: JSON.stringify(payload)
2977
- });
2978
- if (!response.ok) {
2979
- const message = response.headers.get("content-type")?.includes("application/json") ? await extractErrorMessage(response) : `Analytics ingest failed with status ${response.status}`;
2980
- return err(new NetworkError(message));
3688
+ variants {
3689
+ id
3690
+ title
3691
+ sku
3692
+ price
3693
+ compareAtPrice
3694
+ weight
3695
+ weightUnit
3696
+ requiresShipping
3697
+ availableForSale
3698
+ quantity
3699
+ selectedOptions {
3700
+ id
3701
+ value
3702
+ position
2981
3703
  }
2982
- const parsed = await response.json();
2983
- if (!isIngestResponse(parsed)) {
2984
- return err(new NetworkError("Analytics response shape is invalid"));
3704
+ image {
3705
+ id
3706
+ url
3707
+ altText
3708
+ position
2985
3709
  }
2986
- return ok(parsed);
2987
- } catch (error) {
2988
- return err(
2989
- new NetworkError("Failed to send analytics event", { cause: error instanceof Error ? error : void 0 })
2990
- );
3710
+ isOnSale
3711
+ quantityPricing {
3712
+ minQuantity
3713
+ price
3714
+ }
3715
+ }
3716
+ categories {
3717
+ id
3718
+ name
3719
+ handle
3720
+ description
3721
+ }
3722
+ tags {
3723
+ id
3724
+ name
3725
+ }
3726
+ detailSections {
3727
+ id
3728
+ title
3729
+ sectionType
3730
+ content
3731
+ displayIntent
3732
+ position
2991
3733
  }
2992
3734
  }
2993
- async function track(eventName, eventPayload, context) {
2994
- const normalized = resolveTrackEvent(eventName);
2995
- if (normalized.eventType !== "analytics.custom") {
2996
- switch (normalized.eventType) {
2997
- case "analytics.page_view": {
2998
- const eventContext = context ?? (isPlainObject(eventPayload) ? eventPayload : void 0);
2999
- return sendEvent("analytics.page_view", {}, eventContext);
3000
- }
3001
- case "analytics.product_view": {
3002
- if (!isPlainObject(eventPayload)) {
3003
- return err(new NetworkError("productId is required"));
3004
- }
3005
- const payload = eventPayload;
3006
- const decodedProductId = decodeAnalyticsEntityId(payload.productId);
3007
- if (!decodedProductId) {
3008
- return err(new NetworkError("Invalid productId"));
3009
- }
3010
- const decodedVariantId = decodeAnalyticsEntityId(payload.variantId);
3011
- return sendEvent(
3012
- "analytics.product_view",
3013
- { productId: decodedProductId, variantId: decodedVariantId },
3014
- context
3015
- );
3016
- }
3017
- case "analytics.collection_view": {
3018
- if (!isPlainObject(eventPayload)) {
3019
- return err(new NetworkError("collectionId is required"));
3020
- }
3021
- const payload = eventPayload;
3022
- const decodedCollectionId = decodeAnalyticsEntityId(payload.collectionId);
3023
- if (!decodedCollectionId) {
3024
- return err(new NetworkError("Invalid collectionId"));
3025
- }
3026
- return sendEvent("analytics.collection_view", { collectionId: decodedCollectionId }, context);
3027
- }
3028
- case "analytics.search_performed": {
3029
- if (!isPlainObject(eventPayload)) {
3030
- return err(new NetworkError("query is required"));
3031
- }
3032
- const payload = eventPayload;
3033
- const trimmed = payload.query.trim();
3034
- if (!trimmed) {
3035
- return err(new NetworkError("query is required"));
3036
- }
3037
- return sendEvent(
3038
- "analytics.search_performed",
3039
- { query: trimmed, resultsCount: Math.max(0, Math.floor(payload.resultsCount)) },
3040
- context
3041
- );
3042
- }
3043
- case "analytics.add_to_cart": {
3044
- if (!isPlainObject(eventPayload)) {
3045
- return err(new NetworkError("Invalid cartId"));
3046
- }
3047
- const payload = eventPayload;
3048
- const cartId = decodeAnalyticsEntityId(payload.cartId);
3049
- if (!cartId) {
3050
- return err(new NetworkError("Invalid cartId"));
3051
- }
3052
- return sendEvent(
3053
- "analytics.add_to_cart",
3054
- {
3055
- cartId,
3056
- quantity: Math.max(1, Math.floor(payload.quantity)),
3057
- itemsCount: Math.max(0, Math.floor(payload.itemsCount)),
3058
- cartValueCents: toCents(payload.cartValue)
3059
- },
3060
- context
3061
- );
3062
- }
3063
- case "analytics.remove_from_cart": {
3064
- if (!isPlainObject(eventPayload)) {
3065
- return err(new NetworkError("Invalid cartId"));
3066
- }
3067
- const payload = eventPayload;
3068
- const cartId = decodeAnalyticsEntityId(payload.cartId);
3069
- if (!cartId) {
3070
- return err(new NetworkError("Invalid cartId"));
3071
- }
3072
- return sendEvent(
3073
- "analytics.remove_from_cart",
3074
- {
3075
- cartId,
3076
- quantity: Math.max(1, Math.floor(payload.quantity)),
3077
- itemsCount: Math.max(0, Math.floor(payload.itemsCount)),
3078
- cartValueCents: toCents(payload.cartValue)
3079
- },
3080
- context
3081
- );
3082
- }
3083
- case "analytics.checkout_started": {
3084
- if (!isPlainObject(eventPayload)) {
3085
- return err(new NetworkError("Invalid cartId"));
3086
- }
3087
- const payload = eventPayload;
3088
- const decodedCartId = decodeAnalyticsEntityId(payload.cartId);
3089
- if (!decodedCartId) {
3090
- return err(new NetworkError("Invalid cartId"));
3091
- }
3092
- return sendEvent("analytics.checkout_started", { cartId: decodedCartId }, context);
3093
- }
3094
- case "analytics.checkout_step_completed": {
3095
- if (!isPlainObject(eventPayload)) {
3096
- return err(new NetworkError("Invalid cartId"));
3097
- }
3098
- const payload = eventPayload;
3099
- const decodedCartId = decodeAnalyticsEntityId(payload.cartId);
3100
- if (!decodedCartId) {
3101
- return err(new NetworkError("Invalid cartId"));
3102
- }
3103
- return sendEvent("analytics.checkout_step_completed", { cartId: decodedCartId, step: payload.step }, context);
3735
+ }
3736
+ `;
3737
+ function createProductsOperations(client) {
3738
+ return {
3739
+ async list(options) {
3740
+ const result = await client.query({
3741
+ query: PRODUCTS_QUERY,
3742
+ variables: {
3743
+ first: options?.first,
3744
+ after: options?.after,
3745
+ filter: options?.filter,
3746
+ sort: options?.sort
3104
3747
  }
3105
- case "analytics.checkout_completed": {
3106
- if (!isPlainObject(eventPayload)) {
3107
- return err(new NetworkError("Invalid orderId or cartId"));
3108
- }
3109
- const payload = eventPayload;
3110
- const orderId = decodeAnalyticsEntityId(payload.orderId);
3111
- const cartId = decodeAnalyticsEntityId(payload.cartId);
3112
- if (!orderId || !cartId) {
3113
- return err(new NetworkError("Invalid orderId or cartId"));
3114
- }
3115
- return sendEvent(
3116
- "analytics.checkout_completed",
3117
- {
3118
- orderId,
3119
- cartId,
3120
- orderTotalCents: toCents(payload.orderTotal)
3121
- },
3122
- context
3123
- );
3748
+ });
3749
+ if (result.isErr()) {
3750
+ return err(result.error);
3751
+ }
3752
+ const connection = result.value.products;
3753
+ return ok({
3754
+ items: connection.edges.map((edge) => normalizeProductAssetUrls(mapProduct(edge.node), client.config.endpoint)),
3755
+ pageInfo: {
3756
+ hasNextPage: connection.pageInfo.hasNextPage,
3757
+ hasPreviousPage: connection.pageInfo.hasPreviousPage,
3758
+ startCursor: connection.pageInfo.startCursor,
3759
+ endCursor: connection.pageInfo.endCursor
3124
3760
  }
3761
+ });
3762
+ },
3763
+ async get(idOrHandle) {
3764
+ const useId = isGlobalId(idOrHandle);
3765
+ const result = await client.query({
3766
+ query: useId ? PRODUCT_BY_ID_QUERY : PRODUCT_BY_HANDLE_QUERY,
3767
+ variables: useId ? { id: idOrHandle } : { handle: idOrHandle }
3768
+ });
3769
+ if (result.isErr()) {
3770
+ return err(result.error);
3771
+ }
3772
+ if (!result.value.product) {
3773
+ return err(new NotFoundError(`Product not found: ${idOrHandle}`));
3774
+ }
3775
+ return ok(normalizeProductAssetUrls(mapProduct(result.value.product), client.config.endpoint));
3776
+ },
3777
+ async getByHandles(handles) {
3778
+ if (handles.length === 0) {
3779
+ return ok([]);
3780
+ }
3781
+ const result = await client.query({
3782
+ query: PRODUCTS_BY_HANDLES_QUERY,
3783
+ variables: { handles }
3784
+ });
3785
+ if (result.isErr()) {
3786
+ return err(result.error);
3125
3787
  }
3788
+ return ok(
3789
+ result.value.productsByHandles.map(
3790
+ (product) => product ? normalizeProductAssetUrls(mapProduct(product), client.config.endpoint) : null
3791
+ )
3792
+ );
3126
3793
  }
3127
- const properties = isPlainObject(eventPayload) ? eventPayload : {};
3128
- return sendEvent(
3129
- "analytics.custom",
3130
- { ...properties, eventName: normalized.customEventName ?? eventName },
3131
- context
3132
- );
3133
- }
3134
- return {
3135
- track
3136
3794
  };
3137
3795
  }
3138
3796
  const AVAILABLE_SHIPPING_RATES_QUERY = `
@@ -3176,6 +3834,53 @@ function createShippingOperations(client) {
3176
3834
  }
3177
3835
  };
3178
3836
  }
3837
+ const STORE_QUERY = `
3838
+ query Store {
3839
+ store {
3840
+ id
3841
+ name
3842
+ slug
3843
+ description
3844
+ currency
3845
+ timezone
3846
+ logoUrl
3847
+ contactEmail
3848
+ contactPhone
3849
+ trackingDispatchOnUnknownConsent
3850
+ browserTrackingConfig {
3851
+ gtm {
3852
+ enabled
3853
+ containerId
3854
+ }
3855
+ }
3856
+ analytics {
3857
+ enabled
3858
+ dispatchOnUnknownConsent
3859
+ gtm {
3860
+ enabled
3861
+ containerId
3862
+ }
3863
+ }
3864
+ socialLinks
3865
+ }
3866
+ }
3867
+ `;
3868
+ function createStoreOperations(client) {
3869
+ return {
3870
+ async get(options) {
3871
+ const result = await client.query(
3872
+ {
3873
+ query: STORE_QUERY
3874
+ },
3875
+ options
3876
+ );
3877
+ if (result.isErr()) {
3878
+ return err(result.error);
3879
+ }
3880
+ return ok(result.value.store);
3881
+ }
3882
+ };
3883
+ }
3179
3884
  const DEFAULT_CACHE_TTL = 5 * 60 * 1e3;
3180
3885
  function createStorefrontClient(config) {
3181
3886
  const storage = config.storage ?? createDefaultAdapter();
@@ -3194,6 +3899,8 @@ function createStorefrontClient(config) {
3194
3899
  _graphql: graphqlClient,
3195
3900
  _storage: storage,
3196
3901
  _queryCache: queryCache,
3902
+ _analyticsRuntimeConfig: null,
3903
+ _resolvedInitConfig: null,
3197
3904
  cache: {
3198
3905
  clear() {
3199
3906
  queryCache.clear();
@@ -3201,6 +3908,7 @@ function createStorefrontClient(config) {
3201
3908
  },
3202
3909
  // Placeholder - will be assigned below
3203
3910
  products: null,
3911
+ store: null,
3204
3912
  collections: null,
3205
3913
  categories: null,
3206
3914
  cart: null,
@@ -3210,6 +3918,7 @@ function createStorefrontClient(config) {
3210
3918
  account: null,
3211
3919
  shipping: null,
3212
3920
  analytics: null,
3921
+ init: null,
3213
3922
  query(request, options) {
3214
3923
  return graphqlClient.query(request, options);
3215
3924
  },
@@ -3236,6 +3945,7 @@ function createStorefrontClient(config) {
3236
3945
  }
3237
3946
  };
3238
3947
  client.products = createProductsOperations(client);
3948
+ client.store = createStoreOperations(client);
3239
3949
  client.collections = createCollectionsOperations(client);
3240
3950
  client.categories = createCategoriesOperations(client);
3241
3951
  client.cart = createCartOperations(client, storage);
@@ -3244,9 +3954,325 @@ function createStorefrontClient(config) {
3244
3954
  client.auth = createAuthOperations(client, storage);
3245
3955
  client.account = createAccountOperations(client, storage);
3246
3956
  client.shipping = createShippingOperations(client);
3247
- client.analytics = createAnalyticsOperations(client);
3957
+ const analytics = createAnalyticsOperations(client, storage);
3958
+ client.analytics = analytics;
3959
+ client.init = async (options) => {
3960
+ client._analyticsRuntimeConfig = options?.analytics ?? null;
3961
+ const storeResult = await client.store.get({ cache: false });
3962
+ if (storeResult.isErr()) {
3963
+ return err(storeResult.error);
3964
+ }
3965
+ const resolvedConfig = {
3966
+ analytics: {
3967
+ enabled: storeResult.value.analytics.enabled,
3968
+ dispatchOnUnknownConsent: storeResult.value.analytics.dispatchOnUnknownConsent,
3969
+ gtm: storeResult.value.analytics.gtm
3970
+ }
3971
+ };
3972
+ client._analyticsRuntimeConfig = {
3973
+ ...options?.analytics ?? {},
3974
+ dispatchOnUnknownConsent: resolvedConfig.analytics.dispatchOnUnknownConsent
3975
+ };
3976
+ client._resolvedInitConfig = resolvedConfig;
3977
+ analytics.init(resolvedConfig.analytics);
3978
+ return ok(resolvedConfig);
3979
+ };
3248
3980
  return client;
3249
3981
  }
3982
+ function getWindowObject(getWindow2) {
3983
+ if (getWindow2) {
3984
+ return getWindow2();
3985
+ }
3986
+ if (typeof window === "undefined") {
3987
+ return null;
3988
+ }
3989
+ return window;
3990
+ }
3991
+ function getNumberPropFromKeys(properties, keys) {
3992
+ for (const key of keys) {
3993
+ const value = properties[key];
3994
+ if (typeof value === "number" && Number.isFinite(value)) {
3995
+ return value;
3996
+ }
3997
+ }
3998
+ return null;
3999
+ }
4000
+ const DEFAULT_CURRENCY = "RSD";
4001
+ const DEFAULT_DATA_LAYER_NAME = "dataLayer";
4002
+ const GTM_CONSENT_DEFAULT_STATE = {
4003
+ analytics_storage: "denied",
4004
+ ad_storage: "denied",
4005
+ ad_user_data: "denied",
4006
+ ad_personalization: "denied"
4007
+ };
4008
+ const GTM_TRIGGER_EVENT_MAP = {
4009
+ page_view: "ekomerc.page_view",
4010
+ view_item: "ekomerc.view_item",
4011
+ view_item_list: "ekomerc.view_item_list",
4012
+ search: "ekomerc.search",
4013
+ add_to_cart: "ekomerc.add_to_cart",
4014
+ remove_from_cart: "ekomerc.remove_from_cart",
4015
+ begin_checkout: "ekomerc.begin_checkout",
4016
+ purchase: "ekomerc.purchase"
4017
+ };
4018
+ function isObjectRecord(value) {
4019
+ return typeof value === "object" && value !== null;
4020
+ }
4021
+ function isGtmDataLayer(value) {
4022
+ return isObjectRecord(value) && typeof value.push === "function";
4023
+ }
4024
+ function getStringProp(properties, key) {
4025
+ const value = properties[key];
4026
+ return typeof value === "string" && value.length > 0 ? value : null;
4027
+ }
4028
+ function getNumberProp(properties, key) {
4029
+ const value = properties[key];
4030
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
4031
+ }
4032
+ function centsToCurrencyUnits(valueInCents) {
4033
+ if (valueInCents === null) {
4034
+ return null;
4035
+ }
4036
+ return valueInCents / 100;
4037
+ }
4038
+ function getDinarValue(properties, dinarKeys, centsKeys) {
4039
+ const valueInDinars = getNumberPropFromKeys(properties, dinarKeys);
4040
+ if (valueInDinars !== null) {
4041
+ return valueInDinars;
4042
+ }
4043
+ return centsToCurrencyUnits(getNumberPropFromKeys(properties, centsKeys));
4044
+ }
4045
+ function normalizeQuantity(value) {
4046
+ if (value === null) {
4047
+ return 1;
4048
+ }
4049
+ return Math.max(1, Math.floor(value));
4050
+ }
4051
+ function resolveProductId(properties) {
4052
+ return getStringProp(properties, "productId") ?? getStringProp(properties, "product_id") ?? getStringProp(properties, "contentId") ?? getStringProp(properties, "content_id") ?? getStringProp(properties, "id") ?? getStringProp(properties, "item_id");
4053
+ }
4054
+ function buildItem(properties, currency) {
4055
+ const productId = resolveProductId(properties);
4056
+ if (!productId) {
4057
+ return null;
4058
+ }
4059
+ const quantity = normalizeQuantity(getNumberProp(properties, "quantity"));
4060
+ const unitPrice = getDinarValue(
4061
+ properties,
4062
+ ["unitPrice", "unit_price", "price"],
4063
+ ["unitPriceCents", "unit_price_cents", "priceCents"]
4064
+ ) ?? 0;
4065
+ const lineTotal = getDinarValue(
4066
+ properties,
4067
+ ["lineTotal", "line_total", "total"],
4068
+ ["lineTotalCents", "line_total_cents", "totalCents"]
4069
+ ) ?? unitPrice * quantity;
4070
+ const item = {
4071
+ product_id: productId,
4072
+ quantity,
4073
+ unit_price: unitPrice,
4074
+ line_total: lineTotal,
4075
+ currency
4076
+ };
4077
+ const variantId = getStringProp(properties, "variantId") ?? getStringProp(properties, "variant_id");
4078
+ if (variantId) {
4079
+ item.variant_id = variantId;
4080
+ }
4081
+ return item;
4082
+ }
4083
+ function buildNormalizedItems(eventName, properties, currency) {
4084
+ const rawLineItems = properties.lineItems;
4085
+ if (Array.isArray(rawLineItems)) {
4086
+ const items = [];
4087
+ for (const lineItem of rawLineItems) {
4088
+ if (!isObjectRecord(lineItem)) {
4089
+ continue;
4090
+ }
4091
+ const mappedItem = buildItem(lineItem, currency);
4092
+ if (mappedItem) {
4093
+ items.push(mappedItem);
4094
+ }
4095
+ }
4096
+ if (items.length > 0) {
4097
+ return items;
4098
+ }
4099
+ }
4100
+ if (eventName === "view_item" || eventName === "add_to_cart" || eventName === "remove_from_cart" || eventName === "begin_checkout" || eventName === "purchase") {
4101
+ const singleItem = buildItem(properties, currency);
4102
+ return singleItem ? [singleItem] : [];
4103
+ }
4104
+ return [];
4105
+ }
4106
+ function buildAttributionPayload(input) {
4107
+ return {
4108
+ utm_source: input.event.utm.source,
4109
+ utm_medium: input.event.utm.medium,
4110
+ utm_campaign: input.event.utm.campaign,
4111
+ utm_term: input.event.utm.term,
4112
+ utm_content: input.event.utm.content,
4113
+ gclid: input.event.clickIds.gclid,
4114
+ gbraid: input.event.clickIds.gbraid,
4115
+ wbraid: input.event.clickIds.wbraid,
4116
+ fbclid: input.event.clickIds.fbclid,
4117
+ ttclid: input.event.clickIds.ttclid
4118
+ };
4119
+ }
4120
+ function buildGtmDataLayerEvent(input) {
4121
+ const providerEventName = getTrackingProviderEventName("gtm", input.event.eventType);
4122
+ if (providerEventName === null) {
4123
+ return null;
4124
+ }
4125
+ const eventName = GTM_TRIGGER_EVENT_MAP[providerEventName];
4126
+ const dedupe = getTrackingProviderDedupeConvention("gtm");
4127
+ const ekomerc = {
4128
+ event_name: eventName,
4129
+ event_id: input.event[dedupe.canonicalEventIdField],
4130
+ consent_state: input.event.consentState,
4131
+ context: {
4132
+ path: input.event.context.path,
4133
+ referrer: input.event.referrer
4134
+ },
4135
+ attribution: buildAttributionPayload(input)
4136
+ };
4137
+ if (providerEventName === "search") {
4138
+ const query = getStringProp(input.event.properties, "query");
4139
+ if (query) {
4140
+ ekomerc.search_term = query;
4141
+ }
4142
+ const resultsCount = getNumberProp(input.event.properties, "resultsCount");
4143
+ if (resultsCount !== null) {
4144
+ ekomerc.results_count = resultsCount;
4145
+ }
4146
+ }
4147
+ if (providerEventName !== "page_view" && providerEventName !== "search") {
4148
+ const currency = getStringProp(input.event.properties, "currency") ?? DEFAULT_CURRENCY;
4149
+ const value = getDinarValue(
4150
+ input.event.properties,
4151
+ ["value", "total", "orderTotal", "cartValue"],
4152
+ ["priceCents", "cartValueCents", "totalCents", "orderTotalCents"]
4153
+ );
4154
+ const quantity = getNumberProp(input.event.properties, "quantity");
4155
+ const itemsCount = getNumberPropFromKeys(input.event.properties, ["itemsCount", "numItems"]);
4156
+ const items = buildNormalizedItems(providerEventName, input.event.properties, currency);
4157
+ ekomerc.currency = currency;
4158
+ if (value !== null) {
4159
+ ekomerc.value = value;
4160
+ }
4161
+ const cartId = getStringProp(input.event.properties, "cartId");
4162
+ if (cartId) {
4163
+ ekomerc.cart_id = cartId;
4164
+ }
4165
+ const orderId = getStringProp(input.event.properties, "orderId");
4166
+ if (orderId) {
4167
+ ekomerc.order_id = orderId;
4168
+ }
4169
+ if (quantity !== null) {
4170
+ ekomerc.quantity = quantity;
4171
+ }
4172
+ if (itemsCount !== null) {
4173
+ ekomerc.items_count = itemsCount;
4174
+ } else if (items.length > 0) {
4175
+ ekomerc.items_count = items.length;
4176
+ }
4177
+ if (items.length > 0) {
4178
+ ekomerc.items = items;
4179
+ }
4180
+ }
4181
+ return {
4182
+ event: eventName,
4183
+ ekomerc
4184
+ };
4185
+ }
4186
+ function buildGtmConsentModeUpdate({ consentState }) {
4187
+ const consentValue = consentState === "granted" ? "granted" : "denied";
4188
+ return {
4189
+ analytics_storage: consentValue,
4190
+ ad_storage: consentValue,
4191
+ ad_user_data: consentValue,
4192
+ ad_personalization: consentValue
4193
+ };
4194
+ }
4195
+ function getDataLayer(windowObject, dataLayerName) {
4196
+ const dataLayer = Reflect.get(windowObject, dataLayerName);
4197
+ if (isGtmDataLayer(dataLayer)) {
4198
+ return dataLayer;
4199
+ }
4200
+ if (dataLayer !== void 0 && dataLayer !== null) {
4201
+ return null;
4202
+ }
4203
+ const initializedDataLayer = [];
4204
+ if (!Reflect.set(windowObject, dataLayerName, initializedDataLayer)) {
4205
+ return null;
4206
+ }
4207
+ return initializedDataLayer;
4208
+ }
4209
+ function pushConsentCommand(windowObject, dataLayer, mode, consentUpdate) {
4210
+ const gtag = Reflect.get(windowObject, "gtag");
4211
+ if (typeof gtag === "function") {
4212
+ gtag("consent", mode, consentUpdate);
4213
+ return;
4214
+ }
4215
+ dataLayer.push(["consent", mode, consentUpdate]);
4216
+ }
4217
+ function runtimeUnavailable() {
4218
+ return {
4219
+ status: "skipped",
4220
+ reason: "runtime_unavailable"
4221
+ };
4222
+ }
4223
+ function createGtmBrowserAdapter(config, options = {}) {
4224
+ if (!config.enabled || !config.containerId) {
4225
+ return null;
4226
+ }
4227
+ const dataLayerName = options.dataLayerName ?? DEFAULT_DATA_LAYER_NAME;
4228
+ let consentDefaultsApplied = false;
4229
+ function applyConsentDefaults(windowObject, dataLayer) {
4230
+ if (consentDefaultsApplied) {
4231
+ return;
4232
+ }
4233
+ pushConsentCommand(windowObject, dataLayer, "default", GTM_CONSENT_DEFAULT_STATE);
4234
+ consentDefaultsApplied = true;
4235
+ }
4236
+ return {
4237
+ provider: "gtm",
4238
+ dispatch(input) {
4239
+ const payload = buildGtmDataLayerEvent(input);
4240
+ if (payload === null) {
4241
+ return {
4242
+ status: "success"
4243
+ };
4244
+ }
4245
+ const windowObject = getWindowObject(options.getWindow);
4246
+ if (!windowObject) {
4247
+ return runtimeUnavailable();
4248
+ }
4249
+ const dataLayer = getDataLayer(windowObject, dataLayerName);
4250
+ if (!dataLayer) {
4251
+ return runtimeUnavailable();
4252
+ }
4253
+ applyConsentDefaults(windowObject, dataLayer);
4254
+ dataLayer.push(payload);
4255
+ return {
4256
+ status: "success"
4257
+ };
4258
+ },
4259
+ updateConsent(input) {
4260
+ const windowObject = getWindowObject(options.getWindow);
4261
+ if (!windowObject) {
4262
+ return runtimeUnavailable();
4263
+ }
4264
+ const dataLayer = getDataLayer(windowObject, dataLayerName);
4265
+ if (!dataLayer) {
4266
+ return runtimeUnavailable();
4267
+ }
4268
+ applyConsentDefaults(windowObject, dataLayer);
4269
+ pushConsentCommand(windowObject, dataLayer, "update", buildGtmConsentModeUpdate(input));
4270
+ return {
4271
+ status: "success"
4272
+ };
4273
+ }
4274
+ };
4275
+ }
3250
4276
  export {
3251
4277
  AuthError,
3252
4278
  CART_TOKEN_KEY,
@@ -3256,8 +4282,12 @@ export {
3256
4282
  NotFoundError,
3257
4283
  StateError,
3258
4284
  StorefrontError,
4285
+ TRACKING_ATTRIBUTION_QUERY_PARAM_KEYS,
3259
4286
  ValidationError,
4287
+ buildGtmConsentModeUpdate,
4288
+ buildGtmDataLayerEvent,
3260
4289
  createDefaultAdapter,
4290
+ createGtmBrowserAdapter,
3261
4291
  createLocalStorageAdapter,
3262
4292
  createMemoryAdapter,
3263
4293
  createQueryCache,