added payment notification

This commit is contained in:
PhilReact 2025-03-08 03:24:30 +02:00
parent 09c447798c
commit 2ada0fd3d4
5 changed files with 422 additions and 8 deletions

View File

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

View File

@ -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<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 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 its 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
}

View File

@ -2206,6 +2206,7 @@ export const Group = ({
{isMobile && (
<Header
address={userInfo?.address}
isPrivate={isPrivate}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
isThin={

View File

@ -11,6 +11,7 @@ import {
Menu,
ListItemIcon,
ListItemText,
Card,
} from "@mui/material";
import { HomeIcon } from "../../assets/Icons/HomeIcon";
import { LogoutIcon } from "../../assets/Icons/LogoutIcon";
@ -24,6 +25,9 @@ import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import { useRecoilState } from "recoil";
import { fullScreenAtom, hasSettingsChangedAtom } from "../../atoms/global";
import { useAppFullScreen } from "../../useAppFullscreen";
import { useHandlePaymentNotification } from "../../hooks/useHandlePaymentNotification";
import { formatDate } from "../../utils/time";
import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet";
const Header = ({
logoutFunc,
@ -36,8 +40,14 @@ const Header = ({
setMobileViewMode,
myName,
setSelectedDirect,
setNewChat
setNewChat,
address
}) => {
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}
>
<NotificationIcon height={20} width={21} color={hasUnreadDirects || hasUnreadGroups ? "var(--unread)" : "rgba(145, 145, 147, 1)"} />
<NotificationIcon height={20} width={21} color={hasNewPayment || hasUnreadDirects || hasUnreadGroups ? "var(--unread)" : "rgba(145, 145, 147, 1)"} />
</ButtonBase>
{fullScreen && (
<ButtonBase onClick={()=> {
@ -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" />
</MenuItem>
{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%',
gap: '5px',
display: 'flex',
flexDirection: 'column'
}}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
fontSize: '13px'
}}
>
<AccountBalanceWalletIcon
sx={{
color: "var(--unread)",
}}
/>{" "}
{formatDate(latestTx?.timestamp)}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
}}
>
<Typography sx={{
fontSize: '13px'
}}>{latestTx?.amount}</Typography>
</Box>
<Typography sx={{
fontSize: '13px'
}}>{nameAddressOfSender.current[latestTx?.creatorAddress] || getNameOrAddressOfSenderMiddle(latestTx?.creatorAddress)}</Typography>
</Card>
</MenuItem>
)}
</Menu>
</AppBar>
);
@ -358,7 +430,7 @@ const Header = ({
}}
>
<IconButton onClick={handleClick} color="inherit">
<NotificationIcon color={hasUnreadDirects || hasUnreadGroups ? "var(--unread)" : "rgba(255, 255, 255, 1)"} />
<NotificationIcon color={hasNewPayment || hasUnreadDirects || hasUnreadGroups ? "var(--unread)" : "rgba(255, 255, 255, 1)"} />
</IconButton>
</Box>
@ -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" />
</MenuItem>
@ -464,6 +536,65 @@ const Header = ({
},
}} primary="Messaging" />
</MenuItem>
{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%',
gap: '5px',
display: 'flex',
flexDirection: 'column'
}}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
fontSize: '13px'
}}
>
<AccountBalanceWalletIcon
sx={{
color: "var(--unread)",
}}
/>{" "}
{formatDate(latestTx?.timestamp)}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
}}
>
<Typography sx={{
fontSize: '13px'
}}>{latestTx?.amount}</Typography>
</Box>
<Typography sx={{
fontSize: '13px'
}}>{nameAddressOfSender.current[latestTx?.creatorAddress] || getNameOrAddressOfSenderMiddle(latestTx?.creatorAddress)}</Typography>
</Card>
</MenuItem>
)}
</Menu>
</>
);

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
}
}