From 036e3f719f963af7fd29f9f572de081c0b40b84b Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Fri, 15 May 2026 16:54:23 -0400 Subject: [PATCH 1/2] Render App Bridge instead of login redirect when shop/host params are missing in embedded apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an embedded app receives a document request without shop or host query parameters (e.g. after a full page reload during Vite HMR), the server would redirect to the login page. This is incorrect because the app is still running inside the Shopify admin iframe — it just lost the URL search params during navigation/reload. Instead of redirecting to login, we now render a minimal App Bridge page. App Bridge detects it's in the admin iframe, retrieves the session token from the parent frame, and re-authenticates the app seamlessly. This fixes the annoying workflow during local development where: 1. Navigate to a new URL (loses the id_token param) 2. Make a code change 3. Vite triggers a full page reload (not HMR) 4. Server sees no identifiers and incorrectly shows login Non-embedded apps (ShopifyAdmin distribution) are unaffected and still redirect to login as before. --- .../__test-helpers/expect-login-redirect.ts | 8 ---- .../src/server/__test-helpers/index.ts | 1 - .../admin/__tests__/doc-request-path.test.ts | 44 ++++++++++++------- .../helpers/validate-shop-and-host-params.ts | 19 ++++---- .../__tests__/reject-bot-request.test.ts | 5 ++- 5 files changed, 42 insertions(+), 35 deletions(-) delete mode 100644 packages/apps/shopify-app-react-router/src/server/__test-helpers/expect-login-redirect.ts diff --git a/packages/apps/shopify-app-react-router/src/server/__test-helpers/expect-login-redirect.ts b/packages/apps/shopify-app-react-router/src/server/__test-helpers/expect-login-redirect.ts deleted file mode 100644 index c7624fb3dc..0000000000 --- a/packages/apps/shopify-app-react-router/src/server/__test-helpers/expect-login-redirect.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {APP_URL} from './const'; - -export function expectLoginRedirect(response: Response) { - expect(response.status).toBe(302); - - const {pathname} = new URL(response.headers.get('location')!, APP_URL); - expect(pathname).toBe('/auth/login'); -} diff --git a/packages/apps/shopify-app-react-router/src/server/__test-helpers/index.ts b/packages/apps/shopify-app-react-router/src/server/__test-helpers/index.ts index 5252bdef8d..bbf5ef42fa 100644 --- a/packages/apps/shopify-app-react-router/src/server/__test-helpers/index.ts +++ b/packages/apps/shopify-app-react-router/src/server/__test-helpers/index.ts @@ -2,7 +2,6 @@ export * from './const'; export * from './expect-admin-api-client'; export * from './expect-document-request-headers'; export * from './expect-exit-iframe'; -export * from './expect-login-redirect'; export * from './expect-storefront-api-client'; export * from './get-hmac'; export * from './get-jwt'; diff --git a/packages/apps/shopify-app-react-router/src/server/authenticate/admin/__tests__/doc-request-path.test.ts b/packages/apps/shopify-app-react-router/src/server/authenticate/admin/__tests__/doc-request-path.test.ts index 249fbaaede..feec62698c 100644 --- a/packages/apps/shopify-app-react-router/src/server/authenticate/admin/__tests__/doc-request-path.test.ts +++ b/packages/apps/shopify-app-react-router/src/server/authenticate/admin/__tests__/doc-request-path.test.ts @@ -1,4 +1,5 @@ import {shopifyApp} from '../../..'; +import {APP_BRIDGE_URL} from '../../const'; import { API_KEY, APP_URL, @@ -8,7 +9,6 @@ import { getThrownResponse, setUpValidSession, testConfig, - expectLoginRedirect, } from '../../../__test-helpers'; describe('authorize.admin doc request path', () => { @@ -18,22 +18,32 @@ describe('authorize.admin doc request path', () => { {shop: TEST_SHOP, host: 'invalid-domain.test'}, {shop: undefined, host: BASE64_HOST}, {shop: 'invalid', host: BASE64_HOST}, - ])('throws when %s', async ({shop, host}) => { - // GIVEN - const shopify = shopifyApp(testConfig()); - const searchParams = new URLSearchParams(); - if (shop) searchParams.set('shop', shop); - if (host) searchParams.set('host', host); - - // WHEN - const response = await getThrownResponse( - shopify.authenticate.admin, - new Request(`${APP_URL}?${searchParams.toString()}`), - ); - - // THEN - expectLoginRedirect(response); - }); + ])( + 'renders App Bridge when embedded app has missing or invalid params: %s', + async ({shop, host}) => { + // GIVEN + const config = testConfig(); + const shopify = shopifyApp(config); + const searchParams = new URLSearchParams(); + if (shop) searchParams.set('shop', shop); + if (host) searchParams.set('host', host); + + // WHEN + const response = await getThrownResponse( + shopify.authenticate.admin, + new Request(`${APP_URL}?${searchParams.toString()}`), + ); + + // THEN + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe( + 'text/html;charset=utf-8', + ); + expect((await response.text()).trim()).toBe( + ``, + ); + }, + ); it('throws an error if the request URL is the login path', async () => { // GIVEN diff --git a/packages/apps/shopify-app-react-router/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts b/packages/apps/shopify-app-react-router/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts index 41793199a3..b35ef3cff4 100644 --- a/packages/apps/shopify-app-react-router/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts +++ b/packages/apps/shopify-app-react-router/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts @@ -1,7 +1,7 @@ -import {redirect} from 'react-router'; - import {BasicParams, AppDistribution} from '../../../types'; +import {renderAppBridge} from './render-app-bridge'; + export function validateShopAndHostParams( params: BasicParams, request: Request, @@ -12,24 +12,24 @@ export function validateShopAndHostParams( const url = new URL(request.url); const shop = api.utils.sanitizeShop(url.searchParams.get('shop')!); if (!shop) { - logger.debug('Missing or invalid shop, redirecting to login path', { + logger.debug('Missing or invalid shop, rendering App Bridge', { shop, }); - throw redirectToLoginPath(request, params); + throw renderAppBridgeOrError(request, params); } const host = api.utils.sanitizeHost(url.searchParams.get('host')!); if (!host) { - logger.debug('Invalid host, redirecting to login path', { + logger.debug('Invalid host, rendering App Bridge', { shop, host: url.searchParams.get('host'), }); - throw redirectToLoginPath(request, params); + throw renderAppBridgeOrError(request, params); } } } -function redirectToLoginPath(request: Request, params: BasicParams): never { +function renderAppBridgeOrError(request: Request, params: BasicParams): never { const {config, logger} = params; const {pathname} = new URL(request.url); @@ -42,5 +42,8 @@ function redirectToLoginPath(request: Request, params: BasicParams): never { throw new Response(message, {status: 500}); } - throw redirect(config.auth.loginPath); + logger.debug( + 'Missing shop or host params, rendering App Bridge to retrieve session', + ); + throw renderAppBridge(params, request); } diff --git a/packages/apps/shopify-app-react-router/src/server/authenticate/helpers/__tests__/reject-bot-request.test.ts b/packages/apps/shopify-app-react-router/src/server/authenticate/helpers/__tests__/reject-bot-request.test.ts index 60eba4e9e6..dd35ee25a9 100644 --- a/packages/apps/shopify-app-react-router/src/server/authenticate/helpers/__tests__/reject-bot-request.test.ts +++ b/packages/apps/shopify-app-react-router/src/server/authenticate/helpers/__tests__/reject-bot-request.test.ts @@ -36,6 +36,9 @@ describe('Reject bot requests', () => { ); // THEN - expect(response.status).toBe(302); + // The request passes the bot check and proceeds to auth validation. + // For embedded apps without shop/host params, this renders the App Bridge + // page (200) to retrieve the session token from the parent frame. + expect(response.status).toBe(200); }); }); From 264d5c6c7966169a265b78809faf99c778e70d46 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Tue, 19 May 2026 10:37:05 -0400 Subject: [PATCH 2/2] Add changeset for App Bridge render on missing shop/host params --- .changeset/render-app-bridge-missing-params.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/render-app-bridge-missing-params.md diff --git a/.changeset/render-app-bridge-missing-params.md b/.changeset/render-app-bridge-missing-params.md new file mode 100644 index 0000000000..92ddb27522 --- /dev/null +++ b/.changeset/render-app-bridge-missing-params.md @@ -0,0 +1,17 @@ +--- +'@shopify/shopify-app-react-router': patch +--- + +Fixed an issue where embedded apps would incorrectly show the login page when +`shop` or `host` query params were missing from a document request (e.g. after +SPA navigation followed by a full page reload during local development). + +Instead of redirecting to the login path, the server now renders a minimal App +Bridge page. App Bridge detects it is still embedded in the Shopify admin iframe, +retrieves the session token from the parent frame, and re-authenticates +seamlessly — no user interaction required. + +This is a non-breaking change. The previous login redirect was effectively dead +code for embedded apps (`isEmbeddedApp` is always `true` for apps using this +library; the `ShopifyAdmin` distribution is excluded earlier in the pipeline). +No public APIs are added, removed, or changed.