From 70cacdef933506f885c99bf64407655360508475 Mon Sep 17 00:00:00 2001 From: Mbarak Bujra Date: Fri, 26 Jun 2026 23:40:30 -0400 Subject: [PATCH] Handle missing theme write access Route theme creation through the theme Admin request wrapper so ACCESS_DENIED responses use the existing friendly missing access error. Normalize requiredAccess prose that starts with "The user needs" before embedding it in the user-facing message. Co-authored-by: Pi AI Assisted-By: devx/a1027ad2-ceec-4016-b728-d7c842f1fe88 --- .../friendly-theme-create-access-error.md | 5 +++++ .../src/public/node/themes/api.test.ts | 21 +++++++++++++++---- .../cli-kit/src/public/node/themes/api.ts | 13 ++++++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 .changeset/friendly-theme-create-access-error.md diff --git a/.changeset/friendly-theme-create-access-error.md b/.changeset/friendly-theme-create-access-error.md new file mode 100644 index 00000000000..3a0a35384da --- /dev/null +++ b/.changeset/friendly-theme-create-access-error.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': patch +--- + +Show a friendly error when theme creation is denied by missing theme write access diff --git a/packages/cli-kit/src/public/node/themes/api.test.ts b/packages/cli-kit/src/public/node/themes/api.test.ts index 79f5954b6d4..c6da96b898a 100644 --- a/packages/cli-kit/src/public/node/themes/api.test.ts +++ b/packages/cli-kit/src/public/node/themes/api.test.ts @@ -354,6 +354,19 @@ describe('themeCreate', () => { preferredBehaviour: expectedApiOptions, }) }) + + test('throws a friendly error when access is denied by a missing theme write access scope', async () => { + vi.mocked(adminRequestDoc).mockRejectedValue( + themeAccessDeniedError( + 'The user needs write_themes and an exemption from Shopify to modify themes.', + 'themeCreate', + ), + ) + + await expect(themeCreate(params, session)).rejects.toThrow( + 'The authenticated account or access token is missing write_themes and an exemption from Shopify to modify themes.', + ) + }) }) describe('themeUpdate', () => { @@ -808,11 +821,11 @@ describe('parseThemeFileContent', () => { }) }) -function themeAccessDeniedError(requiredAccess?: string): ClientError { +function themeAccessDeniedError(requiredAccess?: string, field = 'themes'): ClientError { const extensions = requiredAccess ? {code: 'ACCESS_DENIED', requiredAccess} : {code: 'ACCESS_DENIED'} const message = requiredAccess - ? `Access denied for themes field. Required access: ${requiredAccess}` - : 'Access denied for themes field.' + ? `Access denied for ${field} field. Required access: ${requiredAccess}` + : `Access denied for ${field} field.` return new ClientError( { @@ -821,7 +834,7 @@ function themeAccessDeniedError(requiredAccess?: string): ClientError { { message, extensions, - path: ['themes'], + path: [field], } as any, ], }, diff --git a/packages/cli-kit/src/public/node/themes/api.ts b/packages/cli-kit/src/public/node/themes/api.ts index 5b91b7130bc..5bbd328b6aa 100644 --- a/packages/cli-kit/src/public/node/themes/api.ts +++ b/packages/cli-kit/src/public/node/themes/api.ts @@ -153,7 +153,7 @@ export async function findDevelopmentThemeByName(name: string, session: AdminSes export async function themeCreate(params: ThemeParams, session: AdminSession): Promise { const themeSource = params.src ?? SkeletonThemeCdn recordEvent('theme-api:create-theme') - const {themeCreate} = await adminRequestDoc({ + const {themeCreate} = await requestThemeAdminDoc({ query: ThemeCreate, session, variables: { @@ -684,7 +684,16 @@ function getThemeAccessRequirementForAccessDeniedError(error: ClientError): stri const requiredAccess = accessDeniedError.extensions?.requiredAccess if (typeof requiredAccess !== 'string') return DEFAULT_THEME_ACCESS_REQUIREMENT - return requiredAccess.trim().replace(/\.$/, '') || DEFAULT_THEME_ACCESS_REQUIREMENT + return formatThemeAccessRequirement(requiredAccess) +} + +function formatThemeAccessRequirement(requiredAccess: string): string { + const requirement = requiredAccess + .trim() + .replace(/\.$/, '') + .replace(/^The user needs\s+/i, '') + + return requirement || DEFAULT_THEME_ACCESS_REQUIREMENT } function themeGid(id: number): string {