Skip to content

Commit 4e6c544

Browse files
MitchLillieclaude
andcommitted
Preserve template application_url during app creation
When `shopify app init` uses an extension-only template (e.g. shopify-app-template-extension-only), the template specifies `application_url = "https://extensions.shopifycdn.com"`. Previously this URL was discarded — createApp() always sent hardcoded placeholder URLs to the platform. The subsequent link() step then pulled the placeholder back from the remote, overwriting the local config. Thread template URLs through CreateAppOptions so the platform receives the correct URL at creation time. Falls back to existing defaults when no template URL is provided (backward compatible). Fixes shop/issues-admin-extensibility#2395 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8658f96 commit 4e6c544

8 files changed

Lines changed: 169 additions & 27 deletions

File tree

packages/app/src/cli/models/app/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,12 +432,16 @@ export class App<
432432
}
433433

434434
creationDefaultOptions(): CreateAppOptions {
435+
const applicationUrl = this.configuration.application_url
436+
const redirectUrls = this.configuration.auth?.redirect_urls
435437
return {
436438
isLaunchable: this.appIsLaunchable(),
437439
scopesArray: getAppScopesArray(this.configuration),
438440
name: this.name,
439441
isEmbedded: this.appIsEmbedded,
440442
directory: this.directory,
443+
applicationUrl,
444+
redirectUrls,
441445
}
442446
}
443447

packages/app/src/cli/models/app/loader.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,56 @@ value = true
35043504
})
35053505
})
35063506
})
3507+
3508+
test('extracts application_url from template config', async () => {
3509+
await inTemporaryDirectory(async (tmpDir) => {
3510+
const config = `
3511+
client_id = ""
3512+
name = "my-app"
3513+
application_url = "https://extensions.shopifycdn.com"
3514+
embedded = true
3515+
3516+
[access_scopes]
3517+
scopes = "write_products"
3518+
3519+
[auth]
3520+
redirect_urls = ["https://shopify.dev/apps/default-app-home/api/auth"]
3521+
`
3522+
await writeFile(joinPath(tmpDir, 'shopify.app.toml'), config)
3523+
await writeFile(joinPath(tmpDir, 'package.json'), '{}')
3524+
3525+
const result = await loadConfigForAppCreation(tmpDir, 'my-app')
3526+
3527+
expect(result).toEqual({
3528+
isLaunchable: false,
3529+
scopesArray: ['write_products'],
3530+
name: 'my-app',
3531+
directory: normalizePath(tmpDir),
3532+
isEmbedded: false,
3533+
applicationUrl: 'https://extensions.shopifycdn.com',
3534+
redirectUrls: ['https://shopify.dev/apps/default-app-home/api/auth'],
3535+
})
3536+
})
3537+
})
3538+
3539+
test('defaults applicationUrl and redirectUrls to undefined when not in template config', async () => {
3540+
await inTemporaryDirectory(async (tmpDir) => {
3541+
const config = `
3542+
client_id = ""
3543+
name = "my-app"
3544+
3545+
[access_scopes]
3546+
scopes = "write_products"
3547+
`
3548+
await writeFile(joinPath(tmpDir, 'shopify.app.toml'), config)
3549+
await writeFile(joinPath(tmpDir, 'package.json'), '{}')
3550+
3551+
const result = await loadConfigForAppCreation(tmpDir, 'my-app')
3552+
3553+
expect(result.applicationUrl).toBeUndefined()
3554+
expect(result.redirectUrls).toBeUndefined()
3555+
})
3556+
})
35073557
})
35083558

