Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core-mobile/app/consts/internalTokenIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { InternalId } from 'store/watchlist'

export const AVAX_NATIVE_ID: InternalId = 'NATIVE-avax'

export const BITCOIN_NATIVE_ID: InternalId = 'NATIVE-btc'

export const ETHEREUM_NATIVE_ID: InternalId = 'NATIVE-eth'
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ export const handleDeeplink = ({
deeplink.callback?.()
navigateFromDeeplinkUrl('/claimStakeReward')
} else if (action === ACTIONS.WatchList) {
const coingeckoId = pathname.split('/')[1]
navigateFromDeeplinkUrl(
`/trackTokenDetail?tokenId=${coingeckoId}&marketType=${MarketType.SEARCH}`
)
const tokenId = pathname.split('/')[1]
if (tokenId) {
// All watchlist tokens now use internalId format
navigateFromDeeplinkUrl(
`/trackTokenDetail?tokenId=${tokenId}&marketType=${MarketType.TRENDING}`
)
}
} else if (action === ACTIONS.OfframpCompleted) {
dispatch(offrampSend({ searchParams }))
} else if (action === ACTIONS.OnrampCompleted) {
Expand Down
22 changes: 20 additions & 2 deletions packages/core-mobile/app/hooks/watchlist/useWatchlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ export const useWatchlist = (): UseWatchListReturnType => {
enabled: isFocused && topTokensCoingeckoIds.length > 0
})

// Map prices from coingeckoId back to internalId for consistent access
const topTokenPricesById = useMemo(() => {
if (!topTokenPrices || !topTokensResponse?.tokens) {
return {}
}

const pricesById: Prices = {}
Object.values(topTokensResponse.tokens).forEach(token => {
const price = token.coingeckoId
? topTokenPrices[token.coingeckoId]
: undefined
if (price) {
pricesById[token.id] = price
}
})
return pricesById
}, [topTokenPrices, topTokensResponse?.tokens])

const isLoadingFavorites = favoriteIds.length > 0 && isLoading

const favorites = useMemo(() => {
Expand Down Expand Up @@ -126,9 +144,9 @@ export const useWatchlist = (): UseWatchListReturnType => {
const prices = useMemo(() => {
return {
...transformedTrendingTokens?.prices,
...topTokenPrices
...topTokenPricesById
}
}, [topTokenPrices, transformedTrendingTokens?.prices])
}, [topTokenPricesById, transformedTrendingTokens?.prices])

const getWatchlistPrice = useCallback(
(id: string): PriceData | undefined => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMigrateFavoriteIds } from 'new/features/track/market/hooks/useMigrateFavoriteIds'

/**
* This component is used to trigger the useMigrateFavoriteIds hook early in the app lifecycle.
* This ensures that user favorites are migrated from coingeckoId to internalId format
* as soon as the user is signed in, enabling proper push notification subscriptions
* even if they don't visit the favorites tab.
* It does not render anything.
* @returns {null} Returns null as it doesn't render any UI.
*/
export const MigrateFavoriteIds = (): null => {
useMigrateFavoriteIds()
return null
}
5 changes: 2 additions & 3 deletions packages/core-mobile/app/new/common/hooks/useTokenDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,8 @@ export const useTokenDetails = ({
)
const token = getMarketTokenById(tokenId)

// when searching, the token id is actually the coingecko id
const coingeckoId =
marketType === MarketType.SEARCH ? tokenId : token?.coingeckoId ?? ''
// All tokens now use internalId, but we still need coingeckoId for Price API calls
const coingeckoId = token?.coingeckoId ?? tokenId.split(':')[1] ?? ''

const chainId =
marketType === MarketType.SEARCH ? undefined : getTokenChainId(token)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import React, { useCallback, useMemo } from 'react'
import { ViewStyle } from 'react-native'
import Animated from 'react-native-reanimated'
import { MarketType } from 'store/watchlist'
import { useMigrateFavoriteIds } from '../hooks/useMigrateFavoriteIds'
import { useTrackSortAndView } from '../hooks/useTrackSortAndView'
import MarketTokensScreen from './MarketTokensScreen'

Expand All @@ -24,7 +23,6 @@ const FavoriteScreen = ({
onScrollResync: () => void
}): JSX.Element => {
const { favorites, prices, charts, isLoadingFavorites } = useWatchlist()
const { hasMigratedFavoriteIds } = useMigrateFavoriteIds()

const { data, sort, view } = useTrackSortAndView(favorites, prices, true)

Expand All @@ -46,7 +44,7 @@ const FavoriteScreen = ({
)
}, [containerStyle.minHeight, emptyComponent])

if (isLoadingFavorites || !hasMigratedFavoriteIds) {
if (isLoadingFavorites) {
return (
<LoadingState
sx={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {
selectWatchlistFavoriteIds,
toggleWatchListFavorite
} from 'store/watchlist'
import Logger from 'utils/Logger'

/**
* @description
* This hook is used to migrate favorite IDs from coingecko/contract address
* to internalId
* This hook migrates existing user favorites from coingeckoId format to internalId format.
* This ensures users don't lose their favorite tokens during the transition.
*/
export const useMigrateFavoriteIds = (): {
hasMigratedFavoriteIds: boolean
Expand All @@ -28,7 +29,7 @@ export const useMigrateFavoriteIds = (): {
const dispatch = useDispatch()
const favoriteIds = useSelector(selectWatchlistFavoriteIds)
const hasMigratedFavoriteIds = useSelector(
selectHasBeenViewedOnce(ViewOnceKey.MIGRATE_TOKEN_FAVORITE_IDS)
selectHasBeenViewedOnce(ViewOnceKey.MIGRATE_TOKEN_FAVORITE_IDSv2)
)

useEffect(() => {
Expand All @@ -41,30 +42,34 @@ export const useMigrateFavoriteIds = (): {
return

favoriteIds.forEach((favoriteId: string) => {
// Check if this favorite is already in internalId format
if (favoriteId.includes(':') || favoriteId.includes('NATIVE')) {
// Already in internalId format, skip migration
return
}

dispatch(toggleWatchListFavorite(favoriteId)) // Remove old in any case

// Look for a token with matching coingeckoId in top tokens
const topToken = topTokens.find(
token => token.coingeckoId?.toLowerCase() === favoriteId.toLowerCase()
)
if (topToken) {
// if the token is in the top tokens, we need to remove it from the favorites
// and add it again with the new id
dispatch(toggleWatchListFavorite(favoriteId))
dispatch(toggleWatchListFavorite(topToken.id))
return
}
const trendingToken = trendingTokens.find(token =>
token.id.toLowerCase().includes(favoriteId.toLowerCase())

// Look for a token with matching coingeckoId in trending tokens
const trendingToken = trendingTokens.find(
token => token.coingeckoId?.toLowerCase() === favoriteId.toLowerCase()
)
if (trendingToken) {
// if the token is in the trending tokens, we need to remove it from the favorites
// and add it again with the new id
dispatch(toggleWatchListFavorite(favoriteId))
dispatch(toggleWatchListFavorite(trendingToken.id))
}
})

// after the migration is done, we need to set the view once
// so we don't run the migration again
dispatch(setViewOnce(ViewOnceKey.MIGRATE_TOKEN_FAVORITE_IDS))
Logger.info('Migrated favorites')
// Mark migration as completed
dispatch(setViewOnce(ViewOnceKey.MIGRATE_TOKEN_FAVORITE_IDSv2))
}, [
hasMigratedFavoriteIds,
favoriteIds.length,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,9 @@ const TrackTokenDetailScreen = (): JSX.Element => {
<View sx={{ paddingHorizontal: 16 }}>
<Animated.View style={{ opacity: headerOpacity }}>
<TokenHeader
name={token?.name ?? ''}
logoUri={token?.logoUri ?? undefined}
symbol={token?.symbol ?? ''}
name={token?.name ?? tokenInfo?.name ?? ''}
logoUri={token?.logoUri ?? tokenInfo?.logoUri}
symbol={token?.symbol ?? tokenInfo?.symbol ?? ''}
currentPrice={currentPrice}
ranges={range}
rank={tokenInfo?.marketCapRank}
Expand Down
2 changes: 2 additions & 0 deletions packages/core-mobile/app/new/routes/(signedIn)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LastTransactedNetworks } from 'common/components/LastTransactedNetworks'
import { MigrateFavoriteIds } from 'new/common/components/MigrateFavoriteIds'
import { Stack } from 'common/components/Stack'
import { stackNavigatorScreenOptions } from 'common/consts/screenOptions'
import { useModalScreenOptions } from 'common/hooks/useModalScreenOptions'
Expand Down Expand Up @@ -203,6 +204,7 @@ export default function WalletLayout(): JSX.Element {
</Stack>
<PolyfillCrypto />
<LastTransactedNetworks />
<MigrateFavoriteIds />
</CollectiblesProvider>
</BridgeProvider>
)
Expand Down
2 changes: 1 addition & 1 deletion packages/core-mobile/app/services/fcm/FCMService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class FCMService {
}
} else if (fcmData.type === NotificationTypes.NEWS) {
return {
url: fcmData.url
url: fcmData.urlV2
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core-mobile/app/services/fcm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const NewsDataSchema = object({
event: nativeEnum(NewsEvents),
title: string(),
body: string(),
url: string()
urlV2: string()
})

export const NotificationPayloadSchema = object({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TokenSubscriptionPayload } from 'services/notifications/priceAlert/types'
import Logger from 'utils/Logger'
import fetchWithAppCheck from 'utils/httpClient'
import Config from 'react-native-config'

export async function setPriceAlertSubscriptions(
payload: TokenSubscriptionPayload
): Promise<void> {
Logger.info(
'[setPriceAlertSubscriptions] Setting token subscriptions:',
payload.tokens.map(t => t.internalId)
)

try {
const response = await fetchWithAppCheck(
Config.NOTIFICATION_SENDER_API_URL +
'/v1/push/price-alerts/custom/subscribe',
JSON.stringify(payload)
)

if (response.ok) {
const result = await response.json()
Logger.info(
'[setPriceAlertSubscriptions] Successfully subscribed to token price alerts:',
result
)
} else {
throw new Error(`${response.status}:${response.statusText}`)
}
} catch (error) {
Logger.error(
`[setPriceAlertSubscriptions] Failed to set token subscriptions:`,
error
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface TokenSubscriptionItem {
internalId: string // EIP-155 format: eip155:43114-0x<contract_address>
}

export interface TokenSubscriptionPayload {
tokens: TokenSubscriptionItem[]
deviceArn: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { setPriceAlertSubscriptions } from 'services/notifications/priceAlert/setPriceAlertSubscriptions'
import FCMService from 'services/fcm/FCMService'
import { registerDeviceToNotificationSender } from 'services/notifications/registerDeviceToNotificationSender'

export async function unsubscribeForPriceAlert(): Promise<void> {
const fcmToken = await FCMService.getFCMToken()
const deviceArn = await registerDeviceToNotificationSender(fcmToken)
await setPriceAlertSubscriptions({ tokens: [], deviceArn })
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,14 @@ class WatchlistService {
const prices: Prices = {}

marketsRaw.forEach(market => {
// Create a synthetic internalId for search results since they don't have one
const syntheticInternalId = `search:${market.id}`

tokens.push({
marketType: MarketType.SEARCH,
id: market.id,
id: syntheticInternalId,
coingeckoId: market.id,
platforms: {}, // Search results don't have platform info
symbol: market.symbol,
name: market.name,
logoUri: market.image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import type { Action } from 'redux'
import { ChannelId, NewsChannelId } from 'services/notifications/channels'
import { handleProcessNotificationData } from 'store/notifications/listeners/handleProcessNotificationData'
import { promptEnableNotifications } from 'store/notifications'
import { toggleWatchListFavorite } from 'store/watchlist'
import { setPriceAlertNotifications } from 'store/notifications/listeners/setPriceAlertNotifications'
import { unsubscribeForPriceAlert } from 'services/notifications/priceAlert/unsubscribeForPriceAlert'
import {
onFcmTokenChange,
processNotificationData,
Expand Down Expand Up @@ -189,6 +192,20 @@ export const addNotificationsListeners = (
)
})
})

startListening({
actionCreator: toggleWatchListFavorite,
effect: setPriceAlertNotifications
})

startListening({
matcher: isAnyOf(onNotificationsTurnedOffForNews),
effect: async () => {
await unsubscribeForPriceAlert().catch(reason => {
Logger.error(`[listeners.ts][unsubscribeForPriceAlert]${reason}`)
})
}
})
}

const onNotificationsTurnedOnForBalanceChange = {
Expand Down
Loading
Loading