diff --git a/packages/protocol-dashboard/src/components/AppBar/AppBar.tsx b/packages/protocol-dashboard/src/components/AppBar/AppBar.tsx index 4f7ee1ecb64..188d6d346d4 100644 --- a/packages/protocol-dashboard/src/components/AppBar/AppBar.tsx +++ b/packages/protocol-dashboard/src/components/AppBar/AppBar.tsx @@ -195,7 +195,13 @@ const UserAccountSnippet = ({ wallet }: UserAccountSnippetProps) => { ) } -const ConnectAudiusProfileButton = ({ wallet }: { wallet: string }) => { +const ConnectAudiusProfileButton = ({ + wallet, + walletProvider +}: { + wallet: string + walletProvider?: any +}) => { const { isOpen, onClick, onClose } = useModalControls() return ( <> @@ -210,6 +216,7 @@ const ConnectAudiusProfileButton = ({ wallet }: { wallet: string }) => { @@ -298,7 +305,7 @@ const AppBar: React.FC = () => { !wallet || !isLoggedIn || audiusProfileDataStatus === 'pending' ? null : ( - + )}
{ 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 ( + {alt} + ) +} + +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 } }