From 9ce18886e960cbb2e526c9bba7e27693ec5a4b0e Mon Sep 17 00:00:00 2001 From: gabrielmeloc22 Date: Fri, 11 Sep 2026 11:04:04 -0300 Subject: [PATCH 1/4] feat(ui): set up Google Workspace directories in the sync wizard An organization admin who finishes Google SAML SSO and moves on to Directory Sync is told to configure it in the Clerk Dashboard. That is the Clerk customer's account, not theirs, so the flow dead-ends for every Google connection. Google directories are read by Clerk rather than pushed to, so the setup collects a credential instead of handing out an endpoint and token. The configure step now takes a service account key and the admin to read the directory as, and the test step gains a sync trigger with the outcome of the last run, since a pull can otherwise sit minutes away with nothing to show and no way to tell a slow sync from a broken one. Providers carry a push or pull mode now rather than a supportsScim flag, which is what the steps branch on. Reads the uploaded key with FileReader rather than Blob.text(), which Safari only gained in 14. Part of ORGS-1843 --- .changeset/dir-sync-google-wizard.md | 7 + packages/localizations/src/ar-SA.ts | 4 - packages/localizations/src/be-BY.ts | 4 - packages/localizations/src/bg-BG.ts | 4 - packages/localizations/src/bn-IN.ts | 4 - packages/localizations/src/ca-ES.ts | 4 - packages/localizations/src/cs-CZ.ts | 4 - packages/localizations/src/da-DK.ts | 4 - packages/localizations/src/de-DE.ts | 4 - packages/localizations/src/el-GR.ts | 4 - packages/localizations/src/en-GB.ts | 4 - packages/localizations/src/en-US.ts | 38 +++- packages/localizations/src/es-CR.ts | 4 - packages/localizations/src/es-ES.ts | 4 - packages/localizations/src/es-MX.ts | 4 - packages/localizations/src/es-UY.ts | 4 - packages/localizations/src/fa-IR.ts | 4 - packages/localizations/src/fi-FI.ts | 4 - packages/localizations/src/fr-FR.ts | 4 - packages/localizations/src/he-IL.ts | 4 - packages/localizations/src/hi-IN.ts | 4 - packages/localizations/src/hr-HR.ts | 4 - packages/localizations/src/hu-HU.ts | 4 - packages/localizations/src/id-ID.ts | 4 - packages/localizations/src/is-IS.ts | 4 - packages/localizations/src/it-IT.ts | 4 - packages/localizations/src/ja-JP.ts | 4 - packages/localizations/src/kk-KZ.ts | 4 - packages/localizations/src/ko-KR.ts | 4 - packages/localizations/src/mn-MN.ts | 4 - packages/localizations/src/ms-MY.ts | 4 - packages/localizations/src/nb-NO.ts | 4 - packages/localizations/src/nl-BE.ts | 4 - packages/localizations/src/nl-NL.ts | 4 - packages/localizations/src/pl-PL.ts | 4 - packages/localizations/src/pt-BR.ts | 4 - packages/localizations/src/pt-PT.ts | 4 - packages/localizations/src/ro-RO.ts | 4 - packages/localizations/src/ru-RU.ts | 4 - packages/localizations/src/sk-SK.ts | 4 - packages/localizations/src/sr-RS.ts | 4 - packages/localizations/src/sv-SE.ts | 4 - packages/localizations/src/ta-IN.ts | 4 - packages/localizations/src/te-IN.ts | 4 - packages/localizations/src/th-TH.ts | 4 - packages/localizations/src/tr-TR.ts | 4 - packages/localizations/src/uk-UA.ts | 4 - packages/localizations/src/vi-VN.ts | 4 - packages/localizations/src/zh-CN.ts | 4 - packages/localizations/src/zh-TW.ts | 4 - packages/shared/src/types/localization.ts | 34 ++- .../ConfigureDirectorySyncContext.tsx | 15 +- .../GoogleCredentialsForm.tsx | 207 ++++++++++++++++++ .../SecurityDirectorySyncSection.tsx | 48 ++-- .../ConfigureDirectorySync/SyncNowRow.tsx | 131 +++++++++++ .../ConfigureDirectorySyncWizard.test.tsx | 67 +++++- .../ConfigureDirectorySync/providerMeta.ts | 32 ++- .../steps/ConfigureStep.tsx | 173 +++++++-------- .../steps/TestSyncStep.tsx | 32 ++- .../OrganizationSecurityPage.test.tsx | 11 +- .../src/customizables/elementDescriptors.ts | 10 + packages/ui/src/internal/appearance.ts | 10 + 62 files changed, 671 insertions(+), 336 deletions(-) create mode 100644 .changeset/dir-sync-google-wizard.md create mode 100644 packages/ui/src/components/ConfigureDirectorySync/GoogleCredentialsForm.tsx create mode 100644 packages/ui/src/components/ConfigureDirectorySync/SyncNowRow.tsx diff --git a/.changeset/dir-sync-google-wizard.md b/.changeset/dir-sync-google-wizard.md new file mode 100644 index 00000000000..726d21a2925 --- /dev/null +++ b/.changeset/dir-sync-google-wizard.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': minor +'@clerk/shared': minor +'@clerk/ui': minor +--- + +Support Google Workspace in the Directory Sync setup flow. Those connections previously showed a notice directing the organization admin to the Clerk Dashboard; they can now upload a service account key and delegated admin email, start a sync, and see the result of the last one. diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index c89ca2c72e9..ef7e3665b48 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -270,10 +270,6 @@ export const arSA: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index 80651105c3e..80d35b6ef62 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -270,10 +270,6 @@ export const beBY: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index 994f42c5fff..036128eba8b 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -271,10 +271,6 @@ export const bgBG: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts index 736ee99cc21..7a7fce549f1 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -276,10 +276,6 @@ export const bnIN: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index 26b4a53fe4d..2f6992148e9 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -277,10 +277,6 @@ export const caES: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index da97038eca8..ec8ba1d9c72 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -274,10 +274,6 @@ export const csCZ: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index 4a1d7d02af7..cf98be068cf 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -270,10 +270,6 @@ export const daDK: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index fc30b05a534..de73cba051b 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -276,10 +276,6 @@ export const deDE: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index 1091e4d608d..b75491a337e 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -270,10 +270,6 @@ export const elGR: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index 6d3bf95a9ca..c1a289ab825 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -270,10 +270,6 @@ export const enGB: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index fb069af7045..c6de4e5f151 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -258,6 +258,16 @@ export const enUS: LocalizationResource = { step3: 'Select Provisioning and set the provisioning mode to Automatic.', step4: 'Assign the users and groups to provision, then turn provisioning On.', }, + google: { + step1: + 'In the Google Cloud console, create a project and a service account, then create a JSON key for it and download the file.', + step2: 'Enable the Admin SDK API for that project.', + step3: + 'In the Google Admin console, open Security, then API controls, then Domain-wide delegation, and add the service account using its client ID.', + step4: + 'Grant it the read-only directory scopes: https://www.googleapis.com/auth/admin.directory.user.readonly and https://www.googleapis.com/auth/admin.directory.group.readonly.', + step5: 'Upload the JSON key below and enter the email of a Google Workspace admin to read the directory as.', + }, okta: { step1: 'In the Okta Admin Console, open the application used for your SSO connection.', step2: 'Open the Provisioning tab and select the Integration setting.', @@ -268,10 +278,17 @@ export const enUS: LocalizationResource = { notice__tokenShownOnce: 'This token is only shown once. Generate a new token if you lose it.', subtitle: 'Add these credentials to your identity provider to configure Directory Sync', title: 'Configure', - warning__googleUnsupported: { - subtitle: 'Please contact support to set up your Google Workspace integration.', - title: 'Google Workspace connections are not configurable via self-serve', - }, + actionLabel__replaceKey: 'Replace JSON key', + actionLabel__saveCredentials: 'Save and enable', + actionLabel__updateCredentials: 'Update credentials', + actionLabel__uploadKey: 'Upload JSON key', + badge__credentialsConfigured: 'Configured', + badge__credentialsMissing: 'Not configured', + error__invalidKeyFile: 'That file is not valid JSON. Upload the key file downloaded from Google.', + formFieldHint__subjectEmail: 'The admin whose directory Clerk reads. They must be an active Workspace admin.', + formFieldInputPlaceholder__subjectEmail: 'admin@yourcompany.com', + formFieldLabel__serviceAccountKey: 'Service account key', + formFieldLabel__subjectEmail: 'Google Workspace admin email', warning__ssoInactive: 'Your SSO connection is configured but not active. Members can be provisioned now, but they can only sign in once SSO is activated.', }, @@ -291,11 +308,24 @@ export const enUS: LocalizationResource = { }, testStep: { actionLabel__complete: 'Complete', + actionLabel__syncNow: 'Sync now', badge__active: 'Active', badge__deprovisioned: 'Deprovisioned', description: 'Users appear here as your identity provider provisions them, most recent activity first.', + description__pull: + 'Clerk reads your directory on a schedule. Start a sync to pull users now, most recent activity first.', + empty__waitingForFirstSync: 'Waiting for the first sync to finish…', empty__waitingForFirstUser: 'Waiting for the first provisioned user…', + error__lastSyncFailed: 'The last sync failed', error__loadUsers: 'Could not load provisioned users', + syncRow: { + neverSynced: 'Not synced yet', + title: 'Directory sync', + }, + syncStatus__cancelled: 'Cancelled', + syncStatus__failed: 'Failed', + syncStatus__running: 'Running', + syncStatus__succeeded: 'Succeeded', note: 'only users with an email address from a configured domain will be processed.', noteLabel: 'Note:', subtitle: 'Assign or push a test user from {{provider}} to verify provisioning.', diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index 3bd948fe871..b11bc78dd6a 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -270,10 +270,6 @@ export const esCR: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 652fd36abb0..7b6150d9458 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -276,10 +276,6 @@ export const esES: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index e7647c73176..0418ee141b5 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -271,10 +271,6 @@ export const esMX: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts index 2ce62d54638..982145e7c31 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -270,10 +270,6 @@ export const esUY: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts index 108f800cd33..8192e36343e 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -275,10 +275,6 @@ export const faIR: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index 2b16351dc6c..8088feb91bb 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -276,10 +276,6 @@ export const fiFI: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index ac4bee57386..1ba53e789e2 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -278,10 +278,6 @@ export const frFR: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index b9693f516c3..d37abf9fbe6 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -270,10 +270,6 @@ export const heIL: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts index 385357d0570..a15648dca01 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -276,10 +276,6 @@ export const hiIN: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index 57d7658a844..e5e7d7b028c 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -277,10 +277,6 @@ export const hrHR: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index 2f905cf81de..17d98d113cc 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -277,10 +277,6 @@ export const huHU: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index 56d5c3d5f8b..2f8a274b9b0 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -270,10 +270,6 @@ export const idID: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index 1f384447cd7..0d497068555 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -276,10 +276,6 @@ export const isIS: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index 419d8d5ca66..dfa687c4ed0 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -276,10 +276,6 @@ export const itIT: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index 7d5eff82c76..aa2b36ee933 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -277,10 +277,6 @@ export const jaJP: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index f80060cc2f0..562b7749de5 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -270,10 +270,6 @@ export const kkKZ: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 371b4c80667..109877596b9 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -274,10 +274,6 @@ export const koKR: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index 7374da7ae2e..5c9ba235215 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -270,10 +270,6 @@ export const mnMN: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts index d085d4206f9..4741ab66424 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -278,10 +278,6 @@ export const msMY: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index 6e1b71b597d..f1be397fb78 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -277,10 +277,6 @@ export const nbNO: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index 3544894670a..8a0061db0fd 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -270,10 +270,6 @@ export const nlBE: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index 5986a847c6e..a5e4137eaf4 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -270,10 +270,6 @@ export const nlNL: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 1b93f6c27f9..117193f2681 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -270,10 +270,6 @@ export const plPL: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index bb33f1549be..1c1c4c88268 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -276,10 +276,6 @@ export const ptBR: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index d672f9f1e8a..1ff271c205f 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -278,10 +278,6 @@ export const ptPT: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index 063cdbf32ef..15beee70f8e 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -276,10 +276,6 @@ export const roRO: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index f07077d04fa..54a94064622 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -270,10 +270,6 @@ export const ruRU: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index cb1e09e225d..6318c03b2b3 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -270,10 +270,6 @@ export const skSK: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index 80893e65d17..f4a85057524 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -270,10 +270,6 @@ export const srRS: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 42ee199f7ad..e7297962e70 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -270,10 +270,6 @@ export const svSE: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts index e1d05c1246c..23a4b981b05 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -278,10 +278,6 @@ export const taIN: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts index 1bdadb0f673..a94c13e80c3 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -277,10 +277,6 @@ export const teIN: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 78af5c3e90b..0f43764a795 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -274,10 +274,6 @@ export const thTH: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index 1ae4e5d7765..8151b0f0679 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -270,10 +270,6 @@ export const trTR: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index f27a90a6bd7..a75fd89bcd3 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -270,10 +270,6 @@ export const ukUA: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index db5113e099e..1e149604435 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -276,10 +276,6 @@ export const viVN: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index ad86a3b8353..4a0c2c54a02 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -270,10 +270,6 @@ export const zhCN: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index efb97374455..bd77ab22fa2 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -273,10 +273,6 @@ export const zhTW: LocalizationResource = { notice__tokenShownOnce: undefined, subtitle: undefined, title: undefined, - warning__googleUnsupported: { - subtitle: undefined, - title: undefined, - }, warning__ssoInactive: undefined, }, navbar: { diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 2689cc55d05..6b5b60b0c61 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1491,11 +1491,18 @@ export type __internal_LocalizationResource = { title: LocalizationValue; subtitle: LocalizationValue; }; - warning__googleUnsupported: { - title: LocalizationValue; - subtitle: LocalizationValue; - }; warning__ssoInactive: LocalizationValue; + formFieldLabel__serviceAccountKey: LocalizationValue; + formFieldLabel__subjectEmail: LocalizationValue; + formFieldInputPlaceholder__subjectEmail: LocalizationValue; + formFieldHint__subjectEmail: LocalizationValue; + actionLabel__uploadKey: LocalizationValue; + actionLabel__replaceKey: LocalizationValue; + actionLabel__saveCredentials: LocalizationValue; + actionLabel__updateCredentials: LocalizationValue; + badge__credentialsConfigured: LocalizationValue; + badge__credentialsMissing: LocalizationValue; + error__invalidKeyFile: LocalizationValue; domainsLabel: LocalizationValue; instructions: { actionLabel__toggle: LocalizationValue; @@ -1517,6 +1524,13 @@ export type __internal_LocalizationResource = { step3: LocalizationValue; step4: LocalizationValue; }; + google: { + step1: LocalizationValue; + step2: LocalizationValue; + step3: LocalizationValue; + step4: LocalizationValue; + step5: LocalizationValue; + }; }; formFieldLabel__endpointUrl: LocalizationValue; formFieldLabel__token: LocalizationValue; @@ -1537,9 +1551,21 @@ export type __internal_LocalizationResource = { title: LocalizationValue; subtitle: LocalizationValue<'provider'>; description: LocalizationValue; + description__pull: LocalizationValue; noteLabel: LocalizationValue; note: LocalizationValue; empty__waitingForFirstUser: LocalizationValue; + empty__waitingForFirstSync: LocalizationValue; + actionLabel__syncNow: LocalizationValue; + error__lastSyncFailed: LocalizationValue; + syncStatus__running: LocalizationValue; + syncStatus__succeeded: LocalizationValue; + syncStatus__failed: LocalizationValue; + syncStatus__cancelled: LocalizationValue; + syncRow: { + title: LocalizationValue; + neverSynced: LocalizationValue; + }; badge__active: LocalizationValue; badge__deprovisioned: LocalizationValue; error__loadUsers: LocalizationValue; diff --git a/packages/ui/src/components/ConfigureDirectorySync/ConfigureDirectorySyncContext.tsx b/packages/ui/src/components/ConfigureDirectorySync/ConfigureDirectorySyncContext.tsx index a7c976d0d7d..c2b7338f345 100644 --- a/packages/ui/src/components/ConfigureDirectorySync/ConfigureDirectorySyncContext.tsx +++ b/packages/ui/src/components/ConfigureDirectorySync/ConfigureDirectorySyncContext.tsx @@ -2,7 +2,12 @@ import { __internal_useOrganizationDirectorySync, __internal_useOrganizationEnterpriseConnections, } from '@clerk/shared/react'; -import type { DirectorySyncProvider, DirectorySyncResource, EnterpriseConnectionResource } from '@clerk/shared/types'; +import type { + DirectorySyncProvider, + DirectorySyncResource, + EnterpriseConnectionResource, + SetDirectorySyncCredentialsParams, +} from '@clerk/shared/types'; import React, { type PropsWithChildren } from 'react'; import type { DirectorySyncProviderMeta } from './providerMeta'; @@ -29,6 +34,10 @@ export interface ConfigureDirectorySyncData { createDirectory: () => Promise; rotateToken: () => Promise; setDirectoryEnabled: (enabled: boolean) => Promise; + /** Stores the credential a pull directory reads the identity provider with. */ + setCredentials: (params: SetDirectorySyncCredentialsParams) => Promise; + /** Starts a sync for a pull directory. */ + syncDirectory: () => Promise; onExit?: () => void; } @@ -60,6 +69,8 @@ export const ConfigureDirectorySyncProvider = ({ createDirectorySync, updateDirectorySync, rotateDirectorySyncToken, + setDirectorySyncCredentials, + syncDirectory, } = __internal_useOrganizationDirectorySync({ enterpriseConnectionId }); // The token is stored with the connection it was issued for, so a response @@ -103,6 +114,8 @@ export const ConfigureDirectorySyncProvider = ({ createDirectory, rotateToken, setDirectoryEnabled, + setCredentials: setDirectorySyncCredentials, + syncDirectory, onExit, }; diff --git a/packages/ui/src/components/ConfigureDirectorySync/GoogleCredentialsForm.tsx b/packages/ui/src/components/ConfigureDirectorySync/GoogleCredentialsForm.tsx new file mode 100644 index 00000000000..f5825209440 --- /dev/null +++ b/packages/ui/src/components/ConfigureDirectorySync/GoogleCredentialsForm.tsx @@ -0,0 +1,207 @@ +import { useRef, useState } from 'react'; + +import { + Badge, + Button, + Col, + descriptors, + Flex, + Input, + localizationKeys, + Text, + useLocalizations, +} from '@/customizables'; +import { useCardState } from '@/elements/contexts'; +import { Alert } from '@/ui/elements/Alert'; +import { handleError } from '@/utils/errorHandler'; + +import { useConfigureDirectorySync } from './ConfigureDirectorySyncContext'; + +// FileReader rather than Blob.text(), which Safari only gained in 14 and which +// clerk-js cannot assume. Mirrors fileToBase64 in AvatarUploader. +const fileToText = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsText(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); + +/** + * Collects the credential a pull-based directory reads the identity provider + * with: the service account key and the administrator it impersonates. + * + * The key never leaves this component except as the request body — it is not + * held in wizard state, because wizard state outlives the request. + */ +export const GoogleCredentialsForm = (): JSX.Element => { + const { directory, setCredentials } = useConfigureDirectorySync(); + const { t } = useLocalizations(); + const card = useCardState(); + const fileInputRef = useRef(null); + + const [serviceAccountJson, setServiceAccountJson] = useState(''); + const [fileName, setFileName] = useState(null); + const [subjectEmail, setSubjectEmail] = useState(''); + const [fileError, setFileError] = useState(null); + + const isConfigured = Boolean(directory?.credentialsConfigured); + const canSubmit = Boolean(serviceAccountJson) && Boolean(subjectEmail) && !card.isLoading; + + const onFileSelected = async (file: File | undefined): Promise => { + if (!file) { + return; + } + const contents = await fileToText(file); + try { + JSON.parse(contents); + } catch { + // Catch the obvious wrong-file case here; anything structurally valid is + // the identity provider's to judge, and its message is better than ours. + setServiceAccountJson(''); + setFileName(null); + setFileError(t(localizationKeys('configureDirectorySync.configureStep.error__invalidKeyFile'))); + return; + } + setFileError(null); + setServiceAccountJson(contents); + setFileName(file.name); + }; + + const onSubmit = async (): Promise => { + if (!canSubmit) { + return; + } + card.setError(undefined); + card.setLoading(); + try { + await setCredentials({ serviceAccountJson, subjectEmail }); + // Drop the key as soon as it has been accepted. Nothing in this flow + // needs it again, and holding it only widens where it can leak from. + setServiceAccountJson(''); + setFileName(null); + } catch (err) { + handleError(err as Error, [], card.setError); + } finally { + card.setIdle(); + } + }; + + return ( + ({ gap: t.space.$5 })} + > + ({ gap: t.space.$2 })} + > + ({ fontSize: t.fontSizes.$sm, fontWeight: t.fontWeights.$medium })} + /> + + + + ({ gap: t.space.$1x5 })}> + void onFileSelected(event.target.files?.[0])} + /> + ({ gap: t.space.$2 })} + > +