From eced5396696fca376e3a22c638fa2434e96d4080 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:56:51 -0300 Subject: [PATCH 01/21] Add `EnterpriseAccount` resource to backend layer --- packages/backend/src/api/resources/JSON.ts | 30 ++++++++++++++++++++++ packages/backend/src/index.ts | 2 ++ 2 files changed, 32 insertions(+) diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index a333d17e028..aa38857a20a 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -12,6 +12,7 @@ export const ObjectType = { Email: 'email', EmailAddress: 'email_address', ExternalAccount: 'external_account', + EnterpriseAccount: 'enterprise_account', FacebookAccount: 'facebook_account', GoogleAccount: 'google_account', Invitation: 'invitation', @@ -105,6 +106,35 @@ export interface ExternalAccountJSON extends ClerkResourceJSON { verification: VerificationJSON | null; } +export interface EnterpriseAccountJSON extends ClerkResourceJSON { + object: typeof ObjectType.EnterpriseAccount; + provider: string; + protocol: string; + active: boolean; + email_address: string; + first_name: string | null; + last_name: string | null; + provider_user_id: string | null; + public_metadata?: Record | null; + verification: VerificationJSON | null; + enterprise_connection: EnterpriseAccountConnectionJSON | null; +} + +export interface EnterpriseAccountConnectionJSON extends ClerkResourceJSON { + provider: string; + protocol: string; + name: string; + domain: string; + active: boolean; + logo_public_url: string | null; + sync_user_attributes: boolean; + allow_subdomains: boolean; + allow_idp_initiated: boolean; + disable_additional_identifications: boolean; + created_at: number; + updated_at: number; +} + export interface SamlAccountJSON extends ClerkResourceJSON { object: typeof ObjectType.SamlAccount; provider: string; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index e74b5ba1037..24d285000af 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -60,6 +60,8 @@ export type { EmailJSON, EmailAddressJSON, ExternalAccountJSON, + EnterpriseAccountJSON, + EnterpriseAccountConnectionJSON, IdentificationLinkJSON, InvitationJSON, OauthAccessTokenJSON, From a3ca6c917d7d51c8a0de4ca12dc420f55caf4dd3 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:27:34 -0300 Subject: [PATCH 02/21] Add `EnterpriseAccount` to types layer --- packages/types/src/enterpriseAccount.ts | 36 +++++++++++++++++++++++++ packages/types/src/index.ts | 1 + 2 files changed, 37 insertions(+) create mode 100644 packages/types/src/enterpriseAccount.ts diff --git a/packages/types/src/enterpriseAccount.ts b/packages/types/src/enterpriseAccount.ts new file mode 100644 index 00000000000..e971e2c9bb4 --- /dev/null +++ b/packages/types/src/enterpriseAccount.ts @@ -0,0 +1,36 @@ +import type { GoogleOauthProvider, MicrosoftOauthProvider } from 'oauth'; +import type { SamlIdpSlug } from 'saml'; +import type { CustomOAuthStrategy } from 'strategies'; +import type { VerificationResource } from 'verification'; + +import type { ClerkResource } from './resource'; + +type EnterpriseProtocol = 'saml' | 'oauth'; + +type EnterpriseProvider = SamlIdpSlug | GoogleOauthProvider | MicrosoftOauthProvider | CustomOAuthStrategy; + +export interface EnterpriseAccountResource extends ClerkResource { + active: boolean; + emailAddress: string; + firstName: string; + lastName: string; + providerUserId: string | null; + publicMetadata: Record; + verification: VerificationResource | null; + enterpriseConnection: EnterpriseAccountConnectionResource | null; + protocol: EnterpriseProtocol; + provider: EnterpriseProvider; +} + +export interface EnterpriseAccountConnectionResource extends ClerkResource { + name: string; + logoPublicUrl: string; + domain: string; + active: boolean; + syncUserAttributes: boolean; + disableAdditionalIdentifications: boolean; + allowSubdomains: boolean; + allowIdpInitiated: boolean; + protocol: EnterpriseProtocol; + provider: EnterpriseProvider; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 95f8505e8a2..108545cecce 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -11,6 +11,7 @@ export * from './displayConfig'; export * from './emailAddress'; export * from './environment'; export * from './externalAccount'; +export * from './enterpriseAccount'; export * from './factors'; export * from './identificationLink'; export * from './identifiers'; From 107dc77a44c0490a2f645c5aba94c0f277d9b9cb Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:27:58 -0300 Subject: [PATCH 03/21] Add `EnterpriseAccountResource` to clerk-js --- packages/backend/src/api/resources/JSON.ts | 30 ------ packages/backend/src/index.ts | 2 - .../src/core/resources/EnterpriseAccount.ts | 98 +++++++++++++++++++ packages/types/src/enterpriseAccount.ts | 14 +-- packages/types/src/json.ts | 31 ++++++ 5 files changed, 136 insertions(+), 39 deletions(-) create mode 100644 packages/clerk-js/src/core/resources/EnterpriseAccount.ts diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index aa38857a20a..a333d17e028 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -12,7 +12,6 @@ export const ObjectType = { Email: 'email', EmailAddress: 'email_address', ExternalAccount: 'external_account', - EnterpriseAccount: 'enterprise_account', FacebookAccount: 'facebook_account', GoogleAccount: 'google_account', Invitation: 'invitation', @@ -106,35 +105,6 @@ export interface ExternalAccountJSON extends ClerkResourceJSON { verification: VerificationJSON | null; } -export interface EnterpriseAccountJSON extends ClerkResourceJSON { - object: typeof ObjectType.EnterpriseAccount; - provider: string; - protocol: string; - active: boolean; - email_address: string; - first_name: string | null; - last_name: string | null; - provider_user_id: string | null; - public_metadata?: Record | null; - verification: VerificationJSON | null; - enterprise_connection: EnterpriseAccountConnectionJSON | null; -} - -export interface EnterpriseAccountConnectionJSON extends ClerkResourceJSON { - provider: string; - protocol: string; - name: string; - domain: string; - active: boolean; - logo_public_url: string | null; - sync_user_attributes: boolean; - allow_subdomains: boolean; - allow_idp_initiated: boolean; - disable_additional_identifications: boolean; - created_at: number; - updated_at: number; -} - export interface SamlAccountJSON extends ClerkResourceJSON { object: typeof ObjectType.SamlAccount; provider: string; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 24d285000af..e74b5ba1037 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -60,8 +60,6 @@ export type { EmailJSON, EmailAddressJSON, ExternalAccountJSON, - EnterpriseAccountJSON, - EnterpriseAccountConnectionJSON, IdentificationLinkJSON, InvitationJSON, OauthAccessTokenJSON, diff --git a/packages/clerk-js/src/core/resources/EnterpriseAccount.ts b/packages/clerk-js/src/core/resources/EnterpriseAccount.ts new file mode 100644 index 00000000000..242564c5058 --- /dev/null +++ b/packages/clerk-js/src/core/resources/EnterpriseAccount.ts @@ -0,0 +1,98 @@ +import type { + EnterpriseAccountConnectionJSON, + EnterpriseAccountConnectionResource, + EnterpriseAccountJSON, + EnterpriseAccountResource, + VerificationResource, +} from '@clerk/types'; + +import { unixEpochToDate } from '../../utils/date'; +import { BaseResource } from './Base'; +import { Verification } from './Verification'; + +export class EnterpriseAccount extends BaseResource implements EnterpriseAccountResource { + id!: string; + protocol: EnterpriseAccountResource['protocol']; + provider: EnterpriseAccountResource['provider']; + providerUserId: string | null = null; + active!: boolean; + emailAddress!: string; + firstName = ''; + lastName = ''; + publicMetadata = {}; + verification: VerificationResource | null = null; + enterpriseConnection: EnterpriseAccountConnectionResource | null = null; + + public constructor(data: Partial, pathRoot: string); + public constructor(data: EnterpriseAccountJSON, pathRoot: string) { + super(); + this.pathRoot = pathRoot; + this.fromJSON(data); + } + + protected fromJSON(data: EnterpriseAccountJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.provider = data.provider; + this.protocol = data.protocol; + this.providerUserId = data.provider_user_id; + this.active = data.active; + this.emailAddress = data.email_address; + this.firstName = data.first_name; + this.lastName = data.last_name; + this.publicMetadata = data.public_metadata; + + if (data.verification) { + this.verification = new Verification(data.verification); + } + + if (data.enterprise_connection) { + this.enterpriseConnection = new EnterpriseAccountConnection(data.enterprise_connection); + } + + return this; + } +} + +export class EnterpriseAccountConnection extends BaseResource implements EnterpriseAccountConnectionResource { + id!: string; + name!: string; + domain!: string; + active!: boolean; + logoPublicUrl: string; + protocol: EnterpriseAccountResource['protocol']; + provider: EnterpriseAccountResource['provider']; + syncUserAttributes!: boolean; + allowSubdomains!: boolean; + allowIdpInitiated!: boolean; + disableAdditionalIdentifications!: boolean; + createdAt!: Date; + updatedAt!: Date; + + constructor(data: EnterpriseAccountConnectionJSON | null) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: EnterpriseAccountConnectionJSON | null): this { + if (data) { + this.id = data.id; + this.name = data.name; + this.domain = data.domain; + this.active = data.active; + this.provider = data.provider; + this.logoPublicUrl = data.logo_public_url; + this.syncUserAttributes = data.sync_user_attributes; + this.allowSubdomains = data.allow_subdomains; + this.allowIdpInitiated = data.allow_idp_initiated; + this.disableAdditionalIdentifications = data.disable_additional_identifications; + this.createdAt = unixEpochToDate(data.created_at); + this.updatedAt = unixEpochToDate(data.updated_at); + } + + return this; + } +} diff --git a/packages/types/src/enterpriseAccount.ts b/packages/types/src/enterpriseAccount.ts index e971e2c9bb4..843a70eaa13 100644 --- a/packages/types/src/enterpriseAccount.ts +++ b/packages/types/src/enterpriseAccount.ts @@ -5,24 +5,26 @@ import type { VerificationResource } from 'verification'; import type { ClerkResource } from './resource'; -type EnterpriseProtocol = 'saml' | 'oauth'; +export type EnterpriseProtocol = 'saml' | 'oauth'; -type EnterpriseProvider = SamlIdpSlug | GoogleOauthProvider | MicrosoftOauthProvider | CustomOAuthStrategy; +export type EnterpriseProvider = SamlIdpSlug | GoogleOauthProvider | MicrosoftOauthProvider | CustomOAuthStrategy; export interface EnterpriseAccountResource extends ClerkResource { + protocol: EnterpriseProtocol; + provider: EnterpriseProvider; active: boolean; emailAddress: string; firstName: string; lastName: string; providerUserId: string | null; - publicMetadata: Record; + publicMetadata: Record | null; verification: VerificationResource | null; enterpriseConnection: EnterpriseAccountConnectionResource | null; - protocol: EnterpriseProtocol; - provider: EnterpriseProvider; } export interface EnterpriseAccountConnectionResource extends ClerkResource { + protocol: EnterpriseProtocol; + provider: EnterpriseProvider; name: string; logoPublicUrl: string; domain: string; @@ -31,6 +33,4 @@ export interface EnterpriseAccountConnectionResource extends ClerkResource { disableAdditionalIdentifications: boolean; allowSubdomains: boolean; allowIdpInitiated: boolean; - protocol: EnterpriseProtocol; - provider: EnterpriseProvider; } diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index ab3f4a69551..3c596ec9742 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -2,6 +2,8 @@ * Currently representing API DTOs in their JSON form. */ +import type { EnterpriseProtocol, EnterpriseProvider } from 'enterpriseAccount'; + import type { DisplayConfigJSON } from './displayConfig'; import type { ActJWTClaim } from './jwt'; import type { OAuthProvider } from './oauth'; @@ -192,6 +194,35 @@ export interface ExternalAccountJSON extends ClerkResourceJSON { verification?: VerificationJSON; } +export interface EnterpriseAccountJSON extends ClerkResourceJSON { + object: 'enterprise_account'; + provider: EnterpriseProvider; + protocol: EnterpriseProtocol; + active: boolean; + email_address: string; + first_name: string | null; + last_name: string | null; + provider_user_id: string | null; + public_metadata?: Record | null; + verification: VerificationJSON | null; + enterprise_connection: EnterpriseAccountConnectionJSON | null; +} + +export interface EnterpriseAccountConnectionJSON extends ClerkResourceJSON { + provider: EnterpriseProvider; + protocol: EnterpriseProtocol; + name: string; + domain: string; + active: boolean; + logo_public_url: string | null; + sync_user_attributes: boolean; + allow_subdomains: boolean; + allow_idp_initiated: boolean; + disable_additional_identifications: boolean; + created_at: number; + updated_at: number; +} + export interface SamlAccountJSON extends ClerkResourceJSON { object: 'saml_account'; provider: SamlIdpSlug; From 0fe99f896299262b87196d659d8694c8ec745a4a Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:11:20 -0300 Subject: [PATCH 04/21] Populate `enterprise_accounts` on `User` resource --- .../src/core/resources/EnterpriseAccount.ts | 20 ++++++------- packages/clerk-js/src/core/resources/User.ts | 7 +++++ .../clerk-js/src/core/resources/internal.ts | 1 + packages/types/src/enterpriseAccount.ts | 20 ++++++------- packages/types/src/json.ts | 30 +++++++++---------- packages/types/src/user.ts | 2 ++ 6 files changed, 45 insertions(+), 35 deletions(-) diff --git a/packages/clerk-js/src/core/resources/EnterpriseAccount.ts b/packages/clerk-js/src/core/resources/EnterpriseAccount.ts index 242564c5058..f820f8fd871 100644 --- a/packages/clerk-js/src/core/resources/EnterpriseAccount.ts +++ b/packages/clerk-js/src/core/resources/EnterpriseAccount.ts @@ -12,11 +12,11 @@ import { Verification } from './Verification'; export class EnterpriseAccount extends BaseResource implements EnterpriseAccountResource { id!: string; - protocol: EnterpriseAccountResource['protocol']; - provider: EnterpriseAccountResource['provider']; + protocol!: EnterpriseAccountResource['protocol']; + provider!: EnterpriseAccountResource['provider']; providerUserId: string | null = null; active!: boolean; - emailAddress!: string; + emailAddress = ''; firstName = ''; lastName = ''; publicMetadata = {}; @@ -59,16 +59,16 @@ export class EnterpriseAccount extends BaseResource implements EnterpriseAccount export class EnterpriseAccountConnection extends BaseResource implements EnterpriseAccountConnectionResource { id!: string; - name!: string; - domain!: string; active!: boolean; - logoPublicUrl: string; - protocol: EnterpriseAccountResource['protocol']; - provider: EnterpriseAccountResource['provider']; - syncUserAttributes!: boolean; - allowSubdomains!: boolean; allowIdpInitiated!: boolean; + allowSubdomains!: boolean; disableAdditionalIdentifications!: boolean; + domain!: string; + logoPublicUrl: string | null = ''; + name!: string; + protocol!: EnterpriseAccountResource['protocol']; + provider!: EnterpriseAccountResource['provider']; + syncUserAttributes!: boolean; createdAt!: Date; updatedAt!: Date; diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts index c46c4180c22..a42e14b4d0a 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -8,6 +8,7 @@ import type { DeletedObjectJSON, DeletedObjectResource, EmailAddressResource, + EnterpriseAccountResource, ExternalAccountJSON, ExternalAccountResource, GetOrganizationMemberships, @@ -38,6 +39,7 @@ import { BaseResource, DeletedObject, EmailAddress, + EnterpriseAccount, ExternalAccount, Image, OrganizationMembership, @@ -61,6 +63,7 @@ export class User extends BaseResource implements UserResource { phoneNumbers: PhoneNumberResource[] = []; web3Wallets: Web3WalletResource[] = []; externalAccounts: ExternalAccountResource[] = []; + enterpriseAccounts: EnterpriseAccountResource[] = []; passkeys: PasskeyResource[] = []; samlAccounts: SamlAccountResource[] = []; @@ -346,6 +349,10 @@ export class User extends BaseResource implements UserResource { this.samlAccounts = (data.saml_accounts || []).map(sa => new SamlAccount(sa, this.path() + '/saml_accounts')); + this.enterpriseAccounts = (data.enterprise_accounts || []).map( + ea => new EnterpriseAccount(ea, this.path() + '/enterprise_accounts'), + ); + this.publicMetadata = data.public_metadata; this.unsafeMetadata = data.unsafe_metadata; diff --git a/packages/clerk-js/src/core/resources/internal.ts b/packages/clerk-js/src/core/resources/internal.ts index 9777cf01ddc..860dd16786e 100644 --- a/packages/clerk-js/src/core/resources/internal.ts +++ b/packages/clerk-js/src/core/resources/internal.ts @@ -9,6 +9,7 @@ export * from './EmailAddress'; export * from './Environment'; export * from './Error'; export * from './ExternalAccount'; +export * from './EnterpriseAccount'; export * from './IdentificationLink'; export * from './Image'; export * from './PhoneNumber'; diff --git a/packages/types/src/enterpriseAccount.ts b/packages/types/src/enterpriseAccount.ts index 843a70eaa13..2915eb468d9 100644 --- a/packages/types/src/enterpriseAccount.ts +++ b/packages/types/src/enterpriseAccount.ts @@ -10,27 +10,27 @@ export type EnterpriseProtocol = 'saml' | 'oauth'; export type EnterpriseProvider = SamlIdpSlug | GoogleOauthProvider | MicrosoftOauthProvider | CustomOAuthStrategy; export interface EnterpriseAccountResource extends ClerkResource { - protocol: EnterpriseProtocol; - provider: EnterpriseProvider; active: boolean; emailAddress: string; + enterpriseConnection: EnterpriseAccountConnectionResource | null; firstName: string; lastName: string; + protocol: EnterpriseProtocol; + provider: EnterpriseProvider; providerUserId: string | null; publicMetadata: Record | null; verification: VerificationResource | null; - enterpriseConnection: EnterpriseAccountConnectionResource | null; } export interface EnterpriseAccountConnectionResource extends ClerkResource { + active: boolean; + allowIdpInitiated: boolean; + allowSubdomains: boolean; + disableAdditionalIdentifications: boolean; + domain: string; + logoPublicUrl: string | null; + name: string; protocol: EnterpriseProtocol; provider: EnterpriseProvider; - name: string; - logoPublicUrl: string; - domain: string; - active: boolean; syncUserAttributes: boolean; - disableAdditionalIdentifications: boolean; - allowSubdomains: boolean; - allowIdpInitiated: boolean; } diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 3c596ec9742..58f97dacf37 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -2,9 +2,8 @@ * Currently representing API DTOs in their JSON form. */ -import type { EnterpriseProtocol, EnterpriseProvider } from 'enterpriseAccount'; - import type { DisplayConfigJSON } from './displayConfig'; +import type { EnterpriseProtocol, EnterpriseProvider } from './enterpriseAccount'; import type { ActJWTClaim } from './jwt'; import type { OAuthProvider } from './oauth'; import type { OrganizationDomainVerificationStatus, OrganizationEnrollmentMode } from './organizationDomain'; @@ -196,29 +195,29 @@ export interface ExternalAccountJSON extends ClerkResourceJSON { export interface EnterpriseAccountJSON extends ClerkResourceJSON { object: 'enterprise_account'; - provider: EnterpriseProvider; - protocol: EnterpriseProtocol; active: boolean; email_address: string; - first_name: string | null; - last_name: string | null; + enterprise_connection: EnterpriseAccountConnectionJSON | null; + first_name: string; + last_name: string; + protocol: EnterpriseProtocol; + provider: EnterpriseProvider; provider_user_id: string | null; - public_metadata?: Record | null; + public_metadata: Record; verification: VerificationJSON | null; - enterprise_connection: EnterpriseAccountConnectionJSON | null; } export interface EnterpriseAccountConnectionJSON extends ClerkResourceJSON { - provider: EnterpriseProvider; - protocol: EnterpriseProtocol; - name: string; - domain: string; active: boolean; - logo_public_url: string | null; - sync_user_attributes: boolean; - allow_subdomains: boolean; allow_idp_initiated: boolean; + allow_subdomains: boolean; disable_additional_identifications: boolean; + domain: string; + logo_public_url: string | null; + name: string; + protocol: EnterpriseProtocol; + provider: EnterpriseProvider; + sync_user_attributes: boolean; created_at: number; updated_at: number; } @@ -249,6 +248,7 @@ export interface UserJSON extends ClerkResourceJSON { phone_numbers: PhoneNumberJSON[]; web3_wallets: Web3WalletJSON[]; external_accounts: ExternalAccountJSON[]; + enterprise_accounts: EnterpriseAccountJSON[]; passkeys: PasskeyJSON[]; saml_accounts: SamlAccountJSON[]; diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts index d45359d6342..2d042cb198e 100644 --- a/packages/types/src/user.ts +++ b/packages/types/src/user.ts @@ -1,6 +1,7 @@ import type { BackupCodeResource } from './backupCode'; import type { DeletedObjectResource } from './deletedObject'; import type { EmailAddressResource } from './emailAddress'; +import type { EnterpriseAccountResource } from './enterpriseAccount'; import type { ExternalAccountResource } from './externalAccount'; import type { ImageResource } from './image'; import type { UserJSON } from './json'; @@ -68,6 +69,7 @@ export interface UserResource extends ClerkResource { phoneNumbers: PhoneNumberResource[]; web3Wallets: Web3WalletResource[]; externalAccounts: ExternalAccountResource[]; + enterpriseAccounts: EnterpriseAccountResource[]; passkeys: PasskeyResource[]; samlAccounts: SamlAccountResource[]; From 7b6c6de99eeb58804229e7c1e7b2828a1f060164 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:11:12 -0300 Subject: [PATCH 05/21] Verify if there are enterprise accounts to display --- .../src/ui/components/UserProfile/AccountPage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx index a4673fa9af4..fc049b80016 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx @@ -12,7 +12,7 @@ import { UserProfileSection } from './UserProfileSection'; import { Web3Section } from './Web3Section'; export const AccountPage = withCardStateProvider(() => { - const { attributes, saml, social } = useEnvironment().userSettings; + const { attributes, social } = useEnvironment().userSettings; const card = useCardState(); const { user } = useUser(); @@ -20,11 +20,11 @@ export const AccountPage = withCardStateProvider(() => { const showEmail = attributes.email_address.enabled; const showPhone = attributes.phone_number.enabled; const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0; - const showSamlAccounts = saml && saml.enabled && user && user.samlAccounts.length > 0; + const showEnterpriseAccounts = user && user.enterpriseAccounts.length > 0; const showWeb3 = attributes.web3_wallet.enabled; const shouldAllowIdentificationCreation = - !showSamlAccounts || + !showEnterpriseAccounts || !user?.samlAccounts?.some( samlAccount => samlAccount.active && samlAccount.samlConnection?.disableAdditionalIdentifications, ); @@ -55,7 +55,7 @@ export const AccountPage = withCardStateProvider(() => { {showConnectedAccounts && } {/*TODO-STEP-UP: Verify that these work as expected*/} - {showSamlAccounts && } + {showEnterpriseAccounts && } {showWeb3 && } From c83c34b7f3eca13d11ba2fa8b805a8d31eb57de0 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:13:03 -0300 Subject: [PATCH 06/21] Update UI description with new enterprise provider union Previously, SAML was the only supported enterprise type, therefore the UI description was only based on `SamlIdpSlug` It'll now use `EnterpriseProvider` to handle multiple connection types. --- packages/types/src/appearance.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index eed90e0fc96..41e4f21191f 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -1,4 +1,5 @@ import type * as CSS from 'csstype'; +import type { EnterpriseProvider } from 'enterpriseAccount'; import type { AlertId, @@ -12,7 +13,6 @@ import type { UserPreviewId, } from './elementIds'; import type { OAuthProvider } from './oauth'; -import type { SamlIdpSlug } from './saml'; import type { BuiltInColors, TransparentColor } from './theme'; import type { Web3Provider } from './web3'; @@ -172,7 +172,7 @@ export type ElementsConfig = { socialButtonsProviderIcon: WithOptions; socialButtonsProviderInitialIcon: WithOptions; - enterpriseButtonsProviderIcon: WithOptions; + enterpriseButtonsProviderIcon: WithOptions; alternativeMethods: WithOptions; alternativeMethodsBlockButton: WithOptions; From b18329dd139203259910efdda351f97cc19862cc Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:14:54 -0300 Subject: [PATCH 07/21] Display enterprise accounts from `User` resource Using `user.enterpriseAccounts` to display on `UserProfile`, instead of `user.samlAccounts` This is going to support the upcoming connection types such as EASIE, OIDC Also, deletes `useSaml` hook as it was a weak abstraction around SAML constants for logo URLs and naming --- .../UserProfile/EnterpriseAccountsSection.tsx | 151 ++++++++++++------ packages/clerk-js/src/ui/hooks/index.ts | 1 - packages/clerk-js/src/ui/hooks/useSaml.ts | 18 --- 3 files changed, 98 insertions(+), 72 deletions(-) delete mode 100644 packages/clerk-js/src/ui/hooks/useSaml.ts diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index 6c49a841adf..a66f9c0de3c 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -1,12 +1,37 @@ +import { iconImageUrl } from '@clerk/shared/constants'; import { useUser } from '@clerk/shared/react'; +import type { + EnterpriseAccountResource, + EnterpriseProvider, + GoogleOauthProvider, + MicrosoftOauthProvider, + SamlIdpSlug, +} from '@clerk/types'; +import { getOAuthProviderData, OAUTH_PROVIDERS, SAML_IDPS } from '@clerk/types'; +import { ProviderInitialIcon } from '../../common'; import { Badge, Box, descriptors, Flex, Image, localizationKeys, Text } from '../../customizables'; import { ProfileSection } from '../../elements'; -import { useSaml } from '../../hooks'; + +const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.startsWith('saml'); + +const isOAuthProvider = (provider: EnterpriseProvider): provider is GoogleOauthProvider | MicrosoftOauthProvider => + OAUTH_PROVIDERS.some(oauth_provider => oauth_provider.provider == provider); + +const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: EnterpriseAccountResource) => { + if (isSamlProvider(provider)) { + return SAML_IDPS[provider]?.name; + } + + if (isOAuthProvider(provider)) { + return getOAuthProviderData({ provider })?.name; + } + + return enterpriseConnection?.name; +}; export const EnterpriseAccountsSection = () => { const { user } = useUser(); - const { getSamlProviderLogoUrl, getSamlProviderName } = useSaml(); return ( { centered={false} > - {user?.samlAccounts.map(account => { - const label = account.emailAddress; - const providerName = getSamlProviderName(account.provider); - const providerLogoUrl = getSamlProviderLogoUrl(account.provider); - const error = account.verification?.error?.longMessage; - - return ( - ({ - gap: t.space.$2, - justifyContent: 'start', - })} - key={account.id} - > - {providerName} ({ width: theme.sizes.$4 })} - /> - - - - {providerName} - - - {label ? `• ${label}` : ''} - - {error && ( - - )} - - - - ); - })} + {user?.enterpriseAccounts.map(account => ( + + ))} ); }; + +const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccountResource }) => { + const providerLogoUrl = iconImageUrl(account.provider) ?? account.enterpriseConnection?.logoPublicUrl; + const providerName = getEnterpriseAccountProviderName(account); + + return providerLogoUrl ? ( + {providerName} ({ width: theme.sizes.$4 })} + /> + ) : ( + + ); +}; + +const EnterpriseAccount = ({ account }: { account: EnterpriseAccountResource }) => { + const label = account.emailAddress; + const providerName = getEnterpriseAccountProviderName(account); + const error = account.verification?.error?.longMessage; + + return ( + ({ + gap: t.space.$2, + justifyContent: 'start', + })} + key={account.id} + > + + + + + {providerName} + + + {label ? `• ${label}` : ''} + + {error && ( + + )} + + + + ); +}; diff --git a/packages/clerk-js/src/ui/hooks/index.ts b/packages/clerk-js/src/ui/hooks/index.ts index 06141362e1d..3ebd8cfcb8b 100644 --- a/packages/clerk-js/src/ui/hooks/index.ts +++ b/packages/clerk-js/src/ui/hooks/index.ts @@ -1,5 +1,4 @@ export * from './useDelayedVisibility'; -export * from './useSaml'; export * from './useWindowEventListener'; export * from './useEmailLink'; export * from './useClipboard'; diff --git a/packages/clerk-js/src/ui/hooks/useSaml.ts b/packages/clerk-js/src/ui/hooks/useSaml.ts deleted file mode 100644 index b5fb5aac1c0..00000000000 --- a/packages/clerk-js/src/ui/hooks/useSaml.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { iconImageUrl } from '@clerk/shared/constants'; -import type { SamlIdpSlug } from '@clerk/types'; -import { SAML_IDPS } from '@clerk/types'; - -function getSamlProviderLogoUrl(provider: SamlIdpSlug = 'saml_custom'): string { - return iconImageUrl(SAML_IDPS[provider]?.logo); -} - -function getSamlProviderName(provider: SamlIdpSlug = 'saml_custom'): string { - return SAML_IDPS[provider]?.name; -} - -export const useSaml = () => { - return { - getSamlProviderLogoUrl, - getSamlProviderName, - }; -}; From 9ae6104125d4dd80ba19786609b07ebed5245390 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:22:22 -0300 Subject: [PATCH 08/21] Disallow user profile changes for multiple enterprise connection types Refer to `user.enterprise_accounts` to verify if the user is allowed to introduce additional identifications --- .../clerk-js/src/ui/components/UserProfile/AccountPage.tsx | 5 +++-- .../clerk-js/src/ui/components/UserProfile/PasswordForm.tsx | 2 +- .../clerk-js/src/ui/components/UserProfile/ProfileForm.tsx | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx index fc049b80016..8588ed7a20a 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx @@ -25,8 +25,9 @@ export const AccountPage = withCardStateProvider(() => { const shouldAllowIdentificationCreation = !showEnterpriseAccounts || - !user?.samlAccounts?.some( - samlAccount => samlAccount.active && samlAccount.samlConnection?.disableAdditionalIdentifications, + !user.enterpriseAccounts.some( + enterpriseAccount => + enterpriseAccount.active && enterpriseAccount.enterpriseConnection?.disableAdditionalIdentifications, ); return ( diff --git a/packages/clerk-js/src/ui/components/UserProfile/PasswordForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/PasswordForm.tsx index 2e1639e6502..07f6fb7e2ae 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/PasswordForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/PasswordForm.tsx @@ -60,7 +60,7 @@ export const PasswordForm = withCardStateProvider((props: PasswordFormProps) => : localizationKeys('userProfile.passwordPage.title__set'); const card = useCardState(); - const passwordEditDisabled = user.samlAccounts.some(sa => sa.active); + const passwordEditDisabled = user.enterpriseAccounts.some(ea => ea.active); // Ensure that messages will not use the updated state of User after a password has been set or changed const successPagePropsRef = useRef[0]>({ diff --git a/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx index 16981171ed3..a14d4254221 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx @@ -47,7 +47,7 @@ export const ProfileForm = withCardStateProvider((props: ProfileFormProps) => { const requiredFieldsFilled = hasRequiredFields && !!lastNameField.value && !!firstNameField.value && optionalFieldsChanged; - const nameEditDisabled = user.samlAccounts.some(sa => sa.active); + const nameEditDisabled = user.enterpriseAccounts.some(ea => ea.active); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); From 7f5056d4b32df4b8443e950fde90a3a9d9d83c78 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:45:49 -0300 Subject: [PATCH 09/21] Introduce UI tests --- .changeset/cyan-shirts-prove.md | 6 + .../UserProfile/EnterpriseAccountsSection.tsx | 35 +-- .../__tests__/AccountPage.test.tsx | 107 ++++++--- .../EnterpriseAccountsSection.test.tsx | 212 ++++++++++++++++++ packages/types/src/enterpriseAccount.ts | 5 +- 5 files changed, 321 insertions(+), 44 deletions(-) create mode 100644 .changeset/cyan-shirts-prove.md create mode 100644 packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx diff --git a/.changeset/cyan-shirts-prove.md b/.changeset/cyan-shirts-prove.md new file mode 100644 index 00000000000..b53c234ef31 --- /dev/null +++ b/.changeset/cyan-shirts-prove.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/types': minor +--- + +Support multiple enterprise protocols in `UserProfile` diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index a66f9c0de3c..22750f1b648 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -15,7 +15,9 @@ import { ProfileSection } from '../../elements'; const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.startsWith('saml'); -const isOAuthProvider = (provider: EnterpriseProvider): provider is GoogleOauthProvider | MicrosoftOauthProvider => +const isOAuthBuiltInProvider = ( + provider: EnterpriseProvider, +): provider is GoogleOauthProvider | MicrosoftOauthProvider => OAUTH_PROVIDERS.some(oauth_provider => oauth_provider.provider == provider); const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: EnterpriseAccountResource) => { @@ -23,7 +25,7 @@ const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: En return SAML_IDPS[provider]?.name; } - if (isOAuthProvider(provider)) { + if (isOAuthBuiltInProvider(provider)) { return getOAuthProviderData({ provider })?.name; } @@ -52,21 +54,26 @@ export const EnterpriseAccountsSection = () => { }; const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccountResource }) => { - const providerLogoUrl = iconImageUrl(account.provider) ?? account.enterpriseConnection?.logoPublicUrl; + const { provider } = account; const providerName = getEnterpriseAccountProviderName(account); - return providerLogoUrl ? ( - {providerName} ({ width: theme.sizes.$4 })} - /> - ) : ( + if (isOAuthBuiltInProvider(provider) || isSamlProvider(provider)) { + return ( + {providerName} ({ width: theme.sizes.$4 })} + /> + ); + } + + return ( ); }; diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx index 65e9e3e1e9f..0b3fef0a402 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx @@ -1,4 +1,4 @@ -import type { SamlAccountJSON } from '@clerk/types'; +import type { EnterpriseAccountJSON } from '@clerk/types'; import { describe, it } from '@jest/globals'; import { render, screen, waitFor } from '../../../../testUtils'; @@ -93,13 +93,48 @@ describe('AccountPage', () => { f.withSaml(); f.withUser({ email_addresses: [emailAddress], - saml_accounts: [ + enterprise_accounts: [ { - id: 'samlacc_foo', + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', provider: 'saml_okta', - email_address: emailAddress, - first_name: firstName, - last_name: lastName, + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, ], first_name: firstName, @@ -118,29 +153,47 @@ describe('AccountPage', () => { const firstName = 'George'; const lastName = 'Clerk'; - const samlAccount: SamlAccountJSON = { - id: 'samlacc_foo', + const enterpriseAccount: EnterpriseAccountJSON = { + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', provider: 'saml_okta', - email_address: emailAddress, - first_name: firstName, - last_name: lastName, - saml_connection: { - id: 'samlc_foo', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', active: true, + allow_idp_initiated: false, + allow_subdomains: false, disable_additional_identifications: true, - allow_idp_initiated: true, - allow_subdomains: true, - domain: 'foo.com', - name: 'Foo', - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - object: 'saml_connection', - provider: 'saml_okta', - sync_user_attributes: true, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', }, - active: true, - object: 'saml_account', - provider_user_id: '', + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }; it('shows only the enterprise accounts of the user', async () => { @@ -151,7 +204,7 @@ describe('AccountPage', () => { f.withSaml(); f.withUser({ email_addresses: [emailAddress], - saml_accounts: [samlAccount], + enterprise_accounts: [enterpriseAccount], first_name: firstName, last_name: lastName, }); @@ -176,7 +229,7 @@ describe('AccountPage', () => { email_addresses: [emailAddress], phone_numbers: [phoneNumber], external_accounts: [{ provider: 'google', email_address: 'test@clerk.com' }], - saml_accounts: [samlAccount], + enterprise_accounts: [enterpriseAccount], first_name: firstName, last_name: lastName, }); diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx new file mode 100644 index 00000000000..c4c3e1410aa --- /dev/null +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx @@ -0,0 +1,212 @@ +import { describe, it } from '@jest/globals'; +import React from 'react'; + +import { render } from '../../../../testUtils'; +import { bindCreateFixtures } from '../../../utils/test/createFixtures'; +import { EnterpriseAccountsSection } from '../EnterpriseAccountsSection'; + +const { createFixtures } = bindCreateFixtures('UserProfile'); + +const withoutEnterpriseConnection = createFixtures.config(f => { + f.withSocialProvider({ provider: 'google' }); + f.withSocialProvider({ provider: 'github' }); + f.withUser({ + external_accounts: [{ provider: 'google', email_address: 'test@clerk.com' }], + }); +}); + +const withOAuthBuiltInEnterpriseConnection = createFixtures.config(f => { + f.withUser({ + enterprise_accounts: [ + { + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'oauth', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'google', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'google', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'oauth', + }, + verification: { + status: 'verified', + strategy: 'oauth_google', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', + }, + ], + }); +}); + +const withOAuthCustomEnterpriseConnection = createFixtures.config(f => { + f.withUser({ + enterprise_accounts: [ + { + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'oauth', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'oauth_custom_roblox', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'oauth_custom_roblox', + name: 'Roblox', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'oauth', + }, + verification: { + status: 'verified', + strategy: 'oauth_custom_roblox', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', + }, + ], + }); +}); + +const withSamlEnterpriseConnection = createFixtures.config(f => { + f.withUser({ + enterprise_accounts: [ + { + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', + }, + ], + }); +}); + +describe('EnterpriseAccountsSection ', () => { + it('renders the component', async () => { + const { wrapper } = await createFixtures(withoutEnterpriseConnection); + + const { getByText } = render(, { wrapper }); + + getByText(/^Enterprise accounts/i); + }); + + describe('with oauth built-in', () => { + it('renders connection', async () => { + const { wrapper } = await createFixtures(withOAuthBuiltInEnterpriseConnection); + + const { getByText, getByRole } = render(, { wrapper }); + + getByText(/^Enterprise accounts/i); + getByText(/google/i); + getByRole('img', { name: /google/i }); + getByText(/test@clerk.com/i); + }); + }); + + describe('with oauth custom', () => { + it('renders connection', async () => { + const { wrapper } = await createFixtures(withOAuthCustomEnterpriseConnection); + + const { getByText } = render(, { wrapper }); + + getByText(/^Enterprise accounts/i); + getByText(/roblox/i); + getByText('R', { exact: true }); + getByText(/test@clerk.com/i); + }); + }); + + describe('with saml', () => { + it('renders connection', async () => { + const { wrapper } = await createFixtures(withSamlEnterpriseConnection); + + const { getByText, getByRole } = render(, { wrapper }); + + getByText(/^Enterprise accounts/i); + getByText(/okta workforce/i); + getByRole('img', { name: /okta workforce/i }); + getByText(/test@clerk.com/i); + }); + }); +}); diff --git a/packages/types/src/enterpriseAccount.ts b/packages/types/src/enterpriseAccount.ts index 2915eb468d9..d474914b622 100644 --- a/packages/types/src/enterpriseAccount.ts +++ b/packages/types/src/enterpriseAccount.ts @@ -1,13 +1,12 @@ -import type { GoogleOauthProvider, MicrosoftOauthProvider } from 'oauth'; +import type { CustomOauthProvider, GoogleOauthProvider, MicrosoftOauthProvider } from 'oauth'; import type { SamlIdpSlug } from 'saml'; -import type { CustomOAuthStrategy } from 'strategies'; import type { VerificationResource } from 'verification'; import type { ClerkResource } from './resource'; export type EnterpriseProtocol = 'saml' | 'oauth'; -export type EnterpriseProvider = SamlIdpSlug | GoogleOauthProvider | MicrosoftOauthProvider | CustomOAuthStrategy; +export type EnterpriseProvider = SamlIdpSlug | GoogleOauthProvider | MicrosoftOauthProvider | CustomOauthProvider; export interface EnterpriseAccountResource extends ClerkResource { active: boolean; From f6e67ee0b5cecaaebb9cc32ca1894896e94d2fb4 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:59:05 -0300 Subject: [PATCH 10/21] Fix icon for SAML providers --- .../UserProfile/EnterpriseAccountsSection.tsx | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index 22750f1b648..578f1cd0c96 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -1,24 +1,27 @@ import { iconImageUrl } from '@clerk/shared/constants'; import { useUser } from '@clerk/shared/react'; import type { + CustomOauthProvider, EnterpriseAccountResource, EnterpriseProvider, GoogleOauthProvider, MicrosoftOauthProvider, SamlIdpSlug, } from '@clerk/types'; -import { getOAuthProviderData, OAUTH_PROVIDERS, SAML_IDPS } from '@clerk/types'; +import { getOAuthProviderData, SAML_IDPS } from '@clerk/types'; import { ProviderInitialIcon } from '../../common'; import { Badge, Box, descriptors, Flex, Image, localizationKeys, Text } from '../../customizables'; import { ProfileSection } from '../../elements'; -const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.startsWith('saml'); +const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.includes('saml'); const isOAuthBuiltInProvider = ( provider: EnterpriseProvider, -): provider is GoogleOauthProvider | MicrosoftOauthProvider => - OAUTH_PROVIDERS.some(oauth_provider => oauth_provider.provider == provider); +): provider is GoogleOauthProvider | MicrosoftOauthProvider => ['google', 'microsoft'].includes(provider); + +const isOAuthCustomProvider = (provider: EnterpriseProvider): provider is CustomOauthProvider => + provider.includes('custom'); const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: EnterpriseAccountResource) => { if (isSamlProvider(provider)) { @@ -57,23 +60,25 @@ const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccount const { provider } = account; const providerName = getEnterpriseAccountProviderName(account); - if (isOAuthBuiltInProvider(provider) || isSamlProvider(provider)) { + if (isOAuthCustomProvider(provider)) { return ( - {providerName} ({ width: theme.sizes.$4 })} + ); } + const src = iconImageUrl(isSamlProvider(provider) ? SAML_IDPS[provider].logo : provider); + return ( - ({ width: theme.sizes.$4 })} /> ); }; From 0b2e26f41f4e2c1980f9676e064c36498d6c6fe7 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:03:38 -0300 Subject: [PATCH 11/21] Update changeset --- .changeset/cyan-shirts-prove.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cyan-shirts-prove.md b/.changeset/cyan-shirts-prove.md index b53c234ef31..294a45fb0fc 100644 --- a/.changeset/cyan-shirts-prove.md +++ b/.changeset/cyan-shirts-prove.md @@ -3,4 +3,4 @@ '@clerk/types': minor --- -Support multiple enterprise protocols in `UserProfile` +Surface enterprise accounts in `UserProfile`, allowing to display more protocols besides SAML From b037709ebb699bca9eedddb6129ed3079ac1a9e8 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:25:48 -0300 Subject: [PATCH 12/21] Update `firstName` and `lastName` to be nullable --- packages/clerk-js/src/core/resources/EnterpriseAccount.ts | 4 ++-- packages/types/src/appearance.ts | 2 +- packages/types/src/enterpriseAccount.ts | 4 ++-- packages/types/src/json.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/core/resources/EnterpriseAccount.ts b/packages/clerk-js/src/core/resources/EnterpriseAccount.ts index f820f8fd871..3e15da29d51 100644 --- a/packages/clerk-js/src/core/resources/EnterpriseAccount.ts +++ b/packages/clerk-js/src/core/resources/EnterpriseAccount.ts @@ -17,8 +17,8 @@ export class EnterpriseAccount extends BaseResource implements EnterpriseAccount providerUserId: string | null = null; active!: boolean; emailAddress = ''; - firstName = ''; - lastName = ''; + firstName: string | null = ''; + lastName: string | null = ''; publicMetadata = {}; verification: VerificationResource | null = null; enterpriseConnection: EnterpriseAccountConnectionResource | null = null; diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index 41e4f21191f..b5f461f8e9f 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -1,5 +1,4 @@ import type * as CSS from 'csstype'; -import type { EnterpriseProvider } from 'enterpriseAccount'; import type { AlertId, @@ -12,6 +11,7 @@ import type { SelectId, UserPreviewId, } from './elementIds'; +import type { EnterpriseProvider } from './enterpriseAccount'; import type { OAuthProvider } from './oauth'; import type { BuiltInColors, TransparentColor } from './theme'; import type { Web3Provider } from './web3'; diff --git a/packages/types/src/enterpriseAccount.ts b/packages/types/src/enterpriseAccount.ts index d474914b622..bd18b136b58 100644 --- a/packages/types/src/enterpriseAccount.ts +++ b/packages/types/src/enterpriseAccount.ts @@ -12,8 +12,8 @@ export interface EnterpriseAccountResource extends ClerkResource { active: boolean; emailAddress: string; enterpriseConnection: EnterpriseAccountConnectionResource | null; - firstName: string; - lastName: string; + firstName: string | null; + lastName: string | null; protocol: EnterpriseProtocol; provider: EnterpriseProvider; providerUserId: string | null; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 58f97dacf37..7e9f9bc75eb 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -198,8 +198,8 @@ export interface EnterpriseAccountJSON extends ClerkResourceJSON { active: boolean; email_address: string; enterprise_connection: EnterpriseAccountConnectionJSON | null; - first_name: string; - last_name: string; + first_name: string | null; + last_name: string | null; protocol: EnterpriseProtocol; provider: EnterpriseProvider; provider_user_id: string | null; From efa346e01e2481685c7118cc4dbc3fbf34d9a97e Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:58:25 -0300 Subject: [PATCH 13/21] Update tests to rely on `enterprise_accounts` --- .../__tests__/PasswordSection.test.tsx | 176 ++++++++++++++++-- .../__tests__/UserProfileSection.test.tsx | 88 ++++++++- 2 files changed, 240 insertions(+), 24 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx index 59d99f13f20..dd5344d2641 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx @@ -115,12 +115,48 @@ describe('PasswordSection', () => { f.withSaml(); f.withUser({ email_addresses: [emailAddress], - saml_accounts: [ + enterprise_accounts: [ { - id: 'samlacc_foo', - provider: 'saml_okta', + object: 'enterprise_account', active: true, - email_address: emailAddress, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, ], }); @@ -152,12 +188,48 @@ describe('PasswordSection', () => { f.withSaml(); f.withUser({ email_addresses: [emailAddress], - saml_accounts: [ + enterprise_accounts: [ { - id: 'samlacc_foo', + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', provider: 'saml_okta', - active: false, - email_address: emailAddress, + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, ], }); @@ -253,12 +325,48 @@ describe('PasswordSection', () => { f.withUser({ password_enabled: true, email_addresses: [emailAddress], - saml_accounts: [ + enterprise_accounts: [ { - id: 'samlacc_foo', - provider: 'saml_okta', + object: 'enterprise_account', active: true, - email_address: emailAddress, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, ], }); @@ -291,12 +399,48 @@ describe('PasswordSection', () => { f.withUser({ password_enabled: true, email_addresses: [emailAddress], - saml_accounts: [ + enterprise_accounts: [ { - id: 'samlacc_foo', + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', provider: 'saml_okta', - active: false, - email_address: emailAddress, + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, ], }); diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx index dd55e66a99a..b1947fb09ba 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx @@ -65,12 +65,48 @@ describe('ProfileSection', () => { first_name: firstName, last_name: lastName, email_addresses: [emailAddress], - saml_accounts: [ + enterprise_accounts: [ { - id: 'samlacc_foo', - provider: 'saml_okta', + object: 'enterprise_account', active: true, - email_address: emailAddress, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, ], }); @@ -104,12 +140,48 @@ describe('ProfileSection', () => { first_name: firstName, last_name: lastName, email_addresses: [emailAddress], - saml_accounts: [ + enterprise_accounts: [ { - id: 'samlacc_foo', + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', provider: 'saml_okta', - active: false, - email_address: emailAddress, + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, ], }); From 38750b2ab21b72a84e3a01b02844fad4dae5032f Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:58:46 -0300 Subject: [PATCH 14/21] Add deprecation JSDocs --- .../clerk-js/src/ui/components/UserProfile/AccountPage.tsx | 4 ++-- .../ui/components/UserProfile/EnterpriseAccountsSection.tsx | 2 +- .../components/UserProfile/__tests__/PasswordSection.test.tsx | 4 ++-- .../UserProfile/__tests__/UserProfileSection.test.tsx | 4 ++-- packages/types/src/json.ts | 3 +++ packages/types/src/user.ts | 3 +++ 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx index 8588ed7a20a..9b9b6e446dc 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx @@ -12,7 +12,7 @@ import { UserProfileSection } from './UserProfileSection'; import { Web3Section } from './Web3Section'; export const AccountPage = withCardStateProvider(() => { - const { attributes, social } = useEnvironment().userSettings; + const { attributes, social, saml } = useEnvironment().userSettings; const card = useCardState(); const { user } = useUser(); @@ -20,7 +20,7 @@ export const AccountPage = withCardStateProvider(() => { const showEmail = attributes.email_address.enabled; const showPhone = attributes.phone_number.enabled; const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0; - const showEnterpriseAccounts = user && user.enterpriseAccounts.length > 0; + const showEnterpriseAccounts = user && saml.enabled && user.enterpriseAccounts.length > 0; const showWeb3 = attributes.web3_wallet.enabled; const shouldAllowIdentificationCreation = diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index 578f1cd0c96..c590a02596a 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -70,7 +70,7 @@ const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccount ); } - const src = iconImageUrl(isSamlProvider(provider) ? SAML_IDPS[provider].logo : provider); + const src = iconImageUrl(isOAuthBuiltInProvider(provider) ? provider : SAML_IDPS[provider].logo); return ( { enterprise_accounts: [ { object: 'enterprise_account', - active: true, + active: false, first_name: 'Laura', last_name: 'Serafim', protocol: 'saml', @@ -204,7 +204,7 @@ describe('PasswordSection', () => { provider: 'saml_okta', name: 'FooCorp', id: 'ent_123', - active: true, + active: false, allow_idp_initiated: false, allow_subdomains: false, disable_additional_identifications: false, diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx index b1947fb09ba..4c3a6d72bd6 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx @@ -143,7 +143,7 @@ describe('ProfileSection', () => { enterprise_accounts: [ { object: 'enterprise_account', - active: true, + active: false, first_name: 'Laura', last_name: 'Serafim', protocol: 'saml', @@ -156,7 +156,7 @@ describe('ProfileSection', () => { provider: 'saml_okta', name: 'FooCorp', id: 'ent_123', - active: true, + active: false, allow_idp_initiated: false, allow_subdomains: false, disable_additional_identifications: false, diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 7e9f9bc75eb..152ed6dd083 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -250,6 +250,9 @@ export interface UserJSON extends ClerkResourceJSON { external_accounts: ExternalAccountJSON[]; enterprise_accounts: EnterpriseAccountJSON[]; passkeys: PasskeyJSON[]; + /** + * @deprecated use `enterprise_accounts` instead + */ saml_accounts: SamlAccountJSON[]; organization_memberships: OrganizationMembershipJSON[]; diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts index 2d042cb198e..a19440eec43 100644 --- a/packages/types/src/user.ts +++ b/packages/types/src/user.ts @@ -71,6 +71,9 @@ export interface UserResource extends ClerkResource { externalAccounts: ExternalAccountResource[]; enterpriseAccounts: EnterpriseAccountResource[]; passkeys: PasskeyResource[]; + /** + * @deprecated use `enterpriseAccounts` instead + */ samlAccounts: SamlAccountResource[]; organizationMemberships: OrganizationMembershipResource[]; From 75c913bb28534bb74b4ca4d2ae8628816063e064 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:07:25 -0300 Subject: [PATCH 15/21] Treat provider as strategy --- .../UserProfile/EnterpriseAccountsSection.tsx | 14 +++++++------- .../__tests__/EnterpriseAccountsSection.test.tsx | 4 ++-- packages/types/src/enterpriseAccount.ts | 4 +++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index c590a02596a..ae216e5d201 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -4,8 +4,7 @@ import type { CustomOauthProvider, EnterpriseAccountResource, EnterpriseProvider, - GoogleOauthProvider, - MicrosoftOauthProvider, + EnterpriseProviderKey, SamlIdpSlug, } from '@clerk/types'; import { getOAuthProviderData, SAML_IDPS } from '@clerk/types'; @@ -16,9 +15,8 @@ import { ProfileSection } from '../../elements'; const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.includes('saml'); -const isOAuthBuiltInProvider = ( - provider: EnterpriseProvider, -): provider is GoogleOauthProvider | MicrosoftOauthProvider => ['google', 'microsoft'].includes(provider); +const isOAuthBuiltInProvider = (provider: EnterpriseProvider): provider is EnterpriseProviderKey => + ['oauth_google', 'oauth_microsoft'].includes(provider); const isOAuthCustomProvider = (provider: EnterpriseProvider): provider is CustomOauthProvider => provider.includes('custom'); @@ -29,7 +27,7 @@ const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: En } if (isOAuthBuiltInProvider(provider)) { - return getOAuthProviderData({ provider })?.name; + return getOAuthProviderData({ strategy: provider })?.name; } return enterpriseConnection?.name; @@ -70,7 +68,9 @@ const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccount ); } - const src = iconImageUrl(isOAuthBuiltInProvider(provider) ? provider : SAML_IDPS[provider].logo); + const src = iconImageUrl( + isOAuthBuiltInProvider(provider) ? provider.replace('oauth_', '').trim() : SAML_IDPS[provider].logo, + ); return ( { provider_user_id: null, public_metadata: {}, email_address: 'test@clerk.com', - provider: 'google', + provider: 'oauth_google', enterprise_connection: { object: 'enterprise_connection', - provider: 'google', + provider: 'oauth_google', name: 'FooCorp', id: 'ent_123', active: true, diff --git a/packages/types/src/enterpriseAccount.ts b/packages/types/src/enterpriseAccount.ts index bd18b136b58..44c42f3108e 100644 --- a/packages/types/src/enterpriseAccount.ts +++ b/packages/types/src/enterpriseAccount.ts @@ -6,7 +6,9 @@ import type { ClerkResource } from './resource'; export type EnterpriseProtocol = 'saml' | 'oauth'; -export type EnterpriseProvider = SamlIdpSlug | GoogleOauthProvider | MicrosoftOauthProvider | CustomOauthProvider; +export type EnterpriseProviderKey = `oauth_${GoogleOauthProvider | MicrosoftOauthProvider}`; + +export type EnterpriseProvider = SamlIdpSlug | EnterpriseProviderKey | CustomOauthProvider; export interface EnterpriseAccountResource extends ClerkResource { active: boolean; From 63c8e3c0ee34b7fc0754946166afb0ee179b728f Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:27:23 -0300 Subject: [PATCH 16/21] Add test for inactive connection --- .../ui/components/UserProfile/AccountPage.tsx | 4 +- .../UserProfile/EnterpriseAccountsSection.tsx | 109 ++++++----- .../__tests__/AccountPage.test.tsx | 175 ++++++++++++------ .../EnterpriseAccountsSection.test.tsx | 71 ++++++- .../__tests__/PasswordSection.test.tsx | 4 +- 5 files changed, 252 insertions(+), 111 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx index 9b9b6e446dc..8588ed7a20a 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx @@ -12,7 +12,7 @@ import { UserProfileSection } from './UserProfileSection'; import { Web3Section } from './Web3Section'; export const AccountPage = withCardStateProvider(() => { - const { attributes, social, saml } = useEnvironment().userSettings; + const { attributes, social } = useEnvironment().userSettings; const card = useCardState(); const { user } = useUser(); @@ -20,7 +20,7 @@ export const AccountPage = withCardStateProvider(() => { const showEmail = attributes.email_address.enabled; const showPhone = attributes.phone_number.enabled; const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0; - const showEnterpriseAccounts = user && saml.enabled && user.enterpriseAccounts.length > 0; + const showEnterpriseAccounts = user && user.enterpriseAccounts.length > 0; const showWeb3 = attributes.web3_wallet.enabled; const shouldAllowIdentificationCreation = diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index ae216e5d201..e382f96265a 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -13,29 +13,17 @@ import { ProviderInitialIcon } from '../../common'; import { Badge, Box, descriptors, Flex, Image, localizationKeys, Text } from '../../customizables'; import { ProfileSection } from '../../elements'; -const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.includes('saml'); - -const isOAuthBuiltInProvider = (provider: EnterpriseProvider): provider is EnterpriseProviderKey => - ['oauth_google', 'oauth_microsoft'].includes(provider); - -const isOAuthCustomProvider = (provider: EnterpriseProvider): provider is CustomOauthProvider => - provider.includes('custom'); +export const EnterpriseAccountsSection = () => { + const { user } = useUser(); -const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: EnterpriseAccountResource) => { - if (isSamlProvider(provider)) { - return SAML_IDPS[provider]?.name; - } + const activeEnterpriseAccounts = user?.enterpriseAccounts.filter( + ({ enterpriseConnection }) => enterpriseConnection?.active, + ); - if (isOAuthBuiltInProvider(provider)) { - return getOAuthProviderData({ strategy: provider })?.name; + if (!activeEnterpriseAccounts?.length) { + return null; } - return enterpriseConnection?.name; -}; - -export const EnterpriseAccountsSection = () => { - const { user } = useUser(); - return ( { centered={false} > - {user?.enterpriseAccounts.map(account => ( + {activeEnterpriseAccounts.map(account => ( { ); }; -const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccountResource }) => { - const { provider } = account; - const providerName = getEnterpriseAccountProviderName(account); - - if (isOAuthCustomProvider(provider)) { - return ( - - ); - } - - const src = iconImageUrl( - isOAuthBuiltInProvider(provider) ? provider.replace('oauth_', '').trim() : SAML_IDPS[provider].logo, - ); - - return ( - {providerName} ({ width: theme.sizes.$4 })} - /> - ); -}; - const EnterpriseAccount = ({ account }: { account: EnterpriseAccountResource }) => { const label = account.emailAddress; const providerName = getEnterpriseAccountProviderName(account); @@ -127,3 +86,55 @@ const EnterpriseAccount = ({ account }: { account: EnterpriseAccountResource }) ); }; + +const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccountResource }) => { + const { provider } = account; + const providerName = getEnterpriseAccountProviderName(account); + + if (isOAuthCustomProvider(provider)) { + return ( + + ); + } + + const src = iconImageUrl( + isOAuthBuiltInProvider(provider) + ? // Remove 'oauth_' prefix since our CDN image paths don't include it + provider.replace('oauth_', '').trim() + : SAML_IDPS[provider].logo, + ); + + return ( + {providerName} ({ width: theme.sizes.$4 })} + /> + ); +}; + +const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: EnterpriseAccountResource) => { + if (isSamlProvider(provider)) { + return SAML_IDPS[provider]?.name; + } + + if (isOAuthBuiltInProvider(provider)) { + return getOAuthProviderData({ strategy: provider })?.name; + } + + return enterpriseConnection?.name; +}; + +const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.includes('saml'); + +const isOAuthBuiltInProvider = (provider: EnterpriseProvider): provider is EnterpriseProviderKey => + ['oauth_google', 'oauth_microsoft'].includes(provider); + +const isOAuthCustomProvider = (provider: EnterpriseProvider): provider is CustomOauthProvider => + provider.includes('custom'); diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx index 0b3fef0a402..602b01f1108 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx @@ -83,68 +83,135 @@ describe('AccountPage', () => { screen.getByText(/google/i); }); - it('shows the enterprise accounts of the user', async () => { - const emailAddress = 'george@jungle.com'; - const firstName = 'George'; - const lastName = 'Clerk'; + describe('with active enterprise connection', () => { + it('shows the enterprise accounts of the user', async () => { + const emailAddress = 'george@jungle.com'; + const firstName = 'George'; + const lastName = 'Clerk'; - const { wrapper } = await createFixtures(f => { - f.withEmailAddress(); - f.withSaml(); - f.withUser({ - email_addresses: [emailAddress], - enterprise_accounts: [ - { - object: 'enterprise_account', - active: true, - first_name: 'Laura', - last_name: 'Serafim', - protocol: 'saml', - provider_user_id: null, - public_metadata: {}, - email_address: 'test@clerk.com', - provider: 'saml_okta', - enterprise_connection: { - object: 'enterprise_connection', - provider: 'saml_okta', - name: 'FooCorp', - id: 'ent_123', + const { wrapper } = await createFixtures(f => { + f.withEmailAddress(); + f.withSaml(); + f.withUser({ + email_addresses: [emailAddress], + enterprise_accounts: [ + { + object: 'enterprise_account', active: true, - allow_idp_initiated: false, - allow_subdomains: false, - disable_additional_identifications: false, - sync_user_attributes: false, - domain: 'foocorp.com', - created_at: 123, - updated_at: 123, - logo_public_url: null, + first_name: 'Laura', + last_name: 'Serafim', protocol: 'saml', - }, - verification: { - status: 'verified', - strategy: 'saml', - verified_at_client: 'foo', - attempts: 0, - error: { - code: 'identifier_already_signed_in', - long_message: "You're already signed in", - message: "You're already signed in", + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', }, - expire_at: 123, - id: 'ver_123', - object: 'verification', + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, - id: 'eac_123', - }, - ], - first_name: firstName, - last_name: lastName, + ], + first_name: firstName, + last_name: lastName, + }); }); + + render(, { wrapper }); + screen.getByText(/Enterprise Accounts/i); + screen.getByText(/Okta Workforce/i); }); + }); - render(, { wrapper }); - screen.getByText(/Enterprise Accounts/i); - screen.getByText(/Okta Workforce/i); + describe('with inactive enterprise connection', () => { + it('does not show the enterprise accounts of the user', async () => { + const emailAddress = 'george@jungle.com'; + const firstName = 'George'; + const lastName = 'Clerk'; + + const { wrapper } = await createFixtures(f => { + f.withEmailAddress(); + f.withUser({ + email_addresses: [emailAddress], + enterprise_accounts: [ + { + object: 'enterprise_account', + active: false, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: false, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', + }, + ], + first_name: firstName, + last_name: lastName, + }); + }); + + render(, { wrapper }); + expect(screen.queryByText(/Enterprise Accounts/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/Okta Workforce/i)).not.toBeInTheDocument(); + }); }); describe('with `disable_additional_identifications`', () => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx index d386f3eb205..ccf69d0b2ee 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx @@ -15,6 +15,57 @@ const withoutEnterpriseConnection = createFixtures.config(f => { }); }); +const withInactiveEnterpriseConnection = createFixtures.config(f => { + f.withSocialProvider({ provider: 'google' }); + f.withSocialProvider({ provider: 'github' }); + f.withUser({ + enterprise_accounts: [ + { + object: 'enterprise_account', + active: false, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'FooCorp', + id: 'ent_123', + active: false, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', + }, + ], + }); +}); + const withOAuthBuiltInEnterpriseConnection = createFixtures.config(f => { f.withUser({ enterprise_accounts: [ @@ -163,12 +214,24 @@ const withSamlEnterpriseConnection = createFixtures.config(f => { }); describe('EnterpriseAccountsSection ', () => { - it('renders the component', async () => { - const { wrapper } = await createFixtures(withoutEnterpriseConnection); + describe('without enterprise accounts', () => { + it('does not render the component', async () => { + const { wrapper } = await createFixtures(withoutEnterpriseConnection); - const { getByText } = render(, { wrapper }); + const { queryByText } = render(, { wrapper }); - getByText(/^Enterprise accounts/i); + expect(queryByText(/^Enterprise accounts/i)).not.toBeInTheDocument(); + }); + }); + + describe('with inactive enterprise accounts accounts', () => { + it('does not render the component', async () => { + const { wrapper } = await createFixtures(withInactiveEnterpriseConnection); + + const { queryByText } = render(, { wrapper }); + + expect(queryByText(/^Enterprise accounts/i)).not.toBeInTheDocument(); + }); }); describe('with oauth built-in', () => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx index 9f24b2c8e70..817985b66a9 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx @@ -402,7 +402,7 @@ describe('PasswordSection', () => { enterprise_accounts: [ { object: 'enterprise_account', - active: true, + active: false, first_name: 'Laura', last_name: 'Serafim', protocol: 'saml', @@ -415,7 +415,7 @@ describe('PasswordSection', () => { provider: 'saml_okta', name: 'FooCorp', id: 'ent_123', - active: true, + active: false, allow_idp_initiated: false, allow_subdomains: false, disable_additional_identifications: false, From 6f33bb6d9791ada4a32dcaab48f777f39be75aa7 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:33:08 -0300 Subject: [PATCH 17/21] Add generic descriptor for SSO connections --- .../clerk-js/src/ui/common/ProviderInitialIcon.tsx | 4 ++-- .../UserProfile/EnterpriseAccountsSection.tsx | 12 ++++++++++-- .../src/ui/customizables/elementDescriptors.ts | 4 +++- packages/types/src/appearance.ts | 4 +++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx b/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx index 5976741eacb..f0c7baaad34 100644 --- a/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx +++ b/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx @@ -15,8 +15,8 @@ export const ProviderInitialIcon = (props: ProviderInitialIconProps) => { return ( ({ ...common.centeredFlex('inline-flex'), width: t.space.$4, diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index e382f96265a..3438ca01065 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -88,11 +88,19 @@ const EnterpriseAccount = ({ account }: { account: EnterpriseAccountResource }) }; const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccountResource }) => { - const { provider } = account; + const { provider, enterpriseConnection } = account; const providerName = getEnterpriseAccountProviderName(account); if (isOAuthCustomProvider(provider)) { - return ( + return enterpriseConnection?.logoPublicUrl ? ( + {providerName} ({ width: theme.sizes.$4 })} + /> + ) : ( ; socialButtonsBlockButtonText: WithOptions; socialButtonsProviderIcon: WithOptions; - socialButtonsProviderInitialIcon: WithOptions; enterpriseButtonsProviderIcon: WithOptions; + ssoConnectionProviderInitialIcon: WithOptions; + alternativeMethods: WithOptions; alternativeMethodsBlockButton: WithOptions; alternativeMethodsBlockButtonText: WithOptions; From 2c3ec625a72f8d4045d4cdc02a53553a94d69269 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:41:22 -0300 Subject: [PATCH 18/21] Include generic `providerInitialIcon` descriptor --- .../src/ui/common/ProviderInitialIcon.tsx | 8 ++++++-- .../UserProfile/EnterpriseAccountsSection.tsx | 18 ++++++++---------- .../src/ui/customizables/elementDescriptors.ts | 7 +++---- packages/types/src/appearance.ts | 8 +++----- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx b/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx index f0c7baaad34..3981275d60d 100644 --- a/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx +++ b/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx @@ -15,8 +15,12 @@ export const ProviderInitialIcon = (props: ProviderInitialIconProps) => { return ( ({ ...common.centeredFlex('inline-flex'), width: t.space.$4, diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index 3438ca01065..97bf57615f4 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -5,6 +5,7 @@ import type { EnterpriseAccountResource, EnterpriseProvider, EnterpriseProviderKey, + OAuthProvider, SamlIdpSlug, } from '@clerk/types'; import { getOAuthProviderData, SAML_IDPS } from '@clerk/types'; @@ -89,13 +90,13 @@ const EnterpriseAccount = ({ account }: { account: EnterpriseAccountResource }) const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccountResource }) => { const { provider, enterpriseConnection } = account; - const providerName = getEnterpriseAccountProviderName(account); + const providerName = getEnterpriseAccountProviderName(account) ?? provider; if (isOAuthCustomProvider(provider)) { return enterpriseConnection?.logoPublicUrl ? ( {providerName} ({ width: theme.sizes.$4 })} @@ -103,23 +104,20 @@ const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccount ) : ( ); } - const src = iconImageUrl( - isOAuthBuiltInProvider(provider) - ? // Remove 'oauth_' prefix since our CDN image paths don't include it - provider.replace('oauth_', '').trim() - : SAML_IDPS[provider].logo, - ); + const providerWithoutPrefix = provider.replace('oauth_', '').trim() as OAuthProvider; + + const src = iconImageUrl(isOAuthBuiltInProvider(provider) ? providerWithoutPrefix : SAML_IDPS[provider].logo); return ( {providerName} ({ width: theme.sizes.$4 })} diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts index 7e43c6ca309..91e4d77e44f 100644 --- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts +++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts @@ -52,10 +52,10 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'socialButtonsBlockButton', 'socialButtonsBlockButtonText', 'socialButtonsProviderIcon', + 'socialButtonsProviderInitialIcon', - 'enterpriseButtonsProviderIcon', - - 'ssoConnectionProviderInitialIcon', + 'providerIcon', + 'providerInitialIcon', 'alternativeMethods', 'alternativeMethodsBlockButton', @@ -273,7 +273,6 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'badge', 'notificationBadge', 'buttonArrowIcon', - 'providerIcon', 'spinner', ] as const).map(camelize) as (keyof ElementsConfig)[]; diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index 975aab88b4e..4f3c536b899 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -11,7 +11,6 @@ import type { SelectId, UserPreviewId, } from './elementIds'; -import type { EnterpriseProvider } from './enterpriseAccount'; import type { OAuthProvider } from './oauth'; import type { SamlIdpSlug } from './saml'; import type { BuiltInColors, TransparentColor } from './theme'; @@ -171,10 +170,10 @@ export type ElementsConfig = { socialButtonsBlockButton: WithOptions; socialButtonsBlockButtonText: WithOptions; socialButtonsProviderIcon: WithOptions; + socialButtonsProviderInitialIcon: WithOptions; - enterpriseButtonsProviderIcon: WithOptions; - - ssoConnectionProviderInitialIcon: WithOptions; + providerIcon: WithOptions; + providerInitialIcon: WithOptions; alternativeMethods: WithOptions; alternativeMethodsBlockButton: WithOptions; @@ -399,7 +398,6 @@ export type ElementsConfig = { badge: WithOptions<'primary' | 'actionRequired'>; notificationBadge: WithOptions; buttonArrowIcon: WithOptions; - providerIcon: WithOptions; spinner: WithOptions; }; From 3e3cee689049caf70bfcee95e1974519eba28b30 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:09:34 -0300 Subject: [PATCH 19/21] Preserve `ProviderInitialIcon` descriptors --- .../src/ui/common/ProviderInitialIcon.tsx | 8 +- .../UserProfile/EnterpriseAccountsSection.tsx | 26 ++-- .../EnterpriseAccountsSection.test.tsx | 125 ++++++++++-------- .../ui/customizables/elementDescriptors.ts | 2 + packages/types/src/appearance.ts | 3 + packages/types/src/enterpriseAccount.ts | 6 +- 6 files changed, 94 insertions(+), 76 deletions(-) diff --git a/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx b/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx index 3981275d60d..5976741eacb 100644 --- a/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx +++ b/packages/clerk-js/src/ui/common/ProviderInitialIcon.tsx @@ -15,12 +15,8 @@ export const ProviderInitialIcon = (props: ProviderInitialIconProps) => { return ( ({ ...common.centeredFlex('inline-flex'), width: t.space.$4, diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index 97bf57615f4..370a3e4d10a 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -4,7 +4,6 @@ import type { CustomOauthProvider, EnterpriseAccountResource, EnterpriseProvider, - EnterpriseProviderKey, OAuthProvider, SamlIdpSlug, } from '@clerk/types'; @@ -92,32 +91,34 @@ const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccount const { provider, enterpriseConnection } = account; const providerName = getEnterpriseAccountProviderName(account) ?? provider; + const providerWithoutPrefix = provider.replace('oauth_', '').trim() as OAuthProvider; + if (isOAuthCustomProvider(provider)) { return enterpriseConnection?.logoPublicUrl ? ( {providerName} ({ width: theme.sizes.$4 })} /> ) : ( ); } - const providerWithoutPrefix = provider.replace('oauth_', '').trim() as OAuthProvider; - - const src = iconImageUrl(isOAuthBuiltInProvider(provider) ? providerWithoutPrefix : SAML_IDPS[provider].logo); + const src = iconImageUrl(isSamlProvider(provider) ? SAML_IDPS[provider].logo : providerWithoutPrefix); return ( {providerName} ({ width: theme.sizes.$4 })} @@ -130,17 +131,14 @@ const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: En return SAML_IDPS[provider]?.name; } - if (isOAuthBuiltInProvider(provider)) { - return getOAuthProviderData({ strategy: provider })?.name; + if (isOAuthCustomProvider(provider)) { + return enterpriseConnection?.name; } - return enterpriseConnection?.name; + return getOAuthProviderData({ strategy: provider })?.name; }; const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.includes('saml'); -const isOAuthBuiltInProvider = (provider: EnterpriseProvider): provider is EnterpriseProviderKey => - ['oauth_google', 'oauth_microsoft'].includes(provider); - -const isOAuthCustomProvider = (provider: EnterpriseProvider): provider is CustomOauthProvider => +const isOAuthCustomProvider = (provider: EnterpriseProvider): provider is `oauth_${CustomOauthProvider}` => provider.includes('custom'); diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx index ccf69d0b2ee..8db29e02afa 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx @@ -115,54 +115,55 @@ const withOAuthBuiltInEnterpriseConnection = createFixtures.config(f => { }); }); -const withOAuthCustomEnterpriseConnection = createFixtures.config(f => { - f.withUser({ - enterprise_accounts: [ - { - object: 'enterprise_account', - active: true, - first_name: 'Laura', - last_name: 'Serafim', - protocol: 'oauth', - provider_user_id: null, - public_metadata: {}, - email_address: 'test@clerk.com', - provider: 'oauth_custom_roblox', - enterprise_connection: { - object: 'enterprise_connection', - provider: 'oauth_custom_roblox', - name: 'Roblox', - id: 'ent_123', +const withOAuthCustomEnterpriseConnection = (logoPublicUrl: string | null) => + createFixtures.config(f => { + f.withUser({ + enterprise_accounts: [ + { + object: 'enterprise_account', active: true, - allow_idp_initiated: false, - allow_subdomains: false, - disable_additional_identifications: false, - sync_user_attributes: false, - domain: 'foocorp.com', - created_at: 123, - updated_at: 123, - logo_public_url: null, + first_name: 'Laura', + last_name: 'Serafim', protocol: 'oauth', - }, - verification: { - status: 'verified', - strategy: 'oauth_custom_roblox', - verified_at_client: 'foo', - attempts: 0, - error: { - code: 'identifier_already_signed_in', - long_message: "You're already signed in", - message: "You're already signed in", + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'oauth_custom_roblox', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'oauth_custom_roblox', + name: 'Roblox', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: logoPublicUrl, + protocol: 'oauth', }, - expire_at: 123, - id: 'ver_123', - object: 'verification', + verification: { + status: 'verified', + strategy: 'oauth_custom_roblox', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', }, - id: 'eac_123', - }, - ], + ], + }); }); -}); const withSamlEnterpriseConnection = createFixtures.config(f => { f.withUser({ @@ -242,21 +243,40 @@ describe('EnterpriseAccountsSection ', () => { getByText(/^Enterprise accounts/i); getByText(/google/i); - getByRole('img', { name: /google/i }); + const img = getByRole('img', { name: /google/i }); + expect(img.getAttribute('src')).toBe('https://img.clerk.com/static/google.svg?width=160'); getByText(/test@clerk.com/i); }); }); describe('with oauth custom', () => { - it('renders connection', async () => { - const { wrapper } = await createFixtures(withOAuthCustomEnterpriseConnection); + describe('with logo', () => { + it('renders connection with logo', async () => { + const mockLogoUrl = 'https://mycdn.com/satic/foo.png'; - const { getByText } = render(, { wrapper }); + const { wrapper } = await createFixtures(withOAuthCustomEnterpriseConnection(mockLogoUrl)); - getByText(/^Enterprise accounts/i); - getByText(/roblox/i); - getByText('R', { exact: true }); - getByText(/test@clerk.com/i); + const { getByText, getByRole } = render(, { wrapper }); + + getByText(/^Enterprise accounts/i); + getByText(/roblox/i); + const img = getByRole('img', { name: /roblox/i }); + expect(img.getAttribute('src')).toContain(mockLogoUrl); + getByText(/test@clerk.com/i); + }); + }); + + describe('without logo', () => { + it('renders connection with initial icon', async () => { + const { wrapper } = await createFixtures(withOAuthCustomEnterpriseConnection(null)); + + const { getByText } = render(, { wrapper }); + + getByText(/^Enterprise accounts/i); + getByText(/roblox/i); + getByText('R', { exact: true }); + getByText(/test@clerk.com/i); + }); }); }); @@ -268,7 +288,8 @@ describe('EnterpriseAccountsSection ', () => { getByText(/^Enterprise accounts/i); getByText(/okta workforce/i); - getByRole('img', { name: /okta workforce/i }); + const img = getByRole('img', { name: /okta/i }); + expect(img.getAttribute('src')).toBe('https://img.clerk.com/static/okta.svg?width=160'); getByText(/test@clerk.com/i); }); }); diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts index 91e4d77e44f..2f49c54046c 100644 --- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts +++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts @@ -54,6 +54,8 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'socialButtonsProviderIcon', 'socialButtonsProviderInitialIcon', + 'enterpriseButtonsProviderIcon', + 'providerIcon', 'providerInitialIcon', diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index 4f3c536b899..ce8ccc75189 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -11,6 +11,7 @@ import type { SelectId, UserPreviewId, } from './elementIds'; +import type { EnterpriseProvider } from './enterpriseAccount'; import type { OAuthProvider } from './oauth'; import type { SamlIdpSlug } from './saml'; import type { BuiltInColors, TransparentColor } from './theme'; @@ -172,6 +173,8 @@ export type ElementsConfig = { socialButtonsProviderIcon: WithOptions; socialButtonsProviderInitialIcon: WithOptions; + enterpriseButtonsProviderIcon: WithOptions; + providerIcon: WithOptions; providerInitialIcon: WithOptions; diff --git a/packages/types/src/enterpriseAccount.ts b/packages/types/src/enterpriseAccount.ts index 44c42f3108e..c2f5f0f3afa 100644 --- a/packages/types/src/enterpriseAccount.ts +++ b/packages/types/src/enterpriseAccount.ts @@ -1,4 +1,4 @@ -import type { CustomOauthProvider, GoogleOauthProvider, MicrosoftOauthProvider } from 'oauth'; +import type { OAuthProvider } from 'oauth'; import type { SamlIdpSlug } from 'saml'; import type { VerificationResource } from 'verification'; @@ -6,9 +6,7 @@ import type { ClerkResource } from './resource'; export type EnterpriseProtocol = 'saml' | 'oauth'; -export type EnterpriseProviderKey = `oauth_${GoogleOauthProvider | MicrosoftOauthProvider}`; - -export type EnterpriseProvider = SamlIdpSlug | EnterpriseProviderKey | CustomOauthProvider; +export type EnterpriseProvider = SamlIdpSlug | `oauth_${OAuthProvider}`; export interface EnterpriseAccountResource extends ClerkResource { active: boolean; From d4cdd221a5115bf55e4ee35955b5c740a5b5dc38 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:06:50 -0300 Subject: [PATCH 20/21] Rely on FAPI for connection name and logo --- .../UserProfile/EnterpriseAccountsSection.tsx | 69 ++++--------------- .../__tests__/AccountPage.test.tsx | 6 +- .../EnterpriseAccountsSection.test.tsx | 12 ++-- .../__tests__/PasswordSection.test.tsx | 8 +-- .../__tests__/UserProfileSection.test.tsx | 4 +- 5 files changed, 30 insertions(+), 69 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index 370a3e4d10a..15484320153 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -1,13 +1,5 @@ -import { iconImageUrl } from '@clerk/shared/constants'; import { useUser } from '@clerk/shared/react'; -import type { - CustomOauthProvider, - EnterpriseAccountResource, - EnterpriseProvider, - OAuthProvider, - SamlIdpSlug, -} from '@clerk/types'; -import { getOAuthProviderData, SAML_IDPS } from '@clerk/types'; +import type { EnterpriseAccountResource, OAuthProvider } from '@clerk/types'; import { ProviderInitialIcon } from '../../common'; import { Badge, Box, descriptors, Flex, Image, localizationKeys, Text } from '../../customizables'; @@ -44,7 +36,7 @@ export const EnterpriseAccountsSection = () => { const EnterpriseAccount = ({ account }: { account: EnterpriseAccountResource }) => { const label = account.emailAddress; - const providerName = getEnterpriseAccountProviderName(account); + const connectionName = account?.enterpriseConnection?.name; const error = account.verification?.error?.longMessage; return ( @@ -66,7 +58,7 @@ const EnterpriseAccount = ({ account }: { account: EnterpriseAccountResource }) truncate colorScheme='body' > - {providerName} + {connectionName} { const { provider, enterpriseConnection } = account; - const providerName = getEnterpriseAccountProviderName(account) ?? provider; const providerWithoutPrefix = provider.replace('oauth_', '').trim() as OAuthProvider; + const connectionName = enterpriseConnection?.name ?? providerWithoutPrefix; - if (isOAuthCustomProvider(provider)) { - return enterpriseConnection?.logoPublicUrl ? ( - {providerName} ({ width: theme.sizes.$4 })} - /> - ) : ( - - ); - } - - const src = iconImageUrl(isSamlProvider(provider) ? SAML_IDPS[provider].logo : providerWithoutPrefix); - - return ( + return enterpriseConnection?.logoPublicUrl ? ( {providerName} ({ width: theme.sizes.$4 })} /> + ) : ( + ); }; - -const getEnterpriseAccountProviderName = ({ provider, enterpriseConnection }: EnterpriseAccountResource) => { - if (isSamlProvider(provider)) { - return SAML_IDPS[provider]?.name; - } - - if (isOAuthCustomProvider(provider)) { - return enterpriseConnection?.name; - } - - return getOAuthProviderData({ strategy: provider })?.name; -}; - -const isSamlProvider = (provider: EnterpriseProvider): provider is SamlIdpSlug => provider.includes('saml'); - -const isOAuthCustomProvider = (provider: EnterpriseProvider): provider is `oauth_${CustomOauthProvider}` => - provider.includes('custom'); diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx index 602b01f1108..c22ffc06372 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/AccountPage.test.tsx @@ -108,7 +108,7 @@ describe('AccountPage', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: true, allow_idp_initiated: false, @@ -173,7 +173,7 @@ describe('AccountPage', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: false, allow_idp_initiated: false, @@ -233,7 +233,7 @@ describe('AccountPage', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: true, allow_idp_initiated: false, diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx index 8db29e02afa..0191ebafbd0 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx @@ -33,7 +33,7 @@ const withInactiveEnterpriseConnection = createFixtures.config(f => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: false, allow_idp_initiated: false, @@ -43,7 +43,7 @@ const withInactiveEnterpriseConnection = createFixtures.config(f => { domain: 'foocorp.com', created_at: 123, updated_at: 123, - logo_public_url: null, + logo_public_url: 'https://img.clerk.com/static/okta.svg', protocol: 'saml', }, verification: { @@ -82,7 +82,7 @@ const withOAuthBuiltInEnterpriseConnection = createFixtures.config(f => { enterprise_connection: { object: 'enterprise_connection', provider: 'oauth_google', - name: 'FooCorp', + name: 'Google', id: 'ent_123', active: true, allow_idp_initiated: false, @@ -92,7 +92,7 @@ const withOAuthBuiltInEnterpriseConnection = createFixtures.config(f => { domain: 'foocorp.com', created_at: 123, updated_at: 123, - logo_public_url: null, + logo_public_url: 'https://img.clerk.com/static/google.svg', protocol: 'oauth', }, verification: { @@ -181,7 +181,7 @@ const withSamlEnterpriseConnection = createFixtures.config(f => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: true, allow_idp_initiated: false, @@ -191,7 +191,7 @@ const withSamlEnterpriseConnection = createFixtures.config(f => { domain: 'foocorp.com', created_at: 123, updated_at: 123, - logo_public_url: null, + logo_public_url: 'https://img.clerk.com/static/okta.svg', protocol: 'saml', }, verification: { diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx index 817985b66a9..a89d1f76bd0 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordSection.test.tsx @@ -129,7 +129,7 @@ describe('PasswordSection', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: true, allow_idp_initiated: false, @@ -202,7 +202,7 @@ describe('PasswordSection', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: false, allow_idp_initiated: false, @@ -339,7 +339,7 @@ describe('PasswordSection', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: true, allow_idp_initiated: false, @@ -413,7 +413,7 @@ describe('PasswordSection', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: false, allow_idp_initiated: false, diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx index 4c3a6d72bd6..75e076ab97f 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfileSection.test.tsx @@ -79,7 +79,7 @@ describe('ProfileSection', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: true, allow_idp_initiated: false, @@ -154,7 +154,7 @@ describe('ProfileSection', () => { enterprise_connection: { object: 'enterprise_connection', provider: 'saml_okta', - name: 'FooCorp', + name: 'Okta Workforce', id: 'ent_123', active: false, allow_idp_initiated: false, From e71267b1c71288c39be2dc6e281ad6c40be8a096 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:54:48 -0300 Subject: [PATCH 21/21] Use CDN for provider logo --- .../UserProfile/EnterpriseAccountsSection.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx index 15484320153..674e402d603 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EnterpriseAccountsSection.tsx @@ -1,3 +1,4 @@ +import { iconImageUrl } from '@clerk/shared/constants'; import { useUser } from '@clerk/shared/react'; import type { EnterpriseAccountResource, OAuthProvider } from '@clerk/types'; @@ -82,16 +83,30 @@ const EnterpriseAccount = ({ account }: { account: EnterpriseAccountResource }) const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccountResource }) => { const { provider, enterpriseConnection } = account; - const providerWithoutPrefix = provider.replace('oauth_', '').trim() as OAuthProvider; + const isCustomOAuthProvider = provider.startsWith('oauth_custom_'); + const providerWithoutPrefix = provider.replace(/(oauth_|saml_)/, '').trim() as OAuthProvider; const connectionName = enterpriseConnection?.name ?? providerWithoutPrefix; + const commonImageProps = { + elementDescriptor: [descriptors.providerIcon], + alt: connectionName, + sx: (theme: any) => ({ width: theme.sizes.$4 }), + elementId: descriptors.enterpriseButtonsProviderIcon.setId(account.provider), + }; + + if (!isCustomOAuthProvider) { + return ( + + ); + } + return enterpriseConnection?.logoPublicUrl ? ( {connectionName} ({ width: theme.sizes.$4 })} /> ) : (