From 6085de6b8d23ef9fba9a29dabe09ad3951a6bacc Mon Sep 17 00:00:00 2001 From: gabrielmeloc22 Date: Fri, 11 Sep 2026 11:04:04 -0300 Subject: [PATCH 01/16] 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 abc75799054..470db047779 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -275,10 +275,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 bdd953bd13c..644fc0cbd6b 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -275,10 +275,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 450becd0a65..28473c91077 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -276,10 +276,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 701c5945ca9..f19a1716394 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -281,10 +281,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 d98555fca89..4553e06ed60 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -282,10 +282,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 afae42d07de..9b101ab0310 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -279,10 +279,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 5b936ef9f7b..2a9ceace914 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -275,10 +275,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 eeba2a2982e..8c258087e8c 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -281,10 +281,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 0ecafa33088..ab622a52c9a 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -275,10 +275,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 d417e56a005..2bffb6864b6 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -275,10 +275,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 9c07fb52146..f1bb01b82ff 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -263,6 +263,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.', @@ -273,10 +283,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.', }, @@ -296,11 +313,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 57879bf85b8..9ef9092db43 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -275,10 +275,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 a9117040297..5b1497de2a7 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -281,10 +281,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 fec6f4d697b..c88ba39e5bd 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -276,10 +276,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 b2ee9573843..e6f2e633ead 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -275,10 +275,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 7b171d34f9f..197d8eb6e16 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -280,10 +280,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 fb752974c2d..da588c9c101 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -281,10 +281,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 7aeb57da5fa..3ff38cfb34b 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -283,10 +283,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 e6858234500..5ad9486ce36 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -275,10 +275,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 98d0c0fae13..94f5eac7015 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -281,10 +281,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 3b734291016..72b5695d0d0 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -282,10 +282,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 a8bff797e71..03f6d882081 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -282,10 +282,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 9c7ceb13b3a..a47cf2b7a9c 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -275,10 +275,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 351c10e08da..64143e402cb 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -281,10 +281,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 31e52f8b457..ad2d2578acf 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -281,10 +281,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 5d8d3db8d89..c4295ec9123 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -282,10 +282,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 4167b373d0e..fbec3fe3040 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -275,10 +275,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 ad5b4cf22d3..67234764b8e 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -279,10 +279,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 b96fd7764c1..0b8c5f17412 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -275,10 +275,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 0f5febb75f6..8180f768af4 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -283,10 +283,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 ad3c06e17df..7832d464801 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -282,10 +282,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 06b4956b051..1b3d5b35c7c 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -275,10 +275,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 8012f9bbd65..43172283c1e 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -275,10 +275,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 22b7b268eb5..878f112f894 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -275,10 +275,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 db238db5c0d..936e28cd25b 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -281,10 +281,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 fafe686c049..bfcda606411 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -283,10 +283,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 6c40143d2ae..084bfd42611 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -281,10 +281,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 aef3e518f58..516636e2a4c 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -275,10 +275,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 082a1613e1f..9f4652d5dde 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -275,10 +275,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 f7d1166ad90..7f33e13154a 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -275,10 +275,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 ac5f30f0c58..53414b6dc96 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -275,10 +275,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 7e33a3f6738..68f5c8beca2 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -283,10 +283,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 296bfeaf459..5327c1355aa 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -282,10 +282,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 8f0b0007edf..f8894539813 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -279,10 +279,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 e5973ed6aba..a996d90fbd5 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -275,10 +275,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 0b5276288d3..147945792cd 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -275,10 +275,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 91843807c98..4df7b7e1584 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -281,10 +281,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 16f338d03ef..35837c6212b 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -275,10 +275,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 8f6091c6a0a..64a4a17d9e2 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -278,10 +278,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 37c552fdb49..6dd53564c2f 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1603,11 +1603,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; @@ -1629,6 +1636,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; @@ -1649,9 +1663,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 e52e8468a1e..04a17a2cda2 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 { sortEnterpriseConnections } from '../ConfigureSSO/domain/organizationEnterpriseConnection'; @@ -30,6 +35,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; } @@ -61,6 +70,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 @@ -104,6 +115,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 })} + > +