Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PAY-2722] Add account switcher #8269

Merged
merged 16 commits into from
May 7, 2024
Merged
59 changes: 58 additions & 1 deletion packages/common/src/api/account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { createApi } from '~/audius-query'
import { Id } from './utils'
import {
ID,
UserMetadata,
managedUserListFromSDK,
userManagerListFromSDK
} from '~/models'

type ResetPasswordArgs = {
email: string
Expand All @@ -18,6 +25,21 @@ const accountApi = createApi({
type: 'query'
}
},
getCurrentWeb3User: {
async fetch(_, { audiusBackend }) {
const libs = await audiusBackend.getAudiusLibsTyped()
// TODO: https://linear.app/audius/issue/PAY-2838/separate-walletentropy-user-and-current-user-in-state
// What happens in the cache if something here is null?

// Note: This cast is mostly safe, but is missing info populated in AudiusBackend.getAccount()
// Okay for now as that info isn't generally available on non-account users and isn't used in manager mode.
return libs.Account?.getWeb3User() as UserMetadata | null
},
options: {
type: 'query',
schemaKey: 'currentWeb3User'
}
},
resetPassword: {
async fetch(args: ResetPasswordArgs, context) {
const { email, password } = args
Expand All @@ -29,10 +51,45 @@ const accountApi = createApi({
options: {
type: 'mutation'
}
},
getManagedAccounts: {
async fetch({ userId }: { userId: ID }, { audiusSdk }) {
const sdk = await audiusSdk()
const managedUsers = await sdk.full.users.getManagedUsers({
id: Id.parse(userId)
})

const { data = [] } = managedUsers
return managedUserListFromSDK(data)
},
options: {
type: 'query',
schemaKey: 'managedUsers'
}
},
getManagers: {
async fetch({ userId }: { userId: ID }, { audiusSdk }) {
const sdk = await audiusSdk()
const managedUsers = await sdk.full.users.getManagers({
id: Id.parse(userId)
})

const { data = [] } = managedUsers
return userManagerListFromSDK(data)
},
options: {
type: 'query',
schemaKey: 'userManagers'
}
}
}
})

export const { useGetCurrentUserId, useResetPassword } = accountApi.hooks
export const {
useGetCurrentUserId,
useGetCurrentWeb3User,
useResetPassword,
useGetManagedAccounts
} = accountApi.hooks

export const accountApiReducer = accountApi.reducer
11 changes: 11 additions & 0 deletions packages/common/src/audius-query/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,19 @@ export const collectionSchema = new schema.Entity(
{ idAttribute: 'playlist_id' }
)

export const managedUserSchema = new schema.Object({
user: userSchema
})

export const userManagerSchema = new schema.Object({
manager: userSchema
})

export const apiResponseSchema = new schema.Object({
currentWeb3User: userSchema,
managedUsers: new schema.Array(managedUserSchema),
user: userSchema,
userManagers: new schema.Array(userManagerSchema),
track: trackSchema,
collection: collectionSchema,
users: new schema.Array(userSchema),
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './useAccountSwitcher'
export * from './useBooleanOnce'
export * from './useFeatureFlag'
export * from './useRemoteVar'
Expand Down
24 changes: 24 additions & 0 deletions packages/common/src/hooks/useAccountSwitcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useCallback } from 'react'
import { useAppContext } from '~/context'
import { UserMetadata } from '~/models/User'

// Matches corresponding key in libs (DISCOVERY_PROVIDER_USER_WALLET_OVERRIDE)
const USER_WALLET_OVERRIDE_KEY = '@audius/user-wallet-override'

export const useAccountSwitcher = () => {
const { localStorage } = useAppContext()
const switchAccount = useCallback(async (user: UserMetadata) => {
if (!user.wallet) {
console.error('User has no wallet address')
return
}

await localStorage.setItem(USER_WALLET_OVERRIDE_KEY, user.wallet)
await localStorage.clearAudiusAccount()
await localStorage.clearAudiusAccountUser()

window.location.reload()
}, [])

return { switchAccount }
}
23 changes: 23 additions & 0 deletions packages/common/src/models/Grant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { full } from '@audius/sdk'
import { Nullable, decodeHashId } from '~/utils'
import { ID } from './Identifiers'

export type Grant = {
grantee_address: string
user_id: Nullable<ID>
is_revoked: boolean
is_approved: boolean
created_at: string
updated_at: string
}

export const grantFromSDK = (input: full.Grant): Grant => {
return {
grantee_address: input.granteeAddress,
user_id: decodeHashId(input.userId) ?? null,
is_revoked: input.isRevoked,
is_approved: input.isApproved,
created_at: input.createdAt,
updated_at: input.updatedAt
}
}
43 changes: 43 additions & 0 deletions packages/common/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Nullable, removeNullable } from '~/utils/typeUtils'

import { Timestamped } from './Timestamped'
import { UserEvent } from './UserEvent'
import { Grant, grantFromSDK } from './Grant'

export type UserMetadata = {
album_count: number
Expand Down Expand Up @@ -85,6 +86,16 @@ export type UserMetadata = {
events?: UserEvent
} & Timestamped

export type ManagedUserMetadata = {
grant: Grant
user: UserMetadata
}

export type UserManagerMetadata = {
grant: Grant
manager: UserMetadata
}

export type ComputedUserProperties = {
_profile_picture_sizes: ProfilePictureSizes
_cover_photo_sizes: CoverPhotoSizes
Expand Down Expand Up @@ -164,3 +175,35 @@ export const userMetadataFromSDK = (

export const userMetadataListFromSDK = (input?: full.UserFull[]) =>
input ? input.map((d) => userMetadataFromSDK(d)).filter(removeNullable) : []

export const managedUserFromSDK = (
input: full.ManagedUser
): ManagedUserMetadata | undefined => {
const user = userMetadataFromSDK(input.user)
if (!user) {
return undefined
}
return {
user,
grant: grantFromSDK(input.grant)
}
}

export const managedUserListFromSDK = (input?: full.ManagedUser[]) =>
input ? input.map((d) => managedUserFromSDK(d)).filter(removeNullable) : []

export const userManagerFromSDK = (
input: full.UserManager
): UserManagerMetadata | undefined => {
const manager = userMetadataFromSDK(input.manager)
if (!manager) {
return undefined
}
return {
manager,
grant: grantFromSDK(input.grant)
}
}

export const userManagerListFromSDK = (input?: full.UserManager[]) =>
input ? input.map((d) => userManagerFromSDK(d)).filter(removeNullable) : []
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ export const audiusBackend = ({
getRemoteVar(IntKeys.DISCOVERY_NODE_MAX_BLOCK_DIFF) ?? undefined,

discoveryNodeSelector,
enableUserIdOverride: isManagerModeEnabled
enableUserWalletOverride: isManagerModeEnabled
},
identityServiceConfig:
AudiusLibs.configIdentityService(identityServiceUrl),
Expand Down
10 changes: 10 additions & 0 deletions packages/libs/src/api/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class Account extends Base {
this.ServiceProvider = serviceProvider

this.getCurrentUser = this.getCurrentUser.bind(this)
this.getWeb3User = this.getWeb3User.bind(this)
this.login = this.login.bind(this)
this.logout = this.logout.bind(this)
this.generateRecoveryLink = this.generateRecoveryLink.bind(this)
Expand Down Expand Up @@ -54,6 +55,15 @@ export class Account extends Base {
return this.userStateManager.getCurrentUser()
}

/**
* Fetches the user metadata for user belonging to the current web3 wallet.
* May be different if acting as a managed account.
* @return {Object} user metadata
*/
getWeb3User() {
return this.userStateManager.getWeb3User()
}

/**
* Logs a user into Audius
*/
Expand Down
64 changes: 38 additions & 26 deletions packages/libs/src/services/discoveryProvider/DiscoveryProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import {
DiscoveryProviderSelection,
DiscoveryProviderSelectionConfig
} from './DiscoveryProviderSelection'
import { DEFAULT_UNHEALTHY_BLOCK_DIFF, REQUEST_TIMEOUT_MS } from './constants'
import {
DEFAULT_UNHEALTHY_BLOCK_DIFF,
DISCOVERY_PROVIDER_USER_WALLET_OVERRIDE,
REQUEST_TIMEOUT_MS
} from './constants'
import * as Requests from './requests'

const MAX_MAKE_REQUEST_RETRY_COUNT = 5
Expand Down Expand Up @@ -65,7 +69,7 @@ export type DiscoveryProviderConfig = {
unhealthySlotDiffPlays?: number
unhealthyBlockDiff?: number
discoveryNodeSelector?: DiscoveryNodeSelector
enableUserIdOverride?: boolean
enableUserWalletOverride?: boolean
} & Pick<
DiscoveryProviderSelectionConfig,
'selectionCallback' | 'monitoringCallbacks' | 'localStorage'
Expand Down Expand Up @@ -97,10 +101,10 @@ type DiscoveryNodeChallenge = {
disbursed_amount: number
}

const getUserIdOverride = async (localStorage?: LocalStorage) => {
const getUserWalletOverride = async (localStorage?: LocalStorage) => {
try {
const userIdOverride = await localStorage?.getItem(
'@audius/user-id-override'
DISCOVERY_PROVIDER_USER_WALLET_OVERRIDE
)
return userIdOverride == null ? undefined : userIdOverride
} catch {
Expand Down Expand Up @@ -152,7 +156,7 @@ export class DiscoveryProvider {
| DiscoveryProviderSelection['monitoringCallbacks']
| undefined

enableUserIdOverride = false
enableUserWalletOverride = false

discoveryProviderEndpoint: string | undefined
isInitialized = false
Expand All @@ -176,7 +180,7 @@ export class DiscoveryProvider {
unhealthySlotDiffPlays,
unhealthyBlockDiff,
discoveryNodeSelector,
enableUserIdOverride = false
enableUserWalletOverride = false
}: DiscoveryProviderConfig) {
this.whitelist = whitelist
this.blacklist = blacklist
Expand All @@ -185,7 +189,7 @@ export class DiscoveryProvider {
this.web3Manager = web3Manager
this.selectionCallback = selectionCallback
this.localStorage = localStorage
this.enableUserIdOverride = enableUserIdOverride
this.enableUserWalletOverride = enableUserWalletOverride

this.unhealthyBlockDiff = unhealthyBlockDiff ?? DEFAULT_UNHEALTHY_BLOCK_DIFF
this.serviceSelector = new DiscoveryProviderSelection(
Expand Down Expand Up @@ -243,12 +247,33 @@ export class DiscoveryProvider {
this.web3Manager &&
this.web3Manager.web3
) {
// Set current user if it exists
const userAccount = await this.getUserAccount(
const walletOverride = this.enableUserWalletOverride
? await getUserWalletOverride(this.localStorage)
: undefined

const web3AccountPromise = this.getUserAccount(
this.web3Manager.getWalletAddress()
)
if (userAccount) {
await this.userStateManager.setCurrentUser(userAccount)

if (walletOverride) {
const overrideAccountPromise = this.getUserAccount(walletOverride)
const [web3User, currentUser] = await Promise.all([
web3AccountPromise,
overrideAccountPromise
])

if (web3User) {
this.userStateManager.setWeb3User(web3User)
}
if (currentUser) {
await this.userStateManager.setCurrentUser(currentUser)
}
} else {
const currentUser = await web3AccountPromise
if (currentUser) {
this.userStateManager.setWeb3User(currentUser)
await this.userStateManager.setCurrentUser(currentUser)
}
}
}
}
Expand Down Expand Up @@ -839,21 +864,8 @@ export class DiscoveryProvider {
* Return user collections (saved & uploaded) along w/ users for those collections
*/
async getUserAccount(wallet: string) {
const userIdOverride = this.enableUserIdOverride
? await getUserIdOverride(this.localStorage)
: undefined
// If override is used, fetch that account instead
if (userIdOverride) {
const req = Requests.getUsers(1, 0, [parseInt(userIdOverride)])
const res = await this._makeRequest<CurrentUser[]>(req)
if (res && res.length > 0 && res[0]) {
return { ...res[0], playlists: [] } as CurrentUser
}
return null
} else {
const req = Requests.getUserAccount(wallet)
return await this._makeRequest<CurrentUser>(req)
}
const req = Requests.getUserAccount(wallet)
return await this._makeRequest<CurrentUser>(req)
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/libs/src/services/discoveryProvider/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const DISCOVERY_PROVIDER_TIMESTAMP =
'@audius/libs:discovery-node-timestamp'
export const DISCOVERY_PROVIDER_USER_WALLET_OVERRIDE =
'@audius/user-wallet-override'
export const DISCOVERY_SERVICE_NAME = 'discovery-node'
export const DEFAULT_UNHEALTHY_BLOCK_DIFF = 15
export const REGRESSED_MODE_TIMEOUT = 2 * 60 * 1000 // two minutes
Expand Down
Loading