Skip to content

Commit 80db198

Browse files
committed
Add shopify store create trial command
Adds a programmatic way to create a Shopify trial store from the CLI, backed by the Signups API's StoreCreate mutation. - Registers `store:create:trial` in @shopify/store with flags for --name, --subdomain, --country (default US), and --json. - Adds cli-kit Signups foundation: `shop-create` OAuth scope, `signupsApi` application + `ensureAuthenticatedSignups`, `signupsFqdn()`, and `signupsRequest` API helper. - Regenerates oclif manifest, README, dev docs, and e2e snapshot. Supersedes #7218.
1 parent a889d3c commit 80db198

18 files changed

Lines changed: 737 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
shopify store create trial [flags]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// This is an autogenerated file. Don't edit this file manually.
2+
/**
3+
* The following flags are available for the `store create trial` command:
4+
* @publicDocs
5+
*/
6+
export interface storecreatetrial {
7+
/**
8+
* The country code for the store (e.g., US, CA, GB).
9+
* @environment SHOPIFY_FLAG_STORE_COUNTRY
10+
*/
11+
'-c, --country <value>'?: string
12+
13+
/**
14+
* Output the result as JSON. Automatically disables color output.
15+
* @environment SHOPIFY_FLAG_JSON
16+
*/
17+
'-j, --json'?: ''
18+
19+
/**
20+
* The name of the store.
21+
* @environment SHOPIFY_FLAG_STORE_NAME
22+
*/
23+
'-n, --name <value>'?: string
24+
25+
/**
26+
* Disable color output.
27+
* @environment SHOPIFY_FLAG_NO_COLOR
28+
*/
29+
'--no-color'?: ''
30+
31+
/**
32+
* The custom myshopify.com subdomain for the store.
33+
* @environment SHOPIFY_FLAG_STORE_SUBDOMAIN
34+
*/
35+
'--subdomain <value>'?: string
36+
37+
/**
38+
* Increase the verbosity of the output.
39+
* @environment SHOPIFY_FLAG_VERBOSE
40+
*/
41+
'--verbose'?: ''
42+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// This is an autogenerated file. Don't edit this file manually.
2+
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'
3+
4+
const data: ReferenceEntityTemplateSchema = {
5+
name: 'store create trial',
6+
description: `Creates a new Shopify trial store associated with your account.`,
7+
overviewPreviewDescription: `Create a new Shopify trial store.`,
8+
type: 'command',
9+
isVisualComponent: false,
10+
defaultExample: {
11+
codeblock: {
12+
tabs: [
13+
{
14+
title: 'store create trial',
15+
code: './examples/store-create-trial.example.sh',
16+
language: 'bash',
17+
},
18+
],
19+
title: 'store create trial',
20+
},
21+
},
22+
definitions: [
23+
{
24+
title: 'Flags',
25+
description: 'The following flags are available for the `store create trial` command:',
26+
type: 'storecreatetrial',
27+
},
28+
],
29+
category: 'store',
30+
related: [
31+
],
32+
}
33+
34+
export default data

packages/cli-kit/src/private/node/session.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ interface BusinessPlatformAPIOAuthOptions {
9292
scopes: BusinessPlatformScope[]
9393
}
9494

95+
/**
96+
* A scope supported by the Signups API.
97+
* The Signups API uses the Identity bearer token directly (no application token exchange).
98+
*/
99+
export type SignupsScope = 'shop-create'
100+
interface SignupsAPIOAuthOptions {
101+
/** List of scopes to request permissions for. */
102+
scopes: SignupsScope[]
103+
}
104+
95105
/**
96106
* It represents the authentication requirements and
97107
* is the input necessary to trigger the authentication
@@ -103,6 +113,7 @@ export interface OAuthApplications {
103113
partnersApi?: PartnersAPIOAuthOptions
104114
businessPlatformApi?: BusinessPlatformAPIOAuthOptions
105115
appManagementApi?: AppManagementAPIOauthOptions
116+
signupsApi?: SignupsAPIOAuthOptions
106117
}
107118

108119
export interface OAuthSession {
@@ -111,6 +122,7 @@ export interface OAuthSession {
111122
storefront?: string
112123
businessPlatform?: string
113124
appManagement?: string
125+
identity?: string
114126
userId: string
115127
}
116128

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

414+
if (applications.signupsApi) {
415+
tokens.identity = session.identity.accessToken
416+
}
417+
402418
return tokens
403419
}
404420

@@ -415,7 +431,8 @@ function getFlattenScopes(apps: OAuthApplications): string[] {
415431
const storefront = apps.storefrontRendererApi?.scopes ?? []
416432
const businessPlatform = apps.businessPlatformApi?.scopes ?? []
417433
const appManagement = apps.appManagementApi?.scopes ?? []
418-
const requestedScopes = [...admin, ...partner, ...storefront, ...businessPlatform, ...appManagement]
434+
const signups = apps.signupsApi?.scopes ?? []
435+
const requestedScopes = [...admin, ...partner, ...storefront, ...businessPlatform, ...appManagement, ...signups]
419436
return allDefaultScopes(requestedScopes)
420437
}
421438

@@ -426,6 +443,9 @@ function getFlattenScopes(apps: OAuthApplications): string[] {
426443
* @returns An object containing the scopes for each application.
427444
*/
428445
function getExchangeScopes(apps: OAuthApplications): ExchangeScopes {
446+
// Note: signupsApi is intentionally excluded here. The Signups API uses the Identity bearer
447+
// token directly rather than an exchanged application token. Its scopes are included in
448+
// getFlattenScopes so they appear on the Identity token, but no exchange is needed.
429449
const adminScope = apps.adminApi?.scopes ?? []
430450
const partnerScope = apps.partnersApi?.scopes ?? []
431451
const storefrontScopes = apps.storefrontRendererApi?.scopes ?? []

packages/cli-kit/src/private/node/session/scopes.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ describe('allDefaultScopes', () => {
2626
])
2727
})
2828

