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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
shopify store create trial [flags]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// This is an autogenerated file. Don't edit this file manually.
/**
* The following flags are available for the `store create trial` command:
* @publicDocs
*/
export interface storecreatetrial {
/**
* The country code for the store (e.g., US, CA, GB).
* @environment SHOPIFY_FLAG_STORE_COUNTRY
*/
'-c, --country <value>'?: string

/**
* Output the result as JSON. Automatically disables color output.
* @environment SHOPIFY_FLAG_JSON
*/
'-j, --json'?: ''

/**
* The name of the store.
* @environment SHOPIFY_FLAG_STORE_NAME
*/
'-n, --name <value>'?: string

/**
* Disable color output.
* @environment SHOPIFY_FLAG_NO_COLOR
*/
'--no-color'?: ''

/**
* The custom myshopify.com subdomain for the store.
* @environment SHOPIFY_FLAG_STORE_SUBDOMAIN
*/
'--subdomain <value>'?: string

/**
* Increase the verbosity of the output.
* @environment SHOPIFY_FLAG_VERBOSE
*/
'--verbose'?: ''
}
34 changes: 34 additions & 0 deletions docs-shopify.dev/commands/store-create-trial.doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// This is an autogenerated file. Don't edit this file manually.
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'

const data: ReferenceEntityTemplateSchema = {
name: 'store create trial',
description: `Creates a new Shopify trial store associated with your account.`,
overviewPreviewDescription: `Create a new Shopify trial store.`,
type: 'command',
isVisualComponent: false,
defaultExample: {
codeblock: {
tabs: [
{
title: 'store create trial',
code: './examples/store-create-trial.example.sh',
language: 'bash',
},
],
title: 'store create trial',
},
},
definitions: [
{
title: 'Flags',
description: 'The following flags are available for the `store create trial` command:',
type: 'storecreatetrial',
},
],
category: 'store',
related: [
],
}

export default data
22 changes: 21 additions & 1 deletion packages/cli-kit/src/private/node/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ interface BusinessPlatformAPIOAuthOptions {
scopes: BusinessPlatformScope[]
}

/**
* A scope supported by the Signups API.
* The Signups API uses the Identity bearer token directly (no application token exchange).
*/
export type SignupsScope = 'shop-create'
interface SignupsAPIOAuthOptions {
/** List of scopes to request permissions for. */
scopes: SignupsScope[]
}

/**
* It represents the authentication requirements and
* is the input necessary to trigger the authentication
Expand All @@ -103,6 +113,7 @@ export interface OAuthApplications {
partnersApi?: PartnersAPIOAuthOptions
businessPlatformApi?: BusinessPlatformAPIOAuthOptions
appManagementApi?: AppManagementAPIOauthOptions
signupsApi?: SignupsAPIOAuthOptions
}

export interface OAuthSession {
Expand All @@ -111,6 +122,7 @@ export interface OAuthSession {
storefront?: string
businessPlatform?: string
appManagement?: string
identity?: string
userId: string
}

Expand Down Expand Up @@ -399,6 +411,10 @@ async function tokensFor(applications: OAuthApplications, session: Session): Pro
tokens.appManagement = session.applications[appId]?.accessToken
}

if (applications.signupsApi) {
tokens.identity = session.identity.accessToken
}

return tokens
}

Expand All @@ -415,7 +431,8 @@ function getFlattenScopes(apps: OAuthApplications): string[] {
const storefront = apps.storefrontRendererApi?.scopes ?? []
const businessPlatform = apps.businessPlatformApi?.scopes ?? []
const appManagement = apps.appManagementApi?.scopes ?? []
const requestedScopes = [...admin, ...partner, ...storefront, ...businessPlatform, ...appManagement]
const signups = apps.signupsApi?.scopes ?? []
const requestedScopes = [...admin, ...partner, ...storefront, ...businessPlatform, ...appManagement, ...signups]
return allDefaultScopes(requestedScopes)
}

Expand All @@ -426,6 +443,9 @@ function getFlattenScopes(apps: OAuthApplications): string[] {
* @returns An object containing the scopes for each application.
*/
function getExchangeScopes(apps: OAuthApplications): ExchangeScopes {
// Note: signupsApi is intentionally excluded here. The Signups API uses the Identity bearer
// token directly rather than an exchanged application token. Its scopes are included in
// getFlattenScopes so they appear on the Identity token, but no exchange is needed.
const adminScope = apps.adminApi?.scopes ?? []
const partnerScope = apps.partnersApi?.scopes ?? []
const storefrontScopes = apps.storefrontRendererApi?.scopes ?? []
Expand Down
6 changes: 6 additions & 0 deletions packages/cli-kit/src/private/node/session/scopes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ describe('allDefaultScopes', () => {
])
})

