Skip to content
Merged
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
6 changes: 6 additions & 0 deletions packages/app/src/cli/models/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,12 +432,18 @@ export class App<
}

creationDefaultOptions(): CreateAppOptions {
const applicationUrl = this.configuration.application_url
const redirectUrls = this.configuration.auth?.redirect_urls
const staticRoot = this.configuration.admin?.static_root
return {
isLaunchable: this.appIsLaunchable(),
scopesArray: getAppScopesArray(this.configuration),
name: this.name,
isEmbedded: this.appIsEmbedded,
directory: this.directory,
applicationUrl,
redirectUrls,
staticRoot,
}
}

Expand Down
74 changes: 74 additions & 0 deletions packages/app/src/cli/models/app/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3504,6 +3504,80 @@ value = true
})
})
})

test('extracts application_url from template config', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const config = `
client_id = ""
name = "my-app"
application_url = "https://extensions.shopifycdn.com"
embedded = true

[access_scopes]
scopes = "write_products"

[auth]
redirect_urls = ["https://shopify.dev/apps/default-app-home/api/auth"]
`
await writeFile(joinPath(tmpDir, 'shopify.app.toml'), config)
await writeFile(joinPath(tmpDir, 'package.json'), '{}')

const result = await loadConfigForAppCreation(tmpDir, 'my-app')

expect(result).toEqual({
isLaunchable: false,
scopesArray: ['write_products'],
name: 'my-app',
directory: normalizePath(tmpDir),
isEmbedded: false,
applicationUrl: 'https://extensions.shopifycdn.com',
redirectUrls: ['https://shopify.dev/apps/default-app-home/api/auth'],
staticRoot: undefined,
})
})
})

test('extracts admin.static_root from template config', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const config = `
client_id = ""
name = "my-app"
application_url = "https://extensions.shopifycdn.com"
embedded = true

[admin]
static_root = "./dist"

[access_scopes]
scopes = "write_products"
`
await writeFile(joinPath(tmpDir, 'shopify.app.toml'), config)
await writeFile(joinPath(tmpDir, 'package.json'), '{}')

const result = await loadConfigForAppCreation(tmpDir, 'my-app')

expect(result.staticRoot).toBe('./dist')
})
})

test('defaults applicationUrl and redirectUrls to undefined when not in template config', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const config = `
client_id = ""
name = "my-app"

[access_scopes]
scopes = "write_products"
`
await writeFile(joinPath(tmpDir, 'shopify.app.toml'), config)
await writeFile(joinPath(tmpDir, 'package.json'), '{}')

const result = await loadConfigForAppCreation(tmpDir, 'my-app')

expect(result.applicationUrl).toBeUndefined()
expect(result.redirectUrls).toBeUndefined()
})
})
})

describe('loadOpaqueApp', () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/app/src/cli/models/app/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ export async function loadConfigForAppCreation(directory: string, name: string):
const isLaunchable = webs.some((web) => isWebType(web, WebType.Frontend) || isWebType(web, WebType.Backend))

const scopesArray = getAppScopesArray(rawConfig as CurrentAppConfiguration)
const appConfig = rawConfig as CurrentAppConfiguration
const applicationUrl = appConfig.application_url
const redirectUrls = appConfig.auth?.redirect_urls
const staticRoot = appConfig.admin?.static_root

return {
isLaunchable,
Expand All @@ -214,6 +218,9 @@ export async function loadConfigForAppCreation(directory: string, name: string):
directory: project.directory,
// By default, and ONLY for `app init`, we consider the app as embedded if it is launchable.
isEmbedded: isLaunchable,
applicationUrl,
redirectUrls,
staticRoot,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {BaseConfigType, ZodSchemaType} from '../schemas.js'
import {zod} from '@shopify/cli-kit/node/schema'
import {joinPath} from '@shopify/cli-kit/node/path'

export const AdminSpecIdentifier = 'admin'

const AdminSchema = zod.object({
admin: zod
.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ export interface AppConfigurationUsedByCli {
auth?: {
redirect_urls: string[]
}
admin?: {
static_root?: string
}
}
3 changes: 3 additions & 0 deletions packages/app/src/cli/utilities/developer-platform-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ export interface CreateAppOptions {
scopesArray?: string[]
directory?: string
isEmbedded?: boolean
applicationUrl?: string
redirectUrls?: string[]
staticRoot?: string
}

interface AppModuleVersionSpecification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,140 @@ describe('createApp', () => {
expect(result).toMatchObject(expectedApp)
})

test('uses applicationUrl and redirectUrls from options when provided', async () => {
// Given
const client = AppManagementClient.getInstance()
const org = testOrganization()
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce({
publicApiVersions: [{handle: '2024-07'}, {handle: '2024-10'}, {handle: '2025-01'}, {handle: 'unstable'}],
})
vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({
appCreate: {
app: {id: '1', key: 'key', activeRoot: {clientCredentials: {secrets: [{key: 'secret'}]}}},
userErrors: [],
},
})

// When
client.token = () => Promise.resolve('token')
await client.createApp(org, {
name: 'app-name',
isLaunchable: false,
applicationUrl: 'https://extensions.shopifycdn.com',
redirectUrls: ['https://shopify.dev/apps/default-app-home/api/auth'],
})

// Then
expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith({
query: CreateApp,
token: 'token',
variables: {
organizationId: 'gid://shopify/Organization/1',
initialVersion: {
source: {
name: 'app-name',
modules: expect.arrayContaining([
{
type: 'app_home',
config: {
app_url: 'https://extensions.shopifycdn.com',
embedded: true,
},
},
{
type: 'app_access',
config: {
redirect_url_allowlist: ['https://shopify.dev/apps/default-app-home/api/auth'],
},
},
]),
},
},
},
unauthorizedHandler: {
handler: expect.any(Function),
type: 'token_refresh',
},
})
})

test('includes admin module with static_root when staticRoot is provided', async () => {
// Given
const client = AppManagementClient.getInstance()
const org = testOrganization()
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce({
publicApiVersions: [{handle: '2024-07'}, {handle: '2024-10'}, {handle: '2025-01'}, {handle: 'unstable'}],
})
vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({
appCreate: {
app: {id: '1', key: 'key', activeRoot: {clientCredentials: {secrets: [{key: 'secret'}]}}},
userErrors: [],
},
})

// When
client.token = () => Promise.resolve('token')
await client.createApp(org, {
name: 'app-name',
isLaunchable: false,
applicationUrl: 'https://extensions.shopifycdn.com',
staticRoot: './dist',
})

// Then
expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
initialVersion: expect.objectContaining({
source: expect.objectContaining({
modules: expect.arrayContaining([
{
type: 'admin',
config: {admin: {static_root: './dist'}},
},
]),
}),
}),
}),
}),
)
})

