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
4 changes: 4 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,16 @@ export class App<
}

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

Expand Down
50 changes: 50 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,56 @@ 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'],
})
})
})

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
5 changes: 5 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,9 @@ 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

return {
isLaunchable,
Expand All @@ -214,6 +217,8 @@ 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,
}
}

Expand Down
2 changes: 2 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,8 @@ export interface CreateAppOptions {
scopesArray?: string[]
directory?: string
isEmbedded?: boolean
applicationUrl?: string
redirectUrls?: string[]
}

interface AppModuleVersionSpecification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,63 @@ 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('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 @@ -1203,14 +1203,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 @@ -1228,7 +1231,7 @@ 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(',')}),
},
},
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
Original file line number Diff line number Diff line change
Expand Up @@ -170,30 +170,18 @@ import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'
const MAGIC_URL = 'https://shopify.dev/apps/default-app-home'
const MAGIC_REDIRECT_URL = 'https://shopify.dev/apps/default-app-home/api/auth'

function getAppVars(
org: Organization,
name: string,
isLaunchable = true,
scopesArray?: string[],
): CreateAppQueryVariables {
if (isLaunchable) {
return {
org: parseInt(org.id, 10),
title: name,
appUrl: 'https://example.com',
redir: ['https://example.com/api/auth'],
requestedAccessScopes: scopesArray ?? [],
type: 'undecided',
}
} else {
return {
org: parseInt(org.id, 10),
title: name,
appUrl: MAGIC_URL,
redir: [MAGIC_REDIRECT_URL],
requestedAccessScopes: scopesArray ?? [],
type: 'undecided',
}
function getAppVars(org: Organization, options: CreateAppOptions): CreateAppQueryVariables {
const {name, isLaunchable = true, scopesArray} = options
const defaultAppUrl = isLaunchable ? 'https://example.com' : MAGIC_URL
const defaultRedirectUrl = isLaunchable ? 'https://example.com/api/auth' : MAGIC_REDIRECT_URL

return {
org: parseInt(org.id, 10),
title: name,
appUrl: options.applicationUrl ?? defaultAppUrl,
redir: options.redirectUrls ?? [defaultRedirectUrl],
requestedAccessScopes: scopesArray ?? [],
type: 'undecided',
}
}

Expand Down Expand Up @@ -395,7 +383,7 @@ export class PartnersClient implements DeveloperPlatformClient {
}

async createApp(org: Organization, options: CreateAppOptions): Promise<OrganizationApp> {
const variables: CreateAppQueryVariables = getAppVars(org, options.name, options.isLaunchable, options.scopesArray)
const variables: CreateAppQueryVariables = getAppVars(org, options)
const result: CreateAppQuerySchema = await this.request(CreateAppQuery, variables)
if (result.appCreate.userErrors.length > 0) {
const errors = result.appCreate.userErrors.map((error) => error.message).join(', ')
Expand Down
Loading