test('transforms shop-create scope to full URI', async () => {
const got = allDefaultScopes(['shop-create'])

expect(got).toContain('https://api.shopify.com/auth/shop.create')
})

test('includes App Management and Store Management', async () => {
// When
const got = allDefaultScopes([])
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-kit/src/private/node/session/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ function scopeTransform(scope: string): string {
return 'https://api.shopify.com/auth/organization.on-demand-user-access'
case 'app-management':
return 'https://api.shopify.com/auth/organization.apps.manage'
case 'shop-create':
return 'https://api.shopify.com/auth/shop.create'
default:
return scope
}
Expand Down
60 changes: 60 additions & 0 deletions packages/cli-kit/src/public/node/api/signups.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {signupsRequest} from './signups.js'
import {graphqlRequest} from './graphql.js'
import {handleDeprecations} from './partners.js'
import {signupsFqdn} from '../context/fqdn.js'
import {beforeEach, describe, expect, test, vi} from 'vitest'

vi.mock('./graphql.js')
vi.mock('../context/fqdn.js')

const signupsFqdnValue = 'shopify.com'
const url = `https://${signupsFqdnValue}/services/signups/graphql`
const mockedToken = 'identity-token'

beforeEach(() => {
vi.mocked(signupsFqdn).mockResolvedValue(signupsFqdnValue)
})

describe('signupsRequest', () => {
test('calls graphqlRequest with correct parameters', async () => {
vi.mocked(graphqlRequest).mockResolvedValue({storeCreate: {shopPermanentDomain: 'test.myshopify.com'}})
const query = 'mutation StoreCreate($signup: ShopInput!) { storeCreate(signup: $signup) { shopPermanentDomain } }'
const variables = {signup: {country: 'US'}}

await signupsRequest(query, mockedToken, variables)

expect(graphqlRequest).toHaveBeenCalledWith({
query,
api: 'Signups',
url,
token: mockedToken,
variables,
responseOptions: {onResponse: handleDeprecations},
})
})

test('calls graphqlRequest without variables when not provided', async () => {
vi.mocked(graphqlRequest).mockResolvedValue({})
const query = 'query { __schema { types { name } } }'

await signupsRequest(query, mockedToken)

expect(graphqlRequest).toHaveBeenCalledWith({
query,
api: 'Signups',
url,
token: mockedToken,
variables: undefined,
responseOptions: {onResponse: handleDeprecations},
})
})

test('returns the response from graphqlRequest', async () => {
const expectedResponse = {storeCreate: {shopPermanentDomain: 'new-store.myshopify.com', polling: false}}
vi.mocked(graphqlRequest).mockResolvedValue(expectedResponse)

const result = await signupsRequest('query', mockedToken)

expect(result).toEqual(expectedResponse)
})
})
32 changes: 32 additions & 0 deletions packages/cli-kit/src/public/node/api/signups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {GraphQLVariables, graphqlRequest} from './graphql.js'
import {handleDeprecations} from './partners.js'
import {signupsFqdn} from '../context/fqdn.js'

async function setupRequest(token: string) {
const api = 'Signups'
const fqdn = await signupsFqdn()
const url = `https://${fqdn}/services/signups/graphql`
return {
token,
api,
url,
responseOptions: {onResponse: handleDeprecations},
}
}

/**
* Executes a GraphQL query against the Signups API.
* Uses the Identity bearer token directly (no application token exchange).
*
* @param query - GraphQL query to execute.
* @param token - Identity access token.
* @param variables - GraphQL variables to pass to the query.
* @returns The response of the query of generic type <T>.
*/
export async function signupsRequest<T>(query: string, token: string, variables?: GraphQLVariables): Promise<T> {
return graphqlRequest<T>({
...(await setupRequest(token)),
query,
variables,
})
}
25 changes: 25 additions & 0 deletions packages/cli-kit/src/public/node/context/fqdn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
businessPlatformFqdn,
appDevFqdn,
adminFqdn,
signupsFqdn,
} from './fqdn.js'
import {Environment, serviceEnvironment} from '../../../private/node/context/service.js'
import {expect, describe, test, vi} from 'vitest'
Expand Down Expand Up @@ -150,6 +151,30 @@ describe('identity', () => {
})
})

