diff --git a/src/App.tsx b/src/App.tsx index a4ae718e64..19187245c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ import Header from 'structure/Header'; import MasterInterfacePage from 'structure/MasterInterfacePage'; import Navigation from 'structure/Navigation'; import { setIndex, setRun, setWelcomeNotifsEmpty } from './redux/slices/userJourneySlice'; +import { useBrowserNotification } from 'hooks/useBrowserNotification'; // Internal Configs import GLOBALS from 'config/Globals'; @@ -172,6 +173,8 @@ export default function App() { setcurrentTime(now); }, []); + useBrowserNotification(); + useEffect(() => { if (!account) return; dispatch(resetSpamSlice()); diff --git a/src/api/index.js b/src/api/index.js index 97260fb187..4d7b7898e1 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -7,6 +7,7 @@ import { appConfig } from 'config/index.js'; // Constants const BASE_URL = appConfig.apiUrl; const TOOLING_BASE_URL = appConfig.toolingApiUrl; +const DELIVERY_NODE_BASE_URL = appConfig.deliveryNodeApiUrl; /** * A function used to make get requests throughout the entire application @@ -55,4 +56,18 @@ export const toolingPostReq = async (path, obj) => { } }; +export const deliveryNodePostReq = async (path, obj) => { + try { + const response = await axios.post(DELIVERY_NODE_BASE_URL + path, obj, { + headers: { + 'Content-Type': 'application/json', + }, + }); + return response; + } catch (error) { + console.error(error.response.data); + throw error.response.data; + } +}; + export * from './ipfs'; diff --git a/src/config/config-alpha.js b/src/config/config-alpha.js index d55b7bfb4e..b17a8620a6 100644 --- a/src/config/config-alpha.js +++ b/src/config/config-alpha.js @@ -14,6 +14,7 @@ export const config = { apiUrl: 'https://backend.epns.io/apis', w2wApiUrl: 'https://backend.epns.io/apis', toolingApiUrl: 'https://tooling.epns.io/apis', + deliveryNodeApiUrl: 'https://delivery-prod.epns.io/apis', ipfsInfuraAPIKey: import.meta.env.VITE_APP_IPFS_INFURA_API_KEY || '22rfiNb1J645FdehoqbKMpLbF6V', ipfsInfuraAPISecret: import.meta.env.VITE_APP_IPFS_INFURA_API_SECRET || 'a757597f020425c3ae532e6be84de552', diff --git a/src/config/config-dev.js b/src/config/config-dev.js index 2446a52d61..d6e03ce97c 100644 --- a/src/config/config-dev.js +++ b/src/config/config-dev.js @@ -14,6 +14,7 @@ export const config = { apiUrl: 'https://backend-dev.epns.io/apis', w2wApiUrl: 'https://backend-dev.epns.io/apis', toolingApiUrl: 'https://tooling.epns.io/apis', + deliveryNodeApiUrl: 'https://delivery-dev.epns.io/apis', ipfsInfuraAPIKey: import.meta.env.VITE_APP_IPFS_INFURA_API_KEY || '2DVyu4GEkiFksOrihKk8NMEWWwY', ipfsInfuraAPISecret: import.meta.env.VITE_APP_IPFS_INFURA_API_SECRET || '8e39eefc3d70b851b47f90611d40cfa5', @@ -53,16 +54,15 @@ export const config = { * Analaytics + Firebase */ googleAnalyticsId: 'UA-165415629-5', - vapidKey: 'BFRmmAEEXOhk31FIsooph5CxlXKh6N0_NocUWHzvtpoUEvqQTwLXu6XtwkrH7ckyr2CvVz1ll-8q4oo6-ZqFJPY', + vapidKey: 'BJYsH1MYRqzfuzduyHLNaUfZCYdAahcJXsdWzdTqleWox0vOLaycyVPdy_J9XWzSIKvRu0xkwxo75mhDiVJhNnw', firebaseConfig: { - apiKey: 'AIzaSyClOk4qP0ttFW-BPnXy7WT920xfdXSbFu8', - authDomain: 'epns-internal.firebaseapp.com', - databaseURL: 'https://epns-internal.firebaseio.com', - projectId: 'epns-internal', - storageBucket: 'epns-internal.appspot.com', - messagingSenderId: '755180533582', - appId: '1:755180533582:web:752ff8db31905506b7d01f', - measurementId: 'G-ZJH2T7R9S1', + apiKey: 'AIzaSyB4aXx2pJ9T5sw0Q1bba3jI1EAGp0Z5kBI', + authDomain: 'push-dev-a6a63.firebaseapp.com', + projectId: 'push-dev-a6a63', + storageBucket: 'push-dev-a6a63.appspot.com', + messagingSenderId: '974364469170', + appId: '1:974364469170:web:47fd6304c6cf36b5bfe6ab', + measurementId: 'G-5YR8N35DY4', }, /** diff --git a/src/config/config-localhost.js b/src/config/config-localhost.js index f796104bbf..d3b6549076 100644 --- a/src/config/config-localhost.js +++ b/src/config/config-localhost.js @@ -14,6 +14,7 @@ export const config = { apiUrl: 'http://localhost:4000/apis', w2wApiUrl: 'http://localhost:4000/apis', toolingApiUrl: 'https://tooling.epns.io/apis', + deliveryNodeApiUrl: 'https://delivery-dev.epns.io/apis', ipfsInfuraAPIKey: import.meta.env.VITE_APP_IPFS_INFURA_API_KEY || '22rfiNb1J645FdehoqbKMpLbF6V', ipfsInfuraAPISecret: import.meta.env.VITE_APP_IPFS_INFURA_API_SECRET || 'a757597f020425c3ae532e6be84de552', diff --git a/src/config/config-prod.js b/src/config/config-prod.js index aea4beaab2..aaf6ccca42 100644 --- a/src/config/config-prod.js +++ b/src/config/config-prod.js @@ -14,6 +14,7 @@ export const config = { apiUrl: 'https://backend.epns.io/apis', w2wApiUrl: 'https://backend.epns.io/apis', toolingApiUrl: 'https://tooling.epns.io/apis', + deliveryNodeApiUrl: 'https://delivery-prod.epns.io/apis', ipfsInfuraAPIKey: import.meta.env.VITE_APP_IPFS_INFURA_API_KEY || '2DVyu4GEkiFksOrihKk8NMEWWwY', ipfsInfuraAPISecret: import.meta.env.VITE_APP_IPFS_INFURA_API_SECRET || '8e39eefc3d70b851b47f90611d40cfa5', @@ -52,16 +53,16 @@ export const config = { * Analaytics + Firebase */ googleAnalyticsId: 'UA-165415629-1', - vapidKey: 'BFRmmAEEXOhk31FIsooph5CxlXKh6N0_NocUWHzvtpoUEvqQTwLXu6XtwkrH7ckyr2CvVz1ll-8q4oo6-ZqFJPY', + vapidKey: 'BOMOB--KihZkwM8SQ_OrPEsuu8UcSYiRB9AvMjsWil3WJDmxBEcDex8g4d5rFGgA8U-7esfRM5pvR98jaE1nX0M', firebaseConfig: { - apiKey: 'AIzaSyClOk4qP0ttFW-BPnXy7WT920xfdXSbFu8', - authDomain: 'epns-internal.firebaseapp.com', - databaseURL: 'https://epns-internal.firebaseio.com', - projectId: 'epns-internal', - storageBucket: 'epns-internal.appspot.com', - messagingSenderId: '755180533582', - appId: '1:755180533582:web:752ff8db31905506b7d01f', - measurementId: 'G-ZJH2T7R9S1', + apiKey: 'AIzaSyBrzkFPyNmVDFzGY7dKz2HocUO4m-ni-Fc', + authDomain: 'epns-ethereum-push-service.firebaseapp.com', + databaseURL: 'https://epns-ethereum-push-service.firebaseio.com', + projectId: 'epns-ethereum-push-service', + storageBucket: 'epns-ethereum-push-service.appspot.com', + messagingSenderId: '915758146133', + appId: '1:915758146133:web:2de388356233f5c22f2adc', + measurementId: 'G-X1L5P2E4EP', }, /** diff --git a/src/config/config-staging.js b/src/config/config-staging.js index 54af695579..f393883487 100644 --- a/src/config/config-staging.js +++ b/src/config/config-staging.js @@ -14,6 +14,7 @@ export const config = { apiUrl: 'https://backend-staging.epns.io/apis', w2wApiUrl: 'https://backend-staging.epns.io/apis', toolingApiUrl: 'https://staging-tooling.epns.io/apis', + deliveryNodeApiUrl: 'https://delivery-staging.epns.io/apis', ipfsInfuraAPIKey: import.meta.env.VITE_APP_IPFS_INFURA_API_KEY || '2DVyu4GEkiFksOrihKk8NMEWWwY', ipfsInfuraAPISecret: import.meta.env.VITE_APP_IPFS_INFURA_API_SECRET || '8e39eefc3d70b851b47f90611d40cfa5', @@ -54,7 +55,7 @@ export const config = { * Analaytics + Firebase */ googleAnalyticsId: 'UA-165415629-5', - vapidKey: 'BFRmmAEEXOhk31FIsooph5CxlXKh6N0_NocUWHzvtpoUEvqQTwLXu6XtwkrH7ckyr2CvVz1ll-8q4oo6-ZqFJPY', + vapidKey: 'BO-oYHtENkaP1nRQMmXAmjbkyWz_4sms1Z5OzE8B7h5gmuXiePvLmbXRiJNA233WtzzEo83yWZAVX1blsJQkNFg', firebaseConfig: { apiKey: 'AIzaSyClOk4qP0ttFW-BPnXy7WT920xfdXSbFu8', authDomain: 'epns-internal.firebaseapp.com', diff --git a/src/firebase.js b/src/firebase.js new file mode 100644 index 0000000000..7b46878467 --- /dev/null +++ b/src/firebase.js @@ -0,0 +1,62 @@ +// External Packages +import { initializeApp } from '@firebase/app'; +import { getMessaging, getToken, onMessage } from '@firebase/messaging'; + +// Internal Components +import { registerDeviceToken } from './services'; + +// Internal Configs +import { appConfig } from 'config'; + +// Initialize the Firebase app in the service worker by passing the generated config +var firebaseConfig = { ...appConfig.firebaseConfig }; +const TOKEN_KEY = 'EPNS_BASE_PUSH_TOKEN'; +const CACHEPREFIX = 'PUSH_TOKEN_'; + +const firebaseApp = initializeApp(firebaseConfig); +const messaging = getMessaging(firebaseApp); + +const getLocalToken = () => localStorage.getItem(TOKEN_KEY); +const setLocalToken = (token) => localStorage.setItem(TOKEN_KEY, token); + +export const getPushToken = async () => { + try { + let token = getLocalToken(TOKEN_KEY); + if (!token) { + token = await getToken(messaging, { + vapidKey: appConfig.vapidKey, + }); + setLocalToken(token); + } + return token; + } catch (err) { + console.error('An error occurred while retrieving token. ', err); + throw err; + } +}; + +export const onMessageListener = () => + new Promise((resolve) => { + onMessage(messaging, (payload) => { + resolve(payload); + }); + }); + +export const browserFunction = async (account) => { + try { + const tokenKey = `${CACHEPREFIX}${account}`; + const tokenExists = localStorage.getItem(tokenKey) || localStorage.getItem(CACHEPREFIX); //temp to prevent more than 1 account to register + if (!tokenExists) { + const response = await getPushToken(); + + await registerDeviceToken({ + token: response, + account: account, + }); + localStorage.setItem(tokenKey, response); + localStorage.setItem(CACHEPREFIX, 'response'); //temp to prevent more than 1 account to register + } + } catch (e) { + console.error('Error setting up the browser notification', e); + } +}; diff --git a/src/helpers/CaipHelper.ts b/src/helpers/CaipHelper.ts index e2c9128ed6..2088df7d0c 100644 --- a/src/helpers/CaipHelper.ts +++ b/src/helpers/CaipHelper.ts @@ -38,6 +38,10 @@ export const convertAddressToAddrCaip = (userAddress: string, chainId: number): return `eip155:${chainId}:${userAddress}`; }; +export const convertAddressToPartialCaip = (userAddress: string): string => { + return `eip155:${userAddress}`; +}; + export const convertAddrCaipToAddress = (addressInCaip: string): string => { const caipArr: string[] = addressInCaip.split(':'); if (caipArr.length == 3 && caipArr[0] == 'eip155') { diff --git a/src/helpers/RoutesHelper.ts b/src/helpers/RoutesHelper.ts index 05c5be8764..8ac00238e5 100644 --- a/src/helpers/RoutesHelper.ts +++ b/src/helpers/RoutesHelper.ts @@ -7,6 +7,7 @@ const apiVersion: number = appConfig.pushNodeApiVersion; const channelsRoute: string = `/v${apiVersion}/channels`; const usersRoute: string = `/v${apiVersion}/users`; const ipfsRoute: string = `/v${apiVersion}/ipfs`; +const deliveryNodeRoute: string = `/v${apiVersion}`; export const usersServiceEndpoints = { userSubscriptions: (userAddressInCAIP: string): string => `${usersRoute}/${userAddressInCAIP}/subscriptions`, @@ -23,5 +24,8 @@ export const ipfsServiceEndpoints = { ipfsUpload: (): string => `${ipfsRoute}/upload`, }; +export const deliveryNodeServiceEndpoints = { + registerDeviceToken: (): string => `${deliveryNodeRoute}/pushtokens/register`, +}; export const getPublicAssetPath = (path: string) => getPreviewBasePath() ? `${getPreviewBasePath()}/${path}` : `./${path}`; diff --git a/src/hooks/useBrowserNotification.ts b/src/hooks/useBrowserNotification.ts new file mode 100644 index 0000000000..daeb17e723 --- /dev/null +++ b/src/hooks/useBrowserNotification.ts @@ -0,0 +1,46 @@ +// React + Web3 Essentials +import { useContext, useEffect, useState } from 'react'; + +// External Packages +import { useAccount } from './useAccount'; +import { GlobalContext } from 'contexts/GlobalContext'; + +export function useBrowserNotification() { + const { account } = useAccount(); + const { readOnlyWallet } = useContext(GlobalContext); + const [triggerNotification, setTriggerNotification] = useState(false); + + useEffect(() => { + if (!('serviceWorker' in navigator)) return; + if (!account || account == readOnlyWallet) return; + (async function () { + const { browserFunction } = require('firebase'); + await browserFunction(account); + })(); + }, [account]); + + useEffect(() => { + if (!('serviceWorker' in navigator)) return; + const { onMessageListener } = require('firebase'); + onMessageListener() + .then((payload) => { + if (!('Notification' in window)) { + // useStream handles this case of showing in page notif (if showing notifs is not allowed) + } else { + const notificationTitle = payload.notification.title; + const notificationOptions = { + title: payload?.notification?.title, + body: payload?.notification?.body, + image: payload?.data?.aimg, + icon: payload?.data?.icon, + data: { + url: payload?.data?.acta || payload?.data?.url, + }, + }; + var notification = new Notification(notificationTitle, notificationOptions); + } + }) + .catch((err) => console.error('failed: ', err)) + .finally(() => setTriggerNotification(!triggerNotification)); //retrigger the listener after it has been used once + }, [triggerNotification]); +} diff --git a/src/services/deliveryNode/index.ts b/src/services/deliveryNode/index.ts new file mode 100644 index 0000000000..20d88dba38 --- /dev/null +++ b/src/services/deliveryNode/index.ts @@ -0,0 +1 @@ +export * from './registerDeviceToken'; diff --git a/src/services/deliveryNode/registerDeviceToken.ts b/src/services/deliveryNode/registerDeviceToken.ts new file mode 100644 index 0000000000..53b1a413be --- /dev/null +++ b/src/services/deliveryNode/registerDeviceToken.ts @@ -0,0 +1,25 @@ +// Internal Components +import { deliveryNodePostReq } from 'api'; +import { convertAddressToPartialCaip } from 'helpers/CaipHelper'; +import { deliveryNodeServiceEndpoints } from 'helpers/RoutesHelper'; + +// Types +type Props = { + token: string; + account: string; +}; + +export const registerDeviceToken = async ({ token, account }: Props) => { + const reqEndpoint = deliveryNodeServiceEndpoints.registerDeviceToken(); + try { + const data = { + wallet: convertAddressToPartialCaip(account.toLowerCase()), + device_token: token, + platform: 'web', + }; + const response = await deliveryNodePostReq(reqEndpoint, data); + } catch (err) { + console.error(err); + throw new Error(err.message); + } +}; diff --git a/src/services/index.ts b/src/services/index.ts index 735e68702d..942e78d437 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,6 @@ -export * from "./channels"; -export * from "./chats"; -export * from "./ipfs"; -export * from "./users"; -export * from "./alias"; \ No newline at end of file +export * from './channels'; +export * from './chats'; +export * from './ipfs'; +export * from './users'; +export * from './alias'; +export * from './deliveryNode';