Skip to content

Commit b9e3a1b

Browse files
Add email+password recovery mode with lookup-key gating (#13897)
<img width="534" height="649" alt="image" src="https://github.com/user-attachments/assets/722fab95-a38d-4bb4-882a-2f3bb9afec12" /> ## Summary - support mode=emailpassword recovery links by prompting for new email + password - gate auto-open reset flow on required params: - mode=password (or warning=RECOVERY_DO_NOT_SHARE) requires email - mode=emailpassword requires lookupKey - pass lookupKey through reset flow as oldLookupKey - defer browser-push confirmation until password-reset flow is finished (so push modal no longer overlays the recovery modal) - preserve backwards compatibility for legacy localStorage payloads - fix password-reset saga so success is not dispatched after a failed reset attempt ## Validation - cd packages/web && npx eslint src/components/password-reset/PasswordResetModal.tsx src/components/password-reset/store/actions.ts src/components/password-reset/store/sagas.tsx src/common/store/pages/signon/sagas.ts - cd packages/common && npx eslint src/services/auth/authService.ts - cd packages/web && npm run typecheck (fails due existing unrelated error in src/pages/oauth-login-page/hooks.ts(50,21): TS18047)
1 parent 768e241 commit b9e3a1b

7 files changed

Lines changed: 300 additions & 44 deletions

File tree

packages/common/src/services/auth/authService.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ export type AuthService = {
3737
signOut: () => Promise<void>
3838
resetPassword: ({
3939
username,
40-
password
40+
password,
41+
oldLookupKey
4142
}: {
4243
username: string
4344
password: string
45+
oldLookupKey?: string
4446
}) => Promise<void>
4547
getWallet: () => EthWallet | null
4648
generateRecoveryInfo: () => Promise<{ login: string; host: string }>
@@ -84,12 +86,18 @@ export const createAuthService = ({
8486

8587
const resetPassword = async ({
8688
username,
87-
password
89+
password,
90+
oldLookupKey
8891
}: {
8992
username: string
9093
password: string
94+
oldLookupKey?: string
9195
}) => {
92-
return hedgehogInstance.resetPassword({ username, password })
96+
return hedgehogInstance.resetPassword({
97+
username,
98+
password,
99+
oldLookupKey
100+
})
93101
}
94102

95103
const generateRecoveryInfo = async () => {

packages/web/index.html

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,16 @@
6666
<script async type="text/javascript">
6767
// Account recovery
6868
try {
69+
const RESET_REQUIRED_KEY = 'password-reset-required'
6970
const urlParams = new URLSearchParams(window.location.search)
7071
const useHashRouting = '%USE_HASH_ROUTING%' === 'true'
7172
const login = urlParams.get('login')
7273
const warning = urlParams.get('warning')
74+
const mode = urlParams.get('mode')
75+
const email = urlParams.get('email')
76+
const lookupKey = urlParams.get('lookupKey')
77+
const hasEmail = Boolean(email)
78+
const hasLookupKey = Boolean(lookupKey)
7379

7480
let entropy = null
7581
if (login) {
@@ -82,9 +88,38 @@
8288
redirectUrl += location.pathname
8389
window.history.replaceState({}, document.title, redirectUrl)
8490
}
85-
if (warning === 'RECOVERY_DO_NOT_SHARE') {
86-
const email = urlParams.get('email')
87-
window.localStorage.setItem('password-reset-required', email)
91+
92+
const isEmailPasswordRecovery = mode === 'emailpassword'
93+
const isPasswordRecovery =
94+
!isEmailPasswordRecovery &&
95+
(warning === 'RECOVERY_DO_NOT_SHARE' || mode === 'password')
96+
const isRecoveryMode = isPasswordRecovery || isEmailPasswordRecovery
97+
98+
if (isRecoveryMode) {
99+
let resetPayload = null
100+
101+
if (isEmailPasswordRecovery && hasLookupKey) {
102+
resetPayload = {
103+
mode: 'emailpassword',
104+
email: email ?? undefined,
105+
lookupKey
106+
}
107+
} else if (isPasswordRecovery && hasEmail) {
108+
resetPayload = {
109+
mode: 'password',
110+
email
111+
}
112+
}
113+
114+
if (resetPayload) {
115+
window.localStorage.setItem(
116+
RESET_REQUIRED_KEY,
117+
JSON.stringify(resetPayload)
118+
)
119+
} else {
120+
window.localStorage.removeItem(RESET_REQUIRED_KEY)
121+
}
122+
88123
let redirectUrl = location.protocol + '//' + location.host
89124
if (useHashRouting) {
90125
redirectUrl += '/#'

packages/web/src/common/store/pages/signon/sagas.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const { toast } = toastActions
9898

9999
const SIGN_UP_TIMEOUT_MILLIS = 20 /* min */ * 60 * 1000
100100
const DEFAULT_HANDLE_VERIFICATION_TIMEOUT_MILLIS = 5_000
101+
const PASSWORD_RESET_REQUIRED_KEY = 'password-reset-required'
101102

102103
const messages = {
103104
incompleteAccount:
@@ -107,6 +108,11 @@ const messages = {
107108
'Your account has been deactivated. Please contact support.'
108109
}
109110

111+
const hasPendingPasswordReset = () => {
112+
if (typeof window === 'undefined') return false
113+
return Boolean(window.localStorage.getItem(PASSWORD_RESET_REQUIRED_KEY))
114+
}
115+
110116
function* getDefautFollowUserIds() {
111117
const { ENVIRONMENT } = yield* getContext('env')
112118
// Users ID to filter out of the suggested artists to follow list and to follow by default
@@ -958,6 +964,9 @@ function* signIn(action: ReturnType<typeof signOnActions.signIn>) {
958964
yield* put(requestPushNotificationPermissions())
959965
} else {
960966
setHasRequestedBrowserPermission()
967+
while (hasPendingPasswordReset()) {
968+
yield* delay(500)
969+
}
961970
yield* put(accountActions.showPushNotificationConfirmation())
962971
if (user.handle === 'fbtest') {
963972
yield put(pushRoute('/fb/share'))

0 commit comments

Comments
 (0)