Skip to content

Commit 7c7e5b1

Browse files
authored
Merge pull request #7225 from Shopify/cx-preserve-application-url
Preserve template application_url during app creation
2 parents 0009967 + dc1ac70 commit 7c7e5b1

10 files changed

Lines changed: 289 additions & 27 deletions

File tree

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

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

434434
creationDefaultOptions(): CreateAppOptions {
435+
const applicationUrl = this.configuration.application_url
436+
const redirectUrls = this.configuration.auth?.redirect_urls
437+
const staticRoot = this.configuration.admin?.static_root
435438
return {
436439
isLaunchable: this.appIsLaunchable(),
437440
scopesArray: getAppScopesArray(this.configuration),
438441
name: this.name,
439442
isEmbedded: this.appIsEmbedded,
440443
directory: this.directory,
444+
applicationUrl,
445+
redirectUrls,
446+
staticRoot,
441447
}
442448
}
443449

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,80 @@ 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+
staticRoot: undefined,
3536+
})
3537+
})
3538+
})
3539+
3540+
test('extracts admin.static_root from template config', async () => {
3541+
await inTemporaryDirectory(async (tmpDir) => {
3542+
const config = `
3543+
client_id = ""
3544+
name = "my-app"
3545+
application_url = "https://extensions.shopifycdn.com"
3546+
embedded = true
3547+
3548+
[admin]
3549+
static_root = "./dist"
3550+
3551+
[access_scopes]
3552+
scopes = "write_products"
3553+
`
3554+
await writeFile(joinPath(tmpDir, 'shopify.app.toml'), config)
3555+
await writeFile(joinPath(tmpDir, 'package.json'), '{}')
3556+
3557+
const result = await loadConfigForAppCreation(tmpDir, 'my-app')
3558+
3559+
expect(result.staticRoot).toBe('./dist')
3560+
})
3561+
})
3562+
3563+
test('defaults applicationUrl and redirectUrls to undefined when not in template config', async () => {
3564+
await inTemporaryDirectory(async (tmpDir) => {
3565+
const config = `
3566+
client_id = ""
3567+
name = "my-app"
3568+
3569+
[access_scopes]
3570+
scopes = "write_products"
3571+
`
3572+
await writeFile(joinPath(tmpDir, 'shopify.app.toml'), config)
3573+
await writeFile(joinPath(tmpDir, 'package.json'), '{}')
3574+
3575+
const result = await loadConfigForAppCreation(tmpDir, 'my-app')
3576+
3577+
expect(result.applicationUrl).toBeUndefined()
3578+
expect(result.redirectUrls).toBeUndefined()
3579+
})
3580+
})
35073581
})
35083582

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

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ 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
212+
const staticRoot = appConfig.admin?.static_root
209213

210214
return {
211215
isLaunchable,
@@ -214,6 +218,9 @@ export async function loadConfigForAppCreation(directory: string, name: string):
214218
directory: project.directory,
215219
// By default, and ONLY for `app init`, we consider the app as embedded if it is launchable.
216220
isEmbedded: isLaunchable,
221+
applicationUrl,
222+
redirectUrls,
223+
staticRoot,
217224
}
218225
}
219226

packages/app/src/cli/models/extensions/specifications/admin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {BaseConfigType, ZodSchemaType} from '../schemas.js'
33
import {zod} from '@shopify/cli-kit/node/schema'
44
import {joinPath} from '@shopify/cli-kit/node/path'
55

