From 9c61aeb2e784c3609274327f8092e89efc769ad5 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Thu, 17 Feb 2022 11:55:36 +0200 Subject: [PATCH 01/11] chore(repo): Fix typo in purge-cache.mjs --- packages/clerk-js/scripts/purge-cache.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/scripts/purge-cache.mjs b/packages/clerk-js/scripts/purge-cache.mjs index c54c66eaf12..f8a0327b1a7 100644 --- a/packages/clerk-js/scripts/purge-cache.mjs +++ b/packages/clerk-js/scripts/purge-cache.mjs @@ -18,12 +18,12 @@ try { await fetch( `https://purge.jsdelivr.net/npm/@clerk/clerk-js@staging/dist/clerk.browser.js`, ); - return; } else { await fetch( `https://purge.jsdelivr.net/npm/@clerk/clerk-js@next/dist/clerk.browser.js`, ); } + console.log('🎉 JSDelivr cache for @clerk/clerk-js was successfully purged!'); } catch (err) { console.error('Something went wrong with `postpublish` in clerk-js!'); console.error(err); From 3de6f9aa3bcfd5f08e2fefaa53d1d170d65f2e34 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Fri, 18 Feb 2022 16:41:50 +0200 Subject: [PATCH 02/11] Clerk js user settings fixes (#51) * fix(clerk-js): Determine SignUp email verification methods correctly Use the email_address.verifications array instead of first_factors array that should be used in SignIn. * fix(clerk-js): Apply minor refactoring to UserProfile rendering logic Unify the way we handle fields across sections for better readability. --- .../src/ui/signUp/SignUpVerify.test.tsx | 22 ++++--------------- .../clerk-js/src/ui/signUp/SignUpVerify.tsx | 4 +--- .../__snapshots__/SignUpVerify.test.tsx.snap | 6 ++--- .../src/ui/userProfile/UserProfile.test.tsx | 2 +- .../src/ui/userProfile/account/Account.tsx | 13 +---------- .../PersonalInformationCard.tsx | 22 +++++++++++++++---- .../ui/userProfile/emailAdressess/utils.ts | 5 +++-- 7 files changed, 31 insertions(+), 43 deletions(-) diff --git a/packages/clerk-js/src/ui/signUp/SignUpVerify.test.tsx b/packages/clerk-js/src/ui/signUp/SignUpVerify.test.tsx index 7e7ef2d85d7..6153df4ec6d 100644 --- a/packages/clerk-js/src/ui/signUp/SignUpVerify.test.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUpVerify.test.tsx @@ -1,4 +1,4 @@ -import { noop, render, renderJSON } from '@clerk/shared/testUtils'; +import { render, renderJSON } from '@clerk/shared/testUtils'; import { AttributeData, SessionResource, @@ -133,21 +133,18 @@ describe('', () => { }); describe('verify email address', () => { - it('renders the sign up verification form', () => { + it('renders the OTP sign up verification form', () => { mockEmailAddressAttribute = { enabled: true, - first_factors: ['email_code'], verifications: ['email_code'], }; - // mockFirstFactors = ['email_code', 'phone_code', 'password']; const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); - it('renders the sign up verification form but prefers email_link if exists', () => { + it('renders the magic link sign up verification form ', () => { mockEmailAddressAttribute = { enabled: true, - first_factors: ['email_link'], verifications: ['email_link'], }; const tree = renderJSON(); @@ -157,29 +154,18 @@ describe('', () => { it('can skip disabled verification strategies', () => { mockEmailAddressAttribute = { enabled: true, - first_factors: ['email_link'], verifications: ['email_link'], }; mockDisabledStrategies = ['email_link']; const { container } = render(); expect(container.querySelector('.cl-otp-input')).not.toBeNull(); }); - - xit( - 'renders the verifiation screen, types the OTP and attempts an email address verification', - noop, - ); }); describe('verify phone number', () => { - it('renders the sign up verification form', () => { + it('renders the OTP sign up verification form', () => { const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); - - xit( - 'renders the verifiation screen, types the OTP and attempts a phone number verification', - noop, - ); }); }); diff --git a/packages/clerk-js/src/ui/signUp/SignUpVerify.tsx b/packages/clerk-js/src/ui/signUp/SignUpVerify.tsx index 94a39bea6d5..99335375494 100644 --- a/packages/clerk-js/src/ui/signUp/SignUpVerify.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUpVerify.tsx @@ -28,10 +28,8 @@ function _SignUpVerifyEmailAddress(): JSX.Element { const { userSettings } = useEnvironment(); const { attributes } = userSettings; - // TODO: SignUp should have a field similar to SignIn's supportedFirstFactors - // listing the available strategies for this signUp const emailLinkStrategyEnabled = - attributes.email_address.first_factors.includes('email_link'); + attributes.email_address.verifications.includes('email_link'); const disableEmailLink = shouldDisableStrategy( useSignUpContext(), 'email_link', diff --git a/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpVerify.test.tsx.snap b/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpVerify.test.tsx.snap index a9e5c2372c3..ee5b8822357 100644 --- a/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpVerify.test.tsx.snap +++ b/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpVerify.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` verify email address renders the sign up verification form 1`] = ` +exports[` verify email address renders the OTP sign up verification form 1`] = ` Array [
verify email address renders the sign up verification form but prefers email_link if exists 1`] = ` +exports[` verify email address renders the magic link sign up verification form 1`] = `
@@ -203,7 +203,7 @@ exports[` verify email address renders the sign up verification f
`; -exports[` verify phone number renders the sign up verification form 1`] = ` +exports[` verify phone number renders the OTP sign up verification form 1`] = ` Array [
({ enabled: true, }, last_name: { - enabled: false, + enabled: true, }, username: { enabled: false, diff --git a/packages/clerk-js/src/ui/userProfile/account/Account.tsx b/packages/clerk-js/src/ui/userProfile/account/Account.tsx index 0066d9d648e..fc43af04984 100644 --- a/packages/clerk-js/src/ui/userProfile/account/Account.tsx +++ b/packages/clerk-js/src/ui/userProfile/account/Account.tsx @@ -1,13 +1,9 @@ -import type { UserSettingsResource } from '@clerk/types'; import React from 'react'; -import { useEnvironment } from 'ui/contexts'; import { PersonalInformationCard } from 'ui/userProfile/account/personalInformation'; import { ProfileCard } from 'ui/userProfile/account/profileCard'; import { PageHeading } from 'ui/userProfile/pageHeading'; export const Account = (): JSX.Element => { - const { userSettings } = useEnvironment(); - return ( <> { subtitle='Manage settings related to your account' /> - {shouldShowPersonalInformation(userSettings) && } + ); }; - -function shouldShowPersonalInformation( - userSettings: UserSettingsResource, -): boolean { - const { attributes: { first_name, last_name } } = userSettings; - return first_name.enabled || last_name.enabled; -} diff --git a/packages/clerk-js/src/ui/userProfile/account/personalInformation/PersonalInformationCard.tsx b/packages/clerk-js/src/ui/userProfile/account/personalInformation/PersonalInformationCard.tsx index 04dec96af3d..a151c248b26 100644 --- a/packages/clerk-js/src/ui/userProfile/account/personalInformation/PersonalInformationCard.tsx +++ b/packages/clerk-js/src/ui/userProfile/account/personalInformation/PersonalInformationCard.tsx @@ -1,12 +1,23 @@ import { List } from '@clerk/shared/components/list'; import { TitledCard } from '@clerk/shared/components/titledCard'; import React from 'react'; -import { useCoreUser } from 'ui/contexts'; +import { useCoreUser, useEnvironment } from 'ui/contexts'; import { useNavigate } from 'ui/hooks'; -export const PersonalInformationCard = (): JSX.Element => { +export const PersonalInformationCard = (): JSX.Element | null => { const user = useCoreUser(); const { navigate } = useNavigate(); + const { userSettings } = useEnvironment(); + const { + attributes: { first_name, last_name }, + } = userSettings; + + const hasAtLeastOneAttributeEnable = + first_name?.enabled || last_name?.enabled; + + if (!hasAtLeastOneAttributeEnable) { + return null; + } const firstNameRow = ( { ); + const showFirstName = first_name.enabled; + const showLastName = last_name.enabled; + return ( { subtitle='Manage personal information settings' > - {firstNameRow} - {lastnameRow} + {showFirstName && firstNameRow} + {showLastName && lastnameRow} ); diff --git a/packages/clerk-js/src/ui/userProfile/emailAdressess/utils.ts b/packages/clerk-js/src/ui/userProfile/emailAdressess/utils.ts index 6a3e0f071d4..ef950b3c3d9 100644 --- a/packages/clerk-js/src/ui/userProfile/emailAdressess/utils.ts +++ b/packages/clerk-js/src/ui/userProfile/emailAdressess/utils.ts @@ -3,8 +3,9 @@ import { EnvironmentResource } from '@clerk/types'; export function magicLinksEnabledForInstance( env: EnvironmentResource, ): boolean { - // TODO: email verification should have a supported strategies field const { userSettings } = env; const { email_address } = userSettings.attributes; - return email_address.enabled && email_address.verifications.includes('email_link'); + return ( + email_address.enabled && email_address.verifications.includes('email_link') + ); } From 8144779e37ca4b0a61ac1d452ddd0ab7ccf40f27 Mon Sep 17 00:00:00 2001 From: Peter Perlepes Date: Mon, 21 Feb 2022 20:06:20 +0200 Subject: [PATCH 03/11] feat(clerk-js): Allow passing of object style search params on fapiclient --- packages/clerk-js/src/core/fapiClient.test.ts | 10 ++++++++++ packages/clerk-js/src/core/fapiClient.ts | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/fapiClient.test.ts b/packages/clerk-js/src/core/fapiClient.test.ts index d604a28b542..18fd7af0687 100644 --- a/packages/clerk-js/src/core/fapiClient.test.ts +++ b/packages/clerk-js/src/core/fapiClient.test.ts @@ -79,6 +79,16 @@ describe('buildUrl(options)', () => { ).toBe('https://clerk.example.com/v1/client/foo?_clerk_js_version=42.0.0'); }); + it('correctly parses search params', () => { + expect( + fapiClient.buildUrl({ path: '/foo', search: { test: '1' } }).href, + ).toBe('https://clerk.example.com/v1/foo?test=1&_clerk_js_version=42.0.0'); + + expect(fapiClient.buildUrl({ path: '/foo', search: 'test=2' }).href).toBe( + 'https://clerk.example.com/v1/foo?test=2&_clerk_js_version=42.0.0', + ); + }); + const cases = [ ['PUT', '_method=PUT'], ['PATCH', '_method=PATCH'], diff --git a/packages/clerk-js/src/core/fapiClient.ts b/packages/clerk-js/src/core/fapiClient.ts index f681e957ec5..1ddd2ea57c2 100644 --- a/packages/clerk-js/src/core/fapiClient.ts +++ b/packages/clerk-js/src/core/fapiClient.ts @@ -20,7 +20,12 @@ export type HTTPMethod = export type FapiRequestInit = RequestInit & { path?: string; - search?: string; + search?: + | string + | URLSearchParams + | string[][] + | Record + | undefined; sessionId?: string; url?: URL; }; From 111b993df6d50abcd441ef88bd087809437e17bb Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Tue, 22 Feb 2022 11:37:53 +0200 Subject: [PATCH 04/11] chore(release): Publish - @clerk/clerk-js@2.14.2-staging.0 - @clerk/clerk-expo@0.8.5-staging.0 --- package-lock.json | 8 ++++---- packages/clerk-js/CHANGELOG.md | 9 +++++++++ packages/clerk-js/package.json | 2 +- packages/expo/CHANGELOG.md | 8 ++++++++ packages/expo/package.json | 4 ++-- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c55ac953e1..e2623a749c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22623,7 +22623,7 @@ }, "packages/clerk-js": { "name": "@clerk/clerk-js", - "version": "2.14.1-staging.0", + "version": "2.14.2-staging.0", "license": "MIT", "dependencies": { "@clerk/types": "^1.25.3-staging.0", @@ -22764,10 +22764,10 @@ }, "packages/expo": { "name": "@clerk/clerk-expo", - "version": "0.8.4-staging.0", + "version": "0.8.5-staging.0", "license": "MIT", "dependencies": { - "@clerk/clerk-js": "^2.14.1-staging.0", + "@clerk/clerk-js": "^2.14.2-staging.0", "@clerk/clerk-react": "^2.11.3-staging.0", "base-64": "^1.0.0" }, @@ -24484,7 +24484,7 @@ "@clerk/clerk-expo": { "version": "file:packages/expo", "requires": { - "@clerk/clerk-js": "^2.14.1-staging.0", + "@clerk/clerk-js": "^2.14.2-staging.0", "@clerk/clerk-react": "^2.11.3-staging.0", "@clerk/types": "^1.25.3-staging.0", "@types/jest": "^27.4.0", diff --git a/packages/clerk-js/CHANGELOG.md b/packages/clerk-js/CHANGELOG.md index d812fc7a31c..de2b0a87fd4 100644 --- a/packages/clerk-js/CHANGELOG.md +++ b/packages/clerk-js/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +### [2.14.2-staging.0](https://github.com/clerkinc/javascript/compare/@clerk/clerk-js@2.14.1-staging.0...@clerk/clerk-js@2.14.2-staging.0) (2022-02-22) + + +### Features + +* **clerk-js:** Allow passing of object style search params on fapiclient ([8144779](https://github.com/clerkinc/javascript/commit/8144779e37ca4b0a61ac1d452ddd0ab7ccf40f27)) + + + ### [2.14.1-staging.0](https://github.com/clerkinc/javascript/compare/@clerk/clerk-js@2.14.0-staging.0...@clerk/clerk-js@2.14.1-staging.0) (2022-02-17) **Note:** Version bump only for package @clerk/clerk-js diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index a8baf767e67..d612ead99a2 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -1,6 +1,6 @@ { "name": "@clerk/clerk-js", - "version": "2.14.1-staging.0", + "version": "2.14.2-staging.0", "license": "MIT", "description": "Clerk.dev JS library", "keywords": [ diff --git a/packages/expo/CHANGELOG.md b/packages/expo/CHANGELOG.md index befad6be677..d516d15ff56 100644 --- a/packages/expo/CHANGELOG.md +++ b/packages/expo/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +### [0.8.5-staging.0](https://github.com/clerkinc/javascript/compare/@clerk/clerk-expo@0.8.4-staging.0...@clerk/clerk-expo@0.8.5-staging.0) (2022-02-22) + +**Note:** Version bump only for package @clerk/clerk-expo + + + + + ### [0.8.4-staging.0](https://github.com/clerkinc/javascript/compare/@clerk/clerk-expo@0.8.3-staging.1...@clerk/clerk-expo@0.8.4-staging.0) (2022-02-17) **Note:** Version bump only for package @clerk/clerk-expo diff --git a/packages/expo/package.json b/packages/expo/package.json index 306f180e07b..601d3249df8 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -1,6 +1,6 @@ { "name": "@clerk/clerk-expo", - "version": "0.8.4-staging.0", + "version": "0.8.5-staging.0", "license": "MIT", "description": "Clerk.dev React Native/Expo library", "keywords": [ @@ -26,7 +26,7 @@ "dev": "tsc -p tsconfig.build.json --watch" }, "dependencies": { - "@clerk/clerk-js": "^2.14.1-staging.0", + "@clerk/clerk-js": "^2.14.2-staging.0", "@clerk/clerk-react": "^2.11.3-staging.0", "base-64": "^1.0.0" }, From dde2f3b87a0e177967ce13f087806ebff2084ff5 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Wed, 23 Feb 2022 00:56:31 +0200 Subject: [PATCH 05/11] fix(clerk-js): Import Clerk CSS after shared css modules/ components --- packages/clerk-js/src/ui/Components.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/Components.tsx b/packages/clerk-js/src/ui/Components.tsx index 5288759c009..935ca739466 100644 --- a/packages/clerk-js/src/ui/Components.tsx +++ b/packages/clerk-js/src/ui/Components.tsx @@ -1,5 +1,6 @@ -import './styles/clerk.scss'; - +/* eslint-disable simple-import-sort/imports */ +/* disable sorting, clerk.scss should always be imported +/* after dependencies from /shared */ import { Modal } from '@clerk/shared/components/modal'; import { camelToSnakeKeys } from '@clerk/shared/utils/object'; import type { @@ -34,6 +35,8 @@ import { SignUp, SignUpModal } from './signUp'; import { UserButton } from './userButton'; import { UserProfile } from './userProfile'; +import './styles/clerk.scss'; + export interface MountProps { key: string; props: T; From f72a555f6adb38870539e9bab63cb638c04517d6 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Wed, 23 Feb 2022 11:26:47 +0200 Subject: [PATCH 06/11] feat(clerk-js): Introduce `UserSettings.instanceIsPasswordBased` --- .../src/core/resources/UserSettings.test.ts | 25 ++++++++++++++- .../src/core/resources/UserSettings.ts | 31 +++++++------------ packages/types/src/userSettings.ts | 1 + 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/clerk-js/src/core/resources/UserSettings.test.ts b/packages/clerk-js/src/core/resources/UserSettings.test.ts index 7858b7083e0..9637420055c 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.test.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.test.ts @@ -32,7 +32,30 @@ describe('UserSettings', () => { expect(res).toEqual(['web3_metamask_signature']); }); - it('returns enabled social provier strategies', function () { + it('returns if the instance is passwordless or password-based', function () { + let sut = new UserSettings({ + attributes: { + password: { + enabled: true, + required: true, + }, + }, + } as any as UserSettingsJSON); + + expect(sut.instanceIsPasswordBased).toEqual(true); + + sut = new UserSettings({ + attributes: { + password: { + enabled: true, + required: false, + }, + }, + } as any as UserSettingsJSON); + expect(sut.instanceIsPasswordBased).toEqual(false); + }); + + it('returns enabled social provider strategies', function () { const sut = new UserSettings({ social: { oauth_google: { diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index b88a4d8f0bd..6b2a68a1100 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -9,7 +9,7 @@ import type { Web3Strategy, } from '@clerk/types'; -import {BaseResource} from './internal'; +import { BaseResource } from './internal'; /** * @internal @@ -23,42 +23,35 @@ export class UserSettings extends BaseResource implements UserSettingsResource { socialProviderStrategies: OAuthStrategy[] = []; web3FirstFactors: Web3Strategy[] = []; - enabledFirstFactorIdentifiers: Array< - keyof UserSettingsResource['attributes'] - > = []; + enabledFirstFactorIdentifiers: Array = []; public constructor(data: UserSettingsJSON) { super(); this.fromJSON(data); } + get instanceIsPasswordBased() { + return this.attributes.password.enabled && this.attributes.password.required; + } + protected fromJSON(data: UserSettingsJSON): this { this.social = data.social; this.attributes = data.attributes; this.signIn = data.sign_in; this.signUp = data.sign_up; - this.socialProviderStrategies = this.getSocialProviderStrategies( - data.social, - ); + this.socialProviderStrategies = this.getSocialProviderStrategies(data.social); this.web3FirstFactors = this.getWeb3FirstFactors(data.attributes); - this.enabledFirstFactorIdentifiers = this.getEnabledFirstFactorIdentifiers( - data.attributes, - ); + this.enabledFirstFactorIdentifiers = this.getEnabledFirstFactorIdentifiers(data.attributes); return this; } - private getEnabledFirstFactorIdentifiers( - attributes: Attributes, - ): Array { + private getEnabledFirstFactorIdentifiers(attributes: Attributes): Array { if (!attributes) { return []; } return Object.entries(attributes) - .filter( - ([name, attr]) => - attr.used_for_first_factor && !name.startsWith('web3'), - ) + .filter(([name, attr]) => attr.used_for_first_factor && !name.startsWith('web3')) .map(([name]) => name) as Array; } @@ -68,9 +61,7 @@ export class UserSettings extends BaseResource implements UserSettingsResource { } return Object.entries(attributes) - .filter( - ([name, attr]) => attr.used_for_first_factor && name.startsWith('web3'), - ) + .filter(([name, attr]) => attr.used_for_first_factor && name.startsWith('web3')) .map(([, desc]) => desc.first_factors) .flat() as any as Web3Strategy[]; } diff --git a/packages/types/src/userSettings.ts b/packages/types/src/userSettings.ts index b5612e84a17..6343ce291e1 100644 --- a/packages/types/src/userSettings.ts +++ b/packages/types/src/userSettings.ts @@ -70,4 +70,5 @@ export interface UserSettingsResource extends ClerkResource { socialProviderStrategies: OAuthStrategy[]; web3FirstFactors: Web3Strategy[]; enabledFirstFactorIdentifiers: Attribute[]; + instanceIsPasswordBased: boolean; } From a9eefc967d4745a54aee0c917ce707b1a51df1be Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Wed, 23 Feb 2022 11:27:24 +0200 Subject: [PATCH 07/11] fix(clerk-js): Render instant password field for password-based instances only --- .../src/ui/signIn/SignInStart.test.tsx | 21 ++++++++-- .../clerk-js/src/ui/signIn/SignInStart.tsx | 41 +++++++++++-------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx b/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx index 3b933a0ed94..8fef924e14d 100644 --- a/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx @@ -89,7 +89,7 @@ fdescribe('', () => { }, password: { enabled: true, - used_for_first_factor: false, + required: true, }, }, social: { @@ -195,10 +195,10 @@ fdescribe('', () => { expect(instantPasswordField).toBeDefined(); const inputField = screen.getByLabelText('Email address'); - await userEvent.type(inputField, 'boss@clerk.dev'); + userEvent.type(inputField, 'boss@clerk.dev'); // simulate password being filled by a pwd manager - await userEvent.type(instantPasswordField, 'wrong pass'); + userEvent.type(instantPasswordField, 'wrong pass'); const signEmailButton = screen.getByRole('button', { name: /Continue/i }); userEvent.click(signEmailButton); @@ -271,6 +271,21 @@ fdescribe('', () => { }); }); + it('does not render instant password is instance is passwordless', () => { + mockUserSettings = new UserSettings({ + attributes: { + password: { + enabled: true, + required: false, + }, + }, + } as unknown as UserSettingsJSON); + + const { container } = render(); + const instantPasswordField = container.querySelector('input#password') as HTMLInputElement; + expect(instantPasswordField).toBeNull(); + }); + it.each(['google', 'facebook'])( 'renders the start screen, presses the %s button and starts an oauth flow', async provider => { diff --git a/packages/clerk-js/src/ui/signIn/SignInStart.tsx b/packages/clerk-js/src/ui/signIn/SignInStart.tsx index 16b08205ef4..86b2881eda7 100644 --- a/packages/clerk-js/src/ui/signIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInStart.tsx @@ -39,6 +39,7 @@ export function _SignInStart(): JSX.Element { const standardFormAttributes = userSettings.enabledFirstFactorIdentifiers; const web3FirstFactors = userSettings.web3FirstFactors; const socialProviderStrategies = userSettings.socialProviderStrategies; + const passwordBasedInstance = userSettings.instanceIsPasswordBased; const identifierInputDisplayValues = getIdentifierControlDisplayValues(standardFormAttributes); @@ -157,24 +158,28 @@ export function _SignInStart(): JSX.Element { )} - - instantPassword.setValue(el.value || '')} - tabIndex={-1} - /> - + <> + {passwordBasedInstance && ( + + instantPassword.setValue(el.value || '')} + tabIndex={-1} + /> + + )} + )} From 9a70576d1a47f01e6dbbfd8704f321daddcfe590 Mon Sep 17 00:00:00 2001 From: Peter Perlepes Date: Wed, 23 Feb 2022 18:53:06 +0200 Subject: [PATCH 08/11] fix(clerk-js,clerk-react): Revert user settings work --- .../clerk-js/src/core/resources/AuthConfig.ts | 42 ++- packages/clerk-js/src/core/resources/Base.ts | 6 +- .../src/core/resources/Environment.ts | 10 +- packages/clerk-js/src/core/resources/Token.ts | 2 +- .../src/core/resources/UserSettings.test.ts | 135 ---------- .../src/core/resources/UserSettings.ts | 79 ------ .../clerk-js/src/core/resources/internal.ts | 1 - packages/clerk-js/src/ui/common/constants.ts | 29 ++- .../src/ui/common/withRedirectToHome.tsx | 10 +- .../src/ui/contexts/EnvironmentContext.tsx | 2 +- .../clerk-js/src/ui/signIn/SignIn.test.tsx | 9 +- .../ui/signIn/SignInAccountSwitcher.test.tsx | 7 +- .../src/ui/signIn/SignInFactorOne.test.tsx | 2 +- .../src/ui/signIn/SignInStart.test.tsx | 115 ++++----- .../clerk-js/src/ui/signIn/SignInStart.tsx | 135 ++++++---- .../src/ui/signIn/strategies/OAuth.tsx | 6 +- .../src/ui/signIn/strategies/Web3.tsx | 4 +- .../clerk-js/src/ui/signUp/SignUp.test.tsx | 9 +- .../src/ui/signUp/SignUpStart.test.tsx | 143 ++++------- .../clerk-js/src/ui/signUp/SignUpStart.tsx | 19 +- .../src/ui/signUp/SignUpVerify.test.tsx | 75 ++---- .../clerk-js/src/ui/signUp/SignUpVerify.tsx | 13 +- .../__snapshots__/SignUpVerify.test.tsx.snap | 6 +- packages/clerk-js/src/ui/signUp/utils.test.ts | 240 +++++++----------- packages/clerk-js/src/ui/signUp/utils.ts | 74 ++++-- .../src/ui/userProfile/UserProfile.test.tsx | 49 +--- .../ui/userProfile/account/Account.test.tsx | 61 +---- .../src/ui/userProfile/account/Account.tsx | 12 +- .../PersonalInformationCard.tsx | 22 +- .../account/profileCard/ProfileCard.test.tsx | 65 +---- .../account/profileCard/ProfileCard.tsx | 15 +- .../__snapshots__/ProfileCard.test.tsx.snap | 214 ---------------- .../emailAdressess/AddNewEmail.test.tsx | 18 +- ...lAddressVerificationWithMagicLink.test.tsx | 5 + .../emailAdressess/EmailDetail.test.tsx | 24 +- .../ui/userProfile/emailAdressess/utils.ts | 9 +- .../security/ChangePassword.test.tsx | 62 ----- .../userProfile/security/ChangePassword.tsx | 6 +- .../ui/userProfile/security/Security.test.tsx | 20 +- .../src/ui/userProfile/security/Security.tsx | 10 +- .../src/ui/userProfile/security/index.tsx | 7 +- packages/types/src/authConfig.ts | 52 ++++ packages/types/src/environment.ts | 2 - packages/types/src/index.ts | 1 - packages/types/src/json.ts | 23 +- packages/types/src/userSettings.ts | 74 ------ 46 files changed, 630 insertions(+), 1294 deletions(-) delete mode 100644 packages/clerk-js/src/core/resources/UserSettings.test.ts delete mode 100644 packages/clerk-js/src/core/resources/UserSettings.ts delete mode 100644 packages/clerk-js/src/ui/userProfile/security/ChangePassword.test.tsx delete mode 100644 packages/types/src/userSettings.ts diff --git a/packages/clerk-js/src/core/resources/AuthConfig.ts b/packages/clerk-js/src/core/resources/AuthConfig.ts index 2d176e87d7c..c9bb1dfc8ad 100644 --- a/packages/clerk-js/src/core/resources/AuthConfig.ts +++ b/packages/clerk-js/src/core/resources/AuthConfig.ts @@ -1,8 +1,29 @@ -import type { AuthConfigJSON, AuthConfigResource } from '@clerk/types'; +import type { + AuthConfigJSON, + AuthConfigResource, + EmailAddressVerificationStrategy, + IdentificationStrategy, + SignInStrategyName, + ToggleType, + ToggleTypeWithRequire, +} from '@clerk/types'; import { BaseResource } from './internal'; export class AuthConfig extends BaseResource implements AuthConfigResource { + id!: string; + firstName!: ToggleTypeWithRequire; + lastName!: ToggleTypeWithRequire; + emailAddress!: ToggleType; + phoneNumber!: ToggleType; + username!: ToggleType; + password!: string; + identificationStrategies!: IdentificationStrategy[]; + identificationRequirements!: IdentificationStrategy[][]; + passwordConditions!: any; + firstFactors!: SignInStrategyName[]; + secondFactors!: SignInStrategyName[]; + emailAddressVerificationStrategies!: EmailAddressVerificationStrategy[]; singleSessionMode!: boolean; public constructor(data: AuthConfigJSON) { @@ -10,8 +31,23 @@ export class AuthConfig extends BaseResource implements AuthConfigResource { this.fromJSON(data); } - protected fromJSON(data: AuthConfigJSON | null): this { - this.singleSessionMode = data ? data.single_session_mode : true; + protected fromJSON(data: AuthConfigJSON): this { + this.id = data.id; + this.firstName = data.first_name; + this.lastName = data.last_name; + this.emailAddress = data.email_address; + this.phoneNumber = data.phone_number; + this.username = data.username; + this.password = data.password; + this.identificationStrategies = data.identification_strategies; + this.identificationRequirements = data.identification_requirements; + this.passwordConditions = data.password_conditions; + this.firstFactors = data.first_factors; + this.secondFactors = data.second_factors; + this.emailAddressVerificationStrategies = + data.email_address_verification_strategies; + this.singleSessionMode = data.single_session_mode; + return this; } } diff --git a/packages/clerk-js/src/core/resources/Base.ts b/packages/clerk-js/src/core/resources/Base.ts index 8818f09cd48..92deea472e8 100644 --- a/packages/clerk-js/src/core/resources/Base.ts +++ b/packages/clerk-js/src/core/resources/Base.ts @@ -6,8 +6,8 @@ import type { HTTPMethod, } from 'core/fapiClient'; +import type Clerk from '../clerk'; import { clerkMissingFapiClientInResources } from '../errors'; -import type { Clerk } from './internal'; import { ClerkAPIResponseError, Client } from './internal'; export type BaseFetchOptions = { forceUpdateClient?: boolean }; @@ -94,7 +94,9 @@ export abstract class BaseResource { return baseWithId; } - return baseWithId.replace(/[^/]$/, '$&/') + encodeURIComponent(action); + return ( + baseWithId.replace(/[^/]$/, '$&/') + encodeURIComponent(action as string) + ); } protected abstract fromJSON(data: ClerkResourceJSON | null): this; diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index 7d91a877ab8..77e680ae2d9 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -3,15 +3,9 @@ import type { DisplayConfigResource, EnvironmentJSON, EnvironmentResource, - UserSettingsResource, } from '@clerk/types'; -import { - AuthConfig, - BaseResource, - DisplayConfig, - UserSettings, -} from './internal'; +import { AuthConfig, BaseResource, DisplayConfig } from './internal'; export class Environment extends BaseResource implements EnvironmentResource { private static instance: Environment; @@ -19,7 +13,6 @@ export class Environment extends BaseResource implements EnvironmentResource { pathRoot = '/environment'; authConfig!: AuthConfigResource; displayConfig!: DisplayConfigResource; - userSettings!: UserSettingsResource; public static getInstance(): Environment { if (!Environment.instance) { @@ -57,7 +50,6 @@ export class Environment extends BaseResource implements EnvironmentResource { if (data) { this.authConfig = new AuthConfig(data.auth_config); this.displayConfig = new DisplayConfig(data.display_config); - this.userSettings = new UserSettings(data.user_settings); } return this; } diff --git a/packages/clerk-js/src/core/resources/Token.ts b/packages/clerk-js/src/core/resources/Token.ts index 900c7f26b47..edde8f44cd1 100644 --- a/packages/clerk-js/src/core/resources/Token.ts +++ b/packages/clerk-js/src/core/resources/Token.ts @@ -1,7 +1,7 @@ import type { JWT, TokenJSON, TokenResource } from '@clerk/types'; import { decode } from 'utils'; -import { BaseResource } from './internal'; +import { BaseResource } from './Base'; export class Token extends BaseResource implements TokenResource { pathRoot = 'tokens'; diff --git a/packages/clerk-js/src/core/resources/UserSettings.test.ts b/packages/clerk-js/src/core/resources/UserSettings.test.ts deleted file mode 100644 index 9637420055c..00000000000 --- a/packages/clerk-js/src/core/resources/UserSettings.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { UserSettingsJSON } from '@clerk/types'; -import { UserSettings } from 'core/resources/internal'; - -describe('UserSettings', () => { - it('returns enabled web3 first factors', function () { - const sut = new UserSettings({ - attributes: { - username: { - enabled: false, - required: false, - used_for_first_factor: false, - first_factors: [], - used_for_second_factor: false, - second_factors: [], - verifications: [], - verify_at_sign_up: false, - }, - web3_wallet: { - enabled: true, - required: true, - used_for_first_factor: true, - first_factors: ['web3_metamask_signature'], - used_for_second_factor: false, - second_factors: [], - verifications: ['web3_metamask_signature'], - verify_at_sign_up: true, - }, - }, - } as any as UserSettingsJSON); - - const res = sut.web3FirstFactors; - expect(res).toEqual(['web3_metamask_signature']); - }); - - it('returns if the instance is passwordless or password-based', function () { - let sut = new UserSettings({ - attributes: { - password: { - enabled: true, - required: true, - }, - }, - } as any as UserSettingsJSON); - - expect(sut.instanceIsPasswordBased).toEqual(true); - - sut = new UserSettings({ - attributes: { - password: { - enabled: true, - required: false, - }, - }, - } as any as UserSettingsJSON); - expect(sut.instanceIsPasswordBased).toEqual(false); - }); - - it('returns enabled social provider strategies', function () { - const sut = new UserSettings({ - social: { - oauth_google: { - enabled: true, - required: false, - authenticatable: true, - strategy: 'oauth_google', - }, - oauth_gitlab: { - enabled: false, - required: false, - authenticatable: false, - strategy: 'oauth_gitlab', - }, - oauth_facebook: { - enabled: true, - required: false, - authenticatable: true, - strategy: 'oauth_facebook', - }, - }, - attributes: { - username: { - enabled: true, - required: false, - }, - web3_wallet: { - enabled: false, - required: false, - }, - }, - } as any as UserSettingsJSON); - - const res = sut.socialProviderStrategies; - expect(res).toEqual(['oauth_facebook', 'oauth_google']); - }); - - it('returns enabled standard form attributes', function () { - const sut = new UserSettings({ - attributes: { - email_address: { - enabled: true, - required: false, - used_for_first_factor: true, - first_factors: ['email_link'], - used_for_second_factor: false, - second_factors: [], - verifications: ['email_link'], - verify_at_sign_up: true, - }, - phone_number: { - enabled: true, - required: false, - used_for_first_factor: true, - first_factors: ['phone_code'], - used_for_second_factor: true, - second_factors: ['phone_code'], - verifications: ['phone_code'], - verify_at_sign_up: true, - }, - username: { - enabled: false, - required: false, - used_for_first_factor: false, - first_factors: [], - used_for_second_factor: false, - second_factors: [], - verifications: [], - verify_at_sign_up: false, - }, - }, - } as any as UserSettingsJSON); - - const res = sut.enabledFirstFactorIdentifiers; - expect(res).toEqual(['email_address', 'phone_number']); - }); -}); diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts deleted file mode 100644 index 6b2a68a1100..00000000000 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { - Attributes, - OauthProviders, - OAuthStrategy, - SignInData, - SignUpData, - UserSettingsJSON, - UserSettingsResource, - Web3Strategy, -} from '@clerk/types'; - -import { BaseResource } from './internal'; - -/** - * @internal - */ -export class UserSettings extends BaseResource implements UserSettingsResource { - id = undefined; - social!: OauthProviders; - attributes!: Attributes; - signIn!: SignInData; - signUp!: SignUpData; - - socialProviderStrategies: OAuthStrategy[] = []; - web3FirstFactors: Web3Strategy[] = []; - enabledFirstFactorIdentifiers: Array = []; - - public constructor(data: UserSettingsJSON) { - super(); - this.fromJSON(data); - } - - get instanceIsPasswordBased() { - return this.attributes.password.enabled && this.attributes.password.required; - } - - protected fromJSON(data: UserSettingsJSON): this { - this.social = data.social; - this.attributes = data.attributes; - this.signIn = data.sign_in; - this.signUp = data.sign_up; - this.socialProviderStrategies = this.getSocialProviderStrategies(data.social); - this.web3FirstFactors = this.getWeb3FirstFactors(data.attributes); - this.enabledFirstFactorIdentifiers = this.getEnabledFirstFactorIdentifiers(data.attributes); - return this; - } - - 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(); - } -} diff --git a/packages/clerk-js/src/core/resources/internal.ts b/packages/clerk-js/src/core/resources/internal.ts index 68194f34860..91ce5a1d740 100644 --- a/packages/clerk-js/src/core/resources/internal.ts +++ b/packages/clerk-js/src/core/resources/internal.ts @@ -1,6 +1,5 @@ export type { default as Clerk } from '../clerk'; export * from './Base'; -export * from './UserSettings'; export * from './AuthConfig'; export * from './Client'; export * from './DisplayConfig'; diff --git a/packages/clerk-js/src/ui/common/constants.ts b/packages/clerk-js/src/ui/common/constants.ts index 30ea730c9bd..e999e29e67d 100644 --- a/packages/clerk-js/src/ui/common/constants.ts +++ b/packages/clerk-js/src/ui/common/constants.ts @@ -1,50 +1,47 @@ import type { OAuthProvider, Web3Provider } from '@clerk/types'; -const FirstFactorConfigs = Object.freeze({ +export const FirstFactorConfigs = Object.freeze({ email_address: { label: 'Email address', + icon: 'clerk-email', fieldType: 'email', }, phone_number: { label: 'Phone number', + icon: 'clerk-phone', fieldType: 'tel', }, username: { label: 'Username', + icon: 'clerk-person', fieldType: 'text', }, email_address_phone_number: { label: 'Email or phone', + icon: 'clerk-person', fieldType: 'text', }, email_address_username: { label: 'Email or username', + icon: 'clerk-person', fieldType: 'text', }, phone_number_username: { label: 'Phone number or username', + icon: 'clerk-person', fieldType: 'text', }, email_address_phone_number_username: { label: 'Email, phone, or username', + icon: 'clerk-person', fieldType: 'text', }, - default: { - label: '', - fieldType: 'text', - }, -} as Record); - -export const getIdentifierControlDisplayValues = (attributes: string[]) => { - const indexKey = attributes.length == 0 ? null : [...attributes].sort().join('_'); - return FirstFactorConfigs[indexKey || 'default']; -}; +} as Record); export const PREFERRED_SIGN_IN_STRATEGIES = Object.freeze({ Password: 'password', OTP: 'otp', }); - interface OAuthProviderData { id: string; name: string; @@ -105,7 +102,9 @@ export const OAUTH_PROVIDERS: OAuthProviders = Object.freeze({ }, }); -export function getOAuthProviderData(name: OAuthProvider): OAuthProviderData | undefined | null { +export function getOAuthProviderData( + name: OAuthProvider, +): OAuthProviderData | undefined | null { return OAUTH_PROVIDERS[name]; } @@ -125,7 +124,9 @@ export const WEB3_PROVIDERS: Web3Providers = Object.freeze({ }, }); -export function getWeb3ProviderData(name: Web3Provider): Web3ProviderData | undefined | null { +export function getWeb3ProviderData( + name: Web3Provider, +): Web3ProviderData | undefined | null { return WEB3_PROVIDERS[name]; } diff --git a/packages/clerk-js/src/ui/common/withRedirectToHome.tsx b/packages/clerk-js/src/ui/common/withRedirectToHome.tsx index e6fc8a92fbd..7c6147148d9 100644 --- a/packages/clerk-js/src/ui/common/withRedirectToHome.tsx +++ b/packages/clerk-js/src/ui/common/withRedirectToHome.tsx @@ -7,26 +7,26 @@ export function withRedirectToHome

( Component: React.ComponentType

, displayName?: string, ): (props: P) => null | JSX.Element { - displayName = displayName || Component.displayName || Component.name || 'Component'; + displayName = + displayName || Component.displayName || Component.name || 'Component'; Component.displayName = displayName; const HOC = (props: P) => { const { navigate } = useNavigate(); const { authConfig, displayConfig } = useEnvironment(); - const { singleSessionMode } = authConfig; const session = useCoreSession({ avoidUndefinedCheck: true }); React.useEffect(() => { - if (singleSessionMode && !!session) { + if (authConfig.singleSessionMode && !!session) { navigate(displayConfig.homeUrl); } }, []); - if (singleSessionMode && !!session) { + if (authConfig.singleSessionMode && !!session) { return null; } - return ; + return ; }; HOC.displayName = `withRedirectToHome(${displayName})`; diff --git a/packages/clerk-js/src/ui/contexts/EnvironmentContext.tsx b/packages/clerk-js/src/ui/contexts/EnvironmentContext.tsx index 0bf1054c81a..5b75d20f016 100644 --- a/packages/clerk-js/src/ui/contexts/EnvironmentContext.tsx +++ b/packages/clerk-js/src/ui/contexts/EnvironmentContext.tsx @@ -8,7 +8,7 @@ const EnvironmentContext = React.createContext( interface EnvironmentProviderProps { children: React.ReactNode; - value: EnvironmentResource; + value: any; } function EnvironmentProvider({ diff --git a/packages/clerk-js/src/ui/signIn/SignIn.test.tsx b/packages/clerk-js/src/ui/signIn/SignIn.test.tsx index a1cf8810f7e..c2a5e93f67d 100644 --- a/packages/clerk-js/src/ui/signIn/SignIn.test.tsx +++ b/packages/clerk-js/src/ui/signIn/SignIn.test.tsx @@ -1,6 +1,7 @@ import { render } from '@clerk/shared/testUtils'; import { EnvironmentResource } from '@clerk/types'; -import { AuthConfig, Session } from 'core/resources/internal'; +import { AuthConfig } from 'core/resources/AuthConfig'; +import { Session } from 'core/resources/Session'; import React from 'react'; import { SignIn } from 'ui/signIn/SignIn'; @@ -30,6 +31,12 @@ jest.mock('ui/contexts', () => { homeUrl: 'https://www.cnn.com', }, authConfig: { + identificationStrategies: [ + 'email_address', + 'oauth_google', + 'oauth_facebook', + ], + firstFactors: ['email_address', 'oauth_google', 'oauth_facebook'], singleSessionMode: true, } as Partial, } as Partial), diff --git a/packages/clerk-js/src/ui/signIn/SignInAccountSwitcher.test.tsx b/packages/clerk-js/src/ui/signIn/SignInAccountSwitcher.test.tsx index 7320f5987d3..8cbf366e313 100644 --- a/packages/clerk-js/src/ui/signIn/SignInAccountSwitcher.test.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInAccountSwitcher.test.tsx @@ -28,7 +28,12 @@ jest.mock('ui/contexts', () => { }, }, authConfig: { - singleSessionMode: false, + identification_strategies: [ + 'email_address', + 'oauth_google', + 'oauth_facebook', + ], + first_factors: ['email_address', 'oauth_google', 'oauth_facebook'], }, })), useSignInContext: () => { diff --git a/packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx index e0ce602a26a..a74133adf9d 100644 --- a/packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx @@ -685,7 +685,7 @@ describe('', () => { expect(container.querySelector('.cl-auth-form-spinner')).toBeDefined(); }); - it('renders the fallback screen', () => { + it('renders the fallback screen', async () => { ( useEnvironment as jest.Mock> ).mockImplementation(() => ({ diff --git a/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx b/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx index 8fef924e14d..8fb33bdeb98 100644 --- a/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx @@ -1,7 +1,14 @@ -import { mocked, render, renderJSON, screen, userEvent, waitFor } from '@clerk/shared/testUtils'; +import { + mocked, + render, + renderJSON, + screen, + userEvent, + waitFor, +} from '@clerk/shared/testUtils'; import { titleize } from '@clerk/shared/utils/string'; -import { EnvironmentResource, SignInResource, UserSettingsJSON, UserSettingsResource } from '@clerk/types'; -import { ClerkAPIResponseError, UserSettings } from 'core/resources/internal'; +import { EnvironmentResource, SignInResource } from '@clerk/types'; +import { ClerkAPIResponseError } from 'core/resources/Error'; import React from 'react'; import { useCoreSignIn } from 'ui/contexts'; @@ -11,7 +18,6 @@ const mockNavigate = jest.fn(); const mockCreateRequest = jest.fn(); const mockAuthenticateWithRedirect = jest.fn(); const mockNavigateAfterSignIn = jest.fn(); -let mockUserSettings: UserSettingsResource; jest.mock('ui/router/RouteContext'); @@ -36,9 +42,14 @@ jest.mock('ui/contexts', () => { preferredSignInStrategy: 'otp', afterSignInUrl: 'http://test.host', }, - userSettings: mockUserSettings, authConfig: { singleSessionMode: false, + identificationStrategies: [ + 'email_address', + 'oauth_google', + 'oauth_facebook', + ], + firstFactors: ['email_address', 'oauth_google', 'oauth_facebook'], }, } as any as EnvironmentResource), ), @@ -69,46 +80,11 @@ jest.mock('ui/hooks', () => ({ }, })); -fdescribe('', () => { +describe('', () => { beforeEach(() => { jest.clearAllMocks(); }); - beforeEach(() => { - mockUserSettings = new UserSettings({ - attributes: { - email_address: { - enabled: true, - required: false, - used_for_first_factor: true, - first_factors: ['email_link'], - used_for_second_factor: false, - second_factors: [], - verifications: ['email_link'], - verify_at_sign_up: false, - }, - password: { - enabled: true, - required: true, - }, - }, - social: { - oauth_google: { - enabled: true, - required: false, - authenticatable: false, - strategy: 'oauth_google', - }, - oauth_facebook: { - enabled: true, - required: false, - authenticatable: false, - strategy: 'oauth_facebook', - }, - }, - } as unknown as UserSettingsJSON); - }); - describe('when user is not signed in', () => { beforeAll(() => { mockCreateRequest.mockReturnValue({ @@ -117,7 +93,7 @@ fdescribe('', () => { }); }); - it('renders the sign in start screen', () => { + it('renders the sign in start screen', async () => { const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); @@ -126,7 +102,7 @@ fdescribe('', () => { render(); const inputField = screen.getByLabelText('Email address'); - userEvent.type(inputField, 'boss@clerk.dev'); + await userEvent.type(inputField, 'boss@clerk.dev'); const signEmailButton = screen.getByRole('button', { name: /Continue/i }); userEvent.click(signEmailButton); @@ -147,15 +123,17 @@ fdescribe('', () => { status: 'complete', }); - const instantPasswordField = container.querySelector('input#password') as HTMLInputElement; + const instantPasswordField = container.querySelector( + 'input#password', + ) as HTMLInputElement; expect(instantPasswordField).toBeDefined(); const inputField = screen.getByLabelText('Email address'); - userEvent.type(inputField, 'boss@clerk.dev'); + await userEvent.type(inputField, 'boss@clerk.dev'); // simulate password being filled by a pwd manager - userEvent.type(instantPasswordField, '123456'); + await userEvent.type(instantPasswordField, '123456'); const signEmailButton = screen.getByRole('button', { name: /Continue/i }); userEvent.click(signEmailButton); @@ -190,7 +168,9 @@ fdescribe('', () => { status: 'needs_first_factor', })); - const instantPasswordField = container.querySelector('input#password') as HTMLInputElement; + const instantPasswordField = container.querySelector( + 'input#password', + ) as HTMLInputElement; expect(instantPasswordField).toBeDefined(); @@ -228,7 +208,8 @@ fdescribe('', () => { data: [ { code: 'form_password_incorrect', - message: 'Password is incorrect. Try again, or use another method.', + message: + 'Password is incorrect. Try again, or use another method.', meta: { param_name: 'password', }, @@ -241,7 +222,9 @@ fdescribe('', () => { status: 'needs_first_factor', })); - const instantPasswordField = container.querySelector('input#password') as HTMLInputElement; + const instantPasswordField = container.querySelector( + 'input#password', + ) as HTMLInputElement; expect(instantPasswordField).toBeDefined(); @@ -271,21 +254,6 @@ fdescribe('', () => { }); }); - it('does not render instant password is instance is passwordless', () => { - mockUserSettings = new UserSettings({ - attributes: { - password: { - enabled: true, - required: false, - }, - }, - } as unknown as UserSettingsJSON); - - const { container } = render(); - const instantPasswordField = container.querySelector('input#password') as HTMLInputElement; - expect(instantPasswordField).toBeNull(); - }); - it.each(['google', 'facebook'])( 'renders the start screen, presses the %s button and starts an oauth flow', async provider => { @@ -347,16 +315,24 @@ fdescribe('', () => { identifier: 'boss@clerk.dev', }); expect(mockNavigate).not.toHaveBeenCalled(); - expect(mockSetSession).toHaveBeenNthCalledWith(1, 'deadbeef', mockNavigateAfterSignIn); + expect(mockSetSession).toHaveBeenNthCalledWith( + 1, + 'deadbeef', + mockNavigateAfterSignIn, + ); }); }); }); describe('when the instance is invitation only', () => { it('renders the external account verification error if available', async () => { - const errorMsg = 'You cannot sign up with sokratis.vidros@gmail.com since this is an invitation-only application'; + const errorMsg = + 'You cannot sign up with sokratis.vidros@gmail.com since this is an invitation-only application'; - mocked(useCoreSignIn as jest.Mock, true).mockImplementationOnce( + mocked( + useCoreSignIn as jest.Mock, + true, + ).mockImplementationOnce( () => ({ create: mockCreateRequest, @@ -380,7 +356,10 @@ fdescribe('', () => { it('renders the external account verification error if available', async () => { const errorMsg = 'You did not grant access to your Google account'; - mocked(useCoreSignIn as jest.Mock, true).mockImplementationOnce( + mocked( + useCoreSignIn as jest.Mock, + true, + ).mockImplementationOnce( () => ({ create: mockCreateRequest, diff --git a/packages/clerk-js/src/ui/signIn/SignInStart.tsx b/packages/clerk-js/src/ui/signIn/SignInStart.tsx index 86b2881eda7..5f148734f15 100644 --- a/packages/clerk-js/src/ui/signIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInStart.tsx @@ -1,14 +1,19 @@ import { Control } from '@clerk/shared/components/control'; import { Form } from '@clerk/shared/components/form'; -import { Input } from '@clerk/shared/components/input'; +import { Input, InputType } from '@clerk/shared/components/input'; import { PhoneInput } from '@clerk/shared/components/phoneInput'; -import { ClerkAPIError, SignInParams } from '@clerk/types'; +import { + ClerkAPIError, + OAuthStrategy, + SignInParams, + Web3Strategy, +} from '@clerk/types'; import cn from 'classnames'; import React from 'react'; import { buildRequest, FieldState, - getIdentifierControlDisplayValues, + FirstFactorConfigs, handleError, PoweredByClerk, Separator, @@ -17,7 +22,12 @@ import { } from 'ui/common'; import { Body, Header } from 'ui/common/authForms'; import { ERROR_CODES } from 'ui/common/constants'; -import { useCoreClerk, useCoreSignIn, useEnvironment, useSignInContext } from 'ui/contexts'; +import { + useCoreClerk, + useCoreSignIn, + useEnvironment, + useSignInContext, +} from 'ui/contexts'; import { useNavigate } from 'ui/hooks'; import { useSupportEmail } from 'ui/hooks/useSupportEmail'; @@ -25,7 +35,7 @@ import { SignUpLink } from './SignUpLink'; import { OAuth, Web3 } from './strategies'; export function _SignInStart(): JSX.Element { - const { userSettings } = useEnvironment(); + const environment = useEnvironment(); const { setSession } = useCoreClerk(); const signIn = useCoreSignIn(); const { navigate } = useNavigate(); @@ -36,20 +46,50 @@ export function _SignInStart(): JSX.Element { const instantPassword = useFieldState('password', ''); const [error, setError] = React.useState(); - const standardFormAttributes = userSettings.enabledFirstFactorIdentifiers; - const web3FirstFactors = userSettings.web3FirstFactors; - const socialProviderStrategies = userSettings.socialProviderStrategies; - const passwordBasedInstance = userSettings.instanceIsPasswordBased; + const { authConfig } = environment; - const identifierInputDisplayValues = getIdentifierControlDisplayValues(standardFormAttributes); + const firstPartyOptions = authConfig.identificationStrategies.filter( + strategy => !strategy.includes('oauth') && !strategy.includes('web3'), + ); + const firstPartyKey = + firstPartyOptions.length == 0 + ? null + : [...firstPartyOptions].sort().join('_'); + + const firstPartyLabel = + firstPartyKey && FirstFactorConfigs[firstPartyKey] + ? FirstFactorConfigs[firstPartyKey].label + : ''; + + const fieldType: InputType = ( + firstPartyKey && FirstFactorConfigs[firstPartyKey] + ? FirstFactorConfigs[firstPartyKey].fieldType + : 'text' + ) as InputType; + + const firstFactors = authConfig.firstFactors; + const web3Options = firstFactors + .filter(fac => fac.startsWith('web3')) + .sort() as Web3Strategy[]; + const oauthOptions = firstFactors + .filter(fac => fac.startsWith('oauth')) + .sort() as OAuthStrategy[]; + + // TODO: Clean up the following code end React.useEffect(() => { async function handleOauthError() { const error = signIn?.firstFactorVerification?.error; - if (error?.code === ERROR_CODES.NOT_ALLOWED_TO_SIGN_UP || error?.code === ERROR_CODES.OAUTH_ACCESS_DENIED) { + if ( + error?.code === ERROR_CODES.NOT_ALLOWED_TO_SIGN_UP || + error?.code === ERROR_CODES.OAUTH_ACCESS_DENIED + ) { setError(error.longMessage); - // TODO: This is a workaround in order to reset the sign in attempt - // so that the oauth error does not persist on full page reloads. + + // TODO: This is a hack to reset the sign in attempt so that the oauth error + // does not persist on full page reloads. + // + // We will revise this strategy as part of the Clerk DX epic. void (await signIn.create({})); } } @@ -57,7 +97,9 @@ export function _SignInStart(): JSX.Element { void handleOauthError(); }); - const buildSignInParams = (fields: Array>): SignInParams => { + const buildSignInParams = ( + fields: Array>, + ): SignInParams => { const hasPassword = fields.some(f => f.name === 'password' && !!f.value); if (!hasPassword) { fields = fields.filter(f => f.name !== 'password'); @@ -94,7 +136,8 @@ export function _SignInStart(): JSX.Element { } const instantPasswordError: ClerkAPIError = e.errors.find( (e: ClerkAPIError) => - e.code === ERROR_CODES.INVALID_STRATEGY_FOR_USER || e.code === ERROR_CODES.FORM_PASSWORD_INCORRECT, + e.code === ERROR_CODES.INVALID_STRATEGY_FOR_USER || + e.code === ERROR_CODES.FORM_PASSWORD_INCORRECT, ); const alreadySignedInError: ClerkAPIError = e.errors.find( (e: ClerkAPIError) => e.code === 'identifier_already_signed_in', @@ -110,35 +153,37 @@ export function _SignInStart(): JSX.Element { } }; - const handleFirstPartySubmit = async (e: React.FormEvent) => { + const handleFirstPartySubmit = async ( + e: React.FormEvent, + ) => { e.preventDefault(); return signInWithFields(identifier, instantPassword); }; - const hasSocialOrWeb3Buttons = !!socialProviderStrategies.length || !!web3FirstFactors.length; - return ( <>

- - + + - {standardFormAttributes.length > 0 && ( + {firstPartyOptions.length > 0 && ( <> - {hasSocialOrWeb3Buttons && } + {(oauthOptions.length > 0 || web3Options.length > 0) && ( + + )}
- {identifierInputDisplayValues.fieldType === phoneFieldType ? ( + {fieldType === phoneFieldType ? ( identifier.setValue(el.value || '')} value={identifier.value} autoFocus @@ -158,28 +203,24 @@ export function _SignInStart(): JSX.Element { )} - <> - {passwordBasedInstance && ( - - instantPassword.setValue(el.value || '')} - tabIndex={-1} - /> - - )} - + + instantPassword.setValue(el.value || '')} + tabIndex={-1} + /> +
)} diff --git a/packages/clerk-js/src/ui/signIn/strategies/OAuth.tsx b/packages/clerk-js/src/ui/signIn/strategies/OAuth.tsx index 3a6a3e115b2..2242e104fac 100644 --- a/packages/clerk-js/src/ui/signIn/strategies/OAuth.tsx +++ b/packages/clerk-js/src/ui/signIn/strategies/OAuth.tsx @@ -22,8 +22,10 @@ export function OAuth({ }: OauthProps): JSX.Element | null { const ctx = useSignInContext(); const signIn = useCoreSignIn(); - const { displayConfig } = useEnvironment(); - + const environment = useEnvironment(); + + const { displayConfig } = environment; + const startOauth = async (e: React.MouseEvent, strategy: OAuthStrategy) => { e.preventDefault(); diff --git a/packages/clerk-js/src/ui/signIn/strategies/Web3.tsx b/packages/clerk-js/src/ui/signIn/strategies/Web3.tsx index 07ceee4f470..f6e569f0b9e 100644 --- a/packages/clerk-js/src/ui/signIn/strategies/Web3.tsx +++ b/packages/clerk-js/src/ui/signIn/strategies/Web3.tsx @@ -21,7 +21,9 @@ export function Web3({ }: Web3Props): JSX.Element | null { const clerk = useCoreClerk(); const ctx = useSignInContext(); - const { displayConfig } = useEnvironment(); + const environment = useEnvironment(); + + const { displayConfig } = environment; const startWeb3 = async (e: React.MouseEvent) => { e.preventDefault(); diff --git a/packages/clerk-js/src/ui/signUp/SignUp.test.tsx b/packages/clerk-js/src/ui/signUp/SignUp.test.tsx index 5d7c38c60e1..7c16913cd31 100644 --- a/packages/clerk-js/src/ui/signUp/SignUp.test.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUp.test.tsx @@ -1,6 +1,7 @@ import { render } from '@clerk/shared/testUtils'; import { EnvironmentResource } from '@clerk/types'; -import { AuthConfig, Session } from 'core/resources/internal'; +import { AuthConfig } from 'core/resources/AuthConfig'; +import { Session } from 'core/resources/Session'; import React from 'react'; import { SignUp } from 'ui/signUp/SignUp'; @@ -26,7 +27,11 @@ jest.mock('ui/contexts', () => { displayConfig: { homeUrl: 'https://www.bbc.com', }, - authConfig: { singleSessionMode: true } as Partial, + authConfig: { + identificationStrategies: ['email_address', 'oauth_google'], + firstFactors: ['email_address', 'oauth_google'], + singleSessionMode: true, + } as Partial, } as Partial), ), withCoreSessionSwitchGuard: (a: any) => a, diff --git a/packages/clerk-js/src/ui/signUp/SignUpStart.test.tsx b/packages/clerk-js/src/ui/signUp/SignUpStart.test.tsx index cfb7903bbfd..9abb3db1554 100644 --- a/packages/clerk-js/src/ui/signUp/SignUpStart.test.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUpStart.test.tsx @@ -1,7 +1,13 @@ -import { render, renderJSON, screen, userEvent, waitFor } from '@clerk/shared/testUtils'; +import { + render, + renderJSON, + screen, + userEvent, + waitFor, +} from '@clerk/shared/testUtils'; import { titleize } from '@clerk/shared/utils/string'; -import { UserSettingsJSON } from '@clerk/types'; -import { Session, UserSettings } from 'core/resources/internal'; +import { Session } from 'core/resources'; +import { AuthConfig } from 'core/resources/AuthConfig'; import React from 'react'; import { useCoreSignUp } from 'ui/contexts'; @@ -11,7 +17,8 @@ const navigateMock = jest.fn(); const mockCreateRequest = jest.fn(); const mockSetSession = jest.fn(); const mockAuthenticateWithRedirect = jest.fn(); -let mockUserSettings: UserSettings; +const mockIdentificationRequirements = jest.fn(); +let mockAuthConfig: Partial; const oldWindowLocation = window.location; const setWindowQueryParams = (params: Array<[string, string]>) => { @@ -55,8 +62,7 @@ jest.mock('ui/contexts', () => { applicationName: 'My test app', afterSignUpUrl: 'http://test.host', }, - userSettings: mockUserSettings, - authConfig: { singleSessionMode: false }, + authConfig: mockAuthConfig, })), }; }); @@ -73,9 +79,9 @@ describe('', () => { const { location } = window; beforeEach(() => { - // mockIdentificationRequirements.mockImplementation(() => [ - // ['email_address', 'oauth_google', 'oauth_facebook'], - // ]); + mockIdentificationRequirements.mockImplementation(() => [ + ['email_address', 'oauth_google', 'oauth_facebook'], + ]); mockCreateRequest.mockImplementation(() => Promise.resolve({ @@ -88,43 +94,13 @@ describe('', () => { }), ); - mockUserSettings = new UserSettings({ - attributes: { - username: { - enabled: true, - }, - first_name: { - enabled: true, - required: true, - }, - last_name: { - enabled: true, - required: true, - }, - password: { - enabled: true, - required: true, - }, - email_address: { - enabled: true, - required: true, - used_for_first_factor: true, - }, - phone_number: { - enabled: true, - }, - }, - social: { - oauth_google: { - enabled: true, - strategy: 'oauth_google', - }, - oauth_facebook: { - enabled: true, - strategy: 'oauth_facebook', - }, - }, - } as UserSettingsJSON); + mockAuthConfig = { + username: 'on', + firstName: 'required', + lastName: 'required', + password: 'required', + identificationRequirements: mockIdentificationRequirements(), + }; }); afterEach(() => { @@ -135,7 +111,7 @@ describe('', () => { global.window.location = location; }); - it('renders the sign up start screen', () => { + it('renders the sign up start screen', async () => { const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); @@ -143,11 +119,14 @@ describe('', () => { it('renders the start screen, types the name, email, and password and creates a sign up attempt', async () => { render(); - userEvent.type(screen.getByLabelText('First name'), 'John'); - userEvent.type(screen.getByLabelText('Last name'), 'Doe'); - userEvent.type(screen.getByLabelText('Username'), 'jdoe'); - userEvent.type(screen.getByLabelText('Email address'), 'jdoe@example.com'); - userEvent.type(screen.getByLabelText('Password'), 'p@ssW0rd'); + await userEvent.type(screen.getByLabelText('First name'), 'John'); + await userEvent.type(screen.getByLabelText('Last name'), 'Doe'); + await userEvent.type(screen.getByLabelText('Username'), 'jdoe'); + await userEvent.type( + screen.getByLabelText('Email address'), + 'jdoe@example.com', + ); + await userEvent.type(screen.getByLabelText('Password'), 'p@ssW0rd'); userEvent.click(screen.getByRole('button', { name: 'Sign up' })); @@ -191,8 +170,9 @@ describe('', () => { }, ); - it('renders the external account verification error if available', () => { - const errorMsg = 'You cannot sign up with sokratis.vidros@gmail.com since this is an invitation-only application'; + it('renders the external account verification error if available', async () => { + const errorMsg = + 'You cannot sign up with sokratis.vidros@gmail.com since this is an invitation-only application'; (useCoreSignUp as jest.Mock).mockImplementation(() => { return { @@ -214,42 +194,25 @@ describe('', () => { expect(mockCreateRequest).toHaveBeenNthCalledWith(1, {}); }); - it('only renders the SSO buttons if no other method is supported', () => { - mockUserSettings = new UserSettings({ - attributes: { - username: { - enabled: false, - }, - email_address: { - enabled: true, - }, - phone_number: { - enabled: true, - }, - password: { - required: false, - }, - }, - social: { - oauth_google: { - enabled: true, - strategy: 'oauth_google', - }, - oauth_facebook: { - enabled: true, - strategy: 'oauth_facebook', - }, - }, - } as UserSettingsJSON); + it('only renders the SSO buttons if no other method is supported', async () => { + mockIdentificationRequirements.mockImplementation(() => [ + ['oauth_google', 'oauth_facebook'], + ]); + mockAuthConfig = { + username: 'off', + identificationRequirements: mockIdentificationRequirements(), + }; render(); screen.getByRole('button', { name: /Google/ }); screen.getByRole('button', { name: /Facebook/ }); - expect(screen.queryByRole('button', { name: 'Sign up' })).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'Sign up' }), + ).not.toBeInTheDocument(); }); describe('when the user does not grant access to their Facebook account', () => { - it('renders the external account verification error if available', () => { + it('renders the external account verification error if available', async () => { const errorMsg = 'You did not grant access to your Facebook account'; (useCoreSignUp as jest.Mock).mockImplementation(() => { @@ -333,17 +296,9 @@ describe('', () => { }); it('does not render the phone number field', async () => { - mockUserSettings = new UserSettings({ - attributes: { - phone_number: { - enabled: true, - required: true, - }, - password: { - required: false, - }, - }, - } as UserSettingsJSON); + mockIdentificationRequirements.mockImplementation(() => [ + ['phone_number'], + ]); const { container } = render(); const labels = container.querySelectorAll('label'); diff --git a/packages/clerk-js/src/ui/signUp/SignUpStart.tsx b/packages/clerk-js/src/ui/signUp/SignUpStart.tsx index 9d713c6df2e..fc8bcb0c742 100644 --- a/packages/clerk-js/src/ui/signUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUpStart.tsx @@ -2,7 +2,7 @@ import { Control } from '@clerk/shared/components/control'; import { Form } from '@clerk/shared/components/form'; import { Input } from '@clerk/shared/components/input'; import { PhoneInput } from '@clerk/shared/components/phoneInput'; -import { SignUpResource } from '@clerk/types'; +import { OAuthStrategy, SignUpResource, Web3Strategy } from '@clerk/types'; import React from 'react'; import type { FieldState } from 'ui/common'; import { @@ -29,14 +29,17 @@ import { getClerkQueryParam } from 'utils/getClerkQueryParam'; import { SignInLink } from './SignInLink'; import { SignUpOAuth } from './SignUpOAuth'; import { SignUpWeb3 } from './SignUpWeb3'; -import { determineFirstPartyFields } from './utils'; +import { + determineFirstPartyFields, + determineOauthOptions, + determineWeb3Options, +} from './utils'; type ActiveIdentifier = 'emailAddress' | 'phoneNumber'; function _SignUpStart(): JSX.Element { const { navigate } = useNavigate(); const environment = useEnvironment(); - const { userSettings } = environment; const { setSession } = useCoreClerk(); const { navigateAfterSignUp } = useSignUpContext(); const [emailOrPhoneActive, setEmailOrPhoneActive] = @@ -59,12 +62,10 @@ function _SignUpStart(): JSX.Element { const [error, setError] = React.useState(); const hasInvitationToken = !!formFields.invitationToken.value; - const fields = determineFirstPartyFields(environment, hasInvitationToken); - const oauthOptions = userSettings.socialProviderStrategies; - const web3Options = userSettings.web3FirstFactors; - - const handleInvitationFlow = () => { + const oauthOptions = determineOauthOptions(environment) as OAuthStrategy[]; + const web3Options = determineWeb3Options(environment) as Web3Strategy[]; + const handleInvitationFlow = async () => { const token = formFields.invitationToken.value; if (!token) { return; @@ -148,7 +149,7 @@ function _SignUpStart(): JSX.Element { } }; - const completeSignUpFlow = (su: SignUpResource) => { + const completeSignUpFlow = async (su: SignUpResource) => { if (su.status === 'complete') { return setSession(su.createdSessionId, navigateAfterSignUp); } else if ( diff --git a/packages/clerk-js/src/ui/signUp/SignUpVerify.test.tsx b/packages/clerk-js/src/ui/signUp/SignUpVerify.test.tsx index 6153df4ec6d..f2ef2540df6 100644 --- a/packages/clerk-js/src/ui/signUp/SignUpVerify.test.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUpVerify.test.tsx @@ -1,11 +1,9 @@ import { render, renderJSON } from '@clerk/shared/testUtils'; import { - AttributeData, SessionResource, + SignInStrategyName, SignUpResource, - UserSettingsJSON, } from '@clerk/types'; -import { UserSettings } from 'core/resources/internal'; import React from 'react'; import { @@ -27,7 +25,7 @@ const mockStartMagicLinkFlow = jest.fn(() => { }, } as any as SignUpResource); }); -let mockEmailAddressAttribute: Partial; +let mockFirstFactors: SignInStrategyName[]; let mockDisabledStrategies: string[] = []; jest.mock('ui/router/RouteContext'); @@ -39,42 +37,16 @@ jest.mock('ui/contexts', () => { applicationName: 'My test app', afterSignUpUrl: 'http://test.host', }, - userSettings: new UserSettings({ - attributes: { - first_name: { - enabled: true, - required: false, - }, - last_name: { - enabled: true, - required: false, - }, - password: { - enabled: true, - required: false, - }, - username: { - enabled: true, - required: false, - }, - email_address: mockEmailAddressAttribute, - }, - social: { - oauth_google: { - enabled: true, - required: false, - authenticatable: false, - strategy: 'oauth_google', - }, - oauth_facebook: { - enabled: true, - required: false, - authenticatable: false, - strategy: 'oauth_facebook', - }, - }, - } as unknown as UserSettingsJSON), - authConfig: { singleSessionMode: false }, + authConfig: { + username: 'on', + firstName: 'required', + lastName: 'required', + password: 'required', + firstFactors: mockFirstFactors, + identificationRequirements: [ + ['email_address', 'phone_address', 'oauth_google', 'oauth_facebook'], + ], + }, })), useCoreSession: () => { return { id: 'sess_id' } as SessionResource; @@ -133,29 +105,20 @@ describe('', () => { }); describe('verify email address', () => { - it('renders the OTP sign up verification form', () => { - mockEmailAddressAttribute = { - enabled: true, - verifications: ['email_code'], - }; + it('renders the sign up verification form', async () => { + mockFirstFactors = ['email_code', 'phone_code', 'password']; const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); - it('renders the magic link sign up verification form ', () => { - mockEmailAddressAttribute = { - enabled: true, - verifications: ['email_link'], - }; + it('renders the sign up verification form but prefers email_link if exists', async () => { + mockFirstFactors = ['email_code', 'phone_code', 'password', 'email_link']; const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); - it('can skip disabled verification strategies', () => { - mockEmailAddressAttribute = { - enabled: true, - verifications: ['email_link'], - }; + it('can skip disabled verification strategies', async () => { + mockFirstFactors = ['email_code', 'phone_code', 'password', 'email_link']; mockDisabledStrategies = ['email_link']; const { container } = render(); expect(container.querySelector('.cl-otp-input')).not.toBeNull(); @@ -163,7 +126,7 @@ describe('', () => { }); describe('verify phone number', () => { - it('renders the OTP sign up verification form', () => { + it('renders the sign up verification form', async () => { const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/packages/clerk-js/src/ui/signUp/SignUpVerify.tsx b/packages/clerk-js/src/ui/signUp/SignUpVerify.tsx index 99335375494..11d2c923ae2 100644 --- a/packages/clerk-js/src/ui/signUp/SignUpVerify.tsx +++ b/packages/clerk-js/src/ui/signUp/SignUpVerify.tsx @@ -24,15 +24,18 @@ import { import { SignUpVerifyEmailAddressWithMagicLink } from './SignUpVerifyEmailAddressWithMagicLink'; +const emailLinkStrategy = 'email_link'; + function _SignUpVerifyEmailAddress(): JSX.Element { - const { userSettings } = useEnvironment(); - const { attributes } = userSettings; + const { authConfig } = useEnvironment(); + const { firstFactors } = authConfig; - const emailLinkStrategyEnabled = - attributes.email_address.verifications.includes('email_link'); + // TODO: SignUp should have a field similar to SignIn's supportedFirstFactors + // listing the available strategies for this signUp + const emailLinkStrategyEnabled = firstFactors.includes(emailLinkStrategy); const disableEmailLink = shouldDisableStrategy( useSignUpContext(), - 'email_link', + emailLinkStrategy, ); if (emailLinkStrategyEnabled && !disableEmailLink) { diff --git a/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpVerify.test.tsx.snap b/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpVerify.test.tsx.snap index ee5b8822357..a9e5c2372c3 100644 --- a/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpVerify.test.tsx.snap +++ b/packages/clerk-js/src/ui/signUp/__snapshots__/SignUpVerify.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` verify email address renders the OTP sign up verification form 1`] = ` +exports[` verify email address renders the sign up verification form 1`] = ` Array [
verify email address renders the magic link sign up verification form 1`] = ` +exports[` verify email address renders the sign up verification form but prefers email_link if exists 1`] = `
@@ -203,7 +203,7 @@ exports[` verify email address renders the magic link sign up ver
`; -exports[` verify phone number renders the OTP sign up verification form 1`] = ` +exports[` verify phone number renders the sign up verification form 1`] = ` Array [
{ // For specs refer to https://www.notion.so/clerkdev/Vocabulary-8f775765258643978f5811c88b140b2d // and the current Instance User settings options describe('returns first party field based on auth config', () => { - type Scenario = [string, any, any]; + type Scenario = [string, Object, Object]; const scenaria: Scenario[] = [ [ 'email only option', { - userSettings: new UserSettings({ - attributes: { - email_address: { - enabled: true, - required: true, - used_for_first_factor: true, - }, - phone_number: { - enabled: true, - required: false, - }, - first_name: { - enabled: true, - required: true, - }, - last_name: { - enabled: true, - required: true, - }, - password: { - enabled: true, - required: true, - }, - username: { - enabled: true, - }, - }, - } as UserSettingsJSON), + authConfig: { + identificationRequirements: [['email_address']], + firstName: 'required', + lastName: 'required', + password: 'required', + username: 'on', + }, }, { emailAddress: 'required', @@ -53,34 +35,13 @@ describe('determineFirstPartyFields()', () => { [ 'phone only option', { - userSettings: new UserSettings({ - attributes: { - email_address: { - enabled: true, - required: false, - }, - phone_number: { - enabled: true, - required: true, - used_for_first_factor: true, - }, - first_name: { - enabled: true, - required: true, - }, - last_name: { - enabled: true, - required: true, - }, - password: { - enabled: true, - required: true, - }, - username: { - enabled: true, - }, - }, - } as UserSettingsJSON), + authConfig: { + identificationRequirements: [['phone_number']], + firstName: 'required', + lastName: 'required', + password: 'required', + username: 'on', + }, }, { phoneNumber: 'required', @@ -93,35 +54,13 @@ describe('determineFirstPartyFields()', () => { [ 'email or phone option', { - userSettings: new UserSettings({ - attributes: { - phone_number: { - enabled: true, - required: true, - used_for_first_factor: true, - }, - email_address: { - enabled: true, - required: true, - used_for_first_factor: true, - }, - first_name: { - enabled: true, - required: true, - }, - last_name: { - enabled: true, - required: true, - }, - password: { - enabled: true, - required: true, - }, - username: { - enabled: true, - }, - }, - } as UserSettingsJSON), + authConfig: { + identificationRequirements: [['email_address'], ['phone_number']], + firstName: 'required', + lastName: 'required', + password: 'required', + username: 'on', + }, }, { emailOrPhone: 'required', @@ -134,31 +73,16 @@ describe('determineFirstPartyFields()', () => { [ 'optional first and last name', { - userSettings: new UserSettings({ - attributes: { - first_name: { - enabled: true, - }, - last_name: { - enabled: true, - }, - password: { - enabled: true, - required: true, - }, - username: { - enabled: true, - }, - email_address: { - enabled: true, - }, - phone_number: { - enabled: true, - }, - }, - } as UserSettingsJSON), + authConfig: { + identificationRequirements: [['email_address'], ['phone_number']], + firstName: 'on', + lastName: 'on', + password: 'required', + username: 'on', + }, }, { + emailOrPhone: 'required', firstName: 'on', lastName: 'on', password: 'required', @@ -168,52 +92,76 @@ describe('determineFirstPartyFields()', () => { [ 'no fields enabled', { - userSettings: new UserSettings({ - attributes: { - phone_number: { - enabled: false, - required: false, - }, - email_address: { - enabled: false, - required: false, - }, - first_name: { - enabled: false, - required: false, - }, - last_name: { - enabled: false, - required: false, - }, - password: { - enabled: false, - required: false, - }, - username: { - enabled: false, - }, - }, - } as UserSettingsJSON), + authConfig: { + identificationRequirements: [], + firstName: 'off', + lastName: 'off', + password: 'off', + username: 'off', + }, }, {}, ], ]; it.each(scenaria)('%s', (___, environment, result) => { - expect(determineFirstPartyFields(environment as EnvironmentResource)).toEqual(result); + expect( + determineFirstPartyFields(environment as EnvironmentResource), + ).toEqual(result); }); it.each(scenaria)('with invitation, %s', (___, environment, result) => { // Email address or phone number cannot be required when there's an // invitation token present. Instead, we'll require the invitationToken // parameter - const expected = { ...result, invitationToken: 'required' }; - delete expected.emailAddress; - delete expected.phoneNumber; - delete expected.emailOrPhone; - const res = determineFirstPartyFields(environment as EnvironmentResource, true); - expect(res).toMatchObject(expected); + const { + // @ts-ignore 2339 + emailAddress: ___emailAddress, + // @ts-ignore 2339 + emailOrPhone: ___emailOrPhone, + // @ts-ignore 2339 + phoneNumber: ___phoneNumber, + ...expected + } = result; + expect( + determineFirstPartyFields(environment as EnvironmentResource, true), + ).toEqual({ ...expected, invitationToken: 'required' }); }); }); }); + +describe('determineOauthOptions(environment)', () => { + it('returns oauth options based on auth config', () => { + const environment = { + authConfig: { + identificationRequirements: [ + ['email_address', 'oauth_google', 'oauth_facebook'], + ['oauth_google'], + ], + }, + } as EnvironmentResource; + + expect(determineOauthOptions(environment)).toEqual([ + 'oauth_facebook', + 'oauth_google', + ]); + }); +}); + +describe('determineWeb3Options(environment)', () => { + it('returns web3 options based on auth config', () => { + const environment = { + authConfig: { + firstFactors: [ + 'email_address', + 'web3_metamask_signature', + 'oauth_facebook', + ], + }, + } as EnvironmentResource; + + expect(determineWeb3Options(environment)).toEqual([ + 'web3_metamask_signature', + ]); + }); +}); diff --git a/packages/clerk-js/src/ui/signUp/utils.ts b/packages/clerk-js/src/ui/signUp/utils.ts index 164c722567b..5c7c232beb2 100644 --- a/packages/clerk-js/src/ui/signUp/utils.ts +++ b/packages/clerk-js/src/ui/signUp/utils.ts @@ -1,5 +1,9 @@ -import { snakeToCamel } from '@clerk/shared/utils'; -import type { Attributes, EnvironmentResource } from '@clerk/types'; +import type { + EnvironmentResource, + IdentificationStrategy, + SignInStrategyName, + ToggleTypeWithRequire, +} from '@clerk/types'; type FieldKeys = | 'emailOrPhone' @@ -11,38 +15,66 @@ type FieldKeys = | 'password' | 'invitationToken'; -// TODO: Refactor SignUp component and remove -// this leftover type type Fields = { - [key in FieldKeys]?: 'on' | 'off' | 'required'; + [key in FieldKeys]?: ToggleTypeWithRequire; }; -function isEmailOrPhone(attributes: Attributes) { - return attributes.email_address.used_for_first_factor && attributes.phone_number.used_for_first_factor; -} - -export function determineFirstPartyFields(environment: EnvironmentResource, hasInvitation?: boolean): Fields { - const {attributes} = environment.userSettings; +export function determineFirstPartyFields( + environment: EnvironmentResource, + hasInvitation?: boolean, +): Fields { + const idRequirements = + environment.authConfig.identificationRequirements.flat(); const fields: Fields = {}; - Object.entries(attributes) - .filter(([key]) => ['username', 'first_name', 'last_name'].includes(key)) - .filter(([, desc]) => desc.enabled) - .forEach(([key, desc]) => (fields[snakeToCamel(key) as keyof Fields] = desc.required ? 'required' : 'on')); - + const idByEmail = idRequirements.includes('email_address'); + const idByPhone = idRequirements.includes('phone_number'); + const idByEmailOrPhone = idByEmail && idByPhone; + const idByUsername = + idRequirements.includes('username') || + environment.authConfig.username === 'on'; if (hasInvitation) { fields.invitationToken = 'required'; - } else if (isEmailOrPhone(attributes)) { + } else if (idByEmailOrPhone) { fields.emailOrPhone = 'required'; - } else if (attributes.email_address.used_for_first_factor) { + } else if (idByEmail) { fields.emailAddress = 'required'; - } else if (attributes.phone_number.used_for_first_factor) { + } else if (idByPhone) { fields.phoneNumber = 'required'; } - if (attributes.password.required) { - fields.password = 'required'; + if (idByEmailOrPhone || idByEmail || idByPhone) { + if (idByUsername) { + fields.username = environment.authConfig.username; + } + + if (environment.authConfig.password === 'required') { + fields.password = environment.authConfig.password; + } + + if (['on', 'required'].includes(environment.authConfig.firstName)) { + fields.firstName = environment.authConfig.firstName; + } + if (['on', 'required'].includes(environment.authConfig.lastName)) { + fields.lastName = environment.authConfig.lastName; + } } return fields; } + +export function determineOauthOptions( + environment: EnvironmentResource, +): IdentificationStrategy[] { + const idRequirements = [ + ...new Set(environment.authConfig.identificationRequirements.flat()), + ]; + return idRequirements.filter(fac => fac.startsWith('oauth')).sort(); +} + +export function determineWeb3Options( + environment: EnvironmentResource, +): SignInStrategyName[] { + const idRequirements = [...new Set(environment.authConfig.firstFactors)]; + return idRequirements.filter(fac => fac.startsWith('web3')).sort(); +} diff --git a/packages/clerk-js/src/ui/userProfile/UserProfile.test.tsx b/packages/clerk-js/src/ui/userProfile/UserProfile.test.tsx index d4b2a7d9f25..0ffd31c77ab 100644 --- a/packages/clerk-js/src/ui/userProfile/UserProfile.test.tsx +++ b/packages/clerk-js/src/ui/userProfile/UserProfile.test.tsx @@ -10,11 +10,8 @@ import { SignInResource, SignUpResource, } from '@clerk/types'; -import { - AuthConfig, - ExternalAccount, - UserSettings, -} from 'core/resources/internal'; +import { AuthConfig } from 'core/resources/AuthConfig'; +import { ExternalAccount } from 'core/resources/ExternalAccount'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -69,47 +66,11 @@ jest.mock('ui/contexts', () => ({ }, }, }, - userSettings: { - attributes: { - phone_number: { - // this should be true since it is a first factor but keeping it false for the needs of the test case - enabled: false, - used_for_second_factor: true, - second_factors: ['phone_code'], - }, - email_address: { - // this should be true since it is a first factor but keeping it false for the needs of the test case - enabled: false, - used_for_first_factor: true, - first_factors: ['email_code'], - }, - first_name: { - enabled: true, - }, - last_name: { - enabled: true, - }, - username: { - enabled: false, - }, - password: { - enabled: false, - }, - web3_wallet: { - enabled: false, - }, - }, - social: { - oauth_google: { - enabled: true, - }, - oauth_facebook: { - enabled: true, - }, - }, - } as Partial, authConfig: { + secondFactors: ['phone_code'], singleSessionMode: true, + firstFactors: ['email_address', 'oauth_google', 'oauth_facebook'], + emailAddressVerificationStrategies: ['email_code'], } as Partial, })), withCoreUserGuard: (a: any) => a, diff --git a/packages/clerk-js/src/ui/userProfile/account/Account.test.tsx b/packages/clerk-js/src/ui/userProfile/account/Account.test.tsx index 8f806cfbc7c..132e131325e 100644 --- a/packages/clerk-js/src/ui/userProfile/account/Account.test.tsx +++ b/packages/clerk-js/src/ui/userProfile/account/Account.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@clerk/shared/testUtils'; -import type { UserResource, UserSettingsResource } from '@clerk/types'; +import type { AuthConfigResource, UserResource } from '@clerk/types'; import React from 'react'; import { Account } from './Account'; @@ -26,21 +26,6 @@ jest.mock('ui/hooks', () => { }; }); -const OTHER_ATTRIBUTES = { - username: { - enabled: false, - }, - email_address: { - enabled: false - }, - phone_number: { - enabled: false - }, - web3_wallet: { - enabled: false - } -}; - describe('', () => { afterEach(() => { jest.clearAllMocks(); @@ -49,18 +34,9 @@ describe('', () => { it('renders personal information for auth config that requires name', () => { mockEnvironment.mockImplementation(() => { return { - userSettings: { - attributes: { - first_name: { - enabled: true, - required: true - }, - last_name: { - enabled: false - }, - ...OTHER_ATTRIBUTES - } - } as UserSettingsResource, + authConfig: { + firstName: 'required', + } as AuthConfigResource, }; }); render(); @@ -70,17 +46,9 @@ describe('', () => { it('renders personal information for auth config that has name turned on', () => { mockEnvironment.mockImplementation(() => { return { - userSettings: { - attributes: { - last_name: { - enabled: true, - }, - first_name: { - enabled: false, - }, - ...OTHER_ATTRIBUTES - } - } as UserSettingsResource + authConfig: { + lastName: 'on', + } as AuthConfigResource, }; }); render(); @@ -90,17 +58,10 @@ describe('', () => { it('does not render personal information for auth config that has named turned off', () => { mockEnvironment.mockImplementation(() => { return { - userSettings: { - attributes: { - first_name: { - enabled: false, - }, - last_name: { - enabled: false, - }, - ...OTHER_ATTRIBUTES - } - } as UserSettingsResource + authConfig: { + firstName: 'off', + lastName: 'off', + } as AuthConfigResource, }; }); render(); diff --git a/packages/clerk-js/src/ui/userProfile/account/Account.tsx b/packages/clerk-js/src/ui/userProfile/account/Account.tsx index fc43af04984..3b47f6c91e5 100644 --- a/packages/clerk-js/src/ui/userProfile/account/Account.tsx +++ b/packages/clerk-js/src/ui/userProfile/account/Account.tsx @@ -1,9 +1,13 @@ +import type { AuthConfigResource } from '@clerk/types'; import React from 'react'; +import { useEnvironment } from 'ui/contexts'; import { PersonalInformationCard } from 'ui/userProfile/account/personalInformation'; import { ProfileCard } from 'ui/userProfile/account/profileCard'; import { PageHeading } from 'ui/userProfile/pageHeading'; export const Account = (): JSX.Element => { + const { authConfig } = useEnvironment(); + return ( <> { subtitle='Manage settings related to your account' /> - + {shouldShowPersonalInformation(authConfig) && } ); }; + +function shouldShowPersonalInformation( + authConfig: AuthConfigResource, +): boolean { + return authConfig.firstName !== 'off' || authConfig.lastName !== 'off'; +} diff --git a/packages/clerk-js/src/ui/userProfile/account/personalInformation/PersonalInformationCard.tsx b/packages/clerk-js/src/ui/userProfile/account/personalInformation/PersonalInformationCard.tsx index a151c248b26..04dec96af3d 100644 --- a/packages/clerk-js/src/ui/userProfile/account/personalInformation/PersonalInformationCard.tsx +++ b/packages/clerk-js/src/ui/userProfile/account/personalInformation/PersonalInformationCard.tsx @@ -1,23 +1,12 @@ import { List } from '@clerk/shared/components/list'; import { TitledCard } from '@clerk/shared/components/titledCard'; import React from 'react'; -import { useCoreUser, useEnvironment } from 'ui/contexts'; +import { useCoreUser } from 'ui/contexts'; import { useNavigate } from 'ui/hooks'; -export const PersonalInformationCard = (): JSX.Element | null => { +export const PersonalInformationCard = (): JSX.Element => { const user = useCoreUser(); const { navigate } = useNavigate(); - const { userSettings } = useEnvironment(); - const { - attributes: { first_name, last_name }, - } = userSettings; - - const hasAtLeastOneAttributeEnable = - first_name?.enabled || last_name?.enabled; - - if (!hasAtLeastOneAttributeEnable) { - return null; - } const firstNameRow = ( { ); - const showFirstName = first_name.enabled; - const showLastName = last_name.enabled; - return ( { subtitle='Manage personal information settings' > - {showFirstName && firstNameRow} - {showLastName && lastnameRow} + {firstNameRow} + {lastnameRow} ); diff --git a/packages/clerk-js/src/ui/userProfile/account/profileCard/ProfileCard.test.tsx b/packages/clerk-js/src/ui/userProfile/account/profileCard/ProfileCard.test.tsx index f7776423183..b834b1a6b2b 100644 --- a/packages/clerk-js/src/ui/userProfile/account/profileCard/ProfileCard.test.tsx +++ b/packages/clerk-js/src/ui/userProfile/account/profileCard/ProfileCard.test.tsx @@ -1,10 +1,6 @@ import { renderJSON } from '@clerk/shared/testUtils'; import { EmailAddressResource, UserResource } from '@clerk/types'; import * as React from 'react'; -import { PartialDeep } from 'type-fest'; -import { - useEnvironment -} from 'ui/contexts'; import { ProfileCard } from './ProfileCard'; @@ -16,8 +12,15 @@ jest.mock('ui/hooks', () => { jest.mock('ui/contexts', () => { return { - useEnvironment: jest.fn(), - useCoreUser: (): PartialDeep => { + useEnvironment: () => ({ + authConfig: { + emailAddress: 'required', + phoneNumber: 'on', + username: 'off', + }, + displayConfig: {}, + }), + useCoreUser: (): Partial => { return { isPrimaryIdentification: jest.fn(() => true), emailAddresses: [ @@ -34,65 +37,15 @@ jest.mock('ui/contexts', () => { ], phoneNumbers: [], externalAccounts: [], - web3Wallets: [{ web3Wallet: '0x123456789' }] }; }, useCoreClerk: jest.fn(), }; }); -(useEnvironment as jest.Mock).mockImplementation(() => ({ - displayConfig: {}, - userSettings: { - attributes: { - email_address: { - enabled: true - }, - phone_number: { - enabled: true - }, - username: { - enabled: false - }, - web3_wallet: { - enabled: false - } - } - } -})); - describe('', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - it('renders the ProfileCard', () => { const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); - - it('renders the ProfileCard ', () => { - (useEnvironment as jest.Mock).mockImplementation(() => ({ - displayConfig: {}, - userSettings: { - attributes: { - email_address: { - enabled: true - }, - phone_number: { - enabled: true - }, - username: { - enabled: false - }, - web3_wallet: { - enabled: true - } - } - } - })); - - const tree = renderJSON(); - expect(tree).toMatchSnapshot(); - }); }); diff --git a/packages/clerk-js/src/ui/userProfile/account/profileCard/ProfileCard.tsx b/packages/clerk-js/src/ui/userProfile/account/profileCard/ProfileCard.tsx index 3966fd25a0c..2e9c9f87591 100644 --- a/packages/clerk-js/src/ui/userProfile/account/profileCard/ProfileCard.tsx +++ b/packages/clerk-js/src/ui/userProfile/account/profileCard/ProfileCard.tsx @@ -21,9 +21,12 @@ function getWeb3WalletAddress(user: UserResource): string { return ''; } +function checkIfSectionIsOn(section: string) { + return section === 'on' || section === 'required'; +} + export function ProfileCard(): JSX.Element { - const { userSettings } = useEnvironment(); - const { attributes } = userSettings; + const { authConfig } = useEnvironment(); const user = useCoreUser(); const { navigate } = useNavigate(); const web3Wallet = getWeb3WalletAddress(user); @@ -97,10 +100,10 @@ export function ProfileCard(): JSX.Element { ); - const showWebWallet = attributes.web3_wallet.enabled; - const showUsername = attributes.username.enabled; - const showEmail = attributes.email_address.enabled; - const showPhone = attributes.phone_number.enabled; + const showWebWallet = !!web3Wallet; + const showUsername = checkIfSectionIsOn(authConfig.username); + const showEmail = checkIfSectionIsOn(authConfig.emailAddress); + const showPhone = checkIfSectionIsOn(authConfig.phoneNumber); return ( renders the ProfileCard 1`] = ` -
-
-

- Profile -

-

- Manage profile settings -

-
-
-
-
- Photo -
-
-
-
- -
- -
-
- -
-
-
-
-
-
- - - - -
-
-`; - exports[` renders the ProfileCard 1`] = `
{ jest.mock('ui/contexts/EnvironmentContext', () => { return { useEnvironment: jest.fn(() => ({ - userSettings: { - attributes: { - email_address: { - enabled: true, - verifications: ['email_link'], - } - } - } - }) as PartialDeep), + authConfig: { + firstFactors: ['email_link'], + emailAddressVerificationStrategies: ['email_link'], + }, + })), }; }); diff --git a/packages/clerk-js/src/ui/userProfile/emailAdressess/EmailAddressVerificationWithMagicLink.test.tsx b/packages/clerk-js/src/ui/userProfile/emailAdressess/EmailAddressVerificationWithMagicLink.test.tsx index b95a78b1231..a7297c5d3a3 100644 --- a/packages/clerk-js/src/ui/userProfile/emailAdressess/EmailAddressVerificationWithMagicLink.test.tsx +++ b/packages/clerk-js/src/ui/userProfile/emailAdressess/EmailAddressVerificationWithMagicLink.test.tsx @@ -7,6 +7,11 @@ import { EmailAddressVerificationWithMagicLink } from './EmailAddressVerificatio jest.mock('ui/contexts', () => { return { + useEnvironment: jest.fn(() => ({ + authConfig: { + firstFactors: ['email_link'], + }, + })), useUserProfileContext: jest.fn(() => { return { routing: 'path', diff --git a/packages/clerk-js/src/ui/userProfile/emailAdressess/EmailDetail.test.tsx b/packages/clerk-js/src/ui/userProfile/emailAdressess/EmailDetail.test.tsx index 1549f7c832a..62464a23cf6 100644 --- a/packages/clerk-js/src/ui/userProfile/emailAdressess/EmailDetail.test.tsx +++ b/packages/clerk-js/src/ui/userProfile/emailAdressess/EmailDetail.test.tsx @@ -1,14 +1,14 @@ import { render, renderJSON, screen, userEvent } from '@clerk/shared/testUtils'; -import { EmailAddressResource, EnvironmentResource, SignInStrategyName, UserSettingsResource } from '@clerk/types'; +import { EmailAddressResource } from '@clerk/types'; +import { SignInStrategyName } from '@clerk/types'; import { ClerkAPIResponseError } from 'core/resources/Error'; import React from 'react'; -import { PartialDeep } from 'type-fest'; -import { EmailDetail } from './EmailDetail'; +import { EmailDetail } from './EmailDetail'; const mockNavigate = jest.fn(); const mockUseCoreUser = jest.fn(); -let mockFirstFactors = {} as PartialDeep; +let mockFirstFactors = [] as SignInStrategyName[]; jest.mock('ui/hooks', () => ({ useNavigate: () => { @@ -39,8 +39,11 @@ jest.mock('ui/contexts', () => { return { useCoreUser: () => mockUseCoreUser(), useEnvironment: jest.fn(() => ({ - userSettings: mockFirstFactors - }) as PartialDeep), + authConfig: { + firstFactors: mockFirstFactors, + emailAddressVerificationStrategies: mockFirstFactors, + }, + })), useUserProfileContext: jest.fn(() => { return { routing: 'path', @@ -72,14 +75,7 @@ describe('', () => { }); it('displays verification errors', async () => { - mockFirstFactors = { - attributes: { - email_address: { - used_for_first_factor: true, - first_factors: ['email_code'] - } - } - } + mockFirstFactors = ['email_code']; mockUseCoreUser.mockImplementation(() => { return { primaryEmailAddressId: '1', diff --git a/packages/clerk-js/src/ui/userProfile/emailAdressess/utils.ts b/packages/clerk-js/src/ui/userProfile/emailAdressess/utils.ts index ef950b3c3d9..285df08801a 100644 --- a/packages/clerk-js/src/ui/userProfile/emailAdressess/utils.ts +++ b/packages/clerk-js/src/ui/userProfile/emailAdressess/utils.ts @@ -3,9 +3,8 @@ import { EnvironmentResource } from '@clerk/types'; export function magicLinksEnabledForInstance( env: EnvironmentResource, ): boolean { - const { userSettings } = env; - const { email_address } = userSettings.attributes; - return ( - email_address.enabled && email_address.verifications.includes('email_link') - ); + // TODO: email verification should have a supported strategies field + const { authConfig } = env; + const { emailAddressVerificationStrategies } = authConfig; + return emailAddressVerificationStrategies.includes('email_link'); } diff --git a/packages/clerk-js/src/ui/userProfile/security/ChangePassword.test.tsx b/packages/clerk-js/src/ui/userProfile/security/ChangePassword.test.tsx deleted file mode 100644 index fc55af4ba17..00000000000 --- a/packages/clerk-js/src/ui/userProfile/security/ChangePassword.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { render, screen } from '@clerk/shared/testUtils'; -import { EnvironmentResource, UserResource } from '@clerk/types'; -import * as React from 'react'; -import { PartialDeep } from 'type-fest'; -import { useEnvironment } from 'ui/contexts'; -import { ChangePassword } from 'ui/userProfile/security/ChangePassword'; - - -jest.mock('ui/hooks', () => ({ - useNavigate: () => ({ navigate: jest.fn() }), -})); - -jest.mock('ui/router/RouteContext'); - -jest.mock('ui/contexts', () => { - return { - useCoreUser: (): Partial => ({ - passwordEnabled: true - }), - useEnvironment: jest.fn(), - }; -}); - -describe('', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('renders the ChangePassword page with showing remove password cta', async () => { - ( - useEnvironment as jest.Mock> - ).mockImplementation(() => ({ - userSettings: { - attributes: { - password: { - enabled: true - } - } - } - })); - - render(); - expect(screen.getByText('Remove password')).toBeInTheDocument(); - }); - - it('renders the ChangePassword page without showing remove password cta', async () => { - ( - useEnvironment as jest.Mock> - ).mockImplementation(() => ({ - userSettings: { - attributes: { - password: { - enabled: false - } - } - } - })); - - render(); - expect(screen.getByText('Remove password')).toBeInTheDocument(); - }); -}); diff --git a/packages/clerk-js/src/ui/userProfile/security/ChangePassword.tsx b/packages/clerk-js/src/ui/userProfile/security/ChangePassword.tsx index 0f46dad5053..c34d7bdd89f 100644 --- a/packages/clerk-js/src/ui/userProfile/security/ChangePassword.tsx +++ b/packages/clerk-js/src/ui/userProfile/security/ChangePassword.tsx @@ -11,8 +11,7 @@ import { PageHeading } from 'ui/userProfile/pageHeading'; export function ChangePassword(): JSX.Element { const user = useCoreUser(); - const { userSettings } = useEnvironment(); - const { attributes } = userSettings; + const { authConfig } = useEnvironment(); const { navigate } = useNavigate(); const password = useFieldState('password', ''); @@ -33,7 +32,8 @@ export function ChangePassword(): JSX.Element { } }; - const showRemovePassword = user.passwordEnabled && !attributes.password.required; + const showRemovePassword = + user.passwordEnabled && authConfig.password !== 'required'; const onClickRemovePassword = async () => { try { diff --git a/packages/clerk-js/src/ui/userProfile/security/Security.test.tsx b/packages/clerk-js/src/ui/userProfile/security/Security.test.tsx index 402ec8ac9ac..d5ec43cd6cd 100644 --- a/packages/clerk-js/src/ui/userProfile/security/Security.test.tsx +++ b/packages/clerk-js/src/ui/userProfile/security/Security.test.tsx @@ -55,24 +55,16 @@ describe('', () => { useEnvironment as jest.Mock>, true, ).mockImplementation( - () => ( - { - userSettings: { - attributes: { - phone_number: { - used_for_second_factor: true, - second_factors: ['phone_code'] - }, - password: { - enabled: true - } - } + () => + ({ + authConfig: { + secondFactors: ['phone_code'], + password: 'on', }, displayConfig: { branded: true, }, - } as PartialDeep - ) + } as PartialDeep), ); const tree = renderJSON(); expect(tree).toMatchSnapshot(); diff --git a/packages/clerk-js/src/ui/userProfile/security/Security.tsx b/packages/clerk-js/src/ui/userProfile/security/Security.tsx index 4b96bb4edb8..c62419f131f 100644 --- a/packages/clerk-js/src/ui/userProfile/security/Security.tsx +++ b/packages/clerk-js/src/ui/userProfile/security/Security.tsx @@ -8,16 +8,16 @@ import { PageHeading } from 'ui/userProfile/pageHeading'; import { ActiveDevicesCard } from './DevicesAndActivity/ActiveDevicesCard'; export function Security(): JSX.Element { - const { userSettings } = useEnvironment(); - const { attributes } = userSettings; + const { authConfig } = useEnvironment(); const { navigate } = useNavigate(); const user = useCoreUser(); - const showPasswordRow = attributes.password.enabled; + const showPasswordRow = + authConfig.password === 'on' || authConfig.password === 'required'; const showSecondFactorRow = - attributes.phone_number.used_for_second_factor && - attributes.phone_number.second_factors.includes('phone_code'); + authConfig.secondFactors.length > 0 && + authConfig.secondFactors.includes('phone_code'); const buildPasswordRow = () => ( - phone_number.used_for_second_factor && - phone_number.second_factors.includes('phone_code'); + authConfig.secondFactors.length > 0 && + authConfig.secondFactors.includes('phone_code'); return ( boolean; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 6e16df6e928..57129c35cc3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -19,7 +19,6 @@ export * from './signUp'; export * from './theme'; export * from './token'; export * from './user'; -export * from './userSettings'; export * from './utils'; export * from './verification'; export * from './web3'; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 173d9673fa3..1340daf328c 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -2,13 +2,17 @@ * Currently representing API DTOs in their JSON form. */ +import { ToggleType, ToggleTypeWithRequire } from './authConfig'; +import { EmailAddressVerificationStrategy } from './emailAddress'; import { OAuthStrategy } from './oauth'; import { SessionStatus } from './session'; import { + IdentificationStrategy, PreferredSignInStrategy, SignInFactor, SignInIdentifier, SignInStatus, + SignInStrategyName, UserData, } from './signIn'; import { SignUpField, SignUpIdentificationField, SignUpStatus } from './signUp'; @@ -20,7 +24,6 @@ import { FontWeight, HexColor, } from './theme'; -import { UserSettingsJSON } from './userSettings'; import { VerificationStatus } from './verification'; export interface ClerkResourceJSON { @@ -83,7 +86,6 @@ export interface ImageJSON { export interface EnvironmentJSON extends ClerkResourceJSON { auth_config: AuthConfigJSON; display_config: DisplayConfigJSON; - user_settings: UserSettingsJSON; } export interface ClientJSON extends ClerkResourceJSON { @@ -212,9 +214,24 @@ export interface PublicUserDataJSON extends ClerkResourceJSON { export interface SessionWithActivitiesJSON extends Omit { user: null; latest_activity: SessionActivityJSON; + // activities: SessionActivityJSON[]; } -export interface AuthConfigJSON extends ClerkResourceJSON { +export interface AuthConfigJSON { + object: 'auth_config'; + id: string; + first_name: ToggleTypeWithRequire; + last_name: ToggleTypeWithRequire; + email_address: ToggleType; + phone_number: ToggleType; + username: ToggleType; + password: ToggleTypeWithRequire; + identification_strategies: IdentificationStrategy[]; + identification_requirements: IdentificationStrategy[][]; + password_conditions: any; + first_factors: SignInStrategyName[]; + second_factors: SignInStrategyName[]; + email_address_verification_strategies: EmailAddressVerificationStrategy[]; single_session_mode: boolean; } diff --git a/packages/types/src/userSettings.ts b/packages/types/src/userSettings.ts deleted file mode 100644 index 6343ce291e1..00000000000 --- a/packages/types/src/userSettings.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ClerkResourceJSON } from './json'; -import { OAuthStrategy } from './oauth'; -import { ClerkResource } from './resource'; -import { Web3Strategy } from './web3'; - -type Attribute = - | 'email_address' - | 'phone_number' - | 'username' - | 'first_name' - | 'last_name' - | 'password' - | 'web3_wallet'; - -type VerificationStrategy = 'email_link' | 'email_code' | 'phone_code'; - -type OauthProviderData = { - enabled: boolean; - required: boolean; - authenticatable: boolean; - strategy: OAuthStrategy; -}; - -export type AttributeData = { - enabled: boolean; - required: boolean; - name: Attribute; - verifications: VerificationStrategy[]; - used_for_first_factor: boolean; - first_factors: VerificationStrategy[]; - used_for_second_factor: boolean; - second_factors: VerificationStrategy[]; - verify_at_sign_up: boolean; -}; - -export type SignInData = { - second_factor: { - required: boolean; - enabled: boolean; - }; -}; - -export type SignUpData = { - allowlist_only: boolean; -}; - -export type OauthProviders = { - [provider in OAuthStrategy]: OauthProviderData; -}; - -export type Attributes = { - [attribute in Attribute]: AttributeData; -}; - -export interface UserSettingsJSON extends ClerkResourceJSON { - id: never; - object: never; - attributes: Attributes; - social: OauthProviders; - sign_in: SignInData; - sign_up: SignUpData; -} - -export interface UserSettingsResource extends ClerkResource { - id: undefined; - social: OauthProviders; - attributes: Attributes; - signIn: SignInData; - signUp: SignUpData; - socialProviderStrategies: OAuthStrategy[]; - web3FirstFactors: Web3Strategy[]; - enabledFirstFactorIdentifiers: Attribute[]; - instanceIsPasswordBased: boolean; -} From 586437f238723da6f03119e2069989eaabe48ddd Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 24 Feb 2022 11:50:01 +0200 Subject: [PATCH 09/11] fix(clerk-js): Render instant password field for password-based instances only --- .../src/ui/signIn/SignInStart.test.tsx | 1 + .../clerk-js/src/ui/signIn/SignInStart.tsx | 44 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx b/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx index 8fb33bdeb98..d59602e055f 100644 --- a/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInStart.test.tsx @@ -43,6 +43,7 @@ jest.mock('ui/contexts', () => { afterSignInUrl: 'http://test.host', }, authConfig: { + password: 'required', singleSessionMode: false, identificationStrategies: [ 'email_address', diff --git a/packages/clerk-js/src/ui/signIn/SignInStart.tsx b/packages/clerk-js/src/ui/signIn/SignInStart.tsx index 5f148734f15..b3c9aa858de 100644 --- a/packages/clerk-js/src/ui/signIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInStart.tsx @@ -76,6 +76,8 @@ export function _SignInStart(): JSX.Element { .filter(fac => fac.startsWith('oauth')) .sort() as OAuthStrategy[]; + const passwordBasedInstance = authConfig.password === 'required'; + // TODO: Clean up the following code end React.useEffect(() => { async function handleOauthError() { @@ -203,24 +205,30 @@ export function _SignInStart(): JSX.Element { )} - - instantPassword.setValue(el.value || '')} - tabIndex={-1} - /> - + <> + {passwordBasedInstance && ( + + + instantPassword.setValue(el.value || '') + } + tabIndex={-1} + /> + + )} + )} From 9d8a050d975fddb3e3163781d010138a888b7bf2 Mon Sep 17 00:00:00 2001 From: gkats Date: Thu, 24 Feb 2022 12:05:59 +0200 Subject: [PATCH 10/11] fix(clerk-js): Helpful error message for sign in without factors Changed the error message in our SignInFactor one component when trying to sign in but there's no available authentication method for the instance. --- packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx | 2 +- packages/clerk-js/src/ui/signIn/SignInFactorOne.tsx | 4 +++- .../src/ui/signIn/strategies/ErrorScreen.test.tsx | 4 ++-- .../clerk-js/src/ui/signIn/strategies/ErrorScreen.tsx | 8 ++++++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx index a74133adf9d..4a8fc70bef3 100644 --- a/packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInFactorOne.test.tsx @@ -705,7 +705,7 @@ describe('', () => { render(); - screen.getByText('Unknown error'); + screen.getByText(/no available authentication method/); }); }); }); diff --git a/packages/clerk-js/src/ui/signIn/SignInFactorOne.tsx b/packages/clerk-js/src/ui/signIn/SignInFactorOne.tsx index 5358f0aaf47..7dbb75fe042 100644 --- a/packages/clerk-js/src/ui/signIn/SignInFactorOne.tsx +++ b/packages/clerk-js/src/ui/signIn/SignInFactorOne.tsx @@ -50,7 +50,9 @@ function _SignInFactorOne(): JSX.Element { }; if (!currentFactor && signIn.status) { - return ; + return ( + + ); } if (!currentFactor) { diff --git a/packages/clerk-js/src/ui/signIn/strategies/ErrorScreen.test.tsx b/packages/clerk-js/src/ui/signIn/strategies/ErrorScreen.test.tsx index baab75f23e2..d6e68adf73c 100644 --- a/packages/clerk-js/src/ui/signIn/strategies/ErrorScreen.test.tsx +++ b/packages/clerk-js/src/ui/signIn/strategies/ErrorScreen.test.tsx @@ -21,8 +21,8 @@ jest.mock('ui/contexts/EnvironmentContext', () => { jest.mock('ui/router/RouteContext'); describe('', () => { - it('renders the fallback error page', async () => { - const tree = renderJSON(); + it('renders the fallback error page', () => { + const tree = renderJSON(); expect(tree).toMatchSnapshot(); }); }); diff --git a/packages/clerk-js/src/ui/signIn/strategies/ErrorScreen.tsx b/packages/clerk-js/src/ui/signIn/strategies/ErrorScreen.tsx index 6238f5fe264..98d6dbf2f2a 100644 --- a/packages/clerk-js/src/ui/signIn/strategies/ErrorScreen.tsx +++ b/packages/clerk-js/src/ui/signIn/strategies/ErrorScreen.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Body, Footer, Header } from 'ui/common/authForms'; -export function ErrorScreen(): JSX.Element { +export function ErrorScreen({ message }: ErrorScreenProps): JSX.Element { return ( <>
-
Unknown error
+
{message}