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