added payment notification

This commit is contained in:
PhilReact 2025-03-08 03:24:58 +02:00
parent 91f787c18a
commit 548ba9b29c
5 changed files with 402 additions and 2 deletions

View File

@ -120,6 +120,7 @@ import {
hasSettingsChangedAtom, hasSettingsChangedAtom,
isDisabledEditorEnterAtom, isDisabledEditorEnterAtom,
isUsingImportExportSettingsAtom, isUsingImportExportSettingsAtom,
lastPaymentSeenTimestampAtom,
mailsAtom, mailsAtom,
oldPinnedAppsAtom, oldPinnedAppsAtom,
qMailLastEnteredTimestampAtom, qMailLastEnteredTimestampAtom,
@ -154,6 +155,7 @@ import { UserLookup } from "./components/UserLookup.tsx/UserLookup";
import { RegisterName } from "./components/RegisterName"; import { RegisterName } from "./components/RegisterName";
import { BuyQortInformation } from "./components/BuyQortInformation"; import { BuyQortInformation } from "./components/BuyQortInformation";
import { QortPayment } from "./components/QortPayment"; import { QortPayment } from "./components/QortPayment";
import { GeneralNotifications } from "./components/GeneralNotifications";
type extStates = type extStates =
| "not-authenticated" | "not-authenticated"
@ -508,6 +510,7 @@ function App() {
const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom)
const resetAtomMailsAtom = useResetRecoilState(mailsAtom) const resetAtomMailsAtom = useResetRecoilState(mailsAtom)
const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom) const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom)
const resetLastPaymentSeenTimestampAtom = useResetRecoilState(lastPaymentSeenTimestampAtom)
const resetAllRecoil = () => { const resetAllRecoil = () => {
resetAtomSortablePinnedAppsAtom(); resetAtomSortablePinnedAppsAtom();
resetAtomCanSaveSettingToQdnAtom(); resetAtomCanSaveSettingToQdnAtom();
@ -518,6 +521,7 @@ function App() {
resetAtomQMailLastEnteredTimestampAtom() resetAtomQMailLastEnteredTimestampAtom()
resetAtomMailsAtom() resetAtomMailsAtom()
resetGroupPropertiesAtom() resetGroupPropertiesAtom()
resetLastPaymentSeenTimestampAtom()
}; };
useEffect(() => { useEffect(() => {
@ -1802,6 +1806,11 @@ function App() {
<CoreSyncStatus /> <CoreSyncStatus />
<Spacer height="20px" /> <Spacer height="20px" />
<QMailStatus /> <QMailStatus />
<Spacer height="20px"/>
{extState === 'authenticated' && (
<GeneralNotifications address={userInfo?.address} />
)}
</Box> </Box>
<Box <Box
sx={{ sx={{

View File

@ -156,6 +156,11 @@ export const qMailLastEnteredTimestampAtom = atom({
default: null, default: null,
}); });
export const lastPaymentSeenTimestampAtom = atom<null | number>({
key: 'lastPaymentSeenTimestampAtom',
default: null,
});
export const mailsAtom = atom({ export const mailsAtom = atom({
key: 'mailsAtom', key: 'mailsAtom',
default: [], default: [],

View File

@ -262,11 +262,12 @@ export const getForeignKey = async (foreignBlockchain)=> {
export const pauseAllQueues = () => controlAllQueues("pause"); export const pauseAllQueues = () => controlAllQueues("pause");
export const resumeAllQueues = () => controlAllQueues("resume"); export const resumeAllQueues = () => controlAllQueues("resume");
const checkDifference = (createdTimestamp) => { export const checkDifference = (createdTimestamp, diff = timeDifferenceForNotificationChatsBackground) => {
return ( return (
Date.now() - createdTimestamp < timeDifferenceForNotificationChatsBackground Date.now() - createdTimestamp < diff
); );
}; };
export const getApiKeyFromStorage = async (): Promise<string | null> => { export const getApiKeyFromStorage = async (): Promise<string | null> => {
return getData<string>("apiKey").catch(() => null); return getData<string>("apiKey").catch(() => null);
}; };
@ -518,6 +519,7 @@ const handleNotificationDirect = async (directs) => {
`_from=${newestLatestTimestamp.address}`); `_from=${newestLatestTimestamp.address}`);
const notification = new window.Notification(title, { const notification = new window.Notification(title, {
body, body,
icon: window.location.origin + "/qortal192.png",
data: { id: notificationId }, data: { id: notificationId },
}); });
@ -559,6 +561,7 @@ const handleNotificationDirect = async (directs) => {
const notification = new window.Notification(title, { const notification = new window.Notification(title, {
body, body,
icon: window.location.origin + "/qortal192.png",
data: { id: notificationId }, data: { id: notificationId },
}); });
@ -746,6 +749,7 @@ const handleNotification = async (groups) => {
const notification = new window.Notification(title, { const notification = new window.Notification(title, {
body, body,
icon: window.location.origin + "/qortal192.png",
data: { id: notificationId }, data: { id: notificationId },
}); });
@ -788,6 +792,7 @@ const handleNotification = async (groups) => {
// Create and show the notification immediately // Create and show the notification immediately
const notification = new window.Notification(title, { const notification = new window.Notification(title, {
body, body,
icon: window.location.origin + "/qortal192.png",
data: { id: notificationId }, data: { id: notificationId },
}); });
@ -2739,6 +2744,31 @@ export async function addTimestampGroupAnnouncement({
}); });
} }
export async function getTimestampLatestPayment() {
const wallet = await getSaveWallet();
const address = wallet.address0;
const key = `latest-payment-${address}`;
const res = await getData<any>(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 async function addEnteredQmailTimestamp() { export async function addEnteredQmailTimestamp() {
const wallet = await getSaveWallet(); const wallet = await getSaveWallet();
const address = wallet.address0; const address = wallet.address0;
@ -3273,6 +3303,7 @@ export const checkNewMessages = async () => {
// Create and show the notification // Create and show the notification
const notification = new window.Notification(title, { const notification = new window.Notification(title, {
body, body,
icon: window.location.origin + "/qortal192.png",
data: { id: notificationId }, data: { id: notificationId },
}); });
@ -3302,6 +3333,95 @@ export const checkNewMessages = async () => {
} }
}; };
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) &&
(!savedtimestamp ||
latestTx.timestamp >
savedtimestamp)
) {
if(latestTx.timestamp){
latestPayment = latestTx
await addTimestampLatestPayment(latestTx.timestamp);
}
// save new timestamp
}
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`;
// 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 = () => {
handleNotificationClick(notificationId);
notification.close(); // Clean up the notification on click
};
// Automatically close the notification after 5 seconds if its not clicked
setTimeout(() => {
notification.close();
}, 10000); // Close after 5 seconds
const targetOrigin = window.location.origin;
window.postMessage(
{
action: "SET_PAYMENT_ANNOUNCEMENT",
payload: latestPayment,
},
targetOrigin
);
}
} catch (error) {
console.error(error)
}
};
const checkActiveChatsForNotifications = async () => { const checkActiveChatsForNotifications = async () => {
try { try {
checkGroupList(); checkGroupList();
@ -3437,6 +3557,7 @@ export const checkThreads = async (bringBack) => {
// Create and show the notification // Create and show the notification
const notification = new window.Notification(title, { const notification = new window.Notification(title, {
body, body,
icon: window.location.origin + "/qortal192.png",
data: { id: notificationId }, data: { id: notificationId },
}); });
@ -3494,6 +3615,7 @@ export const checkThreads = async (bringBack) => {
// }); // });
let notificationCheckInterval let notificationCheckInterval
let paymentsCheckInterval
const createNotificationCheck = () => { const createNotificationCheck = () => {
// Check if an interval already exists before creating it // Check if an interval already exists before creating it
@ -3513,6 +3635,22 @@ const createNotificationCheck = () => {
} }
}, 10 * 60 * 1000); // 10 minutes }, 10 * 60 * 1000); // 10 minutes
} }
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
}
}; };
// Call this function when initializing your app // Call this function when initializing your app

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,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<any>(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
}
}