@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.
@@ -499,251 +499,154 @@ function createAppBridge(options = {}) {
499
499
  return new AppBridge(options);
500
500
  }
501
501
 
502
- // src/react.tsx
503
- import {
504
- createContext,
505
- useContext,
506
- useEffect,
507
- useMemo,
508
- useRef,
509
- useState
510
- } from "react";
511
- import { jsx } from "react/jsx-runtime";
512
- var AppBridgeContext = createContext(null);
513
- function AppBridgeProvider({
514
- children,
515
- clientId,
516
- currentPath,
517
- navigationItems,
518
- navigationEventType = "navigation:go",
519
- navigationUpdateEventType = "navigation:update",
520
- namespace,
521
- onNavigate,
522
- readyEventType = "app:ready",
523
- readyPayload,
524
- requestTimeoutMs,
525
- selfWindow,
526
- targetOrigin,
527
- targetWindow
528
- }) {
529
- const [bridge, setBridge] = useState(null);
530
- const onNavigateRef = useRef(onNavigate);
531
- const normalizedNavigationItems = useMemo(
532
- () => normalizeBridgeNavigationItems(navigationItems),
533
- [navigationItems]
534
- );
535
- const sessionTokenCacheRef = useRef(null);
536
- const projectRef = useRef(readInitialProject(selfWindow));
537
- const pendingSessionTokenRef = useRef(null);
538
- useEffect(() => {
539
- onNavigateRef.current = onNavigate;
540
- }, [onNavigate]);
541
- useEffect(() => {
542
- if (typeof window === "undefined" && !selfWindow) {
543
- return;
544
- }
545
- const resolvedTargetOrigin = targetOrigin ?? getReferrerOrigin(selfWindow);
546
- const resolvedAllowedOrigins = resolvedTargetOrigin ? [resolvedTargetOrigin] : void 0;
547
- const nextBridge = createAppBridge({
548
- allowedOrigins: resolvedAllowedOrigins,
549
- clientId,
550
- namespace,
551
- requestTimeoutMs,
552
- selfWindow,
502
+ // src/runtime.ts
503
+ var GLOBAL_RUNTIME_KEY = "__thorEmbeddedAppRuntime__";
504
+ var THOR_NAVIGATE_EVENT = "thor:navigate";
505
+ var EmbeddedAppRuntime = class {
506
+ constructor(config) {
507
+ this.currentPath = null;
508
+ this.navigationItems = [];
509
+ this.navigationEventType = "navigation:go";
510
+ this.navigationUpdateEventType = "navigation:update";
511
+ this.readyEventType = "app:ready";
512
+ this.sessionTokenCache = null;
513
+ this.pendingSessionToken = null;
514
+ const resolvedWindow = config.selfWindow ?? (typeof window !== "undefined" ? window : void 0);
515
+ if (!resolvedWindow) {
516
+ throw new Error("EmbeddedAppRuntime requires a browser window.");
517
+ }
518
+ this.selfWindow = resolvedWindow;
519
+ this.targetOrigin = config.targetOrigin ?? getReferrerOrigin(resolvedWindow);
520
+ this.clientId = config.clientId;
521
+ this.project = readInitialProject(resolvedWindow);
522
+ this.readyPayload = config.readyPayload ?? { clientId: config.clientId };
523
+ this.bridge = createAppBridge({
524
+ allowedOrigins: this.targetOrigin ? [this.targetOrigin] : void 0,
525
+ namespace: config.namespace,
526
+ requestTimeoutMs: config.requestTimeoutMs,
527
+ selfWindow: resolvedWindow,
553
528
  source: "embedded-app",
554
529
  target: "dashboard",
555
- targetOrigin: resolvedTargetOrigin,
556
- targetWindow
557
- });
558
- setBridge(nextBridge);
559
- return () => {
560
- setBridge((currentBridge) => currentBridge === nextBridge ? null : currentBridge);
561
- nextBridge.destroy();
562
- };
563
- }, [
564
- clientId,
565
- namespace,
566
- requestTimeoutMs,
567
- selfWindow,
568
- targetOrigin,
569
- targetWindow
570
- ]);
571
- useEffect(() => {
572
- if (!bridge || !bridge.hasTargetWindow()) {
573
- return;
574
- }
575
- bridge.send(
576
- readyEventType,
577
- readyPayload ?? {
578
- clientId
579
- }
580
- );
581
- }, [bridge, clientId, readyEventType, readyPayload]);
582
- useEffect(() => {
583
- if (!bridge || !bridge.hasTargetWindow()) {
584
- return;
585
- }
586
- bridge.send("navigation:items:update", {
587
- items: normalizedNavigationItems
530
+ targetOrigin: this.targetOrigin,
531
+ targetWindow: config.targetWindow
588
532
  });
589
- }, [bridge, normalizedNavigationItems]);
590
- useEffect(() => {
591
- if (!bridge || !bridge.hasTargetWindow() || !currentPath) {
592
- return;
593
- }
594
- bridge.send(navigationUpdateEventType, buildNavigationUpdatePayload(currentPath));
595
- }, [bridge, currentPath, navigationUpdateEventType]);
596
- useEffect(() => {
597
- if (!bridge || !onNavigate) {
598
- return;
599
- }
600
- return bridge.on(navigationEventType, (message) => {
533
+ this.removeNavigationRequestHandler = this.bridge.on(this.navigationEventType, (message) => {
601
534
  const destination = resolveNavigationDestination(message.payload);
602
535
  if (!destination) {
603
536
  return;
604
537
  }
605
538
  const nextPath = preserveEmbeddedAppLaunchParams(
606
539
  destination,
607
- typeof window !== "undefined" ? window.location.href : void 0
540
+ this.selfWindow.location.href
608
541
  );
609
- onNavigateRef.current?.(nextPath ?? destination, message);
542
+ this.navigate(nextPath ?? destination, message);
610
543
  });
611
- }, [bridge, navigationEventType, onNavigate]);
612
- useEffect(() => {
613
- if (!bridge || !onNavigate || typeof document === "undefined" || typeof window === "undefined") {
614
- return;
615
- }
616
- const handleLocalNavigation = (path) => {
617
- const sanitizedPath = sanitizeEmbeddedAppPath(path);
618
- if (!sanitizedPath) {
619
- return;
620
- }
621
- const nextPath = preserveEmbeddedAppLaunchParams(
622
- sanitizedPath,
623
- window.location.href
624
- );
625
- onNavigateRef.current?.(nextPath ?? sanitizedPath);
626
- };
627
- const handleDocumentClick = (event) => {
628
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
629
- return;
630
- }
631
- const target = event.target;
632
- if (!(target instanceof Element)) {
633
- return;
634
- }
635
- const anchor = target.closest("a[href]");
636
- if (!(anchor instanceof HTMLAnchorElement)) {
637
- return;
638
- }
639
- if (anchor.hasAttribute("download")) {
640
- return;
641
- }
642
- const targetWindow2 = anchor.target.toLowerCase();
643
- const href = anchor.getAttribute("href");
644
- if (!href) {
645
- return;
646
- }
647
- if (targetWindow2 === "_top" || targetWindow2 === "_parent") {
648
- event.preventDefault();
649
- bridge.redirectToRemote(anchor.href);
650
- return;
651
- }
652
- if (targetWindow2 && targetWindow2 !== "_self") {
653
- return;
654
- }
655
- const nextPath = resolveLocalNavigationPath(href, window.location.origin);
656
- if (!nextPath) {
657
- return;
658
- }
659
- event.preventDefault();
660
- handleLocalNavigation(nextPath);
661
- };
662
- const originalOpen = window.open.bind(window);
663
- window.open = (url, target, features) => {
664
- if (url == null) {
665
- return originalOpen(url, target, features);
666
- }
667
- const href = typeof url === "string" ? url : url.toString();
668
- const targetName = (target ?? "").toLowerCase();
669
- if (targetName === "_top" || targetName === "_parent") {
670
- bridge.redirectToRemote(new URL(href, window.location.href).toString());
671
- return null;
544
+ this.removeNavigationInterceptors = installNavigationInterceptors({
545
+ bridge: this.bridge,
546
+ selfWindow: this.selfWindow,
547
+ navigate: (path) => this.navigate(path)
548
+ });
549
+ this.restoreFetch = installFetchInterceptor({
550
+ bridge: this.bridge,
551
+ getClientId: () => this.clientId,
552
+ getProject: () => this.project,
553
+ setProject: (project) => {
554
+ this.project = project;
555
+ },
556
+ readCachedToken: () => this.sessionTokenCache,
557
+ writeCachedToken: (token) => {
558
+ this.sessionTokenCache = token;
559
+ },
560
+ readPendingToken: () => this.pendingSessionToken,
561
+ writePendingToken: (token) => {
562
+ this.pendingSessionToken = token;
672
563
  }
673
- if (!targetName || targetName === "_self") {
674
- const nextPath = resolveLocalNavigationPath(href, window.location.origin);
675
- if (nextPath) {
676
- handleLocalNavigation(nextPath);
677
- return window;
678
- }
564
+ });
565
+ this.update(config);
566
+ }
567
+ update(config) {
568
+ if (config.clientId) {
569
+ this.clientId = config.clientId;
570
+ if (!this.readyPayload || typeof this.readyPayload === "object" && this.readyPayload !== null && "clientId" in this.readyPayload) {
571
+ this.readyPayload = config.readyPayload ?? { clientId: config.clientId };
679
572
  }
680
- return originalOpen(url, target, features);
681
- };
682
- document.addEventListener("click", handleDocumentClick, true);
683
- return () => {
684
- document.removeEventListener("click", handleDocumentClick, true);
685
- window.open = originalOpen;
686
- };
687
- }, [bridge, onNavigate]);
688
- useEffect(() => {
689
- if (!bridge || typeof window === "undefined") {
573
+ }
574
+ if (config.currentPath !== void 0) {
575
+ this.currentPath = config.currentPath;
576
+ }
577
+ if (config.navigationItems !== void 0) {
578
+ this.navigationItems = normalizeBridgeNavigationItems(config.navigationItems);
579
+ }
580
+ if (config.onNavigate !== void 0) {
581
+ this.onNavigate = config.onNavigate;
582
+ }
583
+ if (config.readyEventType) {
584
+ this.readyEventType = config.readyEventType;
585
+ }
586
+ if (config.readyPayload !== void 0) {
587
+ this.readyPayload = config.readyPayload;
588
+ }
589
+ if (config.targetWindow !== void 0) {
590
+ this.bridge.setTargetWindow(config.targetWindow ?? null);
591
+ }
592
+ this.syncBridgeState();
593
+ }
594
+ clearNavigationHandler() {
595
+ this.onNavigate = void 0;
596
+ }
597
+ destroy() {
598
+ this.removeNavigationRequestHandler?.();
599
+ this.removeNavigationInterceptors();
600
+ this.restoreFetch();
601
+ this.bridge.destroy();
602
+ }
603
+ syncBridgeState() {
604
+ if (!this.bridge.hasTargetWindow()) {
690
605
  return;
691
606
  }
692
- const originalFetch = globalThis.fetch.bind(globalThis);
693
- const interceptedFetch = async (input, init) => {
694
- if (!shouldAttachSessionToken(input)) {
695
- return originalFetch(input, init);
696
- }
697
- const existingAuthorization = getExistingAuthorization(input, init);
698
- if (existingAuthorization) {
699
- return originalFetch(input, init);
700
- }
701
- const nextHeaders = new Headers(
702
- input instanceof Request ? input.headers : init?.headers
607
+ this.bridge.send(this.readyEventType, this.readyPayload);
608
+ this.bridge.send("navigation:items:update", {
609
+ items: this.navigationItems
610
+ });
611
+ if (this.currentPath) {
612
+ this.bridge.send(
613
+ this.navigationUpdateEventType,
614
+ buildNavigationUpdatePayload(this.currentPath)
703
615
  );
704
- if (projectRef.current && !nextHeaders.has("X-Thor-Project")) {
705
- nextHeaders.set("X-Thor-Project", projectRef.current);
706
- }
707
- try {
708
- const sessionToken = await getSessionToken({
709
- bridge,
710
- clientId,
711
- pendingSessionTokenRef,
712
- projectRef,
713
- sessionTokenCacheRef
714
- });
715
- nextHeaders.set("Authorization", `Bearer ${sessionToken}`);
716
- } catch {
717
- if (!projectRef.current) {
718
- throw new Error("Missing Thor embedded session token");
719
- }
720
- }
721
- if (!nextHeaders.has("X-Requested-With")) {
722
- nextHeaders.set("X-Requested-With", "XMLHttpRequest");
723
- }
724
- if (input instanceof Request) {
725
- return originalFetch(
726
- new Request(input, {
727
- headers: nextHeaders
728
- })
729
- );
730
- }
731
- return originalFetch(input, {
732
- ...init,
733
- headers: nextHeaders
734
- });
735
- };
736
- globalThis.fetch = interceptedFetch;
737
- window.fetch = interceptedFetch;
738
- return () => {
739
- globalThis.fetch = originalFetch;
740
- window.fetch = originalFetch;
741
- };
742
- }, [bridge, clientId]);
743
- return /* @__PURE__ */ jsx(AppBridgeContext.Provider, { value: bridge, children });
616
+ }
617
+ }
618
+ navigate(path, message) {
619
+ if (this.onNavigate) {
620
+ this.onNavigate(path, message);
621
+ return;
622
+ }
623
+ if (dispatchNavigateEvent(this.selfWindow.document, path)) {
624
+ return;
625
+ }
626
+ this.selfWindow.location.assign(path);
627
+ }
628
+ };
629
+ function getOrCreateEmbeddedAppRuntime(config) {
630
+ const globalScope = globalThis;
631
+ const existingRuntime = globalScope[GLOBAL_RUNTIME_KEY];
632
+ if (existingRuntime) {
633
+ existingRuntime.update(config);
634
+ return existingRuntime;
635
+ }
636
+ const runtime = new EmbeddedAppRuntime(config);
637
+ globalScope[GLOBAL_RUNTIME_KEY] = runtime;
638
+ return runtime;
744
639
  }
745
- function useAppBridge() {
746
- return useContext(AppBridgeContext);
640
+ function getEmbeddedAppRuntime() {
641
+ return globalThis[GLOBAL_RUNTIME_KEY] ?? null;
642
+ }
643
+ function dispatchNavigateEvent(document, path) {
644
+ const event = new CustomEvent(THOR_NAVIGATE_EVENT, {
645
+ cancelable: true,
646
+ detail: { path }
647
+ });
648
+ document.dispatchEvent(event);
649
+ return event.defaultPrevented;
747
650
  }
748
651
  function getReferrerOrigin(explicitWindow) {
749
652
  const resolvedWindow = explicitWindow ?? (typeof window !== "undefined" ? window : void 0);
@@ -757,40 +660,190 @@ function getReferrerOrigin(explicitWindow) {
757
660
  return void 0;
758
661
  }
759
662
  }
663
+ function installNavigationInterceptors({
664
+ bridge,
665
+ selfWindow,
666
+ navigate
667
+ }) {
668
+ const document = selfWindow.document;
669
+ const handleLocalNavigation = (path) => {
670
+ const sanitizedPath = sanitizeEmbeddedAppPath(path);
671
+ if (!sanitizedPath) {
672
+ return;
673
+ }
674
+ const nextPath = preserveEmbeddedAppLaunchParams(
675
+ sanitizedPath,
676
+ selfWindow.location.href
677
+ );
678
+ navigate(nextPath ?? sanitizedPath);
679
+ };
680
+ const handleDocumentClick = (event) => {
681
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
682
+ return;
683
+ }
684
+ const target = event.target;
685
+ if (!(target instanceof Element)) {
686
+ return;
687
+ }
688
+ const anchor = target.closest("a[href]");
689
+ if (!(anchor instanceof HTMLAnchorElement)) {
690
+ return;
691
+ }
692
+ if (anchor.hasAttribute("download")) {
693
+ return;
694
+ }
695
+ const targetWindow = anchor.target.toLowerCase();
696
+ const href = anchor.getAttribute("href");
697
+ if (!href) {
698
+ return;
699
+ }
700
+ if (targetWindow === "_top" || targetWindow === "_parent") {
701
+ event.preventDefault();
702
+ bridge.redirectToRemote(anchor.href);
703
+ return;
704
+ }
705
+ if (targetWindow && targetWindow !== "_self") {
706
+ return;
707
+ }
708
+ const nextPath = resolveLocalNavigationPath(href, selfWindow.location.origin);
709
+ if (!nextPath) {
710
+ return;
711
+ }
712
+ event.preventDefault();
713
+ handleLocalNavigation(nextPath);
714
+ };
715
+ const originalOpen = selfWindow.open.bind(selfWindow);
716
+ selfWindow.open = (url, target, features) => {
717
+ if (url == null) {
718
+ return originalOpen(url, target, features);
719
+ }
720
+ const href = typeof url === "string" ? url : url.toString();
721
+ const targetName = (target ?? "").toLowerCase();
722
+ if (targetName === "_top" || targetName === "_parent") {
723
+ bridge.redirectToRemote(new URL(href, selfWindow.location.href).toString());
724
+ return null;
725
+ }
726
+ if (!targetName || targetName === "_self") {
727
+ const nextPath = resolveLocalNavigationPath(
728
+ href,
729
+ selfWindow.location.origin
730
+ );
731
+ if (nextPath) {
732
+ handleLocalNavigation(nextPath);
733
+ return selfWindow;
734
+ }
735
+ }
736
+ return originalOpen(url, target, features);
737
+ };
738
+ document.addEventListener("click", handleDocumentClick, true);
739
+ return () => {
740
+ document.removeEventListener("click", handleDocumentClick, true);
741
+ selfWindow.open = originalOpen;
742
+ };
743
+ }
744
+ function installFetchInterceptor({
745
+ bridge,
746
+ getClientId,
747
+ getProject,
748
+ setProject,
749
+ readCachedToken,
750
+ writeCachedToken,
751
+ readPendingToken,
752
+ writePendingToken
753
+ }) {
754
+ if (typeof window === "undefined") {
755
+ return () => {
756
+ };
757
+ }
758
+ const originalFetch = globalThis.fetch.bind(globalThis);
759
+ const interceptedFetch = async (input, init) => {
760
+ if (!shouldAttachSessionToken(input)) {
761
+ return originalFetch(input, init);
762
+ }
763
+ const existingAuthorization = getExistingAuthorization(input, init);
764
+ if (existingAuthorization) {
765
+ return originalFetch(input, init);
766
+ }
767
+ const nextHeaders = new Headers(
768
+ input instanceof Request ? input.headers : init?.headers
769
+ );
770
+ try {
771
+ const sessionToken = await getSessionToken({
772
+ bridge,
773
+ clientId: getClientId(),
774
+ project: getProject,
775
+ setProject,
776
+ readCachedToken,
777
+ writeCachedToken,
778
+ readPendingToken,
779
+ writePendingToken
780
+ });
781
+ nextHeaders.set("Authorization", `Bearer ${sessionToken}`);
782
+ } catch {
783
+ if (!getProject()) {
784
+ throw new Error("Missing Thor embedded session token");
785
+ }
786
+ }
787
+ if (!nextHeaders.has("X-Requested-With")) {
788
+ nextHeaders.set("X-Requested-With", "XMLHttpRequest");
789
+ }
790
+ if (input instanceof Request) {
791
+ return originalFetch(
792
+ new Request(input, {
793
+ headers: nextHeaders
794
+ })
795
+ );
796
+ }
797
+ return originalFetch(input, {
798
+ ...init,
799
+ headers: nextHeaders
800
+ });
801
+ };
802
+ globalThis.fetch = interceptedFetch;
803
+ window.fetch = interceptedFetch;
804
+ return () => {
805
+ globalThis.fetch = originalFetch;
806
+ window.fetch = originalFetch;
807
+ };
808
+ }
760
809
  async function getSessionToken({
761
810
  bridge,
762
811
  clientId,
763
- pendingSessionTokenRef,
764
- projectRef,
765
- sessionTokenCacheRef
812
+ project,
813
+ setProject,
814
+ readCachedToken,
815
+ writeCachedToken,
816
+ readPendingToken,
817
+ writePendingToken
766
818
  }) {
767
- const cachedToken = sessionTokenCacheRef.current;
819
+ const cachedToken = readCachedToken();
768
820
  if (cachedToken && !isExpired(cachedToken.expiresAt)) {
769
821
  return cachedToken.token;
770
822
  }
771
- if (pendingSessionTokenRef.current) {
772
- return pendingSessionTokenRef.current;
823
+ const pendingToken = readPendingToken();
824
+ if (pendingToken) {
825
+ return pendingToken;
773
826
  }
774
- const pendingToken = bridge.getSessionToken({ clientId }).then((response) => {
827
+ const nextPendingToken = bridge.getSessionToken({ clientId }).then((response) => {
775
828
  const token = response.sessionToken ?? response.idToken;
776
829
  if (!token) {
777
830
  throw new Error("Missing Thor embedded session token");
778
831
  }
779
832
  if (response.project) {
780
- projectRef.current = response.project;
833
+ setProject(response.project);
781
834
  }
782
- sessionTokenCacheRef.current = {
835
+ writeCachedToken({
783
836
  token,
784
837
  expiresAt: normalizeTokenExpiry(token, response.exp)
785
- };
786
- pendingSessionTokenRef.current = null;
838
+ });
839
+ writePendingToken(null);
787
840
  return token;
788
841
  }).catch((error) => {
789
- pendingSessionTokenRef.current = null;
842
+ writePendingToken(null);
790
843
  throw error;
791
844
  });
792
- pendingSessionTokenRef.current = pendingToken;
793
- return pendingToken;
845
+ writePendingToken(nextPendingToken);
846
+ return nextPendingToken;
794
847
  }
795
848
  function shouldAttachSessionToken(input) {
796
849
  if (typeof window === "undefined") {
@@ -869,7 +922,9 @@ export {
869
922
  isBridgeMessage,
870
923
  AppBridge,
871
924
  createAppBridge,
872
- AppBridgeProvider,
873
- useAppBridge
925
+ THOR_NAVIGATE_EVENT,
926
+ EmbeddedAppRuntime,
927
+ getOrCreateEmbeddedAppRuntime,
928
+ getEmbeddedAppRuntime
874
929
  };
875
- //# sourceMappingURL=chunk-FGZGTJQS.js.map
930
+ //# sourceMappingURL=chunk-IRTDLWEQ.js.map