35093559
describe('loadOpaqueApp', () => {

packages/app/src/cli/models/app/loader.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ export async function loadConfigForAppCreation(directory: string, name: string):
206206
const isLaunchable = webs.some((web) => isWebType(web, WebType.Frontend) || isWebType(web, WebType.Backend))
207207

208208
const scopesArray = getAppScopesArray(rawConfig as CurrentAppConfiguration)
209+
const appConfig = rawConfig as CurrentAppConfiguration
210+
const applicationUrl = appConfig.application_url
211+
const redirectUrls = appConfig.auth?.redirect_urls
209212

210213
return {
211214
isLaunchable,
@@ -214,6 +217,8 @@ export async function loadConfigForAppCreation(directory: string, name: string):
214217
directory: project.directory,
215218
// By default, and ONLY for `app init`, we consider the app as embedded if it is launchable.
216219
isEmbedded: isLaunchable,
220+
applicationUrl,
221+
redirectUrls,
217222
}
218223
}
219224

packages/app/src/cli/utilities/developer-platform-client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export interface CreateAppOptions {
120120
scopesArray?: string[]
121121
directory?: string
122122
isEmbedded?: boolean
123+
applicationUrl?: string
124+
redirectUrls?: string[]
123125
}
124126

125127
interface AppModuleVersionSpecification {

packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,63 @@ describe('createApp', () => {
652652
expect(result).toMatchObject(expectedApp)
653653
})
654654

655+
test('uses applicationUrl and redirectUrls from options when provided', async () => {
656+
// Given
657+
const client = AppManagementClient.getInstance()
658+
const org = testOrganization()
659+
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce({
660+
publicApiVersions: [{handle: '2024-07'}, {handle: '2024-10'}, {handle: '2025-01'}, {handle: 'unstable'}],
661+
})
662+
vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({
663+
appCreate: {
664+
app: {id: '1', key: 'key', activeRoot: {clientCredentials: {secrets: [{key: 'secret'}]}}},
665+
userErrors: [],
666+
},
667+
})
668+
669+
// When
670+
client.token = () => Promise.resolve('token')
671+
await client.createApp(org, {
672+
name: 'app-name',
673+
isLaunchable: false,
674+
applicationUrl: 'https://extensions.shopifycdn.com',
675+
redirectUrls: ['https://shopify.dev/apps/default-app-home/api/auth'],
676+
})
677+
678+
// Then
679+
expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith({
680+
query: CreateApp,
681+
token: 'token',
682+
variables: {
683+
organizationId: 'gid://shopify/Organization/1',
684+
initialVersion: {
685+
source: {
686+
name: 'app-name',
687+
modules: expect.arrayContaining([
688+
{
689+
type: 'app_home',
690+
config: {
691+
app_url: 'https://extensions.shopifycdn.com',
692+
embedded: true,
693+
},
694+
},
695+
{
696+
type: 'app_access',
697+
config: {
698+
redirect_url_allowlist: ['https://shopify.dev/apps/default-app-home/api/auth'],
699+
},
700+
},
701+
]),
702+
},
703+
},
704+
},
705+
unauthorizedHandler: {
706+
handler: expect.any(Function),
707+
type: 'token_refresh',
708+
},
709+
})
710+
})
711+
655712
test('sets embedded to true in app home module', async () => {
656713
// Given
657714
const client = AppManagementClient.getInstance()

packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,14 +1203,17 @@ function createAppVars(
12031203
apiVersion: string,
12041204
): CreateAppMutationVariables {
12051205
const {isLaunchable, scopesArray, name} = options
1206+
const defaultAppUrl = isLaunchable ? 'https://example.com' : MAGIC_URL
1207+
const defaultRedirectUrl = isLaunchable ? 'https://example.com/api/auth' : MAGIC_REDIRECT_URL
1208+
12061209
const source: AppVersionSource = {
12071210
source: {
12081211
name,
12091212
modules: [
12101213
{
12111214
type: AppHomeSpecIdentifier,
12121215
config: {
1213-
app_url: isLaunchable ? 'https://example.com' : MAGIC_URL,
1216+
app_url: options.applicationUrl ?? defaultAppUrl,
12141217
// Ext-only apps should be embedded = false, however we are hardcoding this to
12151218
// match Partners behaviour for now
12161219
// https://github.com/Shopify/develop-app-inner-loop/issues/2789
@@ -1228,7 +1231,7 @@ function createAppVars(
12281231
{
12291232
type: AppAccessSpecIdentifier,
12301233
config: {
1231-
redirect_url_allowlist: isLaunchable ? ['https://example.com/api/auth'] : [MAGIC_REDIRECT_URL],
1234+
redirect_url_allowlist: options.redirectUrls ?? [defaultRedirectUrl],
12321235
...(scopesArray && {scopes: scopesArray.map((scope) => scope.trim()).join(',')}),
12331236
},
12341237
},

packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,39 @@ describe('createApp', () => {
152152
})
153153
})
154154

155+
test('uses applicationUrl and redirectUrls from options when provided', async () => {
156+
// Given
157+
const partnersClient = PartnersClient.getInstance(testPartnersUserSession)
158+
vi.mocked(appNamePrompt).mockResolvedValue('app-name')
159+
vi.mocked(partnersRequest).mockResolvedValueOnce({appCreate: {app: APP1, userErrors: []}})
160+
const variables = {
161+
org: 1,
162+
title: LOCAL_APP.name,
163+
appUrl: 'https://extensions.shopifycdn.com',
164+
redir: ['https://shopify.dev/apps/default-app-home/api/auth'],
165+
requestedAccessScopes: ['write_products'],
166+
type: 'undecided',
167+
}
168+
169+
// When
170+
await partnersClient.createApp(
171+
{...ORG1, source: OrganizationSource.Partners},
172+
{
173+
name: LOCAL_APP.name,
174+
isLaunchable: false,
175+
scopesArray: ['write_products'],
176+
applicationUrl: 'https://extensions.shopifycdn.com',
177+
redirectUrls: ['https://shopify.dev/apps/default-app-home/api/auth'],
178+
},
179+
)
180+
181+
// Then
182+
expect(partnersRequest).toHaveBeenCalledWith(CreateAppQuery, 'token', variables, undefined, undefined, {
183+
type: 'token_refresh',
184+
handler: expect.any(Function),
185+
})
186+
})
187+
155188
test('throws error if requests has a user error', async () => {
156189
// Given
157190
const partnersClient = PartnersClient.getInstance(testPartnersUserSession)

packages/app/src/cli/utilities/developer-platform-client/partners-client.ts

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -170,30 +170,18 @@ import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'
170170
const MAGIC_URL = 'https://shopify.dev/apps/default-app-home'
171171
const MAGIC_REDIRECT_URL = 'https://shopify.dev/apps/default-app-home/api/auth'
172172

173-
function getAppVars(
174-
org: Organization,
175-
name: string,
176-
isLaunchable = true,
177-
scopesArray?: string[],
178-
): CreateAppQueryVariables {
179-
if (isLaunchable) {
180-
return {
181-
org: parseInt(org.id, 10),
182-
title: name,
183-
appUrl: 'https://example.com',
184-
redir: ['https://example.com/api/auth'],
185-
requestedAccessScopes: scopesArray ?? [],
186-
type: 'undecided',
187-
}
188-
} else {
189-
return {
190-
org: parseInt(org.id, 10),
191-
title: name,
192-
appUrl: MAGIC_URL,
193-
redir: [MAGIC_REDIRECT_URL],
194-
requestedAccessScopes: scopesArray ?? [],
195-
type: 'undecided',
196-
}
173+
function getAppVars(org: Organization, options: CreateAppOptions): CreateAppQueryVariables {
174+
const {name, isLaunchable = true, scopesArray} = options
175+
const defaultAppUrl = isLaunchable ? 'https://example.com' : MAGIC_URL
176+
const defaultRedirectUrl = isLaunchable ? 'https://example.com/api/auth' : MAGIC_REDIRECT_URL
177+
178+
return {
179+
org: parseInt(org.id, 10),
180+
title: name,
181+
appUrl: options.applicationUrl ?? defaultAppUrl,
182+
redir: options.redirectUrls ?? [defaultRedirectUrl],
183+
requestedAccessScopes: scopesArray ?? [],
184+
type: 'undecided',
197185
}
198186
}
199187

@@ -395,7 +383,7 @@ export class PartnersClient implements DeveloperPlatformClient {
395383
}
396384

397385
async createApp(org: Organization, options: CreateAppOptions): Promise<OrganizationApp> {
398-
const variables: CreateAppQueryVariables = getAppVars(org, options.name, options.isLaunchable, options.scopesArray)
386+
const variables: CreateAppQueryVariables = getAppVars(org, options)
399387
const result: CreateAppQuerySchema = await this.request(CreateAppQuery, variables)
400388
if (result.appCreate.userErrors.length > 0) {
401389
const errors = result.appCreate.userErrors.map((error) => error.message).join(', ')

0 commit comments

Comments
 (0)