29+
test('transforms shop-create scope to full URI', async () => {
30+
const got = allDefaultScopes(['shop-create'])
31+
32+
expect(got).toContain('https://api.shopify.com/auth/shop.create')
33+
})
34+
2935
test('includes App Management and Store Management', async () => {
3036
// When
3137
const got = allDefaultScopes([])

packages/cli-kit/src/private/node/session/scopes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ function scopeTransform(scope: string): string {
8181
return 'https://api.shopify.com/auth/organization.on-demand-user-access'
8282
case 'app-management':
8383
return 'https://api.shopify.com/auth/organization.apps.manage'
84+
case 'shop-create':
85+
return 'https://api.shopify.com/auth/shop.create'
8486
default:
8587
return scope
8688
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {signupsRequest} from './signups.js'
2+
import {graphqlRequest} from './graphql.js'
3+
import {handleDeprecations} from './partners.js'
4+
import {signupsFqdn} from '../context/fqdn.js'
5+
import {beforeEach, describe, expect, test, vi} from 'vitest'
6+
7+
vi.mock('./graphql.js')
8+
vi.mock('../context/fqdn.js')
9+
10+
const signupsFqdnValue = 'shopify.com'
11+
const url = `https://${signupsFqdnValue}/services/signups/graphql`
12+
const mockedToken = 'identity-token'
13+
14+
beforeEach(() => {
15+
vi.mocked(signupsFqdn).mockResolvedValue(signupsFqdnValue)
16+
})
17+
18+
describe('signupsRequest', () => {
19+
test('calls graphqlRequest with correct parameters', async () => {
20+
vi.mocked(graphqlRequest).mockResolvedValue({storeCreate: {shopPermanentDomain: 'test.myshopify.com'}})
21+
const query = 'mutation StoreCreate($signup: ShopInput!) { storeCreate(signup: $signup) { shopPermanentDomain } }'
22+
const variables = {signup: {country: 'US'}}
23+
24+
await signupsRequest(query, mockedToken, variables)
25+
26+
expect(graphqlRequest).toHaveBeenCalledWith({
27+
query,
28+
api: 'Signups',
29+
url,
30+
token: mockedToken,
31+
variables,
32+
responseOptions: {onResponse: handleDeprecations},
33+
})
34+
})
35+
36+
test('calls graphqlRequest without variables when not provided', async () => {
37+
vi.mocked(graphqlRequest).mockResolvedValue({})
38+
const query = 'query { __schema { types { name } } }'
39+
40+
await signupsRequest(query, mockedToken)
41+
42+
expect(graphqlRequest).toHaveBeenCalledWith({
43+
query,
44+
api: 'Signups',
45+
url,
46+
token: mockedToken,
47+
variables: undefined,
48+
responseOptions: {onResponse: handleDeprecations},
49+
})
50+
})
51+
52+
test('returns the response from graphqlRequest', async () => {
53+
const expectedResponse = {storeCreate: {shopPermanentDomain: 'new-store.myshopify.com', polling: false}}
54+
vi.mocked(graphqlRequest).mockResolvedValue(expectedResponse)
55+
56+
const result = await signupsRequest('query', mockedToken)
57+
58+
expect(result).toEqual(expectedResponse)
59+
})
60+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {GraphQLVariables, graphqlRequest} from './graphql.js'
2+
import {handleDeprecations} from './partners.js'
3+
import {signupsFqdn} from '../context/fqdn.js'
4+
5+
async function setupRequest(token: string) {
6+
const api = 'Signups'
7+
const fqdn = await signupsFqdn()
8+
const url = `https://${fqdn}/services/signups/graphql`
9+
return {
10+
token,
11+
api,
12+
url,
13+
responseOptions: {onResponse: handleDeprecations},
14+
}
15+
}
16+
17+
/**
18+
* Executes a GraphQL query against the Signups API.
19+
* Uses the Identity bearer token directly (no application token exchange).
20+
*
21+
* @param query - GraphQL query to execute.
22+
* @param token - Identity access token.
23+
* @param variables - GraphQL variables to pass to the query.
24+
* @returns The response of the query of generic type <T>.
25+
*/
26+
export async function signupsRequest<T>(query: string, token: string, variables?: GraphQLVariables): Promise<T> {
27+
return graphqlRequest<T>({
28+
...(await setupRequest(token)),
29+
query,
30+
variables,
31+
})
32+
}

packages/cli-kit/src/public/node/context/fqdn.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@ export async function businessPlatformFqdn(): Promise<string> {
102102
}
103103
}
104104

105+
/**
106+
* It returns the Signups API service we should interact with.
107+
*
108+
* @returns Fully-qualified domain of the Signups service we should interact with.
109+
*/
110+
export async function signupsFqdn(): Promise<string> {
111+
const environment = serviceEnvironment()
112+
const productionFqdn = 'shopify.com'
113+
switch (environment) {
114+
case 'local':
115+
return new DevServerCore().host('shopify')
116+
default:
117+
return productionFqdn
118+
}
119+
}
120+
105121
/**
106122
* It returns the Identity service we should interact with.
107123
*

packages/cli-kit/src/public/node/session.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
BusinessPlatformScope,
1616
EnsureAuthenticatedAdditionalOptions,
1717
PartnersAPIScope,
18+
SignupsScope,
1819
StorefrontRendererScope,
1920
ensureAuthenticated,
2021
setLastSeenAuthMethod,
@@ -274,6 +275,30 @@ ${outputToken.json(scopes)}
274275
return tokens.businessPlatform
275276
}
276277

278+
/**
279+
* Ensure that we have a valid session to access the Signups API.
280+
* The Signups API uses the Identity bearer token directly (no application token exchange).
281+
*
282+
* @param scopes - Optional array of extra scopes to authenticate with.
283+
* @param env - Optional environment variables to use.
284+
* @param options - Optional extra options to use.
285+
* @returns The Identity access token and user ID.
286+
*/
287+
export async function ensureAuthenticatedSignups(
288+
scopes: SignupsScope[] = ['shop-create'],
289+
env = process.env,
290+
options: EnsureAuthenticatedAdditionalOptions = {},
291+
): Promise<{token: string; userId: string}> {
292+
outputDebug(outputContent`Ensuring that the user is authenticated with the Signups API with the following scopes:
293+
${outputToken.json(scopes)}
294+
`)
295+
const tokens = await ensureAuthenticated({signupsApi: {scopes}}, env, options)
296+
if (!tokens.identity) {
297+
throw new BugError('No identity token found after ensuring authenticated')
298+
}
299+
return {token: tokens.identity, userId: tokens.userId}
300+
}
301+
277302
/**
278303
* Logout from Shopify.
279304
*

0 commit comments

Comments
 (0)