describe('signupsFqdn', () => {
test('returns the local fqdn when the environment is local', async () => {
// Given
vi.mocked(serviceEnvironment).mockReturnValue(Environment.Local)

// When
const got = await signupsFqdn()

// Then
expect(got).toEqual('shopify.myshopify.io')
})

test('returns the production fqdn when the environment is production', async () => {
// Given
vi.mocked(serviceEnvironment).mockReturnValue(Environment.Production)

// When
const got = await signupsFqdn()

// Then
expect(got).toEqual('shopify.com')
})
})

describe('adminFqdn', () => {
test('returns the local fqdn when the environment is local', async () => {
// Given
Expand Down
16 changes: 16 additions & 0 deletions packages/cli-kit/src/public/node/context/fqdn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ export async function businessPlatformFqdn(): Promise<string> {
}
}

/**
* It returns the Signups API service we should interact with.
*
* @returns Fully-qualified domain of the Signups service we should interact with.
*/
export async function signupsFqdn(): Promise<string> {
const environment = serviceEnvironment()
const productionFqdn = 'shopify.com'
switch (environment) {
case 'local':
return new DevServerCore().host('shopify')
default:
return productionFqdn
}
Comment on lines +110 to +118
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add coverage for signupsFqdn() in fqdn.test.ts (local vs production), like the existing tests for partnersFqdn, adminFqdn, etc. Without a direct test, regressions in the Signups FQDN routing (especially the local dev-server host mapping) may go unnoticed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a describe('signupsFqdn') block in fqdn.test.ts covering local + production environments, matching the pattern used by partnersFqdn / adminFqdn.

}

/**
* It returns the Identity service we should interact with.
*
Expand Down
47 changes: 47 additions & 0 deletions packages/cli-kit/src/public/node/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ensureAuthenticatedAppManagementAndBusinessPlatform,
ensureAuthenticatedBusinessPlatform,
ensureAuthenticatedPartners,
ensureAuthenticatedSignups,
ensureAuthenticatedStorefront,
ensureAuthenticatedThemes,
} from './session.js'
Expand Down Expand Up @@ -220,6 +221,52 @@ describe('ensureAuthenticatedBusinessPlatform', () => {
})
})

describe('ensureAuthenticatedSignups', () => {
test('returns the identity token and userId when success', async () => {
// Given
vi.mocked(ensureAuthenticated).mockResolvedValueOnce({identity: 'identity_token', userId: '1234-5678'})

// When
const got = await ensureAuthenticatedSignups()

// Then
expect(got).toEqual({token: 'identity_token', userId: '1234-5678'})
})

test('requests the default shop-create scope when no scopes are provided', async () => {
// Given
vi.mocked(ensureAuthenticated).mockResolvedValueOnce({identity: 'identity_token', userId: '1234-5678'})

// When
await ensureAuthenticatedSignups()

// Then
expect(ensureAuthenticated).toHaveBeenCalledWith({signupsApi: {scopes: ['shop-create']}}, expect.anything(), {})
})

test('passes through custom scopes when provided', async () => {
// Given
vi.mocked(ensureAuthenticated).mockResolvedValueOnce({identity: 'identity_token', userId: '1234-5678'})

// When
await ensureAuthenticatedSignups(['shop-create'])

// Then
expect(ensureAuthenticated).toHaveBeenCalledWith({signupsApi: {scopes: ['shop-create']}}, expect.anything(), {})
})

test('throws error if there is no identity token', async () => {
// Given
vi.mocked(ensureAuthenticated).mockResolvedValueOnce({partners: 'partners_token', userId: '1234-5678'})

// When
const got = ensureAuthenticatedSignups()

// Then
await expect(got).rejects.toThrow(`No identity token`)
})
})

describe('ensureAuthenticatedAppManagementAndBusinessPlatform', () => {
test('returns app management and business platform tokens if success', async () => {
// Given
Expand Down
Loading
Loading