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
2 changes: 2 additions & 0 deletions packages/common/src/context/appContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type AppContextType = {
audiusSdk?: AudiusSdkWithServices
remoteConfig: RemoteConfigInstance
localStorage: LocalStorage
/** Restarts the app after an account switch. Web falls back to a page reload. */
reloadApp?: () => void
}

export const AppContext = createContext<AppContextType | null>(null)
Expand Down
10 changes: 7 additions & 3 deletions packages/common/src/hooks/useAccountSwitcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAppContext } from '~/context'
import { UserMetadata } from '~/models/User'

export const useAccountSwitcher = () => {
const { localStorage } = useAppContext()
const { localStorage, reloadApp } = useAppContext()
const { data: currentWeb3User } = useCurrentWeb3Account()

const switchAccount = useCallback(
Expand All @@ -24,9 +24,13 @@ export const useAccountSwitcher = () => {
await localStorage.clearAudiusAccount()
await localStorage.clearAudiusAccountUser()

window.location.reload()
if (reloadApp) {
reloadApp()
} else {
window.location.reload()
}
},
[currentWeb3User, localStorage]
[currentWeb3User, localStorage, reloadApp]
)

/** Convenience method to switch out of Manager Mode and back to the current web3 user */
Expand Down
11 changes: 8 additions & 3 deletions packages/common/src/services/remote-config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export enum FeatureFlags {
TRACK_ROWS = 'track_rows',
NOW_PLAYING_COMMENTS = 'now_playing_comments',
TRACK_TILE_WAVEFORM = 'track_tile_waveform',
NOW_PLAYING_WAVEFORM = 'now_playing_waveform'
NOW_PLAYING_WAVEFORM = 'now_playing_waveform',
MOBILE_MANAGER_MODE = 'mobile_manager_mode'
}

type FlagDefaults = Record<FeatureFlags, boolean>
Expand All @@ -32,7 +33,8 @@ export const environmentFlagDefaults: Record<
development: {
[FeatureFlags.FAN_CLUB_TEXT_POST_POSTING]: true,
[FeatureFlags.TRACK_TILE_WAVEFORM]: true,
[FeatureFlags.NOW_PLAYING_WAVEFORM]: true
[FeatureFlags.NOW_PLAYING_WAVEFORM]: true,
[FeatureFlags.MOBILE_MANAGER_MODE]: true
},
production: {}
}
Expand Down Expand Up @@ -67,5 +69,8 @@ export const flagDefaults: FlagDefaults = {
// Waveform with timestamped comment avatars on desktop web track tiles.
[FeatureFlags.TRACK_TILE_WAVEFORM]: false,
// Waveform scrubber with comment avatars in the mobile Now Playing drawer.
[FeatureFlags.NOW_PLAYING_WAVEFORM]: true
[FeatureFlags.NOW_PLAYING_WAVEFORM]: true,
// Account switcher and manager settings on mobile. Switching back out and
// the manager-mode restrictions stay on regardless of this flag.
[FeatureFlags.MOBILE_MANAGER_MODE]: false
}
4 changes: 3 additions & 1 deletion packages/mobile/src/app/AppContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo, type ReactNode } from 'react'

import { AppContext } from '@audius/common/context'
import RNRestart from 'react-native-restart'
import { useAsync } from 'react-use'