6+
export const AdminSpecIdentifier = 'admin'
7+
68
const AdminSchema = zod.object({
79
admin: zod
810
.object({

packages/app/src/cli/models/extensions/specifications/types/app_config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@ export interface AppConfigurationUsedByCli {
2828
auth?: {
2929
redirect_urls: string[]
3030
}
31+
admin?: {
32+
static_root?: string
33+
}
3134
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ export interface CreateAppOptions {
120120
scopesArray?: string[]
121121
directory?: string
122122
isEmbedded?: boolean
123+
applicationUrl?: string
124+
redirectUrls?: string[]
125+
staticRoot?: string
123126
}
124127

125128
interface AppModuleVersionSpecification {

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

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,140 @@ 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+
712+
test('includes admin module with static_root when staticRoot is provided', async () => {
713+
// Given
714+
const client = AppManagementClient.getInstance()
715+
const org = testOrganization()
716+
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce({
717+
publicApiVersions: [{handle: '2024-07'}, {handle: '2024-10'}, {handle: '2025-01'}, {handle: 'unstable'}],
718+
})
719+
vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({
720+
appCreate: {
721+
app: {id: '1', key: 'key', activeRoot: {clientCredentials: {secrets: [{key: 'secret'}]}}},
722+
userErrors: [],
723+
},
724+
})
725+
726+
// When
727+
client.token = () => Promise.resolve('token')
728+
await client.createApp(org, {
729+
name: 'app-name',
730+
isLaunchable: false,
731+
applicationUrl: 'https://extensions.shopifycdn.com',
732+
staticRoot: './dist',
733+
})
734+
735+
// Then
736+
expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith(
737+
expect.objectContaining({
738+
variables: expect.objectContaining({
739+
initialVersion: expect.objectContaining({
740+
source: expect.objectContaining({
741+
modules: expect.arrayContaining([
742+
{
743+
type: 'admin',
744+
config: {admin: {static_root: './dist'}},
745+
},
746+
]),
747+
}),
748+
}),
749+
}),
750+
}),
751+
)
752+
})
753+
754+
test('does not include admin module when staticRoot is not provided', async () => {
755+
// Given
756+
const client = AppManagementClient.getInstance()
757+
const org = testOrganization()
758+
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce({
759+
publicApiVersions: [{handle: '2024-07'}, {handle: '2024-10'}, {handle: '2025-01'}, {handle: 'unstable'}],
760+
})
761+
vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({
762+
appCreate: {
763+
app: {id: '1', key: 'key', activeRoot: {clientCredentials: {secrets: [{key: 'secret'}]}}},
764+
userErrors: [],
765+
},
766+
})
767+
768+
// When
769+
client.token = () => Promise.resolve('token')
770+
await client.createApp(org, {
771+
name: 'app-name',
772+
isLaunchable: false,
773+
})
774+
775+
// Then
776+
expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith(
777+
expect.objectContaining({
778+
variables: expect.objectContaining({
779+
initialVersion: expect.objectContaining({
780+
source: expect.objectContaining({
781+
modules: expect.not.arrayContaining([expect.objectContaining({type: 'admin'})]),
782+
}),
783+
}),
784+
}),
785+
}),
786+
)
787+
})
788+
655789
test('sets embedded to true in app home module', async () => {
656790
// Given
657791
const client = AppManagementClient.getInstance()

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import {ListOrganizations} from '../../api/graphql/business-platform-destination
8282
import {AppHomeSpecIdentifier} from '../../models/extensions/specifications/app_config_app_home.js'
8383
import {BrandingSpecIdentifier} from '../../models/extensions/specifications/app_config_branding.js'
8484
import {AppAccessSpecIdentifier} from '../../models/extensions/specifications/app_config_app_access.js'
85+
import {AdminSpecIdentifier} from '../../models/extensions/specifications/admin.js'
8586

8687
import {DevSessionCreate, DevSessionCreateMutation} from '../../api/graphql/app-dev/generated/dev-session-create.js'
8788
import {
@@ -1212,14 +1213,17 @@ function createAppVars(
12121213
apiVersion: string,
12131214
): CreateAppMutationVariables {
12141215
const {isLaunchable, scopesArray, name} = options
1216+
const defaultAppUrl = isLaunchable ? 'https://example.com' : MAGIC_URL
1217+
const defaultRedirectUrl = isLaunchable ? 'https://example.com/api/auth' : MAGIC_REDIRECT_URL
1218+
12151219
const source: AppVersionSource = {
12161220
source: {
12171221
name,
12181222
modules: [
12191223
{
12201224
type: AppHomeSpecIdentifier,
12211225
config: {
1222-
app_url: isLaunchable ? 'https://example.com' : MAGIC_URL,
1226+
app_url: options.applicationUrl ?? defaultAppUrl,
12231227
// Ext-only apps should be embedded = false, however we are hardcoding this to
12241228
// match Partners behaviour for now
12251229
// https://github.com/Shopify/develop-app-inner-loop/issues/2789
@@ -1237,10 +1241,18 @@ function createAppVars(
12371241
{
12381242
type: AppAccessSpecIdentifier,
12391243
config: {
1240-
redirect_url_allowlist: isLaunchable ? ['https://example.com/api/auth'] : [MAGIC_REDIRECT_URL],
1244+
redirect_url_allowlist: options.redirectUrls ?? [defaultRedirectUrl],
12411245
...(scopesArray && {scopes: scopesArray.map((scope) => scope.trim()).join(',')}),
12421246
},
12431247
},
1248+
...(options.staticRoot
1249+
? [
1250+
{
1251+
type: AdminSpecIdentifier,
1252+
config: {admin: {static_root: options.staticRoot}},
1253+
},
1254+
]
1255+
: []),
12441256
],
12451257
},
12461258
}

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)

0 commit comments

Comments
 (0)