From 21b042242d8532e27b3170e3d96c8864eb84783d Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 13 Mar 2025 22:54:04 +0100 Subject: [PATCH 01/16] Add token change notification service and listeners Integrates a service and listeners for managing token change notifications tied to watchlist favorites. Includes setup for handling subscription updates, listener connections, and type updates to align with Coingecko IDs. --- .../notifications/tokenChange/service.ts | 19 +++++++++ .../tokenChange/store/listeners.ts | 40 +++++++++++++++++++ .../notifications/tokenChange/types.ts | 9 +++++ .../app/store/middleware/listener.ts | 3 ++ .../core-mobile/app/store/watchlist/slice.ts | 4 +- .../core-mobile/app/store/watchlist/types.ts | 4 +- 6 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 packages/core-mobile/app/services/notifications/tokenChange/service.ts create mode 100644 packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts create mode 100644 packages/core-mobile/app/services/notifications/tokenChange/types.ts diff --git a/packages/core-mobile/app/services/notifications/tokenChange/service.ts b/packages/core-mobile/app/services/notifications/tokenChange/service.ts new file mode 100644 index 0000000000..06b2db4201 --- /dev/null +++ b/packages/core-mobile/app/services/notifications/tokenChange/service.ts @@ -0,0 +1,19 @@ +import Logger from 'utils/Logger' +import { + ITokenChangeNotificationService, + TokenSubscriptionPayload +} from './types' + +export class TokenChangeNotificationService + implements ITokenChangeNotificationService +{ + async setTokenSubscriptions( + payload: TokenSubscriptionPayload + ): Promise { + // TODO: Implement actual API call when the endpoint is available + Logger.warn('Setting token subscriptions:', payload.tokenIds) + } +} + +export const tokenChangeNotificationService = + new TokenChangeNotificationService() diff --git a/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts b/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts new file mode 100644 index 0000000000..affe73d6e4 --- /dev/null +++ b/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts @@ -0,0 +1,40 @@ +import { AppStartListening } from 'store/middleware/listener' +import Logger from 'utils/Logger' +import { tokenChangeNotificationService } from 'services/notifications/tokenChange/service' +import { AnyAction } from '@reduxjs/toolkit' +import { AppListenerEffectAPI } from 'store' +import { + selectWatchlistFavoriteIds, + toggleWatchListFavorite +} from 'store/watchlist' + +const setTokenSubscriptionsForFavorites = async ( + _: AnyAction, + listenerApi: AppListenerEffectAPI +): Promise => { + const state = listenerApi.getState() + const favoriteTokensCoingeckoIds = selectWatchlistFavoriteIds(state) + + Logger.info( + '[services/notifications/tokenChange/store/listeners.ts] Setting token subscriptions for favorites:', + favoriteTokensCoingeckoIds + ) + try { + await tokenChangeNotificationService.setTokenSubscriptions({ + tokenIds: favoriteTokensCoingeckoIds + }) + } catch (error) { + Logger.error( + `[services/notifications/tokenChange/store/listeners.ts][effect]${error}` + ) + } +} + +export const addNotificationsTokenChangeListeners = ( + startListening: AppStartListening +): void => { + startListening({ + actionCreator: toggleWatchListFavorite, + effect: setTokenSubscriptionsForFavorites + }) +} diff --git a/packages/core-mobile/app/services/notifications/tokenChange/types.ts b/packages/core-mobile/app/services/notifications/tokenChange/types.ts new file mode 100644 index 0000000000..5faca14198 --- /dev/null +++ b/packages/core-mobile/app/services/notifications/tokenChange/types.ts @@ -0,0 +1,9 @@ +import { CoingeckoId } from 'store/watchlist' + +export interface TokenSubscriptionPayload { + tokenIds: CoingeckoId[] // List of token addresses to subscribe to +} + +export interface ITokenChangeNotificationService { + setTokenSubscriptions(payload: TokenSubscriptionPayload): Promise +} diff --git a/packages/core-mobile/app/store/middleware/listener.ts b/packages/core-mobile/app/store/middleware/listener.ts index a7504f99fb..e1d86ef4ae 100644 --- a/packages/core-mobile/app/store/middleware/listener.ts +++ b/packages/core-mobile/app/store/middleware/listener.ts @@ -12,6 +12,7 @@ import Logger from 'utils/Logger' import { addNotificationsListeners } from 'store/notifications/listeners/listeners' import { addSeedlessListeners } from 'seedless/store/listeners' import { addWatchlistListeners } from 'store/watchlist/listeners' +import { addNotificationsTokenChangeListeners } from 'services/notifications/tokenChange/store/listeners' import { addAppearanceListeners } from 'store/settings/appearance/listeners' import { addUnifiedBridgeListeners } from 'store/unifiedBridge/listeners' import { AppAddListener, AppStartListening } from 'store/types' @@ -59,6 +60,8 @@ addCurrencyListeners(startListening) addMeldListeners(startListening) +addNotificationsTokenChangeListeners(startListening) + export const addAppListener = addListener as AppAddListener export { listener } diff --git a/packages/core-mobile/app/store/watchlist/slice.ts b/packages/core-mobile/app/store/watchlist/slice.ts index 7209fc9036..68850e4183 100644 --- a/packages/core-mobile/app/store/watchlist/slice.ts +++ b/packages/core-mobile/app/store/watchlist/slice.ts @@ -5,7 +5,7 @@ import { BITCOIN_COINGECKO_ID, ETHEREUM_COINGECKO_ID } from 'consts/coingecko' -import { initialState } from './types' +import { CoingeckoId, initialState } from './types' const DEFAULT_WATCHLIST_FAVORITES = [ ETHEREUM_COINGECKO_ID, @@ -48,7 +48,7 @@ export const selectIsWatchlistFavorite = (coingeckoId: string) => (state: RootState) => state.watchlist.favorites.includes(coingeckoId) -export const selectWatchlistFavoriteIds = (state: RootState): string[] => { +export const selectWatchlistFavoriteIds = (state: RootState): CoingeckoId[] => { return state.watchlist.favorites } diff --git a/packages/core-mobile/app/store/watchlist/types.ts b/packages/core-mobile/app/store/watchlist/types.ts index f8c2c47fee..53a5601fa4 100644 --- a/packages/core-mobile/app/store/watchlist/types.ts +++ b/packages/core-mobile/app/store/watchlist/types.ts @@ -75,6 +75,8 @@ export type TokensAndCharts = { charts: Charts } +export type CoingeckoId = string + export type WatchListFavoriteState = { - favorites: string[] + favorites: CoingeckoId[] } From 1ce32ae4f17b3eabec190f300e3be044e1b43ac0 Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 17 Jul 2025 15:49:14 +0200 Subject: [PATCH 02/16] Implement API call for price alert subscription --- .../DeeplinkContext/utils/handleDeeplink.ts | 15 +++-- .../app/services/fcm/FCMService.ts | 2 +- .../core-mobile/app/services/fcm/types.ts | 3 +- .../notifications/tokenChange/service.ts | 31 ++++++++- .../tokenChange/store/listeners.ts | 64 ++++++++++++++++--- .../notifications/tokenChange/types.ts | 7 +- .../core-mobile/app/store/watchlist/types.ts | 4 +- yarn.lock | 2 +- 8 files changed, 104 insertions(+), 24 deletions(-) diff --git a/packages/core-mobile/app/contexts/DeeplinkContext/utils/handleDeeplink.ts b/packages/core-mobile/app/contexts/DeeplinkContext/utils/handleDeeplink.ts index b1160ebbba..3a9f580c62 100644 --- a/packages/core-mobile/app/contexts/DeeplinkContext/utils/handleDeeplink.ts +++ b/packages/core-mobile/app/contexts/DeeplinkContext/utils/handleDeeplink.ts @@ -69,10 +69,17 @@ 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) { + // Detect if this is an internalId (EIP-155 format) or coingeckoId + const isInternalId = tokenId.startsWith('eip155:') + const marketType = isInternalId + ? MarketType.TRENDING + : MarketType.SEARCH + navigateFromDeeplinkUrl( + `/trackTokenDetail?tokenId=${tokenId}&marketType=${marketType}` + ) + } } else if (action === ACTIONS.OfframpCompleted) { dispatch(offrampSend({ searchParams })) } else if (action === ACTIONS.OnrampCompleted) { diff --git a/packages/core-mobile/app/services/fcm/FCMService.ts b/packages/core-mobile/app/services/fcm/FCMService.ts index b0e80abc95..5239774aef 100644 --- a/packages/core-mobile/app/services/fcm/FCMService.ts +++ b/packages/core-mobile/app/services/fcm/FCMService.ts @@ -138,7 +138,7 @@ class FCMService { } } else if (fcmData.type === NotificationTypes.NEWS) { return { - url: fcmData.url + url: fcmData.urlV2 || fcmData.url // Prioritize urlV2 over url for backward compatibility } } } diff --git a/packages/core-mobile/app/services/fcm/types.ts b/packages/core-mobile/app/services/fcm/types.ts index 4a2b3aab31..40927d2ac9 100644 --- a/packages/core-mobile/app/services/fcm/types.ts +++ b/packages/core-mobile/app/services/fcm/types.ts @@ -36,7 +36,8 @@ export const NewsDataSchema = object({ event: nativeEnum(NewsEvents), title: string(), body: string(), - url: string() + url: string(), + urlV2: string().optional() // New field for notifications with internalId }) export const NotificationPayloadSchema = object({ diff --git a/packages/core-mobile/app/services/notifications/tokenChange/service.ts b/packages/core-mobile/app/services/notifications/tokenChange/service.ts index 06b2db4201..aa9d7327bc 100644 --- a/packages/core-mobile/app/services/notifications/tokenChange/service.ts +++ b/packages/core-mobile/app/services/notifications/tokenChange/service.ts @@ -1,4 +1,6 @@ import Logger from 'utils/Logger' +import Config from 'react-native-config' +import fetchWithAppCheck from 'utils/httpClient' import { ITokenChangeNotificationService, TokenSubscriptionPayload @@ -10,8 +12,33 @@ export class TokenChangeNotificationService async setTokenSubscriptions( payload: TokenSubscriptionPayload ): Promise { - // TODO: Implement actual API call when the endpoint is available - Logger.warn('Setting token subscriptions:', payload.tokenIds) + Logger.info( + '[TokenChangeNotificationService] 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( + '[TokenChangeNotificationService] Successfully subscribed to token price alerts:', + result + ) + } else { + throw new Error(`${response.status}:${response.statusText}`) + } + } catch (error) { + Logger.error( + `[TokenChangeNotificationService] Failed to set token subscriptions:`, + error + ) + } } } diff --git a/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts b/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts index affe73d6e4..707a5d5827 100644 --- a/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts +++ b/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts @@ -1,32 +1,76 @@ -import { AppStartListening } from 'store/middleware/listener' +import { AppStartListening } from 'store/types' import Logger from 'utils/Logger' import { tokenChangeNotificationService } from 'services/notifications/tokenChange/service' import { AnyAction } from '@reduxjs/toolkit' -import { AppListenerEffectAPI } from 'store' +import { AppListenerEffectAPI } from 'store/types' import { selectWatchlistFavoriteIds, toggleWatchListFavorite } from 'store/watchlist' +import FCMService from 'services/fcm/FCMService' +import { registerDeviceToNotificationSender } from 'services/notifications/registerDeviceToNotificationSender' +import NotificationsService from 'services/notifications/NotificationsService' +import { selectNotificationSubscription } from 'store/notifications' +import { ChannelId } from 'services/notifications/channels' const setTokenSubscriptionsForFavorites = async ( _: AnyAction, listenerApi: AppListenerEffectAPI ): Promise => { const state = listenerApi.getState() - const favoriteTokensCoingeckoIds = selectWatchlistFavoriteIds(state) + const userHasEnabledPriceAlertNotifications = selectNotificationSubscription( + ChannelId.PRICE_ALERTS + )(state) + + if (!userHasEnabledPriceAlertNotifications) { + return + } + + const favoriteTokensIds = selectWatchlistFavoriteIds(state) Logger.info( '[services/notifications/tokenChange/store/listeners.ts] Setting token subscriptions for favorites:', - favoriteTokensCoingeckoIds + favoriteTokensIds ) + try { - await tokenChangeNotificationService.setTokenSubscriptions({ - tokenIds: favoriteTokensCoingeckoIds - }) + // Get deviceArn following the same pattern as other notification services + const fcmToken = await FCMService.getFCMToken() + const deviceArn = await registerDeviceToNotificationSender(fcmToken) + + //check if only PRICE_ALERTS notifications are denied + const blockedNotifications = + await NotificationsService.getBlockedNotifications() + if (blockedNotifications.has(ChannelId.PRICE_ALERTS)) { + //unsubscribe from all + await tokenChangeNotificationService.setTokenSubscriptions({ + tokens: [], + deviceArn + }) + return + } + + const tokens = favoriteTokensIds.map(id => ({ + internalId: id + })) + + // Only call the API if we have tokens to subscribe to + if (tokens.length > 0) { + await tokenChangeNotificationService.setTokenSubscriptions({ + tokens, + deviceArn + }) + Logger.info( + `[TokenChange] Successfully subscribed to token price alerts for ${tokens.length} tokens` + ) + } else { + Logger.info( + '[TokenChange] No valid internalIds found for favorite tokens, skipping subscription' + ) + } } catch (error) { - Logger.error( - `[services/notifications/tokenChange/store/listeners.ts][effect]${error}` - ) + // Handle specific APNS/FCM token errors gracefully + Logger.error(`[setTokenSubscriptionsForFavorites]${error}`) } } diff --git a/packages/core-mobile/app/services/notifications/tokenChange/types.ts b/packages/core-mobile/app/services/notifications/tokenChange/types.ts index 5faca14198..1f300dd621 100644 --- a/packages/core-mobile/app/services/notifications/tokenChange/types.ts +++ b/packages/core-mobile/app/services/notifications/tokenChange/types.ts @@ -1,7 +1,10 @@ -import { CoingeckoId } from 'store/watchlist' +export interface TokenSubscriptionItem { + internalId: string // EIP-155 format: eip155:43114-0x +} export interface TokenSubscriptionPayload { - tokenIds: CoingeckoId[] // List of token addresses to subscribe to + tokens: TokenSubscriptionItem[] + deviceArn: string } export interface ITokenChangeNotificationService { diff --git a/packages/core-mobile/app/store/watchlist/types.ts b/packages/core-mobile/app/store/watchlist/types.ts index 53a5601fa4..5b19972fcb 100644 --- a/packages/core-mobile/app/store/watchlist/types.ts +++ b/packages/core-mobile/app/store/watchlist/types.ts @@ -30,7 +30,7 @@ export enum MarketType { } type InternalId = string -type CoingeckoId = string +export type CoingeckoId = string export type MarketToken = | { @@ -75,8 +75,6 @@ export type TokensAndCharts = { charts: Charts } -export type CoingeckoId = string - export type WatchListFavoriteState = { favorites: CoingeckoId[] } diff --git a/yarn.lock b/yarn.lock index b1f3042e3d..bf5d8608d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27800,7 +27800,7 @@ react-native-webview@ava-labs/react-native-webview: peerDependencies: react: "*" react-native: "*" - checksum: ae658f8c94177d1b419ffb73d26bd70474b6c688af507daaabc11451f983f77a906b2aafcdfdd09c96cab846d9722dcaf26a7c22ee7d19fb4cf70578c8812ac5 + checksum: 856a172d7a0046e6d93bf3900031f7743e67103ad7de882acf86db5a90c595a70cb05df54c44ae532a8b3c6e20f3d744771335803b84930e6110d75e323f974b languageName: node linkType: hard From f6843c0f39d79057a0c6a37eb50cd9c4fdd03eba Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 17 Jul 2025 16:14:17 +0200 Subject: [PATCH 03/16] Refactor to integrate feature in existing codebase --- .../notifications/tokenChange/service.ts | 46 ------------------- .../setTokenChangeSubscriptions.ts | 36 +++++++++++++++ .../notifications/tokenChange/types.ts | 4 -- .../tokenChange/unsubscribeForTokenChange.ts | 9 ++++ .../notifications/listeners/listeners.ts | 7 +++ .../setTokenSubscriptionsForFavorites.ts} | 26 +++-------- 6 files changed, 58 insertions(+), 70 deletions(-) delete mode 100644 packages/core-mobile/app/services/notifications/tokenChange/service.ts create mode 100644 packages/core-mobile/app/services/notifications/tokenChange/setTokenChangeSubscriptions.ts create mode 100644 packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts rename packages/core-mobile/app/{services/notifications/tokenChange/store/listeners.ts => store/notifications/listeners/setTokenSubscriptionsForFavorites.ts} (75%) diff --git a/packages/core-mobile/app/services/notifications/tokenChange/service.ts b/packages/core-mobile/app/services/notifications/tokenChange/service.ts deleted file mode 100644 index aa9d7327bc..0000000000 --- a/packages/core-mobile/app/services/notifications/tokenChange/service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Logger from 'utils/Logger' -import Config from 'react-native-config' -import fetchWithAppCheck from 'utils/httpClient' -import { - ITokenChangeNotificationService, - TokenSubscriptionPayload -} from './types' - -export class TokenChangeNotificationService - implements ITokenChangeNotificationService -{ - async setTokenSubscriptions( - payload: TokenSubscriptionPayload - ): Promise { - Logger.info( - '[TokenChangeNotificationService] 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( - '[TokenChangeNotificationService] Successfully subscribed to token price alerts:', - result - ) - } else { - throw new Error(`${response.status}:${response.statusText}`) - } - } catch (error) { - Logger.error( - `[TokenChangeNotificationService] Failed to set token subscriptions:`, - error - ) - } - } -} - -export const tokenChangeNotificationService = - new TokenChangeNotificationService() diff --git a/packages/core-mobile/app/services/notifications/tokenChange/setTokenChangeSubscriptions.ts b/packages/core-mobile/app/services/notifications/tokenChange/setTokenChangeSubscriptions.ts new file mode 100644 index 0000000000..3b08f31b10 --- /dev/null +++ b/packages/core-mobile/app/services/notifications/tokenChange/setTokenChangeSubscriptions.ts @@ -0,0 +1,36 @@ +import { TokenSubscriptionPayload } from 'services/notifications/tokenChange/types' +import Logger from 'utils/Logger' +import fetchWithAppCheck from 'utils/httpClient' +import Config from 'react-native-config' + +export async function setTokenChangeSubscriptions( + payload: TokenSubscriptionPayload +): Promise { + Logger.info( + '[TokenChangeNotificationService] 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( + '[TokenChangeNotificationService] Successfully subscribed to token price alerts:', + result + ) + } else { + throw new Error(`${response.status}:${response.statusText}`) + } + } catch (error) { + Logger.error( + `[TokenChangeNotificationService] Failed to set token subscriptions:`, + error + ) + } +} diff --git a/packages/core-mobile/app/services/notifications/tokenChange/types.ts b/packages/core-mobile/app/services/notifications/tokenChange/types.ts index 1f300dd621..d96d47c51e 100644 --- a/packages/core-mobile/app/services/notifications/tokenChange/types.ts +++ b/packages/core-mobile/app/services/notifications/tokenChange/types.ts @@ -6,7 +6,3 @@ export interface TokenSubscriptionPayload { tokens: TokenSubscriptionItem[] deviceArn: string } - -export interface ITokenChangeNotificationService { - setTokenSubscriptions(payload: TokenSubscriptionPayload): Promise -} diff --git a/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts b/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts new file mode 100644 index 0000000000..0111e1b440 --- /dev/null +++ b/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts @@ -0,0 +1,9 @@ +import { setTokenChangeSubscriptions } from 'services/notifications/tokenChange/setTokenChangeSubscriptions' + +export async function unsubscribeForTokenChange({ + deviceArn +}: { + deviceArn: string +}): Promise { + await setTokenChangeSubscriptions({ tokens: [], deviceArn }) +} diff --git a/packages/core-mobile/app/store/notifications/listeners/listeners.ts b/packages/core-mobile/app/store/notifications/listeners/listeners.ts index 0bf9b8040a..d9f5f2dea0 100644 --- a/packages/core-mobile/app/store/notifications/listeners/listeners.ts +++ b/packages/core-mobile/app/store/notifications/listeners/listeners.ts @@ -12,6 +12,8 @@ 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 { setTokenSubscriptionsForFavorites } from 'store/notifications/listeners/setTokenSubscriptionsForFavorites' import { onFcmTokenChange, processNotificationData, @@ -189,6 +191,11 @@ export const addNotificationsListeners = ( ) }) }) + + startListening({ + actionCreator: toggleWatchListFavorite, + effect: setTokenSubscriptionsForFavorites + }) } const onNotificationsTurnedOnForBalanceChange = { diff --git a/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts b/packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts similarity index 75% rename from packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts rename to packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts index 707a5d5827..2c180c9eaa 100644 --- a/packages/core-mobile/app/services/notifications/tokenChange/store/listeners.ts +++ b/packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts @@ -1,19 +1,16 @@ -import { AppStartListening } from 'store/types' import Logger from 'utils/Logger' -import { tokenChangeNotificationService } from 'services/notifications/tokenChange/service' import { AnyAction } from '@reduxjs/toolkit' import { AppListenerEffectAPI } from 'store/types' -import { - selectWatchlistFavoriteIds, - toggleWatchListFavorite -} from 'store/watchlist' +import { selectWatchlistFavoriteIds } from 'store/watchlist' import FCMService from 'services/fcm/FCMService' import { registerDeviceToNotificationSender } from 'services/notifications/registerDeviceToNotificationSender' import NotificationsService from 'services/notifications/NotificationsService' import { selectNotificationSubscription } from 'store/notifications' import { ChannelId } from 'services/notifications/channels' +import { unsubscribeForTokenChange } from 'services/notifications/tokenChange/unsubscribeForTokenChange' +import { setTokenChangeSubscriptions } from 'services/notifications/tokenChange/setTokenChangeSubscriptions' -const setTokenSubscriptionsForFavorites = async ( +export const setTokenSubscriptionsForFavorites = async ( _: AnyAction, listenerApi: AppListenerEffectAPI ): Promise => { @@ -42,9 +39,7 @@ const setTokenSubscriptionsForFavorites = async ( const blockedNotifications = await NotificationsService.getBlockedNotifications() if (blockedNotifications.has(ChannelId.PRICE_ALERTS)) { - //unsubscribe from all - await tokenChangeNotificationService.setTokenSubscriptions({ - tokens: [], + await unsubscribeForTokenChange({ deviceArn }) return @@ -56,7 +51,7 @@ const setTokenSubscriptionsForFavorites = async ( // Only call the API if we have tokens to subscribe to if (tokens.length > 0) { - await tokenChangeNotificationService.setTokenSubscriptions({ + await setTokenChangeSubscriptions({ tokens, deviceArn }) @@ -73,12 +68,3 @@ const setTokenSubscriptionsForFavorites = async ( Logger.error(`[setTokenSubscriptionsForFavorites]${error}`) } } - -export const addNotificationsTokenChangeListeners = ( - startListening: AppStartListening -): void => { - startListening({ - actionCreator: toggleWatchListFavorite, - effect: setTokenSubscriptionsForFavorites - }) -} From 7025a12bc171ea136263fab1919a03e34b49d357 Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 17 Jul 2025 16:23:26 +0200 Subject: [PATCH 04/16] Ensure correct unsubscription from Token change events --- .../tokenChange/unsubscribeForTokenChange.ts | 10 +++++----- .../app/store/notifications/listeners/listeners.ts | 10 ++++++++++ .../listeners/setTokenSubscriptionsForFavorites.ts | 4 +--- .../listeners/unsubscribeAllNotifications.ts | 4 +++- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts b/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts index 0111e1b440..cc4321344a 100644 --- a/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts +++ b/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts @@ -1,9 +1,9 @@ import { setTokenChangeSubscriptions } from 'services/notifications/tokenChange/setTokenChangeSubscriptions' +import FCMService from 'services/fcm/FCMService' +import { registerDeviceToNotificationSender } from 'services/notifications/registerDeviceToNotificationSender' -export async function unsubscribeForTokenChange({ - deviceArn -}: { - deviceArn: string -}): Promise { +export async function unsubscribeForTokenChange(): Promise { + const fcmToken = await FCMService.getFCMToken() + const deviceArn = await registerDeviceToNotificationSender(fcmToken) await setTokenChangeSubscriptions({ tokens: [], deviceArn }) } diff --git a/packages/core-mobile/app/store/notifications/listeners/listeners.ts b/packages/core-mobile/app/store/notifications/listeners/listeners.ts index d9f5f2dea0..b54ef7e6cd 100644 --- a/packages/core-mobile/app/store/notifications/listeners/listeners.ts +++ b/packages/core-mobile/app/store/notifications/listeners/listeners.ts @@ -14,6 +14,7 @@ import { handleProcessNotificationData } from 'store/notifications/listeners/han import { promptEnableNotifications } from 'store/notifications' import { toggleWatchListFavorite } from 'store/watchlist' import { setTokenSubscriptionsForFavorites } from 'store/notifications/listeners/setTokenSubscriptionsForFavorites' +import { unsubscribeForTokenChange } from 'services/notifications/tokenChange/unsubscribeForTokenChange' import { onFcmTokenChange, processNotificationData, @@ -196,6 +197,15 @@ export const addNotificationsListeners = ( actionCreator: toggleWatchListFavorite, effect: setTokenSubscriptionsForFavorites }) + + startListening({ + matcher: isAnyOf(onNotificationsTurnedOffForNews), + effect: async () => { + await unsubscribeForTokenChange().catch(reason => { + Logger.error(`[listeners.ts][unsubscribeAllNotifications]${reason}`) + }) + } + }) } const onNotificationsTurnedOnForBalanceChange = { diff --git a/packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts b/packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts index 2c180c9eaa..7e766eb13b 100644 --- a/packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts +++ b/packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts @@ -39,9 +39,7 @@ export const setTokenSubscriptionsForFavorites = async ( const blockedNotifications = await NotificationsService.getBlockedNotifications() if (blockedNotifications.has(ChannelId.PRICE_ALERTS)) { - await unsubscribeForTokenChange({ - deviceArn - }) + await unsubscribeForTokenChange() return } diff --git a/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts b/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts index 77a69e42fd..703a8af9f1 100644 --- a/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts +++ b/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts @@ -4,6 +4,7 @@ import { unSubscribeForBalanceChange } from 'services/notifications/balanceChang import { unSubscribeForNews } from 'services/notifications/news/unsubscribeForNews' import messaging from '@react-native-firebase/messaging' import Logger from 'utils/Logger' +import { unsubscribeForTokenChange } from 'services/notifications/tokenChange/unsubscribeForTokenChange' export async function unsubscribeAllNotifications(): Promise { const fcmToken = await FCMService.getFCMToken() @@ -13,7 +14,8 @@ export async function unsubscribeAllNotifications(): Promise { unSubscribeForNews({ deviceArn, channelIds: [] - }) + }), + unsubscribeForTokenChange() ]).catch(error => { //as fallback invalidate token so user doesn't get notifications messaging().deleteToken() From 20d7cddd5b0c3c84043f319191d36cf2a7d1aa79 Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 17 Jul 2025 16:35:57 +0200 Subject: [PATCH 05/16] Refactor: rename "token change" to "price alert" --- .../setPriceAlertSubscriptions.ts} | 10 +++++----- .../notifications/{tokenChange => priceAlert}/types.ts | 0 .../unsubscribeForPriceAlert.ts} | 6 +++--- packages/core-mobile/app/store/middleware/listener.ts | 3 --- .../app/store/notifications/listeners/listeners.ts | 10 +++++----- ...nsForFavorites.ts => setPriceAlertNotifications.ts} | 10 +++++----- .../listeners/unsubscribeAllNotifications.ts | 4 ++-- 7 files changed, 20 insertions(+), 23 deletions(-) rename packages/core-mobile/app/services/notifications/{tokenChange/setTokenChangeSubscriptions.ts => priceAlert/setPriceAlertSubscriptions.ts} (70%) rename packages/core-mobile/app/services/notifications/{tokenChange => priceAlert}/types.ts (100%) rename packages/core-mobile/app/services/notifications/{tokenChange/unsubscribeForTokenChange.ts => priceAlert/unsubscribeForPriceAlert.ts} (54%) rename packages/core-mobile/app/store/notifications/listeners/{setTokenSubscriptionsForFavorites.ts => setPriceAlertNotifications.ts} (85%) diff --git a/packages/core-mobile/app/services/notifications/tokenChange/setTokenChangeSubscriptions.ts b/packages/core-mobile/app/services/notifications/priceAlert/setPriceAlertSubscriptions.ts similarity index 70% rename from packages/core-mobile/app/services/notifications/tokenChange/setTokenChangeSubscriptions.ts rename to packages/core-mobile/app/services/notifications/priceAlert/setPriceAlertSubscriptions.ts index 3b08f31b10..fa907fb537 100644 --- a/packages/core-mobile/app/services/notifications/tokenChange/setTokenChangeSubscriptions.ts +++ b/packages/core-mobile/app/services/notifications/priceAlert/setPriceAlertSubscriptions.ts @@ -1,13 +1,13 @@ -import { TokenSubscriptionPayload } from 'services/notifications/tokenChange/types' +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 setTokenChangeSubscriptions( +export async function setPriceAlertSubscriptions( payload: TokenSubscriptionPayload ): Promise { Logger.info( - '[TokenChangeNotificationService] Setting token subscriptions:', + '[setPriceAlertSubscriptions] Setting token subscriptions:', payload.tokens.map(t => t.internalId) ) @@ -21,7 +21,7 @@ export async function setTokenChangeSubscriptions( if (response.ok) { const result = await response.json() Logger.info( - '[TokenChangeNotificationService] Successfully subscribed to token price alerts:', + '[setPriceAlertSubscriptions] Successfully subscribed to token price alerts:', result ) } else { @@ -29,7 +29,7 @@ export async function setTokenChangeSubscriptions( } } catch (error) { Logger.error( - `[TokenChangeNotificationService] Failed to set token subscriptions:`, + `[setPriceAlertSubscriptions] Failed to set token subscriptions:`, error ) } diff --git a/packages/core-mobile/app/services/notifications/tokenChange/types.ts b/packages/core-mobile/app/services/notifications/priceAlert/types.ts similarity index 100% rename from packages/core-mobile/app/services/notifications/tokenChange/types.ts rename to packages/core-mobile/app/services/notifications/priceAlert/types.ts diff --git a/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts b/packages/core-mobile/app/services/notifications/priceAlert/unsubscribeForPriceAlert.ts similarity index 54% rename from packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts rename to packages/core-mobile/app/services/notifications/priceAlert/unsubscribeForPriceAlert.ts index cc4321344a..fb3dc85cd0 100644 --- a/packages/core-mobile/app/services/notifications/tokenChange/unsubscribeForTokenChange.ts +++ b/packages/core-mobile/app/services/notifications/priceAlert/unsubscribeForPriceAlert.ts @@ -1,9 +1,9 @@ -import { setTokenChangeSubscriptions } from 'services/notifications/tokenChange/setTokenChangeSubscriptions' +import { setPriceAlertSubscriptions } from 'services/notifications/priceAlert/setPriceAlertSubscriptions' import FCMService from 'services/fcm/FCMService' import { registerDeviceToNotificationSender } from 'services/notifications/registerDeviceToNotificationSender' -export async function unsubscribeForTokenChange(): Promise { +export async function unsubscribeForPriceAlert(): Promise { const fcmToken = await FCMService.getFCMToken() const deviceArn = await registerDeviceToNotificationSender(fcmToken) - await setTokenChangeSubscriptions({ tokens: [], deviceArn }) + await setPriceAlertSubscriptions({ tokens: [], deviceArn }) } diff --git a/packages/core-mobile/app/store/middleware/listener.ts b/packages/core-mobile/app/store/middleware/listener.ts index e1d86ef4ae..a7504f99fb 100644 --- a/packages/core-mobile/app/store/middleware/listener.ts +++ b/packages/core-mobile/app/store/middleware/listener.ts @@ -12,7 +12,6 @@ import Logger from 'utils/Logger' import { addNotificationsListeners } from 'store/notifications/listeners/listeners' import { addSeedlessListeners } from 'seedless/store/listeners' import { addWatchlistListeners } from 'store/watchlist/listeners' -import { addNotificationsTokenChangeListeners } from 'services/notifications/tokenChange/store/listeners' import { addAppearanceListeners } from 'store/settings/appearance/listeners' import { addUnifiedBridgeListeners } from 'store/unifiedBridge/listeners' import { AppAddListener, AppStartListening } from 'store/types' @@ -60,8 +59,6 @@ addCurrencyListeners(startListening) addMeldListeners(startListening) -addNotificationsTokenChangeListeners(startListening) - export const addAppListener = addListener as AppAddListener export { listener } diff --git a/packages/core-mobile/app/store/notifications/listeners/listeners.ts b/packages/core-mobile/app/store/notifications/listeners/listeners.ts index b54ef7e6cd..968f507196 100644 --- a/packages/core-mobile/app/store/notifications/listeners/listeners.ts +++ b/packages/core-mobile/app/store/notifications/listeners/listeners.ts @@ -13,8 +13,8 @@ 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 { setTokenSubscriptionsForFavorites } from 'store/notifications/listeners/setTokenSubscriptionsForFavorites' -import { unsubscribeForTokenChange } from 'services/notifications/tokenChange/unsubscribeForTokenChange' +import { setPriceAlertNotifications } from 'store/notifications/listeners/setPriceAlertNotifications' +import { unsubscribeForPriceAlert } from 'services/notifications/priceAlert/unsubscribeForPriceAlert' import { onFcmTokenChange, processNotificationData, @@ -195,14 +195,14 @@ export const addNotificationsListeners = ( startListening({ actionCreator: toggleWatchListFavorite, - effect: setTokenSubscriptionsForFavorites + effect: setPriceAlertNotifications }) startListening({ matcher: isAnyOf(onNotificationsTurnedOffForNews), effect: async () => { - await unsubscribeForTokenChange().catch(reason => { - Logger.error(`[listeners.ts][unsubscribeAllNotifications]${reason}`) + await unsubscribeForPriceAlert().catch(reason => { + Logger.error(`[listeners.ts][unsubscribeForPriceAlert]${reason}`) }) } }) diff --git a/packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts b/packages/core-mobile/app/store/notifications/listeners/setPriceAlertNotifications.ts similarity index 85% rename from packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts rename to packages/core-mobile/app/store/notifications/listeners/setPriceAlertNotifications.ts index 7e766eb13b..270a9e8430 100644 --- a/packages/core-mobile/app/store/notifications/listeners/setTokenSubscriptionsForFavorites.ts +++ b/packages/core-mobile/app/store/notifications/listeners/setPriceAlertNotifications.ts @@ -7,10 +7,10 @@ import { registerDeviceToNotificationSender } from 'services/notifications/regis import NotificationsService from 'services/notifications/NotificationsService' import { selectNotificationSubscription } from 'store/notifications' import { ChannelId } from 'services/notifications/channels' -import { unsubscribeForTokenChange } from 'services/notifications/tokenChange/unsubscribeForTokenChange' -import { setTokenChangeSubscriptions } from 'services/notifications/tokenChange/setTokenChangeSubscriptions' +import { unsubscribeForPriceAlert } from 'services/notifications/priceAlert/unsubscribeForPriceAlert' +import { setPriceAlertSubscriptions } from 'services/notifications/priceAlert/setPriceAlertSubscriptions' -export const setTokenSubscriptionsForFavorites = async ( +export const setPriceAlertNotifications = async ( _: AnyAction, listenerApi: AppListenerEffectAPI ): Promise => { @@ -39,7 +39,7 @@ export const setTokenSubscriptionsForFavorites = async ( const blockedNotifications = await NotificationsService.getBlockedNotifications() if (blockedNotifications.has(ChannelId.PRICE_ALERTS)) { - await unsubscribeForTokenChange() + await unsubscribeForPriceAlert() return } @@ -49,7 +49,7 @@ export const setTokenSubscriptionsForFavorites = async ( // Only call the API if we have tokens to subscribe to if (tokens.length > 0) { - await setTokenChangeSubscriptions({ + await setPriceAlertSubscriptions({ tokens, deviceArn }) diff --git a/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts b/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts index 703a8af9f1..3e8a7266d7 100644 --- a/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts +++ b/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts @@ -4,7 +4,7 @@ import { unSubscribeForBalanceChange } from 'services/notifications/balanceChang import { unSubscribeForNews } from 'services/notifications/news/unsubscribeForNews' import messaging from '@react-native-firebase/messaging' import Logger from 'utils/Logger' -import { unsubscribeForTokenChange } from 'services/notifications/tokenChange/unsubscribeForTokenChange' +import { unsubscribeForPriceAlert } from 'services/notifications/priceAlert/unsubscribeForPriceAlert' export async function unsubscribeAllNotifications(): Promise { const fcmToken = await FCMService.getFCMToken() @@ -15,7 +15,7 @@ export async function unsubscribeAllNotifications(): Promise { deviceArn, channelIds: [] }), - unsubscribeForTokenChange() + unsubscribeForPriceAlert() ]).catch(error => { //as fallback invalidate token so user doesn't get notifications messaging().deleteToken() From c93c51205487d3d9c3fe349e76307f9b666b9a9c Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 24 Jul 2025 14:31:16 +0200 Subject: [PATCH 06/16] Remove url from NewsDataSchema, replaced by urlV2 --- packages/core-mobile/app/services/fcm/FCMService.ts | 2 +- packages/core-mobile/app/services/fcm/types.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core-mobile/app/services/fcm/FCMService.ts b/packages/core-mobile/app/services/fcm/FCMService.ts index 5239774aef..7121aa4248 100644 --- a/packages/core-mobile/app/services/fcm/FCMService.ts +++ b/packages/core-mobile/app/services/fcm/FCMService.ts @@ -138,7 +138,7 @@ class FCMService { } } else if (fcmData.type === NotificationTypes.NEWS) { return { - url: fcmData.urlV2 || fcmData.url // Prioritize urlV2 over url for backward compatibility + url: fcmData.urlV2 } } } diff --git a/packages/core-mobile/app/services/fcm/types.ts b/packages/core-mobile/app/services/fcm/types.ts index 40927d2ac9..3e0cd52ad4 100644 --- a/packages/core-mobile/app/services/fcm/types.ts +++ b/packages/core-mobile/app/services/fcm/types.ts @@ -36,8 +36,7 @@ export const NewsDataSchema = object({ event: nativeEnum(NewsEvents), title: string(), body: string(), - url: string(), - urlV2: string().optional() // New field for notifications with internalId + urlV2: string() }) export const NotificationPayloadSchema = object({ From 9cbfe2aeb284ac8d5f4c12c5703cbb22fe405457 Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 24 Jul 2025 14:33:30 +0200 Subject: [PATCH 07/16] Use allSettled --- .../listeners/unsubscribeAllNotifications.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts b/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts index 3e8a7266d7..93504ff580 100644 --- a/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts +++ b/packages/core-mobile/app/store/notifications/listeners/unsubscribeAllNotifications.ts @@ -9,17 +9,19 @@ import { unsubscribeForPriceAlert } from 'services/notifications/priceAlert/unsu export async function unsubscribeAllNotifications(): Promise { const fcmToken = await FCMService.getFCMToken() const deviceArn = await registerDeviceToNotificationSender(fcmToken) - await Promise.all([ + const result = await Promise.allSettled([ unSubscribeForBalanceChange({ deviceArn }), unSubscribeForNews({ deviceArn, channelIds: [] }), unsubscribeForPriceAlert() - ]).catch(error => { + ]) + if (result.some(r => r.status === 'rejected')) { //as fallback invalidate token so user doesn't get notifications - messaging().deleteToken() - Logger.error(`[unsubscribeAllNotifications.ts][unsubscribe]${error}`) - throw error - }) + await messaging().deleteToken() + Logger.error( + `[unsubscribeAllNotifications.ts][unsubscribe] failed to unsubscribe from notifications` + ) + } } From c54bafff430d277e6a70f51096824b1ee708d370 Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 24 Jul 2025 14:40:33 +0200 Subject: [PATCH 08/16] Use internal ids wherever appropriate --- .../DeeplinkContext/utils/handleDeeplink.ts | 8 ++----- .../app/hooks/watchlist/useWatchlist.ts | 22 +++++++++++++++++-- .../app/new/common/hooks/useTokenDetails.ts | 5 ++--- .../core-mobile/app/store/watchlist/types.ts | 9 ++++---- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/core-mobile/app/contexts/DeeplinkContext/utils/handleDeeplink.ts b/packages/core-mobile/app/contexts/DeeplinkContext/utils/handleDeeplink.ts index 3a9f580c62..8b99b34667 100644 --- a/packages/core-mobile/app/contexts/DeeplinkContext/utils/handleDeeplink.ts +++ b/packages/core-mobile/app/contexts/DeeplinkContext/utils/handleDeeplink.ts @@ -71,13 +71,9 @@ export const handleDeeplink = ({ } else if (action === ACTIONS.WatchList) { const tokenId = pathname.split('/')[1] if (tokenId) { - // Detect if this is an internalId (EIP-155 format) or coingeckoId - const isInternalId = tokenId.startsWith('eip155:') - const marketType = isInternalId - ? MarketType.TRENDING - : MarketType.SEARCH + // All watchlist tokens now use internalId format navigateFromDeeplinkUrl( - `/trackTokenDetail?tokenId=${tokenId}&marketType=${marketType}` + `/trackTokenDetail?tokenId=${tokenId}&marketType=${MarketType.TRENDING}` ) } } else if (action === ACTIONS.OfframpCompleted) { diff --git a/packages/core-mobile/app/hooks/watchlist/useWatchlist.ts b/packages/core-mobile/app/hooks/watchlist/useWatchlist.ts index e284f0f19a..95a033cc56 100644 --- a/packages/core-mobile/app/hooks/watchlist/useWatchlist.ts +++ b/packages/core-mobile/app/hooks/watchlist/useWatchlist.ts @@ -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(() => { @@ -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 => { diff --git a/packages/core-mobile/app/new/common/hooks/useTokenDetails.ts b/packages/core-mobile/app/new/common/hooks/useTokenDetails.ts index 1cfbf4b8d3..533c7499a4 100644 --- a/packages/core-mobile/app/new/common/hooks/useTokenDetails.ts +++ b/packages/core-mobile/app/new/common/hooks/useTokenDetails.ts @@ -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 ?? '' const chainId = marketType === MarketType.SEARCH ? undefined : getTokenChainId(token) diff --git a/packages/core-mobile/app/store/watchlist/types.ts b/packages/core-mobile/app/store/watchlist/types.ts index 5b19972fcb..81a1d2ff28 100644 --- a/packages/core-mobile/app/store/watchlist/types.ts +++ b/packages/core-mobile/app/store/watchlist/types.ts @@ -29,8 +29,7 @@ export enum MarketType { SEARCH = 'SEARCH' // these are the tokens that match the search query (fetched via Coingecko directly) } -type InternalId = string -export type CoingeckoId = string +export type InternalId = string export type MarketToken = | { @@ -66,9 +65,9 @@ export type PriceData = { vol24: number } -export type Charts = { [coingeckoID: string]: ChartData } +export type Charts = { [tokenId: string]: ChartData } -export type Prices = { [coingeckoID: string]: PriceData } +export type Prices = { [tokenId: string]: PriceData } export type TokensAndCharts = { tokens: Record @@ -76,5 +75,5 @@ export type TokensAndCharts = { } export type WatchListFavoriteState = { - favorites: CoingeckoId[] + favorites: InternalId[] } From e9fc43f43fef4437210894c1ccea46b509a7a31c Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 24 Jul 2025 14:47:06 +0200 Subject: [PATCH 09/16] Refactor and cleanup watchlist/slice to use internalIds --- .../app/consts/internalTokenIds.ts | 7 +++ .../core-mobile/app/store/watchlist/slice.ts | 54 ++++++++----------- 2 files changed, 28 insertions(+), 33 deletions(-) create mode 100644 packages/core-mobile/app/consts/internalTokenIds.ts diff --git a/packages/core-mobile/app/consts/internalTokenIds.ts b/packages/core-mobile/app/consts/internalTokenIds.ts new file mode 100644 index 0000000000..0cbbf2b60b --- /dev/null +++ b/packages/core-mobile/app/consts/internalTokenIds.ts @@ -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' diff --git a/packages/core-mobile/app/store/watchlist/slice.ts b/packages/core-mobile/app/store/watchlist/slice.ts index 68850e4183..415214de2a 100644 --- a/packages/core-mobile/app/store/watchlist/slice.ts +++ b/packages/core-mobile/app/store/watchlist/slice.ts @@ -1,16 +1,16 @@ import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from 'store/types' import { - AVAX_COINGECKO_ID, - BITCOIN_COINGECKO_ID, - ETHEREUM_COINGECKO_ID -} from 'consts/coingecko' -import { CoingeckoId, initialState } from './types' + AVAX_NATIVE_ID, + BITCOIN_NATIVE_ID, + ETHEREUM_NATIVE_ID +} from 'consts/internalTokenIds' +import { initialState, InternalId } from './types' const DEFAULT_WATCHLIST_FAVORITES = [ - ETHEREUM_COINGECKO_ID, - BITCOIN_COINGECKO_ID, - AVAX_COINGECKO_ID + ETHEREUM_NATIVE_ID, + BITCOIN_NATIVE_ID, + AVAX_NATIVE_ID ] export const reducerName = 'watchlist' @@ -19,15 +19,14 @@ export const watchlistSlice = createSlice({ name: reducerName, initialState, reducers: { - toggleFavorite: (state, action: PayloadAction) => { - const tokenId = action.payload - - if (!state.favorites.includes(tokenId)) { - // set favorite - state.favorites.push(tokenId) + toggleWatchListFavorite: (state, action: PayloadAction) => { + const index = state.favorites.findIndex( + coingeckoId => coingeckoId === action.payload + ) + if (index !== -1) { + state.favorites.splice(index, 1) } else { - // unset favorite - state.favorites = state.favorites.filter(id => id !== tokenId) + state.favorites.push(action.payload) } }, addDefaultWatchlistFavorites: state => { @@ -36,31 +35,20 @@ export const watchlistSlice = createSlice({ state.favorites = [tokenId, ...state.favorites] } }) - }, - reorderFavorites: (state, action: PayloadAction) => { - state.favorites = action.payload } } }) // selectors export const selectIsWatchlistFavorite = - (coingeckoId: string) => (state: RootState) => - state.watchlist.favorites.includes(coingeckoId) - -export const selectWatchlistFavoriteIds = (state: RootState): CoingeckoId[] => { - return state.watchlist.favorites -} + (internalId: InternalId) => (state: RootState) => + state.watchlist.favorites.includes(internalId) -export const selectWatchlistFavoritesIsEmpty = (state: RootState): boolean => - state.watchlist.favorites.length === 0 +export const selectWatchlistFavoriteIds = (state: RootState): InternalId[] => + state.watchlist.favorites -// actions -export const { - toggleFavorite: toggleWatchListFavorite, - reorderFavorites, - addDefaultWatchlistFavorites -} = watchlistSlice.actions +export const { toggleWatchListFavorite, addDefaultWatchlistFavorites } = + watchlistSlice.actions export const fetchWatchlist = createAction(`${reducerName}/fetchWatchlist`) From 2c136c2716c4c4688154a37365f850f6425a8ed4 Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 24 Jul 2025 14:50:32 +0200 Subject: [PATCH 10/16] Refactor MarketType.SEARCH to use internal ids --- .../services/watchlist/WatchlistService.ts | 6 ++- .../core-mobile/app/store/watchlist/types.ts | 41 +++++++------------ 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/packages/core-mobile/app/services/watchlist/WatchlistService.ts b/packages/core-mobile/app/services/watchlist/WatchlistService.ts index d9f184b3c2..53d861a4a6 100644 --- a/packages/core-mobile/app/services/watchlist/WatchlistService.ts +++ b/packages/core-mobile/app/services/watchlist/WatchlistService.ts @@ -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, diff --git a/packages/core-mobile/app/store/watchlist/types.ts b/packages/core-mobile/app/store/watchlist/types.ts index 81a1d2ff28..44f8fda740 100644 --- a/packages/core-mobile/app/store/watchlist/types.ts +++ b/packages/core-mobile/app/store/watchlist/types.ts @@ -26,37 +26,24 @@ export const initialState: WatchListFavoriteState = { export enum MarketType { TOP = 'TOP', // these are the top tokens in the market + additional tokens in our database TRENDING = 'TRENDING', // these are the trending avalanche tokens in the market - SEARCH = 'SEARCH' // these are the tokens that match the search query (fetched via Coingecko directly) + SEARCH = 'SEARCH' // these are the tokens that match the search query (now also using internalId) } export type InternalId = string -export type MarketToken = - | { - id: InternalId - coingeckoId: string | null | undefined - platforms: Record | Record - marketType: MarketType.TOP | MarketType.TRENDING - symbol: string - name: string - logoUri?: string - testID?: string - currentPrice?: number - priceChange24h?: number - priceChangePercentage24h?: number - } - | { - id: CoingeckoId - coingeckoId: string - marketType: MarketType.SEARCH - symbol: string - name: string - logoUri?: string - testID?: string - currentPrice?: number - priceChange24h?: number - priceChangePercentage24h?: number - } +export type MarketToken = { + id: InternalId + coingeckoId: string | null | undefined + platforms: Record | Record + marketType: MarketType + symbol: string + name: string + logoUri?: string + testID?: string + currentPrice?: number + priceChange24h?: number + priceChangePercentage24h?: number +} export type PriceData = { priceInCurrency: number From 51930434dcf6a8a77b562b910bfc647ec120821e Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 24 Jul 2025 14:57:06 +0200 Subject: [PATCH 11/16] Refactor useMigrateFavoriteIds to remove coingeckoId based fav even if we dont find replacement --- .../market/hooks/useMigrateFavoriteIds.ts | 35 +++++++++++-------- .../core-mobile/app/store/viewOnce/types.ts | 3 +- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/core-mobile/app/new/features/track/market/hooks/useMigrateFavoriteIds.ts b/packages/core-mobile/app/new/features/track/market/hooks/useMigrateFavoriteIds.ts index 9887eac681..a21645131d 100644 --- a/packages/core-mobile/app/new/features/track/market/hooks/useMigrateFavoriteIds.ts +++ b/packages/core-mobile/app/new/features/track/market/hooks/useMigrateFavoriteIds.ts @@ -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 @@ -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(() => { @@ -41,30 +42,34 @@ export const useMigrateFavoriteIds = (): { return favoriteIds.forEach((favoriteId: string) => { + // Check if this favorite is already in internalId format + if (favoriteId.includes(':')) { + // 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, diff --git a/packages/core-mobile/app/store/viewOnce/types.ts b/packages/core-mobile/app/store/viewOnce/types.ts index aac4225bb2..fd1d25ad5f 100644 --- a/packages/core-mobile/app/store/viewOnce/types.ts +++ b/packages/core-mobile/app/store/viewOnce/types.ts @@ -11,7 +11,8 @@ export enum ViewOnceKey { SWAP_ONBOARDING, SEND_ONBOARDING, MIGRATE_TOKEN_FAVORITE_IDS, - AUTO_ENABLE_L2_CHAINS + AUTO_ENABLE_L2_CHAINS, + MIGRATE_TOKEN_FAVORITE_IDSv2 } export type ViewOnceObjectType = { From 4cd39b1f4da5095bb9874d579148f8c9f1de5345 Mon Sep 17 00:00:00 2001 From: Neven Date: Thu, 24 Jul 2025 14:57:31 +0200 Subject: [PATCH 12/16] Call useMigrateFavoriteIds as early as possible --- .../new/features/track/market/components/FavoriteScreen.tsx | 4 +--- packages/core-mobile/app/new/routes/(signedIn)/_layout.tsx | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-mobile/app/new/features/track/market/components/FavoriteScreen.tsx b/packages/core-mobile/app/new/features/track/market/components/FavoriteScreen.tsx index 7a401626de..48ad621db9 100644 --- a/packages/core-mobile/app/new/features/track/market/components/FavoriteScreen.tsx +++ b/packages/core-mobile/app/new/features/track/market/components/FavoriteScreen.tsx @@ -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' @@ -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) @@ -46,7 +44,7 @@ const FavoriteScreen = ({ ) }, [containerStyle.minHeight, emptyComponent]) - if (isLoadingFavorites || !hasMigratedFavoriteIds) { + if (isLoadingFavorites) { return ( + ) From 6a1a8978d764bce9fff8c48223879dc0e2518626 Mon Sep 17 00:00:00 2001 From: Neven Date: Fri, 25 Jul 2025 13:00:50 +0200 Subject: [PATCH 13/16] Include NATIVE as part of internalId namespace when migrating --- .../new/features/track/market/hooks/useMigrateFavoriteIds.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-mobile/app/new/features/track/market/hooks/useMigrateFavoriteIds.ts b/packages/core-mobile/app/new/features/track/market/hooks/useMigrateFavoriteIds.ts index a21645131d..3cfe643226 100644 --- a/packages/core-mobile/app/new/features/track/market/hooks/useMigrateFavoriteIds.ts +++ b/packages/core-mobile/app/new/features/track/market/hooks/useMigrateFavoriteIds.ts @@ -43,7 +43,7 @@ export const useMigrateFavoriteIds = (): { favoriteIds.forEach((favoriteId: string) => { // Check if this favorite is already in internalId format - if (favoriteId.includes(':')) { + if (favoriteId.includes(':') || favoriteId.includes('NATIVE')) { // Already in internalId format, skip migration return } From f161d434669bd3292312825c5547e698f36641b3 Mon Sep 17 00:00:00 2001 From: Neven Date: Fri, 25 Jul 2025 18:14:07 +0200 Subject: [PATCH 14/16] MigrateFavoriteIds initiator --- .../new/common/components/MigrateFavoriteIds.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/core-mobile/app/new/common/components/MigrateFavoriteIds.tsx diff --git a/packages/core-mobile/app/new/common/components/MigrateFavoriteIds.tsx b/packages/core-mobile/app/new/common/components/MigrateFavoriteIds.tsx new file mode 100644 index 0000000000..7ba11db4e8 --- /dev/null +++ b/packages/core-mobile/app/new/common/components/MigrateFavoriteIds.tsx @@ -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 +} From 0de31c495c4508c3e4b04bfebc5e98e25a59041a Mon Sep 17 00:00:00 2001 From: Neven Date: Fri, 25 Jul 2025 19:37:34 +0200 Subject: [PATCH 15/16] Fix extracting coingeckoId --- packages/core-mobile/app/new/common/hooks/useTokenDetails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-mobile/app/new/common/hooks/useTokenDetails.ts b/packages/core-mobile/app/new/common/hooks/useTokenDetails.ts index 533c7499a4..8c0f6f6192 100644 --- a/packages/core-mobile/app/new/common/hooks/useTokenDetails.ts +++ b/packages/core-mobile/app/new/common/hooks/useTokenDetails.ts @@ -75,7 +75,7 @@ export const useTokenDetails = ({ const token = getMarketTokenById(tokenId) // All tokens now use internalId, but we still need coingeckoId for Price API calls - const coingeckoId = token?.coingeckoId ?? '' + const coingeckoId = token?.coingeckoId ?? tokenId.split(':')[1] ?? '' const chainId = marketType === MarketType.SEARCH ? undefined : getTokenChainId(token) From 5dfda70af53fea4744da5d13bca99948a9087c80 Mon Sep 17 00:00:00 2001 From: Neven Date: Fri, 25 Jul 2025 19:38:47 +0200 Subject: [PATCH 16/16] Fix displaying token info for TrackTokenDetailScreen --- .../new/features/track/screens/TrackTokenDetailScreen.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-mobile/app/new/features/track/screens/TrackTokenDetailScreen.tsx b/packages/core-mobile/app/new/features/track/screens/TrackTokenDetailScreen.tsx index 28016b9878..733fcd3b5f 100644 --- a/packages/core-mobile/app/new/features/track/screens/TrackTokenDetailScreen.tsx +++ b/packages/core-mobile/app/new/features/track/screens/TrackTokenDetailScreen.tsx @@ -392,9 +392,9 @@ const TrackTokenDetailScreen = (): JSX.Element => {