@thor-commerce/app-bridge-react 0.7.2 → 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.
@@ -38,6 +38,7 @@ var EMBEDDED_LAUNCH_PARAMS = [
38
38
  "id_token",
39
39
  "link_source",
40
40
  "locale",
41
+ "project",
41
42
  "protocol",
42
43
  "session",
43
44
  "shop",
@@ -103,6 +104,37 @@ function sanitizeEmbeddedAppPath(path) {
103
104
  }
104
105
  return `${url.pathname}${url.search}${url.hash}`;
105
106
  }
107
+ function preserveEmbeddedAppLaunchParams(path, currentHref) {
108
+ if (!path || !currentHref) {
109
+ return path;
110
+ }
111
+ try {
112
+ const currentUrl = new URL(currentHref, NAVIGATION_BASE_URL);
113
+ const nextUrl = new URL(path, currentUrl.href);
114
+ if (nextUrl.origin !== currentUrl.origin) {
115
+ return path;
116
+ }
117
+ let changed = false;
118
+ for (const key of EMBEDDED_LAUNCH_PARAMS) {
119
+ if (!nextUrl.searchParams.has(key) && currentUrl.searchParams.has(key)) {
120
+ const value = currentUrl.searchParams.get(key);
121
+ if (value != null) {
122
+ nextUrl.searchParams.set(key, value);
123
+ changed = true;
124
+ }
125
+ }
126
+ }
127
+ if (!changed) {
128
+ return path;
129
+ }
130
+ if (/^https?:\/\//i.test(path)) {
131
+ return nextUrl.toString();
132
+ }
133
+ return `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;
134
+ } catch {
135
+ return path;
136
+ }
137
+ }
106
138
  function resolveLocalNavigationPath(href, currentOrigin) {
107
139
  if (!href || href.startsWith("#")) {
108
140
  return null;
@@ -177,13 +209,6 @@ function normalizeBridgeNavigationItems(items) {
177
209
  }
178
210
 
179
211
  // src/core.ts
180
- var PRESERVED_EMBEDDED_DOCUMENT_PARAMS = [
181
- "hmac",
182
- "id_token",
183
- "project",
184
- "tenant",
185
- "timestamp"
186
- ];
187
212
  var DEFAULT_NAMESPACE = "thorcommerce:app-bridge";
188
213
  var DEFAULT_TIMEOUT_MS = 1e4;
189
214
  var DEFAULT_REDIRECT_EVENT_TYPE = "navigation:redirect";
@@ -497,262 +522,159 @@ var AppBridge = class {
497
522
  throw new Error("AppBridge could not resolve a browser window for redirect.");
498
523
  }
499
524
  this.selfWindow.location.assign(
500
- preserveEmbeddedDocumentParams(destination, this.selfWindow.location.href)
525
+ preserveEmbeddedAppLaunchParams(destination, this.selfWindow.location.href) ?? destination
501
526
  );
502
527
  }
503
528
  };
504
- function preserveEmbeddedDocumentParams(destination, currentHref) {
505
- try {
506
- const currentUrl = new URL(currentHref);
507
- const nextUrl = new URL(destination, currentUrl.href);
508
- if (nextUrl.origin !== currentUrl.origin) {
509
- return destination;
510
- }
511
- let changed = false;
512
- for (const key of PRESERVED_EMBEDDED_DOCUMENT_PARAMS) {
513
- if (!nextUrl.searchParams.has(key) && currentUrl.searchParams.has(key)) {
514
- const value = currentUrl.searchParams.get(key);
515
- if (value != null) {
516
- nextUrl.searchParams.set(key, value);
517
- changed = true;
518
- }
519
- }
520
- }
521
- return changed ? nextUrl.toString() : destination;
522
- } catch {
523
- return destination;
524
- }
525
- }
526
529
  function createAppBridge(options = {}) {
527
530
  return new AppBridge(options);
528
531
  }
529
532
 
530
- // src/react.tsx
531
- var import_jsx_runtime = require("react/jsx-runtime");
532
- var AppBridgeContext = (0, import_react.createContext)(null);
533
- function AppBridgeProvider({
534
- children,
535
- clientId,
536
- currentPath,
537
- navigationItems,
538
- navigationEventType = "navigation:go",
539
- navigationUpdateEventType = "navigation:update",
540
- namespace,
541
- onNavigate,
542
- readyEventType = "app:ready",
543
- readyPayload,
544
- requestTimeoutMs,
545
- selfWindow,
546
- targetOrigin,
547
- targetWindow
548
- }) {
549
- const [bridge, setBridge] = (0, import_react.useState)(null);
550
- const onNavigateRef = (0, import_react.useRef)(onNavigate);
551
- const normalizedNavigationItems = (0, import_react.useMemo)(
552
- () => normalizeBridgeNavigationItems(navigationItems),
553
- [navigationItems]
554
- );
555
- const sessionTokenCacheRef = (0, import_react.useRef)(null);
556
- const projectRef = (0, import_react.useRef)(readInitialProject(selfWindow));
557
- const pendingSessionTokenRef = (0, import_react.useRef)(null);
558
- (0, import_react.useEffect)(() => {
559
- onNavigateRef.current = onNavigate;
560
- }, [onNavigate]);
561
- (0, import_react.useEffect)(() => {
562
- if (typeof window === "undefined" && !selfWindow) {
563
- 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.");
564
548
  }
565
- const resolvedTargetOrigin = targetOrigin ?? getReferrerOrigin(selfWindow);
566
- const resolvedAllowedOrigins = resolvedTargetOrigin ? [resolvedTargetOrigin] : void 0;
567
- const nextBridge = createAppBridge({
568
- allowedOrigins: resolvedAllowedOrigins,
569
- clientId,
570
- namespace,
571
- requestTimeoutMs,
572
- 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,
573
559
  source: "embedded-app",
574
560
  target: "dashboard",
575
- targetOrigin: resolvedTargetOrigin,
576
- targetWindow
577
- });
578
- setBridge(nextBridge);
579
- return () => {
580
- setBridge((currentBridge) => currentBridge === nextBridge ? null : currentBridge);
581
- nextBridge.destroy();
582
- };
583
- }, [
584
- clientId,
585
- namespace,
586
- requestTimeoutMs,
587
- selfWindow,
588
- targetOrigin,
589
- targetWindow
590
- ]);
591
- (0, import_react.useEffect)(() => {
592
- if (!bridge || !bridge.hasTargetWindow()) {
593
- return;
594
- }
595
- bridge.send(
596
- readyEventType,
597
- readyPayload ?? {
598
- clientId
599
- }
600
- );
601
- }, [bridge, clientId, readyEventType, readyPayload]);
602
- (0, import_react.useEffect)(() => {
603
- if (!bridge || !bridge.hasTargetWindow()) {
604
- return;
605
- }
606
- bridge.send("navigation:items:update", {
607
- items: normalizedNavigationItems
561
+ targetOrigin: this.targetOrigin,
562
+ targetWindow: config.targetWindow
608
563
  });
609
- }, [bridge, normalizedNavigationItems]);
610
- (0, import_react.useEffect)(() => {
611
- if (!bridge || !bridge.hasTargetWindow() || !currentPath) {
612
- return;
613
- }
614
- bridge.send(navigationUpdateEventType, buildNavigationUpdatePayload(currentPath));
615
- }, [bridge, currentPath, navigationUpdateEventType]);
616
- (0, import_react.useEffect)(() => {
617
- if (!bridge || !onNavigate) {
618
- return;
619
- }
620
- return bridge.on(navigationEventType, (message) => {
564
+ this.removeNavigationRequestHandler = this.bridge.on(this.navigationEventType, (message) => {
621
565
  const destination = resolveNavigationDestination(message.payload);
622
566
  if (!destination) {
623
567
  return;
624
568
  }
625
- onNavigateRef.current?.(destination, message);
569
+ const nextPath = preserveEmbeddedAppLaunchParams(
570
+ destination,
571
+ this.selfWindow.location.href
572
+ );
573
+ this.navigate(nextPath ?? destination, message);
626
574
  });
627
- }, [bridge, navigationEventType, onNavigate]);
628
- (0, import_react.useEffect)(() => {
629
- if (!bridge || !onNavigate || typeof document === "undefined" || typeof window === "undefined") {
630
- return;
631
- }
632
- const handleLocalNavigation = (path) => {
633
- const sanitizedPath = sanitizeEmbeddedAppPath(path);
634
- if (!sanitizedPath) {
635
- return;
636
- }
637
- onNavigateRef.current?.(sanitizedPath);
638
- };
639
- const handleDocumentClick = (event) => {
640
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
641
- return;
642
- }
643
- const target = event.target;
644
- if (!(target instanceof Element)) {
645
- return;
646
- }
647
- const anchor = target.closest("a[href]");
648
- if (!(anchor instanceof HTMLAnchorElement)) {
649
- return;
650
- }
651
- if (anchor.hasAttribute("download")) {
652
- return;
653
- }
654
- const targetWindow2 = anchor.target.toLowerCase();
655
- const href = anchor.getAttribute("href");
656
- if (!href) {
657
- return;
658
- }
659
- if (targetWindow2 === "_top" || targetWindow2 === "_parent") {
660
- event.preventDefault();
661
- bridge.redirectToRemote(anchor.href);
662
- return;
663
- }
664
- if (targetWindow2 && targetWindow2 !== "_self") {
665
- return;
666
- }
667
- const nextPath = resolveLocalNavigationPath(href, window.location.origin);
668
- if (!nextPath) {
669
- return;
670
- }
671
- event.preventDefault();
672
- handleLocalNavigation(nextPath);
673
- };
674
- const originalOpen = window.open.bind(window);
675
- window.open = (url, target, features) => {
676
- if (url == null) {
677
- return originalOpen(url, target, features);
678
- }
679
- const href = typeof url === "string" ? url : url.toString();
680
- const targetName = (target ?? "").toLowerCase();
681
- if (targetName === "_top" || targetName === "_parent") {
682
- bridge.redirectToRemote(new URL(href, window.location.href).toString());
683
- 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;
684
594
  }
685
- if (!targetName || targetName === "_self") {
686
- const nextPath = resolveLocalNavigationPath(href, window.location.origin);
687
- if (nextPath) {
688
- handleLocalNavigation(nextPath);
689
- return window;
690
- }
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 };
691
603
  }
692
- return originalOpen(url, target, features);
693
- };
694
- document.addEventListener("click", handleDocumentClick, true);
695
- return () => {
696
- document.removeEventListener("click", handleDocumentClick, true);
697
- window.open = originalOpen;
698
- };
699
- }, [bridge, onNavigate]);
700
- (0, import_react.useEffect)(() => {
701
- 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()) {
702
636
  return;
703
637
  }
704
- const originalFetch = globalThis.fetch.bind(globalThis);
705
- const interceptedFetch = async (input, init) => {
706
- if (!shouldAttachSessionToken(input)) {
707
- return originalFetch(input, init);
708
- }
709
- const existingAuthorization = getExistingAuthorization(input, init);
710
- if (existingAuthorization) {
711
- return originalFetch(input, init);
712
- }
713
- const nextHeaders = new Headers(
714
- 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)
715
646
  );
716
- if (projectRef.current && !nextHeaders.has("X-Thor-Project")) {
717
- nextHeaders.set("X-Thor-Project", projectRef.current);
718
- }
719
- try {
720
- const sessionToken = await getSessionToken({
721
- bridge,
722
- clientId,
723
- pendingSessionTokenRef,
724
- projectRef,
725
- sessionTokenCacheRef
726
- });
727
- nextHeaders.set("Authorization", `Bearer ${sessionToken}`);
728
- } catch {
729
- if (!projectRef.current) {
730
- throw new Error("Missing Thor embedded session token");
731
- }
732
- }
733
- if (!nextHeaders.has("X-Requested-With")) {
734
- nextHeaders.set("X-Requested-With", "XMLHttpRequest");
735
- }
736
- if (input instanceof Request) {
737
- return originalFetch(
738
- new Request(input, {
739
- headers: nextHeaders
740
- })
741
- );
742
- }
743
- return originalFetch(input, {
744
- ...init,
745
- headers: nextHeaders
746
- });
747
- };
748
- globalThis.fetch = interceptedFetch;
749
- window.fetch = interceptedFetch;
750
- return () => {
751
- globalThis.fetch = originalFetch;
752
- window.fetch = originalFetch;
753
- };
754
- }, [bridge, clientId]);
755
- 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;
756
678
  }
757
679
  function getReferrerOrigin(explicitWindow) {
758
680
  const resolvedWindow = explicitWindow ?? (typeof window !== "undefined" ? window : void 0);
@@ -766,40 +688,190 @@ function getReferrerOrigin(explicitWindow) {
766
688
  return void 0;
767
689
  }
768
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
+ }
769
837
  async function getSessionToken({
770
838
  bridge,
771
839
  clientId,
772
- pendingSessionTokenRef,
773
- projectRef,
774
- sessionTokenCacheRef
840
+ project,
841
+ setProject,
842
+ readCachedToken,
843
+ writeCachedToken,
844
+ readPendingToken,
845
+ writePendingToken
775
846
  }) {
776
- const cachedToken = sessionTokenCacheRef.current;
847
+ const cachedToken = readCachedToken();
777
848
  if (cachedToken && !isExpired(cachedToken.expiresAt)) {
778
849
  return cachedToken.token;
779
850
  }
780
- if (pendingSessionTokenRef.current) {
781
- return pendingSessionTokenRef.current;
851
+ const pendingToken = readPendingToken();
852
+ if (pendingToken) {
853
+ return pendingToken;
782
854
  }
783
- const pendingToken = bridge.getSessionToken({ clientId }).then((response) => {
855
+ const nextPendingToken = bridge.getSessionToken({ clientId }).then((response) => {
784
856
  const token = response.sessionToken ?? response.idToken;
785
857
  if (!token) {
786
858
  throw new Error("Missing Thor embedded session token");
787
859
  }
788
860
  if (response.project) {
789
- projectRef.current = response.project;
861
+ setProject(response.project);
790
862
  }
791
- sessionTokenCacheRef.current = {
863
+ writeCachedToken({
792
864
  token,
793
865
  expiresAt: normalizeTokenExpiry(token, response.exp)
794
- };
795
- pendingSessionTokenRef.current = null;
866
+ });
867
+ writePendingToken(null);
796
868
  return token;
797
869
  }).catch((error) => {
798
- pendingSessionTokenRef.current = null;
870
+ writePendingToken(null);
799
871
  throw error;
800
872
  });
801
- pendingSessionTokenRef.current = pendingToken;
802
- return pendingToken;
873
+ writePendingToken(nextPendingToken);
874
+ return nextPendingToken;
803
875
  }
804
876
  function shouldAttachSessionToken(input) {
805
877
  if (typeof window === "undefined") {
@@ -866,6 +938,70 @@ function readInitialProject(explicitWindow) {
866
938
  }
867
939
  }
868
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
+
869
1005
  // src/react-router.tsx
870
1006
  var import_jsx_runtime2 = require("react/jsx-runtime");
871
1007
  function ReactRouterAppBridgeProvider({