diff --git a/src/App.tsx b/src/App.tsx
index 06864eb..57623f6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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() {
+
+ {extState === 'authenticated' && (
+
+ )}
({
+ key: 'lastPaymentSeenTimestampAtom',
+ default: null,
});
\ No newline at end of file
diff --git a/src/background.ts b/src/background.ts
index d971c04..94b80db 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -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 it’s 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) {}
});
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..fbd6bb3
--- /dev/null
+++ b/src/hooks/useHandlePaymentNotification.tsx
@@ -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().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
+ }
+}