Skip to content

Commit 5bbd879

Browse files
Switch protocol dashboard to OAuth 2.0 write scope + image mirrors (#14052)
## Summary - **OAuth 2.0 migration**: Replace deprecated `write_once` scope with standard `write` scope for the Connect Audius Profile flow. Rewrites `useConnectAudiusProfile` hook to construct OAuth URL manually with PKCE, sign wallet signatures with ethers (instead of `audiusLibs.web3Manager`), and exchange auth codes for tokens after the popup completes. Removes all references to old SDK OAuth APIs (`oauth.init`, `getCsrfToken`, `activePopupWindow`, `OAUTH_URL`). - **Web app OAuth page**: Extends `write` scope to support `tx=connect_dashboard_wallet` / `tx=disconnect_dashboard_wallet` params, reusing existing `handleAuthorizeConnectDashboardWallet` / `handleAuthorizeDisconnectDashboardWallet` handlers. - **Image mirror support**: Adds `MirrorImage` component with 3-second per-URL timeout fallback for profile pictures and trending artwork. Replaces raw `<img>` tags and `backgroundImage` inline styles that would stall on unresponsive content nodes. ## Test plan - [x] Connect Audius Profile: click button → popup opens → authenticate → profile picture + name appear - [x] Disconnect Audius Profile: click Unlink → popup opens → authenticate → profile removed - [x] Profile pictures load with mirror fallback (simulate slow node by throttling network) - [ ] Trending tracks/playlists/albums artwork loads correctly with mirror fallback - [ ] Wallet connection via Web3Modal still works (no regression) - [ ] `npm run verify` passes in protocol-dashboard 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f7e0a6 commit 5bbd879

13 files changed

Lines changed: 464 additions & 182 deletions

File tree

packages/protocol-dashboard/src/components/AppBar/AppBar.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,13 @@ const UserAccountSnippet = ({ wallet }: UserAccountSnippetProps) => {
195195
)
196196
}
197197

