mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-03-14 11:52:33 +00:00
added payment notification
This commit is contained in:
parent
09c447798c
commit
2ada0fd3d4
@ -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,
|
||||
});
|
||||
|
@ -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 it’s 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
|
||||
}
|
||||
|
@ -2206,6 +2206,7 @@ export const Group = ({
|
||||
|
||||
{isMobile && (
|
||||
<Header
|
||||
address={userInfo?.address}
|
||||
isPrivate={isPrivate}
|
||||
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
|
||||
isThin={
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
116
src/hooks/useHandlePaymentNotification.tsx
Normal file
116
src/hooks/useHandlePaymentNotification.tsx
Normal 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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user