/*jshint esversion: 8 */ /** * @yaireo/relative-time - javascript function to transform timestamp or date to local relative-time * * @version v1.0.0 * @homepage https://github.com/yairEO/relative-time */ !function (e, t) { var o = o || {}; "function" == typeof o && o.amd ? o([], t) : "object" == typeof exports && "object" == typeof module ? module.exports = t() : "object" == typeof exports ? exports.RelativeTime = t() : e.RelativeTime = t() }(this, (function () { const e = { year: 31536e6, month: 2628e6, day: 864e5, hour: 36e5, minute: 6e4, second: 1e3 }, t = "en", o = { numeric: "auto" }; function n(e) { e = { locale: (e = e || {}).locale || t, options: { ...o, ...e.options } }, this.rtf = new Intl.RelativeTimeFormat(e.locale, e.options) } return n.prototype = { from(t, o) { const n = t - (o || new Date); for (let t in e) if (Math.abs(n) > e[t] || "second" == t) return this.rtf.format(Math.round(n / e[t]), t) } }, n })); const relativeTime = new RelativeTime({ style: 'narrow' }); function syncUserData(obsName, data) { const dataToSend = Crypto.AES.encrypt(JSON.stringify(data), myPrivKey); return floCloudAPI.sendApplicationData(dataToSend, obsName, { receiverID: myFloID }); } async function organizeSyncedData(obsName) { const fetchedData = await floCloudAPI.requestApplicationData(obsName, { mostRecent: true, senderIDs: [myFloID], receiverID: myFloID }); if (fetchedData.length && await compactIDB.readData(obsName, 'lastSyncTime') !== fetchedData[0].time) { await compactIDB.clearData(obsName); const dataToDecrypt = floCloudAPI.util.decodeMessage(fetchedData[0].message); const decryptedData = JSON.parse(Crypto.AES.decrypt(dataToDecrypt, myPrivKey)); for (let key in decryptedData) { floGlobals[obsName][key] = decryptedData[key]; compactIDB.addData(obsName, decryptedData[key], key); } compactIDB.addData(obsName, fetchedData[0].time, 'lastSyncTime'); return true; } else { const idbData = await compactIDB.readAllData(obsName); for (const key in idbData) { if (key !== 'lastSyncTime') floGlobals[obsName][key] = idbData[key]; } return true; } } const userUI = {}; function continueWalletTopup() { let cashier = User.findCashier(); if (!cashier) return notify("No cashier online. Please try again in a while.", 'error'); let amount = parseFloat(getRef('request_cashier_amount').value.trim()); getRef('topup_wallet__details').innerHTML = `Send ${formatAmount(amount)} to UPI ID below`; getRef('topup_wallet__upi_id').value = cashierUPI[cashier]; showProcessStage('topup_wallet_process', 1) getRef('topup_wallet__txid').focusIn(); } function depositMoneyToWallet() { let cashier = User.findCashier(); if (!cashier) return notify("No cashier online. Please try again in a while.", 'error'); let amount = parseFloat(getRef('request_cashier_amount').value.trim()); let upiTxID = getRef('topup_wallet__txid').value.trim(); if (upiTxID === '') return notify("Please enter UPI transaction ID", 'error'); buttonLoader('topup_wallet_button', true); User.cashToToken(cashier, amount, upiTxID).then(result => { console.log(result); showProcessStage('topup_wallet_process', 2); }).catch(error => { console.error(error) getRef('topup_failed_reason').textContent = error; showProcessStage('topup_wallet_process', 3); }) } function withdrawMoneyFromWallet() { let cashier = User.findCashier(); if (!cashier) return notify("No cashier online. Please try again in a while.", 'error'); let amount = parseFloat(getRef('send_cashier_amount').value.trim()); const upiId = getRef('select_upi_id').value; if (!upiId) return notify("Please add an UPI ID to continue", 'error'); buttonLoader('withdraw_rupee_button', true); User.sendToken(cashier, amount, 'for token-to-cash').then(txid => { console.warn(`Withdraw ${amount} from cashier ${cashier}`, txid); User.tokenToCash(cashier, amount, txid, upiId).then(result => { showProcessStage('withdraw_wallet_process', 1); console.log(result); }).catch(error => { getRef('withdrawal_failed_reason').textContent = error; showProcessStage('withdraw_wallet_process', 2); console.error(error) }).finally(() => { buttonLoader('withdraw_rupee_button', false); }); }).catch(error => { getRef('withdrawal_failed_reason').textContent = error; showProcessStage('withdraw_wallet_process', 2); buttonLoader('withdraw_rupee_button', false); console.error(error) }) } async function renderSavedUpiIds() { const frag = document.createDocumentFragment(); for (const upiId in floGlobals.savedUserData.upiIds) { frag.append(render.savedUpiId(upiId)); } getRef('saved_upi_ids_list').innerHTML = ''; getRef('saved_upi_ids_list').append(frag); } function saveUpiId() { const upiId = getRef('get_upi_id').value.trim(); if (upiId === '') return notify("Please add an UPI ID to continue", 'error'); if (floGlobals.savedUserData.upiIds.hasOwnProperty(upiId)) return notify('This UPI ID is already saved', 'error'); floGlobals.savedUserData.upiIds[upiId] = {} syncUserData('savedUserData', floGlobals.savedUserData).then(() => { notify(`Saved ${upiId}`, 'success'); if (pagesData.lastPage === 'settings') { getRef('saved_upi_ids_list').append(render.savedUpiId(upiId)); } else if (pagesData.lastPage === 'wallet') { getRef('select_upi_id').append( createElement('sm-option', { textContent: upiId, attributes: { value: upiId, } }) ) getRef('select_upi_id').parentNode.classList.remove('hide') } hidePopup(); }).catch(error => { notify(error, 'error'); }) } delegate(getRef('saved_upi_ids_list'), 'click', '.saved-upi', e => { if (e.target.closest('.delete-upi')) { const upiId = e.delegateTarget.dataset.upiId; getConfirmation('Do you want delete this UPI ID?', { confirmText: 'Delete', }).then(res => { if (res) { const toDelete = getRef('saved_upi_ids_list').querySelector(`.saved-upi[data-upi-id="${upiId}"]`); if (toDelete) toDelete.remove(); delete floGlobals.savedUserData.upiIds[upiId]; hidePopup(); syncUserData('savedUserData', floGlobals.savedUserData).then(() => { notify(`Deleted UPI ID`, 'success'); }).catch(error => { notify(error, 'error'); }); } }); } }); userUI.renderCashierRequests = function (requests, error = null) { if (error) return console.error(error); else if (typeof requests !== "object" || requests === null) return; if (pagesData.lastPage === 'wallet') { for (let transactionID in requests) { const { note, tag } = requests[transactionID]; let status = tag ? 'done' : (note ? 'failed' : "pending"); getRef('wallet_history_wrapper').querySelectorAll(`[data-vc="${transactionID}"]`).forEach(card => card.remove()); getRef(status !== 'pending' ? 'wallet_history' : 'pending_wallet_transactions').prepend(render.walletRequestCard(requests[transactionID])) } } }; const pendingTransactionsObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'childList') { if (mutation.target.children.length) mutation.target.parentNode.classList.remove('hide') else mutation.target.parentNode.classList.add('hide') } }) }); userUI.renderMoneyRequests = function (requests, error = null) { if (error) return console.error(error); else if (typeof requests !== "object" || requests === null) return; if (pagesData.lastPage === 'requests') { for (let r in requests) { getRef('requests_history_wrapper').querySelectorAll(`[data-vc="${r}"]`).forEach(card => card.remove()); if (requests[r].note) { getRef('payment_request_history').prepend(render.paymentRequestCard(requests[r])); } else { getRef('pending_payment_requests').prepend(render.paymentRequestCard(requests[r])); } } } if (floGlobals.loaded) { for (let r in requests) { if (!requests[r].note) { notify(`You have received payment request from ${getFloIdTitle(requests[r].senderID)}`, '', { pinned: true, action: { label: 'View', callback: () => { window.location.hash = `#/requests` } } }); } } } let totalRequests = 0; for (const request in User.moneyRequests) { if (!User.moneyRequests[request].note) totalRequests++; } const animOptions = { duration: 200, fill: 'forwards', easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)' } if (totalRequests) { if (!getRef('requests_page_button').querySelector('.badge')) { const badge = createElement('span', { className: 'badge', textContent: totalRequests }) getRef('requests_page_button').append(badge) badge.animate([ { transform: 'scale(0) translateY(0.5rem)' }, { transform: 'scale(1) translateY(0)' }, ], animOptions) } else { const badge = getRef('requests_page_button').querySelector('.badge'); badge.textContent = totalRequests; badge.animate([ { transform: 'scale(1)' }, { transform: `scale(1.5)` }, { transform: 'scale(1)' } ], animOptions) } } else { if (getRef('requests_page_button').querySelector('.badge')) { const badge = getRef('requests_page_button').querySelector('.badge') badge.animate([ { transform: 'scale(1) translateY(0)' }, { transform: 'scale(0) translateY(0.5rem)' }, ], animOptions).onfinish = () => { badge.remove() } } } }; userUI.payRequest = function (reqID) { let { message: { amount, remark }, senderID } = User.moneyRequests[reqID]; getConfirmation('Pay?', { message: `Do you want to pay ${amount} to ${senderID}?`, confirmText: 'Pay' }).then(confirmation => { if (confirmation) { User.sendToken(senderID, amount, "|" + remark).then(txid => { console.warn(`Sent ${amount} to ${senderID}`, txid); notify(`Sent ${formatAmount(amount)} to ${getFloIdTitle(senderID)}. It may take a few mins to reflect in their wallet`, 'success'); User.decideRequest(User.moneyRequests[reqID], 'PAID: ' + txid) .then(result => console.log(result)) .catch(error => console.error(error)) }).catch(error => console.error(error)); } }) } userUI.declineRequest = function (reqID) { let request = User.moneyRequests[reqID]; getConfirmation('Decline payment?', { confirmText: 'Decline' }).then(confirmation => { if (confirmation) { User.decideRequest(request, "DECLINED").then(result => { console.log(result); notify("Request declined", 'success'); }).catch(error => console.error(error)) } }) } delegate(getRef('pending_payment_requests'), 'click', '.pay-requested', e => { const vectorClock = e.target.closest('.payment-request').dataset.vc; userUI.payRequest(vectorClock); }) delegate(getRef('pending_payment_requests'), 'click', '.decline-payment', e => { const vectorClock = e.target.closest('.payment-request').dataset.vc; userUI.declineRequest(vectorClock); }) //Cashier const cashierUI = {}; cashierUI.renderRequests = function (requests, error = null) { if (error) return console.error(error); else if (typeof requests !== "object" || requests === null) return; const frag = document.createDocumentFragment(); for (let r in requests) { const oldCard = document.getElementById(r); if (oldCard) oldCard.remove(); frag.prepend(render.cashierRequestCard(requests[r])); } getRef('cashier_request_list').prepend(frag) } cashierUI.completeRequest = function (reqID) { let request = Cashier.Requests[reqID]; if (request.message.mode === "cash-to-token") completeCashToTokenRequest(request); else if (request.message.mode === "token-to-cash") completeTokenToCashRequest(request); } function completeCashToTokenRequest(request) { Cashier.checkIfUpiTxIsValid(request.message.upi_txid).then(_ => { let confirmation = confirm(`Check if you have received UPI transfer\ntxid:${request.message.upi_txid}\namount:${request.message.amount}`); if (!confirmation) return alert("Cancelled"); User.sendToken(request.senderID, request.message.amount, 'for cash-to-token').then(txid => { console.warn(`${request.message.amount} cash-to-token for ${request.senderID}`, txid); Cashier.finishRequest(request, txid).then(result => { console.log(result); console.info('Completed cash-to-token request:', request.vectorClock); alert("Completed request"); }).catch(error => console.error(error)) }).catch(error => console.error(error)) }).catch(error => { console.error(error); alert(error); if (Array.isArray(error) && error[0] === true && typeof error[1] === 'string') Cashier.rejectRequest(request, error[1]).then(result => { console.log(result); console.info('Rejected cash-to-token request:', request.vectorClock); }).catch(error => console.error(error)) }) } function completeTokenToCashRequest(request) { Cashier.checkIfTokenTxIsValid(request.message.token_txid, request.senderID, request.message.amount).then(result => { let upiTxID = prompt(`Token transfer is verified!\n Send ${request.message.amount} to ${request.message.upi_id} and Enter UPI txid`); if (!upiTxID) return alert("Cancelled"); Cashier.finishRequest(request, upiTxID).then(result => { console.log(result); console.info('Completed token-to-cash request:', request.vectorClock); alert("Completed request"); }).catch(error => console.error(error)) }).catch(error => { console.error(error); alert(error); if (Array.isArray(error) && error[0] === true && typeof error[1] === 'string') Cashier.rejectRequest(request, error[1]).then(result => { console.log(result); console.info('Rejected token-to-cash request:', request.vectorClock); }).catch(error => console.error(error)) }) } function getFloIdTitle(floID) { return floGlobals.savedIds[floID] ? floGlobals.savedIds[floID].title : floID; } function formatAmount(amount) { return amount.toLocaleString(`en-IN`, { style: 'currency', currency: 'INR' }) } function getStatusIcon(status) { switch (status) { case 'PENDING': return ''; case 'COMPLETED': return ''; case 'REJECTED': return ''; default: break; } } const render = { savedId(floID, details) { const { title } = details; const clone = getRef('saved_id_template').content.cloneNode(true).firstElementChild; clone.dataset.floId = floID; clone.querySelector('.saved-id__initials').textContent = title.charAt(0); clone.querySelector('.saved-id__title').textContent = title; clone.querySelector('.saved-id__flo-id').textContent = floID; return clone; }, transactionCard(transactionDetails) { const { txid, time, sender, receiver, tokenAmount } = transactionDetails; const clone = getRef('transaction_template').content.cloneNode(true).firstElementChild; clone.dataset.txid = txid; clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000); clone.querySelector('.transaction__amount').textContent = formatAmount(tokenAmount); if (sender === myFloID) { clone.classList.add('sent'); clone.querySelector('.transaction__receiver').textContent = `Sent to ${getFloIdTitle(receiver) || 'Myself'}`; clone.querySelector('.transaction__icon').innerHTML = ``; } else if (receiver === myFloID) { clone.classList.add('received'); clone.querySelector('.transaction__receiver').textContent = `Received from ${getFloIdTitle(sender)}`; clone.querySelector('.transaction__icon').innerHTML = ``; } else { //This should not happen unless API returns transaction that does not involve myFloID row.insertCell().textContent = tx.sender; row.insertCell().textContent = tx.receiver; } return clone; }, cashierRequestCard(details) { const { time, senderID, message: { mode }, note, tag, vectorClock } = details; const clone = getRef('cashier_request_template').content.cloneNode(true).firstElementChild; clone.id = vectorClock; const status = tag || note; //status tag for completed, note for rejected clone.querySelector('.cashier-request__requestor').textContent = senderID; clone.querySelector('.cashier-request__time').textContent = getFormattedTime(time); clone.querySelector('.cashier-request__mode').textContent = mode; if (status) clone.querySelector('.cashier-request__status').textContent = status; else clone.querySelector('.cashier-request__status').innerHTML = ``; return clone; }, walletRequestCard(details) { const { time, message: { mode, amount }, note, tag, vectorClock } = details; const clone = getRef('wallet_request_template').content.cloneNode(true).firstElementChild.firstElementChild; const type = mode === 'cash-to-token' ? 'Wallet top-up' : 'Transfer to bank'; let status = tag ? tag : (note ? 'REJECTED' : "PENDING"); clone.classList.add(status.toLowerCase()); clone.classList.add(mode === 'cash-to-token' ? 'added' : 'withdrawn'); clone.dataset.vc = vectorClock; clone.href = `#/transaction?transactionId=${vectorClock}&type=wallet`; clone.querySelector('.wallet-request__icon').innerHTML = mode === 'cash-to-token' ? `` : ``; clone.querySelector('.wallet-request__details').textContent = type; clone.querySelector('.wallet-request__amount').textContent = formatAmount(amount); clone.querySelector('.wallet-request__time').textContent = getFormattedTime(time); let icon = ''; if (status === 'REJECTED') { icon = `` clone.querySelector('.wallet-request__status').innerHTML = `Failed ${icon}`; } return clone; }, paymentRequestCard(details) { const { time, senderID, message: { amount, remark }, note, vectorClock } = details; const clone = getRef(`${note ? 'processed' : 'pending'}_payment_request_template`).content.cloneNode(true).firstElementChild; clone.dataset.vc = vectorClock; clone.querySelector('.payment-request__requestor').textContent = getFloIdTitle(senderID); clone.querySelector('.payment-request__remark').textContent = remark; clone.querySelector('.payment-request__time').textContent = getFormattedTime(time); clone.querySelector('.payment-request__amount').textContent = amount.toLocaleString(`en-IN`, { style: 'currency', currency: 'INR' }); const status = note ? note.split(':')[0] : 'PENDING'; if (note) { clone.firstElementChild.href = `#/transaction?transactionId=${vectorClock}&type=request`; let icon if (status === 'PAID') icon = `` else icon = `` clone.querySelector('.payment-request__status').innerHTML = `${status.toLowerCase()} ${icon}`; } return clone; }, transactionMessage(details) { const { tokenAmount, time, sender, receiver, flodata } = tokenAPI.util.parseTxData(details) let messageType = sender === receiver ? 'self' : sender === myFloID ? 'sent' : 'received'; const clone = getRef('transaction_message_template').content.cloneNode(true).firstElementChild; clone.classList.add(messageType); clone.querySelector('.transaction-message__amount').textContent = formatAmount(tokenAmount); if (flodata.split('|')[1]) { clone.querySelector('.transaction-message__remark').textContent = flodata.split('|')[1]; } clone.querySelector('.transaction-message__time').textContent = getFormattedTime(time * 1000); return clone; }, savedUpiId(upiId) { const clone = getRef('saved_upi_template').content.cloneNode(true).firstElementChild; clone.dataset.upiId = upiId; clone.querySelector('.saved-upi__id').textContent = upiId; return clone; }, savedIdPickerCard(floID, { title }) { return createElement('li', { className: 'saved-id grid interact', attributes: { 'tabindex': '0', 'data-flo-id': floID }, innerHTML: `