@thor-commerce/app-bridge-react 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/next.cjs CHANGED
@@ -531,240 +531,151 @@ function createAppBridge(options = {}) {
531
531
  return new AppBridge(options);
532
532
  }
533
533
 
534
- // src/react.tsx
535
- var import_jsx_runtime = require("react/jsx-runtime");
536
- var AppBridgeContext = (0, import_react.createContext)(null);
537
- function AppBridgeProvider({
538
- children,
539
- clientId,
540
- currentPath,
541
- navigationItems,
542
- navigationEventType = "navigation:go",
543
- navigationUpdateEventType = "navigation:update",
544
- namespace,
545
- onNavigate,
546
- readyEventType = "app:ready",
547
- readyPayload,
548
- requestTimeoutMs,
549
- selfWindow,
550
- targetOrigin,
551
- targetWindow
552
- }) {
553
- const [bridge, setBridge] = (0, import_react.useState)(null);
554
- const onNavigateRef = (0, import_react.useRef)(onNavigate);
555
- const normalizedNavigationItems = (0, import_react.useMemo)(
556
- () => normalizeBridgeNavigationItems(navigationItems),
557
- [navigationItems]
558
- );
559
- const sessionTokenCacheRef = (0, import_react.useRef)(null);
560
- const projectRef = (0, import_react.useRef)(readInitialProject(selfWindow));
561
- const pendingSessionTokenRef = (0, import_react.useRef)(null);
562
- (0, import_react.useEffect)(() => {
563
- onNavigateRef.current = onNavigate;
564
- }, [onNavigate]);
565
- (0, import_react.useEffect)(() => {
566
- if (typeof window === "undefined" && !selfWindow) {
567
- return;
534
+ // src/runtime.ts
535
+ var GLOBAL_RUNTIME_KEY = "__thorEmbeddedAppRuntime__";
536
+ var THOR_NAVIGATE_EVENT = "thor:navigate";
537
+ var EmbeddedAppRuntime = class {
538
+ constructor(config) {
539
+ this.currentPath = null;
540
+ this.navigationItems = [];
541
+ this.navigationEventType = "navigation:go";
542
+ this.navigationUpdateEventType = "navigation:update";
543
+ this.readyEventType = "app:ready";
544
+ this.sessionTokenCache = null;
545
+ this.pendingSessionToken = null;
546
+ const resolvedWindow = config.selfWindow ?? (typeof window !== "undefined" ? window : void 0);
547
+ if (!resolvedWindow) {
548
+ throw new Error("EmbeddedAppRuntime requires a browser window.");
568
549
  }
569
- const resolvedTargetOrigin = targetOrigin ?? getReferrerOrigin(selfWindow);
570
- const resolvedAllowedOrigins = resolvedTargetOrigin ? [resolvedTargetOrigin] : void 0;
571
- const nextBridge = createAppBridge({
572
- allowedOrigins: resolvedAllowedOrigins,
573
- clientId,
574
- namespace,
575
- requestTimeoutMs,
576
- selfWindow,
550
+ this.selfWindow = resolvedWindow;
551
+ this.targetOrigin = config.targetOrigin ?? getReferrerOrigin(resolvedWindow);
552
+ this.clientId = config.clientId;
553
+ this.project = readInitialProject(resolvedWindow);
554
+ this.readyPayload = config.readyPayload ?? { clientId: config.clientId };
555
+ this.bridge = createAppBridge({
556
+ allowedOrigins: this.targetOrigin ? [this.targetOrigin] : void 0,
557
+ namespace: config.namespace,
558
+ requestTimeoutMs: config.requestTimeoutMs,
559
+ selfWindow: resolvedWindow,
577
560
  source: "embedded-app",
578
561
  target: "dashboard",
579
- targetOrigin: resolvedTargetOrigin,
580
- targetWindow
562
+ targetOrigin: this.targetOrigin,
563
+ targetWindow: config.targetWindow
581
564
  });
582
- setBridge(nextBridge);
583
- return () => {
584
- setBridge((currentBridge) => currentBridge === nextBridge ? null : currentBridge);
585
- nextBridge.destroy();
586
- };
587
- }, [
588
- clientId,
589
- namespace,
590
- requestTimeoutMs,
591
- selfWindow,
592
- targetOrigin,
593
- targetWindow
594
- ]);
595
- (0, import_react.useEffect)(() => {
596
- if (!bridge || !bridge.hasTargetWindow()) {
597
- return;
598
- }
599
- bridge.send(
600
- readyEventType,
601
- readyPayload ?? {
602
- clientId
603
- }
604
- );
605
- }, [bridge, clientId, readyEventType, readyPayload]);
606
- (0, import_react.useEffect)(() => {
607
- if (!bridge || !bridge.hasTargetWindow()) {
608
- return;
609
- }
610
- bridge.send("navigation:items:update", {
611
- items: normalizedNavigationItems
612
- });
613
- }, [bridge, normalizedNavigationItems]);
614
- (0, import_react.useEffect)(() => {
615
- if (!bridge || !bridge.hasTargetWindow() || !currentPath) {
616
- return;
617
- }
618
- bridge.send(navigationUpdateEventType, buildNavigationUpdatePayload(currentPath));
619
- }, [bridge, currentPath, navigationUpdateEventType]);
620
- (0, import_react.useEffect)(() => {
621
- if (!bridge || !onNavigate) {
622
- return;
623
- }
624
- return bridge.on(navigationEventType, (message) => {
565
+ this.removeNavigationRequestHandler = this.bridge.on(this.navigationEventType, (message) => {
625
566
  const destination = resolveNavigationDestination(message.payload);
626
567
  if (!destination) {
627
568
  return;
628
569
  }
629
570
  const nextPath = preserveEmbeddedAppLaunchParams(
630
571
  destination,
631
- typeof window !== "undefined" ? window.location.href : void 0
572
+ this.selfWindow.location.href
632
573
  );
633
- onNavigateRef.current?.(nextPath ?? destination, message);
574
+ this.navigate(nextPath ?? destination, message);
634
575
  });
635
- }, [bridge, navigationEventType, onNavigate]);
636
- (0, import_react.useEffect)(() => {
637
- if (!bridge || !onNavigate || typeof document === "undefined" || typeof window === "undefined") {
638
- return;
639
- }
640
- const handleLocalNavigation = (path) => {
641
- const sanitizedPath = sanitizeEmbeddedAppPath(path);
642
- if (!sanitizedPath) {
643
- return;
644
- }
645
- const nextPath = preserveEmbeddedAppLaunchParams(
646
- sanitizedPath,
647
- window.location.href
648
- );
649
- onNavigateRef.current?.(nextPath ?? sanitizedPath);
650
- };
651
- const handleDocumentClick = (event) => {
652
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
653
- return;
654
- }
655
- const target = event.target;
656
- if (!(target instanceof Element)) {
657
- return;
658
- }
659
- const anchor = target.closest("a[href]");
660
- if (!(anchor instanceof HTMLAnchorElement)) {
661
- return;
662
- }
663
- if (anchor.hasAttribute("download")) {
664
- return;
665
- }
666
- const targetWindow2 = anchor.target.toLowerCase();
667
- const href = anchor.getAttribute("href");
668
- if (!href) {
669
- return;
670
- }
671
- if (targetWindow2 === "_top" || targetWindow2 === "_parent") {
672
- event.preventDefault();
673
- bridge.redirectToRemote(anchor.href);
674
- return;
675
- }
676
- if (targetWindow2 && targetWindow2 !== "_self") {
677
- return;
678
- }
679
- const nextPath = resolveLocalNavigationPath(href, window.location.origin);
680
- if (!nextPath) {
681
- return;
682
- }
683
- event.preventDefault();
684
- handleLocalNavigation(nextPath);
685
- };
686
- const originalOpen = window.open.bind(window);
687
- window.open = (url, target, features) => {
688
- if (url == null) {
689
- return originalOpen(url, target, features);
690
- }
691
- const href = typeof url === "string" ? url : url.toString();
692
- const targetName = (target ?? "").toLowerCase();
693
- if (targetName === "_top" || targetName === "_parent") {
694
- bridge.redirectToRemote(new URL(href, window.location.href).toString());
695
- return null;
576
+ this.removeNavigationInterceptors = installNavigationInterceptors({
577
+ bridge: this.bridge,
578
+ selfWindow: this.selfWindow,
579
+ navigate: (path) => this.navigate(path)
580
+ });
581
+ this.restoreFetch = installFetchInterceptor({
582
+ bridge: this.bridge,
583
+ getClientId: () => this.clientId,
584
+ getProject: () => this.project,
585
+ setProject: (project) => {
586
+ this.project = project;
587
+ },
588
+ readCachedToken: () => this.sessionTokenCache,
589
+ writeCachedToken: (token) => {
590
+ this.sessionTokenCache = token;
591
+ },
592
+ readPendingToken: () => this.pendingSessionToken,
593
+ writePendingToken: (token) => {
594
+ this.pendingSessionToken = token;
696
595
  }
697
- if (!targetName || targetName === "_self") {
698
- const nextPath = resolveLocalNavigationPath(href, window.location.origin);
699
- if (nextPath) {
700
- handleLocalNavigation(nextPath);
701
- return window;
702
- }
596
+ });
597
+ this.update(config);
598
+ }
599
+ update(config) {
600
+ if (config.clientId) {
601
+ this.clientId = config.clientId;
602
+ if (!this.readyPayload || typeof this.readyPayload === "object" && this.readyPayload !== null && "clientId" in this.readyPayload) {
603
+ this.readyPayload = config.readyPayload ?? { clientId: config.clientId };
703
604
  }
704
- return originalOpen(url, target, features);
705
- };
706
- document.addEventListener("click", handleDocumentClick, true);
707
- return () => {
708
- document.removeEventListener("click", handleDocumentClick, true);
709
- window.open = originalOpen;
710
- };
711
- }, [bridge, onNavigate]);
712
- (0, import_react.useEffect)(() => {
713
- if (!bridge || typeof window === "undefined") {
605
+ }
606
+ if (config.currentPath !== void 0) {
607
+ this.currentPath = config.currentPath;
608
+ }
609
+ if (config.navigationItems !== void 0) {
610
+ this.navigationItems = normalizeBridgeNavigationItems(config.navigationItems);
611
+ }
612
+ if (config.onNavigate !== void 0) {
613
+ this.onNavigate = config.onNavigate;
614
+ }
615
+ if (config.readyEventType) {
616
+ this.readyEventType = config.readyEventType;
617
+ }
618
+ if (config.readyPayload !== void 0) {
619
+ this.readyPayload = config.readyPayload;
620
+ }
621
+ if (config.targetWindow !== void 0) {
622
+ this.bridge.setTargetWindow(config.targetWindow ?? null);
623
+ }
624
+ this.syncBridgeState();
625
+ }
626
+ clearNavigationHandler() {
627
+ this.onNavigate = void 0;
628
+ }
629
+ destroy() {
630
+ this.removeNavigationRequestHandler?.();
631
+ this.removeNavigationInterceptors();
632
+ this.restoreFetch();
633
+ this.bridge.destroy();
634
+ }
635
+ syncBridgeState() {
636
+ if (!this.bridge.hasTargetWindow()) {
714
637
  return;
715
638
  }
716
- const originalFetch = globalThis.fetch.bind(globalThis);
717
- const interceptedFetch = async (input, init) => {
718
- if (!shouldAttachSessionToken(input)) {
719
- return originalFetch(input, init);
720
- }
721
- const existingAuthorization = getExistingAuthorization(input, init);
722
- if (existingAuthorization) {
723
- return originalFetch(input, init);
724
- }
725
- const nextHeaders = new Headers(
726
- input instanceof Request ? input.headers : init?.headers
639
+ this.bridge.send(this.readyEventType, this.readyPayload);
640
+ this.bridge.send("navigation:items:update", {
641
+ items: this.navigationItems
642
+ });
643
+ if (this.currentPath) {
644
+ this.bridge.send(
645
+ this.navigationUpdateEventType,
646
+ buildNavigationUpdatePayload(this.currentPath)
727
647
  );
728
- if (projectRef.current && !nextHeaders.has("X-Thor-Project")) {
729
- nextHeaders.set("X-Thor-Project", projectRef.current);
730
- }
731
- try {
732
- const sessionToken = await getSessionToken({
733
- bridge,
734
- clientId,
735
- pendingSessionTokenRef,
736
- projectRef,
737
- sessionTokenCacheRef
738
- });
739
- nextHeaders.set("Authorization", `Bearer ${sessionToken}`);
740
- } catch {
741
- if (!projectRef.current) {
742
- throw new Error("Missing Thor embedded session token");
743
- }
744
- }
745
- if (!nextHeaders.has("X-Requested-With")) {
746
- nextHeaders.set("X-Requested-With", "XMLHttpRequest");
747
- }
748
- if (input instanceof Request) {
749
- return originalFetch(
750
- new Request(input, {
751
- headers: nextHeaders
752
- })
753
- );
754
- }
755
- return originalFetch(input, {
756
- ...init,
757
- headers: nextHeaders
758
- });
759
- };
760
- globalThis.fetch = interceptedFetch;
761
- window.fetch = interceptedFetch;
762
- return () => {
763
- globalThis.fetch = originalFetch;
764
- window.fetch = originalFetch;
765
- };
766
- }, [bridge, clientId]);
767
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AppBridgeContext.Provider, { value: bridge, children });
648
+ }
649
+ }
650
+ navigate(path, message) {
651
+ if (this.onNavigate) {
652
+ this.onNavigate(path, message);
653
+ return;
654
+ }
655
+ if (dispatchNavigateEvent(this.selfWindow.document, path)) {
656
+ return;
657
+ }
658
+ this.selfWindow.location.assign(path);
659
+ }
660
+ };
661
+ function getOrCreateEmbeddedAppRuntime(config) {
662
+ const globalScope = globalThis;
663
+ const existingRuntime = globalScope[GLOBAL_RUNTIME_KEY];
664
+ if (existingRuntime) {
665
+ existingRuntime.update(config);
666
+ return existingRuntime;
667
+ }
668
+ const runtime = new EmbeddedAppRuntime(config);
669
+ globalScope[GLOBAL_RUNTIME_KEY] = runtime;
670
+ return runtime;
671
+ }
672
+ function dispatchNavigateEvent(document, path) {
673
+ const event = new CustomEvent(THOR_NAVIGATE_EVENT, {
674
+ cancelable: true,
675
+ detail: { path }
676
+ });
677
+ document.dispatchEvent(event);
678
+ return event.defaultPrevented;
768
679
  }
769
680
  function getReferrerOrigin(explicitWindow) {
770
681
  const resolvedWindow = explicitWindow ?? (typeof window !== "undefined" ? window : void 0);
@@ -778,40 +689,190 @@ function getReferrerOrigin(explicitWindow) {
778
689
  return void 0;
779
690
  }
780
691
  }
692
+ function installNavigationInterceptors({
693
+ bridge,
694
+ selfWindow,
695
+ navigate
696
+ }) {
697
+ const document = selfWindow.document;
698
+ const handleLocalNavigation = (path) => {
699
+ const sanitizedPath = sanitizeEmbeddedAppPath(path);
700
+ if (!sanitizedPath) {
701
+ return;
702
+ }
703
+ const nextPath = preserveEmbeddedAppLaunchParams(
704
+ sanitizedPath,
705
+ selfWindow.location.href
706
+ );
707
+ navigate(nextPath ?? sanitizedPath);
708
+ };
709
+ const handleDocumentClick = (event) => {
710
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
711
+ return;
712
+ }
713
+ const target = event.target;
714
+ if (!(target instanceof Element)) {
715
+ return;
716
+ }
717
+ const anchor = target.closest("a[href]");
718
+ if (!(anchor instanceof HTMLAnchorElement)) {
719
+ return;
720
+ }
721
+ if (anchor.hasAttribute("download")) {
722
+ return;
723
+ }
724
+ const targetWindow = anchor.target.toLowerCase();
725
+ const href = anchor.getAttribute("href");
726
+ if (!href) {
727
+ return;
728
+ }
729
+ if (targetWindow === "_top" || targetWindow === "_parent") {
730
+ event.preventDefault();
731
+ bridge.redirectToRemote(anchor.href);
732
+ return;
733
+ }
734
+ if (targetWindow && targetWindow !== "_self") {
735
+ return;
736
+ }
737
+ const nextPath = resolveLocalNavigationPath(href, selfWindow.location.origin);
738
+ if (!nextPath) {
739
+ return;
740
+ }
741
+ event.preventDefault();
742
+ handleLocalNavigation(nextPath);
743
+ };
744
+ const originalOpen = selfWindow.open.bind(selfWindow);
745
+ selfWindow.open = (url, target, features) => {
746
+ if (url == null) {
747
+ return originalOpen(url, target, features);
748
+ }
749
+ const href = typeof url === "string" ? url : url.toString();
750
+ const targetName = (target ?? "").toLowerCase();
751
+ if (targetName === "_top" || targetName === "_parent") {
752
+ bridge.redirectToRemote(new URL(href, selfWindow.location.href).toString());
753
+ return null;
754
+ }
755
+ if (!targetName || targetName === "_self") {
756
+ const nextPath = resolveLocalNavigationPath(
757
+ href,
758
+ selfWindow.location.origin
759
+ );
760
+ if (nextPath) {
761
+ handleLocalNavigation(nextPath);
762
+ return selfWindow;
763
+ }
764
+ }
765
+ return originalOpen(url, target, features);
766
+ };
767
+ document.addEventListener("click", handleDocumentClick, true);
768
+ return () => {
769
+ document.removeEventListener("click", handleDocumentClick, true);
770
+ selfWindow.open = originalOpen;
771
+ };
772
+ }
773
+ function installFetchInterceptor({
774
+ bridge,
775
+ getClientId,
776
+ getProject,
777
+ setProject,
778
+ readCachedToken,
779
+ writeCachedToken,
780
+ readPendingToken,
781
+ writePendingToken
782
+ }) {
783
+ if (typeof window === "undefined") {
784
+ return () => {
785
+ };
786
+ }
787
+ const originalFetch = globalThis.fetch.bind(globalThis);
788
+ const interceptedFetch = async (input, init) => {
789
+ if (!shouldAttachSessionToken(input)) {
790
+ return originalFetch(input, init);
791
+ }
792
+ const existingAuthorization = getExistingAuthorization(input, init);
793
+ if (existingAuthorization) {
794
+ return originalFetch(input, init);
795
+ }
796
+ const nextHeaders = new Headers(
797
+ input instanceof Request ? input.headers : init?.headers
798
+ );
799
+ try {
800
+ const sessionToken = await getSessionToken({
801
+ bridge,
802
+ clientId: getClientId(),
803
+ project: getProject,
804
+ setProject,
805
+ readCachedToken,
806
+ writeCachedToken,
807
+ readPendingToken,
808
+ writePendingToken
809
+ });
810
+ nextHeaders.set("Authorization", `Bearer ${sessionToken}`);
811
+ } catch {
812
+ if (!getProject()) {
813
+ throw new Error("Missing Thor embedded session token");
814
+ }
815
+ }
816
+ if (!nextHeaders.has("X-Requested-With")) {
817
+ nextHeaders.set("X-Requested-With", "XMLHttpRequest");
818
+ }
819
+ if (input instanceof Request) {
820
+ return originalFetch(
821
+ new Request(input, {
822
+ headers: nextHeaders
823
+ })
824
+ );
825
+ }
826
+ return originalFetch(input, {
827
+ ...init,
828
+ headers: nextHeaders
829
+ });
830
+ };
831
+ globalThis.fetch = interceptedFetch;
832
+ window.fetch = interceptedFetch;
833
+ return () => {
834
+ globalThis.fetch = originalFetch;
835
+ window.fetch = originalFetch;
836
+ };
837
+ }
781
838
  async function getSessionToken({
782
839
  bridge,
783
840
  clientId,
784
- pendingSessionTokenRef,
785
- projectRef,
786
- sessionTokenCacheRef
841
+ project,
842
+ setProject,
843
+ readCachedToken,
844
+ writeCachedToken,
845
+ readPendingToken,
846
+ writePendingToken
787
847
  }) {
788
- const cachedToken = sessionTokenCacheRef.current;
848
+ const cachedToken = readCachedToken();
789
849
  if (cachedToken && !isExpired(cachedToken.expiresAt)) {
790
850
  return cachedToken.token;
791
851
  }
792
- if (pendingSessionTokenRef.current) {
793
- return pendingSessionTokenRef.current;
852
+ const pendingToken = readPendingToken();
853
+ if (pendingToken) {
854
+ return pendingToken;
794
855
  }
795
- const pendingToken = bridge.getSessionToken({ clientId }).then((response) => {
856
+ const nextPendingToken = bridge.getSessionToken({ clientId }).then((response) => {
796
857
  const token = response.sessionToken ?? response.idToken;
797
858
  if (!token) {
798
859
  throw new Error("Missing Thor embedded session token");
799
860
  }
800
861
  if (response.project) {
801
- projectRef.current = response.project;
862
+ setProject(response.project);
802
863
  }
803
- sessionTokenCacheRef.current = {
864
+ writeCachedToken({
804
865
  token,
805
866
  expiresAt: normalizeTokenExpiry(token, response.exp)
806
- };
807
- pendingSessionTokenRef.current = null;
867
+ });
868
+ writePendingToken(null);
808
869
  return token;
809
870
  }).catch((error) => {
810
- pendingSessionTokenRef.current = null;
871
+ writePendingToken(null);
811
872
  throw error;
812
873
  });
813
- pendingSessionTokenRef.current = pendingToken;
814
- return pendingToken;
874
+ writePendingToken(nextPendingToken);
875
+ return nextPendingToken;
815
876
  }
816
877
  function shouldAttachSessionToken(input) {
817
878
  if (typeof window === "undefined") {
@@ -878,6 +939,70 @@ function readInitialProject(explicitWindow) {
878
939
  }
879
940
  }
880
941
 
942
+ // src/react.tsx
943
+ var import_jsx_runtime = require("react/jsx-runtime");
944
+ var AppBridgeContext = (0, import_react.createContext)(null);
945
+ function AppBridgeProvider({
946
+ children,
947
+ clientId,
948
+ currentPath,
949
+ navigationItems,
950
+ navigationEventType = "navigation:go",
951
+ navigationUpdateEventType = "navigation:update",
952
+ namespace,
953
+ onNavigate,
954
+ readyEventType = "app:ready",
955
+ readyPayload,
956
+ requestTimeoutMs,
957
+ selfWindow,
958
+ targetOrigin,
959
+ targetWindow
960
+ }) {
961
+ const [bridge, setBridge] = (0, import_react.useState)(null);
962
+ (0, import_react.useEffect)(() => {
963
+ if (typeof window === "undefined" && !selfWindow) {
964
+ return;
965
+ }
966
+ const runtime = getOrCreateEmbeddedAppRuntime({
967
+ clientId,
968
+ currentPath,
969
+ navigationEventType,
970
+ navigationItems,
971
+ navigationUpdateEventType,
972
+ namespace,
973
+ onNavigate,
974
+ readyEventType,
975
+ readyPayload,
976
+ requestTimeoutMs,
977
+ selfWindow,
978
+ targetOrigin,
979
+ targetWindow
980
+ });
981
+ setBridge(runtime.bridge);
982
+ return () => {
983
+ runtime.clearNavigationHandler();
984
+ setBridge(
985
+ (currentBridge) => currentBridge === runtime.bridge ? null : currentBridge
986
+ );
987
+ };
988
+ }, [
989
+ clientId,
990
+ currentPath,
991
+ navigationEventType,
992
+ navigationItems,
993
+ navigationUpdateEventType,
994
+ namespace,
995
+ onNavigate,
996
+ readyEventType,
997
+ readyPayload,
998
+ requestTimeoutMs,
999
+ selfWindow,
1000
+ targetOrigin,
1001
+ targetWindow
1002
+ ]);
1003
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AppBridgeContext.Provider, { value: bridge, children });
1004
+ }
1005
+
881
1006
  // src/next.tsx
882
1007
  var import_jsx_runtime2 = require("react/jsx-runtime");
883
1008
  function NextAppBridgeProvider({