diff --git a/.changeset/oauth-consent-last-active-org.md b/.changeset/oauth-consent-last-active-org.md new file mode 100644 index 00000000000..1af3cc141b9 --- /dev/null +++ b/.changeset/oauth-consent-last-active-org.md @@ -0,0 +1,5 @@ +--- +"@clerk/ui": patch +--- + +Default the organization selection in `` to the user's last active organization, falling back to the first membership when it is not set or no longer available. diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index 394fd953833..21d5fac767a 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -46,8 +46,11 @@ function _OAuthConsent() { })) : []; + const lastActiveOrgId = clerk.session?.lastActiveOrganizationId; + const defaultOrg = orgOptions.find(o => o.value === lastActiveOrgId)?.value ?? orgOptions[0]?.value ?? null; + const [selectedOrg, setSelectedOrg] = useState(null); - const effectiveOrg = selectedOrg ?? orgOptions[0]?.value ?? null; + const effectiveOrg = selectedOrg ?? defaultOrg; // onAllow and onDeny are always provided as a pair by the accounts portal. const hasContextCallbacks = Boolean(ctx.onAllow || ctx.onDeny); diff --git a/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx b/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx index c641e0c69ba..dce4347d6fd 100644 --- a/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx +++ b/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx @@ -415,5 +415,87 @@ describe('OAuthConsent', () => { expect(form.querySelector('input[name="organization_id"]')).toBeNull(); }); }); + + it('defaults the selected org to session.lastActiveOrganizationId when it matches a membership', async () => { + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withUser({ + email_addresses: ['jane@example.com'], + organization_memberships: [ + { id: 'org_1', name: 'Acme Corp' }, + { id: 'org_2', name: 'Globex' }, + { id: 'org_3', name: 'Initech' }, + ], + }); + f.withOrganizations(); + }); + + fixtures.clerk.session.lastActiveOrganizationId = 'org_3'; + + props.setProps({ componentName: 'OAuthConsent', __internal_enableOrgSelection: true } as any); + mockOAuthApplication(fixtures.clerk, { getConsentInfo: vi.fn().mockResolvedValue(fakeConsentInfo) }); + + const { baseElement } = render(, { wrapper }); + + await waitFor(() => { + const form = baseElement.querySelector('form[action*="/v1/me/oauth/consent/"]')!; + const hiddenInput = form.querySelector('input[name="organization_id"]') as HTMLInputElement | null; + expect(hiddenInput).not.toBeNull(); + expect(hiddenInput!.value).toBe('org_3'); + }); + }); + + it('falls back to the first membership when lastActiveOrganizationId does not match any membership', async () => { + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withUser({ + email_addresses: ['jane@example.com'], + organization_memberships: [ + { id: 'org_1', name: 'Acme Corp' }, + { id: 'org_2', name: 'Globex' }, + ], + }); + f.withOrganizations(); + }); + + fixtures.clerk.session.lastActiveOrganizationId = 'org_deleted'; + + props.setProps({ componentName: 'OAuthConsent', __internal_enableOrgSelection: true } as any); + mockOAuthApplication(fixtures.clerk, { getConsentInfo: vi.fn().mockResolvedValue(fakeConsentInfo) }); + + const { baseElement } = render(, { wrapper }); + + await waitFor(() => { + const form = baseElement.querySelector('form[action*="/v1/me/oauth/consent/"]')!; + const hiddenInput = form.querySelector('input[name="organization_id"]') as HTMLInputElement | null; + expect(hiddenInput).not.toBeNull(); + expect(hiddenInput!.value).toBe('org_1'); + }); + }); + + it('falls back to the first membership when lastActiveOrganizationId is null', async () => { + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withUser({ + email_addresses: ['jane@example.com'], + organization_memberships: [ + { id: 'org_1', name: 'Acme Corp' }, + { id: 'org_2', name: 'Globex' }, + ], + }); + f.withOrganizations(); + }); + + fixtures.clerk.session.lastActiveOrganizationId = null; + + props.setProps({ componentName: 'OAuthConsent', __internal_enableOrgSelection: true } as any); + mockOAuthApplication(fixtures.clerk, { getConsentInfo: vi.fn().mockResolvedValue(fakeConsentInfo) }); + + const { baseElement } = render(, { wrapper }); + + await waitFor(() => { + const form = baseElement.querySelector('form[action*="/v1/me/oauth/consent/"]')!; + const hiddenInput = form.querySelector('input[name="organization_id"]') as HTMLInputElement | null; + expect(hiddenInput).not.toBeNull(); + expect(hiddenInput!.value).toBe('org_1'); + }); + }); }); });