diff --git a/.changeset/quiet-scopes-offline.md b/.changeset/quiet-scopes-offline.md
new file mode 100644
index 00000000000..8f2b0ac452b
--- /dev/null
+++ b/.changeset/quiet-scopes-offline.md
@@ -0,0 +1,5 @@
+---
+'@clerk/ui': patch
+---
+
+`` and `` no longer show an empty permissions list when an OAuth client requests only the `offline_access` scope.
diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
index e85b0229dce..b9f6fcd00cb 100644
--- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
+++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
@@ -263,22 +263,24 @@ function _OAuthConsent() {
onChange={setSelectedOrg}
/>
)}
-
-
-
-
-
- {displayedScopes.map(item => (
-
- {item.description || item.scope || ''}
-
- ))}
-
-
+ {displayedScopes.length > 0 && (
+
+
+
+
+
+ {displayedScopes.map(item => (
+
+ {item.description || item.scope || ''}
+
+ ))}
+
+
+ )}
{
await waitFor(() => {
expect(getByText('Clerk CLI')).toBeVisible();
+ expect(getByText('This will allow Clerk CLI access to:')).toBeVisible();
expect(getByText('View your identity')).toBeVisible();
expect(getByText('Access your email address')).toBeVisible();
});
@@ -126,6 +127,27 @@ describe('OAuthConsent', () => {
expect(queryByText('Your private metadata')).toBeNull();
});
+ it('hides the scope list when only offline_access is requested', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['jane@example.com'] });
+ });
+
+ props.setProps({ componentName: 'OAuthConsent' } as any);
+ mockOAuthApplication(fixtures.clerk, {
+ getConsentInfo: vi.fn().mockResolvedValue({
+ ...fakeConsentInfo,
+ scopes: [{ scope: 'offline_access', description: 'Offline access', requiresConsent: true }],
+ }),
+ });
+
+ const { getByText, queryByText } = render(, { wrapper });
+
+ await waitFor(() => {
+ expect(getByText(/You'll stay signed in until you sign out or revoke access\./)).toBeVisible();
+ });
+ expect(queryByText('This will allow Clerk CLI access to:')).toBeNull();
+ });
+
it('supports localizing the private metadata scope description', async () => {
const {
wrapper: Wrapper,
diff --git a/packages/ui/src/components/OAuthDeviceVerification/OAuthDeviceVerification.tsx b/packages/ui/src/components/OAuthDeviceVerification/OAuthDeviceVerification.tsx
index bda29d436b3..54a436e6f15 100644
--- a/packages/ui/src/components/OAuthDeviceVerification/OAuthDeviceVerification.tsx
+++ b/packages/ui/src/components/OAuthDeviceVerification/OAuthDeviceVerification.tsx
@@ -313,22 +313,24 @@ function OAuthDeviceVerificationInternal() {
onChange={setSelectedOrg}
/>
)}
-
-
-
-
-
- {displayedScopes.map(scope => (
-
- {scope.description || scope.scope}
-
- ))}
-
-
+ {displayedScopes.length > 0 && (
+
+
+
+
+
+ {displayedScopes.map(scope => (
+
+ {scope.description || scope.scope}
+
+ ))}
+
+
+ )}
{
expect(getByText("You'll stay signed in until you sign out or revoke access.")).toBeVisible();
});
+ it('hides the scope list when only offline_access is requested', async () => {
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: { ...originalLocation, search: '?user_code=BFWS-ZBZM' },
+ });
+ const { wrapper } = await setup({
+ ...verificationInfo,
+ scopes: [{ scope: 'offline_access', description: 'Offline access', requiresConsent: true }],
+ });
+ const { getByText, queryByText } = render(, { wrapper });
+
+ await waitFor(() => expect(getByText('Allow TV App to access your account?')).toBeVisible());
+ expect(getByText("You'll stay signed in until you sign out or revoke access.")).toBeVisible();
+ expect(queryByText('This will allow TV App access to:')).toBeNull();
+ });
+
it('keeps an invalid URL prefill editable without calling FAPI', async () => {
Object.defineProperty(window, 'location', {
configurable: true,