198-
const ConnectAudiusProfileButton = ({ wallet }: { wallet: string }) => {
198+
const ConnectAudiusProfileButton = ({
199+
wallet,
200+
walletProvider
201+
}: {
202+
wallet: string
203+
walletProvider?: any
204+
}) => {
199205
const { isOpen, onClick, onClose } = useModalControls()
200206
return (
201207
<>
@@ -210,6 +216,7 @@ const ConnectAudiusProfileButton = ({ wallet }: { wallet: string }) => {
210216
<ConnectAudiusProfileModal
211217
action='connect'
212218
wallet={wallet}
219+
walletProvider={walletProvider}
213220
isOpen={isOpen}
214221
onClose={onClose}
215222
/>
@@ -298,7 +305,7 @@ const AppBar: React.FC<AppBarProps> = () => {
298305
!wallet ||
299306
!isLoggedIn ||
300307
audiusProfileDataStatus === 'pending' ? null : (
301-
<ConnectAudiusProfileButton wallet={wallet} />
308+
<ConnectAudiusProfileButton wallet={wallet} walletProvider={walletProvider} />
302309
)}
303310
<div
304311
className={clsx({

packages/protocol-dashboard/src/components/ConnectAudiusProfileCard/ConnectAudiusProfileCard.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Box, Flex, Text } from '@audius/harmony'
2+
import { useWeb3ModalProvider } from '@web3modal/ethers/react'
23

34
import Button, { ButtonType } from 'components/Button'
45
import { Card } from 'components/Card/Card'
@@ -18,9 +19,11 @@ const messages = {
1819

1920
type ConnectAudiusProtileBtnProps = {
2021
wallet: string
22+
walletProvider?: any
2123
}
2224
const ConnectAudiusProfileButton = ({
23-
wallet
25+
wallet,
26+
walletProvider
2427
}: ConnectAudiusProtileBtnProps) => {
2528
const { isOpen, onClick, onClose } = useModalControls()
2629
return (
@@ -32,6 +35,7 @@ const ConnectAudiusProfileButton = ({
3235
/>
3336
<ConnectAudiusProfileModal
3437
wallet={wallet}
38+
walletProvider={walletProvider}
3539
isOpen={isOpen}
3640
onClose={onClose}
3741
action='connect'
@@ -42,6 +46,7 @@ const ConnectAudiusProfileButton = ({
4246

4347
export const ConnectAudiusProfileCard = () => {
4448
const { user: accountUser } = useAccountUser()
49+
const { walletProvider } = useWeb3ModalProvider()
4550
const { data: audiusProfileData, status: audiusProfileDataStatus } =
4651
useDashboardWalletUser(accountUser?.wallet)
4752

@@ -66,7 +71,10 @@ export const ConnectAudiusProfileCard = () => {
6671
</Text>
6772
</Flex>
6873
<Box>
69-
<ConnectAudiusProfileButton wallet={accountUser.wallet} />
74+
<ConnectAudiusProfileButton
75+
wallet={accountUser.wallet}
76+
walletProvider={walletProvider}
77+
/>
7078
</Box>
7179
</Card>
7280
)

packages/protocol-dashboard/src/components/ConnectAudiusProfileModal/ConnectAudiusProfileModal.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,20 @@ type ConnectAudiusProfileModalProps = {
2727
isOpen: boolean
2828
onClose: () => void
2929
wallet: string
30+
walletProvider?: any
3031
action: 'disconnect' | 'connect'
3132
}
3233

3334
export const ConnectAudiusProfileModal = ({
3435
isOpen,
3536
onClose,
3637
wallet,
38+
walletProvider,
3739
action
3840
}: ConnectAudiusProfileModalProps) => {
3941
const { connect, disconnect, isWaiting } = useConnectAudiusProfile({
4042
wallet,
43+
walletProvider,
4144
onSuccess: onClose
4245
})
4346
const isConnect = action === 'connect'
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useState, useEffect, useRef, ReactNode } from 'react'
2+
3+
const TIMEOUT_MS = 3000
4+
5+
type MirrorImageProps = {
6+
urls: string[]
7+
alt: string
8+
className?: string
9+
fallback?: ReactNode
10+
onLoad?: () => void
11+
}
12+
13+
const MirrorImage = ({
14+
urls = [],
15+
alt = '',
16+
className,
17+
fallback = null,
18+
onLoad
19+
}: MirrorImageProps) => {
20+
const [idx, setIdx] = useState(0)
21+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
22+
23+
const firstUrl = urls[0] ?? null
24+
useEffect(() => {
25+
setIdx(0)
26+
}, [firstUrl])
27+
28+
useEffect(() => {
29+
if (!urls.length || idx >= urls.length) return
30+
timerRef.current = setTimeout(() => setIdx((i) => i + 1), TIMEOUT_MS)
31+
return () => {
32+
if (timerRef.current) clearTimeout(timerRef.current)
33+
}
34+
}, [idx, urls.length])
35+
36+
const handleLoad = () => {
37+
if (timerRef.current) clearTimeout(timerRef.current)
38+
onLoad?.()
39+
}
40+
41+
const handleError = () => {
42+
if (timerRef.current) clearTimeout(timerRef.current)
43+
setIdx((i) => i + 1)
44+
}
45+
46+
if (!urls.length || idx >= urls.length) return <>{fallback}</>
47+
48+
return (
49+
<img
50+
key={urls[idx]}
51+
src={urls[idx]}
52+
alt={alt}
53+
className={className}
54+
onLoad={handleLoad}
55+
onError={handleError}
56+
/>
57+
)
58+
}
59+
60+
export default MirrorImage

packages/protocol-dashboard/src/components/UserInfo/AudiusProfileBadges.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { cloneElement, ReactElement } from 'react'
22

33
import { BadgeTier } from '@audius/common/models'
4-
import { badgeTiers } from '@audius/common/store'
54
import { Nullable } from '@audius/common/utils'
5+
6+
// Inlined from @audius/common/store to avoid circular dependency
7+
// (store/wallet/utils → api barrel → upload modules → store)
8+
const badgeTiers: { tier: BadgeTier; humanReadableAmount: number }[] = [
9+
{ tier: 'platinum', humanReadableAmount: 10000 },
10+
{ tier: 'gold', humanReadableAmount: 1000 },
11+
{ tier: 'silver', humanReadableAmount: 100 },
12+
{ tier: 'bronze', humanReadableAmount: 10 },
13+
{ tier: 'none', humanReadableAmount: 0 }
14+
]
615
import { User } from '@audius/sdk'
716
import cn from 'classnames'
817

0 commit comments

Comments
 (0)