From 38c0d7abb02d5839948585a8eb5678d5089f3f6e Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Tue, 23 Jun 2026 14:37:12 -0700 Subject: [PATCH 1/2] feat(email-verification): expose isEmailVerified to client + settings UI Expose the identity-service `isEmailVerified` flag to clients and surface email verification status in account settings on all three clients. - identity-service: include `isEmailVerified` in the `/user/email` response - common: add `getUserEmailAndStatus()` to the identity service and a `useCurrentUserEmail` tan-query hook returning `{ email, isEmailVerified }` - settings (web desktop, mobile-web, React Native): show a verified / not-verified status indicator; hide the resend button once verified - desktop: disable the resend button while the request is in-flight to match mobile-web and RN Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/common/src/api/index.ts | 1 + .../common/src/api/tan-query/queryKeys.ts | 1 + .../users/account/useCurrentUserEmail.ts | 28 ++++++++ packages/common/src/messages/settings.ts | 2 + packages/common/src/services/auth/identity.ts | 19 +++++- packages/identity-service/src/routes/user.js | 3 +- .../settings-screen/AccountSettingsItem.tsx | 65 +++++++++++++++---- .../settings-screen/AccountSettingsScreen.tsx | 9 +++ .../components/desktop/SettingsPage.tsx | 58 ++++++++++++----- .../components/mobile/AccountSettingsPage.tsx | 60 +++++++++++++---- 10 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 packages/common/src/api/tan-query/users/account/useCurrentUserEmail.ts diff --git a/packages/common/src/api/index.ts b/packages/common/src/api/index.ts index 82f5d93f7cd..6016cb0dc34 100644 --- a/packages/common/src/api/index.ts +++ b/packages/common/src/api/index.ts @@ -145,6 +145,7 @@ export * from './tan-query/users/useOtherChatUsers' // Account export * from './tan-query/users/account/useResetPassword' export * from './tan-query/users/account/useResendRecoveryEmail' +export * from './tan-query/users/account/useCurrentUserEmail' // Playlist updates export * from './tan-query/playlist-updates/usePlaylistUpdates' diff --git a/packages/common/src/api/tan-query/queryKeys.ts b/packages/common/src/api/tan-query/queryKeys.ts index 7e8b20656fe..8cd09ef3a20 100644 --- a/packages/common/src/api/tan-query/queryKeys.ts +++ b/packages/common/src/api/tan-query/queryKeys.ts @@ -46,6 +46,7 @@ export const QUERY_KEYS = { salesCount: 'salesCount', mutualFollowers: 'mutualFollowers', emailInUse: 'emailInUse', + currentUserEmail: 'currentUserEmail', handleInUse: 'handleInUse', handleReservedStatus: 'handleReservedStatus', search: 'search', diff --git a/packages/common/src/api/tan-query/users/account/useCurrentUserEmail.ts b/packages/common/src/api/tan-query/users/account/useCurrentUserEmail.ts new file mode 100644 index 00000000000..7503d95c1f6 --- /dev/null +++ b/packages/common/src/api/tan-query/users/account/useCurrentUserEmail.ts @@ -0,0 +1,28 @@ +import { useQuery } from '@tanstack/react-query' + +import { UserEmailResponse } from '~/services/auth/identity' + +import { QUERY_KEYS } from '../../queryKeys' +import { QueryKey, QueryOptions } from '../../types' +import { useQueryContext } from '../../utils' + +import { useCurrentUserId } from './useCurrentUserId' + +export const getCurrentUserEmailQueryKey = () => + [QUERY_KEYS.currentUserEmail] as unknown as QueryKey + +/** + * Hook to get the currently logged in user's email address and email + * verification status from identity service. + */ +export const useCurrentUserEmail = (options?: QueryOptions) => { + const { identityService } = useQueryContext() + const { data: currentUserId } = useCurrentUserId() + + return useQuery({ + queryKey: getCurrentUserEmailQueryKey(), + queryFn: () => identityService.getUserEmailAndStatus(), + ...options, + enabled: options?.enabled !== false && !!currentUserId + }) +} diff --git a/packages/common/src/messages/settings.ts b/packages/common/src/messages/settings.ts index 11ac3f06dae..fa5ad5c3b90 100644 --- a/packages/common/src/messages/settings.ts +++ b/packages/common/src/messages/settings.ts @@ -69,6 +69,8 @@ export const settingsMessages = { emailVerificationAlreadyVerified: 'Your email is already verified.', emailVerificationNotSent: 'Unable to send verification email. Please try again!', + emailVerifiedStatus: 'Email verified', + emailNotVerifiedStatus: 'Email not verified', 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 48902b7d474..877ca250fdf 100644 --- a/packages/common/src/services/auth/identity.ts +++ b/packages/common/src/services/auth/identity.ts @@ -30,6 +30,11 @@ type ResendEmailVerificationResponse = { alreadyVerified?: boolean } +export type UserEmailResponse = { + email: string | undefined | null + isEmailVerified: boolean +} + enum TransactionMetadataType { PURCHASE_SOL_AUDIO_SWAP = 'PURCHASE_SOL_AUDIO_SWAP' } @@ -220,16 +225,24 @@ export class IdentityService { } /** - * Get the user's email used for notifications and display. + * Get the user's email and verification status used for notifications and + * display. */ - async getUserEmail() { + async getUserEmailAndStatus() { const headers = await this.getAuthHeaders() - const res = await this._makeRequest<{ email: string | undefined | null }>({ + return await this._makeRequest({ url: '/user/email', method: 'get', headers }) + } + + /** + * Get the user's email used for notifications and display. + */ + async getUserEmail() { + const res = await this.getUserEmailAndStatus() if (!res.email) { throw new Error('No email found') diff --git a/packages/identity-service/src/routes/user.js b/packages/identity-service/src/routes/user.js index c9f25a6dcc9..4f1ebf91ccf 100644 --- a/packages/identity-service/src/routes/user.js +++ b/packages/identity-service/src/routes/user.js @@ -198,7 +198,8 @@ module.exports = function (app) { }) return successResponse({ - email: userData.email + email: userData.email, + isEmailVerified: userData.isEmailVerified }) }) ) diff --git a/packages/mobile/src/screens/settings-screen/AccountSettingsItem.tsx b/packages/mobile/src/screens/settings-screen/AccountSettingsItem.tsx index 84fe27fdbf2..d330cb037d3 100644 --- a/packages/mobile/src/screens/settings-screen/AccountSettingsItem.tsx +++ b/packages/mobile/src/screens/settings-screen/AccountSettingsItem.tsx @@ -2,7 +2,13 @@ import type { ComponentType } from 'react' import type { SvgProps } from 'react-native-svg' -import { Button } from '@audius/harmony-native' +import { + Button, + Flex, + IconError, + IconValidationCheck, + Text +} from '@audius/harmony-native' import { makeStyles } from 'app/styles' import { SettingsRowLabel } from './SettingRowLabel' @@ -16,33 +22,66 @@ type AccountSettingsItemProps = { buttonTitle: string onPress?: () => void disabled?: boolean + /** Optional verification status shown above the action button. */ + isVerified?: boolean + verifiedText?: string + notVerifiedText?: string + /** Hide the action button (e.g. once the email is verified). */ + hideButton?: boolean } const useStyles = makeStyles(({ spacing }) => ({ button: { marginTop: spacing(2) + }, + status: { + marginTop: spacing(2) } })) export const AccountSettingsItem = (props: AccountSettingsItemProps) => { - const { title, titleIcon, description, buttonTitle, onPress, disabled } = - props + const { + title, + titleIcon, + description, + buttonTitle, + onPress, + disabled, + isVerified, + verifiedText, + notVerifiedText, + hideButton + } = props const styles = useStyles() return ( {description} - + {verifiedText && notVerifiedText ? ( + + {isVerified ? ( + + ) : ( + + )} + + {isVerified ? verifiedText : notVerifiedText} + + + ) : null} + {hideButton ? null : ( + + )} ) } diff --git a/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx b/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx index f720c8bc42f..2001cf55ae6 100644 --- a/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx +++ b/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx @@ -2,6 +2,7 @@ import { useCallback, useState } from 'react' import { useCurrentAccountUser, + useCurrentUserEmail, useQueryContext, useResendRecoveryEmail } from '@audius/common/api' @@ -52,6 +53,8 @@ const messages = { emailVerificationAlreadyVerified: 'Your email is already verified.', emailVerificationNotSent: 'Unable to send verification email. Please try again!', + emailVerifiedStatus: 'Email verified', + emailNotVerifiedStatus: 'Email not verified', verifyTitle: 'Verification', verifyDescription: 'Verify your Audius profile by completing identity verification.', @@ -90,6 +93,8 @@ export const AccountSettingsScreen = () => { const { identityService } = useQueryContext() const [isSendingVerificationEmail, setIsSendingVerificationEmail] = useState(false) + const { data: emailData } = useCurrentUserEmail() + const isEmailVerified = emailData?.isEmailVerified const { data: accountData } = useCurrentAccountUser({ select: (user) => pick(user, ['user_id', 'handle', 'name']) }) @@ -191,6 +196,10 @@ export const AccountSettingsScreen = () => { buttonTitle={messages.emailVerificationButtonTitle} onPress={handlePressEmailVerification} disabled={isSendingVerificationEmail} + isVerified={isEmailVerified} + verifiedText={messages.emailVerifiedStatus} + notVerifiedText={messages.emailNotVerifiedStatus} + hideButton={isEmailVerified} /> { const emailFrequency = useSelector(getEmailFrequency) const notificationSettings = useSelector(getBrowserNotificationSettings) const { tier } = useTierAndVerifiedForUser(userId) + const { data: emailData } = useCurrentUserEmail() + const isEmailVerified = emailData?.isEmailVerified const showMatrix = tier === 'gold' || tier === 'platinum' || @@ -649,22 +656,39 @@ export const SettingsPage = () => { title={settingsMessages.emailVerificationCardTitle} description={settingsMessages.emailVerificationCardDescription} > - - - + + + {isEmailVerified ? ( + + ) : ( + + )} + + {isEmailVerified + ? settingsMessages.emailVerifiedStatus + : settingsMessages.emailNotVerifiedStatus} + + + {!isEmailVerified ? ( + + + + ) : null} + ) : 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 d31d37e9b41..b1b8424ba7c 100644 --- a/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx +++ b/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx @@ -1,6 +1,10 @@ import { useState, useContext, useCallback } from 'react' -import { useQueryContext, useCurrentAccountUser } from '@audius/common/api' +import { + useQueryContext, + useCurrentAccountUser, + useCurrentUserEmail +} from '@audius/common/api' import { Name, SquareSizes } from '@audius/common/models' import { useTierAndVerifiedForUser } from '@audius/common/store' import { route } from '@audius/common/utils' @@ -8,8 +12,10 @@ import { Button, IconRecoveryEmail, IconEmailAddress, + IconError, IconKey, IconSignOut, + IconValidationCheck, IconVerified, Flex, Text, @@ -53,6 +59,8 @@ const messages = { emailVerificationAlreadyVerified: 'Your email is already verified.', emailVerificationNotSent: 'Unable to send verification email. Please try again!', + emailVerifiedStatus: 'Email verified', + emailNotVerifiedStatus: 'Email not verified', verifyTitle: 'Verify Your Account', verifyDescription: 'Verify your Audius profile by completing identity verification', @@ -83,6 +91,12 @@ type AccountSettingsItemProps = { buttonTitle: string disabled?: boolean onClick: () => void + /** Optional verification status shown above the action button. */ + isVerified?: boolean + verifiedText?: string + notVerifiedText?: string + /** Hide the action button (e.g. once the email is verified). */ + hideButton?: boolean } const AccountSettingsItem = ({ @@ -91,7 +105,11 @@ const AccountSettingsItem = ({ icon: Icon, buttonTitle, disabled, - onClick + onClick, + isVerified, + verifiedText, + notVerifiedText, + hideButton }: AccountSettingsItemProps) => { const { color } = useTheme() return ( @@ -123,15 +141,29 @@ const AccountSettingsItem = ({ > {description} - + {verifiedText && notVerifiedText ? ( + + {isVerified ? ( + + ) : ( + + )} + + {isVerified ? verifiedText : notVerifiedText} + + + ) : null} + {hideButton ? null : ( + + )} ) } @@ -147,6 +179,8 @@ const AccountSettingsPage = () => { }) }) const { userId, handle, name } = accountData ?? {} + const { data: emailData } = useCurrentUserEmail() + const isEmailVerified = emailData?.isEmailVerified const [showModalSignOut, setShowModalSignOut] = useState(false) const [isSendingVerificationEmail, setIsSendingVerificationEmail] = useState(false) @@ -239,6 +273,10 @@ const AccountSettingsPage = () => { buttonTitle={messages.emailVerificationButtonTitle} onClick={onClickResendVerificationEmail} disabled={isSendingVerificationEmail} + isVerified={isEmailVerified} + verifiedText={messages.emailVerifiedStatus} + notVerifiedText={messages.emailNotVerifiedStatus} + hideButton={isEmailVerified} /> Date: Tue, 23 Jun 2026 15:11:17 -0700 Subject: [PATCH 2/2] revert(contests): remove event permalink additions, keep track permalink pattern --- packages/common/src/hooks/useShareContent.ts | 8 -------- packages/common/src/models/Event.ts | 7 ++----- packages/common/src/store/social/tracks/actions.ts | 6 +----- packages/common/src/store/ui/share-modal/types.ts | 6 +----- .../mobile/src/components/share-drawer/utils.ts | 13 +++++-------- .../src/screens/contest-screen/ContestScreen.tsx | 3 +-- packages/mobile/src/utils/routes.tsx | 11 +++-------- .../src/sdk/api/generated/default/models/Event.ts | 8 -------- .../web/src/common/store/social/tracks/sagas.ts | 7 ++----- .../web/src/components/share-modal/ShareModal.tsx | 4 +--- packages/web/src/components/share-modal/utils.ts | 8 ++------ .../contest-page/components/desktop/ContestPage.tsx | 11 +++++------ .../contest-page/components/mobile/ContestPage.tsx | 9 ++++----- packages/web/src/utils/route.test.ts | 13 ------------- packages/web/src/utils/route.ts | 8 ++------ 15 files changed, 29 insertions(+), 93 deletions(-) diff --git a/packages/common/src/hooks/useShareContent.ts b/packages/common/src/hooks/useShareContent.ts index ca6a8efff6a..948019f6a43 100644 --- a/packages/common/src/hooks/useShareContent.ts +++ b/packages/common/src/hooks/useShareContent.ts @@ -30,14 +30,6 @@ export const useShareContent = ( if (request.type === 'track' || request.type === 'contest') { if (!track || !trackArtist) return null - if (request.type === 'contest') { - return { - type: 'contest', - track, - artist: trackArtist, - ...(request.eventPermalink ? { eventPermalink: request.eventPermalink } : {}) - } - } return { type: request.type, track, artist: trackArtist } } diff --git a/packages/common/src/models/Event.ts b/packages/common/src/models/Event.ts index dc3f78f2bbc..03cfb54432c 100644 --- a/packages/common/src/models/Event.ts +++ b/packages/common/src/models/Event.ts @@ -1,6 +1,8 @@ import { Event as EventSDK } from '@audius/sdk' import type { OverrideProperties } from 'type-fest' + import { Nullable } from '../utils/typeUtils' + import { ID } from './Identifiers' export type Event = OverrideProperties< @@ -9,10 +11,5 @@ export type Event = OverrideProperties< eventId: ID userId: ID entityId: Nullable - /** Canonical contest permalink (e.g. /{handle}/contest-title). - * Set once the API returns a permalink field from event_routes. - * Optional - consumers fall back to deriving the URL from the - * associated track's permalink when this is absent. */ - permalink?: string } > diff --git a/packages/common/src/store/social/tracks/actions.ts b/packages/common/src/store/social/tracks/actions.ts index d9e9b4c02a4..6cdeda8f60d 100644 --- a/packages/common/src/store/social/tracks/actions.ts +++ b/packages/common/src/store/social/tracks/actions.ts @@ -129,9 +129,5 @@ export const shareTrack = createCustomAction( */ export const shareContest = createCustomAction( SHARE_CONTEST, - (trackId: ID, source: ShareSource, eventPermalink?: string) => ({ - trackId, - source, - eventPermalink - }) + (trackId: ID, source: ShareSource) => ({ trackId, source }) ) diff --git a/packages/common/src/store/ui/share-modal/types.ts b/packages/common/src/store/ui/share-modal/types.ts index f6fbee106f4..3cfc2191785 100644 --- a/packages/common/src/store/ui/share-modal/types.ts +++ b/packages/common/src/store/ui/share-modal/types.ts @@ -16,15 +16,11 @@ type ShareTrackContent = { * Contest shares use the same underlying data as a track share (the * contest is keyed off a parent track) but link to the contest page * (`{trackPermalink}/contest`) instead of the track itself. - * When the event has its own permalink from event_routes, that value is - * passed as `eventPermalink` and used directly. */ type ShareContestContent = { type: 'contest' track: Track artist: User - /** Canonical contest permalink from event_routes, if available. */ - eventPermalink?: string } type ShareProfileContent = { @@ -59,7 +55,7 @@ export type ShareContent = export type ShareModalRequest = | { type: 'track'; trackId: ID } - | { type: 'contest'; trackId: ID; eventPermalink?: string } + | { type: 'contest'; trackId: ID } | { type: 'profile'; profileId: ID } | { type: 'collection'; collectionId: ID } diff --git a/packages/mobile/src/components/share-drawer/utils.ts b/packages/mobile/src/components/share-drawer/utils.ts index d733e8904c2..d99f824b6c1 100644 --- a/packages/mobile/src/components/share-drawer/utils.ts +++ b/packages/mobile/src/components/share-drawer/utils.ts @@ -17,14 +17,11 @@ export const getContentUrl = (content: ShareContent) => { return getTrackRoute(track, true) } case 'contest': { - // Contest shares point at the contest page. When the event has its - // own permalink from event_routes, use it directly; otherwise derive - // from the parent track's permalink. - const { track, eventPermalink } = content - return getContestRoute( - { permalink: track.permalink, contestPermalink: eventPermalink }, - true - ) + // Contest shares point at the parent track's contest page — + // `{permalink}/contest` — so sharing copies the contest URL + // rather than the underlying track. + const { track } = content + return getContestRoute(track, true) } case 'profile': { const { profile } = content diff --git a/packages/mobile/src/screens/contest-screen/ContestScreen.tsx b/packages/mobile/src/screens/contest-screen/ContestScreen.tsx index d669672cfd1..73971f3a710 100644 --- a/packages/mobile/src/screens/contest-screen/ContestScreen.tsx +++ b/packages/mobile/src/screens/contest-screen/ContestScreen.tsx @@ -216,8 +216,7 @@ export const ContestScreen = () => { shareModalUIActions.requestOpen({ type: 'contest', trackId, - source: ShareSource.PAGE, - ...(contest?.permalink ? { eventPermalink: contest.permalink } : {}) + source: ShareSource.PAGE }) ) }, [dispatch, trackId, contest?.permalink]) diff --git a/packages/mobile/src/utils/routes.tsx b/packages/mobile/src/utils/routes.tsx index 000411ff510..c87f7e961ff 100644 --- a/packages/mobile/src/utils/routes.tsx +++ b/packages/mobile/src/utils/routes.tsx @@ -16,16 +16,11 @@ export const getTrackRoute = ( } export const getContestRoute = ( - track: { permalink: string; contestPermalink?: string }, + track: { permalink: string }, fullUrl = false ) => { - // If the event has its own permalink from event_routes, use it directly. - if (track.contestPermalink) { - const route = track.contestPermalink - return fullUrl ? `${AUDIUS_URL}${route}` : route - } - // Fallback: derive contest URL from the track permalink. - // `/{handle}/{slug}` → `/{handle}/contest/{slug}`. + // Permalink shape: `/{handle}/{slug}` → contest URL is + // `/{handle}/contest/{slug}`. Mirror the web `contestPage` helper. const [, handle, ...rest] = track.permalink.split('/') const route = `/${handle}/contest/${rest.join('/')}` return fullUrl ? `${AUDIUS_URL}${route}` : route diff --git a/packages/sdk/src/sdk/api/generated/default/models/Event.ts b/packages/sdk/src/sdk/api/generated/default/models/Event.ts index a10a9ecd2fc..8846fdd120d 100644 --- a/packages/sdk/src/sdk/api/generated/default/models/Event.ts +++ b/packages/sdk/src/sdk/api/generated/default/models/Event.ts @@ -79,12 +79,6 @@ export interface Event { * @memberof Event */ eventData: object; - /** - * Canonical contest permalink derived from event_routes. - * @type {string} - * @memberof Event - */ - permalink?: string; } @@ -144,7 +138,6 @@ export function EventFromJSONTyped(json: any, ignoreDiscriminator: boolean): Eve 'createdAt': json['created_at'], 'updatedAt': json['updated_at'], 'eventData': json['event_data'], - 'permalink': !exists(json, 'permalink') ? undefined : json['permalink'], }; } @@ -167,7 +160,6 @@ export function EventToJSON(value?: Event | null): any { 'created_at': value.createdAt, 'updated_at': value.updatedAt, 'event_data': value.eventData, - 'permalink': value.permalink, }; } diff --git a/packages/web/src/common/store/social/tracks/sagas.ts b/packages/web/src/common/store/social/tracks/sagas.ts index cd708b8e3f8..bb3a3545268 100644 --- a/packages/web/src/common/store/social/tracks/sagas.ts +++ b/packages/web/src/common/store/social/tracks/sagas.ts @@ -807,7 +807,7 @@ function* watchShareContest() { yield* takeEvery( socialActions.SHARE_CONTEST, function* (action: ReturnType) { - const { trackId, eventPermalink } = action + const { trackId } = action const track = yield* queryTrack(trackId) if (!track) return @@ -815,10 +815,7 @@ function* watchShareContest() { const user = yield* queryUser(track.owner_id) if (!user) return - // Prefer event_routes permalink; fall back to deriving from track permalink. - const link = eventPermalink - ? fullContestPage(eventPermalink) - : fullContestPage(track.permalink) + const link = fullContestPage(track.permalink) const share = yield* getContext('share') share(link, formatShareText(track.title, user.name)) diff --git a/packages/web/src/components/share-modal/ShareModal.tsx b/packages/web/src/components/share-modal/ShareModal.tsx index ae70abf85b2..2c5ba880753 100644 --- a/packages/web/src/components/share-modal/ShareModal.tsx +++ b/packages/web/src/components/share-modal/ShareModal.tsx @@ -94,9 +94,7 @@ export const ShareModal = NiceModal.create(() => { // Contest copy-link uses the dedicated `shareContest` saga, // which writes the contest URL (`{permalink}/contest`) to // the clipboard rather than the track permalink. - dispatch( - shareContest(content.track.track_id, source, content.eventPermalink) - ) + dispatch(shareContest(content.track.track_id, source)) break case 'profile': dispatch(shareUser(content.profile.user_id, source)) diff --git a/packages/web/src/components/share-modal/utils.ts b/packages/web/src/components/share-modal/utils.ts index 83e32a6e155..e25db6ab855 100644 --- a/packages/web/src/components/share-modal/utils.ts +++ b/packages/web/src/components/share-modal/utils.ts @@ -44,14 +44,10 @@ export const getXShareText = async ( case 'contest': { const { track: { title, permalink, track_id }, - artist, - eventPermalink + artist } = content xText = messageConfig.contestShareText(title, getXShareHandle(artist)) - // Prefer event_routes permalink; fall back to deriving from track permalink. - link = eventPermalink - ? fullContestPage(eventPermalink) - : fullContestPage(permalink) + link = fullContestPage(permalink) // Contest shares route through the parent track's analytics // entry — there's no dedicated 'contest' event kind yet and // the parent track is what gets credited for engagement. diff --git a/packages/web/src/pages/contest-page/components/desktop/ContestPage.tsx b/packages/web/src/pages/contest-page/components/desktop/ContestPage.tsx index 03b5bf07119..a3de06d3467 100644 --- a/packages/web/src/pages/contest-page/components/desktop/ContestPage.tsx +++ b/packages/web/src/pages/contest-page/components/desktop/ContestPage.tsx @@ -306,7 +306,7 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => { if (trackId) { dispatch(fetchTrackSucceeded({ trackId })) } - }, [contest?.permalink, dispatch, trackId]) + }, [dispatch, trackId]) useEffect(() => { return function cleanup() { @@ -405,11 +405,10 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => { shareModalUIActions.requestOpen({ type: 'contest', trackId, - source: ShareSource.PAGE, - ...(contest?.permalink ? { eventPermalink: contest.permalink } : {}) + source: ShareSource.PAGE }) ) - }, [contest?.permalink, dispatch, trackId]) + }, [dispatch, trackId]) const renderActions = useCallback(() => { if (!eventId) return null @@ -491,7 +490,7 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => { return ( { return ( {/* Content is centered to MAX_CONTENT_WIDTH and sits on the diff --git a/packages/web/src/pages/contest-page/components/mobile/ContestPage.tsx b/packages/web/src/pages/contest-page/components/mobile/ContestPage.tsx index 4becfe417ef..0c7ca34206e 100644 --- a/packages/web/src/pages/contest-page/components/mobile/ContestPage.tsx +++ b/packages/web/src/pages/contest-page/components/mobile/ContestPage.tsx @@ -300,11 +300,10 @@ const ContestPage = ({ shareModalUIActions.requestOpen({ type: 'contest', trackId, - source: ShareSource.PAGE, - ...(contest?.permalink ? { eventPermalink: contest.permalink } : {}) + source: ShareSource.PAGE }) ) - }, [contest?.permalink, dispatch, trackId]) + }, [dispatch, trackId]) const { imageUrl: trackCoverArtUrl } = useTrackCoverArt({ trackId, @@ -393,7 +392,7 @@ const ContestPage = ({ if (trackId) { dispatch(remixesPageActions.fetchTrackSucceeded({ trackId })) } - }, [contest?.permalink, dispatch, trackId]) + }, [dispatch, trackId]) useEffect(() => { return function cleanup() { @@ -506,7 +505,7 @@ const ContestPage = ({ return ( {/* Top section: hero banner + meta (title, CTA, deadline, diff --git a/packages/web/src/utils/route.test.ts b/packages/web/src/utils/route.test.ts index 47de2d86455..b3dd8561535 100644 --- a/packages/web/src/utils/route.test.ts +++ b/packages/web/src/utils/route.test.ts @@ -40,16 +40,3 @@ describe('CONTEST_PAGE route pattern', () => { expect(CONTEST_PAGE).not.toBe(TRACK_REMIXES_PAGE) }) }) - -describe('contestPage with event permalink passthrough', () => { - it('returns an event-routes permalink unchanged when it already contains contest', () => { - // event_routes produces slugs like /Protohype/best-remix-contest - // which already contain a 'contest' segment -- return as-is. - expect(contestPage('/Protohype/contest/best-remix-contest')).toBe( - '/Protohype/contest/best-remix-contest' - ) - }) - it('still rewrites a plain track permalink when no contest segment exists', () => { - expect(contestPage('/Artist/my-track')).toBe('/Artist/contest/my-track') - }) -}) diff --git a/packages/web/src/utils/route.ts b/packages/web/src/utils/route.ts index d7679709683..27a166f9173 100644 --- a/packages/web/src/utils/route.ts +++ b/packages/web/src/utils/route.ts @@ -65,13 +65,9 @@ export const fullPickWinnersPage = (permalink: string) => { } export const contestPage = (permalink: string) => { - // If permalink already contains a "contest" segment (i.e. it came from - // event_routes rather than track_routes) return it as-is. - const parts = permalink.split('/') - if (parts.includes('contest')) return permalink - // Track permalink shape: `/{handle}/{slug}`. Inject the literal + // Permalink shape: `/{handle}/{slug}`. Contest URL injects the literal // "contest" segment between handle and slug → `/{handle}/contest/{slug}`. - const [, handle, ...rest] = parts + const [, handle, ...rest] = permalink.split('/') return `/${handle}/contest/${rest.join('/')}` } export const fullContestPage = (permalink: string) => {