test('does not include admin module when staticRoot is not provided', async () => {
// Given
const client = AppManagementClient.getInstance()
const org = testOrganization()
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce({
publicApiVersions: [{handle: '2024-07'}, {handle: '2024-10'}, {handle: '2025-01'}, {handle: 'unstable'}],
})
vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({
appCreate: {
app: {id: '1', key: 'key', activeRoot: {clientCredentials: {secrets: [{key: 'secret'}]}}},
userErrors: [],
},
})

// When
client.token = () => Promise.resolve('token')
await client.createApp(org, {
name: 'app-name',
isLaunchable: false,
})

// Then
expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
initialVersion: expect.objectContaining({
source: expect.objectContaining({
modules: expect.not.arrayContaining([expect.objectContaining({type: 'admin'})]),
}),
}),
}),
}),
)
})

test('sets embedded to true in app home module', async () => {
// Given
const client = AppManagementClient.getInstance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {ListOrganizations} from '../../api/graphql/business-platform-destination
import {AppHomeSpecIdentifier} from '../../models/extensions/specifications/app_config_app_home.js'
import {BrandingSpecIdentifier} from '../../models/extensions/specifications/app_config_branding.js'
import {AppAccessSpecIdentifier} from '../../models/extensions/specifications/app_config_app_access.js'
import {AdminSpecIdentifier} from '../../models/extensions/specifications/admin.js'

import {DevSessionCreate, DevSessionCreateMutation} from '../../api/graphql/app-dev/generated/dev-session-create.js'
import {
Expand Down Expand Up @@ -1212,14 +1213,17 @@ function createAppVars(
apiVersion: string,
): CreateAppMutationVariables {
const {isLaunchable, scopesArray, name} = options
const defaultAppUrl = isLaunchable ? 'https://example.com' : MAGIC_URL
const defaultRedirectUrl = isLaunchable ? 'https://example.com/api/auth' : MAGIC_REDIRECT_URL

const source: AppVersionSource = {
source: {
name,
modules: [
{
type: AppHomeSpecIdentifier,
config: {
app_url: isLaunchable ? 'https://example.com' : MAGIC_URL,
app_url: options.applicationUrl ?? defaultAppUrl,
// Ext-only apps should be embedded = false, however we are hardcoding this to
// match Partners behaviour for now
// https://github.com/Shopify/develop-app-inner-loop/issues/2789
Expand All @@ -1237,10 +1241,18 @@ function createAppVars(
{
type: AppAccessSpecIdentifier,
config: {
redirect_url_allowlist: isLaunchable ? ['https://example.com/api/auth'] : [MAGIC_REDIRECT_URL],
redirect_url_allowlist: options.redirectUrls ?? [defaultRedirectUrl],
...(scopesArray && {scopes: scopesArray.map((scope) => scope.trim()).join(',')}),
},
},
...(options.staticRoot
? [
{
type: AdminSpecIdentifier,
config: {admin: {static_root: options.staticRoot}},
},
]
: []),
],
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,39 @@ describe('createApp', () => {
})
})

test('uses applicationUrl and redirectUrls from options when provided', async () => {
// Given
const partnersClient = PartnersClient.getInstance(testPartnersUserSession)
vi.mocked(appNamePrompt).mockResolvedValue('app-name')
vi.mocked(partnersRequest).mockResolvedValueOnce({appCreate: {app: APP1, userErrors: []}})
const variables = {
org: 1,
title: LOCAL_APP.name,
appUrl: 'https://extensions.shopifycdn.com',
redir: ['https://shopify.dev/apps/default-app-home/api/auth'],
requestedAccessScopes: ['write_products'],
type: 'undecided',
}

// When
await partnersClient.createApp(
{...ORG1, source: OrganizationSource.Partners},
{
name: LOCAL_APP.name,
isLaunchable: false,
scopesArray: ['write_products'],
applicationUrl: 'https://extensions.shopifycdn.com',
redirectUrls: ['https://shopify.dev/apps/default-app-home/api/auth'],
},
)

// Then
expect(partnersRequest).toHaveBeenCalledWith(CreateAppQuery, 'token', variables, undefined, undefined, {
type: 'token_refresh',
handler: expect.any(Function),
})
})

test('throws error if requests has a user error', async () => {
// Given
const partnersClient = PartnersClient.getInstance(testPartnersUserSession)
Expand Down
Loading
Loading