{
const { isOpen, onClick, onClose } = useModalControls()
return (
@@ -32,6 +35,7 @@ const ConnectAudiusProfileButton = ({
/>
{
const { user: accountUser } = useAccountUser()
+ const { walletProvider } = useWeb3ModalProvider()
const { data: audiusProfileData, status: audiusProfileDataStatus } =
useDashboardWalletUser(accountUser?.wallet)
@@ -66,7 +71,10 @@ export const ConnectAudiusProfileCard = () => {
-
+
)
diff --git a/packages/protocol-dashboard/src/components/ConnectAudiusProfileModal/ConnectAudiusProfileModal.tsx b/packages/protocol-dashboard/src/components/ConnectAudiusProfileModal/ConnectAudiusProfileModal.tsx
index a69194e9a56..8464439da86 100644
--- a/packages/protocol-dashboard/src/components/ConnectAudiusProfileModal/ConnectAudiusProfileModal.tsx
+++ b/packages/protocol-dashboard/src/components/ConnectAudiusProfileModal/ConnectAudiusProfileModal.tsx
@@ -27,6 +27,7 @@ type ConnectAudiusProfileModalProps = {
isOpen: boolean
onClose: () => void
wallet: string
+ walletProvider?: any
action: 'disconnect' | 'connect'
}
@@ -34,10 +35,12 @@ export const ConnectAudiusProfileModal = ({
isOpen,
onClose,
wallet,
+ walletProvider,
action
}: ConnectAudiusProfileModalProps) => {
const { connect, disconnect, isWaiting } = useConnectAudiusProfile({
wallet,
+ walletProvider,
onSuccess: onClose
})
const isConnect = action === 'connect'
diff --git a/packages/protocol-dashboard/src/components/MirrorImage/MirrorImage.tsx b/packages/protocol-dashboard/src/components/MirrorImage/MirrorImage.tsx
new file mode 100644
index 00000000000..4ee3cb9ebba
--- /dev/null
+++ b/packages/protocol-dashboard/src/components/MirrorImage/MirrorImage.tsx
@@ -0,0 +1,60 @@
+import { useState, useEffect, useRef, ReactNode } from 'react'
+
+const TIMEOUT_MS = 3000
+
+type MirrorImageProps = {
+ urls: string[]
+ alt: string
+ className?: string
+ fallback?: ReactNode
+ onLoad?: () => void
+}
+
+const MirrorImage = ({
+ urls = [],
+ alt = '',
+ className,
+ fallback = null,
+ onLoad
+}: MirrorImageProps) => {
+ const [idx, setIdx] = useState(0)
+ const timerRef = useRef | null>(null)
+
+ const firstUrl = urls[0] ?? null
+ useEffect(() => {
+ setIdx(0)
+ }, [firstUrl])
+
+ useEffect(() => {
+ if (!urls.length || idx >= urls.length) return
+ timerRef.current = setTimeout(() => setIdx((i) => i + 1), TIMEOUT_MS)
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current)
+ }
+ }, [idx, urls.length])
+
+ const handleLoad = () => {
+ if (timerRef.current) clearTimeout(timerRef.current)
+ onLoad?.()
+ }
+
+ const handleError = () => {
+ if (timerRef.current) clearTimeout(timerRef.current)
+ setIdx((i) => i + 1)
+ }
+
+ if (!urls.length || idx >= urls.length) return <>{fallback}>
+
+ return (
+
+ )
+}
+
+export default MirrorImage
diff --git a/packages/protocol-dashboard/src/components/UserInfo/AudiusProfileBadges.tsx b/packages/protocol-dashboard/src/components/UserInfo/AudiusProfileBadges.tsx
index 4a68d0bc2c5..d7da1c62ab2 100644
--- a/packages/protocol-dashboard/src/components/UserInfo/AudiusProfileBadges.tsx
+++ b/packages/protocol-dashboard/src/components/UserInfo/AudiusProfileBadges.tsx
@@ -1,8 +1,17 @@
import { cloneElement, ReactElement } from 'react'
import { BadgeTier } from '@audius/common/models'
-import { badgeTiers } from '@audius/common/store'
import { Nullable } from '@audius/common/utils'
+
+// Inlined from @audius/common/store to avoid circular dependency
+// (store/wallet/utils → api barrel → upload modules → store)
+const badgeTiers: { tier: BadgeTier; humanReadableAmount: number }[] = [
+ { tier: 'platinum', humanReadableAmount: 10000 },
+ { tier: 'gold', humanReadableAmount: 1000 },
+ { tier: 'silver', humanReadableAmount: 100 },
+ { tier: 'bronze', humanReadableAmount: 10 },
+ { tier: 'none', humanReadableAmount: 0 }
+]
import { User } from '@audius/sdk'
import cn from 'classnames'
diff --git a/packages/protocol-dashboard/src/hooks/useConnectAudiusProfile.ts b/packages/protocol-dashboard/src/hooks/useConnectAudiusProfile.ts
index de57b75c1bc..85661108a7a 100644
--- a/packages/protocol-dashboard/src/hooks/useConnectAudiusProfile.ts
+++ b/packages/protocol-dashboard/src/hooks/useConnectAudiusProfile.ts
@@ -1,139 +1,294 @@
-import { useState } from 'react'
+import { useState, useCallback } from 'react'
-import { DecodedUserToken, OAUTH_URL } from '@audius/sdk'
import { useQueryClient } from '@tanstack/react-query'
import { useDispatch } from 'react-redux'
import { getDashboardWalletUserQueryKey } from 'hooks/useDashboardWalletUsers'
-import { audiusSdk as sdk } from 'services/Audius/sdk'
+import { audiusSdk, apiEndpoint } from 'services/Audius/sdk'
import { disableAudiusProfileRefetch } from 'store/account/slice'
-const env = import.meta.env.VITE_ENVIRONMENT
-
-let resolveUserHandle = null
-let receiveUserHandlePromise = null
-
-const receiveUserId = async (event: MessageEvent) => {
- const oauthOrigin = new URL(OAUTH_URL[env]).origin
- if (
- event.origin !== oauthOrigin ||
- event.source !== sdk.oauth.activePopupWindow ||
- !event.data.state
- ) {
- return
- }
- if (sdk.oauth.getCsrfToken() !== event.data.state) {
- console.error('State mismatch.')
- return
- }
- if (event.data.userHandle != null) {
- resolveUserHandle(event.data.userHandle)
- }
+const API_KEY = '2cc593fc814461263d282a84286fd4f72c79562e'
+
+const AUDIUS_URL = import.meta.env.VITE_AUDIUS_URL || 'https://audius.co'
+const OAUTH_BASE_URL = `${AUDIUS_URL}/oauth/auth`
+
+// --- PKCE helpers ---
+
+function base64url(bytes: Uint8Array): string {
+ return btoa(String.fromCharCode(...bytes))
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, '')
+}
+
+function generateCodeVerifier(): string {
+ const arr = new Uint8Array(32)
+ globalThis.crypto.getRandomValues(arr)
+ return base64url(arr)
+}
+
+async function generateCodeChallenge(verifier: string): Promise {
+ const data = new TextEncoder().encode(verifier)
+ const hash = await globalThis.crypto.subtle.digest('SHA-256', data)
+ return base64url(new Uint8Array(hash))
+}
+
+function generateState(): string {
+ const arr = new Uint8Array(16)
+ globalThis.crypto.getRandomValues(arr)
+ return base64url(arr)
+}
+
+type PopupMessage = {
+ state?: string
+ userHandle?: string
+ userId?: string
+ code?: string
}
export const useConnectAudiusProfile = ({
wallet,
+ walletProvider,
onSuccess
}: {
wallet: string
+ walletProvider?: any
onSuccess: () => void
}) => {
const queryClient = useQueryClient()
const dispatch = useDispatch()
const [isWaiting, setIsWaiting] = useState(false)
- const handleConnectSuccess = async (profile: DecodedUserToken) => {
- window.removeEventListener('message', receiveUserId)
- // Optimistically set user
- await queryClient.cancelQueries({
- queryKey: getDashboardWalletUserQueryKey(wallet)
- })
- dispatch(disableAudiusProfileRefetch())
+
+ const connect = useCallback(async () => {
+ setIsWaiting(true)
try {
- const audiusUser = await sdk.users.getUser({ id: profile.userId })
- if (audiusUser?.data) {
- queryClient.setQueryData(getDashboardWalletUserQueryKey(wallet), {
- wallet,
- user: audiusUser.data
+ const state = generateState()
+ const codeVerifier = generateCodeVerifier()
+ const codeChallenge = await generateCodeChallenge(codeVerifier)
+ const origin = window.location.origin
+ const oauthOrigin = new URL(OAUTH_BASE_URL).origin
+
+ const params = new URLSearchParams({
+ scope: 'write',
+ api_key: API_KEY,
+ state,
+ redirect_uri: 'postMessage',
+ origin,
+ response_type: 'code',
+ code_challenge: codeChallenge,
+ code_challenge_method: 'S256',
+ display: 'popup',
+ tx: 'connect_dashboard_wallet',
+ wallet
+ })
+
+ const popup = window.open(
+ `${OAUTH_BASE_URL}?${params.toString()}`,
+ '',
+ 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=375, height=785, top=100, left=100'
+ )
+ if (!popup) {
+ throw new Error('The login popup was blocked.')
+ }
+
+ // Listen for ALL messages from popup (userHandle, then code)
+ const { userHandle } = await new Promise<{ userHandle: string }>(
+ (resolve, reject) => {
+ const closeCheck = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(closeCheck)
+ reject(new Error('The login popup was closed.'))
+ }
+ }, 500)
+
+ const handler = (event: MessageEvent) => {
+ if (
+ event.origin !== oauthOrigin ||
+ event.source !== popup ||
+ event.data?.state !== state
+ ) {
+ return
+ }
+ if (event.data.userHandle != null) {
+ window.removeEventListener('message', handler)
+ clearInterval(closeCheck)
+ resolve({ userHandle: event.data.userHandle })
+ }
+ }
+ window.addEventListener('message', handler, false)
+ }
+ )
+
+ // Sign with connected Ethereum wallet
+ if (!walletProvider) {
+ throw new Error('Wallet provider not available')
+ }
+ const timestamp = Math.round(new Date().getTime() / 1000)
+ const message = `Connecting Audius user @${userHandle} at ${timestamp}`
+ const hexMessage =
+ '0x' +
+ Array.from(new TextEncoder().encode(message))
+ .map((b) => b.toString(16).padStart(2, '0'))
+ .join('')
+ const signature = await walletProvider.request({
+ method: 'personal_sign',
+ params: [hexMessage, wallet]
+ })
+
+ // Send wallet signature to popup for EntityManager tx
+ popup.postMessage(
+ { state, walletSignature: { message, signature } },
+ oauthOrigin
+ )
+
+ // Wait for auth code from popup (after EntityManager tx + PKCE exchange)
+ const { code } = await new Promise<{ code: string }>(
+ (resolve, reject) => {
+ const closeCheck = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(closeCheck)
+ reject(new Error('The login popup was closed.'))
+ }
+ }, 500)
+
+ const handler = (event: MessageEvent) => {
+ if (
+ event.origin !== oauthOrigin ||
+ event.source !== popup ||
+ event.data?.state !== state
+ ) {
+ return
+ }
+ if (event.data.code != null) {
+ window.removeEventListener('message', handler)
+ clearInterval(closeCheck)
+ resolve({ code: event.data.code })
+ }
+ }
+ window.addEventListener('message', handler, false)
+ }
+ )
+
+ // Exchange code for tokens
+ const tokenRes = await fetch(`${apiEndpoint}/v1/oauth/token`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ grant_type: 'authorization_code',
+ code,
+ code_verifier: codeVerifier,
+ client_id: API_KEY,
+ redirect_uri: 'postMessage'
})
+ })
+
+ if (!tokenRes.ok) {
+ throw new Error(`Token exchange failed: ${tokenRes.status}`)
+ }
+
+ const tokens = await tokenRes.json()
+
+ // Fetch user profile and update cache
+ const meRes = await fetch(`${apiEndpoint}/v1/me`, {
+ headers: { Authorization: `Bearer ${tokens.access_token}` }
+ })
+ if (meRes.ok) {
+ const { data: audiusUser } = await meRes.json()
+ if (audiusUser) {
+ await queryClient.cancelQueries({
+ queryKey: getDashboardWalletUserQueryKey(wallet)
+ })
+ dispatch(disableAudiusProfileRefetch())
+ queryClient.setQueryData(getDashboardWalletUserQueryKey(wallet), {
+ wallet,
+ user: audiusUser
+ })
+ }
}
+
+ popup.close()
setIsWaiting(false)
onSuccess()
- } catch {
- console.error("Couldn't fetch Audius profile data.")
+ } catch (e) {
+ console.error('Connect Audius profile failed:', e)
setIsWaiting(false)
}
- }
+ }, [wallet, walletProvider, queryClient, dispatch, onSuccess])
- const connect = async () => {
- setIsWaiting(true)
- sdk.oauth.init({
- env,
- successCallback: handleConnectSuccess,
- errorCallback: (errorMessage: string) => {
- window.removeEventListener('message', receiveUserId)
- console.error(errorMessage)
- setIsWaiting(false)
- }
- })
- window.removeEventListener('message', receiveUserId)
- receiveUserHandlePromise = new Promise((resolve) => {
- resolveUserHandle = resolve
- })
- window.addEventListener('message', receiveUserId, false)
- sdk.oauth.login({
- scope: 'write_once',
- params: {
- tx: 'connect_dashboard_wallet',
- wallet
- }
- })
-
- // Leg 1: Receive Audius user id from OAuth popup
- const userHandle = await receiveUserHandlePromise
- // Sign wallet signature from EM transaction
- const message = `Connecting Audius user @${userHandle} at ${Math.round(
- new Date().getTime() / 1000
- )}`
- const signature = await window.audiusLibs.web3Manager.sign(message)
-
- const walletSignature = { message, signature }
- // Leg 2: Send wallet signature to OAuth popup
- sdk.oauth.activePopupWindow.postMessage(
- { state: sdk.oauth.getCsrfToken(), walletSignature },
- new URL(OAUTH_URL[env]).origin
- )
- }
-
- const handleDisconnectSuccess = async () => {
- // Optimistically clear the connected user
- await queryClient.cancelQueries({
- queryKey: getDashboardWalletUserQueryKey(wallet)
- })
- dispatch(disableAudiusProfileRefetch())
- queryClient.setQueryData(getDashboardWalletUserQueryKey(wallet), null)
- setIsWaiting(false)
- onSuccess()
- }
-
- const disconnect = async () => {
+ const disconnect = useCallback(async () => {
setIsWaiting(true)
- sdk.oauth.init({
- env,
- successCallback: handleDisconnectSuccess,
- errorCallback: (errorMessage: string) => {
- console.error(errorMessage)
- setIsWaiting(false)
- }
- })
- sdk.oauth.login({
- scope: 'write_once',
- params: {
+
+ try {
+ const state = generateState()
+ const codeVerifier = generateCodeVerifier()
+ const codeChallenge = await generateCodeChallenge(codeVerifier)
+ const origin = window.location.origin
+ const oauthOrigin = new URL(OAUTH_BASE_URL).origin
+
+ const params = new URLSearchParams({
+ scope: 'write',
+ api_key: API_KEY,
+ state,
+ redirect_uri: 'postMessage',
+ origin,
+ response_type: 'code',
+ code_challenge: codeChallenge,
+ code_challenge_method: 'S256',
+ display: 'popup',
tx: 'disconnect_dashboard_wallet',
wallet
+ })
+
+ const popup = window.open(
+ `${AUDIUS_URL}/oauth/auth?${params.toString()}`,
+ '',
+ 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=375, height=785, top=100, left=100'
+ )
+ if (!popup) {
+ throw new Error('The login popup was blocked.')
}
- })
- }
+
+ // Wait for auth code (disconnect doesn't need wallet signature)
+ await new Promise((resolve, reject) => {
+ const closeCheck = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(closeCheck)
+ reject(new Error('The login popup was closed.'))
+ }
+ }, 500)
+
+ const handler = (event: MessageEvent) => {
+ if (
+ event.origin !== oauthOrigin ||
+ event.source !== popup ||
+ event.data?.state !== state
+ ) {
+ return
+ }
+ if (event.data.code != null) {
+ window.removeEventListener('message', handler)
+ clearInterval(closeCheck)
+ resolve()
+ }
+ }
+ window.addEventListener('message', handler, false)
+ })
+
+ // Clear the connected user
+ await queryClient.cancelQueries({
+ queryKey: getDashboardWalletUserQueryKey(wallet)
+ })
+ dispatch(disableAudiusProfileRefetch())
+ queryClient.setQueryData(getDashboardWalletUserQueryKey(wallet), null)
+ popup.close()
+ setIsWaiting(false)
+ onSuccess()
+ } catch (e) {
+ console.error('Disconnect Audius profile failed:', e)
+ setIsWaiting(false)
+ }
+ }, [wallet, queryClient, dispatch, onSuccess])
return { connect, disconnect, isWaiting }
}
diff --git a/packages/protocol-dashboard/src/utils/imageUrls.ts b/packages/protocol-dashboard/src/utils/imageUrls.ts
new file mode 100644
index 00000000000..1e219fc8f6a
--- /dev/null
+++ b/packages/protocol-dashboard/src/utils/imageUrls.ts
@@ -0,0 +1,23 @@
+/**
+ * Builds a full URL list from an Audius API artwork/profilePicture object.
+ * Primary URL first, then each mirror host with the same path.
+ */
+export function getImageUrls(
+ artworkObj: Record | null | undefined,
+ size = '_480x480'
+): string[] {
+ if (!artworkObj) return []
+ const primary: string | undefined = artworkObj[size] || artworkObj['_150x150']
+ if (!primary) return []
+ const mirrors: string[] = Array.isArray(artworkObj.mirrors)
+ ? artworkObj.mirrors
+ : []
+ if (!mirrors.length) return [primary]
+ let path: string
+ try {
+ path = new URL(primary).pathname
+ } catch {
+ return [primary]
+ }
+ return [primary, ...mirrors.map((root) => root.replace(/\/$/, '') + path)]
+}
diff --git a/packages/protocol-dashboard/vite.config.ts b/packages/protocol-dashboard/vite.config.ts
index 47ce64b4e85..ac7cb939450 100644
--- a/packages/protocol-dashboard/vite.config.ts
+++ b/packages/protocol-dashboard/vite.config.ts
@@ -33,7 +33,27 @@ export default defineConfig({
optimizeDeps: {
esbuildOptions: {
- plugins: [fixReactVirtualized]
+ plugins: [
+ fixReactVirtualized,
+ {
+ name: 'resolve-ethers-v6-for-web3modal',
+ setup(build) {
+ // @web3modal/ethers expects ethers v6 but root node_modules has v5.
+ // Redirect bare 'ethers' imports from @web3modal to the local v6.
+ build.onResolve({ filter: /^ethers$/ }, (args) => {
+ if (args.importer.includes('@web3modal')) {
+ return {
+ path: path.resolve(
+ __dirname,
+ 'node_modules/ethers/lib.esm/index.js'
+ )
+ }
+ }
+ return undefined
+ })
+ }
+ }
+ ]
}
},
diff --git a/packages/web/src/pages/oauth-login-page/OAuthLoginPage.tsx b/packages/web/src/pages/oauth-login-page/OAuthLoginPage.tsx
index 7e8f6297d5d..5b40465dbe4 100644
--- a/packages/web/src/pages/oauth-login-page/OAuthLoginPage.tsx
+++ b/packages/web/src/pages/oauth-login-page/OAuthLoginPage.tsx
@@ -43,7 +43,7 @@ import { ContentWrapper } from './components/ContentWrapper'
import { PermissionsSection } from './components/PermissionsSection'
import { useOAuthSetup } from './hooks'
import { messages } from './messages'
-import { WriteOnceTx } from './utils'
+import { DashboardWalletTx } from './utils'
const { signOut } = signOutActions
@@ -391,11 +391,11 @@ export const OAuthLoginPage = () => {
{userAlreadyWriteAuthorized ? null : (
)}
diff --git a/packages/web/src/pages/oauth-login-page/components/PermissionsSection.tsx b/packages/web/src/pages/oauth-login-page/components/PermissionsSection.tsx
index 3816e03073e..dfbdd67c7ab 100644
--- a/packages/web/src/pages/oauth-login-page/components/PermissionsSection.tsx
+++ b/packages/web/src/pages/oauth-login-page/components/PermissionsSection.tsx
@@ -13,7 +13,7 @@ import LoadingSpinner from 'components/loading-spinner/LoadingSpinner'
import styles from '../OAuthLoginPage.module.css'
import { messages } from '../messages'
-import { WriteOnceParams, WriteOnceTx } from '../utils'
+import { DashboardWalletParams, DashboardWalletTx } from '../utils'
type PermissionDetailProps = PropsWithChildren<{}>
const PermissionDetail = ({ children }: PermissionDetailProps) => {
@@ -26,12 +26,14 @@ const PermissionDetail = ({ children }: PermissionDetailProps) => {
)
}
-const getWriteOncePermissionTitle = (tx: WriteOnceTx | null) => {
+const getDashboardWalletPermissionTitle = (tx: DashboardWalletTx | null) => {
switch (tx) {
case 'connect_dashboard_wallet':
return messages.connectDashboardWalletAccess
case 'disconnect_dashboard_wallet':
return messages.disconnectDashboardWalletAccess
+ default:
+ return null
}
}
@@ -44,11 +46,11 @@ export const PermissionsSection = ({
tx
}: {
scope: string | string[] | null
- tx: WriteOnceTx | null
+ tx: DashboardWalletTx | null
isLoggedIn: boolean
isLoading: boolean
userEmail: string | null
- txParams?: WriteOnceParams
+ txParams?: DashboardWalletParams
}) => {
return (
@@ -60,7 +62,7 @@ export const PermissionsSection = ({
- {scope === 'write' || scope === 'write_once' ? (
+ {scope === 'write' ? (
) : (
@@ -68,18 +70,17 @@ export const PermissionsSection = ({
{scope === 'write'
- ? messages.writeAccountAccess
- : scope === 'write_once'
- ? getWriteOncePermissionTitle(tx)
- : messages.readOnlyAccountAccess}
+ ? (getDashboardWalletPermissionTitle(tx) ??
+ messages.writeAccountAccess)
+ : messages.readOnlyAccountAccess}
- {scope === 'write' ? (
+ {scope === 'write' && !tx ? (
{messages.writeAccessGrants}
) : null}
- {scope === 'write_once' ? (
+ {scope === 'write' && tx && txParams ? (
- {txParams?.wallet.slice(0, 6)}...{txParams?.wallet.slice(-4)}
+ {txParams.wallet.slice(0, 6)}...{txParams.wallet.slice(-4)}
) : null}
{scope === 'read' ? (
diff --git a/packages/web/src/pages/oauth-login-page/hooks.ts b/packages/web/src/pages/oauth-login-page/hooks.ts
index 984ef2ba039..9b2698f4664 100644
--- a/packages/web/src/pages/oauth-login-page/hooks.ts
+++ b/packages/web/src/pages/oauth-login-page/hooks.ts
@@ -31,9 +31,8 @@ import {
handleAuthorizeConnectDashboardWallet,
handleAuthorizeDisconnectDashboardWallet,
isValidApiKey,
- validateWriteOnceParams,
- WriteOnceParams,
- WriteOnceTx
+ validateDashboardWalletParams,
+ DashboardWalletParams
} from './utils'
// Collapse space-separated OAuth scopes (e.g. 'read write') to the highest privilege.
@@ -50,7 +49,6 @@ const collapseScopes = (
.flatMap((s) => (s != null ? s.split(/\s+/) : []))
.filter((t) => t.length > 0)
if (tokens.includes('write')) return 'write'
- if (tokens.includes('write_once')) return 'write_once'
if (tokens.includes('read')) return 'read'
return typeof raw === 'string' ? raw : null
}
@@ -115,17 +113,13 @@ const useParsedQueryParams = () => {
const { error, txParams } = useMemo(() => {
let error: string | null = null
- let txParams: WriteOnceParams | null = null // Only used for scope=write_once
+ let txParams: DashboardWalletParams | null = null
if (isRedirectValid === false) {
error = messages.redirectURIInvalidError
} else if (parsedRedirectUri === 'postmessage' && !parsedOrigin) {
// Only applicable if redirect URI set to `postMessage`
error = messages.originInvalidError
- } else if (
- scope !== 'read' &&
- scope !== 'write' &&
- scope !== 'write_once'
- ) {
+ } else if (scope !== 'read' && scope !== 'write') {
error = messages.scopeError
} else if (
responseMode &&
@@ -153,18 +147,18 @@ const useParsedQueryParams = () => {
error = messages.invalidCodeChallengeMethodError
}
}
- } else if (scope === 'write_once') {
- // Write-once scope-specific validations:
- const { error: writeOnceParamsError, txParams: txParamsRes } =
- validateWriteOnceParams({
- tx,
- params: rest,
- willUsePostMessage: parsedRedirectUri === 'postmessage'
- })
- txParams = txParamsRes
-
- if (writeOnceParamsError) {
- error = writeOnceParamsError
+ // Optional dashboard wallet tx params
+ if (!error && tx) {
+ const { error: txParamsError, txParams: txParamsRes } =
+ validateDashboardWalletParams({
+ tx,
+ params: rest,
+ willUsePostMessage: parsedRedirectUri === 'postmessage'
+ })
+ txParams = txParamsRes
+ if (txParamsError) {
+ error = txParamsError
+ }
}
}
return { txParams, error }
@@ -243,7 +237,7 @@ export const useOAuthSetup = ({
const [queryParamsError, setQueryParamsError] = useState(
initError
)
- /** The fetched developer app name if write OAuth (we use `queryParamAppName` if read or writeOnce OAuth and no API key is given) */
+ /** The fetched developer app name if write OAuth (we use `queryParamAppName` if read OAuth and no API key is given) */
const [registeredDeveloperAppName, setRegisteredDeveloperAppName] =
useState()
const appName = registeredDeveloperAppName ?? queryParamAppName
@@ -533,29 +527,31 @@ export const useOAuthSetup = ({
})
return
}
- } else if (scope === 'write_once') {
- // Note: Tx = 'connect_dashboard_wallet' since that's the only option available right now for write_once scope
- if ((tx as WriteOnceTx) === 'connect_dashboard_wallet') {
- const success = await handleAuthorizeConnectDashboardWallet({
- state,
- originUrl: parsedOrigin,
- onError,
- onWaitForWalletSignature: onPendingTransactionApproval,
- onReceivedWalletSignature: onReceiveTransactionApproval,
- account,
- txParams: txParams!
- })
- if (!success) {
- return
- }
- } else if ((tx as WriteOnceTx) === 'disconnect_dashboard_wallet') {
- const success = await handleAuthorizeDisconnectDashboardWallet({
- account,
- txParams: txParams!,
- onError
- })
- if (!success) {
- return
+
+ // Handle dashboard wallet tx if present
+ if (tx && txParams) {
+ if (tx === 'connect_dashboard_wallet') {
+ const success = await handleAuthorizeConnectDashboardWallet({
+ state,
+ originUrl: parsedOrigin,
+ onError,
+ onWaitForWalletSignature: onPendingTransactionApproval,
+ onReceivedWalletSignature: onReceiveTransactionApproval,
+ account,
+ txParams
+ })
+ if (!success) {
+ return
+ }
+ } else if (tx === 'disconnect_dashboard_wallet') {
+ const success = await handleAuthorizeDisconnectDashboardWallet({
+ account,
+ txParams,
+ onError
+ })
+ if (!success) {
+ return
+ }
}
}
}
@@ -630,7 +626,7 @@ export const useOAuthSetup = ({
userEmail,
authorize,
tx,
- txParams: txParams as WriteOnceParams,
+ txParams,
display
}
}
diff --git a/packages/web/src/pages/oauth-login-page/messages.ts b/packages/web/src/pages/oauth-login-page/messages.ts
index d1683f2e9bf..030b44bd319 100644
--- a/packages/web/src/pages/oauth-login-page/messages.ts
+++ b/packages/web/src/pages/oauth-login-page/messages.ts
@@ -38,9 +38,9 @@ export const messages = {
'Whoops, this is an invalid link (the specified wallet is already connected to an Audius account).',
disconnectWalletNotConnectedError:
'Whoops, this is an invalid link (the specified wallet is not connected to an Audius account).',
- writeOnceParamsError:
+ txParamsError:
'Whoops, this is an invalid link (transaction params missing or invalid).',
- writeOnceTxError: `Whoops, this is an invalid link ('tx' missing or invalid).`,
+ txError: `Whoops, this is an invalid link ('tx' missing or invalid).`,
missingFieldError: 'Whoops, you must enter both your email and password.',
originInvalidError:
'Whoops, this is an invalid link (redirect URI is set to `postMessage` but origin is missing).',
diff --git a/packages/web/src/pages/oauth-login-page/utils.ts b/packages/web/src/pages/oauth-login-page/utils.ts
index 36d89e86e35..ed2b986c1d3 100644
--- a/packages/web/src/pages/oauth-login-page/utils.ts
+++ b/packages/web/src/pages/oauth-login-page/utils.ts
@@ -77,7 +77,7 @@ export const formOAuthResponse = async ({
userEmail,
apiKey,
onError,
- txSignature // Only applicable to scope = write_once
+ txSignature
}: {
account: UserMetadata
userEmail?: string | null
@@ -214,7 +214,7 @@ export const getIsAppAuthorized = async ({
)
return foundIndex !== undefined && foundIndex > -1
}
-export type WriteOnceTx =
+export type DashboardWalletTx =
| 'connect_dashboard_wallet'
| 'disconnect_dashboard_wallet'
@@ -226,11 +226,11 @@ type DisconnectDashboardWalletParams = {
wallet: string
}
-export type WriteOnceParams =
+export type DashboardWalletParams =
| ConnectDashboardWalletParams
| DisconnectDashboardWalletParams
-export const validateWriteOnceParams = ({
+export const validateDashboardWalletParams = ({
tx,
params: rawParams,
willUsePostMessage
@@ -240,13 +240,13 @@ export const validateWriteOnceParams = ({
willUsePostMessage: boolean
}) => {
let error = null
- let txParams: WriteOnceParams | null = null
+ let txParams: DashboardWalletParams | null = null
if (tx === 'connect_dashboard_wallet') {
if (!willUsePostMessage) {
error = messages.connectWalletNoPostMessageError
}
if (!rawParams.wallet) {
- error = messages.writeOnceParamsError
+ error = messages.txParamsError
return { error, txParams }
}
txParams = {
@@ -254,7 +254,7 @@ export const validateWriteOnceParams = ({
}
} else if (tx === 'disconnect_dashboard_wallet') {
if (!rawParams.wallet) {
- error = messages.writeOnceParamsError
+ error = messages.txParamsError
return { error, txParams }
}
txParams = {
@@ -262,7 +262,7 @@ export const validateWriteOnceParams = ({
}
} else {
// Unknown 'tx' value
- error = messages.writeOnceTxError
+ error = messages.txError
}
return { error, txParams }
}