diff --git a/.changeset/sdk-error-handling-fixes.md b/.changeset/sdk-error-handling-fixes.md new file mode 100644 index 00000000000..9809726807a --- /dev/null +++ b/.changeset/sdk-error-handling-fixes.md @@ -0,0 +1,13 @@ +--- +'@audius/sdk': patch +--- + +Fix several error-handling bugs: + +- Uploads now reject when transcode/resize polling fails, instead of never settling, and aborting an upload stops status polling right away. +- The storage node selector no longer raises an unhandled promise rejection when the node list fetch fails, and uses the refreshed node list for selection. +- The request signature middleware no longer attaches a previously cached signature after the wallet is gone (e.g. after logout) or its address changes and signing fails. +- `SolanaRelayWalletAdapter.connect()` resets `connecting` when fetching the fee payer fails. +- `SolanaRelay.relay()` rethrows the original response error when the error body isn't JSON. +- `SolanaClient.buildTransaction()` rounds the percentile-based priority fee, which previously threw on fractional values. +- Chats: a failed public key fetch is no longer cached forever, and errors handling websocket events are logged instead of becoming unhandled rejections. diff --git a/packages/common/src/adapters/activity.ts b/packages/common/src/adapters/activity.ts index a31cce83a10..bd36465bb08 100644 --- a/packages/common/src/adapters/activity.ts +++ b/packages/common/src/adapters/activity.ts @@ -36,26 +36,6 @@ function normalizeActivityItem( ) } -export const activityFromSDK = (input: Activity) => { - const { timestamp, itemType: item_type, item } = input - if (item_type === ActivityItemTypeEnum.Track) { - const normalized = normalizeActivityItem(item as object, item_type) - return { - timestamp, - item_type, - item: userTrackMetadataFromSDK(normalized as Track) - } - } else if (item_type === ActivityItemTypeEnum.Playlist) { - const normalized = normalizeActivityItem(item as object, item_type) - return { - timestamp, - item_type, - item: userCollectionMetadataFromSDK(normalized as Playlist) - } - } - return undefined -} - export const trackActivityFromSDK = (input: Activity) => { const { timestamp, itemType: item_type, item } = input if (item_type === ActivityItemTypeEnum.Track) { diff --git a/packages/common/src/adapters/coin.ts b/packages/common/src/adapters/coin.ts index ed3ce87991d..27b20effe48 100644 --- a/packages/common/src/adapters/coin.ts +++ b/packages/common/src/adapters/coin.ts @@ -90,12 +90,6 @@ export const coinFromSdk = (input: CoinSDK | undefined): Coin | undefined => { } } -/** - * Converts a list of SDK `Coin` responses to CoinMetadata list - */ -export const coinMetadataListFromSDK = (input?: CoinSDK[]): CoinMetadata[] => - input ? input.map(coinMetadataFromCoin).filter(removeNullable) : [] - /** * Converts a SDK `UserCoin` response to UserCoin, parsing ownerId from HashId string to number */ @@ -123,40 +117,3 @@ export const coinListFromSDK = (input?: CoinSDK[]): Coin[] => */ export const userCoinListFromSDK = (input?: UserCoinSdk[]): UserCoin[] => input ? input.map(userCoinFromSdk).filter(removeNullable) : [] - -/** - * Creates a map of coins keyed by ticker symbol - */ -export const coinMapFromSDK = ( - input?: CoinSDK[] -): Record => { - const coinMap: Record = {} - - if (input) { - input.forEach((coin) => { - const coinMetadata = coinMetadataFromCoin(coin) - if (coinMetadata.ticker) { - coinMap[coinMetadata.ticker] = coinMetadata - } - }) - } - - return coinMap -} - -/** - * Gets all token symbols from a list of coins - */ -export const getTokenSymbolsFromCoins = (coins: CoinMetadata[]): string[] => - coins.map((coin) => coin.ticker || '').filter(Boolean) - -/** - * Gets token mints by symbol from a list of coins - */ -export const getTokenMintsBySymbolsFromCoins = ( - coins: CoinMetadata[], - symbols: string[] -): string[] => - coins - .filter((coin) => coin.ticker && symbols.includes(coin.ticker)) - .map((coin) => coin.mint) diff --git a/packages/common/src/adapters/notification.ts b/packages/common/src/adapters/notification.ts index 69da6861eb7..eaf967fcc54 100644 --- a/packages/common/src/adapters/notification.ts +++ b/packages/common/src/adapters/notification.ts @@ -494,8 +494,7 @@ export const notificationFromSDK = ( const data = action.data entityId = HashId.parse(data.entityId) commentId = OptionalHashId.parse(data.commentId) - // @ts-ignore - entityType = data.type + entityType = data.type as Entity return HashId.parse(data.commentUserId) }) .filter(removeNullable) @@ -523,8 +522,7 @@ export const notificationFromSDK = ( entityId = HashId.parse(data.entityId) entityUserId = HashId.parse(data.entityUserId) commentId = OptionalHashId.parse(data.commentId) - // @ts-ignore - entityType = data.type + entityType = data.type as Entity return HashId.parse(data.commentUserId) }) .filter(removeNullable) @@ -551,8 +549,7 @@ export const notificationFromSDK = ( entityId = HashId.parse(data.entityId) entityUserId = HashId.parse(data.entityUserId) commentId = OptionalHashId.parse(data.commentId) - // @ts-ignore - entityType = data.type + entityType = data.type as Entity return HashId.parse(data.commentUserId) }) .filter(removeNullable) @@ -577,8 +574,7 @@ export const notificationFromSDK = ( entityId = HashId.parse(data.entityId) entityUserId = HashId.parse(data.entityUserId) commentId = OptionalHashId.parse(data.commentId) - // @ts-ignore - entityType = data.type + entityType = data.type as Entity return HashId.parse(data.reacterUserId) }) .filter(removeNullable) diff --git a/packages/common/src/adapters/search.ts b/packages/common/src/adapters/search.ts index 6b79db45996..a3d22c11ae4 100644 --- a/packages/common/src/adapters/search.ts +++ b/packages/common/src/adapters/search.ts @@ -95,15 +95,3 @@ export const searchResultsFromSDK = ( saved_albums } } - -export const AUTOCOMPLETE_TOTAL_RESULTS = 3 - -export const limitAutocompleteResults = ( - results: SearchResults, - maxResults: number = AUTOCOMPLETE_TOTAL_RESULTS -): SearchResults => { - return Object.keys(results).reduce((acc, key) => { - acc[key] = results[key].slice(0, Math.min(maxResults, results[key].length)) - return acc - }, {} as SearchResults) -} diff --git a/packages/common/src/api/index.ts b/packages/common/src/api/index.ts index 43a406801df..ce002f5ec33 100644 --- a/packages/common/src/api/index.ts +++ b/packages/common/src/api/index.ts @@ -74,7 +74,6 @@ export * from './tan-query/purchases/useSalesAggregate' // Reactions export * from './tan-query/reactions/types' -export * from './tan-query/reactions/utils' // Remixes export * from './tan-query/remixes/useRemixesLineup' diff --git a/packages/common/src/api/tan-query/coins/useCoinPair.ts b/packages/common/src/api/tan-query/coins/useCoinPair.ts index b026d4bc42e..9b3e6e5b47e 100644 --- a/packages/common/src/api/tan-query/coins/useCoinPair.ts +++ b/packages/common/src/api/tan-query/coins/useCoinPair.ts @@ -101,10 +101,3 @@ export const useCoinPair = ( data: queryResult.data || createFallbackPair() } } - -// Hook for getting default AUDIO/USDC pair -export const useDefaultTokenPair = ( - options?: Partial> -) => { - return useCoinPair({ baseSymbol: 'AUDIO', quoteSymbol: 'USDC' }, options) -} diff --git a/packages/common/src/api/tan-query/coins/useFanClubs.ts b/packages/common/src/api/tan-query/coins/useFanClubs.ts index 6bf50544c7b..1a83c57a877 100644 --- a/packages/common/src/api/tan-query/coins/useFanClubs.ts +++ b/packages/common/src/api/tan-query/coins/useFanClubs.ts @@ -133,9 +133,9 @@ export const useFanClubs = ( // Otherwise, return the next offset return allPages.length * pageSize }, - enabled: options?.enabled !== false, select: options?.select ?? ((data) => data.pages.flat() as TData), - ...options + ...options, + enabled: options?.enabled !== false }) } diff --git a/packages/common/src/api/tan-query/coins/useUpdateFanClub.ts b/packages/common/src/api/tan-query/coins/useUpdateFanClub.ts index 1ef94179f80..05cc6193194 100644 --- a/packages/common/src/api/tan-query/coins/useUpdateFanClub.ts +++ b/packages/common/src/api/tan-query/coins/useUpdateFanClub.ts @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { Coin } from '~/adapters/coin' import { useCurrentUserId } from '~/api' -import { useQueryContext } from '~/api/tan-query/utils' +import { uploadBackdropImage, useQueryContext } from '~/api/tan-query/utils' import { QUERY_KEYS } from '../queryKeys' @@ -41,29 +41,10 @@ export const useUpdateFanClub = () => { let bannerImageUrl: string | undefined if (updateCoinRequest.bannerImageFile) { - const uploadResponse = await sdk.services.storage - .uploadFile({ - file: updateCoinRequest.bannerImageFile, - metadata: { - template: 'img_backdrop' - } - }) - .start() - const cid = getBannerImageUrl(uploadResponse.results) - if (!cid) { - throw new Error('Failed to process banner image upload') - } - - // Convert CID to content node URL (same pattern as logo_uri from relay service) - const contentNodeEndpoint = await ( - sdk.services.storage as any - ).storageNodeSelector?.getSelectedNode() - - if (!contentNodeEndpoint) { - throw new Error('No content node available') - } - - bannerImageUrl = `${contentNodeEndpoint}/content/${cid}` + bannerImageUrl = await uploadBackdropImage( + sdk, + updateCoinRequest.bannerImageFile + ) } else if (updateCoinRequest.removeBanner) { bannerImageUrl = '' } @@ -125,14 +106,3 @@ export const useUpdateFanClub = () => { } }) } - -const getBannerImageUrl = (results: Record = {}) => { - const prioritizedSizes = ['2000x', '1500x', '1280x', '1000x', '640x'] - for (const size of prioritizedSizes) { - if (results[size]) { - return results[size] - } - } - const firstResult = Object.values(results)[0] - return firstResult -} diff --git a/packages/common/src/api/tan-query/coins/useWalletCoins.ts b/packages/common/src/api/tan-query/coins/useWalletCoins.ts index d26f501963b..019da55658e 100644 --- a/packages/common/src/api/tan-query/coins/useWalletCoins.ts +++ b/packages/common/src/api/tan-query/coins/useWalletCoins.ts @@ -79,7 +79,7 @@ export const useWalletCoins = ( } return [] }, - enabled: options?.enabled !== false && !!params.walletAddress, - ...options + ...options, + enabled: options?.enabled !== false && !!params.walletAddress }) } diff --git a/packages/common/src/api/tan-query/collection/useUpdateCollection.ts b/packages/common/src/api/tan-query/collection/useUpdateCollection.ts deleted file mode 100644 index 6c89db25c22..00000000000 --- a/packages/common/src/api/tan-query/collection/useUpdateCollection.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Id } from '@audius/sdk' -import { useMutation, useQueryClient } from '@tanstack/react-query' -import { isEqual } from 'lodash' -import { useDispatch } from 'react-redux' - -import { playlistMetadataForUpdateWithSDK } from '~/adapters/collection' -import { fileToSdk } from '~/adapters/track' -import { - getCollectionQueryKey, - getTrackQueryKey, - useCurrentUserId -} from '~/api' -import { useQueryContext } from '~/api/tan-query/utils' -import { isContentUSDCPurchaseGated } from '~/models' -import { Collection } from '~/models/Collection' -import { ID } from '~/models/Identifiers' -import { renameAccountPlaylist } from '~/store/account/slice' -import { EditCollectionValues } from '~/store/cache/collections/types' -import { removeNullable } from '~/utils' -import { updatePlaylistArtwork } from '~/utils/updatePlaylistArtwork' - -import { TQCollection } from '../models' -import { primeCollectionData } from '../utils/primeCollectionData' - -type MutationContext = { - previousCollection: TQCollection | undefined - collectionUpdate: EditCollectionValues -} - -type UpdateCollectionParams = { - collectionId: ID - metadata: EditCollectionValues -} - -export const useUpdateCollection = () => { - const { - audiusSdk, - imageUtils: { generatePlaylistArtwork } - } = useQueryContext() - const queryClient = useQueryClient() - const dispatch = useDispatch() - const { data: currentUserId } = useCurrentUserId() - - return useMutation({ - mutationFn: async ({ collectionId, metadata }: UpdateCollectionParams) => { - const sdk = await audiusSdk() - - if (!collectionId || !currentUserId) throw new Error('Invalid ID') - - const collectionUpdate = (await updatePlaylistArtwork( - metadata, - [], - { updated: metadata.tracks }, - { generateImage: generatePlaylistArtwork } - )) as EditCollectionValues - - const coverArtFile = - collectionUpdate?.artwork?.url && 'file' in collectionUpdate.artwork - ? (collectionUpdate.artwork.file as File) - : undefined - - // Handle account collection shortcut update - if (collectionUpdate?.playlist_name) { - dispatch( - renameAccountPlaylist({ - collectionId, - name: collectionUpdate.playlist_name - }) - ) - } - - // Handle premium metadata for albums - if ( - collectionUpdate.is_album && - isContentUSDCPurchaseGated(collectionUpdate.stream_conditions) - ) { - const priceCents = Number( - collectionUpdate.stream_conditions.usdc_purchase.price - ) - - // Update the stream conditions with the price and splits - collectionUpdate.stream_conditions = { - usdc_purchase: { - price: priceCents, - splits: [ - { - user_id: currentUserId, - percentage: 100 - } - ] - } - } - } - - const sdkMetadata = playlistMetadataForUpdateWithSDK( - collectionUpdate as Collection - ) - - const response = await sdk.playlists.updatePlaylist({ - imageFile: coverArtFile - ? fileToSdk(coverArtFile, 'cover_art') - : undefined, - playlistId: Id.parse(collectionId), - userId: Id.parse(currentUserId), - metadata: sdkMetadata - }) - - // feature-tan-query TODO: Track publishing when making private collections public - - return response - }, - onMutate: async ({ - collectionId, - metadata - }: UpdateCollectionParams): Promise => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ - queryKey: getCollectionQueryKey(collectionId) - }) - - // Snapshot the previous values - const previousCollection = queryClient.getQueryData( - getCollectionQueryKey(collectionId) - ) - - const previousTracks = - previousCollection?.trackIds - .map((trackId) => queryClient.getQueryData(getTrackQueryKey(trackId))) - .filter(removeNullable) ?? [] - - // Optimistically update collection - const mergedCollection = { ...previousCollection, ...metadata } - const collectionUpdate = (await updatePlaylistArtwork( - mergedCollection, - previousTracks, - { updated: mergedCollection.tracks }, - { generateImage: generatePlaylistArtwork } - )) as EditCollectionValues - - if (previousCollection) { - primeCollectionData({ - collections: [{ ...previousCollection, ...metadata }], - queryClient, - forceReplace: true - }) - - const previousTrackIds = previousTracks.map((track) => track.track_id) - const updatedTrackIds = collectionUpdate.tracks?.map( - (track) => track.track_id - ) - if (!isEqual(previousTrackIds, updatedTrackIds)) { - // feature-tan-query TODO: reset lineup on tracks changed - } - } - - // Return context with the previous collection - return { previousCollection, collectionUpdate } - }, - onError: (_err, _, context?: MutationContext) => { - // If the mutation fails, roll back collection data - if (context?.previousCollection) { - primeCollectionData({ - collections: [context.previousCollection], - queryClient, - forceReplace: true - }) - } - }, - onSettled: (_, __, { collectionId }) => { - // Always refetch after error or success to ensure cache is in sync with server - queryClient.invalidateQueries({ queryKey: ['collection', collectionId] }) - } - }) -} diff --git a/packages/common/src/api/tan-query/comments/useComments.ts b/packages/common/src/api/tan-query/comments/useComments.ts index 41e5b610e17..cb96204b222 100644 --- a/packages/common/src/api/tan-query/comments/useComments.ts +++ b/packages/common/src/api/tan-query/comments/useComments.ts @@ -5,17 +5,13 @@ import { keyBy } from 'lodash' import { ID } from '~/models/Identifiers' -import { QueryKey, QueryOptions } from '../types' +import { QueryOptions } from '../types' import { combineQueryResults } from '../utils/combineQueryResults' import { useQueries } from '../utils/useQueries' import { CommentOrReply } from './types' import { getCommentQueryKey } from './utils' -export const getCommentsQueryKey = (commentIds: ID[] | null | undefined) => { - return ['comments', commentIds] as unknown as QueryKey -} - export const useComments = ( commentIds: ID[] | null | undefined, options?: QueryOptions diff --git a/packages/common/src/api/tan-query/comments/useMuteUser.ts b/packages/common/src/api/tan-query/comments/useMuteUser.ts index 44ab55f762a..1fb34768802 100644 --- a/packages/common/src/api/tan-query/comments/useMuteUser.ts +++ b/packages/common/src/api/tan-query/comments/useMuteUser.ts @@ -99,14 +99,20 @@ export const useMuteUser = () => { // Generic toast error dispatch(toast({ content: messages.muteUserError })) + if (trackId === undefined) return // No way to know what comment count should be here, so we just reset the query data queryClient.resetQueries({ - queryKey: ['trackCommentCount', trackId] + queryKey: getTrackCommentCountQueryKey(trackId) }) // Reload data - queryClient.resetQueries({ - queryKey: ['trackCommentList', trackId, currentSort] - }) + if (currentSort !== undefined) { + queryClient.resetQueries({ + queryKey: getTrackCommentListQueryKey({ + trackId, + sortMethod: currentSort + }) + }) + } } }) } diff --git a/packages/common/src/api/tan-query/events/useAllRemixContests.ts b/packages/common/src/api/tan-query/events/useAllRemixContests.ts index 22c2e41e101..77fc04e0fb3 100644 --- a/packages/common/src/api/tan-query/events/useAllRemixContests.ts +++ b/packages/common/src/api/tan-query/events/useAllRemixContests.ts @@ -45,7 +45,7 @@ export const getAllRemixContestsQueryKey = ({ /** * Hook to fetch all remix contest events with infinite query support. - * Calls the dedicated discovery endpoint `GET /v1/events/remix-contests` + * Calls the dedicated validator node endpoint `GET /v1/events/remix-contests` * (SDK: `events.getRemixContests`), which returns events ordered with * currently-active contests first (by soonest-ending end_date) followed by * ended contests. @@ -85,7 +85,7 @@ export const useAllRemixContests = ( }) // Prime related tracks + users (full objects, delivered alongside the - // event list on the discovery endpoint). This turns ContestCard's + // event list on the validator node endpoint). This turns ContestCard's // useTrack / useUser into cache hits so the grid can paint with one // network round-trip instead of N+1. primeRelatedData({ related, queryClient }) diff --git a/packages/common/src/api/tan-query/events/useUserRemixContests.ts b/packages/common/src/api/tan-query/events/useUserRemixContests.ts index 39fa0b67985..9df9303ac93 100644 --- a/packages/common/src/api/tan-query/events/useUserRemixContests.ts +++ b/packages/common/src/api/tan-query/events/useUserRemixContests.ts @@ -138,7 +138,7 @@ export const useUserRemixContests = ( .filter(removeNullable) }, select: (data) => data.pages.flat(), - enabled: options?.enabled !== false && !!userId, - ...options + ...options, + enabled: options?.enabled !== false && !!userId }) } diff --git a/packages/common/src/api/tan-query/jupiter/useCoinExchangeRate.ts b/packages/common/src/api/tan-query/jupiter/useCoinExchangeRate.ts index 52576f984d6..c5f7c166102 100644 --- a/packages/common/src/api/tan-query/jupiter/useCoinExchangeRate.ts +++ b/packages/common/src/api/tan-query/jupiter/useCoinExchangeRate.ts @@ -417,7 +417,8 @@ export const useCoinExchangeRate = ( ) } }, - enabled: !!params.inputMint && !!params.outputMint, - ...options + ...options, + enabled: + !!params.inputMint && !!params.outputMint && options?.enabled !== false }) } diff --git a/packages/common/src/api/tan-query/jupiter/utils.ts b/packages/common/src/api/tan-query/jupiter/utils.ts index ee5982ab0c5..59907226707 100644 --- a/packages/common/src/api/tan-query/jupiter/utils.ts +++ b/packages/common/src/api/tan-query/jupiter/utils.ts @@ -3,8 +3,6 @@ import type { AudiusSdkWithServices } from '@audius/sdk' import { SwapInstructionsResponse, SwapRequest } from '@jup-ag/api' import { createAssociatedTokenAccountIdempotentInstruction, - createCloseAccountInstruction, - createTransferCheckedInstruction, getAccount, getAssociatedTokenAddressSync } from '@solana/spl-token' @@ -18,10 +16,7 @@ import { import { QueryClient, useQueryClient } from '@tanstack/react-query' import type { User } from '~/models/User' -import { - getJupiterQuoteByMintWithRetry, - jupiterInstance -} from '~/services/Jupiter' +import { jupiterInstance } from '~/services/Jupiter' import { INTERNAL_TRANSFER_MEMO_STRING, MEMO_PROGRAM_ID @@ -106,46 +101,6 @@ export async function addTransferFromUserBankInstructions({ return ata } -export async function addTransferToUserBankInstructions({ - tokenInfo, - userPublicKey, - ethAddress, - amountLamports, - sourceAta, - sdk, - feePayer, - instructions -}: { - tokenInfo: UserBankManagedTokenInfo - userPublicKey: PublicKey - ethAddress: string - amountLamports: bigint - sourceAta: PublicKey - sdk: AudiusSdkWithServices - feePayer: PublicKey - instructions: TransactionInstruction[] -}): Promise { - const mint = new PublicKey(tokenInfo.mintAddress) - const userBankAddress = - await sdk.services.claimableTokensClient.deriveUserBank({ - ethWallet: ethAddress, - mint: tokenInfo.claimableTokenMint - }) - - instructions.push( - createTransferCheckedInstruction( - sourceAta, - mint, - userBankAddress, - userPublicKey, - amountLamports, - tokenInfo.decimals - ), - createCloseAccountInstruction(sourceAta, feePayer, userPublicKey) - ) - return userBankAddress -} - /** * Creates an Associated Token Account (ATA) for Jupiter when shared accounts are not supported. * This is used as a fallback when Jupiter's shared account system fails for simple AMMs. @@ -432,47 +387,6 @@ export const prepareOutputUserBank = async ( return result.userBank.toBase58() } -/** - @deprecated - We're moving away from Jupiter as the SO for stuff - use getIsDirectSwappable instead + meteora quotes if needed -*/ -export const isDirectRouteAvailable = async ( - inputMint: string, - outputMint: string, - amountUi: number, - tokens: Record -): Promise => { - try { - // Validate tokens and create configs - const tokenConfigsResult = validateAndCreateTokenConfigs( - inputMint, - outputMint, - tokens - ) - - if ('error' in tokenConfigsResult) { - return false - } - - const { inputTokenConfig, outputTokenConfig } = tokenConfigsResult - - // Try to get a direct quote - await getJupiterQuoteByMintWithRetry({ - inputMint, - outputMint, - inputDecimals: inputTokenConfig.decimals, - outputDecimals: outputTokenConfig.decimals, - amountUi, - swapMode: 'ExactIn', - onlyDirectRoutes: false - }) - - return true - } catch (error) { - // If quote fails, there's no direct path available - return false - } -} - /** * Checks if a swap is direct swappable between two tokens. * @param inputMint - The input mint address diff --git a/packages/common/src/api/tan-query/lineups/usePremiumTracks.ts b/packages/common/src/api/tan-query/lineups/usePremiumTracks.ts deleted file mode 100644 index 18db36bea58..00000000000 --- a/packages/common/src/api/tan-query/lineups/usePremiumTracks.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { OptionalId, EntityType } from '@audius/sdk' -import { - InfiniteData, - useInfiniteQuery, - useQueryClient -} from '@tanstack/react-query' - -import { userTrackMetadataFromSDK } from '~/adapters/track' -import { transformAndCleanList } from '~/adapters/utils' -import { useQueryContext } from '~/api/tan-query/utils' -import { ID } from '~/models/Identifiers' - -import { QUERY_KEYS } from '../queryKeys' -import { QueryKey, LineupData, QueryOptions } from '../types' -import { useCurrentUserId } from '../users/account/useCurrentUserId' -import { makeLoadNextPage } from '../utils/infiniteQueryLoadNextPage' -import { primeTrackData } from '../utils/primeTrackData' - -const DEFAULT_PAGE_SIZE = 10 - -type UsePremiumTracksArgs = { - pageSize?: number -} - -export const getPremiumTracksQueryKey = (pageSize: number) => { - return [QUERY_KEYS.premiumTracks, pageSize] as unknown as QueryKey< - InfiniteData - > -} - -export const usePremiumTracks = ( - { pageSize = DEFAULT_PAGE_SIZE }: UsePremiumTracksArgs = {}, - options?: QueryOptions -) => { - const { audiusSdk } = useQueryContext() - const { data: currentUserId } = useCurrentUserId() - const queryClient = useQueryClient() - - const queryKey = getPremiumTracksQueryKey(pageSize) - - const query = useInfiniteQuery({ - queryKey, - initialPageParam: 0, - getNextPageParam: (lastPage: LineupData[], allPages) => { - if (lastPage.length < pageSize) return undefined - return allPages.length * pageSize - }, - queryFn: async ({ pageParam }) => { - const sdk = await audiusSdk() - const { data: tracks = [] } = - await sdk.tracks.getTrendingUSDCPurchaseTracks({ - userId: OptionalId.parse(currentUserId), - limit: pageSize, - offset: pageParam - }) - const processedTracks = transformAndCleanList( - tracks, - userTrackMetadataFromSDK - ) - primeTrackData({ tracks: processedTracks, queryClient }) - return processedTracks.map((t) => ({ - id: t.track_id, - type: EntityType.TRACK - })) - }, - select: (data) => data?.pages.flat(), - ...options, - enabled: options?.enabled !== false - }) - - const data = query.data ?? [] - const trackIds = data - .filter((d) => d.type === EntityType.TRACK) - .map((d) => d.id as ID) - - return { - data, - trackIds, - isPending: query.isPending, - isLoading: query.isLoading, - isFetching: query.isFetching, - isSuccess: query.isSuccess, - isError: query.isError, - isInitialLoading: query.isInitialLoading, - hasNextPage: query.hasNextPage, - fetchNextPage: query.fetchNextPage, - loadNextPage: makeLoadNextPage(query), - queryKey - } -} diff --git a/packages/common/src/api/tan-query/lineups/useTrackPageLineup.ts b/packages/common/src/api/tan-query/lineups/useTrackPageLineup.ts index 5cd89c0f5b2..3d332fc7cf0 100644 --- a/packages/common/src/api/tan-query/lineups/useTrackPageLineup.ts +++ b/packages/common/src/api/tan-query/lineups/useTrackPageLineup.ts @@ -241,8 +241,6 @@ export const useTrackPageLineup = ( * live observer so the warmed data is shared with `TrackScreenLineup`'s own * query. */ -export const usePrefetchTrackPageLineup = ( - trackId: ID | null | undefined -) => { +export const usePrefetchTrackPageLineup = (trackId: ID | null | undefined) => { useTrackPageLineup({ trackId }) } diff --git a/packages/common/src/api/tan-query/notifications/useMarkNotificationsAsViewed.ts b/packages/common/src/api/tan-query/notifications/useMarkNotificationsAsViewed.ts index 745e85458f2..27f118e70e0 100644 --- a/packages/common/src/api/tan-query/notifications/useMarkNotificationsAsViewed.ts +++ b/packages/common/src/api/tan-query/notifications/useMarkNotificationsAsViewed.ts @@ -29,11 +29,20 @@ export const useMarkNotificationsAsViewed = () => { }) }, onMutate: () => { + const queryKey = getNotificationUnreadCountQueryKey(currentUserId) + const previousUnreadCount = queryClient.getQueryData(queryKey) // Optimistically set unread count to 0 - queryClient.setQueryData( - getNotificationUnreadCountQueryKey(currentUserId), - 0 - ) + queryClient.setQueryData(queryKey, 0) + return { previousUnreadCount } + }, + onError: (_error, _variables, context) => { + // Roll back the optimistic update so the unread badge isn't lost + if (context?.previousUnreadCount !== undefined) { + queryClient.setQueryData( + getNotificationUnreadCountQueryKey(currentUserId), + context.previousUnreadCount + ) + } } }) } diff --git a/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts b/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts index 6a090008574..51185e53382 100644 --- a/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts +++ b/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts @@ -57,10 +57,10 @@ export const usePlaylistUpdates = ( playlistUpdateFromSDK ) }, - enabled: !!currentUserId, refetchInterval: PLAYLIST_UPDATES_POLLING_FREQ_MS, refetchOnWindowFocus: true, - ...options + ...options, + enabled: !!currentUserId && options?.enabled !== false }) } @@ -81,11 +81,3 @@ export const useAllPlaylistUpdateIds = () => usePlaylistUpdates({ select: (updates) => updates.map((u) => u.playlist_id) }) - -/** - * Returns the count of playlists with unviewed updates. - */ -export const usePlaylistUpdatesCount = () => - usePlaylistUpdates({ - select: (updates) => updates.length - }) diff --git a/packages/common/src/api/tan-query/purchases/useUSDCTransactions.ts b/packages/common/src/api/tan-query/purchases/useUSDCTransactions.ts index 70bc0d597f0..4bad9d8a24d 100644 --- a/packages/common/src/api/tan-query/purchases/useUSDCTransactions.ts +++ b/packages/common/src/api/tan-query/purchases/useUSDCTransactions.ts @@ -140,8 +140,6 @@ export const useUSDCTransactions = ( }) }, [queryClient, queryKey]) - // @ts-ignore - queryData.reset = reset // Stable identity for loadNextPage so the consuming Table's `loadMoreRows` // doesn't change every render. We read the latest queryData from a ref // at call time instead of capturing it in the closure deps. @@ -152,9 +150,12 @@ export const useUSDCTransactions = ( if (q.isFetching || !q.hasNextPage) return undefined return q.fetchNextPage() }, []) - // @ts-ignore - queryData.loadNextPage = loadNextPageCallback - return queryData as UseInfiniteQueryResult & { + // Attach helpers directly onto the query result object (rather than + // spreading it) so TanStack Query's tracked-property access keeps working. + return Object.assign(queryData, { + reset, + loadNextPage: loadNextPageCallback + }) as UseInfiniteQueryResult & { reset: typeof reset loadNextPage: typeof loadNextPageCallback } diff --git a/packages/common/src/api/tan-query/reactions/utils.ts b/packages/common/src/api/tan-query/reactions/utils.ts deleted file mode 100644 index 65b2304a361..00000000000 --- a/packages/common/src/api/tan-query/reactions/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactionTypes, reactionsMap } from './types' - -export const getReactionFromRawValue = ( - value: number | null -): ReactionTypes | null => { - if (value === null) return null - const val = (Object.entries(reactionsMap) as [ReactionTypes, number][]).find( - ([_k, v]) => v === value - ) - return val?.[0] ?? null -} - -export const getRawValueFromReaction = ( - reaction: ReactionTypes | null -): number | null => { - if (reaction === null) return null - return reactionsMap[reaction] ?? null -} diff --git a/packages/common/src/api/tan-query/remixes/useRemixes.ts b/packages/common/src/api/tan-query/remixes/useRemixes.ts index ba836b076e0..450c732c945 100644 --- a/packages/common/src/api/tan-query/remixes/useRemixes.ts +++ b/packages/common/src/api/tan-query/remixes/useRemixes.ts @@ -161,8 +161,8 @@ export const useRemixes = ( type: EntityType.TRACK })) }, - enabled: options?.enabled !== false && !!trackId, - ...options + ...options, + enabled: options?.enabled !== false && !!trackId }) return queryData @@ -198,7 +198,7 @@ export const useRemixesCount = ( dispatch(remixesPageActions.setCount({ count })) return count }, - enabled: options?.enabled !== false && !!trackId, - ...options + ...options, + enabled: options?.enabled !== false && !!trackId }) } diff --git a/packages/common/src/api/tan-query/saga-utils/queryCollection.ts b/packages/common/src/api/tan-query/saga-utils/queryCollection.ts index 6dc4b5caab3..8256ecb87b0 100644 --- a/packages/common/src/api/tan-query/saga-utils/queryCollection.ts +++ b/packages/common/src/api/tan-query/saga-utils/queryCollection.ts @@ -78,22 +78,3 @@ export function* queryCollectionByPermalink( const collection = yield* call(queryCollection, collectionId, queryOptions) return collection } - -export function* queryCollectionsByPermalink( - permalinks: string[], - queryOptions?: QueryOptions -) { - const collections = {} as Record - for (const permalink of permalinks) { - // Call each queryCollectionByPermalink individually. They will be batched together in the queryFn (if necessary) - const collection = yield* call( - queryCollectionByPermalink, - permalink, - queryOptions - ) - if (collection) { - collections[permalink] = collection - } - } - return collections -} diff --git a/packages/common/src/api/tan-query/saga-utils/queryTrack.ts b/packages/common/src/api/tan-query/saga-utils/queryTrack.ts index 358456f30d8..b1b3fc3bcf7 100644 --- a/packages/common/src/api/tan-query/saga-utils/queryTrack.ts +++ b/packages/common/src/api/tan-query/saga-utils/queryTrack.ts @@ -4,7 +4,7 @@ import { Track } from '~/models' import { ID } from '~/models/Identifiers' import { getContext } from '~/store/effects' import { getSDK } from '~/store/sdkUtils' -import { removeNullable, Uid } from '~/utils' +import { removeNullable } from '~/utils' import { TQTrack } from '../models' import { QUERY_KEYS } from '../queryKeys' @@ -69,12 +69,6 @@ export function* queryAllTracks() { ) } -export function* queryTrackByUid(uid: string | null | undefined) { - if (!uid) return null - const trackId = Number(Uid.fromString(uid).id) - return yield* queryTrack(trackId) -} - export function* queryCollectionTracks( collectionId: ID | null | undefined, queryOptions?: QueryOptions diff --git a/packages/common/src/api/tan-query/saga-utils/queryUser.ts b/packages/common/src/api/tan-query/saga-utils/queryUser.ts index 8f5f0fbfff1..ea97570b8dc 100644 --- a/packages/common/src/api/tan-query/saga-utils/queryUser.ts +++ b/packages/common/src/api/tan-query/saga-utils/queryUser.ts @@ -5,7 +5,6 @@ import { User } from '~/models/User' import { getContext } from '~/store/effects' import { getSDK } from '~/store/sdkUtils' -import { QUERY_KEYS } from '../queryKeys' import { getUserQueryFn, getUserQueryKey } from '../users/useUser' import { getUserByHandleQueryFn, @@ -80,19 +79,3 @@ export function* queryUsers(ids: ID[], queryOptions?: QueryOptions) { return users } - -export function* queryAllCachedUsers() { - const queryClient = yield* getContext('queryClient') - const queries = queryClient.getQueriesData({ - queryKey: [QUERY_KEYS.user] - }) - return queries.reduce( - (acc, [_, user]) => { - if (user?.user_id) { - acc[user.user_id] = user - } - return acc - }, - {} as Record - ) -} diff --git a/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts b/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts index 6f68c64251c..965162955e9 100644 --- a/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts +++ b/packages/common/src/api/tan-query/tracks/useDownloadTrackStems.ts @@ -169,8 +169,8 @@ export const useGetStemsArchiveJobStatus = ( }, staleTime: 0, gcTime: 0, - enabled: !!jobId, - ...options + ...options, + enabled: !!jobId && options?.enabled !== false }) const jobState = query.data?.state diff --git a/packages/common/src/api/tan-query/tracks/useTracks.ts b/packages/common/src/api/tan-query/tracks/useTracks.ts index 36aa2d1b3b5..fe0ccb5a564 100644 --- a/packages/common/src/api/tan-query/tracks/useTracks.ts +++ b/packages/common/src/api/tan-query/tracks/useTracks.ts @@ -7,7 +7,6 @@ import { useDispatch } from 'react-redux' import { useQueryContext } from '~/api/tan-query/utils' import { ID } from '~/models/Identifiers' -import { getTracksBatcher } from '../batchers/getTracksBatcher' import { TQTrack } from '../models' import { QueryOptions } from '../types' import { useCurrentUserId } from '../users/account/useCurrentUserId' @@ -17,26 +16,6 @@ import { useQueries } from '../utils/useQueries' import { getTrackQueryFn, getTrackQueryKey } from './useTrack' -// This function is only used for batch operations in saga utilities -export const getTracksQueryFn = async ( - trackIds: ID[], - currentUserId: ID | null, - queryClient: any, - sdk: any, - dispatch: any -) => { - const batchGetTracks = getTracksBatcher({ - sdk, - currentUserId, - queryClient, - dispatch - }) - const tracks = await Promise.all( - trackIds.map((trackId) => batchGetTracks.fetch(trackId)) - ) - return tracks.filter((track): track is TQTrack => !!track) -} - export const useTracks = ( trackIds: ID[] | null | undefined, options?: QueryOptions diff --git a/packages/common/src/api/tan-query/users/account/useCurrentAccount.ts b/packages/common/src/api/tan-query/users/account/useCurrentAccount.ts index 86a5dc6d1ae..a362a1ca18d 100644 --- a/packages/common/src/api/tan-query/users/account/useCurrentAccount.ts +++ b/packages/common/src/api/tan-query/users/account/useCurrentAccount.ts @@ -134,7 +134,7 @@ export const useCurrentAccount = ( getLocalAccount(localStorage, queryClient) ?? undefined, staleTime: Infinity, gcTime: Infinity, - enabled: options?.enabled !== false && !!currentUserWallet, - ...options + ...options, + enabled: options?.enabled !== false && !!currentUserWallet }) } diff --git a/packages/common/src/api/tan-query/users/account/useManagers.ts b/packages/common/src/api/tan-query/users/account/useManagers.ts index 7b2351f72dc..6e12ea08328 100644 --- a/packages/common/src/api/tan-query/users/account/useManagers.ts +++ b/packages/common/src/api/tan-query/users/account/useManagers.ts @@ -32,7 +32,7 @@ export const useManagers = ( const data = rawData.filter((g: any) => g.grant.isApproved !== false) return userManagerListFromSDK(data) as TResult }, - enabled: isValidId(userId), - ...options + ...options, + enabled: isValidId(userId) && options?.enabled !== false }) } diff --git a/packages/common/src/api/tan-query/users/useUnfollowUser.ts b/packages/common/src/api/tan-query/users/useUnfollowUser.ts deleted file mode 100644 index ddcb2650176..00000000000 --- a/packages/common/src/api/tan-query/users/useUnfollowUser.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Id } from '@audius/sdk' -import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useDispatch } from 'react-redux' - -import { useQueryContext } from '~/api/tan-query/utils/QueryContext' -import { useAppContext } from '~/context/appContext' -import { Name, FollowSource } from '~/models/Analytics' -import { ID } from '~/models/Identifiers' -import { UserMetadata } from '~/models/User' -import { removeFolloweeId } from '~/store/gated-content/slice' - -import { primeUserData } from '../utils/primeUserData' - -import { useCurrentUserId } from './account/useCurrentUserId' -import { getUserQueryKey } from './useUser' - -type UnfollowUserParams = { - followeeUserId: ID | null | undefined - source: FollowSource -} - -type MutationContext = { - previousUser: UserMetadata | undefined -} - -export const useUnfollowUser = () => { - const { audiusSdk } = useQueryContext() - const queryClient = useQueryClient() - const dispatch = useDispatch() - const { - analytics: { track, make } - } = useAppContext() - const { data: currentUserId } = useCurrentUserId() - - return useMutation({ - mutationFn: async ({ followeeUserId, source }: UnfollowUserParams) => { - if (!currentUserId || !followeeUserId) { - return - } - - const sdk = await audiusSdk() - await sdk.users.unfollowUser({ - userId: Id.parse(currentUserId), - followeeUserId: Id.parse(followeeUserId) - }) - - // Handle gated content - dispatch(removeFolloweeId({ id: followeeUserId })) - - if (source) { - track( - make({ - eventName: Name.UNFOLLOW, - id: followeeUserId.toString(), - source - }) - ) - } - }, - onMutate: async ({ followeeUserId }) => { - if (!followeeUserId || !currentUserId) { - return { previousUser: undefined } - } - - await queryClient.cancelQueries({ - queryKey: getUserQueryKey(followeeUserId) - }) - - const previousUser = queryClient.getQueryData( - getUserQueryKey(followeeUserId) - ) - - const followeeUpdate = { - does_current_user_follow: false, - follower_count: Math.max((previousUser?.follower_count ?? 0) - 1, 0) - } - if (previousUser) { - const updatedUser = { - ...previousUser, - ...followeeUpdate - } - primeUserData({ - users: [updatedUser], - queryClient, - forceReplace: true - }) - } - - const currentUser = queryClient.getQueryData( - getUserQueryKey(currentUserId) - ) - if (currentUser) { - const updatedCurrentUser = { - ...currentUser, - followee_count: Math.max((currentUser?.followee_count ?? 0) - 1, 0) - } - primeUserData({ - users: [updatedCurrentUser], - queryClient, - forceReplace: true - }) - } - - return { previousUser } - }, - onError: (error, _args, context: MutationContext | undefined) => { - const { previousUser } = context ?? {} - - if (previousUser) { - primeUserData({ - users: [previousUser], - queryClient, - forceReplace: true - }) - } - - console.error(error) - } - }) -} diff --git a/packages/common/src/api/tan-query/users/useUpdateProfile.ts b/packages/common/src/api/tan-query/users/useUpdateProfile.ts index 51a878060e3..2bf454b240a 100644 --- a/packages/common/src/api/tan-query/users/useUpdateProfile.ts +++ b/packages/common/src/api/tan-query/users/useUpdateProfile.ts @@ -84,7 +84,7 @@ export const useUpdateProfile = () => { onSuccess: (_data, variables) => { // Re-confirm the mutation in the cache after server success. // We re-apply mutation variables rather than the raw server response - // to guard against discovery-node propagation lag: the node answering + // to guard against validator node propagation lag: the node answering // getUser may not yet have indexed the confirmed block. const currentCache = queryClient.getQueryData( getUserQueryKey(currentUserId) diff --git a/packages/common/src/api/tan-query/users/useUpdateUser.ts b/packages/common/src/api/tan-query/users/useUpdateUser.ts index b1236f66ffd..79ff25891e0 100644 --- a/packages/common/src/api/tan-query/users/useUpdateUser.ts +++ b/packages/common/src/api/tan-query/users/useUpdateUser.ts @@ -75,10 +75,6 @@ export const useUpdateUser = () => { forceReplace: true }) } - }, - onSettled: (_, __) => { - // Always refetch after error or success to ensure cache is in sync with server - // queryClient.invalidateQueries({ queryKey: ['user', userId] }) } }) } diff --git a/packages/common/src/api/tan-query/users/useUser.ts b/packages/common/src/api/tan-query/users/useUser.ts index 7b4cf069fe9..dd865597a2a 100644 --- a/packages/common/src/api/tan-query/users/useUser.ts +++ b/packages/common/src/api/tan-query/users/useUser.ts @@ -20,14 +20,6 @@ export const getUserQueryKey = (userId: ID | null | undefined) => { return [QUERY_KEYS.user, userId] as unknown as QueryKey } -export const getUserComputedPropsQueryKey = (userId: ID | null | undefined) => { - return [ - QUERY_KEYS.user, - userId, - QUERY_KEYS.computedProps - ] as unknown as QueryKey -} - export const getUserQueryFn = async ( userId: ID, currentUserId: ID | null | undefined, diff --git a/packages/common/src/api/tan-query/utils/__tests__/uploadBackdropImage.test.ts b/packages/common/src/api/tan-query/utils/__tests__/uploadBackdropImage.test.ts new file mode 100644 index 00000000000..a148b41d922 --- /dev/null +++ b/packages/common/src/api/tan-query/utils/__tests__/uploadBackdropImage.test.ts @@ -0,0 +1,60 @@ +import type { AudiusSdkWithServices } from '@audius/sdk' +import { describe, it, expect, vi } from 'vitest' + +import { uploadBackdropImage } from '../uploadBackdropImage' + +const makeSdk = ({ + results, + node = 'https://validator.example.com' +}: { + results: Record + node?: string | null +}) => { + const uploadFile = vi.fn(() => ({ + start: vi.fn(async () => ({ results })) + })) + const sdk = { + services: { + storage: { uploadFile }, + storageNodeSelector: { getSelectedNode: vi.fn(async () => node) } + } + } as unknown as AudiusSdkWithServices + return { sdk, uploadFile } +} + +const file = new File(['x'], 'banner.png', { type: 'image/png' }) + +describe('uploadBackdropImage', () => { + it('uploads with the img_backdrop template and returns the largest variant URL', async () => { + const { sdk, uploadFile } = makeSdk({ + results: { '640x': 'cid640', '2000x': 'cid2000', '1000x': 'cid1000' } + }) + + const url = await uploadBackdropImage(sdk, file) + + expect(uploadFile).toHaveBeenCalledWith({ + file, + metadata: { template: 'img_backdrop' } + }) + expect(url).toBe('https://validator.example.com/content/cid2000') + }) + + it('falls back to any variant when no prioritized size is present', async () => { + const { sdk } = makeSdk({ results: { original: 'cidOriginal' } }) + await expect(uploadBackdropImage(sdk, file)).resolves.toBe( + 'https://validator.example.com/content/cidOriginal' + ) + }) + + it('throws when the upload returns no CID', async () => { + const { sdk } = makeSdk({ results: {} }) + await expect(uploadBackdropImage(sdk, file)).rejects.toThrow() + }) + + it('throws when no validator node is available', async () => { + const { sdk } = makeSdk({ results: { '2000x': 'cid' }, node: null }) + await expect(uploadBackdropImage(sdk, file)).rejects.toThrow( + 'No validator node available' + ) + }) +}) diff --git a/packages/common/src/api/tan-query/utils/index.ts b/packages/common/src/api/tan-query/utils/index.ts index b398e207c6b..fa5c4af0ad3 100644 --- a/packages/common/src/api/tan-query/utils/index.ts +++ b/packages/common/src/api/tan-query/utils/index.ts @@ -12,5 +12,6 @@ export * from './primeTrackData' export * from './primeUserData' export * from './QueryContext' export * from './updateTrackData' +export * from './uploadBackdropImage' export * from './updateCollectionData' export * from './useQueries' diff --git a/packages/common/src/api/tan-query/utils/uploadBackdropImage.ts b/packages/common/src/api/tan-query/utils/uploadBackdropImage.ts new file mode 100644 index 00000000000..97c4eebbac5 --- /dev/null +++ b/packages/common/src/api/tan-query/utils/uploadBackdropImage.ts @@ -0,0 +1,48 @@ +import type { AudiusSdkWithServices } from '@audius/sdk' + +const PRIORITIZED_BACKDROP_SIZES = ['2000x', '1500x', '1280x', '1000x', '640x'] + +/** + * Picks the CID of the largest available resized variant from an + * `img_backdrop` upload result, falling back to any variant. + */ +const getBackdropImageCid = (results: Record = {}) => { + for (const size of PRIORITIZED_BACKDROP_SIZES) { + if (results[size]) { + return results[size] + } + } + return Object.values(results)[0] +} + +/** + * Uploads a wide-aspect image (e.g. a fan club banner or contest cover) using + * the `img_backdrop` template and resolves to a URL on the selected validator + * node that serves the largest resized variant. + * + * Throws if the upload produces no CID or no validator node is available. + */ +export const uploadBackdropImage = async ( + sdk: AudiusSdkWithServices, + file: File +): Promise => { + const uploadResponse = await sdk.services.storage + .uploadFile({ + file, + metadata: { template: 'img_backdrop' } + }) + .start() + + const cid = getBackdropImageCid(uploadResponse.results) + if (!cid) { + throw new Error('Failed to process backdrop image upload') + } + + const validatorNodeEndpoint = + await sdk.services.storageNodeSelector.getSelectedNode() + if (!validatorNodeEndpoint) { + throw new Error('No validator node available') + } + + return `${validatorNodeEndpoint}/content/${cid}` +} diff --git a/packages/common/src/api/tan-query/wallets/useAudioBalance.ts b/packages/common/src/api/tan-query/wallets/useAudioBalance.ts index 36201abbdcf..3b08265ad0a 100644 --- a/packages/common/src/api/tan-query/wallets/useAudioBalance.ts +++ b/packages/common/src/api/tan-query/wallets/useAudioBalance.ts @@ -308,92 +308,6 @@ export function* getAccountAudioBalanceSaga() { return yield* call(getWalletBalances, userWallets) } -/** - * Sums current audio account balance combined with balance from all connected wallets - * @returns - */ -export function* getAccountTotalAudioBalanceSaga() { - const queryClient = yield* getContext('queryClient') - const queryContext = yield* getQueryContext() - const accountBalance = yield* call(getAccountAudioBalanceSaga) - const currentUserId = yield* call(queryCurrentUserId) - const fetchConnectedWallets = async () => - await queryClient.fetchQuery( - getConnectedWalletsQueryOptions(queryContext, { - userId: currentUserId - }) - ) - const connectedWallets = yield* call(fetchConnectedWallets) - const connectedWalletsBalance = yield* call( - getWalletBalances, - connectedWallets ?? [] - ) - - return AUDIO(accountBalance + connectedWalletsBalance).value -} - -/** - * Optimistically updates the user's SOL wallet balance in the cache. - * Use this to provide immediate UI feedback before the transaction confirms. - * - * @param amount - The amount to add (positive) or subtract (negative) from the current balance - */ -export function* optimisticallyUpdateUserWAudioBalance(change: AudioWei) { - const queryClient = yield* getContext('queryClient') - const queryContext = yield* getQueryContext() - const currentUserId = yield* call(queryCurrentUserId) - const user = yield* call(queryUser, currentUserId) - - if (!user?.spl_wallet) return - - // Update both staked and non-staked balance queries for the SOL wallet - for (const includeStaked of [true, false]) { - const { queryKey } = getWalletAudioBalanceOptions(queryContext, { - address: user.spl_wallet, - chain: Chain.Sol, - includeStaked - }) - - queryClient.setQueryData(queryKey, (oldBalance) => { - const currentBalance = oldBalance ?? AUDIO(0).value - const newBalance = AUDIO(currentBalance + change).value - // Ensure balance doesn't go negative - return newBalance >= 0 ? newBalance : AUDIO(0).value - }) - } -} - -/** - * Reverts an optimistic balance update by invalidating the cache. - * Use this when a transaction fails and you need to revert to the real balance. - */ -export function* revertOptimisticUserSolBalance() { - const queryClient = yield* getContext('queryClient') - const currentUserId = yield* call(queryCurrentUserId) - const user = yield* call(queryUser, currentUserId) - - if (!user?.spl_wallet) return - - // Invalidate both staked and non-staked balance queries - const solWalletQueries = [ - { - address: user.spl_wallet, - chain: Chain.Sol, - includeStaked: true - }, - { - address: user.spl_wallet, - chain: Chain.Sol, - includeStaked: false - } - ] - - for (const queryParams of solWalletQueries) { - const queryKey = getWalletAudioBalanceQueryKey(queryParams) - queryClient.invalidateQueries({ queryKey }) - } -} - /** * Optimistically updates the AUDIO balance for a SOL wallet in React Query cache. * Updates both staked and non-staked balance queries. diff --git a/packages/common/src/api/tan-query/wallets/useCoinBalance.ts b/packages/common/src/api/tan-query/wallets/useCoinBalance.ts index b255ee00215..e85c272be31 100644 --- a/packages/common/src/api/tan-query/wallets/useCoinBalance.ts +++ b/packages/common/src/api/tan-query/wallets/useCoinBalance.ts @@ -60,10 +60,10 @@ export const useCoinBalance = ({ includeStaked }, { - enabled: isAudio, refetchInterval: isPolling ? pollingInterval : undefined, refetchOnWindowFocus: !isPolling, - ...queryOptions + ...queryOptions, + enabled: isAudio && queryOptions.enabled !== false } ) diff --git a/packages/common/src/api/tan-query/wallets/useDestinationUsdcAccountCheck.ts b/packages/common/src/api/tan-query/wallets/useDestinationUsdcAccountCheck.ts index 72378ca0494..a71da667ca2 100644 --- a/packages/common/src/api/tan-query/wallets/useDestinationUsdcAccountCheck.ts +++ b/packages/common/src/api/tan-query/wallets/useDestinationUsdcAccountCheck.ts @@ -84,8 +84,8 @@ export const useDestinationUsdcAccountCheck = ( return { hasUsdcAccount: false, ataCreationFeeUsdc } }, - enabled: isValidAddress && options?.enabled !== false, staleTime: 60_000, - ...options + ...options, + enabled: isValidAddress && options?.enabled !== false }) } diff --git a/packages/common/src/api/tan-query/wallets/useUSDCBalance.ts b/packages/common/src/api/tan-query/wallets/useUSDCBalance.ts index c9458584992..f807c83ac87 100644 --- a/packages/common/src/api/tan-query/wallets/useUSDCBalance.ts +++ b/packages/common/src/api/tan-query/wallets/useUSDCBalance.ts @@ -93,12 +93,12 @@ export const useUSDCBalance = ({ throw e } }, - enabled: !!ethAddress, // TanStack Query's built-in polling - only poll when isPolling is true refetchInterval: isPolling ? pollingInterval : false, // Prevent refetching when window regains focus during polling to avoid conflicts refetchOnWindowFocus: !isPolling, - ...queryOptions + ...queryOptions, + enabled: !!ethAddress && queryOptions.enabled !== false }) // Map TanStack Query states to the Status enum for API compatibility diff --git a/packages/common/src/api/tan-query/wallets/useUserBalanceHistory.ts b/packages/common/src/api/tan-query/wallets/useUserBalanceHistory.ts index 53d40eb18fb..240c3361eb6 100644 --- a/packages/common/src/api/tan-query/wallets/useUserBalanceHistory.ts +++ b/packages/common/src/api/tan-query/wallets/useUserBalanceHistory.ts @@ -86,7 +86,7 @@ export const useUserBalanceHistory = ( })) ?? [] ) }, - enabled: options?.enabled !== false && !!params.userId, - ...options + ...options, + enabled: options?.enabled !== false && !!params.userId }) } diff --git a/packages/common/src/hooks/useInstanceVar.ts b/packages/common/src/hooks/useInstanceVar.ts index 167a46f2aa9..f2c72c04df3 100644 --- a/packages/common/src/hooks/useInstanceVar.ts +++ b/packages/common/src/hooks/useInstanceVar.ts @@ -19,8 +19,7 @@ export const useInstanceVar = ( const setter = useCallback( (updated: T | ((old: T) => T)) => { if (typeof updated === 'function') { - // @ts-ignore - ref.current = updated(ref.current) + ref.current = (updated as (old: T) => T)(ref.current) } else { ref.current = updated } diff --git a/packages/common/src/hooks/useMintRecovery.ts b/packages/common/src/hooks/useMintRecovery.ts index dca14e83160..d5864cb434d 100644 --- a/packages/common/src/hooks/useMintRecovery.ts +++ b/packages/common/src/hooks/useMintRecovery.ts @@ -47,10 +47,10 @@ export const useMintRecovery = ( return BigInt(0) } }, - enabled: options?.enabled !== false && !!mint, refetchInterval: 2000, // Poll every 2 seconds refetchOnWindowFocus: true, ...options, + enabled: options?.enabled !== false && !!mint, select: (balance: bigint) => { // If there's a balance, trigger recovery if (balance > BigInt(0)) { diff --git a/packages/common/src/hooks/useUserbank.ts b/packages/common/src/hooks/useUserbank.ts index a31d6aac357..3d18d07d623 100644 --- a/packages/common/src/hooks/useUserbank.ts +++ b/packages/common/src/hooks/useUserbank.ts @@ -36,8 +36,8 @@ export const useUserbank = ( }) return accountInfo.toString() }, - enabled: options?.enabled !== false && !!mint && !!wallet, - ...options + ...options, + enabled: options?.enabled !== false && !!mint && !!wallet }) return { diff --git a/packages/common/src/models/Activity.ts b/packages/common/src/models/Activity.ts deleted file mode 100644 index 10b4933c789..00000000000 --- a/packages/common/src/models/Activity.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { UserCollectionMetadata } from './Collection' -import { UserTrackMetadata } from './Track' - -export type CollectionActivity = { - timestamp: string - item_type: 'playlist' - item?: UserCollectionMetadata -} - -export type TrackActivity = { - timestamp: string - item_type: 'track' - item?: UserTrackMetadata -} - -export type RepostActivity = CollectionActivity | TrackActivity diff --git a/packages/common/src/models/Status.ts b/packages/common/src/models/Status.ts index b5dd1f352c5..82b1981a7dc 100644 --- a/packages/common/src/models/Status.ts +++ b/packages/common/src/models/Status.ts @@ -13,19 +13,3 @@ export enum PaginatedStatus { export function statusIsNotFinalized(status: Status) { return [Status.IDLE, Status.LOADING].includes(status) } - -/** - * Reduces an array of `Status` values to the least-complete status or `ERROR` if present. - */ -export function combineStatuses(statuses: Status[]) { - if (statuses.includes(Status.ERROR)) { - return Status.ERROR - } - if (statuses.includes(Status.LOADING)) { - return Status.LOADING - } - if (statuses.includes(Status.IDLE)) { - return Status.IDLE - } - return Status.SUCCESS -} diff --git a/packages/common/src/models/Track.ts b/packages/common/src/models/Track.ts index 6ad7a83a0ea..37b8edcef45 100644 --- a/packages/common/src/models/Track.ts +++ b/packages/common/src/models/Track.ts @@ -115,11 +115,6 @@ export const isContentTokenGated = ( ): gatedConditions is TokenGatedConditions => !!gatedConditions && 'token_gate' in (gatedConditions ?? {}) -export const isContentNftGated = ( - gatedConditions?: Nullable -): gatedConditions is NftGatedConditions => - !!gatedConditions && 'nft_collection' in (gatedConditions ?? {}) - export const isContentUSDCPurchaseGated = ( gatedConditions?: Nullable< AccessConditions | DeepOmit diff --git a/packages/common/src/services/Jupiter.ts b/packages/common/src/services/Jupiter.ts index 707477354da..7a98b1a4346 100644 --- a/packages/common/src/services/Jupiter.ts +++ b/packages/common/src/services/Jupiter.ts @@ -11,13 +11,6 @@ import { PublicKey, TransactionInstruction } from '@solana/web3.js' import { JupiterTokenSymbol } from '~/store/ui/shared/tokenConstants' import { convertBigIntToAmountObject, removeNullable } from '~/utils' -/** - * The error that gets returned if the slippage is exceeded - * @see https://github.com/jup-ag/jupiter-cpi/blob/5eb897736d294767200302efd070b16343d8c618/idl.json#L2910-L2913 - * @see https://station.jup.ag/docs/additional-topics/troubleshooting#swap-execution - */ -export const SLIPPAGE_TOLERANCE_EXCEEDED_ERROR = 6001 - export const DEFAULT_MAX_ACCOUNTS = 20 export const MAX_ALLOWED_ACCOUNTS = 64 const JUP_BASE_PATH = 'https://jup.audius.co/swap/v1' diff --git a/packages/common/src/services/audius-backend/AudiusBackend.ts b/packages/common/src/services/audius-backend/AudiusBackend.ts index 1082f1a70d0..0e6f040b39c 100644 --- a/packages/common/src/services/audius-backend/AudiusBackend.ts +++ b/packages/common/src/services/audius-backend/AudiusBackend.ts @@ -327,7 +327,7 @@ export const audiusBackend = ({ } /** - * Records a first-party internal notification campaign push open in Discovery (API). + * Records a first-party internal notification campaign push open via the validator node API. * Fire-and-forget; failures are logged and swallowed. */ async function reportNotificationCampaignPushOpen({ diff --git a/packages/common/src/services/oauth/formatSocialProfile.ts b/packages/common/src/services/oauth/formatSocialProfile.ts index 666baf21351..d95cf56dc49 100644 --- a/packages/common/src/services/oauth/formatSocialProfile.ts +++ b/packages/common/src/services/oauth/formatSocialProfile.ts @@ -1,4 +1,4 @@ -import { TikTokProfile, TwitterProfile } from '~/store/account/types' +import { TikTokProfile } from '~/store/account/types' export const MAX_HANDLE_LENGTH = 30 export const MAX_DISPLAY_NAME_LENGTH = 32 @@ -7,67 +7,6 @@ export const MAX_DISPLAY_NAME_LENGTH = 32 // indexer rejects bios longer than this with IndexingValidationError. export const MAX_BIO_LENGTH = 256 -export const formatTwitterProfile = async ( - twitterProfile: TwitterProfile, - resizeImage: ( - image: File, - maxWidth?: number, - square?: boolean, - key?: string - ) => Promise -) => { - const profileUrl = twitterProfile.profile_image_url_https.replace( - /_(normal|bigger|mini)/g, - '' - ) - const imageBlob = await fetch(profileUrl).then((r) => r.blob()) - const artworkFile = new File([imageBlob], 'Artwork', { - type: 'image/jpeg' - }) - const file = await resizeImage(artworkFile) - const url = URL.createObjectURL(file) - - let bannerUrl, bannerFile - if (twitterProfile.profile_banner_url) { - const bannerImageBlob = await fetch(twitterProfile.profile_banner_url).then( - (r) => r.blob() - ) - const bannerArtworkFile = new File([bannerImageBlob], 'Artwork', { - type: 'image/webp' - }) - bannerFile = await resizeImage(bannerArtworkFile, 2000, false) - bannerUrl = URL.createObjectURL(bannerFile) - } - // Truncate to MAX_HANDLE_LENGTH characters because we don't support longer handles. - // If the user is verifed, they won't be able to claim the status if - // the handle doesn't match, so just pass through. - let requiresUserReview = false - let handleTooLong = false - - if (twitterProfile.screen_name.length > MAX_HANDLE_LENGTH) { - requiresUserReview = true - handleTooLong = true - if (!twitterProfile.verified) { - twitterProfile.screen_name = twitterProfile.screen_name.slice( - 0, - MAX_HANDLE_LENGTH - ) - } - } - if (twitterProfile.name.length > MAX_DISPLAY_NAME_LENGTH) { - requiresUserReview = true - twitterProfile.name = twitterProfile.name.slice(0, MAX_DISPLAY_NAME_LENGTH) - } - - return { - profile: twitterProfile, - profileImage: { url, file }, - profileBanner: bannerUrl ? { url: bannerUrl, file: bannerFile } : undefined, - requiresUserReview, - handleTooLong - } -} - export const formatTikTokProfile = async ( tikTokProfile: TikTokProfile, resizeImage: ( diff --git a/packages/common/src/services/remote-config/defaults.ts b/packages/common/src/services/remote-config/defaults.ts index f204018b946..891e750c2c5 100644 --- a/packages/common/src/services/remote-config/defaults.ts +++ b/packages/common/src/services/remote-config/defaults.ts @@ -1,88 +1,53 @@ import { IntKeys, StringKeys, DoubleKeys, BooleanKeys } from './types' -const ETH_PROVIDER_URLS = process.env.REACT_APP_ETH_PROVIDER_URL || '' -export const DEFAULT_ENTRY_TTL = 1 /* min */ * 60 /* seconds */ * 1000 /* ms */ const DEFAULT_HANDLE_VERIFICATION_TIMEOUT_MILLIS = 5_000 export const MIN_USDC_PURCHASE_AMOUNT_CENTS = 100 export const MAX_USDC_PURCHASE_AMOUNT_CENTS = 150000 export const MIN_CONTENT_PRICE_CENTS = 100 export const MAX_CONTENT_PRICE_CENTS = 150000 -export const BUY_TOKEN_VIA_SOL_SLIPPAGE_BPS = 200 -export const BUY_SOL_VIA_TOKEN_SLIPPAGE_BPS = 200 export const remoteConfigIntDefaults: { [key in IntKeys]: number | null } = { - [IntKeys.IMAGE_QUICK_FETCH_TIMEOUT_MS]: 5000, - [IntKeys.IMAGE_QUICK_FETCH_PERFORMANCE_BATCH_SIZE]: 20, - [IntKeys.DASHBOARD_WALLET_BALANCE_POLLING_FREQ_MS]: 5000, [IntKeys.NOTIFICATION_POLLING_FREQ_MS]: 60 * 1000, - [IntKeys.SERVICE_MONITOR_HEALTH_CHECK_SAMPLE_RATE]: 0, - [IntKeys.SERVICE_MONITOR_REQUEST_SAMPLE_RATE]: 0, - [IntKeys.INSTAGRAM_HANDLE_CHECK_TIMEOUT]: 4000, [IntKeys.AUTOPLAY_LIMIT]: 10, [IntKeys.ATTESTATION_QUORUM_SIZE]: 0, - [IntKeys.MIN_AUDIO_SEND_AMOUNT]: 5, [IntKeys.CHALLENGE_REFRESH_INTERVAL_MS]: 15000, - [IntKeys.CHALLENGE_REFRESH_INTERVAL_AUDIO_PAGE_MS]: 5000, - [IntKeys.MANUAL_CLAIM_PROMPT_DELAY_MS]: 15000, [IntKeys.MAX_CLAIM_RETRIES]: 5, [IntKeys.CLIENT_ATTESTATION_PARALLELIZATION]: 20, [IntKeys.CHALLENGE_CLAIM_COMPLETION_POLL_FREQUENCY_MS]: 1000, [IntKeys.CHALLENGE_CLAIM_COMPLETION_POLL_TIMEOUT_MS]: 10000, - [IntKeys.MIN_AUDIO_PURCHASE_AMOUNT]: 5, - [IntKeys.MAX_AUDIO_PURCHASE_AMOUNT]: 999, [IntKeys.MIN_USDC_PURCHASE_AMOUNT_CENTS]: MIN_USDC_PURCHASE_AMOUNT_CENTS, [IntKeys.MAX_USDC_PURCHASE_AMOUNT_CENTS]: MAX_USDC_PURCHASE_AMOUNT_CENTS, [IntKeys.MIN_CONTENT_PRICE_CENTS]: MIN_CONTENT_PRICE_CENTS, [IntKeys.MAX_CONTENT_PRICE_CENTS]: MAX_CONTENT_PRICE_CENTS, [IntKeys.BUY_TOKEN_WALLET_POLL_DELAY_MS]: 1000, [IntKeys.BUY_TOKEN_WALLET_POLL_MAX_RETRIES]: 120, - [IntKeys.BUY_TOKEN_VIA_SOL_SLIPPAGE_BPS]: BUY_TOKEN_VIA_SOL_SLIPPAGE_BPS, - [IntKeys.BUY_AUDIO_SLIPPAGE]: 200, [IntKeys.GATED_TRACK_POLL_INTERVAL_MS]: 1000, - [IntKeys.CACHE_ENTRY_TTL]: DEFAULT_ENTRY_TTL, [IntKeys.HANDLE_VERIFICATION_TIMEOUT_MILLIS]: DEFAULT_HANDLE_VERIFICATION_TIMEOUT_MILLIS, [IntKeys.COINFLOW_MAXIMUM_CENTS]: 11000, [IntKeys.MIN_USDC_WITHDRAW_BALANCE_CENTS]: 500, - [IntKeys.BUY_SOL_WITH_TOKEN_SLIPPAGE_BPS]: BUY_SOL_VIA_TOKEN_SLIPPAGE_BPS, [IntKeys.CHAT_BLAST_TIER_REQUIREMENT]: 1 } export const remoteConfigStringDefaults: { [key in StringKeys]: string | null } = { - [StringKeys.AUDIUS_LOGO_VARIANT]: null, - [StringKeys.AUDIUS_LOGO_VARIANT_CLICK_TARGET]: null, [StringKeys.APP_WIDE_NOTICE_TEXT]: null, - [StringKeys.ETH_PROVIDER_URLS]: ETH_PROVIDER_URLS, - [StringKeys.CONTENT_BLOCK_LIST]: null, - [StringKeys.CONTENT_NODE_BLOCK_LIST]: null, - [StringKeys.INSTAGRAM_API_PROFILE_URL]: - 'https://instagram.com/$USERNAME$/?__a=1', - // Audius user id - [StringKeys.TRENDING_PLAYLIST_OMITTED_USER_IDS]: '51', [StringKeys.TRENDING_REWARD_IDS]: 'trending-track,trending-underground,top-api', [StringKeys.CHALLENGE_REWARD_IDS]: 'track-upload,invite-friends,mobile-app,listen-streak,profile-completion,first-playlist', [StringKeys.REWARDS_TWEET_ID_TRACKS]: '1374856377651187713', - [StringKeys.REWARDS_TWEET_ID_PLAYLISTS]: '1374856377651187713', [StringKeys.REWARDS_TWEET_ID_UNDERGROUND]: '1374856377651187713', - [StringKeys.TF]: null, - [StringKeys.TPF]: null, [StringKeys.UTF]: null, [StringKeys.TRENDING_EXPERIMENT]: null, - [StringKeys.PLAYLIST_TRENDING_EXPERIMENT]: null, - [StringKeys.UNDERGROUND_TRENDING_EXPERIMENT]: null, [StringKeys.ORACLE_ETH_ADDRESS]: null, [StringKeys.ORACLE_ENDPOINT]: null, [StringKeys.REWARDS_ATTESTATION_ENDPOINTS]: null, [StringKeys.MIN_APP_VERSION]: '1.0.0', - [StringKeys.BUY_AUDIO_PRESET_AMOUNTS]: '5,10,25,50,100', [StringKeys.AUDIO_FEATURES_DEGRADED_TEXT]: null, [StringKeys.PAY_EXTRA_PRESET_CENT_AMOUNTS]: '200,500,1000', - [StringKeys.EXPLORE_PREMIUM_DENIED_USERS]: '', [StringKeys.BUY_SELL_DENIED_COUNTRIES]: '' } diff --git a/packages/common/src/services/remote-config/feature-flags.ts b/packages/common/src/services/remote-config/feature-flags.ts index 8fe3d7d1037..83221f7fe4e 100644 --- a/packages/common/src/services/remote-config/feature-flags.ts +++ b/packages/common/src/services/remote-config/feature-flags.ts @@ -7,13 +7,10 @@ export enum FeatureFlags { VERIFY_HANDLE_WITH_INSTAGRAM = 'verify_handle_with_instagram', USDC_PURCHASES = 'usdc_purchases', FEATURE_FLAG_ACCESS = 'feature_flag_access', - IOS_USDC_PURCHASE_ENABLED = 'ios_usdc_purchase_enabled', BUY_WITH_COINFLOW = 'buy_with_coinflow', COINFLOW_OFFRAMP_ENABLED = 'coinflow_offramp_enabled', NETWORK_CUT_ENABLED = 'network_cut_enabled', FAST_REFERRAL = 'fast_referral', - REACT_QUERY_SYNC = 'react_query_sync', - COLLAPSED_EXPLORE_HEADER = 'collapsed_explore_header', LAUNCHPAD_VERIFICATION = 'launchpad_verification', FAN_CLUB_TEXT_POST_POSTING = 'fan_club_text_post_posting', QUEUE_NEW_FEATURE_BADGE = 'queue_new_feature_badge', @@ -41,13 +38,10 @@ export const flagDefaults: FlagDefaults = { [FeatureFlags.VERIFY_HANDLE_WITH_INSTAGRAM]: false, [FeatureFlags.USDC_PURCHASES]: true, [FeatureFlags.FEATURE_FLAG_ACCESS]: false, - [FeatureFlags.IOS_USDC_PURCHASE_ENABLED]: true, [FeatureFlags.BUY_WITH_COINFLOW]: false, [FeatureFlags.COINFLOW_OFFRAMP_ENABLED]: false, [FeatureFlags.NETWORK_CUT_ENABLED]: false, [FeatureFlags.FAST_REFERRAL]: false, - [FeatureFlags.REACT_QUERY_SYNC]: false, - [FeatureFlags.COLLAPSED_EXPLORE_HEADER]: false, [FeatureFlags.LAUNCHPAD_VERIFICATION]: true, [FeatureFlags.FAN_CLUB_TEXT_POST_POSTING]: false, [FeatureFlags.QUEUE_NEW_FEATURE_BADGE]: false, diff --git a/packages/common/src/services/remote-config/types.ts b/packages/common/src/services/remote-config/types.ts index e3feca2ce87..e6370f321b8 100644 --- a/packages/common/src/services/remote-config/types.ts +++ b/packages/common/src/services/remote-config/types.ts @@ -1,43 +1,9 @@ export enum IntKeys { - /** - * Duration (in ms) before we consider the fetch of an image against - * a primary creator node a failure - * @deprecated - */ - IMAGE_QUICK_FETCH_TIMEOUT_MS = 'IMAGE_QUICK_FETCH_TIMEOUT_MS', - /** - * The size at which a bundle of image loading performance metrics - * are sent to the analytics sever - */ - IMAGE_QUICK_FETCH_PERFORMANCE_BATCH_SIZE = 'IMAGE_QUICK_FETCH_PERFORMANCE_BATCH_SIZE', - - /** - * Frequency (in ms) to poll for user wallet balance on the client dashboard page - */ - DASHBOARD_WALLET_BALANCE_POLLING_FREQ_MS = 'DASHBOARD_WALLET_BALANCE_POLLING_FREQ_MS', - /** * Frequency (in ms) to poll for notifications from identity service. */ NOTIFICATION_POLLING_FREQ_MS = 'NOTIFICATION_POLLING_FREQ_MS', - /** - * Service monitoring health check analytics sample rate (int out of 100). A value of 50 - * means that half of health checks are recorded. - */ - SERVICE_MONITOR_HEALTH_CHECK_SAMPLE_RATE = 'SERVICE_MONITOR_HEALTH_CHECK_SAMPLE_RATE', - - /** - * Service monitoring request analytics sample rate (int out of 100). A value of 50 - * means that half of all requests are recorded. - */ - SERVICE_MONITOR_REQUEST_SAMPLE_RATE = 'SERVICE_MONITOR_REQUEST_SAMPLE_RATE', - - /** - * Instagram handle taken check timeout - */ - INSTAGRAM_HANDLE_CHECK_TIMEOUT = 'INSTAGRAM_HANDLE_CHECK_TIMEOUT', - /** * Number of random (recommended) tracks to fetch and add to the autoplay queue */ @@ -48,28 +14,11 @@ export enum IntKeys { */ ATTESTATION_QUORUM_SIZE = 'ATTESTATION_QUORUM_SIZE', - /** - * The minimum amount of AUDIO needed to be sent - */ - MIN_AUDIO_SEND_AMOUNT = 'MIN_AUDIO_SEND_AMOUNT', - /** * The refresh interval in milliseconds for user challenges */ CHALLENGE_REFRESH_INTERVAL_MS = 'CHALLENGE_REFRESH_INTERVAL_MS', - /** - * The refresh interval in milliseconds for user challenges when the user is on the $AUDIO page - */ - CHALLENGE_REFRESH_INTERVAL_AUDIO_PAGE_MS = 'CHALLENGE_REFRESH_INTERVAL_AUDIO_PAGE_MS', - - /** - * The time to wait after a challenge is marked completed before showing a claim reward prompt. - * Should be larger than both CHALLENGE_REFRESH_INTERVAL_MS and CHALLENGE_REFRESH_INTERVAL_AUDIO_PAGE_MS - * to allow additional polls to check for disbursement - */ - MANUAL_CLAIM_PROMPT_DELAY_MS = 'MANUAL_CLAIM_PROMPT_DELAY_MS', - /** * The maximum number of times to retry the claim method for a reward on the client * Note: Exponential backoff is used between retries @@ -83,25 +32,15 @@ export enum IntKeys { CLIENT_ATTESTATION_PARALLELIZATION = 'CLIENT_ATTESTATION_PARALLELIZATION', /** - * The time to spend polling DN for updates to the challenge being claimed to check if it has been indexed as complete. + * The time to spend polling the validator node for updates to the challenge being claimed to check if it has been indexed as complete. */ CHALLENGE_CLAIM_COMPLETION_POLL_TIMEOUT_MS = 'CHALLENGE_CLAIM_COMPLETION_POLL_TIMEOUT_MS', /** - * The interval between polls to DN for updates to the challenge being claimed to check if it has been indexed as complete. + * The interval between polls to the validator node for updates to the challenge being claimed to check if it has been indexed as complete. */ CHALLENGE_CLAIM_COMPLETION_POLL_FREQUENCY_MS = 'CHALLENGE_CLAIM_COMPLETION_POLL_FREQUENCY_MS', - /** - * Minimum AUDIO required to purchase in the BuyAudio modal - */ - MIN_AUDIO_PURCHASE_AMOUNT = 'MIN_AUDIO_PURCHASE_AMOUNT', - - /** - * Maximum AUDIO required to purchase in the BuyAudio modal - */ - MAX_AUDIO_PURCHASE_AMOUNT = 'MAX_AUDIO_PURCHASE_AMOUNT', - /** * Minimum price for purchasable content in cents */ @@ -132,41 +71,11 @@ export enum IntKeys { */ BUY_TOKEN_WALLET_POLL_MAX_RETRIES = 'BUY_TOKEN_WALLET_POLL_MAX_RETRIES', - /** - * The maximum amount the price of SOL is allowed to slip before - * the Jupiter swap from SOL to the requested token fails. - * Expressed in percentage basis points (1pt = 0.01%). - * - * Note: Increasing this number will increase how much SOL gets purchased, - * which improves the likelihood the purchase swaps successfully but with the - * downside of charging the user extra for their purchase. - * @see https://station.jup.ag/guides/jupiter-swap/price-impact-slippage-price-warning#slippage - * @see https://station.jup.ag/guides/general/faq#my-swap-fails-reporting-a-slippage-error-what-should-i-do - */ - BUY_TOKEN_VIA_SOL_SLIPPAGE_BPS = 'BUY_TOKEN_VIA_SOL_SLIPPAGE_BPS', - - /** - * The allowed slippage percentage/padding percentage for the BuyAudio Modal - */ - BUY_AUDIO_SLIPPAGE = 'BUY_AUDIO_SLIPPAGE', - - /** - * The maximum amount the price of SOL is allowed to slip before - * the Jupiter swap from USDC to SOL - * Expressed in percentage basis points (1pt = 0.01%). - */ - BUY_SOL_WITH_TOKEN_SLIPPAGE_BPS = 'BUY_SOL_WITH_TOKEN_SLIPPAGE_BPS', - /** * The interval in milliseconds between polls for gated tracks to check for access */ GATED_TRACK_POLL_INTERVAL_MS = 'GATED_TRACK_POLL_INTERVAL_MS', - /** - * Cache entry TTL to determine when a cache value should be overwritten with new instance - */ - CACHE_ENTRY_TTL = 'CACHE_ENTRY_TTL', - /** * Timeout for handle verification from socials. */ @@ -248,49 +157,11 @@ export enum DoubleKeys { } export enum StringKeys { - /** - * Logo variant to display in the top left of the app. - * `AUDIUS_LOGO_VARIANT_CLICK_TARGET` can be used to customize the - * url that is navigated to on click. - */ - AUDIUS_LOGO_VARIANT = 'AUDIUS_LOGO_VARIANT', - - /** - * Click target for the top left Audius logo in the app. - */ - AUDIUS_LOGO_VARIANT_CLICK_TARGET = 'AUDIUS_LOGO_VARIANT_CLICK_TARGET', - /** * Custom text for a top of page notice. */ APP_WIDE_NOTICE_TEXT = 'APP_WIDE_NOTICE_TEXT', - /** - * Custom eth provider urls to use for talking to main-net contracts - */ - ETH_PROVIDER_URLS = 'ETH_PROVIDER_URLS', - - /** - * Blocks content - */ - CONTENT_BLOCK_LIST = 'CONTENT_BLOCK_LIST', - - /** - * Blocks content nodes from selection - */ - CONTENT_NODE_BLOCK_LIST = 'CONTENT_NODE_BLOCK_LIST', - - /** - * Instagram Profile API url. Must contain $USERNAME$ - */ - INSTAGRAM_API_PROFILE_URL = 'INSTAGRAM_API_PROFILE_URL', - - /** - * User ids omitted from trending playlists (used to omit Audius from rewards). - * Comma-separated. - */ - TRENDING_PLAYLIST_OMITTED_USER_IDS = 'TRENDING_PLAYLIST_OMITTED_USER_IDS', - /** Rewards IDs as comma-separated array */ TRENDING_REWARD_IDS = 'TRENDING_REWARD_IDS', CHALLENGE_REWARD_IDS = 'CHALLENGE_REWARD_IDS', @@ -298,26 +169,15 @@ export enum StringKeys { /** Embedded tweet for trending rewards UI tracks */ REWARDS_TWEET_ID_TRACKS = 'REWARDS_TWEET_ID_TRACKS', - /** Embedded tweet for trending rewards UI playlists */ - REWARDS_TWEET_ID_PLAYLISTS = 'REWARDS_TWEET_ID_PLAYLISTS', - /** Embedded tweet for underground trending rewards UI */ REWARDS_TWEET_ID_UNDERGROUND = 'REWARDS_TWEET_ID_UNDERGROUND', - /** TF */ - TF = 'TF', - TPF = 'TPF', + /** UTF */ UTF = 'UTF', /** Trending experiment id */ TRENDING_EXPERIMENT = 'TRENDING_EXPERIMENT', - /** Underground trending experiment id */ - UNDERGROUND_TRENDING_EXPERIMENT = 'UNDERGROUND_TRENDING_EXPERIMENT', - - /** Playlist trending experiment id */ - PLAYLIST_TRENDING_EXPERIMENT = 'PLAYLIST_TRENDING_EXPERIMENT', - /** Ethereum address for oracle */ ORACLE_ETH_ADDRESS = 'ORACLE_ETH_ADDRESS', @@ -330,9 +190,6 @@ export enum StringKeys { /** Minimum required version for the app */ MIN_APP_VERSION = 'MIN_APP_VERSION', - /** Preset amounts for the Buy Audio modal */ - BUY_AUDIO_PRESET_AMOUNTS = 'BUY_AUDIO_PRESET_AMOUNTS', - /** 2-Letter ISO Country Codes of Denied Countries for Buy/Sell */ BUY_SELL_DENIED_COUNTRIES = 'BUY_SELL_DENIED_COUNTRIES', @@ -340,10 +197,7 @@ export enum StringKeys { AUDIO_FEATURES_DEGRADED_TEXT = 'AUDIO_FEATURES_DEGRADED_TEXT', /** Preset amounts for the Pay Extra feature in USDC purchases, specified in cents */ - PAY_EXTRA_PRESET_CENT_AMOUNTS = 'PAY_EXTRA_PRESET_CENT_AMOUNTS', - - /** Denylist of user ids for explore premium tracks page */ - EXPLORE_PREMIUM_DENIED_USERS = 'EXPLORE_PREMIUM_DENIED_USERS' + PAY_EXTRA_PRESET_CENT_AMOUNTS = 'PAY_EXTRA_PRESET_CENT_AMOUNTS' } export type AllRemoteConfigKeys = diff --git a/packages/common/src/services/wallet-client/WalletClient.ts b/packages/common/src/services/wallet-client/WalletClient.ts index 90e433e9088..783483125da 100644 --- a/packages/common/src/services/wallet-client/WalletClient.ts +++ b/packages/common/src/services/wallet-client/WalletClient.ts @@ -8,10 +8,7 @@ import { ID } from '~/models/Identifiers' import { SolanaWalletAddress } from '~/models/Wallet' import { isNullOrUndefined } from '~/utils/typeUtils' -import { - AudiusBackend, - pollForTokenBalanceChange -} from '../audius-backend' +import { AudiusBackend, pollForTokenBalanceChange } from '../audius-backend' import { Env } from '../env' export const MIN_TRANSFERRABLE_WEI = AUDIO('0.001').value diff --git a/packages/common/src/store/account/sagas.ts b/packages/common/src/store/account/sagas.ts index 6b0c0dec04d..05da465d7ff 100644 --- a/packages/common/src/store/account/sagas.ts +++ b/packages/common/src/store/account/sagas.ts @@ -368,7 +368,7 @@ function* setLocalStorageAccountAndUser( // Prefer cached user data (which may include recent optimistic mutations // like profile_type changes) over the server-fetched response. This prevents // setLocalStorageAccountAndUser from overwriting changes that haven't yet - // propagated to all discovery nodes. + // propagated to all validator nodes. const queryClient = yield* getContext('queryClient') const cachedUser = queryClient.getQueryData( getUserQueryKey(accountUser.user_id) diff --git a/packages/common/src/store/pages/audio-rewards/selectors.ts b/packages/common/src/store/pages/audio-rewards/selectors.ts index c256e695e24..8ed68abfeb7 100644 --- a/packages/common/src/store/pages/audio-rewards/selectors.ts +++ b/packages/common/src/store/pages/audio-rewards/selectors.ts @@ -62,4 +62,3 @@ export const getHCaptchaStatus = (state: CommonState) => export const getShowRewardClaimedToast = (state: CommonState) => state.pages.audioRewards.showRewardClaimedToast - diff --git a/packages/common/src/store/sagas.ts b/packages/common/src/store/sagas.ts index 4c5d72b2448..d59461d0d7b 100644 --- a/packages/common/src/store/sagas.ts +++ b/packages/common/src/store/sagas.ts @@ -1,11 +1,3 @@ -// import collectionsErrorSagas from 'common/store/cache/collections/errorSagas' -// import collectionsSagas from 'common/store/cache/collections/sagas' -// import cacheSagas from 'common/store/cache/sagas' -// import tracksSagas from 'common/store/cache/tracks/sagas' -// import usersSagas from 'common/store/cache/users/sagas' -// import recoveryEmailSagas from 'common/store/recovery-email/sagas' -// import signOutSagas from 'common/store/sign-out/sagas' - import niceModalBridgeSagas from '~/services/nice-modal-bridge/sagas' import { accountSagas } from '~/store/account' import { buyUSDCSagas } from '~/store/buy-usdc' @@ -49,53 +41,4 @@ export const sagas = (_ctx: CommonStoreContext) => ({ duplidateAddConfirmationModalUI: duplicateAddConfirmationModalUISagas, withdrawUSDC: withdrawUSDCSagas, niceModalBridge: niceModalBridgeSagas - - // signOut: signOutSagas - // recoveryEmail: recoveryEmailSagas - // TODO: - // pull in the following from web - // once AudiusBackend and dependencies are migrated - // ./pages/explore/exploreCollections/sagas.ts - // ./pages/explore/sagas.ts - // components/add-to-collection/store/sagas.ts - // components/share-sound-to-tiktok-modal/store/sagas.ts - // store/social/tracks/sagas.ts - // store/social/users/sagas.ts - // store/social/collections/sagas.ts - // pages/audio-rewards-page/store/sagas.ts - // store/wallet/sagas.ts - // store/lineup/sagas.js - // pages/feed/store/lineups/feed/sagas.js - // pages/feed/store/sagas.js - // pages/collection/store/lineups/tracks/sagas.js - // pages/collection/store/sagas.js - // pages/track/store/lineups/tracks/sagas.js - // pages/track/store/sagas.js - // store/ui/stemsUpload/sagas.ts - // pages/user-list/followers/sagas.ts - // pages/user-list/following/sagas.ts - // pages/user-list/reposts/sagas.ts - // pages/user-list/favorites/sagas.ts - // pages/user-list/mutuals/sagas.ts - // pages/search-explore-page/store/sagas.ts - // pages/search-explore-page/store/exploreCollections/sagas.ts - // store/solana/sagas.ts - // pages/trending-page/store/sagas.ts - // pages/trending-page/store/lineups/trending/sagas.ts - // pages/trending-underground-page/store/lineups/tracks/sagas.ts - // pages/trending-underground-page/store/sagas.ts - // pages/premium-tracks/sagas.ts - // pages/premium-tracks/lineups/tracks/sagas.ts - // pages/smart-collection/store/sagas.ts - // store/application/ui/theme/sagas.ts - // pages/search-page/store/sagas.ts - // pages/search-page/store/lineups/tracks/sagas.ts - // notifications/store/sagas.ts - // notifications/store/mobileSagas.ts - // pages/remixes-page/store/sagas.ts - // pages/remixes-page/store/lineups/tracks/sagas.ts - // - // pull in the following from web - // once the player and dependencies are migrated - // store/queue/sagas.ts }) diff --git a/packages/common/src/store/storeContext.ts b/packages/common/src/store/storeContext.ts index c5832f1db1d..ec02a16230d 100644 --- a/packages/common/src/store/storeContext.ts +++ b/packages/common/src/store/storeContext.ts @@ -7,11 +7,7 @@ import nacl from 'tweetnacl' import { AuthService, IdentityService } from '~/services' import { SolanaWalletService } from '~/services/solana' -import { - AllTrackingEvents, - AnalyticsEvent, - IdentifyTraits -} from '../models' +import { AllTrackingEvents, AnalyticsEvent, IdentifyTraits } from '../models' import { AudioPlayer } from '../services/audio-player' import { AudiusBackend } from '../services/audius-backend' import { Env } from '../services/env' diff --git a/packages/common/src/utils/challenges.ts b/packages/common/src/utils/challenges.ts index 44b562c05fd..6df422b2de7 100644 --- a/packages/common/src/utils/challenges.ts +++ b/packages/common/src/utils/challenges.ts @@ -442,17 +442,6 @@ export const isPlayCountChallenge = ( ) } -export const isAudioMatchingChallenge = ( - challenge: ChallengeRewardID -): challenge is - | ChallengeName.AudioMatchingSell - | ChallengeName.AudioMatchingBuy => { - return ( - challenge === ChallengeName.AudioMatchingSell || - challenge === ChallengeName.AudioMatchingBuy - ) -} - /** Returns true if the challenge is not a cooldown challenge by checking * whether it has `cooldown_days` defined and whether the challenge has been * created for more than `cooldown_days` days. diff --git a/packages/common/src/utils/commonPasswordCheck.ts b/packages/common/src/utils/commonPasswordCheck.ts index 65baab4d005..df108d8188e 100644 --- a/packages/common/src/utils/commonPasswordCheck.ts +++ b/packages/common/src/utils/commonPasswordCheck.ts @@ -1,4 +1,5 @@ -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) +const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)) const HIBP_URL = 'https://api.pwnedpasswords.com/range/' @@ -24,8 +25,7 @@ export const commonPasswordCheck = async ( ]) if (result) { - // @ts-ignore - const text = (await result?.text()) as string + const text = await result.text() const hashArr = text.split(/\s+/g).map((s) => s.slice(0, s.indexOf(':'))) // If there is no match, return false diff --git a/packages/common/src/utils/contentTypeUtils.ts b/packages/common/src/utils/contentTypeUtils.ts index 8e8445dfeeb..0e09df13ea0 100644 --- a/packages/common/src/utils/contentTypeUtils.ts +++ b/packages/common/src/utils/contentTypeUtils.ts @@ -8,11 +8,6 @@ export const isContentPartialTrack = >( content?: Nullable> ): content is T => !!content && 'track_id' in content -/** Typeguard for determining if content is a Collection but can return as a subset of the Collection type */ -export const isContentPartialCollection = >( - content?: Nullable> -): content is T => !!content && 'playlist_id' in content - /** Typeguard for determiniing if content is a Track */ export const isContentTrack = ( content?: Nullable> diff --git a/packages/common/src/utils/formatTrackTimestamp.ts b/packages/common/src/utils/formatTrackTimestamp.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/common/src/utils/formatUtil.ts b/packages/common/src/utils/formatUtil.ts index da01390e575..c0162ea440a 100644 --- a/packages/common/src/utils/formatUtil.ts +++ b/packages/common/src/utils/formatUtil.ts @@ -92,10 +92,6 @@ export const formatNumberCommas = (num: number | string) => { ) } -export const trimRightZeros = (number: string) => { - return number.replace(/(\d)0+$/gm, '$1') -} - export const checkOnlyNumeric = (number: string) => { const reg = /^\d+$/ return reg.test(number) @@ -112,10 +108,6 @@ export const checkOnlyWeiFloat = (number: string) => { return true } -export const checkWeiNumber = (number: string) => { - return checkOnlyNumeric(number) || checkOnlyWeiFloat(number) -} - /** Capitalizes the given input string */ export const formatCapitalizeString = (word: string) => { const lowerCase = word.toLowerCase() @@ -184,12 +176,6 @@ export const getHash = (str: string) => }, 0) ).toString(36) -/** - * Formats a number to be a double digit - */ -export const formatDoubleDigit = (value: number) => - value.toString().padStart(2, '0') - /** * Formats a ticker to be uppercase and trimmed */ diff --git a/packages/common/src/utils/reducer.ts b/packages/common/src/utils/reducer.ts index 493442f3ea4..29534a30005 100644 --- a/packages/common/src/utils/reducer.ts +++ b/packages/common/src/utils/reducer.ts @@ -1,14 +1,5 @@ -// TODO(nkang): Note - this is copied from web/utils/reducer.ts. Uncomment the commented code if needed for the common store later on. - -// import { UseSelectorHook } from './selectorHook' -// import { useSelector as untypedUseSelector } from 'react-redux' import { Action } from 'redux' -/** - * Typed version of Redux useSelector - */ -// export const useSelector: UseSelectorHook = untypedUseSelector - /** * Handy util for generating a reducer function from an actionmap. * diff --git a/packages/common/src/utils/sagaHelpers.ts b/packages/common/src/utils/sagaHelpers.ts index 1b48b68d5c9..721dc6e4c9c 100644 --- a/packages/common/src/utils/sagaHelpers.ts +++ b/packages/common/src/utils/sagaHelpers.ts @@ -1,19 +1,9 @@ /** Helper Sagas */ import { QueryClient } from '@tanstack/react-query' -import { eventChannel, END, EventChannel } from 'redux-saga' -import { ActionPattern, GetContextEffect } from 'redux-saga/effects' -import { - all, - call, - delay, - getContext, - put, - select, - spawn, - take, - takeEvery -} from 'typed-redux-saga' +import { EventChannel } from 'redux-saga' +import { GetContextEffect } from 'redux-saga/effects' +import { call, getContext, put, select, take } from 'typed-redux-saga' import { Action } from 'typesafe-actions' // this import needs to be very specific for SSR bundlesize @@ -27,22 +17,6 @@ const messages = { somethingWrong: 'Something went wrong' } -/** - * Calls the provided array of calls in batches with delayMs milliseconds between each batch. - */ -export function* batchYield( - calls: Generator[], - batchSize: number, - delayMs: number -) { - let remainingCalls = calls - while (remainingCalls.length > 0) { - yield all(remainingCalls.slice(0, batchSize)) - remainingCalls = remainingCalls.slice(batchSize) - yield delay(delayMs) - } -} - /** * Takes a channel that yields the value of a Redux action, waits for it to yield and then * dispatches the action. @@ -57,14 +31,6 @@ export function* actionChannelDispatcher(channel: EventChannel) { } } -export function* channelCanceller( - channel: EventChannel>, - action: ActionPattern> -) { - yield take(action) - channel.close() -} - /** * Waits for the selector to return a truthy value. * @param {function} selector @@ -88,28 +54,6 @@ export function* waitForValue( return value } -function doEveryImpl(millis: number, times: number | null) { - return eventChannel((emitter) => { - // Emit once at the start - emitter(times || true) - - // Emit once every millis - const iv = setInterval(() => { - if (times !== null) { - times -= 1 - } - if (times === null || times > 0) { - emitter(times || true) - } else { - emitter(END) - } - }, millis) - return () => { - clearInterval(iv) - } - }) -} - /** * Waits for a query function to return a truthy value. * Similar to waitForValue but for tanstack query data. @@ -133,24 +77,6 @@ export function* waitForQueryValue( return value as TResult } -/** - * Repeatedly calls a saga/async function on an interval for up to a set number of times. - * @param {number} millis - * @param {function *} fn - * @param {number?} times - */ -export function* doEvery( - millis: number, - fn: (...args: any) => any, - times: number | null = null -) { - const chan: EventChannel = yield call(doEveryImpl, millis, times) - yield spawn(function* () { - yield takeEvery(chan, fn) - }) - return chan -} - /** * Waits for account to finish loading, whether it fails or succeeds. */ diff --git a/packages/common/src/utils/selectorHelpers.ts b/packages/common/src/utils/selectorHelpers.ts index 81dde8f7012..b10dc4927ed 100644 --- a/packages/common/src/utils/selectorHelpers.ts +++ b/packages/common/src/utils/selectorHelpers.ts @@ -1,23 +1,6 @@ import { isEqual } from 'lodash' import { createSelectorCreator, defaultMemoize } from 'reselect' -export const shallowCompare = (a: T, b: T) => - a && - b && - Object.keys(a).length === Object.keys(b).length && - Object.keys(a).every( - (key) => Object.prototype.hasOwnProperty.call(b, key) && a[key] === b[key] - ) - -export const areSetsEqual = (a: Set, b: Set) => - a.size === b.size && [...a].every((value) => b.has(value)) - -// A selector creator that memoizes based on a shallow object comparison. -export const createShallowSelector = createSelectorCreator( - defaultMemoize, - shallowCompare -) - export const createDeepEqualSelector = createSelectorCreator( defaultMemoize, isEqual diff --git a/packages/common/src/utils/socialLinks.ts b/packages/common/src/utils/socialLinks.ts index c87870958e0..7c0fc1e0686 100644 --- a/packages/common/src/utils/socialLinks.ts +++ b/packages/common/src/utils/socialLinks.ts @@ -104,10 +104,3 @@ export const getUserSocialLinks = (user?: UserSocialSource | null) => { return links.slice(0, 4) } - -export const socialLinksUtils = { - SOCIAL_BASE_URLS, - getUserSocialLinks, - sanitizeSocialHandle, - sanitizeWebsiteUrl -} diff --git a/packages/common/src/utils/stringUtils.ts b/packages/common/src/utils/stringUtils.ts index baea3534f20..2365ab86566 100644 --- a/packages/common/src/utils/stringUtils.ts +++ b/packages/common/src/utils/stringUtils.ts @@ -1,44 +1,5 @@ import { getTokenDecimalPlaces } from './decimal' -/** - * Converts a provided URL params object to a query string - */ -export const paramsToQueryString = (params: { - [key: string]: string | string[] -}) => { - if (!params) return '' - return Object.entries(params) - .filter((p) => p[1] !== undefined && p[1] !== null) - .map((p) => { - if (Array.isArray(p[1])) { - // Otherwise, join in the form of - // ?key=val1&key=val2&key=val3... - return p[1].map((val) => `${p[0]}=${encodeURIComponent(val)}`).join('&') - } - return `${p[0]}=${encodeURIComponent(p[1]!)}` - }) - .join('&') -} - -/** - * Track permalinks have the following format: '//' - */ -export const parseTrackRouteFromPermalink = (permalink: string) => { - const [, handle, slug] = permalink.split('/') - return { slug, trackId: null, handle } -} - -/** - * Playlist permalinks have the following format: '//playlist/' - * - * @param permalink - * @returns playlist id - */ -export const parsePlaylistIdFromPermalink = (permalink: string) => { - const playlistNameWithId = permalink?.split('/').slice(-1)[0] ?? '' - return parseInt(playlistNameWithId.split('-').slice(-1)[0]) -} - /** Takes a list of comma-separated strings, splits and parses them into numbers */ export const parseIntList = (str: string) => { return str.split(',').map((s) => Number.parseInt(s)) diff --git a/packages/common/src/utils/uid.ts b/packages/common/src/utils/uid.ts index de2dd4dacc1..32c621d4e07 100644 --- a/packages/common/src/utils/uid.ts +++ b/packages/common/src/utils/uid.ts @@ -66,39 +66,6 @@ export class Uid { } } -export function makeUids( - kinds: string[], - ids: (number | string)[], - source?: string -) { - if (Array.isArray(kinds)) { - // Multiple kinds in the ids array. - const totals: Record = {} - const counts: Record = {} - ids.forEach((id, i) => { - const key = `${kinds[i]}${id}` - if (key in totals) totals[key] += 1 - else totals[key] = 0 - counts[i] = totals[key] - }) - const uids = ids.map((id, i) => Uid.build(kinds[i], id, source, counts[i])) - return uids.map((uid) => uid.toString()) - } else { - // Single kind. - const kind = kinds - - const totals: Record = {} - const counts: Record = {} - ids.forEach((id, i) => { - if (id in totals) totals[id] += 1 - else totals[id] = 0 - counts[i] = totals[id] - }) - const uids = ids.map((id, i) => Uid.build(kind, id, source, counts[i])) - return uids.map((uid) => uid.toString()) - } -} - export function makeUid(kind: string, id: ID, source?: string) { const uid = Uid.build(kind, id, source) return uid.toString() @@ -132,12 +99,6 @@ export const makeKindId = (kind: string, id: ID | string | undefined) => */ export const getIdFromKindId = (kindId: string) => kindId.split('-')[1] -/** - * Gets the kind from a kind id - * @param kindId - */ -export const getKindFromKindId = (kindId: string) => kindId.split('-')[0] - /** * Gets the numeric id from a uid * @param uid @@ -147,19 +108,13 @@ export const getNumericIdFromUid = (uid: string) => export const uuid = () => { // https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript/873856#873856 - const s = [] + const s: string[] = [] const hexDigits = '0123456789abcdef' - // all the ts-ignores are needed so the mobile package types things correctly - // remove when upgrading to latest typescript for (let i = 0; i < 36; i++) { - // @ts-ignore s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) } - // @ts-ignore s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010 - // @ts-ignore - s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01 - // @ts-ignore + s[19] = hexDigits.substr((parseInt(s[19], 16) & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01 s[8] = s[13] = s[18] = s[23] = '-' const uuid = s.join('') diff --git a/packages/common/src/utils/urlUtils.ts b/packages/common/src/utils/urlUtils.ts index 0517cc54b30..98da161cec0 100644 --- a/packages/common/src/utils/urlUtils.ts +++ b/packages/common/src/utils/urlUtils.ts @@ -19,8 +19,6 @@ export const isInternalAudiusUrl = (url: string) => url.startsWith('/') || (isAudiusUrl(url) && !externalAudiusLinks.some((externalLink) => externalLink.includes(url))) -export const isExternalAudiusUrl = (url: string) => - new RegExp(audiusUrlRegex).test(url) export const getPathFromAudiusUrl = (url: string) => url.startsWith('/') ? url diff --git a/packages/harmony/src/components/button/FilterButton/FilterButtonKeyHandler.tsx b/packages/harmony/src/components/button/FilterButton/FilterButtonKeyHandler.tsx deleted file mode 100644 index b5aff789014..00000000000 --- a/packages/harmony/src/components/button/FilterButton/FilterButtonKeyHandler.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { RefObject, useState, ReactNode, useEffect } from 'react' - -import { FilterButtonOptionType } from './types' - -type FilterButtonKeyHandlerProps = { - children: (activeValue: Value | null) => ReactNode - disabled?: boolean - onChange: (value: Value) => void - optionRefs: RefObject - options: FilterButtonOptionType[] - scrollRef: RefObject -} - -/** - * Handles key events for the popup inside the FilterButton component - * - * Calls the `children` function with the currently active value - */ -export const FilterButtonKeyHandler = ( - props: FilterButtonKeyHandlerProps -) => { - const { disabled, options, onChange, optionRefs, scrollRef, children } = props - const [activeIndex, setActiveIndex] = useState(null) - const activeValue = activeIndex !== null ? options[activeIndex]?.value : null - - useEffect(() => { - const adjustScrollPosition = (newIndex: number | null) => { - if (newIndex !== null) { - if (optionRefs.current) { - optionRefs.current[newIndex].scrollIntoView({ - behavior: 'smooth', - block: 'start' - }) - } - } else { - scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' }) - } - } - - const handleKeyDown = (event: KeyboardEvent) => { - if (disabled) { - return - } - - switch (event.key) { - case 'ArrowUp': - event.stopPropagation() - event.preventDefault() - setActiveIndex((prevIndex) => { - const getNewIndex = () => { - if (prevIndex === null) { - return options.length - 1 - } - - return prevIndex > 0 ? prevIndex - 1 : null - } - - const newIndex = getNewIndex() - adjustScrollPosition(newIndex) - - return newIndex - }) - break - case 'ArrowDown': - event.stopPropagation() - event.preventDefault() - setActiveIndex((prevIndex) => { - const getNewIndex = () => { - if (prevIndex === null) { - return 0 - } - - return prevIndex < options.length - 1 ? prevIndex + 1 : null - } - - const newIndex = getNewIndex() - adjustScrollPosition(newIndex) - - return newIndex - }) - break - case 'Enter': - event.stopPropagation() - event.preventDefault() - if (activeIndex !== null && options[activeIndex]) { - onChange(options[activeIndex].value) - } - break - default: - break - } - } - - window.addEventListener('keydown', handleKeyDown) - - return () => { - window.removeEventListener('keydown', handleKeyDown) - } - }, [disabled, options, activeIndex, scrollRef, optionRefs, onChange]) - - return <>{children(activeValue)} -} diff --git a/packages/harmony/src/components/button/FollowButton/FollowButton.tsx b/packages/harmony/src/components/button/FollowButton/FollowButton.tsx index 9892298539d..ea6d1d203a5 100644 --- a/packages/harmony/src/components/button/FollowButton/FollowButton.tsx +++ b/packages/harmony/src/components/button/FollowButton/FollowButton.tsx @@ -88,8 +88,6 @@ export const FollowButton = forwardRef( setIsPressing(false) }, []) - useEffect(() => {}, [value]) - const handleClick = useCallback(() => { if (value) { onUnfollow?.() diff --git a/packages/harmony/src/components/modal/Modal.tsx b/packages/harmony/src/components/modal/Modal.tsx index 51c75bbe785..eb38a7cb4a4 100644 --- a/packages/harmony/src/components/modal/Modal.tsx +++ b/packages/harmony/src/components/modal/Modal.tsx @@ -228,22 +228,21 @@ export const Modal = forwardRef(function Modal( }, [isOpenProp, modalState]) useEffect(() => { - if (isOpen) { - if (bgModal) { - bgModal.classList.add('bgModalVisible') - } + if (!isOpen) { + if (bgModal) bgModal.classList.remove('bgModalVisible') + return + } - // Need to prevent safari iOS bounce - // overscroll effect by intercepting - // touchmove events. - if (modalRoot) modalRoot.addEventListener('touchmove', onTouchMove) - return () => { - if (bgModal) bgModal.classList.remove('bgModalVisible') - } + if (bgModal) bgModal.classList.add('bgModalVisible') + + // Need to prevent safari iOS bounce + // overscroll effect by intercepting + // touchmove events. + if (modalRoot) modalRoot.addEventListener('touchmove', onTouchMove) + return () => { + if (bgModal) bgModal.classList.remove('bgModalVisible') + if (modalRoot) modalRoot.removeEventListener('touchmove', onTouchMove) } - if (bgModal) bgModal.classList.remove('bgModalVisible') - if (modalRoot) modalRoot.removeEventListener('touchmove', onTouchMove) - return () => {} }, [isOpen, bgModal, onTouchMove, modalRoot]) const transition = useTransition(isOpenProp, null, { diff --git a/packages/harmony/src/components/modal/findAncestor.ts b/packages/harmony/src/components/modal/findAncestor.ts index 96cf61c8e94..e6a303cf3e1 100644 --- a/packages/harmony/src/components/modal/findAncestor.ts +++ b/packages/harmony/src/components/modal/findAncestor.ts @@ -1,17 +1,9 @@ /** - * Legacy browser support for `Element.closest` + * Finds the closest ancestor of `el` (including `el` itself) matching the + * given selector, or null if there is none. * @param el * @param selector query selector */ export const findAncestor = (el: Element | null, selector: string) => { - if (el !== null && el.closest) { - return el.closest(selector) - } - // Fall back to just looping back through parents - while ( - (el = el?.parentElement ?? null) && - // @ts-ignore - !(el.matches || el.matchesSelector).call(el, selector) - ); - return el + return el?.closest(selector) ?? null } diff --git a/packages/harmony/src/components/scrubber/Slider.tsx b/packages/harmony/src/components/scrubber/Slider.tsx index 24a2fa1d960..5b35d44ff40 100644 --- a/packages/harmony/src/components/scrubber/Slider.tsx +++ b/packages/harmony/src/components/scrubber/Slider.tsx @@ -228,6 +228,24 @@ export const Slider = ({ setDragPercentTouch(e) } + // Detach any in-progress drag listeners if unmounted mid-drag + useEffect(() => { + return () => { + if (mouseMoveRef.current) { + document.removeEventListener('mousemove', mouseMoveRef.current) + } + if (mouseUpRef.current) { + document.removeEventListener('mouseup', mouseUpRef.current) + } + if (touchMoveRef.current) { + document.removeEventListener('touchmove', touchMoveRef.current) + } + if (touchEndRef.current) { + document.removeEventListener('touchend', touchEndRef.current) + } + } + }, []) + // Watch interactions to the scrubber and call to animate useEffect(() => { if (!dragPercent.current) { diff --git a/packages/mobile/src/components/audio/Airplay.tsx b/packages/mobile/src/components/audio/Airplay.tsx index fea0407bbbf..5daf161c868 100644 --- a/packages/mobile/src/components/audio/Airplay.tsx +++ b/packages/mobile/src/components/audio/Airplay.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef } from 'react' import { castActions } from '@audius/common/store' +import type { EmitterSubscription } from 'react-native' import { requireNativeComponent, NativeEventEmitter, @@ -36,7 +37,7 @@ export const useAirplay = () => { * codebase. */ const Airplay = () => { - const listenerRef = useRef(null) + const listenerRef = useRef(null) const dispatch = useDispatch() useEffect(() => { @@ -68,7 +69,7 @@ const Airplay = () => { ) return () => { - listenerRef.current?.stop?.() + listenerRef.current?.remove() } }, [listenerRef, dispatch]) diff --git a/packages/mobile/src/components/audio/AudioPlayer.tsx b/packages/mobile/src/components/audio/AudioPlayer.tsx index 0b37caa463f..ef18d146e89 100644 --- a/packages/mobile/src/components/audio/AudioPlayer.tsx +++ b/packages/mobile/src/components/audio/AudioPlayer.tsx @@ -59,6 +59,7 @@ import { import { useChromecast } from './GoogleCast' import { addTracksAroundCurrent } from './addTracksAroundCurrent' +import { RESTART_THRESHOLD_SEC, SKIP_DURATION_SEC } from './constants' import { useSavePodcastProgress } from './useSavePodcastProgress' export const DEFAULT_IMAGE_URL = @@ -90,9 +91,6 @@ const { const { getIsReachable } = reachabilitySelectors const { getNftAccessSignatureMap } = gatedContentSelectors -// TODO: These constants are the same in now playing drawer. Move them to shared location -const SKIP_DURATION_SEC = 15 -const RESTART_THRESHOLD_SEC = 3 const RECORD_LISTEN_SECONDS = 1 const TRACK_END_BUFFER = 2 diff --git a/packages/mobile/src/components/audio/GoogleCast.tsx b/packages/mobile/src/components/audio/GoogleCast.tsx index 251662b2dd1..20fd40f1fbe 100644 --- a/packages/mobile/src/components/audio/GoogleCast.tsx +++ b/packages/mobile/src/components/audio/GoogleCast.tsx @@ -18,7 +18,7 @@ import { } from 'react-native-google-cast' import TrackPlayer, { Event } from 'react-native-track-player' import { useDispatch, useSelector } from 'react-redux' -import { useAsync, usePrevious } from 'react-use' +import { usePrevious } from 'react-use' const { setIsCasting } = castActions const { getPlaying, getSeek, getCounter } = playbackSelectors @@ -118,16 +118,23 @@ export const useChromecast = () => { }, [prevTrack, track, counter, internalCounter, setInternalCounter]) // Load media when the cast connects - useAsync(async () => { - if (castState === CastState.CONNECTED) { + useEffect(() => { + if (castState !== CastState.CONNECTED) return + let cancelled = false + let activeTrackSubscription: + | ReturnType + | undefined + + const load = async () => { const { position: currentPosition } = await TrackPlayer.getProgress() const currentPlaying = await TrackPlayer.getActiveTrack() + if (cancelled) return if (currentPlaying) { loadCast(track, currentPosition, currentPlaying?.url) } else { // If nothing is currently playing, listen for something to start // playing and then load it to cast. - TrackPlayer.addEventListener( + activeTrackSubscription = TrackPlayer.addEventListener( Event.PlaybackActiveTrackChanged, async () => { const { position: currentPosition } = @@ -138,6 +145,14 @@ export const useChromecast = () => { ) } } + load().catch((e) => { + console.error('Failed to load media onto cast device', e) + }) + + return () => { + cancelled = true + activeTrackSubscription?.remove() + } }, [castState, track, loadCast]) // Play & pause the cast device diff --git a/packages/mobile/src/components/audio/constants.ts b/packages/mobile/src/components/audio/constants.ts new file mode 100644 index 00000000000..a0847618509 --- /dev/null +++ b/packages/mobile/src/components/audio/constants.ts @@ -0,0 +1,8 @@ +/** How far the jump forward / backward controls seek, in seconds */ +export const SKIP_DURATION_SEC = 15 + +/** + * If the current position is past this many seconds, "previous" restarts the + * current track instead of going to the previous one + */ +export const RESTART_THRESHOLD_SEC = 3 diff --git a/packages/mobile/src/components/comments/CommentActionBar.tsx b/packages/mobile/src/components/comments/CommentActionBar.tsx index 4f87a26b0b0..a8a75345495 100644 --- a/packages/mobile/src/components/comments/CommentActionBar.tsx +++ b/packages/mobile/src/components/comments/CommentActionBar.tsx @@ -24,7 +24,7 @@ export const CommentActionBar = (props: CommentActionBarProps) => { const { isCurrentUserReacted, reactCount, id: commentId } = comment const [reactToComment] = useReactToComment() - const [reactionState, setReactionState] = useState(isCurrentUserReacted) // TODO: need to pull starting value from metadata + const [reactionState, setReactionState] = useState(isCurrentUserReacted) const { setReplyingAndEditingState } = useCurrentCommentSection() const handleCommentReact = useCallback(() => { diff --git a/packages/mobile/src/components/contest-card/ContestCard.tsx b/packages/mobile/src/components/contest-card/ContestCard.tsx index c524cd46864..4c1f9eb869d 100644 --- a/packages/mobile/src/components/contest-card/ContestCard.tsx +++ b/packages/mobile/src/components/contest-card/ContestCard.tsx @@ -282,7 +282,7 @@ export const ContestCard = (props: ContestCardProps) => { } // Skip contests hosted by deactivated users — same filter as the web - // ContestCard. The discovery endpoint can still surface tombstoned + // ContestCard. The validator node API can still surface tombstoned // hosts (the fake "Audius" / life-audius-airdrop accounts) and // rendering them on explore was the QA bug we needed to suppress. if (user.is_deactivated) { diff --git a/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx b/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx index cd355cd834a..b8ea8211175 100644 --- a/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx +++ b/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx @@ -30,6 +30,10 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context' import TrackPlayer from 'react-native-track-player' import { useDispatch, useSelector } from 'react-redux' +import { + RESTART_THRESHOLD_SEC, + SKIP_DURATION_SEC +} from 'app/components/audio/constants' import { BOTTOM_BAR_HEIGHT } from 'app/components/bottom-tab-bar' import { useCommentDrawer } from 'app/components/comments/CommentDrawerContext' import Drawer, { @@ -59,8 +63,6 @@ const { seekTo: seek, reset, next, previous } = playbackActions const { getPlaying, getTrackId, getCounter, getBuffering } = playbackSelectors const STATUS_BAR_FADE_CUTOFF = 0.6 -const SKIP_DURATION_SEC = 15 -const RESTART_THRESHOLD_SEC = 3 // If the top screen inset is greater than this, // the status bar will be hidden when the drawer is open diff --git a/packages/mobile/src/haptics.ts b/packages/mobile/src/haptics.ts index 23bc3a50b16..f3ffdfa9f6a 100644 --- a/packages/mobile/src/haptics.ts +++ b/packages/mobile/src/haptics.ts @@ -8,7 +8,6 @@ const options = { export const light = () => { if (Platform.OS === 'android') { - // @ts-ignore ReactNativeHapticFeedback.trigger('keyboardPress', options) } else { ReactNativeHapticFeedback.trigger('impactLight', options) @@ -18,7 +17,6 @@ export const light = () => { export const medium = () => { if (Platform.OS === 'android') { // This should be updated to match ios impactMedium - // @ts-ignore ReactNativeHapticFeedback.trigger('keyboardPress', options) } else { ReactNativeHapticFeedback.trigger('impactMedium', options) diff --git a/packages/mobile/src/hooks/useEnterContest.ts b/packages/mobile/src/hooks/useEnterContest.ts index fb1df1cba70..7a62b9ebd91 100644 --- a/packages/mobile/src/hooks/useEnterContest.ts +++ b/packages/mobile/src/hooks/useEnterContest.ts @@ -22,16 +22,22 @@ export const useEnterContest = (trackId: ID | undefined) => { let artwork: { url: string; file: any } | undefined const imageUrl = originalTrack?.artwork?.[SquareSizes.SIZE_480_BY_480] ?? '' if (imageUrl) { - const response = await fetch(imageUrl) - const blob = await response.blob() - const file = new File([blob], 'image.jpg', { type: blob.type }) - artwork = { - url: imageUrl, - file: { - // @ts-ignore: KJ - _data is on the file for some reason - ...file._data, - uri: imageUrl + try { + const response = await fetch(imageUrl) + const blob = await response.blob() + const file = new File([blob], 'image.jpg', { type: blob.type }) + artwork = { + url: imageUrl, + file: { + // @ts-ignore: KJ - _data is on the file for some reason + ...file._data, + uri: imageUrl + } } + } catch (e) { + // Artwork is a convenience pre-fill; continue to the upload flow + // without it rather than leaving the button doing nothing. + console.warn('Failed to pre-fill contest artwork', e) } } diff --git a/packages/mobile/src/notifications.ts b/packages/mobile/src/notifications.ts index 1ca54ee4de2..cce06661294 100644 --- a/packages/mobile/src/notifications.ts +++ b/packages/mobile/src/notifications.ts @@ -80,7 +80,7 @@ type Token = { type NotificationNavigation = { navigate: (notification: any) => void } -/** First-party Discovery campaign open — mobile remote push opens only. */ +/** Reports a first-party campaign open to the API — mobile remote push opens only. */ async function reportNotificationCampaignPushOpen( campaignId: string ): Promise { diff --git a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx index a67a8700b8f..2fc6e2f6e22 100644 --- a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx @@ -391,10 +391,7 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { - // @ts-ignore hard to correctly type navigation params (PAY-1141) - params?.chatId - } + getId={({ params }) => params?.chatId} options={{ ...screenOptions, fullScreenGestureEnabled: false }} /> diff --git a/packages/mobile/src/screens/chat-screen/ChatScreen.tsx b/packages/mobile/src/screens/chat-screen/ChatScreen.tsx index bb1a7706d0f..25c7bb336c1 100644 --- a/packages/mobile/src/screens/chat-screen/ChatScreen.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatScreen.tsx @@ -216,7 +216,7 @@ const useGetLatestMessages = ( // Remove listener on unmount return () => handle.remove() - }) + }, [fetchLatestMessagesCallback]) } export const ChatScreen = () => { diff --git a/packages/mobile/src/screens/edit-track-screen/EditTrackForm.tsx b/packages/mobile/src/screens/edit-track-screen/EditTrackForm.tsx index ae0a61a8152..61ac5d273de 100644 --- a/packages/mobile/src/screens/edit-track-screen/EditTrackForm.tsx +++ b/packages/mobile/src/screens/edit-track-screen/EditTrackForm.tsx @@ -314,7 +314,6 @@ export const EditTrackForm = (props: EditTrackFormProps) => { values.title || messages.untitled } - // @ts-ignore filePath={selectedTrackFile || streamUrl || ''} trackId={values.track_id} isUpload={isUpload} diff --git a/packages/mobile/src/screens/external-wallets/components/SolanaPhoneOption.tsx b/packages/mobile/src/screens/external-wallets/components/SolanaPhoneOption.tsx index 586f786d522..317787f2e00 100644 --- a/packages/mobile/src/screens/external-wallets/components/SolanaPhoneOption.tsx +++ b/packages/mobile/src/screens/external-wallets/components/SolanaPhoneOption.tsx @@ -54,7 +54,6 @@ export const SolanaPhoneOption = () => { let publicKey: string const { - // @ts-ignore: wallet adapter types are wrong signed_payloads: [signature] } = await transact(async (wallet) => { const freshAccount = await authorizeSession(wallet) diff --git a/packages/mobile/src/screens/form-screen/FormScreen.tsx b/packages/mobile/src/screens/form-screen/FormScreen.tsx index 258b1884528..45eaf9a3501 100644 --- a/packages/mobile/src/screens/form-screen/FormScreen.tsx +++ b/packages/mobile/src/screens/form-screen/FormScreen.tsx @@ -64,7 +64,6 @@ export const FormScreen = (props: FormScreenProps) => { backgroundColor='white' borderTop='default' gap='m' - // style={css({ position: 'absolute', bottom: 0, left: 0, right: 0 })} > {bottomSection ?? ( <> diff --git a/packages/mobile/src/screens/form-screen/useRevertOnCancel.ts b/packages/mobile/src/screens/form-screen/useRevertOnCancel.ts index 3eba398d722..db58bcf2352 100644 --- a/packages/mobile/src/screens/form-screen/useRevertOnCancel.ts +++ b/packages/mobile/src/screens/form-screen/useRevertOnCancel.ts @@ -13,14 +13,11 @@ export const useRevertOnCancel = (active?: boolean) => { useEffect(() => { if (!active || !initialValues) return - const listener = navigation.addListener('beforeRemove', ({ data }) => { + // addListener returns an unsubscribe function (not the listener itself) + return navigation.addListener('beforeRemove', ({ data }) => { if (data.action.type === 'POP') { setValues(initialValues) } }) - - return () => { - navigation.removeListener('beforeRemove', listener) - } }, [navigation, initialValues, setValues, active]) } diff --git a/packages/mobile/src/screens/profile-screen/ProfileHeader/ProfileSocials.tsx b/packages/mobile/src/screens/profile-screen/ProfileHeader/ProfileSocials.tsx index 16a81271687..7b4a072d3d5 100644 --- a/packages/mobile/src/screens/profile-screen/ProfileHeader/ProfileSocials.tsx +++ b/packages/mobile/src/screens/profile-screen/ProfileHeader/ProfileSocials.tsx @@ -64,7 +64,6 @@ export const ProfileSocials = () => { const styles = useStyles() const { tier } = useTierAndVerifiedForUser(user_id) - // const tier = 'none' // Need to start opacity at 1 so skeleton is visible. const opacity = useRef(new Animated.Value(1)).current diff --git a/packages/mobile/src/screens/settings-screen/SettingsRow.tsx b/packages/mobile/src/screens/settings-screen/SettingsRow.tsx index 636238727ed..46cec0176c9 100644 --- a/packages/mobile/src/screens/settings-screen/SettingsRow.tsx +++ b/packages/mobile/src/screens/settings-screen/SettingsRow.tsx @@ -48,7 +48,6 @@ export const SettingsRow = (props: SettingsRowProps) => { return ( - {/* @ts-ignore */} {children} {onPress || url ? ( diff --git a/packages/mobile/src/services/sdk/audius-sdk.ts b/packages/mobile/src/services/sdk/audius-sdk.ts index 707a13ad216..ed7fee1b6e0 100644 --- a/packages/mobile/src/services/sdk/audius-sdk.ts +++ b/packages/mobile/src/services/sdk/audius-sdk.ts @@ -20,8 +20,8 @@ let sdkInstance: AudiusSdkWithServices const initSdk = async () => { inProgress = true - // For now, the only solana relay we want to use is on DN 1, so hardcode - // the selection there. + // For now, we only want to use a single validator node's solana relay, so + // hardcode the selection to the configured endpoint. const solanaRelay = new SolanaRelay( new Configuration({ basePath: '/solana', @@ -55,7 +55,6 @@ const initSdk = async () => { }) ) - // Overrides some DN configuration from optimizely const audiusWalletClient = await getAudiusWalletClient() const audiusSdk = createSdkWithServices({ @@ -77,7 +76,7 @@ const initSdk = async () => { export const audiusSdk = async () => { if (inProgress) { await new Promise((resolve) => { - sdkEventEmitter.addListener(SDK_LOADED_EVENT_NAME, resolve) + sdkEventEmitter.once(SDK_LOADED_EVENT_NAME, resolve) }) return sdkInstance } else if (!sdkInstance) { diff --git a/packages/mobile/src/store/cache/collections/createPlaylistRequestedSaga.ts b/packages/mobile/src/store/cache/collections/createPlaylistRequestedSaga.ts index 3ec45f642e6..fd798bc0b3b 100644 --- a/packages/mobile/src/store/cache/collections/createPlaylistRequestedSaga.ts +++ b/packages/mobile/src/store/cache/collections/createPlaylistRequestedSaga.ts @@ -34,7 +34,6 @@ export function* createPlaylistRequestedSaga() { } case 'route': { if (navigationRef.isReady()) { - // @ts-ignore navigationRef is not parametrized correctly (PAY-1141) navigationRef.navigate('Collection', { id: playlistId }) } break diff --git a/packages/mobile/src/store/chat/sagas.ts b/packages/mobile/src/store/chat/sagas.ts index 724a6f8a965..b3bb542e586 100644 --- a/packages/mobile/src/store/chat/sagas.ts +++ b/packages/mobile/src/store/chat/sagas.ts @@ -20,7 +20,6 @@ function* watchGoToChat() { StackActions.replace('Chat', { chatId, presetMessage }) ) } else { - // @ts-ignore navigationRef is not parametrized correctly (PAY-1141) navigationRef.navigate('Chat', { chatId, presetMessage }) } } diff --git a/packages/mobile/src/utils/theme.ts b/packages/mobile/src/utils/theme.ts index b95c1ed3759..c5f646e8192 100644 --- a/packages/mobile/src/utils/theme.ts +++ b/packages/mobile/src/utils/theme.ts @@ -286,9 +286,7 @@ export const useTheme = (baseStyles: object, toTheme: object) => { const newStyles = {} Object.keys(toTheme).forEach((key) => { - // @ts-ignore if (toTheme[key] in themeStyles) { - // @ts-ignore newStyles[key] = themeStyles[toTheme[key]] } }) diff --git a/packages/sdk/src/sdk/api/challenges/types.ts b/packages/sdk/src/sdk/api/challenges/types.ts index bd9d2de1b8b..e8153a0c0b9 100644 --- a/packages/sdk/src/sdk/api/challenges/types.ts +++ b/packages/sdk/src/sdk/api/challenges/types.ts @@ -1,4 +1,3 @@ -// TODO: Not sure if still needed export enum ChallengeId { TRACK_UPLOADS = 'u', REFERRALS = 'r', diff --git a/packages/sdk/src/sdk/api/chats/ChatsApi.ts b/packages/sdk/src/sdk/api/chats/ChatsApi.ts index 74cc4abf63d..73938792e74 100644 --- a/packages/sdk/src/sdk/api/chats/ChatsApi.ts +++ b/packages/sdk/src/sdk/api/chats/ChatsApi.ts @@ -856,7 +856,14 @@ export class ChatsApi private async getPublicKey(userId: string): Promise { if (!this.publicKeyCache[userId]) { - this.publicKeyCache[userId] = this.fetchPublicKey(userId) + const promise = this.fetchPublicKey(userId) + this.publicKeyCache[userId] = promise + // Don't cache failures, so a transient error can be retried + promise.catch(() => { + if (this.publicKeyCache[userId] === promise) { + delete this.publicKeyCache[userId] + } + }) } const cachedPromise = this.publicKeyCache[userId] if (!cachedPromise) { @@ -1021,7 +1028,12 @@ export class ChatsApi }) } } - handleAsync() + handleAsync().catch((e) => { + this.logger.error( + '[audius-sdk]: Error: Failed to handle websocket event', + e + ) + }) }) ws.addEventListener('open', () => { this.eventEmitter.emit('open') diff --git a/packages/sdk/src/sdk/middleware/addRequestSignatureMiddleware.test.ts b/packages/sdk/src/sdk/middleware/addRequestSignatureMiddleware.test.ts index 80482d75ad6..ccc8732923f 100644 --- a/packages/sdk/src/sdk/middleware/addRequestSignatureMiddleware.test.ts +++ b/packages/sdk/src/sdk/middleware/addRequestSignatureMiddleware.test.ts @@ -1,3 +1,4 @@ +import { generatePrivateKey } from 'viem/accounts' import { describe, it, expect, vi } from 'vitest' import { createAppWalletClient, Logger } from '../services' @@ -105,6 +106,39 @@ describe('addRequestSignatureMiddleware', () => { expect(ctx?.init.headers).not.toHaveProperty('Encoded-Data-Signature') }) + it('does not reuse a cached signature once the wallet is gone', async () => { + const appWalletClient = createAppWalletClient({ + apiKey: '', + apiSecret: generatePrivateKey() + }) + const getAddresses = vi.fn(() => appWalletClient.getAddresses()) + const services = { + audiusWalletClient: { + ...appWalletClient, + getAddresses, + signMessage: appWalletClient.signMessage.bind(appWalletClient) + } as any, + logger: { warn: vi.fn() } as any + } + + const m = addRequestSignatureMiddleware({ services }) + const request = () => + m.pre?.({ + fetch, + url: 'example.com', + init: { headers: { 'x-something': 'value' } } + }) + + const ctx = await request() + expect(ctx?.init.headers).toHaveProperty('Encoded-Data-Signature') + + // Simulate logging out + getAddresses.mockRejectedValue(new HedgehogWalletNotFoundError()) + const ctx2 = await request() + expect(ctx2?.init.headers).not.toHaveProperty('Encoded-Data-Message') + expect(ctx2?.init.headers).not.toHaveProperty('Encoded-Data-Signature') + }) + it('logs warning for other errors', async () => { const mockLogger = { warn: vi.fn() diff --git a/packages/sdk/src/sdk/middleware/addRequestSignatureMiddleware.ts b/packages/sdk/src/sdk/middleware/addRequestSignatureMiddleware.ts index e0105e90e23..4fbc23c0245 100644 --- a/packages/sdk/src/sdk/middleware/addRequestSignatureMiddleware.ts +++ b/packages/sdk/src/sdk/middleware/addRequestSignatureMiddleware.ts @@ -82,6 +82,13 @@ export const addRequestSignatureMiddleware = ({ timestamp = currentTimestamp } } catch (e) { + // Drop the cached signature: it's expired, belongs to a different + // address, or the wallet is gone (e.g. logged out), so it must not be + // attached to further requests + message = null + signature = null + signatureAddress = null + timestamp = null // Don't log a warning for HedgehogWalletNotFoundError as it's expected when user is logged out if (!(e instanceof HedgehogWalletNotFoundError)) { logger?.warn(`Unable to add request signature: ${e}`) diff --git a/packages/sdk/src/sdk/services/AntiAbuseOracle/types.ts b/packages/sdk/src/sdk/services/AntiAbuseOracle/types.ts index ff0252f5477..25e0c5b194d 100644 --- a/packages/sdk/src/sdk/services/AntiAbuseOracle/types.ts +++ b/packages/sdk/src/sdk/services/AntiAbuseOracle/types.ts @@ -1,8 +1,5 @@ import { z } from 'zod' -// @ts-ignore:next-line ignore the unused import, used in jsdoc -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { ChallengesApi } from '../../api/challenges/ChallengesApi' import { ChallengeId } from '../../api/challenges/types' import { AntiAbuseOracleSelectorService } from '../AntiAbuseOracleSelector' @@ -11,11 +8,7 @@ export const GetAttestationSchema = z.object({ handle: z.string(), /** The challenge identifier. As in, the challenge "name." */ challengeId: z.nativeEnum(ChallengeId), - /** - * Identifier for the completed challenge instance. - * - * @see {@link ChallengesApi.generateSpecifier} - */ + /** Identifier for the completed challenge instance. */ specifier: z.string(), /** The amount being claimed, in decimal wAUDIO. */ amount: z.number() @@ -33,13 +26,13 @@ export type AttestationResponse = { export type AntiAbuseOracleHealthCheckResponse = { /** The version number of AAO deployed. */ version: number - /** The discovery node endpoint the AAO is using. */ + /** The validator node endpoint the AAO is using. */ selectedDiscoveryNode: string /** A list of other AAO endpoints. (note: Empty at time of writing!) */ otherAntiAbuseOracleEndpoints: string[] /** The wallet public key address for this AAO. */ walletPubkey: string - /** The wallet public key address for this AAO from a discovery node plugin. */ + /** The wallet public key address for this AAO from a validator node plugin. */ antiAbuseWalletPubkey?: string /** Whether the database container is healthy (UP) or unhealthy (DOWN). */ db: 'UP' | 'DOWN' diff --git a/packages/sdk/src/sdk/services/EntityManager/EntityManagerClient.ts b/packages/sdk/src/sdk/services/EntityManager/EntityManagerClient.ts index 112efab0446..a2eed6743e4 100644 --- a/packages/sdk/src/sdk/services/EntityManager/EntityManagerClient.ts +++ b/packages/sdk/src/sdk/services/EntityManager/EntityManagerClient.ts @@ -11,6 +11,7 @@ import { productionConfig } from '../../config/production' import fetch, { Headers } from '../../utils/fetch' import { mergeConfigWithDefaults } from '../../utils/mergeConfigs' import { getNonce } from '../../utils/nonce' +import { wait } from '../../utils/wait' import type { AudiusWalletClient } from '../AudiusWalletClient' import type { LoggerService } from '../Logger' @@ -201,9 +202,7 @@ export class EntityManagerClient implements EntityManagerService { `Could not confirm write within ${confirmationTimeout}ms` ) } - await new Promise((resolve) => - setTimeout(resolve, confirmationPollingInterval) - ) + await wait(confirmationPollingInterval) confirmation = await confirmBlock() } diff --git a/packages/sdk/src/sdk/services/Solana/SolanaRelay.ts b/packages/sdk/src/sdk/services/Solana/SolanaRelay.ts index a2a8014e84a..f91196ceddb 100644 --- a/packages/sdk/src/sdk/services/Solana/SolanaRelay.ts +++ b/packages/sdk/src/sdk/services/Solana/SolanaRelay.ts @@ -29,17 +29,17 @@ import { } from './types' /** - * Client for the Solana Relay Plugin on Discovery. + * Client for the Solana Relay Plugin on validator nodes. */ export class SolanaRelay extends BaseAPI { /** * Public key of the currently selected transaction fee payer - * from the selected Discovery Node. + * from the selected validator node. */ private feePayer: PublicKey | null = null /** - * Gets a random fee payer public key from the selected discovery node's + * Gets a random fee payer public key from the selected validator node's * Solana relay plugin. * * Used when relay transactions don't specify a fee payer override. @@ -110,7 +110,7 @@ export class SolanaRelay extends BaseAPI { } /** - * Relays a transaction to the selected discovery node's Solana relay plugin. + * Relays a transaction to the selected validator node's Solana relay plugin. */ public async relay( params: RelayRequest, @@ -146,8 +146,12 @@ export class SolanaRelay extends BaseAPI { // SendTransactionError to transparently raise to the caller. if (e instanceof Error && e.name === 'ResponseError') { const resp = (e as runtime.ResponseError).response.clone() - const body = await resp.json() + // The error body isn't guaranteed to be JSON; if it can't be parsed, + // surface the original ResponseError instead of a SyntaxError + const body = await resp.json().catch(() => null) if ( + body !== null && + typeof body === 'object' && 'error' in body && 'transactionMessage' in body && 'signature' in body @@ -285,7 +289,7 @@ export class SolanaRelay extends BaseAPI { const noTokenInput = !tokenOutputAmount if (noAudioInput && noTokenInput) { throw new Error( - 'Invalid arguments. Either solInputAmount or tokenOutputAmount must be provided' + 'Invalid arguments. Either audioInputAmount or tokenOutputAmount must be provided' ) } diff --git a/packages/sdk/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts b/packages/sdk/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts index 3a5f5e1dbc3..1118596cc82 100644 --- a/packages/sdk/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts +++ b/packages/sdk/src/sdk/services/Solana/SolanaRelayWalletAdapter.ts @@ -11,7 +11,7 @@ import type { SolanaRelay } from './SolanaRelay' import type { SolanaWalletAdapter } from './types' /** - * Wallet adapter that uses the Solana Relay Plugin on Discovery Node. + * Wallet adapter that uses the Solana Relay Plugin on validator nodes. * * Implementing this interface allows consumers to choose to pay for their own * transactions rather than using our relay, simply by using their own wallet @@ -58,18 +58,21 @@ export class SolanaRelayWalletAdapter implements SolanaWalletAdapter { } /** - * On connection, grabs the fee payer from the Discovery Node plugin. + * On connection, grabs the fee payer from the validator node's relay plugin. */ public async connect() { this._connecting = true - this._publicKey = await this.solanaRelay.getFeePayer() - if (!this._publicKey) { - throw new Error( - 'Failed to connect SolanaRelayWalletAdapter: Failed to get fee payer.' - ) + try { + this._publicKey = await this.solanaRelay.getFeePayer() + if (!this._publicKey) { + throw new Error( + 'Failed to connect SolanaRelayWalletAdapter: Failed to get fee payer.' + ) + } + this._connected = true + } finally { + this._connecting = false } - this._connecting = false - this._connected = true } public async disconnect() { @@ -78,7 +81,7 @@ export class SolanaRelayWalletAdapter implements SolanaWalletAdapter { } /** - * Sends a transaction using the relay to Discovery Node. + * Sends a transaction using the relay on a validator node. * @param transaction the transaction to send. */ public async sendTransaction( diff --git a/packages/sdk/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts b/packages/sdk/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts index 54086f158bc..4118cb99a9e 100644 --- a/packages/sdk/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts +++ b/packages/sdk/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts @@ -65,7 +65,7 @@ export class RewardManagerError extends Error { * Connected client to the Solana RewardManager program. * * The RewardManager program is in charge of disbursing the community awards - * based on attestations from N uniquely owned discovery nodes and an anti abuse + * based on attestations from N uniquely owned validator nodes and an anti abuse * oracle node. */ export class RewardManagerClient { diff --git a/packages/sdk/src/sdk/services/Solana/programs/SolanaClient.test.ts b/packages/sdk/src/sdk/services/Solana/programs/SolanaClient.test.ts new file mode 100644 index 00000000000..2b05e11bbf6 --- /dev/null +++ b/packages/sdk/src/sdk/services/Solana/programs/SolanaClient.test.ts @@ -0,0 +1,52 @@ +import { + ComputeBudgetProgram, + Keypair, + SystemProgram, + TransactionMessage +} from '@solana/web3.js' +import { describe, it, expect, vi } from 'vitest' + +import type { SolanaWalletAdapter } from '../types' + +import { SolanaClient } from './SolanaClient' + +const feePayer = Keypair.generate().publicKey +const recentBlockhash = Keypair.generate().publicKey.toBase58() + +describe('SolanaClient', () => { + describe('buildTransaction', () => { + // The default VERY_HIGH priority uses a 1.5x multiplier, so an odd recent + // fee produces a fractional microLamports value + it('rounds a fractional priority fee', async () => { + const client = new SolanaClient({ + rpcEndpoints: ['http://localhost:8899'], + solanaWalletAdapter: {} as SolanaWalletAdapter + }) + vi.spyOn( + client.connection, + 'getRecentPrioritizationFees' + ).mockResolvedValue([{ slot: 1, prioritizationFee: 200_001 }]) + + const transaction = await client.buildTransaction({ + feePayer, + recentBlockhash, + computeLimit: { units: 1000 }, + instructions: [ + SystemProgram.transfer({ + fromPubkey: feePayer, + toPubkey: Keypair.generate().publicKey, + lamports: 1 + }) + ] + }) + + const { instructions } = TransactionMessage.decompile(transaction.message) + const expected = ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 300_002 + }) + expect( + instructions.some((i) => Buffer.from(i.data).equals(expected.data)) + ).toBe(true) + }) + }) +}) diff --git a/packages/sdk/src/sdk/services/Solana/programs/SolanaClient.ts b/packages/sdk/src/sdk/services/Solana/programs/SolanaClient.ts index 3209db7642c..71ee55b70d4 100644 --- a/packages/sdk/src/sdk/services/Solana/programs/SolanaClient.ts +++ b/packages/sdk/src/sdk/services/Solana/programs/SolanaClient.ts @@ -119,9 +119,13 @@ export class SolanaClient { 0 ) const baseFee = (orderedFees[percentileIndex] ?? 0) * multiplier - const microLamports = Math.min( - Math.max(baseFee, priorityFee.minimumMicroLamports ?? 0), - priorityFee.maximumMicroLamports ?? Number.MAX_SAFE_INTEGER + // Round since the multiplier can produce a fractional fee, which + // setComputeUnitPrice rejects (it converts the value to a BigInt) + const microLamports = Math.round( + Math.min( + Math.max(baseFee, priorityFee.minimumMicroLamports ?? 0), + priorityFee.maximumMicroLamports ?? Number.MAX_SAFE_INTEGER + ) ) if (microLamports !== undefined) { instructions.push( diff --git a/packages/sdk/src/sdk/services/Storage/Storage.test.ts b/packages/sdk/src/sdk/services/Storage/Storage.test.ts index 40419746747..05bea729f29 100644 --- a/packages/sdk/src/sdk/services/Storage/Storage.test.ts +++ b/packages/sdk/src/sdk/services/Storage/Storage.test.ts @@ -137,6 +137,30 @@ describe('pollProcessingStatus', () => { expect(mockFetch).toHaveBeenCalledTimes(1) }) + it('stops polling once the upload is aborted', async () => { + const ac = new AbortController() + mockFetch.mockImplementation(async () => { + ac.abort() + return statusResponse({ id: 'upload-1', status: 'busy' }) + }) + + const storage = new Storage({ storageNodeSelector: nodeSelector }) + const pollWithSignal = ( + storage as unknown as { + pollProcessingStatus: ( + id: string, + template: string, + total: number, + onProgress: undefined, + abortSignal: AbortSignal + ) => Promise + } + ).pollProcessingStatus('upload-1', 'audio', 1, undefined, ac.signal) + + await expect(pollWithSignal).rejects.toThrow('Upload aborted') + expect(mockFetch).toHaveBeenCalledTimes(1) + }, 20000) + // Image resizes have no '320' result to wait for; the gate is audio-only. it('does not require a 320 result for image templates', async () => { mockFetch.mockResolvedValue( diff --git a/packages/sdk/src/sdk/services/Storage/Storage.ts b/packages/sdk/src/sdk/services/Storage/Storage.ts index ebac3693c90..75649b84e26 100644 --- a/packages/sdk/src/sdk/services/Storage/Storage.ts +++ b/packages/sdk/src/sdk/services/Storage/Storage.ts @@ -108,20 +108,23 @@ export class Storage implements StorageService { transcode: 0 }) }, - onSuccess: async () => { + onSuccess: () => { const uploadId = upload?.url?.split('/').pop() if (!uploadId) { reject(new Error('No upload ID received')) return } - const res = await this.pollProcessingStatus( + // tus does not await onSuccess, so failures from polling + // must be forwarded to reject explicitly. + this.pollProcessingStatus( uploadId, metadata.template, cachedTotal, onProgress, ac.signal ) - resolve(res) + .then(resolve) + .catch(reject) } }) @@ -185,14 +188,14 @@ export class Storage implements StorageService { secondOffset: number userId: number }) { - const contentNodeEndpoint = await this.storageNodeSelector.getSelectedNode() + const storageNodeEndpoint = await this.storageNodeSelector.getSelectedNode() - if (!contentNodeEndpoint) { - throw new Error('No content node available') + if (!storageNodeEndpoint) { + throw new Error('No validator node available') } const url = new URL( - `${contentNodeEndpoint}/generate_preview/${cid}/${secondOffset}` + `${storageNodeEndpoint}/generate_preview/${cid}/${secondOffset}` ) url.searchParams.set('userId', String(userId)) @@ -232,10 +235,11 @@ export class Storage implements StorageService { : MAX_IMAGE_RESIZE_TIMEOUT_MS while (Date.now() - start < maxPollingMs) { + // Checked outside the try so the abort isn't swallowed and retried below + if (abortSignal?.aborted) { + throw new Error('Upload aborted') + } try { - if (abortSignal?.aborted) { - throw new Error('Upload aborted') - } const resp = await this.getProcessingStatus(id) if (resp?.status === 'done') { if (template !== 'audio' || resp.results?.['320']) { diff --git a/packages/sdk/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts b/packages/sdk/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts index abd1a2ebad8..ca9b8e59164 100644 --- a/packages/sdk/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts +++ b/packages/sdk/src/sdk/services/StorageNodeSelector/StorageNodeSelector.test.ts @@ -121,6 +121,27 @@ describe('StorageNodeSelector', () => { ]) }) + it('falls back to bootstrap nodes when the node list fetch fails', async () => { + server.use( + rest.get(`${discoveryNode}/health_check`, (_req, res) => { + return res.networkError('Failed to connect') + }) + ) + const bootstrapNodes = [storageNodeA, storageNodeB] + + const storageNodeSelector = new StorageNodeSelector({ + bootstrapNodes, + logger, + endpoint: discoveryNode + }) + + expect( + [storageNodeA.endpoint, storageNodeB.endpoint].includes( + (await storageNodeSelector.getSelectedNode())! + ) + ).toBe(true) + }) + it('tries selecting all nodes', async () => { server.use( rest.get(`${storageNodeA.endpoint}/health_check`, (_req, res, ctx) => { diff --git a/packages/sdk/src/sdk/services/StorageNodeSelector/StorageNodeSelector.ts b/packages/sdk/src/sdk/services/StorageNodeSelector/StorageNodeSelector.ts index 5ba98f96af2..f7f0b700f67 100644 --- a/packages/sdk/src/sdk/services/StorageNodeSelector/StorageNodeSelector.ts +++ b/packages/sdk/src/sdk/services/StorageNodeSelector/StorageNodeSelector.ts @@ -4,7 +4,7 @@ import { RendezvousHash } from '../../utils/rendezvous' import type { LoggerService } from '../Logger' import { getDefaultStorageNodeSelectorConfig } from './getDefaultConfig' -import { isNodeHealthy } from './getNStorageNodes' +import { isNodeHealthy } from './isNodeHealthy' import type { HealthCheckResponseData, StorageNode, @@ -33,7 +33,9 @@ export class StorageNodeSelector implements StorageNodeSelectorService { this.nodes = this.config.bootstrapNodes ?? [] this.selectionState = 'healthy_only' - this.updateAvailableStorageNodes(this.config.endpoint) + this.updateAvailableStorageNodes(this.config.endpoint).catch((e) => { + this.logger.warn('Failed to update list of available storage nodes', e) + }) } private async updateAvailableStorageNodes(endpoint: string) { @@ -47,16 +49,19 @@ export class StorageNodeSelector implements StorageNodeSelectorService { const responseData: { data: HealthCheckResponseData } = await healthCheckResponse.json() - const contentNodes = responseData.data.network?.content_nodes + const storageNodes = responseData.data.network?.content_nodes - if (!contentNodes) { + if (!storageNodes) { this.logger.warn( - 'API health check did not contain any available content nodes' + 'API health check did not contain any available storage nodes' ) return } - this.nodes = contentNodes + this.nodes = storageNodes + // Drop any ordering computed from the bootstrap nodes so the next + // selection uses the refreshed list + this.orderedNodes = undefined this.selectionState = 'healthy_only' } @@ -87,7 +92,7 @@ export class StorageNodeSelector implements StorageNodeSelectorService { if (selectedNode) { this.selectedNode = selectedNode - this.logger.info('Selected content node', this.selectedNode) + this.logger.info('Selected storage node', this.selectedNode) } else { // No healthy nodes found this.selectedNode = null @@ -102,23 +107,26 @@ export class StorageNodeSelector implements StorageNodeSelectorService { if (!this.orderedNodes?.length) { this.orderedNodes = this.orderNodes(new Date().toString()) } + // Hold a local reference: the list can be replaced by a node list refresh + // while we await health checks below + const orderedNodes = this.orderedNodes - if (this.orderedNodes.length === 0) { + if (orderedNodes.length === 0) { return undefined } const currentNodeIndex = this.selectedNode - ? this.orderedNodes.indexOf(this.selectedNode) + ? orderedNodes.indexOf(this.selectedNode) : -1 let selectedNode: string | undefined let nextNodeIndex = currentNodeIndex - while (nextNodeIndex !== this.orderedNodes.length - 1) { + while (nextNodeIndex !== orderedNodes.length - 1) { nextNodeIndex++ - const nextNode = this.orderedNodes[nextNodeIndex] - if (!nextNode) continue // should never happen unless this.orderedNodes has falsy values - if (await isNodeHealthy(nextNode)) { + const nextNode = orderedNodes[nextNodeIndex] + if (!nextNode) continue // should never happen unless orderedNodes has falsy values + if (await isNodeHealthy(nextNode, this.logger)) { selectedNode = nextNode break } diff --git a/packages/sdk/src/sdk/services/StorageNodeSelector/getNStorageNodes.ts b/packages/sdk/src/sdk/services/StorageNodeSelector/getNStorageNodes.ts deleted file mode 100644 index bfb08f9f85b..00000000000 --- a/packages/sdk/src/sdk/services/StorageNodeSelector/getNStorageNodes.ts +++ /dev/null @@ -1,96 +0,0 @@ -import axios from 'axios' - -import { RendezvousHash } from '../../utils/rendezvous' - -export type StorageNode = { - owner: any - endpoint: string - spID: number - type: string - blockNumber: number - delegateOwnerWallet: string -} - -/** - * Selects numNodes storage nodes from the given list of allNodes, optionally ordering them (descending) by rendezvous score. - * @dev This makes the wallet lowercase - not suitable for use with CIDs because they're case sensitive (use RendezvousHash directly instead). - */ -export const getNStorageNodes = async ( - allNodes: StorageNode[], - numNodes = 0, - wallet = '', - logger = console -): Promise => { - try { - if (numNodes === 0) numNodes = allNodes.length - let sortedEndpoints: string[] - - // Sort endpoints by rendezvous score if a rendezvous key is provided - if (wallet?.length) { - const endpoints = allNodes.map((n) => n.endpoint.toLowerCase()) - const hash = new RendezvousHash(...endpoints) - sortedEndpoints = hash.getN(endpoints.length, wallet.toLowerCase()) - } else { - sortedEndpoints = allNodes.map((n) => n.endpoint) - } - - // Check multiple nodes at a time for health until we have numNodes healthy nodes - const healthyEndpoints: string[] = [] - for (let i = 0; i < sortedEndpoints.length; i += numNodes) { - const batch = sortedEndpoints.slice(i, i + numNodes) - const healthCheckPromises = batch.map( - async (endpoint) => await isNodeHealthy(endpoint, logger) - ) - const healthCheckResults = await Promise.all(healthCheckPromises) - - for (let j = 0; j < healthCheckResults.length; j++) { - if (healthCheckResults[j]) { - healthyEndpoints.push(batch[j]!) - } - } - - if (healthyEndpoints.length >= numNodes) { - return healthyEndpoints.slice(0, numNodes) - } - } - - if (numNodes !== allNodes.length && sortedEndpoints.length < numNodes) { - logger.error( - `getNStorageNodes: Could not select ${numNodes} healthy nodes from ${allNodes.length} nodes` - ) - } - - return healthyEndpoints - } catch (e) { - logger.error(`getNStorageNodes: Error selecting nodes: ${e}`) - return [] - } -} - -export const isNodeHealthy = async (endpoint: string, logger = console) => { - try { - const resp = await axios({ - baseURL: endpoint, - url: `/health_check`, - method: 'get', - timeout: 10_000 - }) - if (resp.status !== 200) { - logger.warn( - `isNodeHealthy: ${endpoint} returned non-200 status ${resp.status}` - ) - return false - } - - // check has space + not slow - const health = resp.data.data - - // skip nodes if avg transcode time is over 3 minutes - const avgTranscodeTime = health.transcodeStats?.AvgTranscodeTime || 0 - const isSlowTranscode = avgTranscodeTime > 180 - return health.diskHasSpace && !isSlowTranscode - } catch (e) { - logger.error(`isNodeHealthy: Error checking health: ${e}`) - return false - } -} diff --git a/packages/sdk/src/sdk/services/StorageNodeSelector/isNodeHealthy.ts b/packages/sdk/src/sdk/services/StorageNodeSelector/isNodeHealthy.ts new file mode 100644 index 00000000000..47d936f1561 --- /dev/null +++ b/packages/sdk/src/sdk/services/StorageNodeSelector/isNodeHealthy.ts @@ -0,0 +1,34 @@ +import axios from 'axios' + +import type { LoggerService } from '../Logger' + +export const isNodeHealthy = async ( + endpoint: string, + logger: Pick = console +) => { + try { + const resp = await axios({ + baseURL: endpoint, + url: `/health_check`, + method: 'get', + timeout: 10_000 + }) + if (resp.status !== 200) { + logger.warn( + `isNodeHealthy: ${endpoint} returned non-200 status ${resp.status}` + ) + return false + } + + // check has space + not slow + const health = resp.data.data + + // skip nodes if avg transcode time is over 3 minutes + const avgTranscodeTime = health.transcodeStats?.AvgTranscodeTime || 0 + const isSlowTranscode = avgTranscodeTime > 180 + return health.diskHasSpace && !isSlowTranscode + } catch (e) { + logger.error(`isNodeHealthy: Error checking health: ${e}`) + return false + } +} diff --git a/packages/sdk/src/sdk/types/SolanaAddress.ts b/packages/sdk/src/sdk/types/SolanaAddress.ts index 3fd86da6366..2c797bb219d 100644 --- a/packages/sdk/src/sdk/types/SolanaAddress.ts +++ b/packages/sdk/src/sdk/types/SolanaAddress.ts @@ -11,9 +11,8 @@ export const SolanaAddressSchema = z .max(44, 'Solana address must not exceed 44 characters') .refine((val) => { try { - // @ts-ignore - need an unused variable to check if the destinationWallet is valid - const ignored = new PublicKey(val) - return true + // Throws if the address is not a valid public key + return !!new PublicKey(val) } catch (err) { console.debug(err) return false diff --git a/packages/sdk/src/sdk/utils/deepPartial.ts b/packages/sdk/src/sdk/utils/deepPartial.ts deleted file mode 100644 index fa6a5990e5e..00000000000 --- a/packages/sdk/src/sdk/utils/deepPartial.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Adjusted from Terry: https://stackoverflow.com/questions/61132262/typescript-deep-partial -export type DeepPartial = T extends any[] - ? T - : T extends Set - ? T - : T extends object - ? { - [P in keyof T]?: DeepPartial - } - : T diff --git a/packages/sdk/src/sdk/utils/localStorage.ts b/packages/sdk/src/sdk/utils/localStorage.ts deleted file mode 100644 index d1aa2e4cea9..00000000000 --- a/packages/sdk/src/sdk/utils/localStorage.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { LocalStorage } from '@audius/hedgehog' - -/** - * Fallback for localStorage that works in node and the browser - * @returns localStorage - */ -export const getPlatformLocalStorage = async (): Promise => { - if (typeof window === 'undefined' || window === null) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const LocalStorage = (await import('node-localstorage')).LocalStorage - return new LocalStorage('./local-storage') - } else { - return window.localStorage - } -} diff --git a/packages/sdk/src/sdk/utils/object.ts b/packages/sdk/src/sdk/utils/object.ts deleted file mode 100644 index d5471ad9d37..00000000000 --- a/packages/sdk/src/sdk/utils/object.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Return properties from a list for which the object is missing values - */ -export const objectMissingValues = ( - object: Record, - properties: string[] -) => { - return properties.filter((p) => object[p] === undefined || object[p] === null) -} diff --git a/packages/sdk/src/sdk/utils/uuid.ts b/packages/sdk/src/sdk/utils/uuid.ts deleted file mode 100644 index 9b95c9bcfcd..00000000000 --- a/packages/sdk/src/sdk/utils/uuid.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const uuid = () => { - // https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript/873856#873856 - const s = [] - const hexDigits = '0123456789abcdef' - // all the ts-ignores are needed so the mobile package types things correctly - // remove when upgrading to latest typescript - for (let i = 0; i < 36; i++) { - // @ts-ignore - s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) - } - // @ts-ignore - s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010 - // @ts-ignore - s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01 - // @ts-ignore - s[8] = s[13] = s[18] = s[23] = '-' - - const uuid = s.join('') - return uuid -} diff --git a/packages/web/src/common/store/pages/signon/sagas.ts b/packages/web/src/common/store/pages/signon/sagas.ts index 83012b50249..4fcf28d1877 100644 --- a/packages/web/src/common/store/pages/signon/sagas.ts +++ b/packages/web/src/common/store/pages/signon/sagas.ts @@ -522,7 +522,7 @@ function* signUp() { ) // Optimistically update localStorage so getLocalAccount returns - // complete user data before discovery provider has indexed. + // complete user data before the validator node has indexed. const updatedUser = { ...account.user, handle, diff --git a/packages/web/src/components/stripe-on-ramp-modal/StripeOnRampModal.tsx b/packages/web/src/components/stripe-on-ramp-modal/StripeOnRampModal.tsx index b3326ff95bf..d25692aa0ba 100644 --- a/packages/web/src/components/stripe-on-ramp-modal/StripeOnRampModal.tsx +++ b/packages/web/src/components/stripe-on-ramp-modal/StripeOnRampModal.tsx @@ -38,6 +38,9 @@ const MountStripeSession = ({ session }: { session: OnrampSession }) => { } session.mount('#stripe-onramp-modal') session.addEventListener('onramp_session_updated', handleSessionUpdate) + return () => { + session.removeEventListener('onramp_session_updated', handleSessionUpdate) + } }, [session, handleSessionUpdate]) return ( @@ -78,7 +81,11 @@ export const StripeOnRampModal = () => { clientSecret: stripeClientSecret }) } - initializeStripeSession().then(setSession) + initializeStripeSession() + .then(setSession) + .catch((e) => { + console.error('Failed to initialize Stripe onramp session', e) + }) } }, [isOpen, stripeClientSecret]) diff --git a/packages/web/src/hooks/README.md b/packages/web/src/hooks/README.md index 8c87a19d9fb..ff33e7023a2 100644 --- a/packages/web/src/hooks/README.md +++ b/packages/web/src/hooks/README.md @@ -7,13 +7,9 @@ Each individual file should have more extensive documentation & examples. ## Hooks -### `useModalState` - -Allows for `useState` style ergonomics for getting & setting modal visibility. - ### `useNavigateToPage` -Wraps `redux-first-history` navigation in a single function. +Wraps react-router's `useNavigate` in a single function. ### `useInstanceVar` @@ -23,14 +19,6 @@ Convenience wrapper around useRef. Allows `useState`-like ergonomics. Wraps `classnames` with mobile switching logic to conditionally add mobile classes. -### `useHotkeys` - -Easily create mappings from keycodes to callbacks. - -### `useTabs` - -Creates tabs. - ### `usePortal` Portals a node out of its place in the dom hierarchy. @@ -38,7 +26,3 @@ Portals a node out of its place in the dom hierarchy. ### `useHasChangedRoute` Wraps the react router `useLocation` hook to allow a function to be called on route changes - -### `useScript` - -Loads a script in the DOM given a source url diff --git a/packages/web/src/hooks/useLaunchCoin.ts b/packages/web/src/hooks/useLaunchCoin.ts index 63cf9bcc5f4..86332aecbc4 100644 --- a/packages/web/src/hooks/useLaunchCoin.ts +++ b/packages/web/src/hooks/useLaunchCoin.ts @@ -4,6 +4,7 @@ import { getConnectedWalletsQueryOptions, getCurrentAccountQueryKey, getUserQueryKey, + uploadBackdropImage, useQueryContext } from '@audius/common/api' import { @@ -184,29 +185,10 @@ export const useLaunchCoin = () => { let resolvedBannerImageUrl = bannerImageUrl if (bannerImageFile) { - const uploadResponse = await sdk.services.storage - .uploadFile({ - file: bannerImageFile, - metadata: { - template: 'img_backdrop' - } - }) - .start() - const cid = getBannerImageUrl(uploadResponse.results) - if (!cid) { - throw new Error('Failed to process banner image upload') - } - - // Convert CID to content node URL - const contentNodeEndpoint = await ( - sdk.services.storage as any - ).storageNodeSelector?.getSelectedNode() - - if (!contentNodeEndpoint) { - throw new Error('No content node available') - } - - resolvedBannerImageUrl = `${contentNodeEndpoint}/content/${cid}` + resolvedBannerImageUrl = await uploadBackdropImage( + sdk, + bannerImageFile + ) } // Create coin in Audius database @@ -282,14 +264,3 @@ export const useLaunchCoin = () => { } }) } - -const getBannerImageUrl = (results: Record = {}) => { - const prioritizedSizes = ['2000x', '1500x', '1280x', '1000x', '640x'] - for (const size of prioritizedSizes) { - if (results[size]) { - return results[size] - } - } - const firstResult = Object.values(results)[0] - return firstResult -} diff --git a/packages/web/src/hooks/useScript.ts b/packages/web/src/hooks/useScript.ts deleted file mode 100644 index 7878a2fe637..00000000000 --- a/packages/web/src/hooks/useScript.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useEffect, useState } from 'react' - -export const useScript = (src: string, isModule = false) => { - const [scriptLoaded, setScriptLoaded] = useState(false) - - useEffect(() => { - const alreadyLoaded = [...document.getElementsByTagName('script')].find( - (script) => script.src === src - ) - if (alreadyLoaded) { - setScriptLoaded(true) - } else { - const script = document.createElement('script') - script.src = src - script.async = true - if (isModule) { - script.type = 'module' - } - script.onload = () => setScriptLoaded(true) - document.body.appendChild(script) - } - }, [src, isModule]) - - return scriptLoaded -} diff --git a/packages/web/src/pages/dev-tools/messages.ts b/packages/web/src/pages/dev-tools/messages.ts index 45524afa157..3ee820c34eb 100644 --- a/packages/web/src/pages/dev-tools/messages.ts +++ b/packages/web/src/pages/dev-tools/messages.ts @@ -37,7 +37,7 @@ export const messages = { aaoButton: 'Open AAO UI', healthzTitle: 'Health Monitor (Healthz)', healthzDescription: - 'Access the health monitoring dashboard for Audius network services including discovery nodes, content nodes, and other protocol infrastructure.', + 'Access the health monitoring dashboard for Audius network services including Open Audio validator nodes and other protocol infrastructure.', healthzButton: 'Open Healthz Dashboard', userIdParserTitle: 'User ID Parser', userIdParserDescription: diff --git a/packages/web/src/pages/host-remix-contest-page/useUploadContestCover.ts b/packages/web/src/pages/host-remix-contest-page/useUploadContestCover.ts index 29cec4a9b20..fceb314d339 100644 --- a/packages/web/src/pages/host-remix-contest-page/useUploadContestCover.ts +++ b/packages/web/src/pages/host-remix-contest-page/useUploadContestCover.ts @@ -1,12 +1,12 @@ import { useCallback, useState } from 'react' -import { useQueryContext } from '@audius/common/api' +import { uploadBackdropImage, useQueryContext } from '@audius/common/api' /** - * Handles mediorum upload of a contest cover photo. Mirrors the pattern - * used by useLaunchCoin / useUpdateFanClub for banner images — uploads the - * file with `img_backdrop` template (wide aspect-ratio) and resolves to a - * content-node URL the form can persist in `eventData.coverPhotoUrl`. + * Handles upload of a contest cover photo. Uses the shared + * `uploadBackdropImage` helper (also used for fan club banners), which uploads + * the file with the `img_backdrop` template (wide aspect-ratio) and resolves + * to a validator node URL the form can persist in `eventData.coverPhotoUrl`. */ export const useUploadContestCover = () => { const { audiusSdk } = useQueryContext() @@ -17,33 +17,7 @@ export const useUploadContestCover = () => { setIsUploading(true) try { const sdk = await audiusSdk() - const uploadResponse = await sdk.services.storage - .uploadFile({ - file, - metadata: { template: 'img_backdrop' } - }) - .start() - - const results = (uploadResponse.results ?? {}) as Record - const prioritizedSizes = ['2000x', '1500x', '1280x', '1000x', '640x'] - let cid: string | undefined - for (const size of prioritizedSizes) { - if (results[size]) { - cid = results[size] - break - } - } - if (!cid) cid = Object.values(results)[0] - if (!cid) throw new Error('Upload returned no CID') - - const contentNodeEndpoint = await ( - sdk.services.storage as any - ).storageNodeSelector?.getSelectedNode() - if (!contentNodeEndpoint) { - throw new Error('No content node available') - } - - return `${contentNodeEndpoint}/content/${cid}` + return await uploadBackdropImage(sdk, file) } catch (error) { console.error('HostRemixContest', error as Error) return null diff --git a/packages/web/src/pages/pay-and-earn-page/components/WithdrawalsTab.tsx b/packages/web/src/pages/pay-and-earn-page/components/WithdrawalsTab.tsx index 75d62648427..39bfce4d2f0 100644 --- a/packages/web/src/pages/pay-and-earn-page/components/WithdrawalsTab.tsx +++ b/packages/web/src/pages/pay-and-earn-page/components/WithdrawalsTab.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useCurrentUserId, @@ -128,46 +128,47 @@ const useWithdrawalTransactionPoller = ( method: GetUSDCTransactionsMethodEnum.Send }) + // Read the latest transactions/reset via refs so the polling effect below + // only restarts when the transaction being waited on changes. `reset` gets a + // new identity on every render and `transactions` changes on every refetch, + // so depending on them directly restarted the 60s timeout on each poll and + // the poller never stopped. + const transactionsRef = useRef(transactions) + transactionsRef.current = transactions + const resetRef = useRef(reset) + resetRef.current = reset + useEffect(() => { if (!lastCompletedTransaction || !userId) return - setIsPolling(true) - let timedOut = false - const timerId = setTimeout(() => { - timedOut = true - setIsPolling(false) - }, 60000) - - const pollInterval = setInterval(async () => { - // Check if transaction exists in current data - const found = transactions?.some( + const isFound = () => + !!transactionsRef.current?.some( (t) => t.signature === lastCompletedTransaction ) + if (isFound()) return - if (found) { - clearInterval(pollInterval) - clearTimeout(timerId) - setIsPolling(false) - return - } + setIsPolling(true) + + const stopPolling = () => { + clearInterval(pollInterval) + clearTimeout(timerId) + setIsPolling(false) + } - // If we've timed out, stop polling - if (timedOut) { - clearInterval(pollInterval) + // Stop polling after 60s even if the transaction never shows up + const timerId = setTimeout(stopPolling, 60000) + + const pollInterval = setInterval(() => { + if (isFound()) { + stopPolling() return } - // Refetch transactions - await reset() + resetRef.current() }, 1000) - // Cleanup - return () => { - clearInterval(pollInterval) - clearTimeout(timerId) - setIsPolling(false) - } - }, [lastCompletedTransaction, userId, transactions, reset]) + return stopPolling + }, [lastCompletedTransaction, userId]) return { isPolling, diff --git a/packages/web/src/utils/sagaPollingDaemons.ts b/packages/web/src/utils/sagaPollingDaemons.ts index 1bd83096079..62f485aba90 100644 --- a/packages/web/src/utils/sagaPollingDaemons.ts +++ b/packages/web/src/utils/sagaPollingDaemons.ts @@ -21,33 +21,36 @@ export function* foregroundPollingDaemon( const isNativeMobile = yield* getContext('isNativeMobile') let isBrowserInBackground = false + const handleVisibilityChange = () => { + isBrowserInBackground = document.hidden + } if (!isNativeMobile) { - document.addEventListener( - 'visibilitychange', - () => { - if (document.hidden) { - isBrowserInBackground = true - } else { - isBrowserInBackground = false - } - }, - false - ) + document.addEventListener('visibilitychange', handleVisibilityChange, false) } - while (true) { - if (!isBrowserInBackground || isElectron() || isNativeMobile) { - yield put(action) - } - if (customDelayTimeConfig && !isNativeMobile) { - const currentRoute: string = yield select(getLocationPathname) - if (customDelayTimeConfig[currentRoute]) { - yield delay(customDelayTimeConfig[currentRoute]) + try { + while (true) { + if (!isBrowserInBackground || isElectron() || isNativeMobile) { + yield put(action) + } + if (customDelayTimeConfig && !isNativeMobile) { + const currentRoute: string = yield select(getLocationPathname) + if (customDelayTimeConfig[currentRoute]) { + yield delay(customDelayTimeConfig[currentRoute]) + } else { + yield delay(defaultDelayTimeMs) + } } else { yield delay(defaultDelayTimeMs) } - } else { - yield delay(defaultDelayTimeMs) + } + } finally { + if (!isNativeMobile) { + document.removeEventListener( + 'visibilitychange', + handleVisibilityChange, + false + ) } } }