From acfa485a095c0e6d1d40d91e8febb08878ccc2da Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Tue, 16 Jun 2026 14:59:35 +0300 Subject: [PATCH] Add preview store save URL to store info Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/cli/README.md | 2 +- packages/cli/oclif.manifest.json | 4 +- packages/store/src/cli/commands/store/info.ts | 2 +- .../src/cli/services/store/info/index.test.ts | 42 +++++++++++++++++ .../src/cli/services/store/info/index.ts | 46 ++++++++++++++++++- .../cli/services/store/info/result.test.ts | 5 ++ .../src/cli/services/store/info/result.ts | 1 + .../src/cli/services/store/info/types.ts | 1 + 8 files changed, 98 insertions(+), 5 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 6de20213148..6601b04a33f 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2196,7 +2196,7 @@ DESCRIPTION Surface metadata about a Shopify store. Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, - store owner, type, plan, feature preview, and admin URL. + store owner, type, plan, feature preview, admin URL, and save URL for preview stores. Some details may be omitted when they are not available for the store. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 95c6110983a..e0c67e83ca1 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -6012,8 +6012,8 @@ "args": { }, "customPluginName": "@shopify/store", - "description": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, and admin URL.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.", - "descriptionWithMarkdown": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, and admin URL.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.", + "description": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and save URL for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.", + "descriptionWithMarkdown": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and save URL for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.", "examples": [ "<%= config.bin %> <%= command.id %> --store shop.myshopify.com", "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --json" diff --git a/packages/store/src/cli/commands/store/info.ts b/packages/store/src/cli/commands/store/info.ts index 99b28c38eb2..0a4ed544f8f 100644 --- a/packages/store/src/cli/commands/store/info.ts +++ b/packages/store/src/cli/commands/store/info.ts @@ -7,7 +7,7 @@ import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' export default class StoreInfo extends StoreCommand { static summary = 'Surface metadata about a Shopify store.' - static descriptionWithMarkdown = `Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, and admin URL. + static descriptionWithMarkdown = `Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and save URL for preview stores. Some details may be omitted when they are not available for the store. diff --git a/packages/store/src/cli/services/store/info/index.test.ts b/packages/store/src/cli/services/store/info/index.test.ts index 29ff3b4ee2d..0fe0e076556 100644 --- a/packages/store/src/cli/services/store/info/index.test.ts +++ b/packages/store/src/cli/services/store/info/index.test.ts @@ -5,6 +5,7 @@ import {STORE_AUTH_APP_CLIENT_ID} from '../auth/config.js' import {loadStoredStoreSession} from '../auth/session-lifecycle.js' import {clearStoredStoreAppSession, getCurrentStoredStoreAppSession} from '../auth/session-store.js' import {recordStoreFqdnMetadata} from '../attribution.js' +import {claimPreviewStore} from '../create/preview/client.js' import {AbortError, BugError} from '@shopify/cli-kit/node/error' import {adminUrl} from '@shopify/cli-kit/node/api/admin' import {graphqlRequest} from '@shopify/cli-kit/node/api/graphql' @@ -24,6 +25,7 @@ vi.mock('./organization-shop.js') vi.mock('../auth/session-lifecycle.js') vi.mock('../auth/session-store.js') vi.mock('../attribution.js') +vi.mock('../create/preview/client.js') vi.mock('@shopify/cli-kit/node/api/graphql') vi.mock('@shopify/cli-kit/node/session') vi.mock('@shopify/cli-kit/node/api/admin', async () => { @@ -126,6 +128,7 @@ describe('getStoreInfo', () => { expect(fetchOrganizationShop).toHaveBeenCalledWith({store: SHOP, organizationId: '149572536', noPrompt: false}) expect(loadStoredStoreSession).not.toHaveBeenCalled() expect(graphqlRequest).not.toHaveBeenCalled() + expect(claimPreviewStore).not.toHaveBeenCalled() expect(result).toEqual({ id: 'gid://shopify/Shop/72193245184', displayName: 'My Shop (Org)', @@ -161,6 +164,45 @@ describe('getStoreInfo', () => { featurePreview: 'extended_variants', adminUrl: 'https://admin.shopify.com/store/shop', }) + expect(claimPreviewStore).not.toHaveBeenCalled() + }) + + test('returns a save URL for locally stored preview stores', async () => { + vi.mocked(getCurrentStoredStoreAppSession).mockReturnValueOnce({ + store: SHOP, + clientId: STORE_AUTH_APP_CLIENT_ID, + userId: 'preview:placeholder-uuid', + accessToken: 'shpat_preview_token', + scopes: [], + acquiredAt: '2026-06-08T12:00:00.000Z', + kind: 'preview', + preview: { + placeholderAccountUuid: 'placeholder-uuid', + shopId: '123', + name: 'Lavender Candles', + createdAt: '2026-06-08T12:00:00.000Z', + accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', + }, + }) + vi.mocked(claimPreviewStore).mockResolvedValueOnce({ + claimUrl: 'https://admin.shopify.com/store-transfer/accept/claim-token', + }) + + const result = await getStoreInfo({store: SHOP}) + + expect(fetchDestinationsContext).not.toHaveBeenCalled() + expect(fetchOrganizationShop).not.toHaveBeenCalled() + expect(claimPreviewStore).toHaveBeenCalledWith({ + shopId: '123', + adminApiToken: 'shpat_preview_token', + }) + expect(result).toEqual({ + id: 'gid://shopify/Shop/123', + displayName: 'Lavender Candles', + subdomain: SHOP, + adminUrl: 'https://admin.shopify.com/store/shop', + saveUrl: 'https://admin.shopify.com/store-transfer/accept/claim-token', + }) }) test('falls back to stored store auth when BP cannot resolve a store-auth store', async () => { diff --git a/packages/store/src/cli/services/store/info/index.ts b/packages/store/src/cli/services/store/info/index.ts index 30d5c3cbb5b..2c2a01d5332 100644 --- a/packages/store/src/cli/services/store/info/index.ts +++ b/packages/store/src/cli/services/store/info/index.ts @@ -5,6 +5,7 @@ import {classifyAdminApiError, throwIfStoredStoreAuthIsInvalid} from '../admin-e import {recordStoreFqdnMetadata} from '../attribution.js' import {loadStoredStoreSession} from '../auth/session-lifecycle.js' import {getCurrentStoredStoreAppSession} from '../auth/session-store.js' +import {claimPreviewStore} from '../create/preview/client.js' import {AbortError} from '@shopify/cli-kit/node/error' import {adminUrl} from '@shopify/cli-kit/node/api/admin' import {graphqlRequest} from '@shopify/cli-kit/node/api/graphql' @@ -59,7 +60,17 @@ export async function getStoreInfo(options: GetStoreInfoOptions): Promise +} + +function isPreviewStoreSession(session: StoredStoreAppSession | undefined): session is PreviewStoreSession { + return session?.kind === 'preview' && session.preview !== undefined +} + +async function fetchPreviewStoreSaveUrl(previewSession: PreviewStoreSession): Promise { + const claim = await claimPreviewStore({ + shopId: previewSession.preview.shopId, + adminApiToken: previewSession.accessToken, + }) + return claim.claimUrl +} + async function safeFetchOrganizationShop( ctx: DestinationsContext, store: string, @@ -198,6 +226,22 @@ function buildBusinessPlatformResult(args: BuildBusinessPlatformResultArgs): Sto return {...compact(fields), subdomain: store} as StoreInfoResult } +function buildPreviewStoreResult(args: { + store: string + previewSession: PreviewStoreSession + saveUrl: string +}): StoreInfoResult { + const {store, previewSession, saveUrl} = args + const fields: Partial = { + id: buildShopGid(previewSession.preview.shopId), + displayName: previewSession.preview.name, + adminUrl: buildAdminUrl(extractMyshopifyHandle(store)), + saveUrl, + } + + return {...compact(fields), subdomain: store} as StoreInfoResult +} + // The BP `ShopifyShopID` scalar is the bare numeric id; the admin GID is derived locally. function buildShopGid(shopifyShopId: string | undefined): string | undefined { if (!shopifyShopId) return undefined diff --git a/packages/store/src/cli/services/store/info/result.test.ts b/packages/store/src/cli/services/store/info/result.test.ts index 82327445d52..01761261a3d 100644 --- a/packages/store/src/cli/services/store/info/result.test.ts +++ b/packages/store/src/cli/services/store/info/result.test.ts @@ -35,6 +35,7 @@ describe('renderStoreInfoResult', () => { plan: 'grow', featurePreview: 'extended_variants', adminUrl: 'https://admin.shopify.com/store/acme-widgets', + saveUrl: 'https://admin.shopify.com/store-transfer/accept/claim-token', }), 'json', ) @@ -52,6 +53,7 @@ describe('renderStoreInfoResult', () => { plan: 'grow', featurePreview: 'extended_variants', adminUrl: 'https://admin.shopify.com/store/acme-widgets', + saveUrl: 'https://admin.shopify.com/store-transfer/accept/claim-token', }) expect(renderInfo).not.toHaveBeenCalled() }) @@ -73,6 +75,7 @@ describe('renderStoreInfoResult', () => { plan: 'grow', featurePreview: 'extended_variants', adminUrl: 'https://admin.shopify.com/store/acme-widgets', + saveUrl: 'https://admin.shopify.com/store-transfer/accept/claim-token', }), 'text', ) @@ -86,6 +89,7 @@ describe('renderStoreInfoResult', () => { expect(items).toContain('Plan: Grow') expect(items).toContain('Feature Preview: extended_variants') expect(items).toContain('Admin URL: https://admin.shopify.com/store/acme-widgets') + expect(items).toContain('Save URL: https://admin.shopify.com/store-transfer/accept/claim-token') }) test('formats the store owner as "name (email)" when both are present', () => { @@ -110,5 +114,6 @@ describe('renderStoreInfoResult', () => { expect(items.some((item) => item.startsWith('Store owner'))).toBe(false) expect(items.some((item) => item.startsWith('Type'))).toBe(false) expect(items.some((item) => item.startsWith('Plan'))).toBe(false) + expect(items.some((item) => item.startsWith('Save URL'))).toBe(false) }) }) diff --git a/packages/store/src/cli/services/store/info/result.ts b/packages/store/src/cli/services/store/info/result.ts index b07b6a23f4b..21b5b7ea728 100644 --- a/packages/store/src/cli/services/store/info/result.ts +++ b/packages/store/src/cli/services/store/info/result.ts @@ -26,6 +26,7 @@ function storeDetailItems(result: StoreInfoResult): string[] { push(items, 'Plan', result.plan ? capitalizeWords(result.plan) : undefined) push(items, 'Feature Preview', result.featurePreview) push(items, 'Admin URL', result.adminUrl) + push(items, 'Save URL', result.saveUrl) return items } diff --git a/packages/store/src/cli/services/store/info/types.ts b/packages/store/src/cli/services/store/info/types.ts index fd2a719b91c..99f30bf8128 100644 --- a/packages/store/src/cli/services/store/info/types.ts +++ b/packages/store/src/cli/services/store/info/types.ts @@ -26,6 +26,7 @@ export interface StoreInfoResult { plan?: string featurePreview?: string adminUrl?: string + saveUrl?: string } /**