Skip to content

Commit fc8c6f2

Browse files
committed
Improve theme scope error message
1 parent 69fafaa commit fc8c6f2

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/cli-kit': patch
3+
---
4+
5+
Improve the error shown when theme commands use an Admin API token that is missing required theme access scopes.

packages/cli-kit/src/public/node/themes/api.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,26 @@ describe('fetchThemes', () => {
188188
expect(themes[0]!.processing).toBeFalsy()
189189
expect(themes[1]!.processing).toBeTruthy()
190190
})
191+
192+
test('throws a friendly error when the token is missing the required themes access scope', async () => {
193+
// Given
194+
const errorResponse = {
195+
status: 200,
196+
errors: [
197+
{
198+
message: 'Access denied for themes field. Required access: `read_themes` access scope.',
199+
extensions: {code: 'ACCESS_DENIED', requiredAccess: '`read_themes` access scope.'},
200+
path: ['themes'],
201+
} as any,
202+
],
203+
}
204+
vi.mocked(adminRequestDoc).mockRejectedValue(new ClientError(errorResponse, {query: ''}))
205+
206+
// When/Then
207+
await expect(fetchThemes(session)).rejects.toThrow(
208+
'The authenticated account or access token is missing `read_themes` access scope.',
209+
)
210+
})
191211
})
192212

193213
describe('fetchChecksums', () => {

packages/cli-kit/src/public/node/themes/api.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {AdminSession} from '../session.js'
3030
import {AbortError} from '../error.js'
3131
import {outputDebug} from '../output.js'
3232
import {recordTiming, recordEvent, recordError} from '../analytics.js'
33+
import {ClientError} from 'graphql-request'
3334

3435
export type ThemeParams = Partial<Pick<Theme, 'name' | 'role' | 'processing' | 'src'>>
3536
export type AssetParams = Pick<ThemeAsset, 'key'> & Partial<Pick<ThemeAsset, 'value' | 'attachment'>>
@@ -90,7 +91,11 @@ export async function fetchThemes(session: AdminSession): Promise<Theme[]> {
9091
variables: {after},
9192
responseOptions: {handleErrors: false},
9293
preferredBehaviour: THEME_API_NETWORK_BEHAVIOUR,
94+
}).catch((error: unknown) => {
95+
abortIfMissingThemeAccessScope(error)
96+
throw error
9397
})
98+
9499
if (!response.themes) {
95100
unexpectedGraphQLError('Failed to fetch themes')
96101
}
@@ -606,6 +611,37 @@ function unexpectedGraphQLError(message: string): never {
606611
throw recordError(new AbortError(message))
607612
}
608613

614+
function abortIfMissingThemeAccessScope(error: unknown): void {
615+
if (!(error instanceof ClientError)) return
616+
617+
const requiredAccess = getRequiredAccessForAccessDeniedError(error)
618+
if (!requiredAccess) return
619+
620+
const tryMessage = [
621+
'If you authenticated with a custom app Admin API access token, open the custom app in your Shopify admin,',
622+
'add the required theme access scopes, reinstall the app, and use the new access token.',
623+
'For theme pull, theme list, and theme info, add `read_themes`.',
624+
'For theme push and theme dev, add both `read_themes` and `write_themes`.',
625+
'If you authenticated with your Shopify account, make sure your staff or collaborator account can access Online Store themes, then run `shopify auth logout` and try again.',
626+
'See https://shopify.dev/api/usage/access-scopes.',
627+
].join(' ')
628+
629+
throw recordError(
630+
new AbortError(`The authenticated account or access token is missing ${requiredAccess}.`, tryMessage),
631+
)
632+
}
633+
634+
function getRequiredAccessForAccessDeniedError(error: ClientError): string | undefined {
635+
const graphQLErrors = error.response.errors
636+
if (!Array.isArray(graphQLErrors)) return undefined
637+
638+
const accessDeniedError = graphQLErrors.find((graphQLError) => graphQLError.extensions?.code === 'ACCESS_DENIED')
639+
const requiredAccess = accessDeniedError?.extensions?.requiredAccess
640+
if (typeof requiredAccess !== 'string') return undefined
641+
642+
return requiredAccess.replace(/\.$/, '')
643+
}
644+
609645
function themeGid(id: number): string {
610646
return `gid://shopify/OnlineStoreTheme/${id}`
611647
}

0 commit comments

Comments
 (0)