diff --git a/.changeset/dirty-lizards-count.md b/.changeset/dirty-lizards-count.md new file mode 100644 index 00000000000..a999cf46882 --- /dev/null +++ b/.changeset/dirty-lizards-count.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': minor +--- + +Improve the resilience of the SDK against situations where the /v1/environment endpoint is not reachable. This is achieved by allowing the initialization of the environment with default values. diff --git a/integration/tests/reverification.test.ts b/integration/tests/reverification.test.ts index 749b0a67a9b..6807df716a7 100644 --- a/integration/tests/reverification.test.ts +++ b/integration/tests/reverification.test.ts @@ -160,7 +160,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withReverification] })( fictionalEmail: true, withPhoneNumber: true, }); - const bapiFakeUser = await u.services.users.createBapiUser({ + await u.services.users.createBapiUser({ ...delFakeUser, username: undefined, phoneNumber: undefined, diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index c885f42820a..2c56eb95669 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,7 +1,7 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "577kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "78kB" }, + { "path": "./dist/clerk.js", "maxSize": "577.5kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "78.5kB" }, { "path": "./dist/clerk.headless.js", "maxSize": "51KB" }, { "path": "./dist/ui-common*.js", "maxSize": "94KB" }, { "path": "./dist/vendors*.js", "maxSize": "30KB" }, @@ -12,7 +12,7 @@ { "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" }, { "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" }, { "path": "./dist/signin*.js", "maxSize": "12.4KB" }, - { "path": "./dist/signup*.js", "maxSize": "6.5KB" }, + { "path": "./dist/signup*.js", "maxSize": "6.55KB" }, { "path": "./dist/userbutton*.js", "maxSize": "5KB" }, { "path": "./dist/userprofile*.js", "maxSize": "15KB" }, { "path": "./dist/userverification*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 86fe2155823..58ee4533f35 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -326,9 +326,6 @@ export class Clerk implements ClerkInterface { public constructor(key: string, options?: DomainOrProxyUrl) { key = (key || '').trim(); - this.#domain = options?.domain; - this.#proxyUrl = options?.proxyUrl; - if (!key) { return errorThrower.throwMissingPublishableKeyError(); } @@ -339,8 +336,11 @@ export class Clerk implements ClerkInterface { return errorThrower.throwInvalidPublishableKeyError({ key }); } - this.#publishableKey = key; + this.#domain = options?.domain; + this.#proxyUrl = options?.proxyUrl; + this.environment = Environment.getInstance(); this.#instanceType = publishableKey.instanceType; + this.#publishableKey = key; this.#fapiClient = createFapiClient({ domain: this.domain, @@ -2085,16 +2085,20 @@ export class Clerk implements ClerkInterface { } }; - await Promise.all([initEnvironmentPromise, initClient()]).catch(async e => { - // limit the changes for this specific error for now + const [envResult, clientResult] = await Promise.allSettled([initEnvironmentPromise, initClient()]); + if (clientResult.status === 'rejected') { + const e = clientResult.reason; + if (isClerkAPIResponseError(e) && e.errors[0].code === 'requires_captcha') { - await initEnvironmentPromise; + if (envResult.status === 'rejected') { + await initEnvironmentPromise; + } initComponents(); await initClient(); } else { throw e; } - }); + } this.#authService?.setClientUatCookieForDevelopmentInstances(); diff --git a/packages/clerk-js/src/core/resources/AuthConfig.ts b/packages/clerk-js/src/core/resources/AuthConfig.ts index 7de2c33b628..2b07dcb61ce 100644 --- a/packages/clerk-js/src/core/resources/AuthConfig.ts +++ b/packages/clerk-js/src/core/resources/AuthConfig.ts @@ -4,29 +4,33 @@ import { unixEpochToDate } from '../../utils/date'; import { BaseResource } from './internal'; export class AuthConfig extends BaseResource implements AuthConfigResource { - singleSessionMode!: boolean; claimedAt: Date | null = null; - reverification!: boolean; + reverification: boolean = false; + singleSessionMode: boolean = false; - public constructor(data: AuthConfigJSON) { + public constructor(data: Partial | null = null) { super(); + this.fromJSON(data); } - protected fromJSON(data: AuthConfigJSON | null): this { - this.singleSessionMode = data ? data.single_session_mode : true; - this.claimedAt = data?.claimed_at ? unixEpochToDate(data.claimed_at) : null; - this.reverification = data ? data.reverification : true; + protected fromJSON(data: Partial | null): this { + if (!data) { + return this; + } + this.claimedAt = this.withDefault(data.claimed_at ? unixEpochToDate(data.claimed_at) : null, this.claimedAt); + this.reverification = this.withDefault(data.reverification, this.reverification); + this.singleSessionMode = this.withDefault(data.single_session_mode, this.singleSessionMode); return this; } public __internal_toSnapshot(): AuthConfigJSONSnapshot { return { - object: 'auth_config', - id: this.id || '', - single_session_mode: this.singleSessionMode, claimed_at: this.claimedAt ? this.claimedAt.getTime() : null, + id: this.id ?? '', + object: 'auth_config', reverification: this.reverification, + single_session_mode: this.singleSessionMode, }; } } diff --git a/packages/clerk-js/src/core/resources/Base.ts b/packages/clerk-js/src/core/resources/Base.ts index 3db191e9e74..0e35e23bd86 100644 --- a/packages/clerk-js/src/core/resources/Base.ts +++ b/packages/clerk-js/src/core/resources/Base.ts @@ -166,6 +166,18 @@ export abstract class BaseResource { protected abstract fromJSON(data: ClerkResourceJSON | null): this; + /** + * Returns the provided value if it is not `undefined` or `null`, otherwise returns the default value. + * + * @template T - The type of the value. + * @param value - The value to check. + * @param defaultValue - The default value to return if the provided value is `undefined` or `null`. + * @returns The provided value if it is not `undefined` or `null`, otherwise the default value. + */ + protected withDefault(value: T | undefined | null, defaultValue: T): T { + return value ?? defaultValue; + } + protected async _baseGet(opts: BaseFetchOptions = {}): Promise { const json = await BaseResource._fetch( { diff --git a/packages/clerk-js/src/core/resources/CommerceSettings.ts b/packages/clerk-js/src/core/resources/CommerceSettings.ts index 5c0e0db1928..3bee52e021f 100644 --- a/packages/clerk-js/src/core/resources/CommerceSettings.ts +++ b/packages/clerk-js/src/core/resources/CommerceSettings.ts @@ -10,9 +10,11 @@ import { BaseResource } from './internal'; * @internal */ export class __experimental_CommerceSettings extends BaseResource implements __experimental_CommerceSettingsResource { - stripePublishableKey!: string; + stripePublishableKey: string = ''; - public constructor(data: __experimental_CommerceSettingsJSON | __experimental_CommerceSettingsJSONSnapshot) { + public constructor( + data: __experimental_CommerceSettingsJSON | __experimental_CommerceSettingsJSONSnapshot | null = null, + ) { super(); this.fromJSON(data); } diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index c9fb68c4a89..453a2bf90de 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -12,48 +12,49 @@ import type { import { BaseResource } from './internal'; export class DisplayConfig extends BaseResource implements DisplayConfigResource { - id!: string; - afterSignInUrl!: string; - afterSignOutAllUrl!: string; - afterSignOutOneUrl!: string; - afterSignOutUrl!: string; - afterSignUpUrl!: string; - afterSwitchSessionUrl!: string; - applicationName!: string; - backendHost!: string; - branded!: boolean; - captchaPublicKey: string | null = null; - captchaWidgetType: CaptchaWidgetType = null; + afterCreateOrganizationUrl: string = ''; + afterJoinWaitlistUrl: string = ''; + afterLeaveOrganizationUrl: string = ''; + afterSignInUrl: string = ''; + afterSignOutAllUrl: string = ''; + afterSignOutOneUrl: string = ''; + afterSignOutUrl: string = ''; + afterSignUpUrl: string = ''; + afterSwitchSessionUrl: string = ''; + applicationName: string = ''; + backendHost: string = ''; + branded: boolean = false; + captchaHeartbeat: boolean = false; + captchaHeartbeatIntervalMs?: number; + captchaOauthBypass: OAuthStrategy[] = ['oauth_google', 'oauth_microsoft', 'oauth_apple']; captchaProvider: CaptchaProvider = 'turnstile'; + captchaPublicKey: string | null = null; captchaPublicKeyInvisible: string | null = null; - captchaOauthBypass: OAuthStrategy[] = []; - captchaHeartbeat: boolean = false; - captchaHeartbeatIntervalMs?: number = undefined; - homeUrl!: string; - instanceEnvironmentType!: string; - faviconImageUrl!: string; - logoImageUrl!: string; - preferredSignInStrategy!: PreferredSignInStrategy; - signInUrl!: string; - signUpUrl!: string; - supportEmail!: string; - theme!: DisplayThemeJSON; - userProfileUrl!: string; + captchaWidgetType: CaptchaWidgetType = null; clerkJSVersion?: string; + createOrganizationUrl: string = ''; experimental__forceOauthFirst?: boolean; - organizationProfileUrl!: string; - createOrganizationUrl!: string; - afterLeaveOrganizationUrl!: string; - afterCreateOrganizationUrl!: string; + faviconImageUrl: string = ''; googleOneTapClientId?: string; - showDevModeWarning!: boolean; - termsUrl!: string; - privacyPolicyUrl!: string; - waitlistUrl!: string; - afterJoinWaitlistUrl!: string; + homeUrl: string = ''; + id: string = ''; + instanceEnvironmentType: string = ''; + logoImageUrl: string = ''; + organizationProfileUrl: string = ''; + preferredSignInStrategy: PreferredSignInStrategy = 'password'; + privacyPolicyUrl: string = ''; + showDevModeWarning: boolean = false; + signInUrl: string = ''; + signUpUrl: string = ''; + supportEmail: string = ''; + termsUrl: string = ''; + theme: DisplayThemeJSON = {} as DisplayThemeJSON; + userProfileUrl: string = ''; + waitlistUrl: string = ''; - public constructor(data: DisplayConfigJSON | DisplayConfigJSONSnapshot) { + public constructor(data: DisplayConfigJSON | DisplayConfigJSONSnapshot | null = null) { super(); + this.fromJSON(data); } @@ -62,86 +63,97 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource return this; } - this.id = data.id; - this.instanceEnvironmentType = data.instance_environment_type; - this.applicationName = data.application_name; - this.theme = data.theme; - this.preferredSignInStrategy = data.preferred_sign_in_strategy; - this.logoImageUrl = data.logo_image_url; - this.faviconImageUrl = data.favicon_image_url; - this.homeUrl = data.home_url; - this.signInUrl = data.sign_in_url; - this.signUpUrl = data.sign_up_url; - this.userProfileUrl = data.user_profile_url; - this.afterSignInUrl = data.after_sign_in_url; - this.afterSignUpUrl = data.after_sign_up_url; - this.afterSignOutOneUrl = data.after_sign_out_one_url; - this.afterSignOutAllUrl = data.after_sign_out_all_url; - this.afterSwitchSessionUrl = data.after_switch_session_url; - this.branded = data.branded; - this.captchaPublicKey = data.captcha_public_key; - this.captchaWidgetType = data.captcha_widget_type; - this.captchaProvider = data.captcha_provider; - this.captchaPublicKeyInvisible = data.captcha_public_key_invisible; - // These are the OAuth strategies we used to bypass the captcha for by default - // before the introduction of the captcha_oauth_bypass field - this.captchaOauthBypass = data.captcha_oauth_bypass || ['oauth_google', 'oauth_microsoft', 'oauth_apple']; - this.captchaHeartbeat = data.captcha_heartbeat || false; - this.captchaHeartbeatIntervalMs = data.captcha_heartbeat_interval_ms; - this.supportEmail = data.support_email || ''; - this.clerkJSVersion = data.clerk_js_version; - this.organizationProfileUrl = data.organization_profile_url; - this.createOrganizationUrl = data.create_organization_url; - this.afterLeaveOrganizationUrl = data.after_leave_organization_url; - this.afterCreateOrganizationUrl = data.after_create_organization_url; - this.googleOneTapClientId = data.google_one_tap_client_id; - this.showDevModeWarning = data.show_devmode_warning; - this.termsUrl = data.terms_url; - this.privacyPolicyUrl = data.privacy_policy_url; - this.waitlistUrl = data.waitlist_url; - this.afterJoinWaitlistUrl = data.after_join_waitlist_url; + this.afterCreateOrganizationUrl = this.withDefault( + data.after_create_organization_url, + this.afterCreateOrganizationUrl, + ); + this.afterJoinWaitlistUrl = this.withDefault(data.after_join_waitlist_url, this.afterJoinWaitlistUrl); + this.afterLeaveOrganizationUrl = this.withDefault( + data.after_leave_organization_url, + this.afterLeaveOrganizationUrl, + ); + this.afterSignInUrl = this.withDefault(data.after_sign_in_url, this.afterSignInUrl); + this.afterSignOutAllUrl = this.withDefault(data.after_sign_out_all_url, this.afterSignOutAllUrl); + this.afterSignOutOneUrl = this.withDefault(data.after_sign_out_one_url, this.afterSignOutOneUrl); + this.afterSignUpUrl = this.withDefault(data.after_sign_up_url, this.afterSignUpUrl); + this.afterSwitchSessionUrl = this.withDefault(data.after_switch_session_url, this.afterSwitchSessionUrl); + this.applicationName = this.withDefault(data.application_name, this.applicationName); + this.branded = this.withDefault(data.branded, this.branded); + this.captchaHeartbeat = this.withDefault(data.captcha_heartbeat, this.captchaHeartbeat); + this.captchaHeartbeatIntervalMs = this.withDefault( + data.captcha_heartbeat_interval_ms, + this.captchaHeartbeatIntervalMs, + ); + this.captchaOauthBypass = this.withDefault(data.captcha_oauth_bypass, this.captchaOauthBypass); + this.captchaProvider = this.withDefault(data.captcha_provider, this.captchaProvider); + this.captchaPublicKey = this.withDefault(data.captcha_public_key, this.captchaPublicKey); + this.captchaPublicKeyInvisible = this.withDefault( + data.captcha_public_key_invisible, + this.captchaPublicKeyInvisible, + ); + this.captchaWidgetType = this.withDefault(data.captcha_widget_type, this.captchaWidgetType); + this.clerkJSVersion = this.withDefault(data.clerk_js_version, this.clerkJSVersion); + this.createOrganizationUrl = this.withDefault(data.create_organization_url, this.createOrganizationUrl); + this.faviconImageUrl = this.withDefault(data.favicon_image_url, this.faviconImageUrl); + this.googleOneTapClientId = this.withDefault(data.google_one_tap_client_id, this.googleOneTapClientId); + this.homeUrl = this.withDefault(data.home_url, this.homeUrl); + this.id = this.withDefault(data.id, this.id); + this.instanceEnvironmentType = this.withDefault(data.instance_environment_type, this.instanceEnvironmentType); + this.logoImageUrl = this.withDefault(data.logo_image_url, this.logoImageUrl); + this.organizationProfileUrl = this.withDefault(data.organization_profile_url, this.organizationProfileUrl); + this.preferredSignInStrategy = this.withDefault(data.preferred_sign_in_strategy, this.preferredSignInStrategy); + this.privacyPolicyUrl = this.withDefault(data.privacy_policy_url, this.privacyPolicyUrl); + this.showDevModeWarning = this.withDefault(data.show_devmode_warning, this.showDevModeWarning); + this.signInUrl = this.withDefault(data.sign_in_url, this.signInUrl); + this.signUpUrl = this.withDefault(data.sign_up_url, this.signUpUrl); + this.supportEmail = this.withDefault(data.support_email, this.supportEmail); + this.termsUrl = this.withDefault(data.terms_url, this.termsUrl); + this.theme = this.withDefault(data.theme, this.theme); + this.userProfileUrl = this.withDefault(data.user_profile_url, this.userProfileUrl); + this.waitlistUrl = this.withDefault(data.waitlist_url, this.waitlistUrl); + return this; } public __internal_toSnapshot(): DisplayConfigJSONSnapshot { return { object: 'display_config', - id: this.id, - instance_environment_type: this.instanceEnvironmentType, - application_name: this.applicationName, - theme: this.theme, - preferred_sign_in_strategy: this.preferredSignInStrategy, - logo_image_url: this.logoImageUrl, - favicon_image_url: this.faviconImageUrl, - home_url: this.homeUrl, - sign_in_url: this.signInUrl, - sign_up_url: this.signUpUrl, - user_profile_url: this.userProfileUrl, + after_create_organization_url: this.afterCreateOrganizationUrl, + after_join_waitlist_url: this.afterJoinWaitlistUrl, + after_leave_organization_url: this.afterLeaveOrganizationUrl, after_sign_in_url: this.afterSignInUrl, - after_sign_up_url: this.afterSignUpUrl, - after_sign_out_one_url: this.afterSignOutOneUrl, after_sign_out_all_url: this.afterSignOutAllUrl, + after_sign_out_one_url: this.afterSignOutOneUrl, + after_sign_up_url: this.afterSignUpUrl, after_switch_session_url: this.afterSwitchSessionUrl, + application_name: this.applicationName, branded: this.branded, - captcha_public_key: this.captchaPublicKey, - captcha_widget_type: this.captchaWidgetType, + captcha_heartbeat_interval_ms: this.captchaHeartbeatIntervalMs, + captcha_heartbeat: this.captchaHeartbeat, + captcha_oauth_bypass: this.captchaOauthBypass, captcha_provider: this.captchaProvider, captcha_public_key_invisible: this.captchaPublicKeyInvisible, - captcha_oauth_bypass: this.captchaOauthBypass, - captcha_heartbeat: this.captchaHeartbeat, - captcha_heartbeat_interval_ms: this.captchaHeartbeatIntervalMs, - support_email: this.supportEmail, + captcha_public_key: this.captchaPublicKey, + captcha_widget_type: this.captchaWidgetType, clerk_js_version: this.clerkJSVersion, - organization_profile_url: this.organizationProfileUrl, create_organization_url: this.createOrganizationUrl, - after_leave_organization_url: this.afterLeaveOrganizationUrl, - after_create_organization_url: this.afterCreateOrganizationUrl, + favicon_image_url: this.faviconImageUrl, google_one_tap_client_id: this.googleOneTapClientId, + home_url: this.homeUrl, + id: this.id, + instance_environment_type: this.instanceEnvironmentType, + logo_image_url: this.logoImageUrl, + organization_profile_url: this.organizationProfileUrl, + preferred_sign_in_strategy: this.preferredSignInStrategy, + privacy_policy_url: this.privacyPolicyUrl, show_devmode_warning: this.showDevModeWarning, + sign_in_url: this.signInUrl, + sign_up_url: this.signUpUrl, + support_email: this.supportEmail, terms_url: this.termsUrl, - privacy_policy_url: this.privacyPolicyUrl, + theme: this.theme, + user_profile_url: this.userProfileUrl, waitlist_url: this.waitlistUrl, - after_join_waitlist_url: this.afterJoinWaitlistUrl, }; } } diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index ae033e5ecb1..f589088a749 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -15,13 +15,13 @@ import { OrganizationSettings } from './OrganizationSettings'; export class Environment extends BaseResource implements EnvironmentResource { private static instance: Environment; + authConfig: AuthConfigResource = new AuthConfig(); + displayConfig: DisplayConfigResource = new DisplayConfig(); + maintenanceMode: boolean = false; pathRoot = '/environment'; - authConfig!: AuthConfigResource; - displayConfig!: DisplayConfigResource; - userSettings!: UserSettingsResource; - organizationSettings!: OrganizationSettingsResource; - __experimental_commerceSettings!: __experimental_CommerceSettingsResource; - maintenanceMode!: boolean; + userSettings: UserSettingsResource = new UserSettings(); + organizationSettings: OrganizationSettingsResource = new OrganizationSettings(); + __experimental_commerceSettings: __experimental_CommerceSettingsResource = new __experimental_CommerceSettings(); public static getInstance(): Environment { if (!Environment.instance) { @@ -33,54 +33,55 @@ export class Environment extends BaseResource implements EnvironmentResource { constructor(data: EnvironmentJSON | EnvironmentJSONSnapshot | null = null) { super(); + this.fromJSON(data); } - fetch({ touch, fetchMaxTries }: { touch: boolean; fetchMaxTries?: number } = { touch: false }): Promise { - if (touch) { - return this._basePatch({}); + protected fromJSON(data: EnvironmentJSONSnapshot | EnvironmentJSON | null): this { + if (!data) { + return this; } - return this._baseGet({ fetchMaxTries }); + + this.authConfig = new AuthConfig(data.auth_config); + this.displayConfig = new DisplayConfig(data.display_config); + this.maintenanceMode = this.withDefault(data.maintenance_mode, this.maintenanceMode); + this.organizationSettings = new OrganizationSettings(data.organization_settings); + this.userSettings = new UserSettings(data.user_settings); + this.__experimental_commerceSettings = new __experimental_CommerceSettings(data.commerce_settings); + + return this; } - isSingleSession = (): boolean => { - return this.authConfig.singleSessionMode; + fetch({ touch, fetchMaxTries }: { touch: boolean; fetchMaxTries?: number } = { touch: false }): Promise { + return touch ? this._basePatch({}) : this._baseGet({ fetchMaxTries }); + } + + isDevelopmentOrStaging = (): boolean => { + return !this.isProduction(); }; isProduction = (): boolean => { return this.displayConfig.instanceEnvironmentType === 'production'; }; - isDevelopmentOrStaging = (): boolean => { - return !this.isProduction(); + isSingleSession = (): boolean => { + return this.authConfig.singleSessionMode; }; onWindowLocationHost = (): boolean => { return this.displayConfig.backendHost === window.location.host; }; - protected fromJSON(data: EnvironmentJSONSnapshot | EnvironmentJSON | null): this { - if (data) { - this.authConfig = new AuthConfig(data.auth_config); - this.__experimental_commerceSettings = new __experimental_CommerceSettings(data.commerce_settings); - this.displayConfig = new DisplayConfig(data.display_config); - this.userSettings = new UserSettings(data.user_settings); - this.organizationSettings = new OrganizationSettings(data.organization_settings); - this.maintenanceMode = data.maintenance_mode; - } - return this; - } - public __internal_toSnapshot(): EnvironmentJSONSnapshot { return { object: 'environment', - id: this.id || '', auth_config: this.authConfig.__internal_toSnapshot(), display_config: this.displayConfig.__internal_toSnapshot(), - user_settings: this.userSettings.__internal_toSnapshot(), + id: this.id ?? '', + maintenance_mode: this.maintenanceMode, organization_settings: this.organizationSettings.__internal_toSnapshot(), + user_settings: this.userSettings.__internal_toSnapshot(), commerce_settings: this.__experimental_commerceSettings.__internal_toSnapshot(), - maintenance_mode: this.maintenanceMode, }; } } diff --git a/packages/clerk-js/src/core/resources/OrganizationSettings.ts b/packages/clerk-js/src/core/resources/OrganizationSettings.ts index 183f8e55b45..ea49f111865 100644 --- a/packages/clerk-js/src/core/resources/OrganizationSettings.ts +++ b/packages/clerk-js/src/core/resources/OrganizationSettings.ts @@ -8,39 +8,47 @@ import type { import { BaseResource } from './internal'; export class OrganizationSettings extends BaseResource implements OrganizationSettingsResource { - enabled!: boolean; - maxAllowedMemberships!: number; - actions!: { - adminDelete: boolean; - }; - domains!: { + actions: { adminDelete: boolean } = { adminDelete: false }; + domains: { enabled: boolean; enrollmentModes: OrganizationEnrollmentMode[]; defaultRole: string | null; + } = { + enabled: false, + enrollmentModes: [], + defaultRole: null, }; + enabled: boolean = false; + maxAllowedMemberships: number = 1; - public constructor(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot) { + public constructor(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot | null = null) { super(); this.fromJSON(data); } protected fromJSON(data: OrganizationSettingsJSON | OrganizationSettingsJSONSnapshot | null): this { - const { enabled = false, max_allowed_memberships = 0, actions, domains } = data || {}; - this.enabled = enabled; - this.maxAllowedMemberships = max_allowed_memberships; - this.actions = { adminDelete: actions?.admin_delete || false }; - this.domains = { - enabled: domains?.enabled || false, - enrollmentModes: domains?.enrollment_modes || [], - defaultRole: domains?.default_role || null, - }; + if (!data) { + return this; + } + + if (data.actions) { + this.actions.adminDelete = this.withDefault(data.actions.admin_delete, this.actions.adminDelete); + } + + if (data.domains) { + this.domains.enabled = this.withDefault(data.domains.enabled, this.domains.enabled); + this.domains.enrollmentModes = this.withDefault(data.domains.enrollment_modes, this.domains.enrollmentModes); + this.domains.defaultRole = this.withDefault(data.domains.default_role, this.domains.defaultRole); + } + + this.enabled = this.withDefault(data.enabled, this.enabled); + this.maxAllowedMemberships = this.withDefault(data.max_allowed_memberships, this.maxAllowedMemberships); + return this; } public __internal_toSnapshot(): OrganizationSettingsJSONSnapshot { return { - enabled: this.enabled, - max_allowed_memberships: this.maxAllowedMemberships, actions: { admin_delete: this.actions.adminDelete, }, @@ -49,6 +57,8 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe enrollment_modes: this.domains.enrollmentModes, default_role: this.domains.defaultRole, }, + enabled: this.enabled, + max_allowed_memberships: this.maxAllowedMemberships, } as unknown as OrganizationSettingsJSONSnapshot; } } diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index 15f9abb9c3e..152a5e28233 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -28,43 +28,165 @@ export type Actions = { delete_self: boolean; }; +const DISABLED_ATTRIBUTE = { + enabled: false, + first_factors: [], + name: 'phone_number', + required: false, + second_factors: [], + used_for_first_factor: false, + used_for_second_factor: false, + verifications: [], + verify_at_sign_up: false, +}; + /** * @internal */ export class UserSettings extends BaseResource implements UserSettingsResource { id = undefined; - social!: OAuthProviders; - saml!: SamlSettings; - enterpriseSSO!: EnterpriseSSOSettings; + actions: Actions = { create_organization: false, delete_self: false }; + attributes: Attributes = { + email_address: { + enabled: true, + first_factors: ['email_code'], + name: 'email_address', + required: true, + second_factors: [], + used_for_first_factor: true, + used_for_second_factor: false, + verifications: ['email_code'], + verify_at_sign_up: true, + }, + phone_number: { + ...DISABLED_ATTRIBUTE, + name: 'phone_number', + }, + username: { + ...DISABLED_ATTRIBUTE, + name: 'username', + }, + web3_wallet: { + ...DISABLED_ATTRIBUTE, + name: 'web3_wallet', + }, + first_name: { + ...DISABLED_ATTRIBUTE, + name: 'first_name', + }, + last_name: { + ...DISABLED_ATTRIBUTE, + name: 'last_name', + }, + password: { + enabled: true, + first_factors: [], + name: 'password', + required: true, + second_factors: [], + used_for_first_factor: false, + used_for_second_factor: false, + verifications: [], + verify_at_sign_up: false, + }, + authenticator_app: { + ...DISABLED_ATTRIBUTE, + name: 'authenticator_app', + }, + backup_code: { + ...DISABLED_ATTRIBUTE, + name: 'backup_code', + }, + passkey: { + ...DISABLED_ATTRIBUTE, + name: 'passkey', + }, + }; + enterpriseSSO: EnterpriseSSOSettings = { + enabled: false, + }; + passkeySettings: PasskeySettingsData = { + allow_autofill: false, + show_sign_in_button: false, + }; + passwordSettings: PasswordSettingsData = {} as PasswordSettingsData; + saml: SamlSettings = { + enabled: false, + }; + signIn: SignInData = { + second_factor: { + required: false, + enabled: false, + }, + }; + signUp: SignUpData = { + allowlist_only: false, + captcha_enabled: false, + legal_consent_enabled: false, + mode: 'public', + progressive: true, + }; + social: OAuthProviders = {} as OAuthProviders; + usernameSettings: UsernameSettingsData = {} as UsernameSettingsData; + + get authenticatableSocialStrategies(): OAuthStrategy[] { + if (!this.social) { + return []; + } + + return Object.entries(this.social) + .filter(([, desc]) => desc.enabled && desc.authenticatable) + .map(([, desc]) => desc.strategy) + .sort(); + } + + get enabledFirstFactorIdentifiers(): Array { + if (!this.attributes) { + return []; + } + + return Object.entries(this.attributes) + .filter(([name, attr]) => attr.used_for_first_factor && !name.startsWith('web3')) + .map(([name]) => name) as Array; + } + + get socialProviderStrategies(): OAuthStrategy[] { + if (!this.social) { + return []; + } + + return Object.entries(this.social) + .filter(([, desc]) => desc.enabled) + .map(([, desc]) => desc.strategy) + .sort(); + } - attributes!: Attributes; - actions!: Actions; - signIn!: SignInData; - signUp!: SignUpData; - passwordSettings!: PasswordSettingsData; - passkeySettings!: PasskeySettingsData; - usernameSettings!: UsernameSettingsData; + get web3FirstFactors(): Web3Strategy[] { + if (!this.attributes) { + return []; + } - socialProviderStrategies: OAuthStrategy[] = []; - authenticatableSocialStrategies: OAuthStrategy[] = []; - web3FirstFactors: Web3Strategy[] = []; - enabledFirstFactorIdentifiers: Array = []; + return Object.entries(this.attributes) + .filter(([name, attr]) => attr.used_for_first_factor && name.startsWith('web3')) + .map(([, desc]) => desc.first_factors) + .flat() as any as Web3Strategy[]; + } - public constructor(data: UserSettingsJSON | UserSettingsJSONSnapshot) { + public constructor(data: UserSettingsJSON | UserSettingsJSONSnapshot | null = null) { super(); this.fromJSON(data); } get instanceIsPasswordBased() { - return this.attributes.password.enabled && this.attributes.password.required; + return Boolean(this.attributes?.password?.enabled && this.attributes.password?.required); } get hasValidAuthFactor() { - return ( - this.attributes.email_address.enabled || - this.attributes.phone_number.enabled || - (this.attributes.password.required && this.attributes.username.required) + return Boolean( + this.attributes?.email_address?.enabled || + this.attributes?.phone_number?.enabled || + (this.attributes.password?.required && this.attributes.username?.required), ); } @@ -73,90 +195,59 @@ export class UserSettings extends BaseResource implements UserSettingsResource { return this; } - this.social = data.social; - this.saml = data.saml; - this.enterpriseSSO = data.enterprise_sso; - this.attributes = Object.fromEntries( - Object.entries(data.attributes).map(a => [a[0], { ...a[1], name: a[0] }]), - ) as Attributes; - this.actions = data.actions; - this.signIn = data.sign_in; - this.signUp = data.sign_up; - this.passwordSettings = { - ...data.password_settings, - min_length: Math.max(data?.password_settings?.min_length, defaultMinPasswordLength), - max_length: - data?.password_settings?.max_length === 0 - ? defaultMaxPasswordLength - : Math.min(data?.password_settings?.max_length, defaultMaxPasswordLength), - }; - this.usernameSettings = { - ...data.username_settings, - min_length: Math.max(data?.username_settings?.min_length, defaultMinUsernameLength), - max_length: Math.min(data?.username_settings?.max_length, defaultMaxUsernameLength), - }; - this.passkeySettings = data.passkey_settings; - this.socialProviderStrategies = this.getSocialProviderStrategies(data.social); - this.authenticatableSocialStrategies = this.getAuthenticatableSocialStrategies(data.social); - this.web3FirstFactors = this.getWeb3FirstFactors(this.attributes); - this.enabledFirstFactorIdentifiers = this.getEnabledFirstFactorIdentifiers(this.attributes); + this.attributes = this.withDefault( + data.attributes + ? (Object.fromEntries(Object.entries(data.attributes).map(a => [a[0], { ...a[1], name: a[0] }])) as Attributes) + : null, + this.attributes, + ); + this.actions = this.withDefault(data.actions, this.actions); + this.enterpriseSSO = this.withDefault(data.enterprise_sso, this.enterpriseSSO); + this.passkeySettings = this.withDefault(data.passkey_settings, this.passkeySettings); + this.passwordSettings = data.password_settings + ? { + ...data.password_settings, + min_length: Math.max( + data.password_settings?.min_length ?? defaultMinPasswordLength, + defaultMinPasswordLength, + ), + max_length: + data.password_settings?.max_length === 0 + ? defaultMaxPasswordLength + : Math.min(data.password_settings?.max_length ?? defaultMaxPasswordLength, defaultMaxPasswordLength), + } + : this.passwordSettings; + this.saml = this.withDefault(data.saml, this.saml); + this.signIn = this.withDefault(data.sign_in, this.signIn); + this.signUp = this.withDefault(data.sign_up, this.signUp); + this.social = this.withDefault(data.social, this.social); + this.usernameSettings = data.username_settings + ? { + ...data.username_settings, + min_length: Math.max( + data.username_settings?.min_length ?? defaultMinUsernameLength, + defaultMinUsernameLength, + ), + max_length: Math.min( + data.username_settings?.max_length ?? defaultMaxUsernameLength, + defaultMaxUsernameLength, + ), + } + : this.usernameSettings; return this; } public __internal_toSnapshot(): UserSettingsJSONSnapshot { return { - social: this.social, - saml: this.saml, - attributes: this.attributes, actions: this.actions, + attributes: this.attributes, + passkey_settings: this.passkeySettings, + password_settings: this.passwordSettings, + saml: this.saml, sign_in: this.signIn, sign_up: this.signUp, - password_settings: this.passwordSettings, - passkey_settings: this.passkeySettings, + social: this.social, } as unknown as UserSettingsJSONSnapshot; } - - private getEnabledFirstFactorIdentifiers(attributes: Attributes): Array { - if (!attributes) { - return []; - } - - return Object.entries(attributes) - .filter(([name, attr]) => attr.used_for_first_factor && !name.startsWith('web3')) - .map(([name]) => name) as Array; - } - - private getWeb3FirstFactors(attributes: Attributes): Web3Strategy[] { - if (!attributes) { - return []; - } - - return Object.entries(attributes) - .filter(([name, attr]) => attr.used_for_first_factor && name.startsWith('web3')) - .map(([, desc]) => desc.first_factors) - .flat() as any as Web3Strategy[]; - } - - private getSocialProviderStrategies(social: OAuthProviders): OAuthStrategy[] { - if (!social) { - return []; - } - - return Object.entries(social) - .filter(([, desc]) => desc.enabled) - .map(([, desc]) => desc.strategy) - .sort(); - } - - private getAuthenticatableSocialStrategies(social: OAuthProviders): OAuthStrategy[] { - if (!social) { - return []; - } - - return Object.entries(social) - .filter(([, desc]) => desc.enabled && desc.authenticatable) - .map(([, desc]) => desc.strategy) - .sort(); - } } diff --git a/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts b/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts new file mode 100644 index 00000000000..95a50f77110 --- /dev/null +++ b/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts @@ -0,0 +1,49 @@ +import { unixEpochToDate } from '../../../utils/date'; +import { AuthConfig } from '../AuthConfig'; + +jest.mock('../../../utils/date', () => ({ + unixEpochToDate: jest.fn(timestamp => new Date(timestamp)), +})); + +describe('AuthConfig', () => { + it('initializes with default values', () => { + const authConfig = new AuthConfig(); + + expect(authConfig.claimedAt).toBeNull(); + expect(authConfig.reverification).toBe(false); + expect(authConfig.singleSessionMode).toBe(false); + }); + + it('initializes with provided values', () => { + const mockData = { + claimed_at: 1672531200000, + reverification: true, + single_session_mode: true, + }; + + const authConfig = new AuthConfig(mockData); + + expect(unixEpochToDate).toHaveBeenCalledWith(1672531200000); + expect(authConfig.claimedAt).toEqual(new Date(1672531200000)); + expect(authConfig.reverification).toBe(true); + expect(authConfig.singleSessionMode).toBe(true); + }); + + it('converts to JSON snapshot correctly', () => { + const authConfig = new AuthConfig({ + claimed_at: 1672531200000, + reverification: true, + single_session_mode: true, + }); + + const snapshot = authConfig.__internal_toSnapshot(); + + expect(snapshot).toEqual({ + object: 'auth_config', + claimed_at: 1672531200000, + id: '', + reverification: true, + single_session_mode: true, + }); + }); +}); diff --git a/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts b/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts index 813fb21be21..e1f996a7d04 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts @@ -3,6 +3,12 @@ import type { EnvironmentJSONSnapshot } from '@clerk/types'; import { Environment } from '../internal'; describe('Environment', () => { + it('defaults values when instantiated without arguments', () => { + const environment = new Environment(); + + expect(environment).toMatchSnapshot(); + }); + it('has the same initial properties', () => { const environmentJSON = { object: 'environment', diff --git a/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts b/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts index c8460fcf411..94ae59e76bf 100644 --- a/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts @@ -3,6 +3,10 @@ import type { UserSettingsJSON } from '@clerk/types'; import { UserSettings } from '../internal'; describe('UserSettings', () => { + it('defaults values when instantiated with no arguments', function () { + expect(new UserSettings()).toMatchSnapshot(); + }); + it('returns enabled web3 first factors', function () { const sut = new UserSettings({ attributes: { diff --git a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap index bee8b3b9ac2..1fe9cb48a80 100644 --- a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap +++ b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap @@ -10,7 +10,7 @@ exports[`Environment __internal_toSnapshot() 1`] = ` "single_session_mode": true, }, "commerce_settings": { - "stripe_publishable_key": undefined, + "stripe_publishable_key": "", }, "display_config": { "after_create_organization_url": "", @@ -26,14 +26,14 @@ exports[`Environment __internal_toSnapshot() 1`] = ` "captcha_heartbeat": false, "captcha_heartbeat_interval_ms": undefined, "captcha_oauth_bypass": [], - "captcha_provider": null, + "captcha_provider": "turnstile", "captcha_public_key": null, "captcha_public_key_invisible": null, "captcha_widget_type": null, "clerk_js_version": "5", "create_organization_url": "", "favicon_image_url": "", - "google_one_tap_client_id": null, + "google_one_tap_client_id": undefined, "home_url": "", "id": "display_config_DUMMY_ID", "instance_environment_type": "development", @@ -41,12 +41,12 @@ exports[`Environment __internal_toSnapshot() 1`] = ` "object": "display_config", "organization_profile_url": "", "preferred_sign_in_strategy": "password", - "privacy_policy_url": null, + "privacy_policy_url": "", "show_devmode_warning": true, "sign_in_url": "", "sign_up_url": "", "support_email": "", - "terms_url": null, + "terms_url": "", "theme": { "accounts": { "background_color": "#ffffff", @@ -268,10 +268,237 @@ exports[`Environment __internal_toSnapshot() 1`] = ` } `; +exports[`Environment defaults values when instantiated without arguments 1`] = ` +Environment { + "__experimental_commerceSettings": __experimental_CommerceSettings { + "pathRoot": "", + "stripePublishableKey": "", + }, + "authConfig": AuthConfig { + "claimedAt": null, + "pathRoot": "", + "reverification": false, + "singleSessionMode": false, + }, + "displayConfig": DisplayConfig { + "afterCreateOrganizationUrl": "", + "afterJoinWaitlistUrl": "", + "afterLeaveOrganizationUrl": "", + "afterSignInUrl": "", + "afterSignOutAllUrl": "", + "afterSignOutOneUrl": "", + "afterSignOutUrl": "", + "afterSignUpUrl": "", + "afterSwitchSessionUrl": "", + "applicationName": "", + "backendHost": "", + "branded": false, + "captchaHeartbeat": false, + "captchaOauthBypass": [ + "oauth_google", + "oauth_microsoft", + "oauth_apple", + ], + "captchaProvider": "turnstile", + "captchaPublicKey": null, + "captchaPublicKeyInvisible": null, + "captchaWidgetType": null, + "createOrganizationUrl": "", + "faviconImageUrl": "", + "homeUrl": "", + "id": "", + "instanceEnvironmentType": "", + "logoImageUrl": "", + "organizationProfileUrl": "", + "pathRoot": "", + "preferredSignInStrategy": "password", + "privacyPolicyUrl": "", + "showDevModeWarning": false, + "signInUrl": "", + "signUpUrl": "", + "supportEmail": "", + "termsUrl": "", + "theme": {}, + "userProfileUrl": "", + "waitlistUrl": "", + }, + "isDevelopmentOrStaging": [Function], + "isProduction": [Function], + "isSingleSession": [Function], + "maintenanceMode": false, + "onWindowLocationHost": [Function], + "organizationSettings": OrganizationSettings { + "actions": { + "adminDelete": false, + }, + "domains": { + "defaultRole": null, + "enabled": false, + "enrollmentModes": [], + }, + "enabled": false, + "maxAllowedMemberships": 1, + "pathRoot": "", + }, + "pathRoot": "/environment", + "userSettings": UserSettings { + "actions": { + "create_organization": false, + "delete_self": false, + }, + "attributes": { + "authenticator_app": { + "enabled": false, + "first_factors": [], + "name": "authenticator_app", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "backup_code": { + "enabled": false, + "first_factors": [], + "name": "backup_code", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "email_address": { + "enabled": true, + "first_factors": [ + "email_code", + ], + "name": "email_address", + "required": true, + "second_factors": [], + "used_for_first_factor": true, + "used_for_second_factor": false, + "verifications": [ + "email_code", + ], + "verify_at_sign_up": true, + }, + "first_name": { + "enabled": false, + "first_factors": [], + "name": "first_name", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "last_name": { + "enabled": false, + "first_factors": [], + "name": "last_name", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "passkey": { + "enabled": false, + "first_factors": [], + "name": "passkey", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "password": { + "enabled": true, + "first_factors": [], + "name": "password", + "required": true, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "phone_number": { + "enabled": false, + "first_factors": [], + "name": "phone_number", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "username": { + "enabled": false, + "first_factors": [], + "name": "username", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "web3_wallet": { + "enabled": false, + "first_factors": [], + "name": "web3_wallet", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + }, + "enterpriseSSO": { + "enabled": false, + }, + "id": undefined, + "passkeySettings": { + "allow_autofill": false, + "show_sign_in_button": false, + }, + "passwordSettings": {}, + "pathRoot": "", + "saml": { + "enabled": false, + }, + "signIn": { + "second_factor": { + "enabled": false, + "required": false, + }, + }, + "signUp": { + "allowlist_only": false, + "captcha_enabled": false, + "legal_consent_enabled": false, + "mode": "public", + "progressive": true, + }, + "social": {}, + "usernameSettings": {}, + }, +} +`; + exports[`Environment has the same initial properties 1`] = ` Environment { "__experimental_commerceSettings": __experimental_CommerceSettings { "pathRoot": "", + "stripePublishableKey": "", }, "authConfig": AuthConfig { "claimedAt": null, @@ -286,21 +513,23 @@ Environment { "afterSignInUrl": "", "afterSignOutAllUrl": "", "afterSignOutOneUrl": "", + "afterSignOutUrl": "", "afterSignUpUrl": "", "afterSwitchSessionUrl": "", "applicationName": "", + "backendHost": "", "branded": true, "captchaHeartbeat": false, "captchaHeartbeatIntervalMs": undefined, "captchaOauthBypass": [], - "captchaProvider": null, + "captchaProvider": "turnstile", "captchaPublicKey": null, "captchaPublicKeyInvisible": null, "captchaWidgetType": null, "clerkJSVersion": "5", "createOrganizationUrl": "", "faviconImageUrl": "", - "googleOneTapClientId": null, + "googleOneTapClientId": undefined, "homeUrl": "", "id": "display_config_DUMMY_ID", "instanceEnvironmentType": "development", @@ -308,12 +537,12 @@ Environment { "organizationProfileUrl": "", "pathRoot": "", "preferredSignInStrategy": "password", - "privacyPolicyUrl": null, + "privacyPolicyUrl": "", "showDevModeWarning": true, "signInUrl": "", "signUpUrl": "", "supportEmail": "", - "termsUrl": null, + "termsUrl": "", "theme": { "accounts": { "background_color": "#ffffff", @@ -489,13 +718,9 @@ Environment { "verify_at_sign_up": false, }, }, - "authenticatableSocialStrategies": [ - "oauth_google", - ], - "enabledFirstFactorIdentifiers": [ - "email_address", - ], - "enterpriseSSO": undefined, + "enterpriseSSO": { + "enabled": false, + }, "id": undefined, "passkeySettings": { "allow_autofill": true, @@ -544,14 +769,7 @@ Environment { "strategy": "oauth_google", }, }, - "socialProviderStrategies": [ - "oauth_google", - ], - "usernameSettings": { - "max_length": NaN, - "min_length": NaN, - }, - "web3FirstFactors": [], + "usernameSettings": {}, }, } `; diff --git a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/UserSettings.test.ts.snap b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/UserSettings.test.ts.snap new file mode 100644 index 00000000000..5ed6282b2f3 --- /dev/null +++ b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/UserSettings.test.ts.snap @@ -0,0 +1,154 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserSettings defaults values when instantiated with no arguments 1`] = ` +UserSettings { + "actions": { + "create_organization": false, + "delete_self": false, + }, + "attributes": { + "authenticator_app": { + "enabled": false, + "first_factors": [], + "name": "authenticator_app", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "backup_code": { + "enabled": false, + "first_factors": [], + "name": "backup_code", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "email_address": { + "enabled": true, + "first_factors": [ + "email_code", + ], + "name": "email_address", + "required": true, + "second_factors": [], + "used_for_first_factor": true, + "used_for_second_factor": false, + "verifications": [ + "email_code", + ], + "verify_at_sign_up": true, + }, + "first_name": { + "enabled": false, + "first_factors": [], + "name": "first_name", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "last_name": { + "enabled": false, + "first_factors": [], + "name": "last_name", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "passkey": { + "enabled": false, + "first_factors": [], + "name": "passkey", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "password": { + "enabled": true, + "first_factors": [], + "name": "password", + "required": true, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "phone_number": { + "enabled": false, + "first_factors": [], + "name": "phone_number", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "username": { + "enabled": false, + "first_factors": [], + "name": "username", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + "web3_wallet": { + "enabled": false, + "first_factors": [], + "name": "web3_wallet", + "required": false, + "second_factors": [], + "used_for_first_factor": false, + "used_for_second_factor": false, + "verifications": [], + "verify_at_sign_up": false, + }, + }, + "enterpriseSSO": { + "enabled": false, + }, + "id": undefined, + "passkeySettings": { + "allow_autofill": false, + "show_sign_in_button": false, + }, + "passwordSettings": {}, + "pathRoot": "", + "saml": { + "enabled": false, + }, + "signIn": { + "second_factor": { + "enabled": false, + "required": false, + }, + }, + "signUp": { + "allowlist_only": false, + "captcha_enabled": false, + "legal_consent_enabled": false, + "mode": "public", + "progressive": true, + }, + "social": {}, + "usernameSettings": {}, +} +`; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 1d3100156c3..bd538b1f196 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -49,7 +49,7 @@ const useAutoFillPasskey = () => { await authenticateWithPasskey({ flow: 'autofill' }); } - if (passkeySettings.allow_autofill && attributes.passkey.enabled) { + if (passkeySettings.allow_autofill && attributes.passkey?.enabled) { runAutofillPasskey(); } }, []); @@ -395,7 +395,7 @@ function SignInStartInternal(): JSX.Element { signUpMode: userSettings.signUp.mode, redirectUrl, redirectUrlComplete, - passwordEnabled: userSettings.attributes.password.required, + passwordEnabled: userSettings.attributes.password?.required ?? false, }); } else { handleError(e, [identifierField, instantPasswordField], card.setError); @@ -483,7 +483,7 @@ function SignInStartInternal(): JSX.Element { ) : null} - {userSettings.attributes.passkey.enabled && + {userSettings.attributes.passkey?.enabled && userSettings.passkeySettings.show_sign_in_button && isWebSupported && ( diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx index 4057763d3e0..fced8e73920 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx @@ -6,11 +6,7 @@ import { SignUpEmailLinkCard } from './SignUpEmailLinkCard'; export const SignUpVerifyEmail = withCardStateProvider(() => { const { userSettings } = useEnvironment(); const { attributes } = userSettings; - const emailLinkStrategyEnabled = attributes.email_address.verifications.includes('email_link'); + const emailLinkStrategyEnabled = attributes.email_address?.verifications?.includes('email_link'); - if (emailLinkStrategyEnabled) { - return ; - } - - return ; + return emailLinkStrategyEnabled ? : ; }); diff --git a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts index a2f9ae591c9..4e83dbaa513 100644 --- a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts +++ b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts @@ -37,7 +37,7 @@ export type Fields = { }; type FieldDeterminationProps = { - attributes: Attributes; + attributes: Partial; activeCommIdentifierType?: ActiveIdentifier; hasTicket?: boolean; hasEmail?: boolean; @@ -103,7 +103,10 @@ export function minimizeFieldsForExistingSignup(fields: Fields, signUp: SignUpRe } } -export const getInitialActiveIdentifier = (attributes: Attributes, isProgressiveSignUp: boolean): ActiveIdentifier => { +export const getInitialActiveIdentifier = ( + attributes: Partial, + isProgressiveSignUp: boolean, +): ActiveIdentifier => { if (emailOrPhone(attributes, isProgressiveSignUp)) { // If we are in the case of Email OR Phone, email takes priority return 'emailAddress'; @@ -111,11 +114,11 @@ export const getInitialActiveIdentifier = (attributes: Attributes, isProgressive const { email_address, phone_number } = attributes; - if (email_address.enabled && isProgressiveSignUp ? email_address.required : email_address.used_for_first_factor) { + if (email_address?.enabled && isProgressiveSignUp ? email_address.required : email_address?.used_for_first_factor) { return 'emailAddress'; } - if (phone_number.enabled && isProgressiveSignUp ? phone_number.required : phone_number.used_for_first_factor) { + if (phone_number?.enabled && isProgressiveSignUp ? phone_number.required : phone_number?.used_for_first_factor) { return 'phoneNumber'; } @@ -128,14 +131,14 @@ export function showFormFields(userSettings: UserSettingsResource): boolean { return userSettings.hasValidAuthFactor || (!authenticatableSocialStrategies.length && !web3FirstFactors.length); } -export function emailOrPhone(attributes: Attributes, isProgressiveSignUp: boolean) { +export function emailOrPhone(attributes: Partial, isProgressiveSignUp: boolean) { const { email_address, phone_number } = attributes; - if (isProgressiveSignUp) { - return email_address.enabled && phone_number.enabled && !email_address.required && !phone_number.required; - } - - return email_address.used_for_first_factor && phone_number.used_for_first_factor; + return Boolean( + isProgressiveSignUp + ? email_address?.enabled && phone_number?.enabled && !email_address.required && !phone_number.required + : email_address?.used_for_first_factor && phone_number?.used_for_first_factor, + ); } function getField(fieldKey: FieldKey, fieldProps: FieldDeterminationProps): Field | undefined { @@ -169,7 +172,7 @@ function getEmailAddressField({ if (isProgressiveSignUp) { // If there is no ticket, or there is a ticket along with an email, and email address is enabled, // we have to show it in the SignUp form - const show = (!hasTicket || (hasTicket && hasEmail)) && attributes.email_address.enabled; + const show = (!hasTicket || (hasTicket && hasEmail)) && attributes.email_address?.enabled; if (!show) { return; @@ -182,15 +185,15 @@ function getEmailAddressField({ } return { - required: attributes.email_address.required, + required: Boolean(attributes.email_address?.required), disabled: !!hasTicket && !!hasEmail, }; } const show = (!hasTicket || (hasTicket && hasEmail)) && - attributes.email_address.enabled && - attributes.email_address.used_for_first_factor && + attributes.email_address?.enabled && + attributes.email_address?.used_for_first_factor && activeCommIdentifierType === 'emailAddress'; if (!show) { @@ -211,7 +214,7 @@ function getPhoneNumberField({ }: FieldDeterminationProps): Field | undefined { if (isProgressiveSignUp) { // If there is no ticket and phone number is enabled, we have to show it in the SignUp form - const show = attributes.phone_number.enabled; + const show = attributes.phone_number?.enabled; if (!show) { return; @@ -224,13 +227,13 @@ function getPhoneNumberField({ } return { - required: attributes.phone_number.required, + required: Boolean(attributes.phone_number?.required), }; } const show = !hasTicket && - attributes.phone_number.enabled && + attributes.phone_number?.enabled && attributes.phone_number.used_for_first_factor && activeCommIdentifierType === 'phoneNumber'; @@ -244,15 +247,15 @@ function getPhoneNumberField({ } // Currently, password is always enabled so only show if required -function getPasswordField(attributes: Attributes): Field | undefined { - const show = attributes.password.enabled && attributes.password.required; +function getPasswordField(attributes: Partial): Field | undefined { + const show = attributes.password?.enabled && attributes.password.required; if (!show) { return; } return { - required: attributes.password.required, + required: Boolean(attributes.password?.required), }; } @@ -276,16 +279,16 @@ function getLegalAcceptedField(legalConsentRequired?: boolean): Field | undefine }; } -function getGenericField(fieldKey: FieldKey, attributes: Attributes): Field | undefined { +function getGenericField(fieldKey: FieldKey, attributes: Partial): Field | undefined { const attrKey = camelToSnake(fieldKey); // @ts-expect-error - TS doesn't know that the key exists - if (!attributes[attrKey].enabled) { + if (!attributes[attrKey]?.enabled) { return; } return { // @ts-expect-error - TS doesn't know that the key exists - required: attributes[attrKey].required, + required: attributes[attrKey]?.required, }; } diff --git a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx index 5e0f5694784..8dde4b31b59 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx @@ -16,12 +16,12 @@ export const AccountPage = withCardStateProvider(() => { const card = useCardState(); const { user } = useUser(); - const showUsername = attributes.username.enabled; - const showEmail = attributes.email_address.enabled; - const showPhone = attributes.phone_number.enabled; + const showUsername = attributes.username?.enabled; + 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 && enterpriseSSO.enabled; - const showWeb3 = attributes.web3_wallet.enabled; + const showWeb3 = attributes.web3_wallet?.enabled; const shouldAllowIdentificationCreation = !showEnterpriseAccounts || diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx index 43ed2258827..b41659c24b9 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx @@ -125,7 +125,7 @@ const getTranslationKeyByStrategy = (strategy: PrepareEmailAddressVerificationPa function isEmailLinksEnabledForInstance(env: EnvironmentResource): boolean { const { userSettings } = env; const { email_address } = userSettings.attributes; - return email_address.enabled && email_address.verifications.includes('email_link'); + return Boolean(email_address?.enabled && email_address?.verifications.includes('email_link')); } /** diff --git a/packages/clerk-js/src/ui/components/UserProfile/MfaPhoneCodeScreen.tsx b/packages/clerk-js/src/ui/components/UserProfile/MfaPhoneCodeScreen.tsx index 2bad9c2e64a..c0c0f803c9b 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/MfaPhoneCodeScreen.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/MfaPhoneCodeScreen.tsx @@ -27,7 +27,7 @@ export const MfaPhoneCodeScreen = withCardStateProvider((props: MfaPhoneCodeScre const ref = React.useRef(); const wizard = useWizard({ defaultStep: 2 }); - const isInstanceWithBackupCodes = useEnvironment().userSettings.attributes.backup_code.enabled; + const isInstanceWithBackupCodes = useEnvironment().userSettings.attributes.backup_code?.enabled; return ( diff --git a/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx index a14d4254221..b7c0a01260e 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ProfileForm.tsx @@ -21,8 +21,8 @@ export const ProfileForm = withCardStateProvider((props: ProfileFormProps) => { } const { first_name, last_name } = useEnvironment().userSettings.attributes; - const showFirstName = first_name.enabled; - const showLastName = last_name.enabled; + const showFirstName = first_name?.enabled; + const showLastName = last_name?.enabled; const userFirstName = user.firstName || ''; const userLastName = user.lastName || ''; @@ -30,13 +30,13 @@ export const ProfileForm = withCardStateProvider((props: ProfileFormProps) => { type: 'text', label: localizationKeys('formFieldLabel__firstName'), placeholder: localizationKeys('formFieldInputPlaceholder__firstName'), - isRequired: last_name.required, + isRequired: last_name?.required, }); const lastNameField = useFormControl('lastName', user.lastName || '', { type: 'text', label: localizationKeys('formFieldLabel__lastName'), placeholder: localizationKeys('formFieldInputPlaceholder__lastName'), - isRequired: last_name.required, + isRequired: last_name?.required, }); const userInfoChanged = diff --git a/packages/clerk-js/src/ui/components/UserProfile/SecurityPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/SecurityPage.tsx index 5e668d58d53..bd21f0258d2 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/SecurityPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/SecurityPage.tsx @@ -15,7 +15,7 @@ export const SecurityPage = withCardStateProvider(() => { const card = useCardState(); const { user } = useUser(); const showPassword = instanceIsPasswordBased; - const showPasskey = attributes.passkey.enabled; + const showPasskey = attributes.passkey?.enabled; const showMfa = getSecondFactors(attributes).length > 0; const showDelete = user?.deleteSelfEnabled; diff --git a/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx index 43a36bfd83c..68d46e06830 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx @@ -26,7 +26,7 @@ export const UsernameForm = withCardStateProvider((props: UsernameFormProps) => return null; } - const isUsernameRequired = userSettings.attributes.username.required; + const isUsernameRequired = userSettings.attributes.username?.required; const canSubmit = (isUsernameRequired ? usernameField.value.length > 0 : true) && user.username !== usernameField.value; diff --git a/packages/clerk-js/src/ui/components/UserProfile/utils.ts b/packages/clerk-js/src/ui/components/UserProfile/utils.ts index ebc4fdb570f..2474b03b404 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/utils.ts +++ b/packages/clerk-js/src/ui/components/UserProfile/utils.ts @@ -16,7 +16,7 @@ export const currentSessionFirst = (id: string) => (a: IDable) => (a.id === id ? export const defaultFirst = (a: PhoneNumberResource) => (a.defaultSecondFactor ? -1 : 1); -export function getSecondFactors(attributes: Attributes): string[] { +export function getSecondFactors(attributes: Partial): string[] { const secondFactors: string[] = []; Object.entries(attributes).forEach(([, attr]) => { @@ -28,7 +28,7 @@ export function getSecondFactors(attributes: Attributes): string[] { return secondFactors; } -export function getSecondFactorsAvailableToAdd(attributes: Attributes, user: UserResource): string[] { +export function getSecondFactorsAvailableToAdd(attributes: Partial, user: UserResource): string[] { let sfs = getSecondFactors(attributes); // If user.totp_enabled, skip totp from the list of choices