ribcpayments/js/main_UI.js
tripathyr 7a74795889
Update main_UI.js
Added sender check .. it should be from cashier
2025-09-03 17:09:00 +05:30

500 lines
18 KiB
JavaScript

"use strict";
// Global variables
const appPages = ['dashboard', 'settings'];
// Global variables
const { html, render: renderElem } = uhtml;
//Checks for internet connection status
if (!navigator.onLine)
floGlobals.connectionErrorNotification = notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error')
window.addEventListener('offline', () => {
floGlobals.connectionErrorNotification = notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error')
})
window.addEventListener('online', () => {
getRef('notification_drawer').remove(floGlobals.connectionErrorNotification)
notify('We are back online.', 'success')
})
// Use instead of document.getElementById
const domRefs = {};
function getRef(elementId) {
if (!domRefs.hasOwnProperty(elementId)) {
domRefs[elementId] = {
count: 1,
ref: null,
};
return document.getElementById(elementId);
} else {
if (domRefs[elementId].count < 3) {
domRefs[elementId].count = domRefs[elementId].count + 1;
return document.getElementById(elementId);
} else {
if (!domRefs[elementId].ref)
domRefs[elementId].ref = document.getElementById(elementId);
return domRefs[elementId].ref;
}
}
}
// Use when a function needs to be executed after user finishes changes
const debounce = (callback, wait) => {
let timeoutId = null;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback.apply(null, args);
}, wait);
};
}
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
function notify(message, mode, options = {}) {
let icon
switch (mode) {
case 'success':
icon = `<svg class="icon icon--success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>`
break;
case 'error':
icon = `<svg class="icon icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>`
options.pinned = true
break;
}
if (mode === 'error') {
console.error(message)
}
return getRef("notification_drawer").push(message, { icon, ...options });
}
function getFormattedTime(timestamp, format) {
try {
timestamp = parseInt(timestamp)
if (String(timestamp).length < 13)
timestamp *= 1000
let [day, month, date, year] = new Date(timestamp).toString().split(' '),
minutes = new Date(timestamp).getMinutes(),
hours = new Date(timestamp).getHours(),
currentTime = new Date().toString().split(' ')
minutes = minutes < 10 ? `0${minutes}` : minutes
let finalHours = ``;
if (hours > 12)
finalHours = `${hours - 12}:${minutes}`
else if (hours === 0)
finalHours = `12:${minutes}`
else
finalHours = `${hours}:${minutes}`
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`
switch (format) {
case 'date-only':
return `${month} ${date}, ${year}`;
case 'time-only':
return finalHours;
case 'relative':
return relativeTime.from(timestamp)
default:
return `${month} ${date}, ${year} at ${finalHours}`;
}
} catch (e) {
console.error(e);
return timestamp;
}
}
window.addEventListener('hashchange', e => routeTo(window.location.hash))
window.addEventListener("load", () => {
document.body.classList.remove('hidden')
document.addEventListener("pointerdown", (e) => {
if (e.target.closest("button, .interact")) {
createRipple(e, e.target.closest("button, .interact"));
}
});
document.addEventListener('copy', () => {
notify('copied', 'success')
})
document.addEventListener('keydown', e => {
if (e.key === '/') {
e.preventDefault();
getRef('search_payments').focusIn()
}
})
getRef('search_payments').addEventListener('input', e => {
const searchQuery = e.target.value.toLowerCase();
const filteredInterns = []
floGlobals.internTxs.forEach((intern, floId) => {
if (floId.toLowerCase().includes(searchQuery) || floGlobals.appObjects.RIBC.internList[floId].toLowerCase().includes(searchQuery))
filteredInterns.push({ floId, name: floGlobals.appObjects.RIBC.internList[floId] })
})
// sort filtered by relevance to search query (name first, then floId)
filteredInterns.sort((a, b) => {
if (a.name.toLowerCase().includes(searchQuery) && b.name.toLowerCase().includes(searchQuery)) {
return a.name.toLowerCase().indexOf(searchQuery) - b.name.toLowerCase().indexOf(searchQuery)
} else if (a.name.toLowerCase().includes(searchQuery)) {
return -1
} else if (b.name.toLowerCase().includes(searchQuery)) {
return 1
} else {
return a.floId.toLowerCase().indexOf(searchQuery) - b.floId.toLowerCase().indexOf(searchQuery)
}
})
renderElem(getRef("intern_payment_list"), html`${filteredInterns.map(intern => render.internCard(intern.floId))}`);
})
});
function createRipple(event, target) {
const circle = document.createElement("span");
const diameter = Math.max(target.clientWidth, target.clientHeight);
const radius = diameter / 2;
const targetDimensions = target.getBoundingClientRect();
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (targetDimensions.left + radius)}px`;
circle.style.top = `${event.clientY - (targetDimensions.top + radius)}px`;
circle.classList.add("ripple");
const rippleAnimation = circle.animate(
[
{
transform: "scale(3)",
opacity: 0,
},
],
{
duration: 1000,
fill: "forwards",
easing: "ease-out",
}
);
target.append(circle);
rippleAnimation.onfinish = () => {
circle.remove();
};
}
const appState = {
params: {},
}
function routeTo(targetPage) {
const routingAnimation = { in: slideInUp, out: slideOutUp }
let pageId
let subPageId1
let searchParams
let params
if (targetPage === '') {
pageId = 'home'
history.replaceState(null, null, '#/home');
} else {
if (targetPage.includes('/')) {
if (targetPage.includes('?')) {
const splitAddress = targetPage.split('?')
searchParams = splitAddress.pop();
[, pageId, subPageId1] = splitAddress.pop().split('/')
} else {
[, pageId, subPageId1] = targetPage.split('/')
}
} else {
pageId = targetPage
}
}
if (!getRef(pageId)?.classList.contains('page')) return
appState.currentPage = pageId
if (searchParams) {
const urlSearchParams = new URLSearchParams('?' + searchParams);
params = Object.fromEntries(urlSearchParams.entries());
}
if (params)
appState.params = params
switch (pageId) {
case 'intern':
if (params && params.id) {
render.intern(params.id)
}
break;
}
switch (appState.lastPage) {
case 'intern':
routingAnimation.in = slideInRight;
routingAnimation.out = slideOutRight;
break;
}
switch (pageId) {
case 'intern':
routingAnimation.in = slideInLeft;
routingAnimation.out = slideOutLeft;
break;
}
if (appState.lastPage !== pageId) {
document.querySelectorAll('.page').forEach(page => page.classList.add('hidden'))
getRef(pageId).closest('.page').classList.remove('hidden')
if (appState.lastPage) {
getRef(appState.lastPage).animate(routingAnimation.out, { duration: floGlobals.prefersReducedMotion ? 0 : 150, fill: 'forwards', easing: 'ease' }).onfinish = (e) => {
e.target.effect.target.classList.add('hidden')
}
}
getRef(pageId).classList.remove('hidden')
getRef(pageId).animate(routingAnimation.in, { duration: floGlobals.prefersReducedMotion ? 0 : 150, fill: 'forwards', easing: 'ease' }).onfinish = (e) => {
appState.lastPage = pageId
}
}
}
const slideInLeft = [
{
opacity: 0,
transform: 'translateX(1rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutLeft = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(-1rem)'
},
]
const slideInRight = [
{
opacity: 0,
transform: 'translateX(-1rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutRight = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(1rem)'
},
]
const slideInDown = [
{
opacity: 0,
transform: 'translateY(-1rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutDown = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(1rem)'
},
]
const slideInUp = [
{
opacity: 0,
transform: 'translateY(1rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutUp = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(-1rem)'
},
]
floGlobals.payer = 'FThgnJLcuStugLc24FJQggmp2WgaZjrBSn';
floGlobals.internTxs = new Map()
function formatAmount(amount = 0) {
if (!amount)
return '₹0';
return amount.toLocaleString(`en-IN`, { style: 'currency', currency: 'inr' })
}
function fetchRibcData() {
return floCloudAPI.requestObjectData("RIBC", {
application: "InternManage",
receiverID: "FMyRTrz9CG4TFNM6rCQgy3VQ5NF23bY2xD",
senderID: ["FCja6sLv58e3RMy41T5AmWyvXEWesqBCkX", "FFS5hFXG7DBtdgzrLwixZLpenAmsCKRddm", "FS4jMAcSimRMrhoRhk5cjuJERS2otiwq4A"],
})
}
function fetchTransactions() {
return floBlockchainAPI
.readAllTxs("FThgnJLcuStugLc24FJQggmp2WgaZjrBSn")
.then(({ items }) => items)
}
const render = {
internCard(floId) {
const { total, txs } = floGlobals.internTxs.get(floId);
return html`
<li class="intern-card">
<div class="flex flex-direction-column gap-0-5">
<h3>${floGlobals.appObjects.RIBC.internList[floId]}</h3>
<sm-copy value=${floId}></sm-copy>
</div>
<div class="flex flex-direction-column">
<p>Last payment: <b>${formatAmount(txs[0].amount)}</b> on ${getFormattedTime(txs[0].time, 'date-only')}</p>
<p>Total paid: <b>${formatAmount(total)}</b></p>
</div>
<a href=${`#/intern?id=${floId}`} class="button button--small button--colored margin-left-auto">
View details
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z"/></svg>
</a>
</li>
`;
},
internPaymentList() {
const renderedList = []
floGlobals.internTxs.forEach((data, internId) => {
renderedList.push(render.internCard(internId));
})
renderElem(getRef("intern_payment_list"), html`${renderedList}`);
},
paymentCard(tx) {
const { txid, amount, time } = tx;
return html`
<li class="payment-card">
<time>${getFormattedTime(time, 'date-only')}</time>
<div class="flex align-items-center space-between">
<p class="amount">${formatAmount(amount)}</p>
<a class="button button--small button--colored" href=${`https://flosight.duckdns.org/tx/${txid}`} target="_blank"
rel="noopener noreferrer">
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
width="24px" fill="#000000">
<path d="M0 0h24v24H0z" fill="none"></path> <path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"> </path>
</svg>
View transaction
</a>
</div>
</li>
`;
},
intern(floId) {
renderElem(getRef('intern'), html`
<a href="#/home" class="button button--colored margin-right-auto back-button">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none" opacity=".87"/><path d="M17.51 3.87L15.73 2.1 5.84 12l9.9 9.9 1.77-1.77L9.38 12l8.13-8.13z"/></svg>
Back
</a>
<section id="intern__details" class="flex flex-direction-column gap-1">
<h1>${floGlobals.appObjects.RIBC.internList[floId]}</h1>
<div>
<p style="font-size: 0.9rem;">FLO Address</p>
<sm-copy value=${floId}></sm-copy>
</div>
<p>Total paid: <b>${formatAmount(floGlobals.internTxs.get(floId).total)}</b></p>
</section>
<section class="flex flex-direction-column gap-1">
<h4>Payment history</h4>
<ul id="payment_history">
${floGlobals.internTxs.get(floId).txs.map(tx => render.paymentCard(tx))}
</ul>
</section>
`)
}
}
const oldInterns = {
"FEvLovuDjWo4pXX3Y4SKDh8sq1AxJzqz9Z": "Megha Rani",
"F765ofUHBhfXhvzrSgnPjvCvJXXCpoW6be": "Madhu Verma",
"FHZtDh1NPepaPbbPwW65GjnDdVV1uo8NSA": "Vridhi Raj",
"FKa43RxHUAdJbgV6KipQ4PvXi6Kgw4HmFn": "Aakriti Sinha",
"FFaB6N1ETZsykXVS2PdM5xhj5BBoqsfsXC": "Ritika Agrawal",
"FSdjJCJdU43a1dyWY6dRES1ekoupEjFPqQ": "Muskan Kumari",
"FK96PZh4NskoJfWoyqcvLpSo7YnTLWMmdD": "Shambhavi Singh",
"FJK9EDGhKj4Wr2zeCo3zRPXCNU6CXFFQAN": "Shivam Kumar Pandey",
"FPtrQK6aSCgFeSNpzC68YTznHPfiz7CCvW": "Shruti Kashyap",
"FHWXdnjRRJErqazye4Y9MRmE42D4Bp6Bj7": "Rashi Sanghvi",
"FCTGD4M3DvMKupX3j2y5f3cQNDD9i6LUp7": "Gunjan Kumar Ranjan",
"F8zYh6rCuorGmnMtqGFpaKGeBqQaj9WVtG": "Kriti Shreya",
"FFoVnVMJv8BTfbk7ij9T5jPHs7VKSz886A": "Jaidev",
"F87Ai2ErAMFe3UmAR7S63UYX2jE9ofaXSH": "Keerthana A V",
"FEzy6pzEkm1TMXf1BGQz8dXvVZM3L1HFu2": "Saloni Jaitley",
"FB4tu13HCxHAadvUDmgDBhvE9MtCkgRacn": "Divyansh Bhardwaj",
"FLzcrXhzK1XzLnku5sT6yzURBcqQ5ZDNJy": "Tanishk Goyal",
"F7HVKrF68Y6YKE9XXpHhAcxt6MwRLcUD67": "Salomi Sarkar",
"FBYnAqhBt99XbTtCH6LAzjJ5yNZVPkYXhk": "Divyansh Bhardwaj (New FLO ID)",
"FF7jVqwGS8fGG9fxmbVkEvD1Qo11hDyg8b": "Ahana Chakraborty",
"FKknmmQd3PVaGbBbPFAJcQsARvw48NfeDF": "Prattay Mazumdar",
"FSoa46pVWsNuZDp26X9H9Fi6ijMk7cy7mc": "Jayant Kumar",
"FCqLr9nymnbh7ahta1gGC78z634y4GHJGQ": "Rakhijeet Singh",
"FEHKFxQxycsxw2qQQSn2Y1BCT6Mfb8EMko": "Abhijeet Anand",
}
function getInputAddresses(tx) {
const ins = tx?.vin || [];
const out = [];
for (const v of ins) {
// Blockbook puts addresses in vin[i].addresses
const addrs = v?.addresses || (v?.addr ? [v.addr] : []);
for (const a of addrs) if (a) out.push(a);
}
return out;
}
function isFromPayer(tx) {
const ins = getInputAddresses(tx);
// Must have at least one input AND all inputs must be the cashier
return ins.length > 0 && ins.every(a => a === floGlobals.payer);
}
function getReceiverAddress(vout) {
// return the first address in outputs that isn't the payer
for (const output of vout) {
const addrs = output?.scriptPubKey?.addresses || [];
for (const address of addrs) {
if (address && address !== floGlobals.payer) return address;
}
}
return undefined; // no distinct receiver (shouldn't happen for payments)
}
function parseFloAmount(floData) {
// matches "send 3000 rupee#" or "send 8000.0000000000 rupee#"
const m = /send\s+([\d.]+)\s+[A-Za-z0-9#]+/i.exec(floData || "");
return m ? parseFloat(m[1]) : 0;
}
function main() {
return Promise.all([fetchTransactions(), fetchRibcData()]).then(([txs]) => {
console.log(floGlobals.appObjects.RIBC.internList)
floGlobals.appObjects.RIBC.internList = {
...floGlobals.appObjects.RIBC.internList,
...oldInterns
}
txs.forEach((tx) => {
if (!isFromPayer(tx)) { return; }
const floId = getReceiverAddress(tx.vout);
if (!floGlobals.appObjects.RIBC.internList[floId]) return; // not an intern
const { txid, floData, time } = tx
if (!floGlobals.internTxs.has(floId))
floGlobals.internTxs.set(floId, {
total: 0,
txs: []
});
const amount = parseFloAmount(floData); // get amount from floData
floGlobals.internTxs.get(floId).total += amount;
floGlobals.internTxs.get(floId).txs.push({
txid,
amount,
time
});
});
floGlobals.internTxs.forEach((intern) => {
intern.txs.sort((a, b) => b.time - a.time)
})
// sort floGlobals.internTxs by date of last payment
floGlobals.internTxs = new Map([...floGlobals.internTxs.entries()].sort((a, b) => b[1].txs[0].time - a[1].txs[0].time));
render.internPaymentList();
routeTo(window.location.hash)
}).catch(err => {
notify(`Error fetching data: ${err}`, "error")
})
}