diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 28462fb..baf3794 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -170,4 +170,9 @@ export const mailsAtom = atom({ export const lastEnteredGroupIdAtom = atom({ key: 'lastEnteredGroupIdAtom', default: null, -}); \ No newline at end of file +}); + +export const lastPaymentSeenTimestampAtom = atom({ + key: 'lastPaymentSeenTimestampAtom', + default: null, +}); diff --git a/src/background.ts b/src/background.ts index fbf55b2..0f27c81 100644 --- a/src/background.ts +++ b/src/background.ts @@ -242,7 +242,7 @@ export const getForeignKey = async (foreignBlockchain)=> { export const pauseAllQueues = () => controlAllQueues("pause"); export const resumeAllQueues = () => controlAllQueues("resume"); -const checkDifference = (createdTimestamp) => { +export const checkDifference = (createdTimestamp) => { return ( Date.now() - createdTimestamp < timeDifferenceForNotificationChatsBackground ); @@ -3383,6 +3383,143 @@ export const checkThreads = async (bringBack) => { } }; +export async function getTimestampLatestPayment() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `latest-payment-${address}`; + const res = await getData(key).catch(() => null); + if (res) { + const parsedData = res; + return parsedData; + } else return 0 +} + +export async function addTimestampLatestPayment(timestamp) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + + return await new Promise((resolve, reject) => { + storeData(`latest-payment-${address}`, timestamp) + .then(() => resolve(true)) + .catch((error) => { + reject(new Error(error.message || "Error saving data")); + }); + }); +} + + +export const checkPaymentsForNotifications = async (address, isBackground) => { + 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) && + (!savedtimestamp || + latestTx.timestamp > + savedtimestamp) + ) { + if(latestTx.timestamp){ + latestPayment = latestTx + await addTimestampLatestPayment(latestTx.timestamp); + } + + // save new timestamp + } + + + + if ( + latestPayment + ) { + const title = "New payment!"; + const body = `You have received a new payment of ${latestPayment?.amount} QORT`; + // Create a unique notification ID with type and group announcement details + const notificationId = + encodeURIComponent("payment_notification_" + + Date.now() + + "_type=payment-announcement"); + + + if(!isNative){ + // Create and show the notification + const notification = new window.Notification(title, { + body, + icon: window.location.origin + "/qortal192.png", + data: { id: notificationId }, + }); + + // Handle notification click with specific actions based on `notificationId` + notification.onclick = () => { + notification.close(); // Clean up the notification on click + }; + + // Automatically close the notification after 5 seconds if it’s not clicked + setTimeout(() => { + notification.close(); + }, 10000); // Close after 5 seconds + + } else { + const notificationId = generateId() + LocalNotifications.schedule({ + notifications: [ + { + title, + body, + id: notificationId, + schedule: { at: new Date(Date.now() + 1000) }, // 1 second from now + extra: { + type: 'payment', + } + } + ] + }); + + + } + + + if(!isBackground){ + const targetOrigin = window.location.origin; + + window.postMessage( + { + action: "SET_PAYMENT_ANNOUNCEMENT", + payload: latestPayment, + }, + targetOrigin + ); + } + + } + + } catch (error) { + console.error(error) + } +}; + if(isNative){ // Configure Background Fetch @@ -3397,6 +3534,7 @@ BackgroundFetch.configure({ checkActiveChatsForNotifications(); checkNewMessages(); checkThreads(); + checkPaymentsForNotifications(address, true) await new Promise((res)=> { setTimeout(() => { @@ -3461,3 +3599,26 @@ const initializeBackButton = () => { if(isNative){ initializeBackButton(); } + + + + +let paymentsCheckInterval + + + + if (!paymentsCheckInterval) { + paymentsCheckInterval = setInterval(async () => { + try { + // This would replace the Chrome alarm callback + const wallet = await getSaveWallet(); + const address = wallet?.address0; + if (!address) return; + + checkPaymentsForNotifications(address); + + } catch (error) { + console.error('Error checking payments:', error); + } + }, 3 * 60 * 1000); // 3 minutes + } diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 1e599c1..c68e46e 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -2206,6 +2206,7 @@ export const Group = ({ {isMobile && (
{ + const {latestTx, + getNameOrAddressOfSenderMiddle, + hasNewPayment, + setLastEnteredTimestampPayment, + nameAddressOfSender} = useHandlePaymentNotification(address) const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); @@ -48,6 +58,9 @@ const Header = ({ const handleClose = () => { setAnchorEl(null); + if(hasNewPayment){ + setLastEnteredTimestampPayment(Date.now()) + } }; if (isThin) { @@ -92,7 +105,7 @@ const Header = ({ onClick={handleClick} > - + {fullScreen && ( { @@ -171,9 +184,9 @@ const Header = ({ slotProps={{ paper: { sx: { + mnWidth: '148px', backgroundColor: 'var(--bg-primary)', color: '#fff', - width: '148px', borderRadius: '5px' }, }, @@ -228,6 +241,65 @@ const Header = ({ }, }} primary="Messaging" /> + {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)} + + + + )} ); @@ -358,7 +430,7 @@ const Header = ({ }} > - + @@ -411,7 +483,7 @@ const Header = ({ sx: { backgroundColor: 'var(--bg-primary)', color: '#fff', - width: '148px', + mnWidth: '148px', borderRadius: '5px' }, }, @@ -439,7 +511,7 @@ const Header = ({ "& .MuiTypography-root": { fontSize: "12px", fontWeight: 600, - color: hasUnreadDirects ? "var(--unread)" :"rgba(250, 250, 250, 0.5)" + color: hasUnreadGroups ? "var(--unread)" :"rgba(250, 250, 250, 0.5)" }, }} primary="Groups" /> @@ -464,6 +536,65 @@ const Header = ({ }, }} primary="Messaging" /> + {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..9e9effb --- /dev/null +++ b/src/hooks/useHandlePaymentNotification.tsx @@ -0,0 +1,116 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { getBaseApiReact } from '../App'; +import { getData, storeData } from '../utils/chromeStorage'; +import { checkDifference, getNameInfoForOthers } 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) { + storeData(`last-seen-payment-${address}`, 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)) return false; + if ( + !lastEnteredTimestampPayment || + lastEnteredTimestampPayment < latestTx?.timestamp + ) + return true; + + return false; + }, [lastEnteredTimestampPayment, latestTx]); + + const getLastSeenData = useCallback(async () => { + try { + if (!address) return; + const key = `last-seen-payment-${address}`; + + const res = await getData(key).catch(() => null); + 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(); + + 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(); + // Handler function for incoming messages + const messageHandler = (event) => { + if (event.origin !== window.location.origin) { + return; + } + const message = event.data; + if (message?.action === "SET_PAYMENT_ANNOUNCEMENT" && message?.payload) { + setLatestTx(message.payload); + } + }; + + // Attach the event listener + window.addEventListener("message", messageHandler); + + // Clean up the event listener on component unmount + return () => { + window.removeEventListener("message", messageHandler); + }; + }, [getLastSeenData]); + return { + latestTx, + getNameOrAddressOfSenderMiddle, + hasNewPayment, + setLastEnteredTimestampPayment, + nameAddressOfSender + } +}