Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/common/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/api/tan-query/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const QUERY_KEYS = {
salesCount: 'salesCount',
mutualFollowers: 'mutualFollowers',
emailInUse: 'emailInUse',
currentUserEmail: 'currentUserEmail',
handleInUse: 'handleInUse',
handleReservedStatus: 'handleReservedStatus',
search: 'search',
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserEmailResponse>

/**
* 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
})
}
8 changes: 0 additions & 8 deletions packages/common/src/hooks/useShareContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/messages/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 2 additions & 5 deletions packages/common/src/models/Event.ts
Original file line number Diff line number Diff line change
@@ -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<
Expand All @@ -9,10 +11,5 @@ export type Event = OverrideProperties<
eventId: ID
userId: ID
entityId: Nullable<ID>
/** 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
}
>
19 changes: 16 additions & 3 deletions packages/common/src/services/auth/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand Down Expand Up @@ -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<UserEmailResponse>({
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')
Expand Down
6 changes: 1 addition & 5 deletions packages/common/src/store/social/tracks/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
)
6 changes: 1 addition & 5 deletions packages/common/src/store/ui/share-modal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 }

Expand Down
3 changes: 2 additions & 1 deletion packages/identity-service/src/routes/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ module.exports = function (app) {
})

return successResponse({
email: userData.email
email: userData.email,
isEmailVerified: userData.isEmailVerified
})
})
)
Expand Down
13 changes: 5 additions & 8 deletions packages/mobile/src/components/share-drawer/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions packages/mobile/src/screens/contest-screen/ContestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
65 changes: 52 additions & 13 deletions packages/mobile/src/screens/settings-screen/AccountSettingsItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 (
<SettingsRow>
<SettingsRowLabel label={title} icon={titleIcon} />
<SettingsRowDescription>{description}</SettingsRowDescription>
<Button
style={styles.button}
variant='secondary'
size='small'
fullWidth
onPress={onPress}
disabled={disabled}
>
{buttonTitle}
</Button>
{verifiedText && notVerifiedText ? (
<Flex row alignItems='center' gap='xs' style={styles.status}>
{isVerified ? (
<IconValidationCheck size='s' />
) : (
<IconError size='s' color='subdued' />
)}
<Text variant='body' size='s' color='subdued'>
{isVerified ? verifiedText : notVerifiedText}
</Text>
</Flex>
) : null}
{hideButton ? null : (
<Button
style={styles.button}
variant='secondary'
size='small'
fullWidth
onPress={onPress}
disabled={disabled}
>
{buttonTitle}
</Button>
)}
</SettingsRow>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useState } from 'react'

import {
useCurrentAccountUser,
useCurrentUserEmail,
useQueryContext,
useResendRecoveryEmail
} from '@audius/common/api'
Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -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'])
})
Expand Down Expand Up @@ -191,6 +196,10 @@ export const AccountSettingsScreen = () => {
buttonTitle={messages.emailVerificationButtonTitle}
onPress={handlePressEmailVerification}
disabled={isSendingVerificationEmail}
isVerified={isEmailVerified}
verifiedText={messages.emailVerifiedStatus}
notVerifiedText={messages.emailNotVerifiedStatus}
hideButton={isEmailVerified}
/>
<AccountSettingsItem
title={messages.verifyTitle}
Expand Down
11 changes: 3 additions & 8 deletions packages/mobile/src/utils/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 0 additions & 8 deletions packages/sdk/src/sdk/api/generated/default/models/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@ export interface Event {
* @memberof Event
*/
eventData: object;
/**
* Canonical contest permalink derived from event_routes.
* @type {string}
* @memberof Event
*/
permalink?: string;
}


Expand Down Expand Up @@ -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'],
};
}

Expand All @@ -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,
};
}

Loading
Loading