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