diff --git a/.github/workflows/ci.js.yml b/.github/workflows/ci.js.yml index 259e975a322..6672b486104 100644 --- a/.github/workflows/ci.js.yml +++ b/.github/workflows/ci.js.yml @@ -19,9 +19,9 @@ jobs: node-version: [16.19, 18.15, 19.8] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm i -g npm@8.19.0 diff --git a/package-lock.json b/package-lock.json index f121d7bc801..04d3adfe003 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33975,10 +33975,10 @@ }, "packages/backend": { "name": "@clerk/backend", - "version": "0.18.0-staging.2", + "version": "0.18.0-staging.3", "license": "MIT", "dependencies": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "@peculiar/webcrypto": "1.4.1", "@types/node": "16.18.6", "deepmerge": "4.2.2", @@ -34044,11 +34044,11 @@ }, "packages/chrome-extension": { "name": "@clerk/chrome-extension", - "version": "0.2.7-staging.2", + "version": "0.2.7-staging.3", "license": "MIT", "dependencies": { - "@clerk/clerk-js": "^4.39.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2" + "@clerk/clerk-js": "^4.39.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3" }, "devDependencies": { "@testing-library/dom": "^8.19.0", @@ -34077,12 +34077,12 @@ }, "packages/clerk-js": { "name": "@clerk/clerk-js", - "version": "4.39.0-staging.2", + "version": "4.39.0-staging.3", "license": "MIT", "dependencies": { - "@clerk/localizations": "^1.12.0-staging.2", - "@clerk/shared": "^0.15.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/localizations": "^1.12.0-staging.3", + "@clerk/shared": "^0.15.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@emotion/cache": "11.10.5", "@emotion/react": "11.10.5", "@floating-ui/react": "0.19.0", @@ -34171,16 +34171,16 @@ }, "packages/expo": { "name": "@clerk/clerk-expo", - "version": "0.16.4-staging.2", + "version": "0.16.4-staging.3", "license": "MIT", "dependencies": { - "@clerk/clerk-js": "^4.39.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", + "@clerk/clerk-js": "^4.39.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", "base-64": "1.0.0", "react-native-url-polyfill": "1.3.0" }, "devDependencies": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "@types/base-64": "^1.0.0", "@types/jest": "^27.4.0", "@types/node": "^16.11.55", @@ -35385,11 +35385,11 @@ }, "packages/fastify": { "name": "@clerk/fastify", - "version": "0.3.0-staging.0", + "version": "0.3.0-staging.1", "license": "MIT", "dependencies": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/types": "^3.36.0-staging.3", "cookies": "0.8.0" }, "devDependencies": { @@ -35407,13 +35407,13 @@ } }, "packages/gatsby-plugin-clerk": { - "version": "4.2.7-staging.2", + "version": "4.2.7-staging.3", "license": "MIT", "dependencies": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", - "@clerk/clerk-sdk-node": "^4.8.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", + "@clerk/clerk-sdk-node": "^4.8.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "cookie": "0.5.0", "tslib": "2.4.1" }, @@ -35437,10 +35437,10 @@ }, "packages/localizations": { "name": "@clerk/localizations", - "version": "1.12.0-staging.2", + "version": "1.12.0-staging.3", "license": "MIT", "dependencies": { - "@clerk/types": "^3.36.0-staging.2" + "@clerk/types": "^3.36.0-staging.3" }, "devDependencies": { "tsup": "*", @@ -35455,13 +35455,13 @@ }, "packages/nextjs": { "name": "@clerk/nextjs", - "version": "4.17.0-staging.0", + "version": "4.17.0-staging.1", "license": "MIT", "dependencies": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", - "@clerk/clerk-sdk-node": "^4.8.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", + "@clerk/clerk-sdk-node": "^4.8.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "path-to-regexp": "6.2.1", "tslib": "2.4.1" }, @@ -36631,11 +36631,11 @@ }, "packages/react": { "name": "@clerk/clerk-react", - "version": "4.15.4-staging.2", + "version": "4.15.4-staging.3", "license": "MIT", "dependencies": { - "@clerk/shared": "^0.15.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/shared": "^0.15.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "swr": "1.3.0", "tslib": "2.4.1" }, @@ -36668,13 +36668,13 @@ }, "packages/remix": { "name": "@clerk/remix", - "version": "2.5.6-staging.2", + "version": "2.5.6-staging.3", "license": "MIT", "dependencies": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", - "@clerk/shared": "^0.15.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", + "@clerk/shared": "^0.15.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "cookie": "0.5.0", "tslib": "2.4.1" }, @@ -36703,11 +36703,11 @@ }, "packages/sdk-node": { "name": "@clerk/clerk-sdk-node", - "version": "4.8.7-staging.2", + "version": "4.8.7-staging.3", "license": "MIT", "dependencies": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@types/cookies": "0.7.7", "@types/express": "4.17.14", "@types/node-fetch": "2.6.2", @@ -37878,10 +37878,10 @@ }, "packages/shared": { "name": "@clerk/shared", - "version": "0.15.7-staging.2", + "version": "0.15.7-staging.3", "license": "ISC", "devDependencies": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "@testing-library/dom": "8.19.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "13.4.0", @@ -37901,10 +37901,10 @@ }, "packages/themes": { "name": "@clerk/themes", - "version": "1.6.4-staging.2", + "version": "1.6.4-staging.3", "license": "MIT", "devDependencies": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "typescript": "*" }, "engines": { @@ -37916,7 +37916,7 @@ }, "packages/types": { "name": "@clerk/types", - "version": "3.36.0-staging.2", + "version": "3.36.0-staging.3", "license": "MIT", "dependencies": { "csstype": "3.1.1" @@ -39425,7 +39425,7 @@ "@clerk/backend": { "version": "file:packages/backend", "requires": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "@cloudflare/workers-types": "^3.18.0", "@peculiar/webcrypto": "1.4.1", "@types/chai": "^4.3.3", @@ -39464,8 +39464,8 @@ "@clerk/chrome-extension": { "version": "file:packages/chrome-extension", "requires": { - "@clerk/clerk-js": "^4.39.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", + "@clerk/clerk-js": "^4.39.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", "@testing-library/dom": "^8.19.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -39492,9 +39492,9 @@ "@clerk/clerk-expo": { "version": "file:packages/expo", "requires": { - "@clerk/clerk-js": "^4.39.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/clerk-js": "^4.39.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@types/base-64": "^1.0.0", "@types/jest": "^27.4.0", "@types/node": "^16.11.55", @@ -40431,9 +40431,9 @@ "@babel/preset-env": "^7.12.1", "@babel/preset-react": "^7.12.5", "@babel/preset-typescript": "^7.12.1", - "@clerk/localizations": "^1.12.0-staging.2", - "@clerk/shared": "^0.15.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/localizations": "^1.12.0-staging.3", + "@clerk/shared": "^0.15.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@emotion/cache": "11.10.5", "@emotion/jest": "^11.10.5", "@emotion/react": "11.10.5", @@ -40477,8 +40477,8 @@ "@clerk/clerk-react": { "version": "file:packages/react", "requires": { - "@clerk/shared": "^0.15.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/shared": "^0.15.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@testing-library/dom": "^8.19.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -40506,8 +40506,8 @@ "@clerk/clerk-sdk-node": { "version": "file:packages/sdk-node", "requires": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@types/cookie": "^0.5.0", "@types/cookies": "0.7.7", "@types/express": "4.17.14", @@ -41429,8 +41429,8 @@ "@clerk/fastify": { "version": "file:packages/fastify", "requires": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/types": "^3.36.0-staging.3", "cookies": "0.8.0", "jest": "*", "ts-jest": "*", @@ -41441,7 +41441,7 @@ "@clerk/localizations": { "version": "file:packages/localizations", "requires": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "tsup": "*", "typescript": "*" } @@ -41449,10 +41449,10 @@ "@clerk/nextjs": { "version": "file:packages/nextjs", "requires": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", - "@clerk/clerk-sdk-node": "^4.8.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", + "@clerk/clerk-sdk-node": "^4.8.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@types/node": "^16.11.55", "@types/react": "*", "@types/react-dom": "*", @@ -42371,10 +42371,10 @@ "@clerk/remix": { "version": "file:packages/remix", "requires": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", - "@clerk/shared": "^0.15.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", + "@clerk/shared": "^0.15.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@types/cookie": "^0.5.0", "@types/node": "^16.11.55", "@types/react": "*", @@ -42395,7 +42395,7 @@ "@clerk/shared": { "version": "file:packages/shared", "requires": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "@testing-library/dom": "8.19.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "13.4.0", @@ -42413,7 +42413,7 @@ "@clerk/themes": { "version": "file:packages/themes", "requires": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "typescript": "*" } }, @@ -54762,10 +54762,10 @@ "gatsby-plugin-clerk": { "version": "file:packages/gatsby-plugin-clerk", "requires": { - "@clerk/backend": "^0.18.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2", - "@clerk/clerk-sdk-node": "^4.8.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/backend": "^0.18.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3", + "@clerk/clerk-sdk-node": "^4.8.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@types/cookie": "^0.5.0", "@types/node": "^16.11.55", "cookie": "0.5.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index d210e6f8ede..406a27371a9 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.18.0-staging.3](https://github.com/clerkinc/javascript/compare/@clerk/backend@0.18.0-staging.2...@clerk/backend@0.18.0-staging.3) (2023-05-02) + +**Note:** Version bump only for package @clerk/backend + ### [0.17.2](https://github.com/clerkinc/javascript/compare/@clerk/backend@0.17.2-staging.0...@clerk/backend@0.17.2) (2023-04-19) **Note:** Version bump only for package @clerk/backend diff --git a/packages/backend/package.json b/packages/backend/package.json index 985b35241a7..a9f0e8d2593 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@clerk/backend", - "version": "0.18.0-staging.2", + "version": "0.18.0-staging.3", "license": "MIT", "description": "Clerk Backend SDK - REST Client for Backend API & JWT verification utilities", "types": "./dist/types/index.d.ts", @@ -25,7 +25,7 @@ "test:cloudflare-workerd": "tests/cloudflare-workerd/run.sh" }, "dependencies": { - "@clerk/types": "^3.36.0-staging.2", + "@clerk/types": "^3.36.0-staging.3", "@peculiar/webcrypto": "1.4.1", "@types/node": "16.18.6", "deepmerge": "4.2.2", diff --git a/packages/chrome-extension/CHANGELOG.md b/packages/chrome-extension/CHANGELOG.md index 51813859b5d..a0e9e1ba69c 100644 --- a/packages/chrome-extension/CHANGELOG.md +++ b/packages/chrome-extension/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +### [0.2.7-staging.3](https://github.com/clerkinc/javascript/compare/@clerk/chrome-extension@0.2.7-staging.2...@clerk/chrome-extension@0.2.7-staging.3) (2023-05-02) + +**Note:** Version bump only for package @clerk/chrome-extension + ### [0.2.6](https://github.com/clerkinc/javascript/compare/@clerk/chrome-extension@0.2.6-staging.0...@clerk/chrome-extension@0.2.6) (2023-04-19) **Note:** Version bump only for package @clerk/chrome-extension diff --git a/packages/chrome-extension/package.json b/packages/chrome-extension/package.json index 84e18f7d8a4..e2d1bda8422 100644 --- a/packages/chrome-extension/package.json +++ b/packages/chrome-extension/package.json @@ -1,6 +1,6 @@ { "name": "@clerk/chrome-extension", - "version": "0.2.7-staging.2", + "version": "0.2.7-staging.3", "license": "MIT", "description": "Clerk SDK for Chrome extensions", "keywords": [ @@ -28,8 +28,8 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@clerk/clerk-js": "^4.39.0-staging.2", - "@clerk/clerk-react": "^4.15.4-staging.2" + "@clerk/clerk-js": "^4.39.0-staging.3", + "@clerk/clerk-react": "^4.15.4-staging.3" }, "devDependencies": { "@testing-library/dom": "^8.19.0", diff --git a/packages/clerk-js/CHANGELOG.md b/packages/clerk-js/CHANGELOG.md index 16cc8eb03f7..8c59a352c96 100644 --- a/packages/clerk-js/CHANGELOG.md +++ b/packages/clerk-js/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.39.0-staging.3](https://github.com/clerkinc/javascript/compare/@clerk/clerk-js@4.39.0-staging.2...@clerk/clerk-js@4.39.0-staging.3) (2023-05-02) + +### Features + +- **clerk-js:** Add resetPasswordFlow to SignIn resource ([6155f5b](https://github.com/clerkinc/javascript/commit/6155f5bde6fe0a140bffb7d8087c2246716abf7e)) +- **clerk-js:** Create page ([3fbf8e7](https://github.com/clerkinc/javascript/commit/3fbf8e7157774412096ff432e622540ae2d96ef4)) +- **clerk-js:** Introduce Reset Password flow ([e903c4f](https://github.com/clerkinc/javascript/commit/e903c4f430ae629625177637bb14f965a37596e1)) +- **clerk-js:** Localize "Password don't match" field error ([c573599](https://github.com/clerkinc/javascript/commit/c573599a370d4f3925d0e8a87b37f28f157bb62b)) +- **clerk-js:** Prepare Reset password field for complexity and strength ([9736d94](https://github.com/clerkinc/javascript/commit/9736d94409593a26546b8a7b1a2dec7c023e61b1)) +- **clerk-js:** Reset password for first factor ([280b5df](https://github.com/clerkinc/javascript/commit/280b5df2428b790e679a04004461aadb2717ae2b)) +- **clerk-js:** Reset password MFA ([5978756](https://github.com/clerkinc/javascript/commit/5978756640bc5f5bb4726f72ca2e53ba43f009d6)) + +### Bug Fixes + +- **clerk-js,types:** Remove after_sign_out_url as it not returned by FAPI ([#1121](https://github.com/clerkinc/javascript/issues/1121)) ([d87493d](https://github.com/clerkinc/javascript/commit/d87493d13e2c7a3ffbf37ba728e6cde7f6f14682)) +- **clerk-js:** Add error when preparing for reset_password_code ([7ac766e](https://github.com/clerkinc/javascript/commit/7ac766eacf5199944c271a87f81c045709ec3aa7)) +- **clerk-js:** Allow children to be passed in VerificationCodeCard ([eb556f8](https://github.com/clerkinc/javascript/commit/eb556f8a557c5371a56b0b0b72162fd63e85263f)) +- **clerk-js:** Password settings maximum allowed length ([bfcb799](https://github.com/clerkinc/javascript/commit/bfcb7993d156d548f35ee7274e7e023c866c01af)) +- **clerk-js:** Remove forgotten console.log ([823a0c0](https://github.com/clerkinc/javascript/commit/823a0c0c2e83cff1e4c2793994c6a4069881b568)) +- **clerk-js:** Update type of resetPasswordFlow in SignInResource ([637b791](https://github.com/clerkinc/javascript/commit/637b791b0086be35a67e7d8a6a0e7c42989296b5)) +- **clerk-js:** Use redirectWithAuth after multi session signOut ([928a206](https://github.com/clerkinc/javascript/commit/928a2067c10129b6d561473df062fabdee22e2d7)) + ### [4.38.3](https://github.com/clerkinc/javascript/compare/@clerk/clerk-js@4.38.3-staging.0...@clerk/clerk-js@4.38.3) (2023-04-19) **Note:** Version bump only for package @clerk/clerk-js diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 1699ff24213..6a091567449 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -1,6 +1,6 @@ { "name": "@clerk/clerk-js", - "version": "4.39.0-staging.2", + "version": "4.39.0-staging.3", "license": "MIT", "description": "Clerk JS library", "keywords": [ @@ -41,9 +41,9 @@ "test:coverage": "jest --collectCoverage && open coverage/lcov-report/index.html" }, "dependencies": { - "@clerk/localizations": "^1.12.0-staging.2", - "@clerk/shared": "^0.15.7-staging.2", - "@clerk/types": "^3.36.0-staging.2", + "@clerk/localizations": "^1.12.0-staging.3", + "@clerk/shared": "^0.15.7-staging.3", + "@clerk/types": "^3.36.0-staging.3", "@emotion/cache": "11.10.5", "@emotion/react": "11.10.5", "@floating-ui/react": "0.19.0", diff --git a/packages/clerk-js/src/core/errors.ts b/packages/clerk-js/src/core/errors.ts index d6b6a37e794..42b948358f3 100644 --- a/packages/clerk-js/src/core/errors.ts +++ b/packages/clerk-js/src/core/errors.ts @@ -68,6 +68,12 @@ export function clerkVerifyEmailAddressCalledBeforeCreate(type: 'SignIn' | 'Sign throw new Error(`${errorPrefix} You need to start a ${type} flow by calling ${type}.create() first.`); } +export function clerkResetPasswordMissingEmailOrPhone(): never { + throw new Error( + `${errorPrefix} You need to provide either phoneNumberId or an emailAddressId when calling prepareFirstFactor with 'reset_password_code' as strategy`, + ); +} + export function clerkInvalidStrategy(functionaName: string, strategy: string): never { throw new Error(`${errorPrefix} Strategy "${strategy}" is not a valid strategy for ${functionaName}.`); } diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index df7de46ce05..3c5a3178496 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -53,7 +53,6 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource this.userProfileUrl = data.user_profile_url; this.afterSignInUrl = data.after_sign_in_url; this.afterSignUpUrl = data.after_sign_up_url; - this.afterSignOutUrl = data.after_sign_out_url; this.afterSignOutOneUrl = data.after_sign_out_one_url; this.afterSignOutAllUrl = data.after_sign_out_all_url; this.afterSwitchSessionUrl = data.after_switch_session_url; diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 94e7afb9444..95b606189db 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -32,6 +32,7 @@ import { clerkInvalidFAPIResponse, clerkInvalidStrategy, clerkMissingOptionError, + clerkResetPasswordMissingEmailOrPhone, clerkVerifyEmailAddressCalledBeforeCreate, clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; @@ -93,12 +94,11 @@ export class SignIn extends BaseResource implements SignInResource { break; case 'reset_password_code': if (factor.emailAddressId) { - config = { emailAddressId: factor.emailAddressId } as ResetPasswordCodeFactorConfig; - } - if (factor.phoneNumberId) { - config = { - phoneNumberId: factor.phoneNumberId, - } as ResetPasswordCodeFactorConfig; + config = { emailAddressId: factor?.emailAddressId } as ResetPasswordCodeFactorConfig; + } else if (factor.phoneNumberId) { + config = { phoneNumberId: factor?.phoneNumberId } as ResetPasswordCodeFactorConfig; + } else { + clerkResetPasswordMissingEmailOrPhone(); } break; default: diff --git a/packages/clerk-js/src/core/resources/UserSettings.test.ts b/packages/clerk-js/src/core/resources/UserSettings.test.ts index dde1200ccbe..1cf7e8d0a84 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.test.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.test.ts @@ -56,7 +56,7 @@ describe('UserSettings', () => { expect(sut.instanceIsPasswordBased).toEqual(false); }); - it('respects default values for min and max length', function () { + it('respects default values for min and max password length', function () { let sut = new UserSettings({ attributes: { password: { @@ -72,7 +72,7 @@ describe('UserSettings', () => { expect(sut.passwordSettings).toMatchObject({ min_length: 8, - max_length: 100, + max_length: 72, }); sut = new UserSettings({ @@ -92,6 +92,24 @@ describe('UserSettings', () => { min_length: 10, max_length: 50, }); + + sut = new UserSettings({ + attributes: { + password: { + enabled: true, + required: false, + }, + }, + password_settings: { + min_length: 10, + max_length: 100, + }, + } as any as UserSettingsJSON); + + expect(sut.passwordSettings).toMatchObject({ + min_length: 10, + max_length: 72, + }); }); it('returns true if the instance has a valid auth factor', function () { diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index 6ef8c12486f..334ad64bcb0 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -13,6 +13,9 @@ import type { import { BaseResource } from './internal'; +const defaultMaxPasswordLength = 72; +const defaultMinPasswordLength = 8; + /** * @internal */ @@ -66,8 +69,11 @@ export class UserSettings extends BaseResource implements UserSettingsResource { this.signUp = data.sign_up; this.passwordSettings = { ...data.password_settings, - min_length: Math.max(data?.password_settings?.min_length, 8), - max_length: data?.password_settings?.max_length === 0 ? 100 : data?.password_settings?.max_length, + min_length: Math.max(data?.password_settings?.min_length, defaultMinPasswordLength), + max_length: + data?.password_settings?.max_length === 0 + ? defaultMaxPasswordLength + : Math.min(data?.password_settings?.max_length, defaultMaxPasswordLength), }; this.socialProviderStrategies = this.getSocialProviderStrategies(data.social); this.authenticatableSocialStrategies = this.getAuthenticatableSocialStrategies(data.social); diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx index 4a44e0a72a1..f5205ed6a70 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx @@ -53,7 +53,7 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { }); const { - props: { errorText, ...restEmailAddressProps }, + props: { errorText, hasLostFocus, setError, isSuccessful, setSuccessful, ...restEmailAddressProps }, } = emailAddressField; const roleField = useFormControl('role', 'basic_member', { diff --git a/packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx b/packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx new file mode 100644 index 00000000000..f83f13bd311 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx @@ -0,0 +1,117 @@ +import { useCallback } from 'react'; + +import { clerkInvalidFAPIResponse } from '../../../core/errors'; +import { withRedirectToHomeSingleSessionGuard } from '../../common'; +import { useCoreSignIn } from '../../contexts'; +import { Col, descriptors, localizationKeys, useLocalizations } from '../../customizables'; +import { Card, CardAlert, Form, Header, useCardState, withCardStateProvider } from '../../elements'; +import { MIN_PASSWORD_LENGTH } from '../../hooks'; +import { useSupportEmail } from '../../hooks/useSupportEmail'; +import { useRouter } from '../../router'; +import { handleError, useFormControl } from '../../utils'; + +export const _ResetPassword = () => { + const signIn = useCoreSignIn(); + const card = useCardState(); + const { navigate } = useRouter(); + const supportEmail = useSupportEmail(); + const { t } = useLocalizations(); + + const passwordField = useFormControl('password', '', { + type: 'password', + label: localizationKeys('formFieldLabel__newPassword'), + isRequired: true, + enableErrorAfterBlur: true, + complexity: true, + strengthMeter: true, + }); + const confirmField = useFormControl('confirmPassword', '', { + type: 'password', + label: localizationKeys('formFieldLabel__confirmPassword'), + isRequired: true, + enableErrorAfterBlur: true, + }); + + const canSubmit = + passwordField.value.trim().length >= MIN_PASSWORD_LENGTH && passwordField.value === confirmField.value; + + const checkPasswordMatch = useCallback( + (confirmPassword: string) => { + return passwordField.value && confirmPassword && passwordField.value !== confirmPassword + ? t(localizationKeys('formFieldError__notMatchingPasswords')) + : undefined; + }, + [passwordField.value], + ); + const validateForm = () => { + confirmField.setError(checkPasswordMatch(confirmField.value)); + }; + + const resetPassword = async () => { + passwordField.setError(undefined); + confirmField.setError(undefined); + try { + const { status, createdSessionId } = await signIn.resetPassword({ + password: passwordField.value, + }); + + switch (status) { + case 'complete': + if (createdSessionId) { + const queryParams = new URLSearchParams(); + queryParams.set('createdSessionId', createdSessionId); + return navigate(`../reset-password-success?${queryParams.toString()}`); + } + return console.error(clerkInvalidFAPIResponse(status, supportEmail)); + case 'needs_second_factor': + return navigate('../factor-two'); + default: + return console.error(clerkInvalidFAPIResponse(status, supportEmail)); + } + } catch (e) { + handleError(e, [passwordField, confirmField], card.setError); + } + }; + + return ( + + {card.error} + + navigate('../')} /> + + + + + + + + + { + confirmField.setError(checkPasswordMatch(e.target.value)); + return confirmField.props.onChange(e); + }} + /> + + + + + + ); +}; + +export const ResetPassword = withRedirectToHomeSingleSessionGuard(withCardStateProvider(_ResetPassword)); diff --git a/packages/clerk-js/src/ui/components/SignIn/ResetPasswordSuccess.tsx b/packages/clerk-js/src/ui/components/SignIn/ResetPasswordSuccess.tsx new file mode 100644 index 00000000000..d34d2345b36 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SignIn/ResetPasswordSuccess.tsx @@ -0,0 +1,39 @@ +import { withRedirectToHomeSingleSessionGuard } from '../../common'; +import { Col, descriptors, localizationKeys, Text } from '../../customizables'; +import { Card, CardAlert, Header, useCardState, withCardStateProvider } from '../../elements'; +import { useSetSessionWithTimeout } from '../../hooks/useSetSessionWithTimeout'; +import { Flex, Spinner } from '../../primitives'; + +export const _ResetPasswordSuccess = () => { + const card = useCardState(); + useSetSessionWithTimeout(); + return ( + + {card.error} + + + + + + + + + + + ); +}; + +export const ResetPasswordSuccess = withRedirectToHomeSingleSessionGuard(withCardStateProvider(_ResetPasswordSuccess)); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 4144be83380..5f9b9ca33b9 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -5,6 +5,8 @@ import { SignInEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowC import { ComponentContext, useCoreClerk, useSignInContext, withCoreSessionSwitchGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; +import { ResetPassword } from './ResetPassword'; +import { ResetPasswordSuccess } from './ResetPasswordSuccess'; import { SignInAccountSwitcher } from './SignInAccountSwitcher'; import { SignInFactorOne } from './SignInFactorOne'; import { SignInFactorTwo } from './SignInFactorTwo'; @@ -32,6 +34,12 @@ function SignInRoutes(): JSX.Element { + + + + + + (() => - determineStartingSignInFactor(availableFactors, signIn.identifier, preferredSignInStrategy), - ); + const [{ currentFactor }, setFactor] = React.useState<{ + currentFactor: SignInFactor | undefined | null; + prevCurrentFactor: SignInFactor | undefined | null; + }>(() => ({ + currentFactor: determineStartingSignInFactor(availableFactors, signIn.identifier, preferredSignInStrategy), + prevCurrentFactor: undefined, + })); const [showAllStrategies, setShowAllStrategies] = React.useState( () => !currentFactor || !factorHasLocalStrategy(currentFactor), ); @@ -65,7 +70,10 @@ export function _SignInFactorOne(): JSX.Element { lastPreparedFactorKeyRef.current = factorKey(currentFactor); }; const selectFactor = (factor: SignInFactor) => { - setCurrentFactor(factor); + setFactor(prev => ({ + currentFactor: factor, + prevCurrentFactor: prev.currentFactor, + })); toggleAllStrategies(); }; if (showAllStrategies) { @@ -84,7 +92,20 @@ export function _SignInFactorOne(): JSX.Element { switch (currentFactor?.strategy) { case 'password': - return ; + return ( + { + handleFactorPrepare(); + setFactor(prev => ({ + currentFactor: { + ...factor, + }, + prevCurrentFactor: prev.currentFactor, + })); + }} + onShowAlternativeMethodsClick={toggleAllStrategies} + /> + ); case 'email_code': return ( ); + case 'reset_password_code': + return ( + + setFactor(prev => ({ + currentFactor: prev.prevCurrentFactor, + prevCurrentFactor: prev.currentFactor, + })) + } + /> + ); default: return ; } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx index fcc5ee9d80c..b69d0ffd832 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx @@ -1,13 +1,12 @@ -import type { EmailCodeFactor, PhoneCodeFactor } from '@clerk/types'; +import type { EmailCodeFactor, PhoneCodeFactor, ResetPasswordCodeFactor } from '@clerk/types'; import React from 'react'; import { clerkInvalidFAPIResponse } from '../../../core/errors'; import { useCoreClerk, useCoreSignIn, useOptions, useSignInContext } from '../../contexts'; -import type { LocalizationKey } from '../../customizables'; import type { VerificationCodeCardProps } from '../../elements'; -import { VerificationCodeCard } from '../../elements'; -import { useCardState } from '../../elements/contexts'; +import { useCardState, VerificationCodeCard } from '../../elements'; import { useSupportEmail } from '../../hooks/useSupportEmail'; +import type { LocalizationKey } from '../../localization'; import { useRouter } from '../../router'; import { handleError } from '../../utils'; @@ -15,7 +14,7 @@ export type SignInFactorOneCodeCard = Pick< VerificationCodeCardProps, 'onShowAlternativeMethodsClicked' | 'showAlternativeMethods' | 'onBackLinkClicked' > & { - factor: EmailCodeFactor | PhoneCodeFactor; + factor: EmailCodeFactor | PhoneCodeFactor | ResetPasswordCodeFactor; factorAlreadyPrepared: boolean; onFactorPrepare: () => void; }; @@ -65,6 +64,8 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) => return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn }); case 'needs_second_factor': return navigate('../factor-two'); + case 'needs_new_password': + return navigate('../reset-password'); default: return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneForgotPasswordCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneForgotPasswordCard.tsx new file mode 100644 index 00000000000..c2ee8ec3d34 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneForgotPasswordCard.tsx @@ -0,0 +1,37 @@ +import type { ResetPasswordCodeFactor } from '@clerk/types'; + +import { useCoreSignIn } from '../../contexts'; +import { Flow, localizationKeys } from '../../customizables'; +import type { SignInFactorOneCodeCard } from './SignInFactorOneCodeForm'; +import { SignInFactorOneCodeForm } from './SignInFactorOneCodeForm'; + +type SignInForgotPasswordCardProps = SignInFactorOneCodeCard & { factor: ResetPasswordCodeFactor }; + +export const SignInFactorOneForgotPasswordCard = (props: SignInForgotPasswordCardProps) => { + const { supportedFirstFactors } = useCoreSignIn(); + const resetPasswordFactor = supportedFirstFactors.find(({ strategy }) => strategy === 'reset_password_code') as + | ResetPasswordCodeFactor + | undefined; + + return ( + + + + ); +}; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx index 8ba610a53af..ba96f3eaef2 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx @@ -1,3 +1,4 @@ +import type { ResetPasswordCodeFactor } from '@clerk/types'; import React from 'react'; import { clerkInvalidFAPIResponse } from '../../../core/errors'; @@ -10,10 +11,11 @@ import { handleError, useFormControl } from '../../utils'; type SignInFactorOnePasswordProps = { onShowAlternativeMethodsClick: React.MouseEventHandler; + onFactorPrepare: (f: ResetPasswordCodeFactor) => void; }; export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) => { - const { onShowAlternativeMethodsClick } = props; + const { onShowAlternativeMethodsClick, onFactorPrepare } = props; const card = useCardState(); const { setActive } = useCoreClerk(); const signIn = useCoreSignIn(); @@ -47,6 +49,17 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) .catch(err => handleError(err, [passwordControl], card.setError)); }; + const resetPasswordFactor = signIn.supportedFirstFactors.find( + ({ strategy }) => strategy === 'reset_password_code', + ) as ResetPasswordCodeFactor | undefined; + + const goToForgotPassword = () => { + resetPasswordFactor && + onFactorPrepare({ + ...resetPasswordFactor, + }); + }; + return ( @@ -82,7 +95,7 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) {...passwordControl.props} autoFocus actionLabel={localizationKeys('formFieldAction__forgotPassword')} - onActionClicked={onShowAlternativeMethodsClick} + onActionClicked={resetPasswordFactor ? goToForgotPassword : onShowAlternativeMethodsClick} /> diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx index 953837d6c8b..6ce1f69c00d 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx @@ -1,3 +1,4 @@ +import type { SignInResource } from '@clerk/types'; import React from 'react'; import { clerkInvalidFAPIResponse } from '../../../core/errors'; @@ -5,6 +6,7 @@ import { useCoreClerk, useCoreSignIn, useEnvironment, useSignInContext } from '. import { Col, descriptors, localizationKeys } from '../../customizables'; import { Card, CardAlert, Footer, Form, Header, useCardState } from '../../elements'; import { useSupportEmail } from '../../hooks/useSupportEmail'; +import { useRouter } from '../../router'; import { handleError, useFormControl } from '../../utils'; type SignInFactorTwoBackupCodeCardProps = { @@ -17,6 +19,7 @@ export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCa const { displayConfig } = useEnvironment(); const { navigateAfterSignIn } = useSignInContext(); const { setActive } = useCoreClerk(); + const { navigate } = useRouter(); const supportEmail = useSupportEmail(); const card = useCardState(); const codeControl = useFormControl('code', '', { @@ -25,13 +28,22 @@ export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCa isRequired: true, }); - const handleBackupCodeSubmit: React.FormEventHandler = async e => { + const isResettingPassword = (resource: SignInResource) => + resource.firstFactorVerification?.strategy === 'reset_password_code' && + resource.firstFactorVerification?.status === 'verified'; + + const handleBackupCodeSubmit: React.FormEventHandler = e => { e.preventDefault(); return signIn .attemptSecondFactor({ strategy: 'backup_code', code: codeControl.value }) .then(res => { switch (res.status) { case 'complete': + if (isResettingPassword(res) && res.createdSessionId) { + const queryParams = new URLSearchParams(); + queryParams.set('createdSessionId', res.createdSessionId); + return navigate(`../reset-password-success?${queryParams.toString()}`); + } return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn }); default: return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); @@ -46,9 +58,13 @@ export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCa & { @@ -29,6 +31,7 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => const card = useCardState(); const { navigateAfterSignIn } = useSignInContext(); const { setActive } = useCoreClerk(); + const { navigate } = useRouter(); const supportEmail = useSupportEmail(); const { experimental_enableClerkImages } = useOptions(); @@ -49,6 +52,10 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => } : undefined; + const isResettingPassword = (resource: SignInResource) => + resource.firstFactorVerification?.strategy === 'reset_password_code' && + resource.firstFactorVerification?.status === 'verified'; + const action: VerificationCodeCardProps['onCodeEntryFinishedAction'] = (code, resolve, reject) => { signIn .attemptSecondFactor({ strategy: props.factor.strategy, code }) @@ -56,6 +63,11 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => await resolve(); switch (res.status) { case 'complete': + if (isResettingPassword(res) && res.createdSessionId) { + const queryParams = new URLSearchParams(); + queryParams.set('createdSessionId', res.createdSessionId); + return navigate(`../reset-password-success?${queryParams.toString()}`); + } return setActive({ session: res.createdSessionId, beforeEmit: navigateAfterSignIn }); default: return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); @@ -67,7 +79,9 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => return ( experimental_enableClerkImages ? signIn.userData.experimental_imageUrl : signIn.userData.profileImageUrl } onShowAlternativeMethodsClicked={props.onShowAlternativeMethodsClicked} - /> + > + {isResettingPassword(signIn) && ( + + )} + ); }; diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx new file mode 100644 index 00000000000..9e83190f439 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx @@ -0,0 +1,80 @@ +import type { SignInResource } from '@clerk/types'; +import { describe, it } from '@jest/globals'; + +import { bindCreateFixtures, render, screen } from '../../../../testUtils'; +import { ResetPassword } from '../ResetPassword'; + +const { createFixtures } = bindCreateFixtures('SignIn'); + +describe('ResetPassword', () => { + it('renders the component', async () => { + const { wrapper } = await createFixtures(); + + render(, { wrapper }); + screen.getByRole('heading', { name: /Reset password/i }); + + screen.getByLabelText(/New password/i); + screen.getByLabelText(/Confirm password/i); + }); + + describe('Actions', () => { + it('resets the password and does not require MFA', async () => { + const { wrapper, fixtures } = await createFixtures(); + fixtures.signIn.resetPassword.mockResolvedValue({ + status: 'complete', + createdSessionId: '1234_session_id', + resetPasswordFlow: { + hasNewPassword: true, + commType: 'email_address', + }, + } as SignInResource); + const { userEvent } = render(, { wrapper }); + + await userEvent.type(screen.getByLabelText(/New password/i), 'testtest'); + await userEvent.type(screen.getByLabelText(/Confirm password/i), 'testtest'); + await userEvent.click(screen.getByRole('button', { name: /Reset Password/i })); + expect(fixtures.signIn.resetPassword).toHaveBeenCalledWith({ + password: 'testtest', + }); + expect(fixtures.router.navigate).toHaveBeenCalledWith( + '../reset-password-success?createdSessionId=1234_session_id', + ); + }); + it('resets the password and requires MFA', async () => { + const { wrapper, fixtures } = await createFixtures(); + fixtures.signIn.resetPassword.mockResolvedValue({ + status: 'needs_second_factor', + createdSessionId: '1234_session_id', + resetPasswordFlow: { + hasNewPassword: true, + }, + } as SignInResource); + const { userEvent } = render(, { wrapper }); + + await userEvent.type(screen.getByLabelText(/New password/i), 'testtest'); + await userEvent.type(screen.getByLabelText(/Confirm password/i), 'testtest'); + await userEvent.click(screen.getByRole('button', { name: /Reset Password/i })); + expect(fixtures.router.navigate).toHaveBeenCalledWith('../factor-two'); + }); + + it('results in error if the passwords do not match', async () => { + const { wrapper } = await createFixtures(); + + const { baseElement, userEvent } = render(, { wrapper }); + + await userEvent.type(screen.getByLabelText(/new password/i), 'testewrewr'); + await userEvent.type(screen.getByLabelText(/confirm password/i), 'testrwerrwqrwe'); + await userEvent.click(baseElement); //so that error renders + screen.getByText(/match/i); + }); + + it('navigates to the root page upon pressing the back link', async () => { + const { wrapper, fixtures } = await createFixtures(); + + const { userEvent } = render(, { wrapper }); + + await userEvent.click(screen.getByText(/back/i)); + expect(fixtures.router.navigate).toHaveBeenCalledWith('../'); + }); + }); +}); diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPasswordSuccess.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPasswordSuccess.test.tsx new file mode 100644 index 00000000000..89ea928ec93 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPasswordSuccess.test.tsx @@ -0,0 +1,42 @@ +import { describe, it } from '@jest/globals'; + +import { bindCreateFixtures, render, runFakeTimers, screen } from '../../../../testUtils'; +import { ResetPasswordSuccess } from '../ResetPasswordSuccess'; + +const { createFixtures: createFixturesWithQuery } = bindCreateFixtures('SignIn', { + router: { + queryString: '?createdSessionId=1234_session_id', + }, +}); + +const { createFixtures } = bindCreateFixtures('SignIn'); + +describe('ResetPasswordSuccess', () => { + it('renders the component', async () => { + const { wrapper } = await createFixtures(); + + render(, { wrapper }); + screen.getByRole('heading', { name: /Reset password/i }); + screen.getByText(/Your password was successfully changed. Signing you in, please wait a moment/i); + }); + + it('sets active session after 2000 ms', async () => { + const { wrapper, fixtures } = await createFixturesWithQuery(); + runFakeTimers(timers => { + render(, { wrapper }); + timers.advanceTimersByTime(1000); + expect(fixtures.clerk.setActive).not.toHaveBeenCalled(); + timers.advanceTimersByTime(1000); + expect(fixtures.clerk.setActive).toHaveBeenCalled(); + }); + }); + + it('does not set a session if createdSessionId is missing', async () => { + const { wrapper, fixtures } = await createFixtures(); + runFakeTimers(timers => { + render(, { wrapper }); + timers.advanceTimersByTime(2000); + expect(fixtures.clerk.setActive).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx index 9b793212eba..e5914c23745 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorOne.test.tsx @@ -1,7 +1,6 @@ import type { SignInResource } from '@clerk/types'; import { describe, it, jest } from '@jest/globals'; import { waitFor } from '@testing-library/dom'; -import React from 'react'; import { ClerkAPIResponseError } from '../../../../core/resources'; import { act, bindCreateFixtures, render, runFakeTimers, screen } from '../../../../testUtils'; @@ -128,7 +127,7 @@ describe('SignInFactorOne', () => { f.withEmailAddress(); f.withPassword(); f.withPreferredSignInStrategy({ strategy: 'password' }); - f.startSignInWithEmailAddress({ supportEmailCode: true, supportPassword: true }); + f.startSignInWithEmailAddress({ supportEmailCode: true, supportPassword: true, supportResetPassword: false }); }); const { userEvent } = render(, { wrapper }); await userEvent.click(screen.getByText('Forgot password')); @@ -136,6 +135,25 @@ describe('SignInFactorOne', () => { screen.getByText('Sign in with your password'); }); + it('should render the Forgot Password component when clicking on "Forgot password" (email)', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withEmailAddress(); + f.withPassword(); + f.withPreferredSignInStrategy({ strategy: 'password' }); + f.startSignInWithEmailAddress({ + supportEmailCode: true, + supportPassword: true, + supportResetPassword: true, + }); + }); + const { userEvent } = render(, { wrapper }); + + fixtures.signIn.prepareFirstFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); + await userEvent.click(screen.getByText('Forgot password')); + screen.getByText('Check your email'); + screen.getByText('to reset your password'); + }); + it('shows a UI error when submission fails', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress(); @@ -164,6 +182,53 @@ describe('SignInFactorOne', () => { }); }); + describe('Forgot Password', () => { + it('shows an input to add the code sent to phone', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withEmailAddress(); + f.withPassword(); + f.withPreferredSignInStrategy({ strategy: 'password' }); + f.startSignInWithPhoneNumber({ + supportPassword: true, + supportResetPassword: true, + }); + }); + fixtures.signIn.prepareFirstFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); + const { userEvent } = render(, { wrapper }); + await userEvent.click(screen.getByText('Forgot password')); + + screen.getByText('Check your phone'); + screen.getByText('Reset password code'); + }); + + it('redirects to `reset-password` on successful code verification', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withEmailAddress(); + f.withPassword(); + f.withPreferredSignInStrategy({ strategy: 'password' }); + f.startSignInWithEmailAddress({ + supportEmailCode: true, + supportPassword: true, + supportResetPassword: true, + }); + }); + fixtures.signIn.prepareFirstFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); + fixtures.signIn.attemptFirstFactor.mockReturnValueOnce( + Promise.resolve({ status: 'needs_new_password' } as SignInResource), + ); + const { userEvent } = render(, { wrapper }); + await userEvent.click(screen.getByText('Forgot password')); + await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); + expect(fixtures.signIn.attemptFirstFactor).toHaveBeenCalledWith({ + strategy: 'reset_password_code', + code: '123456', + }); + await waitFor(() => { + expect(fixtures.router.navigate).toHaveBeenCalledWith('../reset-password'); + }); + }); + }); + describe('Verification link', () => { it('shows message to use the magic link in their email', async () => { const { wrapper, fixtures } = await createFixtures(f => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx index 7d15bf30d44..42ea0a71cae 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -35,6 +35,18 @@ describe('SignInFactorTwo', () => { expect(inputs.length).toBe(6); }); + it('correctly shows text indicating user need to complete 2FA to reset password', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInFactorTwo({ + supportResetPassword: true, + }); + }); + fixtures.signIn.prepareSecondFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); + render(, { wrapper }); + + screen.getByText(/before resetting your password/i); + }); + it('sets an active session when user submits second factor successfully', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.startSignInFactorTwo(); @@ -53,6 +65,34 @@ describe('SignInFactorTwo', () => { }); }); }); + + it('redirects to reset-password-success after second factor successfully', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInFactorTwo({ + supportResetPassword: true, + }); + }); + fixtures.signIn.prepareSecondFactor.mockReturnValueOnce(Promise.resolve({} as SignInResource)); + fixtures.signIn.attemptSecondFactor.mockReturnValueOnce( + Promise.resolve({ + status: 'complete', + firstFactorVerification: { + status: 'verified', + strategy: 'reset_password_code', + }, + createdSessionId: '1234_session_id', + } as SignInResource), + ); + const { userEvent } = render(, { wrapper }); + + await userEvent.type(screen.getByLabelText(/Enter verification code/i), '123456'); + await waitFor(() => { + expect(fixtures.clerk.setActive).not.toHaveBeenCalled(); + expect(fixtures.router.navigate).toHaveBeenCalledWith( + '../reset-password-success?createdSessionId=1234_session_id', + ); + }); + }); }); describe('Selected Second Factor Method', () => { diff --git a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx index ff7d2279a94..74aa3e445f8 100644 --- a/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/__tests__/SignInStart.test.tsx @@ -1,6 +1,5 @@ import type { SignInResource } from '@clerk/types'; import { OAUTH_PROVIDERS } from '@clerk/types'; -import React from 'react'; import { bindCreateFixtures, fireEvent, render, screen } from '../../../../testUtils'; import { SignInStart } from '../SignInStart'; diff --git a/packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx index 620c2955121..05c34154634 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx @@ -2,8 +2,9 @@ import { useCallback, useRef } from 'react'; import { useWizard, Wizard } from '../../common'; import { useCoreUser } from '../../contexts'; -import { localizationKeys } from '../../customizables'; +import { localizationKeys, useLocalizations } from '../../customizables'; import { ContentPage, Form, FormButtons, SuccessPage, useCardState, withCardStateProvider } from '../../elements'; +import { MIN_PASSWORD_LENGTH } from '../../hooks'; import { handleError, useFormControl } from '../../utils'; import { UserProfileBreadcrumbs } from './UserProfileNavbar'; @@ -30,6 +31,7 @@ export const PasswordPage = withCardStateProvider(() => { : localizationKeys('userProfile.passwordPage.title'); const card = useCardState(); const wizard = useWizard(); + const { t } = useLocalizations(); // Ensure that messages will not use the updated state of User after a password has been set or changed const successPagePropsRef = useRef[0]>({ @@ -62,7 +64,8 @@ export const PasswordPage = withCardStateProvider(() => { label: localizationKeys('formFieldLabel__signOutOfOtherSessions'), }); - const isPasswordMatch = passwordField.value.trim().length > 0 && passwordField.value === confirmField.value; + const isPasswordMatch = + passwordField.value.trim().length >= MIN_PASSWORD_LENGTH && passwordField.value === confirmField.value; const hasErrors = !!passwordField.errorText || !!confirmField.errorText; const canSubmit = (user.passwordEnabled ? currentPasswordField.value && isPasswordMatch : isPasswordMatch) && !hasErrors; @@ -70,7 +73,7 @@ export const PasswordPage = withCardStateProvider(() => { const checkPasswordMatch = useCallback( (confirmPassword: string) => { return passwordField.value && confirmPassword && passwordField.value !== confirmPassword - ? "Passwords don't match." + ? t(localizationKeys('formFieldError__notMatchingPasswords')) : undefined; }, [passwordField.value], diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index a5951ce9289..6eabcca6c9b 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -182,6 +182,7 @@ export const useUserProfileContext = (): UserProfileContextType => { export const useUserButtonContext = () => { const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as UserButtonCtx; const { navigate } = useNavigate(); + const Clerk = useCoreClerk(); const { displayConfig } = useEnvironment(); const options = useOptions(); @@ -193,7 +194,7 @@ export const useUserButtonContext = () => { const userProfileUrl = ctx.userProfileUrl || displayConfig.userProfileUrl; const afterMultiSessionSingleSignOutUrl = ctx.afterMultiSessionSingleSignOutUrl || displayConfig.afterSignOutOneUrl; - const navigateAfterMultiSessionSingleSignOut = () => navigate(afterMultiSessionSingleSignOutUrl); + const navigateAfterMultiSessionSingleSignOut = () => Clerk.redirectWithAuth(afterMultiSessionSingleSignOutUrl); const afterSignOutUrl = ctx.afterSignOutUrl; const navigateAfterSignOut = () => navigate(afterSignOutUrl); diff --git a/packages/clerk-js/src/ui/elements/Form.tsx b/packages/clerk-js/src/ui/elements/Form.tsx index c7f1d3f9e05..657f1c2425c 100644 --- a/packages/clerk-js/src/ui/elements/Form.tsx +++ b/packages/clerk-js/src/ui/elements/Form.tsx @@ -60,7 +60,6 @@ const FormRoot = (props: FormProps): JSX.Element => { */}