import * as analytics from 'app/services/analytics'
Expand Down Expand Up @@ -31,7 +32,8 @@ export const AppContextProvider = (props: AppContextProviderProps) => {
audiusSdk: sdk,
audiusBackend: audiusBackendInstance,
trackDownload,
remoteConfig: remoteConfigInstance
remoteConfig: remoteConfigInstance,
reloadApp: () => RNRestart.Restart()
}),
[sdk]
)
Expand Down
2 changes: 2 additions & 0 deletions packages/mobile/src/app/Drawers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { InboxUnavailableDrawer } from 'app/components/inbox-unavailable-drawer/
import { LeavingAudiusDrawer } from 'app/components/leaving-audius-drawer'
import { LockedContentDrawer } from 'app/components/locked-content-drawer'
import { LockedTextPostDrawer } from 'app/components/locked-text-post-drawer'
import { AccountSwitcherDrawer } from 'app/components/manager-mode/AccountSwitcherDrawer'
import { ManagerModeDrawer } from 'app/components/manager-mode-drawer/ManagerModeDrawer'
import { OverflowMenuDrawer } from 'app/components/overflow-menu-drawer'
import { PlaybackRateDrawer } from 'app/components/playback-rate-drawer'
Expand Down Expand Up @@ -163,6 +164,7 @@ const nativeDrawersMap: { [DrawerName in Drawer]?: LazyComponent } = {
DeleteTrackConfirmation: () => DeleteTrackConfirmationDrawer,
ShareToStoryProgress: () => ShareToStoryProgressDrawer,
ManagerMode: () => ManagerModeDrawer,
AccountSwitcher: () => AccountSwitcherDrawer,
RateCallToAction: () => RateCtaDrawer,
RemoveAllDownloads: () => RemoveAllDownloadsDrawer,
RemoveDownloadedCollection: () => RemoveDownloadedCollectionDrawer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import { useEffect } from 'react'

import { useFeatureFlag } from '@audius/common/hooks'
import { FeatureFlags } from '@audius/common/services'

import { useDrawer } from 'app/hooks/useDrawer'
import { useNavigation } from 'app/hooks/useNavigation'
import { useRoute } from 'app/hooks/useRoute'
import type { ProfileTabScreenParamList } from 'app/screens/app-screen/ProfileTabScreen'

const PATH_TO_SCREEN = {
'accounts-you-manage': 'AccountsYouManageScreen',
'managing-you': 'AccountsManagingYouScreen'
} as const

/** Handles /settings/accounts-you-manage and /settings/managing-you links */
export const useShowManagerModeNotAvailable = () => {
const { params } = useRoute()
const path = (params as { path: string })?.path
const navigation = useNavigation<ProfileTabScreenParamList>()
const { onOpen: openManagerModeDrawer } = useDrawer('ManagerMode')
const { isEnabled: isManagerModeEnabled, isLoaded } = useFeatureFlag(
FeatureFlags.MOBILE_MANAGER_MODE
)

useEffect(() => {
if (path === 'accounts-you-manage') {
if (!isLoaded || !(path in PATH_TO_SCREEN)) return
if (isManagerModeEnabled) {
navigation.push(PATH_TO_SCREEN[path as keyof typeof PATH_TO_SCREEN])
} else if (path === 'accounts-you-manage') {
openManagerModeDrawer()
}
}, [path, openManagerModeDrawer])
}, [path, isLoaded, isManagerModeEnabled, navigation, openManagerModeDrawer])
}
42 changes: 42 additions & 0 deletions packages/mobile/src/components/manager-mode/AccountRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ReactNode } from 'react'

import type { UserMetadata } from '@audius/common/models'

import { Flex, Text } from '@audius/harmony-native'
import { ProfilePicture } from 'app/components/core'
import { UserBadges } from 'app/components/user-badges'

type AccountRowProps = {
user: Pick<UserMetadata, 'user_id' | 'name' | 'handle'>
isSelected?: boolean
/** Rendered on the right side of the row */
children?: ReactNode
}

export const AccountRow = ({ user, isSelected, children }: AccountRowProps) => {
const textColor = isSelected ? 'accent' : 'default'
return (
<Flex row alignItems='center' gap='m' flex={1}>
<ProfilePicture userId={user.user_id} size='large' />
<Flex flex={1} gap='2xs'>
<Flex row alignItems='center' gap='xs'>
<Text
variant='body'
size='m'
strength='strong'
color={textColor}
numberOfLines={1}
style={{ flexShrink: 1 }}
>
{user.name}
</Text>
<UserBadges userId={user.user_id} badgeSize='xs' />
</Flex>
<Text variant='body' size='s' color={textColor} numberOfLines={1}>
@{user.handle}
</Text>
</Flex>
{children}
</Flex>
)
}
118 changes: 118 additions & 0 deletions packages/mobile/src/components/manager-mode/AccountSwitcherDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useCallback, useMemo, useState } from 'react'

import { useAccountSwitcher } from '@audius/common/hooks'
import type { UserMetadata } from '@audius/common/models'
import { TouchableOpacity } from 'react-native'

import {
Flex,
IconCheck,
IconUserArrowRotate,
Text
} from '@audius/harmony-native'
import { NativeDrawer } from 'app/components/drawer'
import { useDrawer } from 'app/hooks/useDrawer'

import { DrawerHeader } from '../core/DrawerHeader'

import { AccountRow } from './AccountRow'
import { useManagerModeAccounts } from './useManagerModeAccounts'

const messages = {
switchAccount: 'Switch Account',
managedAccounts: 'Managed Accounts',
switching: 'Switching...'
}

type SwitcherRowProps = {
user: UserMetadata
isSelected: boolean
isSwitching: boolean
onPress: (user: UserMetadata) => void
}

const SwitcherRow = ({
user,
isSelected,
isSwitching,
onPress
}: SwitcherRowProps) => (
<TouchableOpacity
disabled={isSelected || isSwitching}
onPress={() => onPress(user)}
accessibilityRole='button'
accessibilityState={{ selected: isSelected }}
>
<Flex ph='xl' pv='m'>
<AccountRow user={user} isSelected={isSelected}>
{isSelected ? <IconCheck size='m' color='accent' /> : null}
</AccountRow>
</Flex>
</TouchableOpacity>
)

