From a484a4a3d68954fda7529520d3c53fde9d02f69f Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Wed, 10 Jun 2026 14:16:10 -0700 Subject: [PATCH] Add email verification frontend --- packages/common/src/messages/settings.ts | 8 ++ packages/common/src/services/auth/identity.ts | 18 +++ packages/common/src/utils/route.ts | 2 + .../settings-screen/AccountSettingsScreen.tsx | 39 +++++- packages/web/src/app/web-player/WebPlayer.tsx | 10 ++ .../components/desktop/SettingsPage.tsx | 60 ++++++++++ .../components/mobile/AccountSettingsPage.tsx | 34 ++++++ .../VerifyEmailPage.module.css | 48 ++++++++ .../verify-email-page/VerifyEmailPage.tsx | 113 ++++++++++++++++++ .../web/src/pages/verify-email-page/index.ts | 2 + 10 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 packages/web/src/pages/verify-email-page/VerifyEmailPage.module.css create mode 100644 packages/web/src/pages/verify-email-page/VerifyEmailPage.tsx create mode 100644 packages/web/src/pages/verify-email-page/index.ts diff --git a/packages/common/src/messages/settings.ts b/packages/common/src/messages/settings.ts index f548f19f4a0..11ac3f06dae 100644 --- a/packages/common/src/messages/settings.ts +++ b/packages/common/src/messages/settings.ts @@ -31,6 +31,7 @@ export const settingsMessages = { labelAccountCardTitle: 'Label Account', notificationsCardTitle: 'Configure Notifications', accountRecoveryCardTitle: 'Resend Recovery Email', + emailVerificationCardTitle: 'Email Verification', changeEmailCardTitle: 'Change Email', changePasswordCardTitle: 'Change Password', accountsYouManageTitle: 'Accounts You Manage', @@ -46,6 +47,8 @@ export const settingsMessages = { notificationsCardDescription: 'Review your notification preferences.', accountRecoveryCardDescription: 'Resend your password reset email and store it safely. This email is the only way to recover your account if you forget your password.', + emailVerificationCardDescription: + 'Verify that you can receive email at the address connected to your Audius account.', changeEmailCardDescription: 'Change the email you use to sign in and receive emails.', changePasswordCardDescription: 'Change the password to your Audius account.', @@ -61,6 +64,11 @@ export const settingsMessages = { commentSettingsButtonText: 'Comment Settings', notificationsButtonText: 'Configure Notifications', accountRecoveryButtonText: 'Resend Email', + emailVerificationButtonText: 'Resend Verification Email', + emailVerificationSent: 'Verification email sent!', + emailVerificationAlreadyVerified: 'Your email is already verified.', + emailVerificationNotSent: + 'Unable to send verification email. Please try again!', changeEmailButtonText: 'Change Email', changePasswordButtonText: 'Change Password', desktopAppButtonText: 'Get The App', diff --git a/packages/common/src/services/auth/identity.ts b/packages/common/src/services/auth/identity.ts index d037b144c79..48902b7d474 100644 --- a/packages/common/src/services/auth/identity.ts +++ b/packages/common/src/services/auth/identity.ts @@ -25,6 +25,11 @@ type CreateStripeSessionResponse = { status: string } +type ResendEmailVerificationResponse = { + status: true + alreadyVerified?: boolean +} + enum TransactionMetadataType { PURCHASE_SOL_AUDIO_SWAP = 'PURCHASE_SOL_AUDIO_SWAP' } @@ -232,6 +237,19 @@ export class IdentityService { return res.email } + /** + * Resend the current user's email verification link. + */ + async resendEmailVerification() { + const headers = await this.getAuthHeaders() + + return await this._makeRequest({ + url: '/email/resend-verification', + method: 'post', + headers + }) + } + /** * Change the user's email used for notifications and display. */ diff --git a/packages/common/src/utils/route.ts b/packages/common/src/utils/route.ts index 9b5b0d77902..994f210f995 100644 --- a/packages/common/src/utils/route.ts +++ b/packages/common/src/utils/route.ts @@ -55,6 +55,7 @@ export const HOME_PAGE = '/' export const NOT_FOUND_PAGE = '/404' export const SIGN_IN_PAGE = '/signin' export const SIGN_IN_CONFIRM_EMAIL_PAGE = '/signin/confirm-email' +export const EMAIL_VERIFICATION_PAGE = '/verify-email' export const SIGN_UP_PAGE = '/signup' export const SIGN_ON_ALIASES = Object.freeze([ '/login', @@ -285,6 +286,7 @@ export const publicSiteRoutes = [ // ordered list of routes the App attempts to match in increasing order of route selectivity export const orderedRoutes = [ SIGN_IN_PAGE, + EMAIL_VERIFICATION_PAGE, SIGN_UP_PAGE, ...SIGN_ON_ALIASES, SIGN_UP_EMAIL_PAGE, diff --git a/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx b/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx index 2157d74f204..f720c8bc42f 100644 --- a/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx +++ b/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx @@ -1,7 +1,8 @@ -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import { useCurrentAccountUser, + useQueryContext, useResendRecoveryEmail } from '@audius/common/api' import { modalsActions, useTierAndVerifiedForUser } from '@audius/common/store' @@ -43,6 +44,14 @@ const messages = { recoveryButtonTitle: 'Resend Recovery Email', recoveryEmailSent: 'Recovery Email Sent!', recoveryEmailNotSent: 'Unable to send recovery email. Please try again!', + emailVerificationTitle: 'Email Verification', + emailVerificationDescription: + 'Verify that you can receive email at the address connected to your Audius account.', + emailVerificationButtonTitle: 'Resend Verification Email', + emailVerificationSent: 'Verification email sent!', + emailVerificationAlreadyVerified: 'Your email is already verified.', + emailVerificationNotSent: + 'Unable to send verification email. Please try again!', verifyTitle: 'Verification', verifyDescription: 'Verify your Audius profile by completing identity verification.', @@ -78,6 +87,9 @@ export const AccountSettingsScreen = () => { const styles = useStyles() const { toast } = useToast() const dispatch = useDispatch() + const { identityService } = useQueryContext() + const [isSendingVerificationEmail, setIsSendingVerificationEmail] = + useState(false) const { data: accountData } = useCurrentAccountUser({ select: (user) => pick(user, ['user_id', 'handle', 'name']) }) @@ -102,6 +114,23 @@ export const AccountSettingsScreen = () => { }) }, [resendRecoveryEmail, toast]) + const handlePressEmailVerification = useCallback(async () => { + setIsSendingVerificationEmail(true) + try { + const result = await identityService.resendEmailVerification() + toast({ + content: result.alreadyVerified + ? messages.emailVerificationAlreadyVerified + : messages.emailVerificationSent, + type: 'info' + }) + } catch (e) { + toast({ content: messages.emailVerificationNotSent, type: 'error' }) + } finally { + setIsSendingVerificationEmail(false) + } + }, [identityService, toast]) + const handlePressChangeEmail = useCallback(() => { navigation.push('ChangeEmail') }, [navigation]) @@ -155,6 +184,14 @@ export const AccountSettingsScreen = () => { buttonTitle={messages.recoveryButtonTitle} onPress={handlePressRecoveryEmail} /> + })) ) const SettingsPage = lazy(() => import('pages/settings-page/SettingsPage')) +const VerifyEmailPage = lazy(() => import('pages/verify-email-page')) const TrackCommentsPage = lazy(() => import('pages/track-page/TrackCommentsPage').then((m) => ({ default: m.TrackCommentsPage @@ -294,6 +295,7 @@ const { COIN_EXCLUSIVE_TRACKS_PAGE, COIN_EXCLUSIVE_TRACKS_MOBILE_ROUTE, CHECK_PAGE, + EMAIL_VERIFICATION_PAGE, TRENDING_PLAYLISTS_PAGE, TRENDING_PLAYLISTS_PAGE_LEGACY, DEACTIVATE_PAGE, @@ -1055,6 +1057,10 @@ const WebPlayer = (props: WebPlayerProps) => { path={LABEL_ACCOUNT_SETTINGS_PAGE} element={} /> + } + /> } /> {isMobile ? ( <> @@ -1512,6 +1518,10 @@ const WebPlayer = (props: WebPlayerProps) => { path={LABEL_ACCOUNT_SETTINGS_PAGE} element={} /> + } + /> } /> { setIsNotificationSettingsModalVisible ] = useState(false) const [isEmailToastVisible, setIsEmailToastVisible] = useState(false) + const [isEmailVerificationToastVisible, setIsEmailVerificationToastVisible] = + useState(false) + const [isEmailVerificationLoading, setIsEmailVerificationLoading] = + useState(false) const [isChangePasswordModalVisible, setIsChangePasswordModalVisible] = useState(false) const [isChangeEmailModalVisible, setIsChangeEmailModalVisible] = @@ -176,6 +180,9 @@ export const SettingsPage = () => { const [emailToastText, setEmailToastText] = useState( settingsMessages.emailSent ) + const [emailVerificationToastText, setEmailVerificationToastText] = useState( + settingsMessages.emailVerificationSent + ) const [, setIsInboxSettingsModalVisible] = useModalState('InboxSettings') const [, setIsCommentSettingsModalVisible] = useModalState('CommentSettings') @@ -260,6 +267,35 @@ export const SettingsPage = () => { dispatch ]) + const showEmailVerificationToast = useCallback(() => { + const fn = async () => { + setIsEmailVerificationLoading(true) + try { + const result = await identityService.resendEmailVerification() + setEmailVerificationToastText( + result.alreadyVerified + ? settingsMessages.emailVerificationAlreadyVerified + : settingsMessages.emailVerificationSent + ) + setIsEmailVerificationToastVisible(true) + } catch (e) { + console.error(e) + setEmailVerificationToastText(settingsMessages.emailVerificationNotSent) + setIsEmailVerificationToastVisible(true) + } finally { + setIsEmailVerificationLoading(false) + } + setTimeout(() => { + setIsEmailVerificationToastVisible(false) + }, EMAIL_TOAST_TIMEOUT) + } + fn() + }, [ + setIsEmailVerificationToastVisible, + setEmailVerificationToastText, + identityService + ]) + const handleDownloadDesktopAppClicked = useCallback(() => { dispatch(make(Name.ACCOUNT_HEALTH_DOWNLOAD_DESKTOP, { source: 'settings' })) window.location.href = `https://audius.co${DOWNLOAD_LINK}` @@ -607,6 +643,30 @@ export const SettingsPage = () => { ) : null} + {!isManagedAccount ? ( + } + title={settingsMessages.emailVerificationCardTitle} + description={settingsMessages.emailVerificationCardDescription} + > + + + + + ) : null} {!isManagedAccount ? ( } diff --git a/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx b/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx index f830daddec1..d31d37e9b41 100644 --- a/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx +++ b/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx @@ -45,6 +45,14 @@ const messages = { recoveryButtonTitle: 'Resend Recovery Email', recoveryEmailSent: 'Recovery Email Sent!', recoveryEmailNotSent: 'Unable to send recovery email. Please try again!', + emailVerificationTitle: 'Email Verification', + emailVerificationDescription: + 'Verify that you can receive email at the address connected to your Audius account.', + emailVerificationButtonTitle: 'Resend Verification Email', + emailVerificationSent: 'Verification email sent!', + emailVerificationAlreadyVerified: 'Your email is already verified.', + emailVerificationNotSent: + 'Unable to send verification email. Please try again!', verifyTitle: 'Verify Your Account', verifyDescription: 'Verify your Audius profile by completing identity verification', @@ -140,6 +148,8 @@ const AccountSettingsPage = () => { }) const { userId, handle, name } = accountData ?? {} const [showModalSignOut, setShowModalSignOut] = useState(false) + const [isSendingVerificationEmail, setIsSendingVerificationEmail] = + useState(false) const { toast } = useContext(ToastContext) const { isVerified } = useTierAndVerifiedForUser(userId) @@ -173,6 +183,22 @@ const AccountSettingsPage = () => { [authService, identityService, toast, record] ) + const onClickResendVerificationEmail = useCallback(async () => { + setIsSendingVerificationEmail(true) + try { + const result = await identityService.resendEmailVerification() + toast( + result.alreadyVerified + ? messages.emailVerificationAlreadyVerified + : messages.emailVerificationSent + ) + } catch (e) { + toast(messages.emailVerificationNotSent) + } finally { + setIsSendingVerificationEmail(false) + } + }, [identityService, toast]) + const goToChangePasswordSettingsPage = useCallback(() => { goToRoute(CHANGE_PASSWORD_SETTINGS_PAGE) }, [goToRoute]) @@ -206,6 +232,14 @@ const AccountSettingsPage = () => { buttonTitle={messages.recoveryButtonTitle} onClick={onClickRecover} /> + = { + success: { + title: messages.successTitle, + description: messages.successDescription, + Icon: IconCheck + }, + expired: { + title: messages.expiredTitle, + description: messages.expiredDescription, + Icon: IconEmailAddress + }, + invalid: { + title: messages.invalidTitle, + description: messages.invalidDescription, + Icon: IconError + }, + error: { + title: messages.errorTitle, + description: messages.errorDescription, + Icon: IconError + } +} + +const getStatus = (status: string | null): VerificationStatus => { + if ( + status === 'success' || + status === 'expired' || + status === 'invalid' || + status === 'error' + ) { + return status + } + return 'invalid' +} + +export const VerifyEmailPage = () => { + const [searchParams] = useSearchParams() + const status = getStatus(searchParams.get('status')) + const { title, description, Icon } = statusConfig[status] + + return ( + +
+
+
+ +
+
+ + {title} + + + {description} + +
+
+ + +
+
+
+
+ ) +} + +export default VerifyEmailPage diff --git a/packages/web/src/pages/verify-email-page/index.ts b/packages/web/src/pages/verify-email-page/index.ts new file mode 100644 index 00000000000..cf9eec6f0ea --- /dev/null +++ b/packages/web/src/pages/verify-email-page/index.ts @@ -0,0 +1,2 @@ +export { default } from './VerifyEmailPage' +export * from './VerifyEmailPage'