Skip to content

Commit

Permalink
Add support for new notification subscription endpoint
Browse files Browse the repository at this point in the history
Includes retries if the current FCM token is out of date.
  • Loading branch information
jinchung committed Nov 29, 2023
1 parent 93ebbc4 commit 2d7be88
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 1 deletion.
1 change: 1 addition & 0 deletions globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ declare module 'react-native-dotenv' {
export const CLOUDINARY_API_KEY: string;
export const CLOUDINARY_API_SECRET: string;
export const CLOUDINARY_CLOUD_NAME: string;
export const NOTIFICATIONS_API_KEY: string;
export const PINATA_API_KEY: string;
export const PINATA_API_SECRET: string;
export const PINATA_API_URL: string;
Expand Down
151 changes: 150 additions & 1 deletion src/notifications/settings/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,97 @@
import { NOTIFICATIONS_API_KEY } from 'react-native-dotenv';
import { logger, RainbowError } from '@/logger';
import { RainbowNetworks } from '@/networks';
import {
NotificationTopicType,
NotificationSubscriptionWalletsType,
WalletNotificationSettings,
} from '@/notifications/settings/types';
import { getFCMToken, saveFCMToken } from '@/notifications/tokens';
import messaging from '@react-native-firebase/messaging';
import { trackChangedNotificationSettings } from '@/notifications/analytics';
import { NotificationTopic } from '@/notifications/settings/constants';
import { logger } from '@/logger';
import { rainbowFetch } from '@/rainbow-fetch';

const NOTIFICATION_SUBSCRIPTIONS_URL =
'https://notifications.p.rainbow.me/api/v1/subscriptions';

const INVALID_FCM_TOKEN_ERROR =
'failed to validate FCM token: invalid or expired FCM token';

type NotificationsSubscriptionResponse = {
error: boolean;
shouldRetry: boolean;
};

const updateNotificationSubscription = async (
firebaseToken: string,
wallets: NotificationSubscriptionWalletsType[]
): Promise<NotificationsSubscriptionResponse> => {
try {
const options = {
firebase_token: firebaseToken,
wallets: wallets,
};
await rainbowFetch(NOTIFICATION_SUBSCRIPTIONS_URL, {
method: 'put',
body: JSON.stringify(options),
headers: {
Authorization: `Bearer ${NOTIFICATIONS_API_KEY}`,
},
});

// success
return {
error: false,
shouldRetry: false,
};
} catch (error: any) {
// if INVALID_FCM_TOKEN_ERROR message, retry with updated FCM token
if (error.message === INVALID_FCM_TOKEN_ERROR) {
return {
error: true,
shouldRetry: true,
};
}

logger.error(new RainbowError('Failed to subscribe to notifications'), {
message: (error as Error).message,
});

return {
error: true,
shouldRetry: false,
};
}
};

const updateNotificationSubscriptionWithRetry = async (
firebaseToken: string,
wallets: NotificationSubscriptionWalletsType[]
): Promise<boolean> => {
const subscriptionResponse = await updateNotificationSubscription(
firebaseToken,
wallets
);

if (!subscriptionResponse.error) {
// success
return true;
} else if (subscriptionResponse.shouldRetry) {
// retry with an updated FCM token
await saveFCMToken();
const refreshedFirebaseToken = await getFCMToken();
if (!refreshedFirebaseToken) return false;

const subscriptionRetryResponse = await updateNotificationSubscription(
refreshedFirebaseToken,
wallets
);
return !subscriptionRetryResponse.error;
} else {
return false;
}
};

/**
Firebase functions for subscribing/unsubscribing to topics.
Expand All @@ -30,6 +116,69 @@ export const subscribeWalletToAllEnabledTopics = (
);
};

// returns updated wallet settings on success, undefined otherwise
export const publishWalletSettings = async (
walletSettings: WalletNotificationSettings[]
): Promise<WalletNotificationSettings[] | undefined> => {
try {
const subscriptionPayload = parseWalletSettings(walletSettings);
let firebaseToken = await getFCMToken();

// refresh the FCM token if not found
if (!firebaseToken) {
await saveFCMToken();
firebaseToken = await getFCMToken();
if (!firebaseToken) return;
}

const success = await updateNotificationSubscriptionWithRetry(
firebaseToken,
subscriptionPayload
);
if (success) {
return walletSettings.map(setting => {
return {
...setting,
successfullyFinishedInitialSubscription: true,
};
});
} else {
return;
}
} catch (e: any) {
logger.error(
new RainbowError('Failed to publish wallet notification settings'),
{
message: (e as Error).message,
}
);

return;
}
};

const parseWalletSettings = (
walletSettings: WalletNotificationSettings[]
): NotificationSubscriptionWalletsType[] => {
return walletSettings.flatMap(setting => {
const topics = Object.keys(setting.topics).filter(
topic => !!setting.topics[topic]
);
const notificationChainIds = RainbowNetworks.filter(
network => network.enabled && network.features.notifications
).map(network => network.id);

return notificationChainIds.map(chainId => {
return {
type: setting.type,
chain_id: chainId,
address: setting.address.toLowerCase(),
transaction_action_types: topics,
};
});
});
};

export const unsubscribeWalletFromAllNotificationTopics = (
type: string,
chainId: number,
Expand Down
12 changes: 12 additions & 0 deletions src/notifications/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ export type WalletNotificationTopics = {
[key: NotificationTopicType]: boolean;
};

export type NotificationSubscriptionWalletsType = {
type: NotificationRelationshipType;
chain_id: number;
address: string;
transaction_action_types: NotificationTopicType[];
};

export type NotificationSubscriptionType = {
firebase_token: string;
wallets: NotificationSubscriptionWalletsType[];
};

export type WalletNotificationSettings = {
address: string;
topics: WalletNotificationTopics;
Expand Down

0 comments on commit 2d7be88

Please sign in to comment.