diff --git a/src/App.tsx b/src/App.tsx
index 3520489..777c25e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -120,6 +120,7 @@ import {
hasSettingsChangedAtom,
isDisabledEditorEnterAtom,
isUsingImportExportSettingsAtom,
+ lastPaymentSeenTimestampAtom,
mailsAtom,
oldPinnedAppsAtom,
qMailLastEnteredTimestampAtom,
@@ -154,6 +155,7 @@ import { UserLookup } from "./components/UserLookup.tsx/UserLookup";
import { RegisterName } from "./components/RegisterName";
import { BuyQortInformation } from "./components/BuyQortInformation";
import { QortPayment } from "./components/QortPayment";
+import { GeneralNotifications } from "./components/GeneralNotifications";
type extStates =
| "not-authenticated"
@@ -508,6 +510,7 @@ function App() {
const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom)
const resetAtomMailsAtom = useResetRecoilState(mailsAtom)
const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom)
+ const resetLastPaymentSeenTimestampAtom = useResetRecoilState(lastPaymentSeenTimestampAtom)
const resetAllRecoil = () => {
resetAtomSortablePinnedAppsAtom();
resetAtomCanSaveSettingToQdnAtom();
@@ -518,6 +521,7 @@ function App() {
resetAtomQMailLastEnteredTimestampAtom()
resetAtomMailsAtom()
resetGroupPropertiesAtom()
+ resetLastPaymentSeenTimestampAtom()
};
useEffect(() => {
@@ -1802,6 +1806,11 @@ function App() {
+
+ {extState === 'authenticated' && (
+
+ )}
+
({
+ key: 'lastPaymentSeenTimestampAtom',
+ default: null,
+});
+
export const mailsAtom = atom({
key: 'mailsAtom',
default: [],
diff --git a/src/background.ts b/src/background.ts
index 96febcd..c43dacf 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -262,11 +262,12 @@ export const getForeignKey = async (foreignBlockchain)=> {
export const pauseAllQueues = () => controlAllQueues("pause");
export 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 (): Promise => {
return getData("apiKey").catch(() => null);
};
@@ -518,6 +519,7 @@ const handleNotificationDirect = async (directs) => {
`_from=${newestLatestTimestamp.address}`);
const notification = new window.Notification(title, {
body,
+ icon: window.location.origin + "/qortal192.png",
data: { id: notificationId },
});
@@ -559,6 +561,7 @@ const handleNotificationDirect = async (directs) => {
const notification = new window.Notification(title, {
body,
+ icon: window.location.origin + "/qortal192.png",
data: { id: notificationId },
});
@@ -746,6 +749,7 @@ const handleNotification = async (groups) => {
const notification = new window.Notification(title, {
body,
+ icon: window.location.origin + "/qortal192.png",
data: { id: notificationId },
});
@@ -788,6 +792,7 @@ const handleNotification = async (groups) => {
// Create and show the notification immediately
const notification = new window.Notification(title, {
body,
+ icon: window.location.origin + "/qortal192.png",
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(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() {
const wallet = await getSaveWallet();
const address = wallet.address0;
@@ -3273,6 +3303,7 @@ export const checkNewMessages = async () => {
// Create and show the notification
const notification = new window.Notification(title, {
body,
+ icon: window.location.origin + "/qortal192.png",
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 it’s 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 () => {
try {
checkGroupList();
@@ -3437,6 +3557,7 @@ export const checkThreads = async (bringBack) => {
// Create and show the notification
const notification = new window.Notification(title, {
body,
+ icon: window.location.origin + "/qortal192.png",
data: { id: notificationId },
});
@@ -3494,6 +3615,7 @@ export const checkThreads = async (bringBack) => {
// });
let notificationCheckInterval
+let paymentsCheckInterval
const createNotificationCheck = () => {
// Check if an interval already exists before creating it
@@ -3513,6 +3635,22 @@ const createNotificationCheck = () => {
}
}, 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
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 && (
+
+ )}
+
+
+ >
+ );
+};
diff --git a/src/hooks/useHandlePaymentNotification.tsx b/src/hooks/useHandlePaymentNotification.tsx
new file mode 100644
index 0000000..cfed663
--- /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
+ }
+}