From 857f2980b75961c1b0ea3eb08a510bc9729f64e9 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sat, 8 Mar 2025 03:24:48 +0200 Subject: [PATCH] added payment notification --- src/App.tsx | 9 +- src/atoms/global.ts | 5 + src/background.ts | 141 ++++++++++++++++++++- src/components/GeneralNotifications.tsx | 132 +++++++++++++++++++ src/hooks/useHandlePaymentNotification.tsx | 108 ++++++++++++++++ 5 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 src/components/GeneralNotifications.tsx create mode 100644 src/hooks/useHandlePaymentNotification.tsx diff --git a/src/App.tsx b/src/App.tsx index 06864eb..57623f6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -116,7 +116,7 @@ import { MainAvatar } from "./components/MainAvatar"; import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil"; -import { canSaveSettingToQdnAtom, fullScreenAtom, groupsPropertiesAtom, hasSettingsChangedAtom, isDisabledEditorEnterAtom, isUsingImportExportSettingsAtom, mailsAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global"; +import { canSaveSettingToQdnAtom, fullScreenAtom, groupsPropertiesAtom, hasSettingsChangedAtom, isDisabledEditorEnterAtom, isUsingImportExportSettingsAtom, lastPaymentSeenTimestampAtom, mailsAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global"; import { useAppFullScreen } from "./useAppFullscreen"; import { NotAuthenticated } from "./ExtStates/NotAuthenticated"; import { useFetchResources } from "./common/useFetchResources"; @@ -137,6 +137,7 @@ import { BuyQortInformation } from "./components/BuyQortInformation"; import { WalletIcon } from "./assets/Icons/WalletIcon"; import { useBlockedAddresses } from "./components/Chat/useBlockUsers"; import { QortPayment } from "./components/QortPayment"; +import { GeneralNotifications } from "./components/GeneralNotifications"; type extStates = | "not-authenticated" @@ -436,6 +437,7 @@ function App() { const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) const resetAtomMailsAtom = useResetRecoilState(mailsAtom) const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom) + const resetLastPaymentSeenTimestampAtom = useResetRecoilState(lastPaymentSeenTimestampAtom) const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); resetAtomCanSaveSettingToQdnAtom(); @@ -446,6 +448,7 @@ function App() { resetAtomQMailLastEnteredTimestampAtom() resetAtomMailsAtom() resetGroupPropertiesAtom() + resetLastPaymentSeenTimestampAtom() }; useEffect(() => { if (!isMobile) return; @@ -1860,6 +1863,10 @@ function App() { + + {extState === 'authenticated' && ( + + )} ({ + key: 'lastPaymentSeenTimestampAtom', + default: null, }); \ No newline at end of file diff --git a/src/background.ts b/src/background.ts index d971c04..94b80db 100644 --- a/src/background.ts +++ b/src/background.ts @@ -128,9 +128,9 @@ export const getForeignKey = async (foreignBlockchain)=> { const pauseAllQueues = () => controlAllQueues("pause"); const resumeAllQueues = () => controlAllQueues("resume"); -const checkDifference = (createdTimestamp) => { +export const checkDifference = (createdTimestamp, diff = timeDifferenceForNotificationChatsBackground) => { return ( - Date.now() - createdTimestamp < timeDifferenceForNotificationChatsBackground + Date.now() - createdTimestamp < diff ); }; export const getApiKeyFromStorage = async () => { @@ -1747,6 +1747,40 @@ async function sendChat({ qortAddress, recipientPublicKey, message }) { return _response; } +export async function getTimestampLatestPayment() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `latest-payment-${address}`; + + return new Promise((resolve, reject) => { + chrome.storage.local.get([key], (result) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message || "Error retrieving data")); + } else { + const timestamp = result[key]; + resolve(timestamp || 0); + } + }); + }); +} + +export async function addTimestampLatestPayment(timestamp) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + + + return new Promise((resolve, reject) => { + chrome.storage.local.set({ [`latest-payment-${address}`]: timestamp }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message || "Error saving data")); + } else { + resolve(true); + } + }); + }); +} + + export async function addEnteredQmailTimestampFunc() { const wallet = await getSaveWallet(); const address = wallet.address0; @@ -5114,6 +5148,95 @@ chrome.notifications?.onClicked?.addListener((notificationId) => { ); }); +export const checkPaymentsForNotifications = async (address) => { + try { + const isDisableNotifications = + (await getUserSettings({ key: "disable-push-notifications" })) || false; + if(isDisableNotifications) return + let latestPayment = null + const savedtimestamp = await getTimestampLatestPayment(); + + const url = await createEndpoint( + `/transactions/search?txType=PAYMENT&address=${address}&confirmationStatus=CONFIRMED&limit=5&reverse=true` + ); + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + const responseData = await response.json(); + + const latestTx = responseData.filter( + (tx) => tx?.creatorAddress !== address && tx?.recipient === address + )[0]; + if (!latestTx) { + return; // continue to the next group + } + if ( + checkDifference(latestTx.timestamp, 86400000) && + (!savedtimestamp || + latestTx.timestamp > + savedtimestamp) + ) { + if(latestTx.timestamp){ + latestPayment = latestTx + await addTimestampLatestPayment(latestTx.timestamp); + } + + // save new timestamp + } + + console.log('latestPayment', latestPayment) + + if ( + latestPayment + ) { + // Create a unique notification ID with type and group announcement details + const notificationId = + encodeURIComponent("payment_notification_" + + Date.now() + + "_type=payment-announcement"); + + const title = "New payment!"; + const body = `You have received a new payment of ${latestPayment?.amount} QORT`; + + + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title, + message: body, + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + + // Automatically close the notification after 5 seconds if it’s not clicked + setTimeout(() => { + notification.close(); + }, 10000); // Close after 5 seconds + + chrome.runtime.sendMessage({ + action: "SET_PAYMENT_ANNOUNCEMENT", + payload: latestPayment, + }); + + } + + } catch (error) { + console.error(error) + } +}; + // Reconnect when service worker wakes up chrome.runtime?.onStartup.addListener(() => { console.log("Service worker started up, reconnecting WebSocket..."); @@ -5151,6 +5274,13 @@ chrome.alarms?.get("checkForNotifications", (existingAlarm) => { } }); +chrome.alarms?.get("checkForPayments", (existingAlarm) => { + if (!existingAlarm) { + // If the alarm does not exist, create it + chrome.alarms.create("checkForPayments", { periodInMinutes: 3 }); + } +}); + chrome.alarms?.onAlarm.addListener(async (alarm) => { try { if (alarm.name === "checkForNotifications") { @@ -5161,6 +5291,13 @@ chrome.alarms?.onAlarm.addListener(async (alarm) => { checkActiveChatsForNotifications(); checkNewMessages(); checkThreads(); + } else if (alarm.name === "checkForPayments") { + + const wallet = await getSaveWallet(); + const address = wallet.address0; + if (!address) return; + + checkPaymentsForNotifications(address); } } catch (error) {} }); diff --git a/src/components/GeneralNotifications.tsx b/src/components/GeneralNotifications.tsx new file mode 100644 index 0000000..532c0e6 --- /dev/null +++ b/src/components/GeneralNotifications.tsx @@ -0,0 +1,132 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { + Box, + ButtonBase, + Card, + MenuItem, + Popover, + Tooltip, + Typography, +} from "@mui/material"; +import NotificationsIcon from "@mui/icons-material/Notifications"; +import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; +import { formatDate } from "../utils/time"; +import { useHandlePaymentNotification } from "../hooks/useHandlePaymentNotification"; + +export const GeneralNotifications = ({ address }) => { + const [anchorEl, setAnchorEl] = useState(null); + const {latestTx, + getNameOrAddressOfSenderMiddle, + hasNewPayment, setLastEnteredTimestampPayment, nameAddressOfSender} = useHandlePaymentNotification(address) + + const handlePopupClick = (event) => { + event.stopPropagation(); // Prevent parent onClick from firing + setAnchorEl(event.currentTarget); + }; + + return ( + <> + { + handlePopupClick(e); + + + }} + style={{}} + > + + + + { + if(hasNewPayment){ + setLastEnteredTimestampPayment(Date.now()) + } + setAnchorEl(null) + + }} // Close popover on click outside + > + + {!hasNewPayment && No new notifications} + {hasNewPayment && ( + { + // executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } }); + // executeEvent("open-apps-mode", { }); + }} + > + + + {" "} + {formatDate(latestTx?.timestamp)} + + + + {latestTx?.amount} + + {nameAddressOfSender.current[latestTx?.creatorAddress] || getNameOrAddressOfSenderMiddle(latestTx?.creatorAddress)} + + + + )} + + + + ); +}; diff --git a/src/hooks/useHandlePaymentNotification.tsx b/src/hooks/useHandlePaymentNotification.tsx new file mode 100644 index 0000000..fbd6bb3 --- /dev/null +++ b/src/hooks/useHandlePaymentNotification.tsx @@ -0,0 +1,108 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { getBaseApiReact } from '../App'; +import { addTimestampLatestPayment, checkDifference, getNameInfoForOthers, getTimestampLatestPayment } from '../background'; +import { useRecoilState } from 'recoil'; +import { lastPaymentSeenTimestampAtom } from '../atoms/global'; + +export const useHandlePaymentNotification = (address) => { + const [latestTx, setLatestTx] = useState(null); + + const nameAddressOfSender = useRef({}) + const isFetchingName = useRef({}) + + + const [lastEnteredTimestampPayment, setLastEnteredTimestampPayment] = + useRecoilState(lastPaymentSeenTimestampAtom); + + useEffect(() => { + if (lastEnteredTimestampPayment && address) { + addTimestampLatestPayment(Date.now()).catch((error) => { + console.error(error); + }); + } + }, [lastEnteredTimestampPayment, address]); + + const getNameOrAddressOfSender = useCallback(async(senderAddress)=> { + if(isFetchingName.current[senderAddress]) return senderAddress + try { + isFetchingName.current[senderAddress] = true + const res = await getNameInfoForOthers(senderAddress) + nameAddressOfSender.current[senderAddress] = res || senderAddress + } catch (error) { + console.error(error) + } finally { + isFetchingName.current[senderAddress] = false + } + + }, []) + + const getNameOrAddressOfSenderMiddle = useCallback(async(senderAddress)=> { + getNameOrAddressOfSender(senderAddress) + return senderAddress + + }, [getNameOrAddressOfSender]) + + const hasNewPayment = useMemo(() => { + if (!latestTx) return false; + if (!checkDifference(latestTx?.timestamp, 86400000)) return false; + if ( + !lastEnteredTimestampPayment || + lastEnteredTimestampPayment < latestTx?.timestamp + ) + return true; + + return false; + }, [lastEnteredTimestampPayment, latestTx]); + + console.log('hasNewPayment', hasNewPayment) + + const getLastSeenData = useCallback(async () => { + try { + if (!address) return; + console.log('address', address) + const key = `last-seen-payment-${address}`; + + const res = await getTimestampLatestPayment().catch(() => null); + console.log('res', res) + if (res) { + setLastEnteredTimestampPayment(res); + } + + const response = await fetch( + `${getBaseApiReact()}/transactions/search?txType=PAYMENT&address=${address}&confirmationStatus=CONFIRMED&limit=5&reverse=true` + ); + + const responseData = await response.json(); + console.log('responseData', responseData) + const latestTx = responseData.filter( + (tx) => tx?.creatorAddress !== address && tx?.recipient === address + )[0]; + if (!latestTx) { + return; // continue to the next group + } + + setLatestTx(latestTx); + } catch (error) { + console.error(error); + } + }, [address, setLastEnteredTimestampPayment]); + + + useEffect(() => { + getLastSeenData(); + + chrome?.runtime?.onMessage.addListener((message, sender, sendResponse) => { + console.log('message', message) + if (message?.action === "SET_PAYMENT_ANNOUNCEMENT" && message?.payload) { + setLatestTx(message.payload); + } + }); + }, [getLastSeenData]); + return { + latestTx, + getNameOrAddressOfSenderMiddle, + hasNewPayment, + setLastEnteredTimestampPayment, + nameAddressOfSender + } +}