457 lines
22 KiB
JavaScript
457 lines
22 KiB
JavaScript
/*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' });
|
|
const userUI = {};
|
|
|
|
getRef('wallet_popup__cta').addEventListener('click', function () {
|
|
let cashier = User.findCashier();
|
|
let amount = parseFloat(getRef('request_cashier_amount').value.trim());
|
|
if (walletAction === 'deposit') {
|
|
//get UPI txid from user
|
|
let upiTxID = prompt(`Send Rs. ${amount} to ${cashierUPI[cashier]} and enter UPI txid`);
|
|
if (!upiTxID)
|
|
return alert("Cancelled");
|
|
User.cashToToken(cashier, amount, upiTxID).then(result => {
|
|
console.log(result);
|
|
alert("Requested cashier. please wait!");
|
|
}).catch(error => console.error(error))
|
|
} else {
|
|
//get confirmation from user
|
|
let upiID = prompt(`${amount} ${floGlobals.currency}# will be sent to ${cashier}. Enter UPI ID`);
|
|
if (!upiID)
|
|
return alert("Cancelled");
|
|
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 => {
|
|
console.log(result);
|
|
alert("Requested cashier. please wait!");
|
|
}).catch(error => console.error(error))
|
|
}).catch(error => console.error(error))
|
|
}
|
|
})
|
|
function walletAction(type) {
|
|
let cashier = User.findCashier();
|
|
if (!cashier)
|
|
return notify("No cashier online. Please try again in a while.", 'error');
|
|
showPopup('wallet_popup')
|
|
}
|
|
|
|
userUI.sendMoneyToUser = function (floID, amount, remark) {
|
|
getConfirmation('Confirm', { message: `Do you want to SEND ${amount} to ${floID}?` }).then(confirmation => {
|
|
if (confirmation) {
|
|
User.sendToken(floID, amount, "|" + remark).then(txid => {
|
|
console.warn(`Sent ${amount} to ${floID}`, txid);
|
|
notify(`Sent ${amount} to ${floID}. It may take a few mins to reflect in their wallet`, 'success');
|
|
hidePopup()
|
|
}).catch(error => console.error(error));
|
|
}
|
|
})
|
|
}
|
|
|
|
userUI.requestMoneyFromUser = function (floID, amount, remark) {
|
|
getConfirmation('Confirm', { message: `Do you want to REQUEST ${amount} from ${floID}?` }).then(confirmation => {
|
|
if (confirmation) {
|
|
User.requestToken(floID, amount, remark).then(result => {
|
|
console.log(`Requested ${amount} from ${floID}`, result);
|
|
notify(`Requested ${amount} from ${floID}`, 'success');
|
|
hidePopup()
|
|
}).catch(error => console.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) {
|
|
let oldCard = getRef('pending_wallet_transactions').querySelector(`[data-vc-${transactionID}]`);
|
|
if (oldCard) {
|
|
oldCard.remove()
|
|
getRef('wallet_history').prepend(render.walletRequestCard(requests[transactionID]))
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
delegate(getRef('wallet_history_wrapper'), 'click', '.wallet-request', e => {
|
|
let transactionID = e.delegateTarget.dataset.vc;
|
|
let request = User.cashierRequests[transactionID];
|
|
console.log(request)
|
|
})
|
|
|
|
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) {
|
|
let oldCard = getRef('user-money-requests').querySelector(`[data-vc-${r}]`);
|
|
if (oldCard)
|
|
oldCard.replaceWith(render.paymentRequestCard(requests[r]));
|
|
}
|
|
}
|
|
};
|
|
|
|
userUI.payRequest = function (reqID) {
|
|
let request = User.moneyRequests[reqID];
|
|
getConfirmation('Pay?', { message: `Do you want to pay ${request.message.amount} to ${request.senderID}?`, confirmText: 'Pay' }).then(confirmation => {
|
|
if (confirmation) {
|
|
User.sendToken(request.senderID, request.message.amount, "|" + request.message.remark).then(txid => {
|
|
console.warn(`Sent ${request.message.amount} to ${request.senderID}`, txid);
|
|
notify(`Sent ${request.message.amount} to ${request.senderID}. It may take a few mins to reflect in their wallet`, 'success');
|
|
User.decideRequest(request, '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('user-money-requests'), 'click', '.pay-requested', e => {
|
|
const vectorClock = e.target.closest('.payment-request').dataset.vc;
|
|
userUI.payRequest(vectorClock);
|
|
})
|
|
delegate(getRef('user-money-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.append(render.cashierRequestCard(requests[r]));
|
|
}
|
|
getRef('cashier_request_list').append(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 '<i class="fas fa-clock"></i>';
|
|
case 'COMPLETED':
|
|
return '<i class="fas fa-check"></i>';
|
|
case 'REJECTED':
|
|
return '<i class="fas fa-times"></i>';
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
const render = {
|
|
savedId(floID, details) {
|
|
const { title } = details.hasOwnProperty('title') ? details : { 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;
|
|
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 = `<svg class="icon sent" 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="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>`;
|
|
} else if (receiver === myFloID) {
|
|
clone.classList.add('received');
|
|
clone.querySelector('.transaction__receiver').textContent = `Received from ${getFloIdTitle(sender)}`;
|
|
clone.querySelector('.transaction__icon').innerHTML = `<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="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
|
|
} 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 = `<button class="button" onclick="cashierUI.completeRequest('${vectorClock}')">Process</button>`;
|
|
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' ? 'Deposit' : 'Withdraw';
|
|
let status = tag ? tag : (note ? 'REJECTED' : "PENDING");
|
|
clone.classList.add(status.toLowerCase());
|
|
clone.dataset.vc = vectorClock;
|
|
clone.href = `#/transaction?transactionId=${vectorClock}`;
|
|
clone.querySelector('.wallet-request__icon').innerHTML = type === 'Deposit' ?
|
|
`<svg class="icon" 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 d="M21 18v1c0 1.1-.9 2-2 2H5c-1.11 0-2-.9-2-2V5c0-1.1.89-2 2-2h14c1.1 0 2 .9 2 2v1h-9c-1.11 0-2 .9-2 2v8c0 1.1.89 2 2 2h9zm-9-2h10V8H12v8zm4-2.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" /></svg>`
|
|
:
|
|
`<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24" /></g><g><g><rect height="7" width="3" x="4" y="10" /><rect height="7" width="3" x="10.5" y="10" /><rect height="3" width="20" x="2" y="19" /><rect height="7" width="3" x="17" y="10" /><polygon points="12,1 2,6 2,8 22,8 22,6" /></g></g></svg>`;
|
|
clone.querySelector('.wallet-request__details').textContent = `${type} ${formatAmount(amount)}`;
|
|
clone.querySelector('.wallet-request__time').textContent = getFormattedTime(time);
|
|
let icon = '';
|
|
if (status === 'REJECTED') {
|
|
icon = `<svg class="icon failed" 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 d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>`
|
|
clone.querySelector('.wallet-request__status').innerHTML = `Failed ${icon}`;
|
|
}
|
|
return clone;
|
|
},
|
|
paymentRequestCard(details) {
|
|
const { time, senderID, message: { amount, remark }, note, vectorClock } = details;
|
|
const clone = getRef('payment_request_template').content.cloneNode(true).firstElementChild;
|
|
clone.dataset.vc = vectorClock;
|
|
clone.querySelector('.payment-request__requestor').textContent = getFloIdTitle(senderID);
|
|
clone.querySelector('.payment-request__time').textContent = getFormattedTime(time);
|
|
clone.querySelector('.payment-request__amount').textContent = amount.toLocaleString(`en-IN`, { style: 'currency', currency: 'INR' });
|
|
clone.querySelector('.payment-request__remark').textContent = remark;
|
|
|
|
let status = note;
|
|
if (status)
|
|
clone.querySelector('.payment-request__actions').textContent = note;
|
|
else
|
|
clone.querySelector('.payment-request__actions').innerHTML =
|
|
`<button class="button pay-requested">Pay</button>
|
|
<button class="button decline-payment">Decline</button>`;
|
|
|
|
return clone;
|
|
},
|
|
transactionMessage(details) {
|
|
const { tokenAmount, time, sender, receiver } = 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);
|
|
clone.querySelector('.transaction-message__time').textContent = getFormattedTime(time * 1000);
|
|
return clone;
|
|
}
|
|
};
|
|
|
|
let currentUserAction;
|
|
function showTokenTransfer(type) {
|
|
getRef('tt_button').textContent = type;
|
|
currentUserAction = type;
|
|
if (type === 'send') {
|
|
getRef('token_transfer__title').textContent = 'Send money to FLO ID';
|
|
} else {
|
|
getRef('token_transfer__title').textContent = 'Request money from FLO ID';
|
|
}
|
|
showPopup('token_transfer_popup');
|
|
}
|
|
userUI.renderSavedIds = async function () {
|
|
floGlobals.savedIds = {}
|
|
const frag = document.createDocumentFragment()
|
|
const savedIds = await floCloudAPI.requestApplicationData('savedIds', { mostRecent: true, senderIDs: [myFloID], receiverID: myFloID });
|
|
if (savedIds.length && await compactIDB.readData('savedIds', 'lastSyncTime') !== savedIds[0].time) {
|
|
await compactIDB.clearData('savedIds');
|
|
const dataToDecrypt = floCloudAPI.util.decodeMessage(savedIds[0].message)
|
|
const data = JSON.parse(Crypto.AES.decrypt(dataToDecrypt, myPrivKey));
|
|
for (let key in data) {
|
|
floGlobals.savedIds[key] = data[key];
|
|
compactIDB.addData('savedIds', data[key], key);
|
|
}
|
|
compactIDB.addData('savedIds', savedIds[0].time, 'lastSyncTime');
|
|
} else {
|
|
const idsToRender = await compactIDB.readAllData('savedIds');
|
|
for (const key in idsToRender) {
|
|
if (key !== 'lastSyncTime')
|
|
floGlobals.savedIds[key] = idsToRender[key];
|
|
}
|
|
}
|
|
for (const key in floGlobals.savedIds) {
|
|
frag.append(render.savedId(key, floGlobals.savedIds[key]));
|
|
}
|
|
getRef('saved_ids_list').append(frag);
|
|
}
|
|
async function saveId() {
|
|
const floID = getRef('flo_id_to_save').value.trim();
|
|
const title = getRef('flo_id_title_to_save').value.trim();
|
|
floGlobals.savedIds[floID] = { title }
|
|
getRef('saved_ids_list').append(render.savedId(floID, { title }));
|
|
syncSavedIds().then(() => {
|
|
notify(`Saved ${floID}`, 'success');
|
|
hidePopup();
|
|
}).catch(error => {
|
|
notify(error, 'error');
|
|
})
|
|
}
|
|
function syncSavedIds() {
|
|
const dataToSend = Crypto.AES.encrypt(JSON.stringify(floGlobals.savedIds), myPrivKey);
|
|
return floCloudAPI.sendApplicationData(dataToSend, 'savedIds', { receiverID: myFloID });
|
|
}
|
|
delegate(getRef('saved_ids_list'), 'click', '.saved-id', e => {
|
|
if (e.target.closest('.edit-saved')) {
|
|
const target = e.target.closest('.saved-id');
|
|
getRef('edit_saved_id').setAttribute('value', target.dataset.floId);
|
|
getRef('newAddrLabel').value = getFloIdTitle(target.dataset.floId);
|
|
showPopup('edit_saved_popup');
|
|
} else {
|
|
const target = e.target.closest('.saved-id');
|
|
window.location.hash = `#/contact?floId=${target.dataset.floId}`;
|
|
}
|
|
});
|
|
function deleteSaved() {
|
|
getConfirmation('Do you want delete this FLO ID?', {
|
|
confirmText: 'Delete',
|
|
}).then(res => {
|
|
if (res) {
|
|
const toDelete = getRef('saved_ids_list').querySelector(`.saved-id[data-flo-id="${getRef('edit_saved_id').value}"]`);
|
|
if (toDelete)
|
|
toDelete.remove();
|
|
delete floGlobals.savedIds[getRef('edit_saved_id').value];
|
|
hidePopup();
|
|
syncSavedIds().then(() => {
|
|
notify(`Deleted saved ID`, 'success');
|
|
}).catch(error => {
|
|
notify(error, 'error');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function executeUserAction() {
|
|
const floID = getRef('tt_flo_id').value.trim(),
|
|
amount = parseFloat(getRef('tt_amount').value),
|
|
remark = getRef('tt_remark').value.trim();
|
|
if (currentUserAction === 'send') {
|
|
userUI.sendMoneyToUser(floID, amount, remark);
|
|
|
|
} else {
|
|
userUI.requestMoneyFromUser(floID, amount, remark);
|
|
}
|
|
}
|
|
|
|
function changeUpi() {
|
|
const upiID = getRef('upi_id').value.trim();
|
|
Cashier.updateUPI(upiID).then(() => {
|
|
notify('UPI ID updated successfully', 'success');
|
|
}).catch(err => {
|
|
notify(err, 'error');
|
|
});
|
|
}
|
|
function getSignedIn() {
|
|
return new Promise((resolve, reject) => {
|
|
if (window.location.hash.includes('sign_in') || window.location.hash.includes('sign_up')) {
|
|
showPage(window.location.hash);
|
|
} else {
|
|
location.hash = `#/sign_in`;
|
|
}
|
|
getRef('sign_in_button').onclick = () => {
|
|
resolve(getRef('private_key_field').value.trim());
|
|
getRef('private_key_field').value = '';
|
|
showPage('loading');
|
|
};
|
|
getRef('sign_up_button').onclick = () => {
|
|
resolve(getRef('generated_private_key').value.trim());
|
|
getRef('generated_private_key').value = '';
|
|
showPage('loading');
|
|
};
|
|
});
|
|
}
|
|
function signOut() {
|
|
getConfirmation('Sign out?', 'You are about to sign out of the app, continue?', 'Stay', 'Leave')
|
|
.then(async (res) => {
|
|
if (res) {
|
|
await floDapps.clearCredentials();
|
|
location.reload();
|
|
}
|
|
});
|
|
} |