Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/store-list-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/store': minor
---

Show each store's plan in `shopify store list`
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ListAccessibleShopsQuery = {
shopifyShopId?: string | null
name: string
storeType?: Types.Store | null
planName?: string | null
primaryDomain?: string | null
url?: string | null
createdAt: unknown
Expand Down Expand Up @@ -116,6 +117,7 @@ export const ListAccessibleShops = {
{kind: 'Field', name: {kind: 'Name', value: 'shopifyShopId'}},
{kind: 'Field', name: {kind: 'Name', value: 'name'}},
{kind: 'Field', name: {kind: 'Name', value: 'storeType'}},
{kind: 'Field', name: {kind: 'Name', value: 'planName'}},
{kind: 'Field', name: {kind: 'Name', value: 'primaryDomain'}},
{kind: 'Field', name: {kind: 'Name', value: 'url'}},
{kind: 'Field', name: {kind: 'Name', value: 'createdAt'}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ query ListAccessibleShops($first: Int!) {
shopifyShopId
name
storeType
planName
primaryDomain
url
createdAt
Expand Down
3 changes: 2 additions & 1 deletion packages/store/src/cli/services/store/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export {devStorePlanHandles} from '@shopify/organizations'
export type {DevStorePlan} from '@shopify/organizations'

/**
* `store info`: a raw BP plan name (`Shop.planName`) → the public plan handle it reports.
* A raw BP plan name (`Shop.planName`) → the public plan handle reported by `store info` and
* `store list`.
* The raw names are Shopify-internal and intentionally differ from the marketing names
* (e.g. `professional` is Grow, `unlimited` is Advanced). The public handle is also accepted
* as a key, because the exact form BP returns isn't pinned down by the schema. Anything not
Expand Down
4 changes: 2 additions & 2 deletions packages/store/src/cli/services/store/info/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {mapPlanToPublicHandle} from './plan.js'
import {classifyAdminApiError, throwIfStoredStoreAuthIsInvalid} from '../admin-errors.js'
import {recordStoreFqdnMetadata} from '../attribution.js'
import {throwStoredAuthInvalidError} from '../auth/recovery.js'
import {loadStoredStoreSession} from '../auth/session-lifecycle.js'
import {getPreviewStore, PreviewStoreRequestError} from '../create/preview/client.js'
import {planHandle} from '../plan.js'
import {storeTypeHandle} from '../store-type.js'
import {StoreLookupStoreNotFoundError, fetchDestinationsContext} from '../../../utilities/store-lookup/destinations.js'
import {fetchOrganizationShop} from '../../../utilities/store-lookup/organization-shop.js'
Expand Down Expand Up @@ -242,7 +242,7 @@ function buildBusinessPlatformResult(args: BuildBusinessPlatformResultArgs): Sto
organizationName: destinationsCtx.owningOrg?.name,
storeOwner: buildBusinessPlatformStoreOwner(orgShop),
type: storeTypeHandle(orgShop?.storeType),
plan: mapPlanToPublicHandle(orgShop?.planName),
plan: planHandle(orgShop?.planName),
featurePreview: orgShop?.developerPreviewHandle,
adminUrl: buildAdminUrl(extractMyshopifyHandle(store)),
}
Expand Down
33 changes: 0 additions & 33 deletions packages/store/src/cli/services/store/info/plan.test.ts

This file was deleted.

10 changes: 0 additions & 10 deletions packages/store/src/cli/services/store/info/plan.ts

This file was deleted.

12 changes: 12 additions & 0 deletions packages/store/src/cli/services/store/list/bp-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function accessibleShopNode(overrides: Partial<AccessibleShopNode> = {}): Access
shopifyShopId: '1',
name: 'Acme Production',
storeType: 'PRODUCTION',
planName: 'shopify_plus',
primaryDomain: 'acme.myshopify.com',
url: null,
createdAt: '2026-01-15T00:00:00Z',
Expand Down Expand Up @@ -79,6 +80,7 @@ describe('listBusinessPlatformStores', () => {
organizationName: 'Acme',
name: 'Acme Production',
type: 'production',
plan: 'plus',
},
],
hasMore: false,
Expand Down Expand Up @@ -111,6 +113,16 @@ describe('listBusinessPlatformStores', () => {
expect(result).toEqual({entries: [], hasMore: false})
})

test('omits the plan for an unrecognized plan name', async () => {
vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValue(
shopPage({shops: [accessibleShopNode({planName: 'some_new_plan'})]}),
)

const result = await listBusinessPlatformStores({token: 'bp-token', organization})

expect(result.entries[0]?.plan).toBeUndefined()
})

test('fetches a single bounded page for the selected organization and orders newest first', async () => {
vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValue(
shopPage({
Expand Down
2 changes: 2 additions & 0 deletions packages/store/src/cli/services/store/list/bp-source.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {STORE_LIST_LIMIT} from './constants.js'
import {type StoreListEntry} from './types.js'
import {businessPlatformTokenRefreshHandler} from '../business-platform.js'
import {planHandle} from '../plan.js'
import {storeTypeHandle} from '../store-type.js'
import {
ListAccessibleShops,
Expand Down Expand Up @@ -80,6 +81,7 @@ function toStoreListEntry(node: ShopNode, organization: Organization): StoreList
organizationName: organization.businessName,
name: node.name,
type: storeTypeHandle(node.storeType),
plan: planHandle(node.planName),
}
}

Expand Down
37 changes: 33 additions & 4 deletions packages/store/src/cli/services/store/list/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('writeStoreListResult', () => {
mockAndCaptureOutput().clear()
})

test('renders organization context and rows with subdomain, name, type, and created date', () => {
test('renders organization context and rows with subdomain, name, type, plan, and created date', () => {
const output = mockAndCaptureOutput()

writeStoreListResult(
Expand All @@ -25,6 +25,7 @@ describe('writeStoreListResult', () => {
organizationName: 'Acme',
name: 'My Shop',
type: 'dev',
plan: 'plus',
},
],
},
Expand All @@ -37,6 +38,7 @@ describe('writeStoreListResult', () => {
expect(output.info()).not.toContain('my-shop.myshopify.com')
expect(output.info()).toContain('My Shop')
expect(output.info()).toContain('Dev')
expect(output.info()).toContain('Plus')
expect(output.info()).toContain('May 22, 2026')
expect(output.info()).toContain('shopify store auth list')
})
Expand All @@ -56,6 +58,7 @@ describe('writeStoreListResult', () => {
organizationName: 'Acme',
name: 'My Shop',
type: 'dev',
plan: 'grow',
},
],
},
Expand All @@ -74,12 +77,36 @@ describe('writeStoreListResult', () => {
│ │
╰──────────────────────────────────────────────────────────────────────────────╯

Subdomain Name Type Created
───────── ─────── ──── ────────────
my-shop My Shop Dev May 22, 2026"
Subdomain Name Type Plan Created
───────── ─────── ──── ──── ────────────
my-shop My Shop Dev Grow May 22, 2026"
`)
})

test('leaves the plan column blank when the plan is unrecognized', () => {
const output = mockAndCaptureOutput()

writeStoreListResult(
{
source: 'organization',
organization,
stores: [
{
store: 'my-shop.myshopify.com',
createdAt: '2026-05-22T00:00:00Z',
organizationId: '1234',
organizationName: 'Acme',
name: 'My Shop',
type: 'dev',
},
],
},
'text',
)

expect(trimmedLines(output.info())).toContain('my-shop My Shop Dev May 22, 2026')
})

test('renders the subdomain handle for non-myshopify hosts (local dev)', () => {
const output = mockAndCaptureOutput()

Expand Down Expand Up @@ -167,6 +194,7 @@ describe('writeStoreListResult', () => {
organizationName: 'Acme',
name: 'My Shop',
type: 'dev',
plan: 'plus',
},
],
},
Expand All @@ -183,6 +211,7 @@ describe('writeStoreListResult', () => {
organizationName: 'Acme',
name: 'My Shop',
type: 'dev',
plan: 'plus',
},
],
organization,
Expand Down
3 changes: 3 additions & 0 deletions packages/store/src/cli/services/store/list/result.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {STORE_LIST_LIMIT} from './constants.js'
import {type ListStoresResult, type StoreListEntry, type StoreListOrganization} from './types.js'
import {extractSubdomain, formatShortDate} from '../display.js'
import {planLabel} from '../plan.js'
import {storeTypeLabel} from '../store-type.js'
import {outputResult, outputWarn} from '@shopify/cli-kit/node/output'
import {renderInfo, renderTable, type AlertCustomSection, type TokenItem} from '@shopify/cli-kit/node/ui'
Expand Down Expand Up @@ -81,12 +82,14 @@ function renderOrganizationTable(stores: StoreListEntry[]): void {
subdomain: subdomainFor(entry.store),
name: entry.name ?? '',
type: storeTypeLabel(entry.type),
plan: planLabel(entry.plan),
created: formatShortDate(entry.createdAt),
})),
columns: {
subdomain: {header: 'Subdomain'},
name: {header: 'Name'},
type: {header: 'Type'},
plan: {header: 'Plan'},
created: {header: 'Created'},
},
})
Expand Down
1 change: 1 addition & 0 deletions packages/store/src/cli/services/store/list/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface StoreListEntry {
organizationName: string
name?: string
type?: string
plan?: string
}

export interface StoreListOrganization {
Expand Down
47 changes: 47 additions & 0 deletions packages/store/src/cli/services/store/plan.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {planHandle, planLabel} from './plan.js'
import {describe, test, expect} from 'vitest'

describe('planHandle', () => {
test('maps internal plan names to public handles', () => {
expect(planHandle('basic')).toBe('basic')
expect(planHandle('professional')).toBe('grow')
expect(planHandle('unlimited')).toBe('advanced')
expect(planHandle('shopify_plus')).toBe('plus')
})

test('accepts the public handles themselves', () => {
expect(planHandle('grow')).toBe('grow')
expect(planHandle('advanced')).toBe('advanced')
expect(planHandle('plus')).toBe('plus')
})

test('is case-insensitive', () => {
expect(planHandle('Professional')).toBe('grow')
expect(planHandle('SHOPIFY_PLUS')).toBe('plus')
})

test('returns undefined for unrecognized plans', () => {
expect(planHandle('staff')).toBeUndefined()
expect(planHandle('development_legacy')).toBeUndefined()
expect(planHandle('some_new_plan')).toBeUndefined()
})

test('returns undefined when no plan is provided', () => {
expect(planHandle(undefined)).toBeUndefined()
expect(planHandle(null)).toBeUndefined()
expect(planHandle('')).toBeUndefined()
})
})

describe('planLabel', () => {
test('title-cases the public handle', () => {
expect(planLabel('basic')).toBe('Basic')
expect(planLabel('grow')).toBe('Grow')
expect(planLabel('advanced')).toBe('Advanced')
expect(planLabel('plus')).toBe('Plus')
})

test('renders an empty column for an unrecognized plan', () => {
expect(planLabel(undefined)).toBe('')
})
})
17 changes: 17 additions & 0 deletions packages/store/src/cli/services/store/plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {PLAN_HANDLES_BY_NAME} from './constants.js'
import {capitalizeWords} from '@shopify/cli-kit/common/string'

/**
* Maps a raw BP plan name (`Shop.planName`) to its public handle, or undefined when the plan
* isn't recognized. Matching is case-insensitive; see {@link PLAN_HANDLES_BY_NAME}.
*/
export function planHandle(planName: string | null | undefined): string | undefined {
if (!planName) return undefined
return PLAN_HANDLES_BY_NAME[planName.toLowerCase()]
}

// Title-cased label for the `store list` table column (`plus` -> `Plus`). Unrecognized plans have
// no handle, so the column is left blank rather than showing a raw internal plan name.
export function planLabel(handle: string | undefined): string {
return handle ? capitalizeWords(handle) : ''
}
Loading