From 5d9d9d42d9d2fbc630d02bb34345a196bc402393 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Fri, 13 Mar 2026 16:30:47 -0400 Subject: [PATCH] Redirect to app redirect landing page in admin --- .../dev/extension/payload/store.test.ts | 36 +++++++++- .../dev/processes/setup-dev-processes.test.ts | 67 ++++++++++++++++++- .../dev/processes/setup-dev-processes.ts | 6 +- .../app/src/cli/utilities/app/app-url.test.ts | 29 ++++++++ packages/app/src/cli/utilities/app/app-url.ts | 8 ++- 5 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 packages/app/src/cli/utilities/app/app-url.test.ts diff --git a/packages/app/src/cli/services/dev/extension/payload/store.test.ts b/packages/app/src/cli/services/dev/extension/payload/store.test.ts index cd9a5229da8..ab0676626f7 100644 --- a/packages/app/src/cli/services/dev/extension/payload/store.test.ts +++ b/packages/app/src/cli/services/dev/extension/payload/store.test.ts @@ -7,8 +7,18 @@ import { import {UIExtensionPayload, ExtensionsEndpointPayload} from './models.js' import * as payload from '../payload.js' import {ExtensionInstance} from '../../../../models/extensions/extension-instance.js' +import {normalizeStoreFqdn, storeAdminUrl} from '@shopify/cli-kit/node/context/fqdn' import {beforeEach, describe, expect, test, vi} from 'vitest' +vi.mock('@shopify/cli-kit/node/context/fqdn', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + normalizeStoreFqdn: vi.fn(original.normalizeStoreFqdn), + storeAdminUrl: vi.fn(original.storeAdminUrl), + } +}) + describe('getExtensionsPayloadStoreRawPayload()', () => { test('returns the raw payload', async () => { // Given @@ -34,7 +44,7 @@ describe('getExtensionsPayloadStoreRawPayload()', () => { app: { title: 'mock-app-name', apiKey: 'mock-api-key', - url: 'https://mock-store-fqdn.myshopify.com/admin/oauth/redirect_from_cli?client_id=mock-api-key', + url: 'https://admin.shopify.com/store/mock-store-fqdn/extensions-dev/preview?client_id=mock-api-key', mobileUrl: 'https://mock-store-fqdn.myshopify.com/admin/apps/mock-api-key?shop=mock-store-fqdn.myshopify.com&host=bW9jay1zdG9yZS1mcWRuLm15c2hvcGlmeS5jb20vYWRtaW4vYXBwcy9tb2NrLWFwaS1rZXk', }, @@ -52,6 +62,30 @@ describe('getExtensionsPayloadStoreRawPayload()', () => { extensions: [{mock: 'extension-payload'}, {mock: 'extension-payload'}, {mock: 'extension-payload'}], }) }) + + test('uses the admin-web preflight URL for local development stores', async () => { + vi.spyOn(payload, 'getUIExtensionPayload').mockResolvedValue({ + mock: 'extension-payload', + } as unknown as UIExtensionPayload) + vi.mocked(normalizeStoreFqdn).mockReturnValue('mock-store-fqdn.my.shop.dev') + vi.mocked(storeAdminUrl).mockReturnValue('admin.shop.dev/store/mock-store-fqdn') + + const options = { + apiKey: 'mock-api-key', + appName: 'mock-app-name', + url: 'https://mock-url.com', + websocketURL: 'wss://mock-websocket-url.com', + extensions: [{}], + storeFqdn: 'mock-store-fqdn.my.shop.dev', + manifestVersion: '3', + } as unknown as ExtensionsPayloadStoreOptions + + const rawPayload = await getExtensionsPayloadStoreRawPayload(options, 'mock-bundle-path') + + expect(rawPayload.app.url).toBe( + 'https://admin.shop.dev/store/mock-store-fqdn/extensions-dev/preview?client_id=mock-api-key', + ) + }) }) describe('ExtensionsPayloadStore()', () => { diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts index eb1585b6a92..c5e7232c1c5 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts @@ -36,7 +36,7 @@ import {getEnvironmentVariables} from '@shopify/cli-kit/node/environment' import {isStorefrontPasswordProtected} from '@shopify/theme' import {fetchTheme} from '@shopify/cli-kit/node/themes/api' import {firstPartyDev} from '@shopify/cli-kit/node/context/local' -import {adminFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {adminFqdn, normalizeStoreFqdn, storeAdminUrl} from '@shopify/cli-kit/node/context/fqdn' vi.mock('../../context/identifiers.js') vi.mock('@shopify/cli-kit/node/session.js') @@ -50,6 +50,8 @@ vi.mock('@shopify/cli-kit/node/context/fqdn', async (importOriginal) => { return { ...original, adminFqdn: vi.fn(), + normalizeStoreFqdn: vi.fn(original.normalizeStoreFqdn), + storeAdminUrl: vi.fn(original.storeAdminUrl), } }) @@ -312,6 +314,69 @@ describe('setup-dev-processes', () => { }) }) + test('uses the admin-web preflight URL for local development stores', async () => { + const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() + const storeFqdn = 'test.my.shop.dev' + const storeId = '123456789' + const remoteAppUpdated = true + const graphiqlPort = 1234 + const commandOptions: DevConfig['commandOptions'] = { + ...appContextResult, + directory: '', + update: false, + commandConfig: new Config({root: ''}), + skipDependenciesInstallation: false, + tunnel: {mode: 'auto'}, + } + const network: DevConfig['network'] = { + proxyUrl: 'https://example.com/proxy', + proxyPort: 444, + backendPort: 111, + frontendPort: 222, + currentUrls: { + applicationUrl: 'https://example.com/application', + redirectUrlWhitelist: ['https://example.com/redirect'], + }, + } + const localApp = testAppWithConfig() + vi.spyOn(loader, 'reloadApp').mockResolvedValue(localApp) + vi.mocked(normalizeStoreFqdn).mockReturnValue('test.my.shop.dev') + vi.mocked(storeAdminUrl).mockReturnValue('admin.shop.dev/store/test') + + const remoteApp: DevConfig['remoteApp'] = { + apiKey: 'api-key', + apiSecretKeys: [{secret: 'api-secret'}], + id: '1234', + title: 'App', + organizationId: '5678', + grantedScopes: [], + flags: [], + developerPlatformClient, + } + + const res = await setupDevProcesses({ + localApp, + commandOptions, + network, + remoteApp, + remoteAppUpdated, + storeFqdn, + storeId, + developerPlatformClient, + partnerUrlsUpdated: true, + graphiqlPort, + graphiqlKey: 'somekey', + }) + + expect(res.previewUrl).toBe('https://admin.shop.dev/store/test/extensions-dev/preview?client_id=api-key') + expect(res.processes[1]).toMatchObject({ + type: 'graphiql', + options: { + appUrl: 'https://admin.shop.dev/store/test/extensions-dev/preview?client_id=api-key', + }, + }) + }) + test('process list includes dev-session when useDevSession is true', async () => { const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({supportsDevSessions: true}) const storeFqdn = 'store.myshopify.io' diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts index a384b8ac927..ba914d3e3c9 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts @@ -25,7 +25,7 @@ import {isTruthy} from '@shopify/cli-kit/node/context/utilities' import {firstPartyDev} from '@shopify/cli-kit/node/context/local' import {getEnvironmentVariables} from '@shopify/cli-kit/node/environment' import {outputInfo} from '@shopify/cli-kit/node/output' -import {adminFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {adminFqdn, normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' interface ProxyServerProcess extends BaseProcess<{ port: number @@ -108,8 +108,10 @@ export async function setupDevProcesses({ // appPreviewUrl is the direct app URL (used by GraphiQL and dev session fallback) // previewURL is what's shown to the user (may be dev console for 1P devs) + const isLocalStore = normalizeStoreFqdn(storeFqdn).endsWith('.my.shop.dev') + let appPreviewUrl: string - if (is1PDev) { + if (is1PDev || isLocalStore) { appPreviewUrl = buildAppURLForWeb(storeFqdn, apiKey) } else { const adminDomain = await adminFqdn() diff --git a/packages/app/src/cli/utilities/app/app-url.test.ts b/packages/app/src/cli/utilities/app/app-url.test.ts new file mode 100644 index 00000000000..e3f5c40d01f --- /dev/null +++ b/packages/app/src/cli/utilities/app/app-url.test.ts @@ -0,0 +1,29 @@ +import {buildAppURLForWeb} from './app-url.js' +import {describe, expect, test, vi} from 'vitest' +import {normalizeStoreFqdn, storeAdminUrl} from '@shopify/cli-kit/node/context/fqdn' + +vi.mock('@shopify/cli-kit/node/context/fqdn', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + normalizeStoreFqdn: vi.fn(original.normalizeStoreFqdn), + storeAdminUrl: vi.fn(original.storeAdminUrl), + } +}) + +describe('buildAppURLForWeb', () => { + test('builds the admin-web preflight preview URL for production stores', () => { + const url = buildAppURLForWeb('my-store.myshopify.com', 'api-key') + + expect(url).toBe('https://admin.shopify.com/store/my-store/extensions-dev/preview?client_id=api-key') + }) + + test('uses the same admin-web path in local development with the admin.shop.dev host', () => { + vi.mocked(normalizeStoreFqdn).mockReturnValue('my-store.my.shop.dev') + vi.mocked(storeAdminUrl).mockReturnValue('admin.shop.dev/store/my-store') + + const url = buildAppURLForWeb('my-store.my.shop.dev', 'api-key') + + expect(url).toBe('https://admin.shop.dev/store/my-store/extensions-dev/preview?client_id=api-key') + }) +}) diff --git a/packages/app/src/cli/utilities/app/app-url.ts b/packages/app/src/cli/utilities/app/app-url.ts index 79afa5cd860..2c83e0a3f4d 100644 --- a/packages/app/src/cli/utilities/app/app-url.ts +++ b/packages/app/src/cli/utilities/app/app-url.ts @@ -2,8 +2,12 @@ import {normalizeStoreFqdn, storeAdminUrl} from '@shopify/cli-kit/node/context/f export function buildAppURLForWeb(storeFqdn: string, apiKey: string) { const normalizedFQDN = normalizeStoreFqdn(storeFqdn) - const adminUrl = storeAdminUrl(normalizedFQDN) - return `https://${adminUrl}/admin/oauth/redirect_from_cli?client_id=${apiKey}` + const storeName = normalizedFQDN.split('.')[0] + const localAdminUrl = storeAdminUrl(normalizedFQDN) + const adminDomain = localAdminUrl === normalizedFQDN ? 'admin.shopify.com' : localAdminUrl.split('/')[0] + const searchParams = new URLSearchParams({client_id: apiKey}) + + return `https://${adminDomain}/store/${storeName}/extensions-dev/preview?${searchParams.toString()}` } export function buildAppURLForAdmin(storeFqdn: string, apiKey: string, adminDomain: string) {