From f83526bbc9d9c909dc37a6266c8b4ae09d503005 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:58:59 -0300 Subject: [PATCH 01/14] Add `OrganizationCreationDefaults` resource --- .../resources/OrganizationCreationDefaults.ts | 42 +++++++++++++++++++ packages/shared/src/types/index.ts | 3 +- .../src/types/organizationCreationDefaults.ts | 20 +++++++++ packages/shared/src/types/snapshots.ts | 3 ++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts create mode 100644 packages/shared/src/types/organizationCreationDefaults.ts diff --git a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts new file mode 100644 index 00000000000..83b927b68dd --- /dev/null +++ b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts @@ -0,0 +1,42 @@ +import type { + OrganizationCreationDefaultsJSON, + OrganizationCreationDefaultsJSONSnapshot, + OrganizationCreationDefaultsResource, +} from '@clerk/shared/types'; + +import { BaseResource } from './internal'; + +export class OrganizationCreationDefaults extends BaseResource implements OrganizationCreationDefaultsResource { + creationAdvisory: { + type: 'existing_org_with_domain'; + severity: 'warning'; + } | null = null; + + public constructor(data: OrganizationCreationDefaultsJSON | OrganizationCreationDefaultsJSONSnapshot | null = null) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: OrganizationCreationDefaultsJSON | OrganizationCreationDefaultsJSONSnapshot | null): this { + if (!data) { + return this; + } + + if (data.creation_advisory) { + this.creationAdvisory = this.withDefault(data.creation_advisory, this.creationAdvisory ?? null); + } + + return this; + } + + public __internal_toSnapshot(): OrganizationCreationDefaultsJSONSnapshot { + return { + creation_advisory: this.creationAdvisory + ? { + type: this.creationAdvisory.type, + severity: this.creationAdvisory.severity, + } + : null, + } as unknown as OrganizationCreationDefaultsJSONSnapshot; + } +} diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index d1f50cfde7c..068e5728cae 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -13,6 +13,7 @@ export type * from './customPages'; export type * from './deletedObject'; export type * from './devtools'; export type * from './displayConfig'; +export type * from './elementIds'; export type * from './emailAddress'; export type * from './enterpriseAccount'; export type * from './environment'; @@ -32,6 +33,7 @@ export type * from './localization'; export type * from './multiDomain'; export type * from './oauth'; export type * from './organization'; +export type * from './organizationCreationDefaults'; export type * from './organizationDomain'; export type * from './organizationInvitation'; export type * from './organizationMembership'; @@ -49,7 +51,6 @@ export type * from './protectConfig'; export type * from './redirects'; export type * from './resource'; export type * from './role'; -export type * from './elementIds'; export type * from './router'; /** * TODO @revamp-hooks: Drop this in the next major release. diff --git a/packages/shared/src/types/organizationCreationDefaults.ts b/packages/shared/src/types/organizationCreationDefaults.ts new file mode 100644 index 00000000000..e510af241f8 --- /dev/null +++ b/packages/shared/src/types/organizationCreationDefaults.ts @@ -0,0 +1,20 @@ +import type { ClerkResourceJSON } from './json'; +import type { ClerkResource } from './resource'; + +export type OrganizationCreationAdvisoryType = 'existing_org_with_domain'; + +export type OrganizationCreationAdvisorySeverity = 'warning'; + +export interface OrganizationCreationDefaultsJSON extends ClerkResourceJSON { + creation_advisory: { + type: OrganizationCreationAdvisoryType; + severity: OrganizationCreationAdvisorySeverity; + } | null; +} + +export interface OrganizationCreationDefaultsResource extends ClerkResource { + creationAdvisory: { + type: OrganizationCreationAdvisoryType; + severity: OrganizationCreationAdvisorySeverity; + } | null; +} diff --git a/packages/shared/src/types/snapshots.ts b/packages/shared/src/types/snapshots.ts index 6db74374c44..a1d239c329f 100644 --- a/packages/shared/src/types/snapshots.ts +++ b/packages/shared/src/types/snapshots.ts @@ -28,6 +28,7 @@ import type { VerificationJSON, Web3WalletJSON, } from './json'; +import type { OrganizationCreationDefaultsJSON } from './organizationCreationDefaults'; import type { OrganizationSettingsJSON } from './organizationSettings'; import type { ProtectConfigJSON } from './protectConfig'; import type { SignInJSON } from './signIn'; @@ -143,6 +144,8 @@ export type OrganizationMembershipJSONSnapshot = OrganizationMembershipJSON; export type OrganizationSettingsJSONSnapshot = OrganizationSettingsJSON; +export type OrganizationCreationDefaultsJSONSnapshot = OrganizationCreationDefaultsJSON; + export type PasskeyJSONSnapshot = Override; export type PhoneNumberJSONSnapshot = Override< From 81557835b8a31ac9880863c95eb438a746a9a434 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:02:02 -0300 Subject: [PATCH 02/14] Add TODOs for UI tests --- .../__tests__/TaskChooseOrganization.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx index dbe835f3540..987235c642b 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx @@ -353,4 +353,10 @@ describe('TaskChooseOrganization', () => { expect(await findByText('Existing Org')).toBeInTheDocument(); }); }); + + describe('with organization creation defaults', () => { + it.todo('displays warning when organization already exists for user email domain'); + + it.todo('prefills create organization form with defaults'); + }); }); From 3751b00c8dd755ecc5d254803f3108b4ba72638a Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:04:34 -0300 Subject: [PATCH 03/14] Add alert for creation advisory --- .../resources/OrganizationCreationDefaults.ts | 10 ++++++++ packages/localizations/src/en-US.ts | 4 +++ packages/shared/src/types/localization.ts | 3 +++ .../CreateOrganizationScreen.tsx | 14 +++++++++++ .../OrganizationCreationDefaultsAlert.tsx | 25 +++++++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx diff --git a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts index 83b927b68dd..16ac507e142 100644 --- a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts +++ b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts @@ -29,6 +29,16 @@ export class OrganizationCreationDefaults extends BaseResource implements Organi return this; } + static async retrieve(): Promise { + return await BaseResource._fetch({ + path: '/me/organization_creation_defaults', + method: 'GET', + }).then(res => { + const data = res?.response as unknown as OrganizationCreationDefaultsJSON; + return new OrganizationCreationDefaults(data); + }); + } + public __internal_toSnapshot(): OrganizationCreationDefaultsJSONSnapshot { return { creation_advisory: this.creationAdvisory diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 8f8cba4e707..9eb252dafb2 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -884,6 +884,10 @@ export const enUS: LocalizationResource = { actionLink: 'Sign out', actionText: 'Signed in as {{identifier}}', }, + alerts: { + existingOrgWithDomain: + 'An organization already exists for the detected company name and email domain. Join by invitation.', + }, }, taskResetPassword: { formButtonPrimary: 'Reset Password', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 806826143d8..7b5e19d33df 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1314,6 +1314,9 @@ export type __internal_LocalizationResource = { title: LocalizationValue; subtitle: LocalizationValue; }; + alerts: { + existingOrgWithDomain: LocalizationValue; + }; }; taskResetPassword: { title: LocalizationValue; diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index 2cf177d11a4..75d7c0baa07 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -4,6 +4,7 @@ import type { CreateOrganizationParams } from '@clerk/shared/types'; import { useEnvironment } from '@/ui/contexts'; import { useSessionTasksContext, useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; import { localizationKeys } from '@/ui/customizables'; +// or from '@/ui/elements' import { useCardState } from '@/ui/elements/contexts'; import { Form } from '@/ui/elements/Form'; import { FormButtonContainer } from '@/ui/elements/FormButtons'; @@ -14,6 +15,17 @@ import { handleError } from '@/ui/utils/errorHandler'; import { useFormControl } from '@/ui/utils/useFormControl'; import { organizationListParams } from '../../../OrganizationSwitcher/utils'; +import { OrganizationCreationDefaultsAlert } from './OrganizationCreationDefaultsAlert'; + +// TODO: Replace with actual API call to OrganizationCreationDefaults.retrieve() +const organizationCreationDefaults = { + creationAdvisory: { + type: 'existing_org_with_domain' as const, + severity: 'warning' as const, + }, + pathRoot: '', + reload: () => Promise.resolve({} as any), +}; type CreateOrganizationScreenProps = { onCancel?: () => void; @@ -88,7 +100,9 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = + ({ padding: `${t.space.$none} ${t.space.$10} ${t.space.$8}` })}> + + ); +} + +const advisoryTypeToLocalizationKey: Record = { + existing_org_with_domain: localizationKeys('taskChooseOrganization.alerts.existingOrgWithDomain'), +}; From 928ef242ef89a99546bc1a37f870759bb0abe390 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:23:03 -0300 Subject: [PATCH 04/14] Update entity to include form defaults --- .../resources/OrganizationCreationDefaults.ts | 30 ++++++++++++++----- .../src/types/organizationCreationDefaults.ts | 12 ++++++-- .../CreateOrganizationForm.tsx | 1 + .../CreateOrganizationScreen.tsx | 3 +- .../OrganizationCreationDefaultsAlert.tsx | 6 ++-- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts index 16ac507e142..f872602acb5 100644 --- a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts +++ b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts @@ -1,4 +1,6 @@ import type { + OrganizationCreationAdvisorySeverity, + OrganizationCreationAdvisoryType, OrganizationCreationDefaultsJSON, OrganizationCreationDefaultsJSONSnapshot, OrganizationCreationDefaultsResource, @@ -7,10 +9,17 @@ import type { import { BaseResource } from './internal'; export class OrganizationCreationDefaults extends BaseResource implements OrganizationCreationDefaultsResource { - creationAdvisory: { - type: 'existing_org_with_domain'; - severity: 'warning'; + advisory: { + type: OrganizationCreationAdvisoryType; + severity: OrganizationCreationAdvisorySeverity; } | null = null; + form: { + name: string; + slug: string; + } = { + name: '', + slug: '', + }; public constructor(data: OrganizationCreationDefaultsJSON | OrganizationCreationDefaultsJSONSnapshot | null = null) { super(); @@ -22,8 +31,13 @@ export class OrganizationCreationDefaults extends BaseResource implements Organi return this; } - if (data.creation_advisory) { - this.creationAdvisory = this.withDefault(data.creation_advisory, this.creationAdvisory ?? null); + if (data.advisory) { + this.advisory = this.withDefault(data.advisory, this.advisory ?? null); + } + + if (data.form) { + this.form.name = this.withDefault(data.form.name, this.form.name); + this.form.slug = this.withDefault(data.form.slug, this.form.slug); } return this; @@ -41,10 +55,10 @@ export class OrganizationCreationDefaults extends BaseResource implements Organi public __internal_toSnapshot(): OrganizationCreationDefaultsJSONSnapshot { return { - creation_advisory: this.creationAdvisory + advisory: this.advisory ? { - type: this.creationAdvisory.type, - severity: this.creationAdvisory.severity, + type: this.advisory.type, + severity: this.advisory.severity, } : null, } as unknown as OrganizationCreationDefaultsJSONSnapshot; diff --git a/packages/shared/src/types/organizationCreationDefaults.ts b/packages/shared/src/types/organizationCreationDefaults.ts index e510af241f8..badc42917d0 100644 --- a/packages/shared/src/types/organizationCreationDefaults.ts +++ b/packages/shared/src/types/organizationCreationDefaults.ts @@ -6,15 +6,23 @@ export type OrganizationCreationAdvisoryType = 'existing_org_with_domain'; export type OrganizationCreationAdvisorySeverity = 'warning'; export interface OrganizationCreationDefaultsJSON extends ClerkResourceJSON { - creation_advisory: { + advisory: { type: OrganizationCreationAdvisoryType; severity: OrganizationCreationAdvisorySeverity; } | null; + form: { + name: string; + slug: string; + }; } export interface OrganizationCreationDefaultsResource extends ClerkResource { - creationAdvisory: { + advisory: { type: OrganizationCreationAdvisoryType; severity: OrganizationCreationAdvisorySeverity; } | null; + form: { + name: string; + slug: string; + }; } diff --git a/packages/ui/src/components/CreateOrganization/CreateOrganizationForm.tsx b/packages/ui/src/components/CreateOrganization/CreateOrganizationForm.tsx index 0bc7f29a13d..f206a91f31d 100644 --- a/packages/ui/src/components/CreateOrganization/CreateOrganizationForm.tsx +++ b/packages/ui/src/components/CreateOrganization/CreateOrganizationForm.tsx @@ -36,6 +36,7 @@ type CreateOrganizationFormProps = { }; }; +// TODO -> Prefill form with organization creation defaults export const CreateOrganizationForm = withCardStateProvider((props: CreateOrganizationFormProps) => { const card = useCardState(); const wizard = useWizard({ onNextStep: () => card.setError(undefined) }); diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index 75d7c0baa07..35ba1a75a56 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -4,7 +4,6 @@ import type { CreateOrganizationParams } from '@clerk/shared/types'; import { useEnvironment } from '@/ui/contexts'; import { useSessionTasksContext, useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; import { localizationKeys } from '@/ui/customizables'; -// or from '@/ui/elements' import { useCardState } from '@/ui/elements/contexts'; import { Form } from '@/ui/elements/Form'; import { FormButtonContainer } from '@/ui/elements/FormButtons'; @@ -19,7 +18,7 @@ import { OrganizationCreationDefaultsAlert } from './OrganizationCreationDefault // TODO: Replace with actual API call to OrganizationCreationDefaults.retrieve() const organizationCreationDefaults = { - creationAdvisory: { + advisory: { type: 'existing_org_with_domain' as const, severity: 'warning' as const, }, diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx index 067f8c1b28d..eb88d8ffcb0 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx @@ -1,21 +1,21 @@ import type { OrganizationCreationAdvisoryType, OrganizationCreationDefaultsResource } from '@clerk/shared/types'; import { type LocalizationKey, localizationKeys } from '@/localization'; -import { Alert } from '@/ui/elements/Alert'; // or from '@/ui/elements' +import { Alert } from '@/ui/elements/Alert'; export function OrganizationCreationDefaultsAlert({ organizationCreationDefaults, }: { organizationCreationDefaults: OrganizationCreationDefaultsResource; }) { - if (!organizationCreationDefaults.creationAdvisory) { + if (!organizationCreationDefaults.advisory) { return null; } return ( ); } From 47b4a85ec209a5b9c707b30a8ff791885d2cc9a9 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:41:05 -0300 Subject: [PATCH 05/14] Render logo field --- .../resources/OrganizationCreationDefaults.ts | 3 + .../src/types/organizationCreationDefaults.ts | 2 + .../CreateOrganizationScreen.tsx | 60 ++++++++++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts index f872602acb5..460b63751ed 100644 --- a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts +++ b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts @@ -16,9 +16,11 @@ export class OrganizationCreationDefaults extends BaseResource implements Organi form: { name: string; slug: string; + logo: string | null; } = { name: '', slug: '', + logo: null, }; public constructor(data: OrganizationCreationDefaultsJSON | OrganizationCreationDefaultsJSONSnapshot | null = null) { @@ -38,6 +40,7 @@ export class OrganizationCreationDefaults extends BaseResource implements Organi if (data.form) { this.form.name = this.withDefault(data.form.name, this.form.name); this.form.slug = this.withDefault(data.form.slug, this.form.slug); + this.form.logo = this.withDefault(data.form.logo, this.form.logo); } return this; diff --git a/packages/shared/src/types/organizationCreationDefaults.ts b/packages/shared/src/types/organizationCreationDefaults.ts index badc42917d0..4b3cd280f19 100644 --- a/packages/shared/src/types/organizationCreationDefaults.ts +++ b/packages/shared/src/types/organizationCreationDefaults.ts @@ -13,6 +13,7 @@ export interface OrganizationCreationDefaultsJSON extends ClerkResourceJSON { form: { name: string; slug: string; + logo: string | null; }; } @@ -24,5 +25,6 @@ export interface OrganizationCreationDefaultsResource extends ClerkResource { form: { name: string; slug: string; + logo: string | null; }; } diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index 35ba1a75a56..1c0d19fc826 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -1,27 +1,37 @@ import { useOrganizationList } from '@clerk/shared/react'; import type { CreateOrganizationParams } from '@clerk/shared/types'; +import React from 'react'; import { useEnvironment } from '@/ui/contexts'; import { useSessionTasksContext, useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; -import { localizationKeys } from '@/ui/customizables'; +import { Icon, localizationKeys } from '@/ui/customizables'; import { useCardState } from '@/ui/elements/contexts'; import { Form } from '@/ui/elements/Form'; import { FormButtonContainer } from '@/ui/elements/FormButtons'; import { FormContainer } from '@/ui/elements/FormContainer'; import { Header } from '@/ui/elements/Header'; +import { IconButton } from '@/ui/elements/IconButton'; +import { Upload } from '@/ui/icons'; import { createSlug } from '@/ui/utils/createSlug'; import { handleError } from '@/ui/utils/errorHandler'; import { useFormControl } from '@/ui/utils/useFormControl'; +import { OrganizationProfileAvatarUploader } from '../../../OrganizationProfile/OrganizationProfileAvatarUploader'; import { organizationListParams } from '../../../OrganizationSwitcher/utils'; import { OrganizationCreationDefaultsAlert } from './OrganizationCreationDefaultsAlert'; // TODO: Replace with actual API call to OrganizationCreationDefaults.retrieve() +// TODO - Only replace if .organization_settings.organization_creation_defaults.enabled const organizationCreationDefaults = { advisory: { type: 'existing_org_with_domain' as const, severity: 'warning' as const, }, + form: { + name: '', + slug: '', + logo: null, + }, pathRoot: '', reload: () => Promise.resolve({} as any), }; @@ -38,6 +48,7 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = userMemberships: organizationListParams.userMemberships, }); const { organizationSettings } = useEnvironment(); + const [file, setFile] = React.useState(); const nameField = useFormControl('name', '', { type: 'text', @@ -68,6 +79,10 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = const organization = await createOrganization(createOrgParams); + if (file) { + await organization.setLogo({ file }); + } + await setActive({ organization, navigate: async ({ session }) => { @@ -88,6 +103,11 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = slugField.setValue(val); }; + const onAvatarRemove = () => { + card.setIdle(); + return setFile(null); + }; + const isSubmitButtonDisabled = !nameField.value || !isLoaded; return ( @@ -101,8 +121,44 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = ({ padding: `${t.space.$none} ${t.space.$10} ${t.space.$8}` })}> - + + await setFile(file)} + onAvatarRemove={file ? onAvatarRemove : null} + avatarPreviewPlaceholder={ + ({ + color: t.colors.$colorMutedForeground, + transitionDuration: t.transitionDuration.$controls, + })} + /> + } + sx={t => ({ + width: t.sizes.$16, + height: t.sizes.$16, + borderRadius: t.radii.$md, + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$dashed, + borderColor: t.colors.$borderAlpha200, + backgroundColor: t.colors.$neutralAlpha50, + ':hover': { + backgroundColor: t.colors.$neutralAlpha50, + svg: { + transform: 'scale(1.2)', + }, + }, + })} + /> + } + /> Date: Wed, 17 Dec 2025 14:51:54 -0300 Subject: [PATCH 06/14] Add `organizationCreationDefaults` to environment resource --- .../src/core/resources/OrganizationSettings.ts | 12 ++++++++++++ packages/shared/src/types/organizationSettings.ts | 6 ++++++ .../CreateOrganizationScreen.tsx | 4 ++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/resources/OrganizationSettings.ts b/packages/clerk-js/src/core/resources/OrganizationSettings.ts index 8960b347d62..a9fa5873d49 100644 --- a/packages/clerk-js/src/core/resources/OrganizationSettings.ts +++ b/packages/clerk-js/src/core/resources/OrganizationSettings.ts @@ -23,6 +23,11 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe } = { disabled: false, }; + organizationCreationDefaults: { + enabled: boolean; + } = { + enabled: false, + }; enabled: boolean = false; maxAllowedMemberships: number = 1; forceOrganizationSelection!: boolean; @@ -51,6 +56,13 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe this.slug.disabled = this.withDefault(data.slug.disabled, this.slug.disabled); } + if (data.organization_creation_defaults) { + this.organizationCreationDefaults.enabled = this.withDefault( + data.organization_creation_defaults.enabled, + this.organizationCreationDefaults.enabled, + ); + } + this.enabled = this.withDefault(data.enabled, this.enabled); this.maxAllowedMemberships = this.withDefault(data.max_allowed_memberships, this.maxAllowedMemberships); this.forceOrganizationSelection = this.withDefault( diff --git a/packages/shared/src/types/organizationSettings.ts b/packages/shared/src/types/organizationSettings.ts index ab9e0704e1e..e9a24b8e0f0 100644 --- a/packages/shared/src/types/organizationSettings.ts +++ b/packages/shared/src/types/organizationSettings.ts @@ -20,6 +20,9 @@ export interface OrganizationSettingsJSON extends ClerkResourceJSON { slug: { disabled: boolean; }; + organization_creation_defaults: { + enabled: boolean; + }; } export interface OrganizationSettingsResource extends ClerkResource { @@ -37,5 +40,8 @@ export interface OrganizationSettingsResource extends ClerkResource { slug: { disabled: boolean; }; + organizationCreationDefaults: { + enabled: boolean; + }; __internal_toSnapshot: () => OrganizationSettingsJSONSnapshot; } diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index 1c0d19fc826..c5e7b1a1175 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -1,6 +1,6 @@ import { useOrganizationList } from '@clerk/shared/react'; import type { CreateOrganizationParams } from '@clerk/shared/types'; -import React from 'react'; +import { useState } from 'react'; import { useEnvironment } from '@/ui/contexts'; import { useSessionTasksContext, useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; @@ -48,7 +48,7 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = userMemberships: organizationListParams.userMemberships, }); const { organizationSettings } = useEnvironment(); - const [file, setFile] = React.useState(); + const [file, setFile] = useState(); const nameField = useFormControl('name', '', { type: 'text', From 7c9d1ecba0ac0bd2609da24aa1ff86e1cd2a5eda Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:22:15 -0300 Subject: [PATCH 07/14] Prefill with org defaults --- .../src/core/resources/Environment.ts | 2 ++ packages/clerk-js/src/core/resources/User.ts | 3 ++ packages/shared/src/types/user.ts | 2 ++ .../CreateOrganizationScreen.tsx | 35 +++++++------------ .../OrganizationCreationDefaultsAlert.tsx | 19 ++++++---- .../__tests__/TaskChooseOrganization.test.tsx | 10 ++++-- .../tasks/TaskChooseOrganization/index.tsx | 19 ++++++++-- packages/ui/src/elements/AvatarUploader.tsx | 3 +- 8 files changed, 59 insertions(+), 34 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index 5e961e3f354..53a3404bc6f 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -16,6 +16,8 @@ import { APIKeySettings } from './APIKeySettings'; import { AuthConfig, BaseResource, CommerceSettings, DisplayConfig, ProtectConfig, UserSettings } from './internal'; import { OrganizationSettings } from './OrganizationSettings'; +// TODO -> Update with new flag for default orgs +// Use it to conditionally trigger query export class Environment extends BaseResource implements EnvironmentResource { private static instance: Environment; diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts index ae7ef203c63..079b7ae2f75 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -53,6 +53,7 @@ import { UserOrganizationInvitation, Web3Wallet, } from './internal'; +import { OrganizationCreationDefaults } from './OrganizationCreationDefaults'; export class User extends BaseResource implements UserResource { pathRoot = '/me'; @@ -275,6 +276,8 @@ export class User extends BaseResource implements UserResource { getOrganizationMemberships: GetOrganizationMemberships = retrieveMembership => OrganizationMembership.retrieve(retrieveMembership); + getOrganizationCreationDefaults = () => OrganizationCreationDefaults.retrieve(); + leaveOrganization = async (organizationId: string): Promise => { const json = ( await BaseResource._fetch({ diff --git a/packages/shared/src/types/user.ts b/packages/shared/src/types/user.ts index 93d7a9ef4a4..ac1c40b2fbd 100644 --- a/packages/shared/src/types/user.ts +++ b/packages/shared/src/types/user.ts @@ -7,6 +7,7 @@ import type { ExternalAccountResource } from './externalAccount'; import type { ImageResource } from './image'; import type { UserJSON } from './json'; import type { OAuthScope } from './oauth'; +import type { OrganizationCreationDefaultsResource } from './organizationCreationDefaults'; import type { OrganizationInvitationStatus } from './organizationInvitation'; import type { OrganizationMembershipResource } from './organizationMembership'; import type { OrganizationSuggestionResource, OrganizationSuggestionStatus } from './organizationSuggestion'; @@ -115,6 +116,7 @@ export interface UserResource extends ClerkResource, BillingPayerMethods { getOrganizationSuggestions: ( params?: GetUserOrganizationSuggestionsParams, ) => Promise>; + getOrganizationCreationDefaults: () => Promise; leaveOrganization: (organizationId: string) => Promise; createTOTP: () => Promise; verifyTOTP: (params: VerifyTOTPParams) => Promise; diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index c5e7b1a1175..e0f3ee05eed 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -1,5 +1,5 @@ import { useOrganizationList } from '@clerk/shared/react'; -import type { CreateOrganizationParams } from '@clerk/shared/types'; +import type { CreateOrganizationParams, OrganizationCreationDefaultsResource } from '@clerk/shared/types'; import { useState } from 'react'; import { useEnvironment } from '@/ui/contexts'; @@ -20,24 +20,9 @@ import { OrganizationProfileAvatarUploader } from '../../../OrganizationProfile/ import { organizationListParams } from '../../../OrganizationSwitcher/utils'; import { OrganizationCreationDefaultsAlert } from './OrganizationCreationDefaultsAlert'; -// TODO: Replace with actual API call to OrganizationCreationDefaults.retrieve() -// TODO - Only replace if .organization_settings.organization_creation_defaults.enabled -const organizationCreationDefaults = { - advisory: { - type: 'existing_org_with_domain' as const, - severity: 'warning' as const, - }, - form: { - name: '', - slug: '', - logo: null, - }, - pathRoot: '', - reload: () => Promise.resolve({} as any), -}; - type CreateOrganizationScreenProps = { onCancel?: () => void; + organizationCreationDefaults?: OrganizationCreationDefaultsResource; }; export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) => { @@ -50,12 +35,12 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = const { organizationSettings } = useEnvironment(); const [file, setFile] = useState(); - const nameField = useFormControl('name', '', { + const nameField = useFormControl('name', props.organizationCreationDefaults?.form.name ?? '', { type: 'text', label: localizationKeys('taskChooseOrganization.createOrganization.formFieldLabel__name'), placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__name'), }); - const slugField = useFormControl('slug', '', { + const slugField = useFormControl('slug', props.organizationCreationDefaults?.form.slug ?? '', { type: 'text', label: localizationKeys('taskChooseOrganization.createOrganization.formFieldLabel__slug'), placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__slug'), @@ -81,6 +66,11 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = if (file) { await organization.setLogo({ file }); + } else if (defaultLogoUrl) { + const response = await fetch(defaultLogoUrl); + const blob = await response.blob(); + const logoFile = new File([blob], 'logo', { type: blob.type }); + await organization.setLogo({ file: logoFile }); } await setActive({ @@ -109,6 +99,7 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = }; const isSubmitButtonDisabled = !nameField.value || !isLoaded; + const defaultLogoUrl = file === undefined ? props.organizationCreationDefaults?.form.logo : undefined; return ( <> @@ -122,11 +113,11 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = ({ padding: `${t.space.$none} ${t.space.$10} ${t.space.$8}` })}> - + await setFile(file)} - onAvatarRemove={file ? onAvatarRemove : null} + onAvatarRemove={file || defaultLogoUrl ? onAvatarRemove : null} avatarPreviewPlaceholder={ + + + ); } +// TODO -> Update with latest advisory where meta is returned +// TODO -> Include email domain in message, eg: {{ meta }} const advisoryTypeToLocalizationKey: Record = { existing_org_with_domain: localizationKeys('taskChooseOrganization.alerts.existingOrgWithDomain'), }; diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx index 987235c642b..ec40cf1b8fd 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx @@ -355,8 +355,14 @@ describe('TaskChooseOrganization', () => { }); describe('with organization creation defaults', () => { - it.todo('displays warning when organization already exists for user email domain'); + describe('when enabled on environment', () => { + it.todo('displays warning when organization already exists for user email domain'); - it.todo('prefills create organization form with defaults'); + it.todo('prefills create organization form with defaults'); + }); + + describe('when disabled on environment', () => { + it.todo('does not fetch for creation defaults'); + }); }); }); diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index dc3375e0d0b..eac9b229241 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -1,7 +1,9 @@ import { useClerk, useSession, useUser } from '@clerk/shared/react'; +import type { OrganizationCreationDefaultsResource } from '@clerk/shared/types'; import { useState } from 'react'; -import { useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; +import { useFetch } from '@/hooks'; +import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; import { descriptors, Flex, Flow, localizationKeys, Spinner } from '@/ui/customizables'; import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; @@ -16,6 +18,14 @@ import { CreateOrganizationScreen } from './CreateOrganizationScreen'; const TaskChooseOrganizationInternal = () => { const { user } = useUser(); const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); + const { environment } = useEnvironment(); + const organizationCreationDefaults = useFetch( + environment.organizationSettings.organizationCreationDefaults.enabled + ? user?.getOrganizationCreationDefaults + : undefined, + 'organization-creation-defaults', + { staleTime: Infinity }, + ); const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; const hasExistingResources = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); @@ -47,7 +57,10 @@ const TaskChooseOrganizationInternal = () => { /> ) : ( - + )} @@ -103,6 +116,7 @@ const TaskChooseOrganizationCardFooter = () => { type TaskChooseOrganizationFlowsProps = { initialFlow: 'create' | 'choose'; + organizationCreationDefaults?: OrganizationCreationDefaultsResource; }; const TaskChooseOrganizationFlows = withCardStateProvider((props: TaskChooseOrganizationFlowsProps) => { @@ -112,6 +126,7 @@ const TaskChooseOrganizationFlows = withCardStateProvider((props: TaskChooseOrga return ( setCurrentFlow('choose') : undefined} + organizationCreationDefaults={props.organizationCreationDefaults} /> ); } diff --git a/packages/ui/src/elements/AvatarUploader.tsx b/packages/ui/src/elements/AvatarUploader.tsx index f1ae367f2c2..8a7f99b7a0d 100644 --- a/packages/ui/src/elements/AvatarUploader.tsx +++ b/packages/ui/src/elements/AvatarUploader.tsx @@ -90,9 +90,10 @@ export const AvatarUploader = (props: AvatarUploaderProps) => { await handleFileDrop(f); }; + const hasExistingImage = !!(avatarPreview.props as { imageUrl?: string })?.imageUrl; const previewElement = objectUrl ? React.cloneElement(avatarPreview, { imageUrl: objectUrl }) - : avatarPreviewPlaceholder + : avatarPreviewPlaceholder && !hasExistingImage ? React.cloneElement(avatarPreviewPlaceholder, { onClick: openDialog }) : avatarPreview; From aedae265848becd3d95847c186cc24857f72b7d8 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:03:41 -0300 Subject: [PATCH 08/14] Conditionally fetch for defaults --- packages/clerk-js/src/core/resources/Environment.ts | 2 -- .../CreateOrganization/CreateOrganizationForm.tsx | 1 - .../SessionTasks/tasks/TaskChooseOrganization/index.tsx | 6 ++---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index 53a3404bc6f..5e961e3f354 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -16,8 +16,6 @@ import { APIKeySettings } from './APIKeySettings'; import { AuthConfig, BaseResource, CommerceSettings, DisplayConfig, ProtectConfig, UserSettings } from './internal'; import { OrganizationSettings } from './OrganizationSettings'; -// TODO -> Update with new flag for default orgs -// Use it to conditionally trigger query export class Environment extends BaseResource implements EnvironmentResource { private static instance: Environment; diff --git a/packages/ui/src/components/CreateOrganization/CreateOrganizationForm.tsx b/packages/ui/src/components/CreateOrganization/CreateOrganizationForm.tsx index f206a91f31d..0bc7f29a13d 100644 --- a/packages/ui/src/components/CreateOrganization/CreateOrganizationForm.tsx +++ b/packages/ui/src/components/CreateOrganization/CreateOrganizationForm.tsx @@ -36,7 +36,6 @@ type CreateOrganizationFormProps = { }; }; -// TODO -> Prefill form with organization creation defaults export const CreateOrganizationForm = withCardStateProvider((props: CreateOrganizationFormProps) => { const card = useCardState(); const wizard = useWizard({ onNextStep: () => card.setError(undefined) }); diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index eac9b229241..ef33b51c3b5 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -18,11 +18,9 @@ import { CreateOrganizationScreen } from './CreateOrganizationScreen'; const TaskChooseOrganizationInternal = () => { const { user } = useUser(); const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); - const { environment } = useEnvironment(); + const { organizationSettings } = useEnvironment(); const organizationCreationDefaults = useFetch( - environment.organizationSettings.organizationCreationDefaults.enabled - ? user?.getOrganizationCreationDefaults - : undefined, + organizationSettings.organizationCreationDefaults?.enabled ? user?.getOrganizationCreationDefaults : undefined, 'organization-creation-defaults', { staleTime: Infinity }, ); From 26488513799f6029518b6167aaac5c43e20860f2 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:12:19 -0300 Subject: [PATCH 09/14] Include meta into advisory message --- .../resources/OrganizationCreationDefaults.ts | 6 +++-- packages/localizations/src/en-US.ts | 2 +- packages/shared/src/types/localization.ts | 2 +- .../src/types/organizationCreationDefaults.ts | 6 +++-- .../OrganizationCreationDefaultsAlert.tsx | 26 +++++++++++++------ .../tasks/TaskChooseOrganization/index.tsx | 2 +- 6 files changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts index 460b63751ed..865d9085f3a 100644 --- a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts +++ b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts @@ -10,8 +10,9 @@ import { BaseResource } from './internal'; export class OrganizationCreationDefaults extends BaseResource implements OrganizationCreationDefaultsResource { advisory: { - type: OrganizationCreationAdvisoryType; + code: OrganizationCreationAdvisoryType; severity: OrganizationCreationAdvisorySeverity; + meta: Record; } | null = null; form: { name: string; @@ -60,7 +61,8 @@ export class OrganizationCreationDefaults extends BaseResource implements Organi return { advisory: this.advisory ? { - type: this.advisory.type, + code: this.advisory.code, + meta: this.advisory.meta, severity: this.advisory.severity, } : null, diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 9eb252dafb2..5c2d1c27319 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -886,7 +886,7 @@ export const enUS: LocalizationResource = { }, alerts: { existingOrgWithDomain: - 'An organization already exists for the detected company name and email domain. Join by invitation.', + 'An organization already exists for the detected company name and {{email}}. Join by invitation.', }, }, taskResetPassword: { diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 7b5e19d33df..18d9f439ed0 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1315,7 +1315,7 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue; }; alerts: { - existingOrgWithDomain: LocalizationValue; + existingOrgWithDomain: LocalizationValue<'email'>; }; }; taskResetPassword: { diff --git a/packages/shared/src/types/organizationCreationDefaults.ts b/packages/shared/src/types/organizationCreationDefaults.ts index 4b3cd280f19..5c3fdb2a24d 100644 --- a/packages/shared/src/types/organizationCreationDefaults.ts +++ b/packages/shared/src/types/organizationCreationDefaults.ts @@ -7,8 +7,9 @@ export type OrganizationCreationAdvisorySeverity = 'warning'; export interface OrganizationCreationDefaultsJSON extends ClerkResourceJSON { advisory: { - type: OrganizationCreationAdvisoryType; + code: OrganizationCreationAdvisoryType; severity: OrganizationCreationAdvisorySeverity; + meta: Record; } | null; form: { name: string; @@ -19,8 +20,9 @@ export interface OrganizationCreationDefaultsJSON extends ClerkResourceJSON { export interface OrganizationCreationDefaultsResource extends ClerkResource { advisory: { - type: OrganizationCreationAdvisoryType; + code: OrganizationCreationAdvisoryType; severity: OrganizationCreationAdvisorySeverity; + meta: Record; } | null; form: { name: string; diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx index 9e15365aaf0..7cca5d332ba 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx @@ -1,14 +1,15 @@ -import type { OrganizationCreationAdvisoryType, OrganizationCreationDefaultsResource } from '@clerk/shared/types'; +import type { OrganizationCreationDefaultsResource } from '@clerk/shared/types'; import { Alert, Text } from '@/customizables'; -import { type LocalizationKey, localizationKeys } from '@/localization'; +import { localizationKeys } from '@/localization'; export function OrganizationCreationDefaultsAlert({ organizationCreationDefaults, }: { organizationCreationDefaults?: OrganizationCreationDefaultsResource; }) { - if (!organizationCreationDefaults?.advisory) { + const localizationKey = advisoryToLocalizationKey(organizationCreationDefaults?.advisory); + if (!localizationKey) { return null; } @@ -16,15 +17,24 @@ export function OrganizationCreationDefaultsAlert({ ); } -// TODO -> Update with latest advisory where meta is returned -// TODO -> Include email domain in message, eg: {{ meta }} -const advisoryTypeToLocalizationKey: Record = { - existing_org_with_domain: localizationKeys('taskChooseOrganization.alerts.existingOrgWithDomain'), +const advisoryToLocalizationKey = (advisory?: OrganizationCreationDefaultsResource['advisory']) => { + if (!advisory) { + return null; + } + + switch (advisory.code) { + case 'existing_org_with_domain': + return localizationKeys('taskChooseOrganization.alerts.existingOrgWithDomain', { + email: advisory.meta.email, + }); + default: + return null; + } }; diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index ef33b51c3b5..e2310987b3f 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -114,7 +114,7 @@ const TaskChooseOrganizationCardFooter = () => { type TaskChooseOrganizationFlowsProps = { initialFlow: 'create' | 'choose'; - organizationCreationDefaults?: OrganizationCreationDefaultsResource; + organizationCreationDefaults?: OrganizationCreationDefaultsResource | null; }; const TaskChooseOrganizationFlows = withCardStateProvider((props: TaskChooseOrganizationFlowsProps) => { From fd12f1c1a389287ffe2a98a0783f87ef22b33386 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:25:24 -0300 Subject: [PATCH 10/14] Implement tests --- packages/clerk-js/src/test/fixture-helpers.ts | 4 + .../__tests__/TaskChooseOrganization.test.tsx | 74 ++++++++++++++++++- .../tasks/TaskChooseOrganization/index.tsx | 1 - 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/test/fixture-helpers.ts b/packages/clerk-js/src/test/fixture-helpers.ts index 86547dae2c0..b1564bcd841 100644 --- a/packages/clerk-js/src/test/fixture-helpers.ts +++ b/packages/clerk-js/src/test/fixture-helpers.ts @@ -344,6 +344,9 @@ const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON) const withOrganizationSlug = (enabled = false) => { os.slug.disabled = !enabled; }; + const withOrganizationCreationDefaults = (enabled = false) => { + os.organization_creation_defaults.enabled = enabled; + }; const withOrganizationDomains = (modes?: OrganizationEnrollmentMode[], defaultRole?: string) => { os.domains.enabled = true; @@ -356,6 +359,7 @@ const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON) withOrganizationDomains, withForceOrganizationSelection, withOrganizationSlug, + withOrganizationCreationDefaults, }; }; diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx index ec40cf1b8fd..0af877bdb58 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx @@ -356,13 +356,81 @@ describe('TaskChooseOrganization', () => { describe('with organization creation defaults', () => { describe('when enabled on environment', () => { - it.todo('displays warning when organization already exists for user email domain'); + it('displays warning when organization already exists for user email domain', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withForceOrganizationSelection(); + f.withOrganizationCreationDefaults(true); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + tasks: [{ key: 'choose-organization' }], + }); + }); + + fixtures.clerk.user?.getOrganizationCreationDefaults.mockReturnValueOnce( + Promise.resolve({ + advisory: { + code: 'existing_org_with_domain', + severity: 'warning', + meta: { email: 'test@clerk.com' }, + }, + }), + ); + + const { findByText } = render(, { wrapper }); + + expect( + await findByText(/an organization already exists for the detected company name and test@clerk\.com/i), + ).toBeInTheDocument(); + }); + + it('prefills create organization form with defaults', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withForceOrganizationSelection(); + f.withOrganizationCreationDefaults(true); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + tasks: [{ key: 'choose-organization' }], + }); + }); - it.todo('prefills create organization form with defaults'); + fixtures.clerk.user?.getOrganizationCreationDefaults.mockReturnValueOnce( + Promise.resolve({ + form: { + name: 'Test Org', + slug: 'test-org', + logo: null, + }, + }), + ); + + const { findByText } = render(, { wrapper }); + + expect(await findByText('Test Org')).toBeInTheDocument(); + expect(await findByText('test-org')).toBeInTheDocument(); + }); }); describe('when disabled on environment', () => { - it.todo('does not fetch for creation defaults'); + it('does not fetch for creation defaults', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withForceOrganizationSelection(); + f.withOrganizationCreationDefaults(false); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + tasks: [{ key: 'choose-organization' }], + }); + }); + + render(, { wrapper }); + + expect(fixtures.clerk.user?.getOrganizationCreationDefaults).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index e2310987b3f..7913f736626 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -22,7 +22,6 @@ const TaskChooseOrganizationInternal = () => { const organizationCreationDefaults = useFetch( organizationSettings.organizationCreationDefaults?.enabled ? user?.getOrganizationCreationDefaults : undefined, 'organization-creation-defaults', - { staleTime: Infinity }, ); const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; From 22c5fced3f3cd8a679d06c3cd298d135dbb85c4c Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:47:13 -0300 Subject: [PATCH 11/14] Add changeset --- .changeset/calm-maps-work.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/calm-maps-work.md diff --git a/.changeset/calm-maps-work.md b/.changeset/calm-maps-work.md new file mode 100644 index 00000000000..9583e7cb076 --- /dev/null +++ b/.changeset/calm-maps-work.md @@ -0,0 +1,8 @@ +--- +'@clerk/localizations': minor +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/ui': minor +--- + +Surface organization creation defaults with prefilled form fields and advisory warnings From 3a076b0e97a7620d51dc50f48a7a55e103e2d037 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:14:22 -0300 Subject: [PATCH 12/14] Finish integrating with advisory --- packages/localizations/src/en-US.ts | 2 +- packages/shared/src/types/localization.ts | 2 +- packages/shared/src/types/organizationCreationDefaults.ts | 2 +- .../OrganizationCreationDefaultsAlert.tsx | 6 +++--- .../__tests__/TaskChooseOrganization.test.tsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 5c2d1c27319..6a9f5044bdc 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -885,7 +885,7 @@ export const enUS: LocalizationResource = { actionText: 'Signed in as {{identifier}}', }, alerts: { - existingOrgWithDomain: + organizationAlreadyExists: 'An organization already exists for the detected company name and {{email}}. Join by invitation.', }, }, diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 18d9f439ed0..2cab1eab393 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1315,7 +1315,7 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue; }; alerts: { - existingOrgWithDomain: LocalizationValue<'email'>; + organizationAlreadyExists: LocalizationValue<'email'>; }; }; taskResetPassword: { diff --git a/packages/shared/src/types/organizationCreationDefaults.ts b/packages/shared/src/types/organizationCreationDefaults.ts index 5c3fdb2a24d..d5db52f1782 100644 --- a/packages/shared/src/types/organizationCreationDefaults.ts +++ b/packages/shared/src/types/organizationCreationDefaults.ts @@ -1,7 +1,7 @@ import type { ClerkResourceJSON } from './json'; import type { ClerkResource } from './resource'; -export type OrganizationCreationAdvisoryType = 'existing_org_with_domain'; +export type OrganizationCreationAdvisoryType = 'organization_already_exists'; export type OrganizationCreationAdvisorySeverity = 'warning'; diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx index 7cca5d332ba..ed8192e5639 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/OrganizationCreationDefaultsAlert.tsx @@ -30,9 +30,9 @@ const advisoryToLocalizationKey = (advisory?: OrganizationCreationDefaultsResour } switch (advisory.code) { - case 'existing_org_with_domain': - return localizationKeys('taskChooseOrganization.alerts.existingOrgWithDomain', { - email: advisory.meta.email, + case 'organization_already_exists': + return localizationKeys('taskChooseOrganization.alerts.organizationAlreadyExists', { + email: advisory.meta.organization_domain, }); default: return null; diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx index 0af877bdb58..624f39c8fe5 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx @@ -371,7 +371,7 @@ describe('TaskChooseOrganization', () => { fixtures.clerk.user?.getOrganizationCreationDefaults.mockReturnValueOnce( Promise.resolve({ advisory: { - code: 'existing_org_with_domain', + code: 'organization_already_exists', severity: 'warning', meta: { email: 'test@clerk.com' }, }, From 6589db14629e586814d82baf1562dde0acf7c1e6 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:30:00 -0300 Subject: [PATCH 13/14] Use loading spinner for loading status --- .../resources/OrganizationCreationDefaults.ts | 3 ++ .../src/types/organizationCreationDefaults.ts | 2 + .../OrganizationProfileAvatarUploader.tsx | 9 +++- .../CreateOrganizationScreen.tsx | 1 + packages/ui/src/elements/Avatar.tsx | 41 ++++++++++++++++++- .../ui/src/elements/OrganizationAvatar.tsx | 8 +++- 6 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts index 865d9085f3a..cd4b8afb45a 100644 --- a/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts +++ b/packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts @@ -18,10 +18,12 @@ export class OrganizationCreationDefaults extends BaseResource implements Organi name: string; slug: string; logo: string | null; + blurHash: string | null; } = { name: '', slug: '', logo: null, + blurHash: null, }; public constructor(data: OrganizationCreationDefaultsJSON | OrganizationCreationDefaultsJSONSnapshot | null = null) { @@ -42,6 +44,7 @@ export class OrganizationCreationDefaults extends BaseResource implements Organi this.form.name = this.withDefault(data.form.name, this.form.name); this.form.slug = this.withDefault(data.form.slug, this.form.slug); this.form.logo = this.withDefault(data.form.logo, this.form.logo); + this.form.blurHash = this.withDefault(data.form.blur_hash, this.form.blurHash); } return this; diff --git a/packages/shared/src/types/organizationCreationDefaults.ts b/packages/shared/src/types/organizationCreationDefaults.ts index d5db52f1782..95d3211110a 100644 --- a/packages/shared/src/types/organizationCreationDefaults.ts +++ b/packages/shared/src/types/organizationCreationDefaults.ts @@ -15,6 +15,7 @@ export interface OrganizationCreationDefaultsJSON extends ClerkResourceJSON { name: string; slug: string; logo: string | null; + blur_hash: string | null; }; } @@ -28,5 +29,6 @@ export interface OrganizationCreationDefaultsResource extends ClerkResource { name: string; slug: string; logo: string | null; + blurHash: string | null; }; } diff --git a/packages/ui/src/components/OrganizationProfile/OrganizationProfileAvatarUploader.tsx b/packages/ui/src/components/OrganizationProfile/OrganizationProfileAvatarUploader.tsx index 6c146a20312..fa2c5133528 100644 --- a/packages/ui/src/components/OrganizationProfile/OrganizationProfileAvatarUploader.tsx +++ b/packages/ui/src/components/OrganizationProfile/OrganizationProfileAvatarUploader.tsx @@ -8,9 +8,13 @@ import { Col, descriptors, Text } from '../../customizables'; import { localizationKeys } from '../../localization'; export const OrganizationProfileAvatarUploader = ( - props: Omit & { organization: Partial }, + props: Omit & { + organization: Partial; + /** Shows a loading spinner while the image is loading */ + showLoadingSpinner?: boolean; + }, ) => { - const { organization, ...rest } = props; + const { organization, showLoadingSpinner, ...rest } = props; return ( @@ -28,6 +32,7 @@ export const OrganizationProfileAvatarUploader = ( avatarPreview={ theme.sizes.$16} + showLoadingSpinner={showLoadingSpinner} {...organization} /> } diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index e0f3ee05eed..f2a8106f967 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -118,6 +118,7 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = organization={{ name: nameField.value, imageUrl: defaultLogoUrl ?? undefined }} onAvatarChange={async file => await setFile(file)} onAvatarRemove={file || defaultLogoUrl ? onAvatarRemove : null} + showLoadingSpinner={!!defaultLogoUrl} avatarPreviewPlaceholder={ & { rounded?: boolean; boxElementDescriptor?: ElementDescriptor; imageElementDescriptor?: ElementDescriptor; + /** Shows a loading spinner while the image is loading */ + showLoadingSpinner?: boolean; }; export const Avatar = (props: AvatarProps) => { @@ -28,8 +30,18 @@ export const Avatar = (props: AvatarProps) => { sx, boxElementDescriptor, imageElementDescriptor, + showLoadingSpinner = false, } = props; const [error, setError] = React.useState(false); + const [loaded, setLoaded] = React.useState(false); + + // Reset loaded state when imageUrl changes + React.useEffect(() => { + setLoaded(false); + setError(false); + }, [imageUrl]); + + const isLoading = showLoadingSpinner && imageUrl && !loaded && !error; const ImgOrFallback = initials && (!imageUrl || error) ? ( @@ -40,8 +52,15 @@ export const Avatar = (props: AvatarProps) => { title={title} alt={`${title}'s logo`} src={imageUrl || ''} - sx={{ objectFit: 'cover', width: '100%', height: '100%' }} + sx={{ + objectFit: 'cover', + width: '100%', + height: '100%', + opacity: showLoadingSpinner ? (loaded ? 1 : 0) : 1, + transition: 'opacity 0.2s ease-in-out', + }} onError={() => setError(true)} + onLoad={() => setLoaded(true)} size={imageFetchSize} /> ); @@ -67,6 +86,24 @@ export const Avatar = (props: AvatarProps) => { > {ImgOrFallback} + {isLoading && ( + ({ + position: 'absolute', + inset: 0, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: t.colors.$avatarBackground, + })} + > + + + )} + {/* /** * This Box is the "shimmer" effect for the avatar. * The ":after" selector is responsible for the border shimmer animation. diff --git a/packages/ui/src/elements/OrganizationAvatar.tsx b/packages/ui/src/elements/OrganizationAvatar.tsx index 7f449455955..ad7a078f664 100644 --- a/packages/ui/src/elements/OrganizationAvatar.tsx +++ b/packages/ui/src/elements/OrganizationAvatar.tsx @@ -4,15 +4,19 @@ import type { PropsOfComponent } from '../styledSystem'; import { Avatar } from './Avatar'; type OrganizationAvatarProps = PropsOfComponent & - Partial>; + Partial> & { + /** Shows a loading spinner while the image is loading */ + showLoadingSpinner?: boolean; + }; export const OrganizationAvatar = (props: OrganizationAvatarProps) => { - const { name = '', imageUrl, ...rest } = props; + const { name = '', imageUrl, showLoadingSpinner, ...rest } = props; return ( From 5611bb548557a895cc0d0fa67d83c2f72a008318 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:56:34 -0300 Subject: [PATCH 14/14] Add delay to trigger loading spinner --- packages/ui/src/elements/Avatar.tsx | 37 ++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/elements/Avatar.tsx b/packages/ui/src/elements/Avatar.tsx index 6234c58d018..8a1ef25cba7 100644 --- a/packages/ui/src/elements/Avatar.tsx +++ b/packages/ui/src/elements/Avatar.tsx @@ -19,6 +19,9 @@ type AvatarProps = PropsOfComponent & { showLoadingSpinner?: boolean; }; +const SPINNER_DELAY_MS = 150; +const SPINNER_MIN_DURATION_MS = 400; + export const Avatar = (props: AvatarProps) => { const { size = () => 26, @@ -34,14 +37,42 @@ export const Avatar = (props: AvatarProps) => { } = props; const [error, setError] = React.useState(false); const [loaded, setLoaded] = React.useState(false); + const [spinnerVisible, setSpinnerVisible] = React.useState(false); + const spinnerShownAtRef = React.useRef(null); - // Reset loaded state when imageUrl changes React.useEffect(() => { setLoaded(false); setError(false); + setSpinnerVisible(false); + spinnerShownAtRef.current = null; }, [imageUrl]); - const isLoading = showLoadingSpinner && imageUrl && !loaded && !error; + React.useEffect(() => { + if (!showLoadingSpinner || !imageUrl || loaded || error) { + return; + } + + const timer = setTimeout(() => { + setSpinnerVisible(true); + spinnerShownAtRef.current = Date.now(); + }, SPINNER_DELAY_MS); + + return () => clearTimeout(timer); + }, [showLoadingSpinner, imageUrl, loaded, error]); + + const handleImageLoad = React.useCallback(() => { + if (spinnerShownAtRef.current) { + const elapsed = Date.now() - spinnerShownAtRef.current; + const remaining = SPINNER_MIN_DURATION_MS - elapsed; + if (remaining > 0) { + const timer = setTimeout(() => setLoaded(true), remaining); + return () => clearTimeout(timer); + } + } + setLoaded(true); + }, []); + + const isLoading = showLoadingSpinner && spinnerVisible && imageUrl && !loaded && !error; const ImgOrFallback = initials && (!imageUrl || error) ? ( @@ -60,7 +91,7 @@ export const Avatar = (props: AvatarProps) => { transition: 'opacity 0.2s ease-in-out', }} onError={() => setError(true)} - onLoad={() => setLoaded(true)} + onLoad={handleImageLoad} size={imageFetchSize} /> );