diff --git a/packages/template/src/dev-tool/dev-tool-core.ts b/packages/template/src/dev-tool/dev-tool-core.ts index c0bdd3c9e..6191623b5 100644 --- a/packages/template/src/dev-tool/dev-tool-core.ts +++ b/packages/template/src/dev-tool/dev-tool-core.ts @@ -2,7 +2,7 @@ import type { RequestLogEntry } from "@hexclave/shared/dist/interface/client-interface"; import { DEV_TOOL_ROOT_ID } from "@hexclave/shared/dist/utils/dev-tool"; -import { runAsynchronously } from "@hexclave/shared/dist/utils/promises"; +import { runAsynchronously, runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises"; import { isLocalhost } from "@hexclave/shared/dist/utils/urls"; import type { StackClientApp } from "../lib/hexclave-app"; import { envVars } from "../generated/env"; @@ -1933,11 +1933,6 @@ function createComponentsTab(app: StackClientApp): HTMLElement { }); } - function getCompactUrl(url: string): string { - const resolved = new URL(url, window.location.origin); - return `${resolved.pathname}${resolved.search}${resolved.hash}`; - } - const sidebar = h('div', { className: 'sdt-pg-sidebar' }); const mainArea = h('div', { className: 'sdt-pg-main' }); @@ -1988,7 +1983,6 @@ function createComponentsTab(app: StackClientApp): HTMLElement { const header = h('div', { className: 'sdt-pg-header' }); const headerTop = h('div', { className: 'sdt-pg-header-top' }); headerTop.appendChild(h('h3', { className: 'sdt-pg-title' }, `${page.label} Page`)); - headerTop.appendChild(h('a', { href: page.url, target: '_blank', rel: 'noopener noreferrer', className: 'sdt-pg-title-url' }, getCompactUrl(page.url))); if (page.versionStatus === 'outdated') { headerTop.appendChild(h('span', { className: 'sdt-pg-badge sdt-pg-badge-outdated' }, 'Outdated')); } @@ -2000,8 +1994,19 @@ function createComponentsTab(app: StackClientApp): HTMLElement { const openBtn = h('button', { className: 'sdt-pg-copy-btn sdt-pg-open-btn' }); setHtml(openBtn, 'Open '); openBtn.addEventListener('click', () => { - const resolved = new URL(page.url, window.location.origin); - window.open(resolved.toString(), '_blank', 'noopener,noreferrer'); + const openedWindow = window.open('about:blank', '_blank'); + if (openedWindow != null) { + openedWindow.opener = null; + } + runAsynchronouslyWithAlert(async () => { + const redirectUrl = await app[hexclaveAppInternalsSymbol].getRedirectToHandlerUrl(page.key); + const resolved = new URL(redirectUrl, window.location.origin); + if (openedWindow != null) { + openedWindow.location.replace(resolved.toString()); + } else { + window.open(resolved.toString(), '_blank', 'noopener,noreferrer'); + } + }); }); codeRow.appendChild(openBtn); header.appendChild(codeRow); diff --git a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts index be344a23b..ff31d2821 100644 --- a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +++ b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { AccessToken } from "@hexclave/shared/dist/sessions"; import { Store } from "@hexclave/shared/dist/utils/stores"; +import { hexclaveAppInternalsSymbol } from "../../common"; import { StackClientApp } from "../interfaces/client-app"; function createAccessTokenString(refreshTokenId: string): string { @@ -49,6 +50,42 @@ function createMockDocument(): Document { } describe("StackClientApp cross-domain auth", () => { + it("exposes redirect-back-aware handler URLs for devtool previews", async () => { + const previousWindow = Reflect.get(globalThis, "window"); + const hadPreviousWindow = Reflect.has(globalThis, "window"); + Reflect.set(globalThis, "window", { + location: { + href: "http://localhost/music?track=1#song", + }, + }); + + try { + const clientApp = new StackClientApp({ + baseUrl: "http://localhost:12345", + projectId: "00000000-0000-4000-8000-000000000000", + publishableClientKey: "stack-pk-test", + tokenStore: "memory", + redirectMethod: "none", + urls: { + signIn: "/handler/sign-in", + }, + noAutomaticPrefetch: true, + }); + + const redirectUrl = await clientApp[hexclaveAppInternalsSymbol].getRedirectToHandlerUrl("signIn"); + + const resolved = new URL(redirectUrl, "http://localhost"); + expect(resolved.pathname).toBe("/handler/sign-in"); + expect(resolved.searchParams.get("after_auth_return_to")).toBe("/music?track=1#song"); + } finally { + if (hadPreviousWindow) { + Reflect.set(globalThis, "window", previousWindow); + } else { + Reflect.deleteProperty(globalThis, "window"); + } + } + }); + it("uses the fresh post-auth refresh token when minting a cross-domain handoff", async () => { const freshAccessToken = createAccessTokenString("fresh-refresh-token-id"); const clientApp = new StackClientApp({ diff --git a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts index e6817584e..b7f1b5da8 100644 --- a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts @@ -3069,6 +3069,20 @@ export class _HexclaveClientAppImplIncomplete { const rawUrls = getUrls(this._urlOptions, { projectId: this.projectId }); const rawHandlerUrl = rawUrls[handlerName]; if (!rawHandlerUrl) { @@ -3096,8 +3110,7 @@ export class _HexclaveClientAppImplIncomplete { await this._redirectTo({ url, ...options }); }, + getRedirectToHandlerUrl: async (handlerName: keyof HandlerUrls, options?: RedirectToOptions) => { + return await this._getRedirectToHandlerUrl(handlerName, options); + }, redirectToHandler: async (handlerName: keyof HandlerUrls, options?: RedirectToOptions) => { await this._redirectToHandler(handlerName, options); }, diff --git a/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts b/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts index 53d75d20e..a47b61315 100644 --- a/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts +++ b/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts @@ -134,6 +134,7 @@ export type StackClientApp, getRedirectMethod(): RedirectMethod, redirectToUrl(url: string | URL, options?: { replace?: boolean }): Promise, + getRedirectToHandlerUrl(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise, redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise, signInWithTokens(tokens: { accessToken: string, refreshToken: string }): Promise, awaitPendingAuthResolutions(): Promise,