diff --git a/packages/app/src/cli/models/app/app.ts b/packages/app/src/cli/models/app/app.ts index 5c6a5eab81c..2a207145ba8 100644 --- a/packages/app/src/cli/models/app/app.ts +++ b/packages/app/src/cli/models/app/app.ts @@ -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, } } diff --git a/packages/app/src/cli/models/app/loader.test.ts b/packages/app/src/cli/models/app/loader.test.ts index 4f35ca534b0..79c2de4e264 100644 --- a/packages/app/src/cli/models/app/loader.test.ts +++ b/packages/app/src/cli/models/app/loader.test.ts @@ -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', () => { diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index 2e5aad94508..c1c64af1f1c 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -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, @@ -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, } } diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index 8b27e3b501a..1962aae976a 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -120,6 +120,8 @@ export interface CreateAppOptions { scopesArray?: string[] directory?: string isEmbedded?: boolean + applicationUrl?: string + redirectUrls?: string[] } interface AppModuleVersionSpecification { diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts index 5e530f3c72f..5aef498b7f9 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts @@ -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() diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index 0e0dbbf6592..6f98dbd5eaa 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -1203,6 +1203,9 @@ 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, @@ -1210,7 +1213,7 @@ function createAppVars( { 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 @@ -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(',')}), }, }, diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts index e2cce11bf64..e4e1d260ae0 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts @@ -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) diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts index 9f54e23a50e..e7c849cc964 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts @@ -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', } } @@ -395,7 +383,7 @@ export class PartnersClient implements DeveloperPlatformClient { } async createApp(org: Organization, options: CreateAppOptions): Promise { - 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(', ')