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 => {
*/}