export const AccountSwitcherDrawer = () => {
const { onClose } = useDrawer('AccountSwitcher')
const { web3User, currentUserId, accounts } = useManagerModeAccounts()
const { switchAccount } = useAccountSwitcher()
const [isSwitching, setIsSwitching] = useState(false)

// Keep the account in use at the top of the managed list
const sortedAccounts = useMemo(() => {
const selected = accounts.filter(
({ user }) => user.user_id === currentUserId
)
const rest = accounts.filter(({ user }) => user.user_id !== currentUserId)
return [...selected, ...rest]
}, [accounts, currentUserId])

const handlePress = useCallback(
(user: UserMetadata) => {
if (user.user_id === currentUserId) return
setIsSwitching(true)
switchAccount(user)
},
[currentUserId, switchAccount]
)

if (!web3User) return null

return (
<NativeDrawer drawerName='AccountSwitcher' onClose={onClose}>
<Flex pt='xl' pb='2xl'>
<Flex ph='xl'>
<DrawerHeader
icon={IconUserArrowRotate}
title={isSwitching ? messages.switching : messages.switchAccount}
/>
</Flex>
<SwitcherRow
user={web3User}
isSelected={currentUserId === web3User.user_id}
isSwitching={isSwitching}
onPress={handlePress}
/>
<Flex
ph='xl'
pv='s'
backgroundColor='surface1'
borderTop='default'
borderBottom='default'
>
<Text variant='title' size='xs'>
{messages.managedAccounts}
</Text>
</Flex>
{sortedAccounts.map(({ user }) => (
<SwitcherRow
key={user.user_id}
user={user}
isSelected={currentUserId === user.user_id}
isSwitching={isSwitching}
onPress={handlePress}
/>
))}
</Flex>
</NativeDrawer>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { ComponentType } from 'react'

import { useCurrentAccountUser } from '@audius/common/api'
import { useAccountSwitcher, useIsManagedAccount } from '@audius/common/hooks'

import { Button, Flex, IconUserArrowRotate, Text } from '@audius/harmony-native'
import { Screen, ScreenContent } from 'app/components/core'

const messages = {
title: 'Not Available',
description: (handle?: string) =>
`Messages aren't available while you're managing ${
handle ? `@${handle}` : 'another account'
}.`,
switchBack: 'Switch to My Account'
}

export const ManagedAccountUnavailableScreen = () => {
const { data: handle } = useCurrentAccountUser({
select: (user) => user?.handle
})
const { switchToWeb3User } = useAccountSwitcher()

return (
<Screen variant='secondary' title={messages.title} topbarRight={null}>
<ScreenContent>
<Flex flex={1} p='2xl' gap='xl' alignItems='center' pt='4xl'>
<IconUserArrowRotate color='subdued' size='3xl' />
<Text variant='body' size='l' textAlign='center'>
{messages.description(handle)}
</Text>
<Button variant='secondary' onPress={switchToWeb3User}>
{messages.switchBack}
</Button>
</Flex>
</ScreenContent>
</Screen>
)
}

/**
* Renders the unavailable screen instead of `Component` while managing
* another account. Used for screens that act as the signed-in wallet
* (e.g. DMs), which a manager can't do on the managed account's behalf.
*/
export const withManagedAccountRestriction = <P extends object>(
Component: ComponentType<P>
) => {
const Restricted = (props: P) => {
const isManagedAccount = useIsManagedAccount()
if (isManagedAccount) return <ManagedAccountUnavailableScreen />
return <Component {...props} />
}
Restricted.displayName = `withManagedAccountRestriction(${
Component.displayName ?? Component.name
})`
return Restricted
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect, useRef } from 'react'

import { useAccountSwitcher } from '@audius/common/hooks'

import { useManagerModeAccounts } from './useManagerModeAccounts'

/**
* Switches back to the signed-in user if their access to the managed account
* they're using was revoked. Runs once per app launch.
*/
export const useManagedAccountAccessCheck = () => {
const { currentUserId, isManagedAccount, isAccountsLoaded, accounts } =
useManagerModeAccounts()
const { switchToWeb3User } = useAccountSwitcher()
const hasChecked = useRef(false)

useEffect(() => {
if (hasChecked.current || !currentUserId || !isAccountsLoaded) return
hasChecked.current = true
if (
isManagedAccount &&
!accounts.some(({ user }) => user.user_id === currentUserId)
) {
switchToWeb3User()
}
}, [
accounts,
currentUserId,
isAccountsLoaded,
isManagedAccount,
switchToWeb3User
])
}
Loading
Loading