added payment notification

This commit is contained in:
PhilReact 2025-03-08 03:24:48 +02:00
parent 2f756f5c2d
commit 857f2980b7
5 changed files with 392 additions and 3 deletions

View File

@ -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() {
<CoreSyncStatus />
<Spacer height="20px" />
<QMailStatus />
<Spacer height="20px"/>
{extState === 'authenticated' && (
<GeneralNotifications address={userInfo?.address} />
)}
</Box>
<Box
sx={{

View File

@ -154,4 +154,9 @@ export const mailsAtom = atom({
export const groupsPropertiesAtom = atom({
key: 'groupsPropertiesAtom',
default: {},
});
export const lastPaymentSeenTimestampAtom = atom<null | number>({
key: 'lastPaymentSeenTimestampAtom',
default: null,
});

View File

@ -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 its 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) {}
});

View File

@ -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 (
<>
<ButtonBase
onClick={(e) => {
handlePopupClick(e);
}}
style={{}}
>
<NotificationsIcon
sx={{
color: hasNewPayment ? "var(--unread)" : "rgba(255, 255, 255, 0.5)",
}}
/>
</ButtonBase>
<Popover
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => {
if(hasNewPayment){
setLastEnteredTimestampPayment(Date.now())
}
setAnchorEl(null)
}} // Close popover on click outside
>
<Box
sx={{
width: "300px",
maxWidth: "100%",
maxHeight: "60vh",
overflow: "auto",
padding: "5px",
display: "flex",
flexDirection: "column",
alignItems: hasNewPayment ? "flex-start" : "center",
}}
>
{!hasNewPayment && <Typography sx={{
userSelect: 'none'
}}>No new notifications</Typography>}
{hasNewPayment && (
<MenuItem
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
width: "100%",
alignItems: "flex-start",
textWrap: "auto",
cursor: 'default'
}}
onClick={(e) => {
// executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } });
// executeEvent("open-apps-mode", { });
}}
>
<Card sx={{
padding: '10px',
width: '100%',
backgroundColor: "#1F2023",
gap: '5px',
display: 'flex',
flexDirection: 'column'
}}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
}}
>
<AccountBalanceWalletIcon
sx={{
color: "white",
}}
/>{" "}
{formatDate(latestTx?.timestamp)}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
}}
>
<Typography>{latestTx?.amount}</Typography>
</Box>
<Typography sx={{
fontSize: '0.8rem'
}}>{nameAddressOfSender.current[latestTx?.creatorAddress] || getNameOrAddressOfSenderMiddle(latestTx?.creatorAddress)}</Typography>
</Card>
</MenuItem>
)}
</Box>
</Popover>
</>
);
};

View File

@ -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<any>().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
}
}