+
+
+
-
-
-
RanchiMall Pay
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save
+
+
+
+
+
+
+
+
+
+
+
@@ -319,8 +579,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -341,12 +633,13 @@
-
+
+
+
+
+
+
@@ -369,13 +668,18 @@
diff --git a/scripts/components.js b/scripts/components.js
index 85d5d80..122b6f3 100644
--- a/scripts/components.js
+++ b/scripts/components.js
@@ -689,148 +689,150 @@ customElements.define('sm-input',
})
const smNotifications = document.createElement('template')
smNotifications.innerHTML = `
-
-
-`;
-
-
+ .hide{
+ opacity: 0 !important;
+ pointer-events: none !important;
+ }
+ .notification-panel{
+ display: grid;
+ width: 100%;
+ gap: 0.5rem;
+ position: fixed;
+ left: 0;
+ top: 0;
+ z-index: 100;
+ max-height: 100%;
+ padding: 1rem;
+ overflow: hidden auto;
+ -ms-scroll-chaining: none;
+ overscroll-behavior: contain;
+ touch-action: none;
+ }
+ .notification-panel:empty{
+ display:none;
+ }
+ .notification{
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ position: relative;
+ border-radius: 0.3rem;
+ background: rgba(var(--foreground-color, (255,255,255)), 1);
+ overflow: hidden;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ -ms-word-break: break-all;
+ word-break: break-all;
+ word-break: break-word;
+ -ms-hyphens: auto;
+ -webkit-hyphens: auto;
+ hyphens: auto;
+ max-width: 100%;
+ padding: 1rem;
+ align-items: center;
+ touch-action: none;
+ }
+ .icon-container:not(:empty){
+ margin-right: 0.5rem;
+ height: var(--icon-height);
+ width: var(--icon-width);
+ }
+ h4:first-letter,
+ p:first-letter{
+ text-transform: uppercase;
+ }
+ h4{
+ font-weight: 400;
+ }
+ p{
+ line-height: 1.6;
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+ color: rgba(var(--text-color, (17,17,17)), 0.9);
+ overflow-wrap: break-word;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ -ms-word-break: break-all;
+ word-break: break-all;
+ word-break: break-word;
+ -ms-hyphens: auto;
+ -webkit-hyphens: auto;
+ hyphens: auto;
+ max-width: 100%;
+ }
+ .notification:last-of-type{
+ margin-bottom: 0;
+ }
+ .icon {
+ height: 100%;
+ width: 100%;
+ fill: rgba(var(--text-color, (17,17,17)), 0.7);
+ }
+ .icon--success {
+ fill: var(--green);
+ }
+ .icon--failure,
+ .icon--error {
+ fill: var(--danger-color);
+ }
+ .close{
+ height: 2rem;
+ width: 2rem;
+ border: none;
+ cursor: pointer;
+ margin-left: 1rem;
+ border-radius: 50%;
+ padding: 0.3rem;
+ transition: background-color 0.3s, transform 0.3s;
+ background-color: transparent;
+ }
+ .close:active{
+ transform: scale(0.9);
+ }
+ @media screen and (min-width: 640px){
+ .notification-panel{
+ max-width: 28rem;
+ width: max-content;
+ top: auto;
+ bottom: 0;
+ }
+ .notification{
+ width: auto;
+ border: solid 1px rgba(var(--text-color, (17,17,17)), 0.2);
+ }
+ }
+ @media (any-hover: hover){
+ ::-webkit-scrollbar{
+ width: 0.5rem;
+ }
+
+ ::-webkit-scrollbar-thumb{
+ background: rgba(var(--text-color, (17,17,17)), 0.3);
+ border-radius: 1rem;
+ &:hover{
+ background: rgba(var(--text-color, (17,17,17)), 0.5);
+ }
+ }
+ .close:hover{
+ background-color: rgba(var(--text-color, (17,17,17)), 0.1);
+ }
+ }
+
+
+ `;
customElements.define('sm-notifications', class extends HTMLElement {
constructor() {
super();
@@ -849,7 +851,23 @@ customElements.define('sm-notifications', class extends HTMLElement {
this.createNotification = this.createNotification.bind(this)
this.removeNotification = this.removeNotification.bind(this)
this.clearAll = this.clearAll.bind(this)
+ this.handlePointerMove = this.handlePointerMove.bind(this)
+
+ this.startX = 0;
+ this.currentX = 0;
+ this.endX = 0;
+ this.swipeDistance = 0;
+ this.swipeDirection = '';
+ this.swipeThreshold = 0;
+ this.startTime = 0;
+ this.swipeTime = 0;
+ this.swipeTimeThreshold = 200;
+ this.currentTarget = null;
+
+ this.mediaQuery = window.matchMedia('(min-width: 640px)')
+ this.handleOrientationChange = this.handleOrientationChange.bind(this)
+ this.isLandscape = false
}
randString(length) {
@@ -867,16 +885,16 @@ customElements.define('sm-notifications', class extends HTMLElement {
notification.classList.add('notification');
let composition = ``;
composition += `
-
-
-
- `;
+
+
+
+ `;
}
notification.innerHTML = composition;
return notification;
@@ -884,28 +902,45 @@ customElements.define('sm-notifications', class extends HTMLElement {
push(message, options = {}) {
const notification = this.createNotification(message, options);
- this.notificationPanel.append(notification);
+ if (this.isLandscape)
+ this.notificationPanel.append(notification);
+ else
+ this.notificationPanel.prepend(notification);
+ this.notificationPanel.animate(
+ [
+ {
+ transform: `translateY(${this.isLandscape ? '' : '-'}${notification.clientHeight}px)`,
+ },
+ {
+ transform: `none`,
+ },
+ ], this.animationOptions
+ )
notification.animate([
{
- transform: `translateY(1rem)`,
+ transform: `translateY(-1rem)`,
opacity: '0'
},
{
transform: `none`,
opacity: '1'
},
- ], this.animationOptions);
+ ], this.animationOptions).onfinish = (e) => {
+ e.target.commitStyles()
+ e.target.cancel()
+ }
return notification.id;
}
- removeNotification(notification) {
+ removeNotification(notification, direction = 'left') {
+ const sign = direction === 'left' ? '-' : '+';
notification.animate([
{
- transform: `none`,
+ transform: this.currentX ? `translateX(${this.currentX}px)` : `none`,
opacity: '1'
},
{
- transform: `translateY(0.5rem)`,
+ transform: `translateX(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`,
opacity: '0'
}
], this.animationOptions).onfinish = () => {
@@ -919,7 +954,70 @@ customElements.define('sm-notifications', class extends HTMLElement {
});
}
+ handlePointerMove(e) {
+ this.currentX = e.clientX - this.startX;
+ this.currentTarget.style.transform = `translateX(${this.currentX}px)`;
+ }
+
+ handleOrientationChange(e) {
+ this.isLandscape = e.matches
+ if (e.matches) {
+ // landscape
+
+ } else {
+ // portrait
+ }
+ }
connectedCallback() {
+
+ this.handleOrientationChange(this.mediaQuery);
+
+ this.mediaQuery.addEventListener('change', this.handleOrientationChange);
+ this.notificationPanel.addEventListener('pointerdown', e => {
+ if (e.target.closest('.notification')) {
+ this.swipeThreshold = this.clientWidth / 2;
+ this.currentTarget = e.target.closest('.notification');
+ this.currentTarget.setPointerCapture(e.pointerId);
+ this.startTime = Date.now();
+ this.startX = e.clientX;
+ this.startY = e.clientY;
+ this.notificationPanel.addEventListener('pointermove', this.handlePointerMove);
+ }
+ });
+ this.notificationPanel.addEventListener('pointerup', e => {
+ this.endX = e.clientX;
+ this.endY = e.clientY;
+ this.swipeDistance = Math.abs(this.endX - this.startX);
+ this.swipeTime = Date.now() - this.startTime;
+ if (this.endX > this.startX) {
+ this.swipeDirection = 'right';
+ } else {
+ this.swipeDirection = 'left';
+ }
+ if (this.swipeTime < this.swipeTimeThreshold) {
+ if (this.swipeDistance > 50)
+ this.removeNotification(this.currentTarget, this.swipeDirection);
+ } else {
+ if (this.swipeDistance > this.swipeThreshold) {
+ this.removeNotification(this.currentTarget, this.swipeDirection);
+ } else {
+ this.currentTarget.animate([
+ {
+ transform: `translateX(${this.currentX}px)`,
+ },
+ {
+ transform: `none`,
+ },
+ ], this.animationOptions).onfinish = (e) => {
+ e.target.commitStyles()
+ e.target.cancel()
+ }
+ }
+ }
+ this.notificationPanel.removeEventListener('pointermove', this.handlePointerMove)
+ this.notificationPanel.releasePointerCapture(e.pointerId);
+ this.currentX = 0;
+ });
this.notificationPanel.addEventListener('click', e => {
if (e.target.closest('.close')) {
this.removeNotification(e.target.closest('.notification'));
@@ -941,8 +1039,10 @@ customElements.define('sm-notifications', class extends HTMLElement {
childList: true,
});
}
+ disconnectedCallback() {
+ mediaQueryList.removeEventListener('change', handleOrientationChange);
+ }
});
-
class Stack {
constructor() {
this.items = [];
@@ -1830,7 +1930,6 @@ smCopy.innerHTML = `
}
.copy{
display: grid;
- width: 100%;
gap: 0.5rem;
padding: var(--padding);
align-items: center;
@@ -1851,8 +1950,15 @@ smCopy.innerHTML = `
cursor: pointer;
border: none;
padding: 0.4rem;
- background-color: inherit;
+ background-color: rgba(var(--text-color, (17,17,17)), 0.06);
border-radius: var(--button-border-radius, 0.3rem);
+ transition: background-color 0.2s;
+ font-family: inherit;
+ color: inherit;
+ font-size: 0.7rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.05rem;
}
.copy-button:active{
background-color: var(--button-background-color);
@@ -1866,9 +1972,6 @@ smCopy.innerHTML = `
.copy:hover .copy-button{
opacity: 1;
}
- .copy-button{
- opacity: 0.6;
- }
.copy-button:hover{
background-color: var(--button-background-color);
}
@@ -1878,7 +1981,7 @@ smCopy.innerHTML = `
-
+ COPY
@@ -2265,14 +2368,19 @@ customElements.define('strip-select', class extends HTMLElement {
this.slottedOptions = undefined;
this._value = undefined;
this.scrollDistance = 0;
+ this.assignedElements = [];
this.scrollLeft = this.scrollLeft.bind(this);
this.scrollRight = this.scrollRight.bind(this);
this.fireEvent = this.fireEvent.bind(this);
+ this.setSelectedOption = this.setSelectedOption.bind(this);
}
get value() {
return this._value;
}
+ set value(val) {
+ this.setSelectedOption(val);
+ }
scrollLeft() {
this.stripSelect.scrollBy({
left: -this.scrollDistance,
@@ -2286,6 +2394,19 @@ customElements.define('strip-select', class extends HTMLElement {
behavior: 'smooth'
});
}
+ setSelectedOption(value) {
+ if (this._value === value) return
+ this._value = value;
+ this.assignedElements.forEach(elem => {
+ if (elem.value === value) {
+ elem.setAttribute('active', '');
+ elem.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
+ }
+ else
+ elem.removeAttribute('active')
+ });
+ }
+
fireEvent() {
this.dispatchEvent(
new CustomEvent("change", {
@@ -2306,17 +2427,17 @@ customElements.define('strip-select', class extends HTMLElement {
const navButtonLeft = this.shadowRoot.querySelector('.nav-button--left');
const navButtonRight = this.shadowRoot.querySelector('.nav-button--right');
slot.addEventListener('slotchange', e => {
- const assignedElements = slot.assignedElements();
- assignedElements.forEach(elem => {
+ this.assignedElements = slot.assignedElements();
+ this.assignedElements.forEach(elem => {
if (elem.hasAttribute('selected')) {
elem.setAttribute('active', '');
this._value = elem.value;
}
});
if (!this.hasAttribute('multiline')) {
- if (assignedElements.length > 0) {
- firstOptionObserver.observe(slot.assignedElements()[0]);
- lastOptionObserver.observe(slot.assignedElements()[slot.assignedElements().length - 1]);
+ if (this.assignedElements.length > 0) {
+ firstOptionObserver.observe(this.assignedElements[0]);
+ lastOptionObserver.observe(this.assignedElements[this.assignedElements.length - 1]);
}
else {
navButtonLeft.classList.add('hide');
@@ -2343,10 +2464,7 @@ customElements.define('strip-select', class extends HTMLElement {
resObs.observe(this);
this.stripSelect.addEventListener('option-clicked', e => {
if (this._value !== e.target.value) {
- this._value = e.target.value;
- slot.assignedElements().forEach(elem => elem.removeAttribute('active'));
- e.target.setAttribute('active', '');
- e.target.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
+ this.setSelectedOption(e.target.value);
this.fireEvent();
}
});
diff --git a/scripts/fn_ui.js b/scripts/fn_ui.js
index b9e1502..2d503f5 100644
--- a/scripts/fn_ui.js
+++ b/scripts/fn_ui.js
@@ -1,37 +1,47 @@
-/*jshint esversion: 6 */
+/*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 = {};
-userUI.requestTokenFromCashier = function () {
+getRef('wallet_popup__cta').addEventListener('click', function () {
let cashier = User.findCashier();
- if (!cashier)
- return alert("No cashier online");
let amount = parseFloat(getRef('request_cashier_amount').value.trim());
- //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))
-}
-
-userUI.withdrawCashFromCashier = function () {
- let cashier = User.findCashier();
- if (!cashier)
- return alert("No cashier online");
- let amount = parseFloat(getRef('request_cashier_amount').value.trim());
- //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 => {
+ 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))
- }).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) {
@@ -63,13 +73,15 @@ userUI.renderCashierRequests = function (requests, error = null) {
return console.error(error);
else if (typeof requests !== "object" || requests === null)
return;
- const frag = document.createDocumentFragment()
- for (let r in requests) {
- let oldCard = document.getElementById(r);
- if (oldCard) oldCard.remove();
- frag.append(render.walletRequestCard(requests[r]))
+ if (pagesData.lastPage === 'history' && pagesData.params.type === 'wallet') {
+ const frag = document.createDocumentFragment()
+ for (let transactionID in requests) {
+ let oldCard = getRef('wallet_history').querySelector(`#${transactionID}`);
+ if (oldCard) oldCard.remove();
+ frag.append(render.walletRequestCard(transactionID, requests[transactionID]))
+ }
+ getRef('wallet_history').prepend(frag)
}
- getRef('user-cashier-requests').append(frag)
}
userUI.renderMoneyRequests = function (requests, error = null) {
@@ -86,6 +98,32 @@ userUI.renderMoneyRequests = function (requests, error = null) {
getRef('user-money-requests').append(frag)
}
+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);
+}
+
userUI.payRequest = function (reqID) {
let request = User.moneyRequests[reqID];
getConfirmation('Pay?', { message: `Do you want to pay ${request.message.amount} to ${request.senderID}?` }).then(confirmation => {
@@ -183,71 +221,116 @@ function completeTokenToCashRequest(request) {
})
}
-function renderAllTokenTransactions() {
- tokenAPI.getAllTxs(myFloID).then(result => {
- getRef('token_transactions').innerHTML = ''
- const frag = document.createDocumentFragment();
- for (let txid in result.transactions) {
- frag.append(render.transactionCard(txid, tokenAPI.util.parseTxData(result.transactions[txid])))
- }
- getRef('token_transactions').append(frag)
- }).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 = {
- transactionCard(txid, transactionDetails) {
- const { time, sender, receiver, tokenAmount } = transactionDetails
+ 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 = tokenAmount
+ clone.dataset.txid = txid;
+ clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000);
+ clone.querySelector('.transaction__amount').textContent = formatAmount(tokenAmount);
if (sender === myFloID) {
- clone.querySelector('.transaction__amount').classList.add('sent')
- clone.querySelector('.transaction__receiver').textContent = `Sent to ${receiver || 'Myself'}`
- clone.querySelector('.transaction__icon').innerHTML = ` `
+ clone.classList.add('sent');
+ clone.querySelector('.transaction__receiver').textContent = `Sent to ${getFloIdTitle(receiver) || 'Myself'}`;
+ clone.querySelector('.transaction__icon').innerHTML = ` `;
} else if (receiver === myFloID) {
- clone.querySelector('.transaction__amount').classList.add('received')
- clone.querySelector('.transaction__receiver').textContent = `Received from ${sender}`
- clone.querySelector('.transaction__icon').innerHTML = ` `
+ 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
+ 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
+ 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
+ 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
+ clone.querySelector('.cashier-request__status').textContent = status;
else
- clone.querySelector('.cashier-request__status').innerHTML = `Process `
- return clone
+ clone.querySelector('.cashier-request__status').innerHTML = `Process `;
+ return clone;
},
walletRequestCard(details) {
- const { time, receiverID, message: { mode }, note, tag, vectorClock } = details;
+ const { time, message: { mode, amount }, note, tag, vectorClock } = details;
const clone = getRef('wallet_request_template').content.cloneNode(true).firstElementChild;
- clone.id = vectorClock
- clone.querySelector('.wallet-request__requestor').textContent = receiverID
- clone.querySelector('.wallet-request__time').textContent = getFormattedTime(time)
- clone.querySelector('.wallet-request__mode').textContent = mode === 'cash-to-token' ? 'Deposit' : 'Withdraw'
- let status = tag ? (tag + ":" + note) : (note || "PENDING");
- clone.querySelector('.wallet-request__status').textContent = status
- return clone
+ clone.id = vectorClock;
+ clone.querySelector('.wallet-request__details').textContent = `${mode === 'cash-to-token' ? 'Deposit' : 'Withdraw'} ${formatAmount(amount)}`;
+ clone.querySelector('.wallet-request__time').textContent = getFormattedTime(time);
+ let status = tag ? tag : (note ? 'REJECTED' : "PENDING");
+ let icon = '';
+ switch (status) {
+ case 'COMPLETED':
+ clone.children[1].append(
+ createElement('div', {
+ className: 'flex flex-wrap align-center wallet-request__note',
+ innerHTML: `Transaction ID: `
+ })
+ );
+ icon = ` `
+ break;
+ case 'REJECTED':
+ clone.children[1].append(
+ createElement('div', {
+ className: 'wallet-request__note',
+ innerHTML: note.split(':')[1]
+ })
+ );
+ icon = ` `
+ break;
+ case 'PENDING':
+ icon = ` `
+ break;
+
+ default:
+ break;
+ }
+ clone.querySelector('.wallet-request__status').innerHTML = `${icon}${status}`;
+ clone.querySelector('.wallet-request__status').classList.add(status.toLowerCase());
+ return clone;
},
paymentRequestCard(details) {
const { time, senderID, message: { amount, remark }, note, vectorClock } = details;
const clone = getRef('payment_request_template').content.cloneNode(true).firstElementChild;
- clone.id = vectorClock
- clone.querySelector('.payment-request__requestor').textContent = 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
+ clone.id = vectorClock;
+ clone.querySelector('.payment-request__requestor').textContent = 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)
@@ -257,20 +340,75 @@ const render = {
`Pay
Decline `;
- return clone
+ 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
+let currentUserAction;
function showTokenTransfer(type) {
getRef('tt_button').textContent = type;
- currentUserAction = 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')
+ showPopup('token_transfer_popup');
+}
+
+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() {
@@ -278,28 +416,46 @@ function executeUserAction() {
amount = parseFloat(getRef('tt_amount').value),
remark = getRef('tt_remark').value.trim();
if (currentUserAction === 'send') {
- userUI.sendMoneyToUser(floID, amount, remark)
+ userUI.sendMoneyToUser(floID, amount, remark);
} else {
- userUI.requestMoneyFromUser(floID, amount, remark)
+ userUI.requestMoneyFromUser(floID, amount, remark);
}
}
function changeUpi() {
- const upiID = getRef('upi_id').value.trim()
+ const upiID = getRef('upi_id').value.trim();
Cashier.updateUPI(upiID).then(() => {
- notify('UPI ID updated successfully', 'success')
+ notify('UPI ID updated successfully', 'success');
}).catch(err => {
- notify(err, 'error')
- })
+ 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()
+ await floDapps.clearCredentials();
+ location.reload();
}
- })
+ });
}
\ No newline at end of file
diff --git a/scripts/std_ui.js b/scripts/std_ui.js
index 1985074..926ba1c 100644
--- a/scripts/std_ui.js
+++ b/scripts/std_ui.js
@@ -2,6 +2,9 @@
// Global variables
const domRefs = {};
const currentYear = new Date().getFullYear();
+let paymentsHistoryLoader = null;
+let walletHistoryLoader = null;
+let contactHistoryLoader = null;
//Checks for internet connection status
if (!navigator.onLine)
@@ -186,7 +189,7 @@ function getFormattedTime(time, format) {
return `${month} ${date}, ${year}`;
break;
default:
- return `${month} ${date} ${year}, ${finalHours}`;
+ return `${month} ${date}, ${year} at ${finalHours}`;
}
} catch (e) {
console.error(e);
@@ -253,17 +256,21 @@ function createRipple(event, target) {
}
const pagesData = {
- params: {}
+ params: {},
+ openedPages: new Set(),
}
-let tempData
async function showPage(targetPage, options = {}) {
- const { firstLoad, hashChange, isPreview } = options
+ const { firstLoad, hashChange } = options
let pageId
let params = {}
let searchParams
if (targetPage === '') {
- pageId = 'home'
+ if (typeof myFloID === "undefined") {
+ pageId = 'sign_in'
+ } else {
+ pageId = 'home'
+ }
} else {
if (targetPage.includes('/')) {
if (targetPage.includes('?')) {
@@ -281,115 +288,216 @@ async function showPage(targetPage, options = {}) {
pageId = targetPage
}
}
+ if (typeof myFloID === "undefined" && !(['sign_up', 'sign_in', 'loading', 'landing'].includes(pageId))) return
+ else if (typeof myFloID !== "undefined" && (['sign_up', 'sign_in', 'loading', 'landing'].includes(pageId))) {
+ history.replaceState(null, null, '#/home');
+ pageId = 'home'
+ }
if (searchParams) {
const urlSearchParams = new URLSearchParams('?' + searchParams);
params = Object.fromEntries(urlSearchParams.entries());
}
+ switch (pageId) {
+ case 'sign_in':
+ setTimeout(() => {
+ getRef('private_key_field').focusIn()
+ }, 0);
+ targetPage = 'sign_in'
+ break;
+ case 'sign_up':
+ const { floID, privKey } = floCrypto.generateNewID()
+ getRef('generated_flo_id').value = floID
+ getRef('generated_private_key').value = privKey
+ targetPage = 'sign_up'
+ break;
+ case 'contact':
+ getRef('contact__title').textContent = getFloIdTitle(params.floId)
+ Promise.all([
+ tokenAPI.fetch_api(`api/v1.0/getTokenTransactions?token=rupee&senderFloAddress=${myFloID}&destFloAddress=${params.floId}`),
+ tokenAPI.fetch_api(`api/v1.0/getTokenTransactions?token=rupee&senderFloAddress=${params.floId}&destFloAddress=${myFloID}`)])
+ .then(([sentTransactions, receivedTransactions]) => {
+ const allTransactions = Object.values({ ...sentTransactions.transactions, ...receivedTransactions.transactions }).sort((a, b) => b.transactionDetails.time - a.transactionDetails.time)
+ if (contactHistoryLoader) {
+ contactHistoryLoader.update(allTransactions)
+ } else {
+ contactHistoryLoader = new LazyLoader('#contact__transactions', allTransactions, render.transactionMessage, { bottomFirst: true });
+ }
+ contactHistoryLoader.init()
+ }).catch(err => {
+ console.error(err)
+ })
+ break;
+ case 'history':
+ const paymentTransactions = []
+ if (paymentsHistoryLoader)
+ paymentsHistoryLoader.clear()
+ getRef('payments_history').innerHTML = ' '
+ tokenAPI.getAllTxs(myFloID).then(({ transactions }) => {
+ for (const transactionId in transactions) {
+ paymentTransactions.push({
+ ...tokenAPI.util.parseTxData(transactions[transactionId]),
+ txid: transactionId
+ })
+ }
+ if (paymentsHistoryLoader) {
+ paymentsHistoryLoader.update(paymentTransactions)
+ } else {
+ paymentsHistoryLoader = new LazyLoader('#payments_history', paymentTransactions, render.transactionCard);
+ }
+ paymentsHistoryLoader.init()
+ }).catch(e => {
+ console.error(e)
+ })
+ break;
+ case 'wallet':
+ const walletTransactions = []
+ if (walletHistoryLoader)
+ walletHistoryLoader.clear()
+ getRef('wallet_history').innerHTML = ' '
+ const requests = User.cashierRequests;
+ for (const transactionId in requests) {
+ walletTransactions.push(User.cashierRequests[transactionId])
+ }
+ if (walletHistoryLoader) {
+ walletHistoryLoader.update(walletTransactions)
+ } else {
+ walletHistoryLoader = new LazyLoader('#wallet_history', walletTransactions, render.walletRequestCard);
+ }
+ walletHistoryLoader.init()
+ break;
+ default:
+ break;
+ }
+ if (pageId !== 'history') {
+ if (paymentsHistoryLoader)
+ paymentsHistoryLoader.clear()
+ }
+ if (pageId !== 'contact') {
+ if (contactHistoryLoader)
+ contactHistoryLoader.clear()
+ }
+ if (pageId !== 'wallet') {
+ if (walletHistoryLoader)
+ walletHistoryLoader.clear()
+ }
+
if (pagesData.lastPage !== pageId) {
+ const animOptions = {
+ duration: 100,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
+ }
+ let previousActiveElement = getRef('main_navbar').querySelector('.nav-item--active')
+ const currentActiveElement = document.querySelector(`.nav-item[href="#/${pageId}"]`)
+ if (currentActiveElement) {
+ if (getRef('main_navbar').classList.contains('hide')) {
+ getRef('main_card').classList.remove('nav-hidden')
+ getRef('main_navbar').classList.remove('hide-away')
+ getRef('main_navbar').classList.remove('hide')
+ getRef('main_navbar').animate([
+ {
+ transform: isMobileView ? `translateY(100%)` : `translateX(-100%)`,
+ opacity: 0,
+ },
+ {
+ transform: `none`,
+ opacity: 1,
+ },
+ ], { ...animOptions, easing: 'ease-in' })
+ }
+ getRef('main_header').classList.remove('hide')
+ const previousActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(previousActiveElement)
+ const currentActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(currentActiveElement)
+ const isOnTop = previousActiveElementIndex < currentActiveElementIndex
+ const currentIndicator = createElement('div', { className: 'nav-item__indicator' });
+ let previousIndicator = getRef('main_navbar').querySelector('.nav-item__indicator')
+ if (!previousIndicator) {
+ previousIndicator = currentIndicator.cloneNode(true)
+ previousActiveElement = currentActiveElement
+ previousActiveElement.append(previousIndicator)
+ } else if (currentActiveElementIndex !== previousActiveElementIndex) {
+ const indicatorDimensions = previousIndicator.getBoundingClientRect()
+ const currentActiveElementDimensions = currentActiveElement.getBoundingClientRect()
+ let moveBy
+ if (isMobileView) {
+ moveBy = ((currentActiveElementDimensions.width - indicatorDimensions.width) / 2) + indicatorDimensions.width
+ } else {
+ moveBy = ((currentActiveElementDimensions.height - indicatorDimensions.height) / 2) + indicatorDimensions.height
+ }
+ indicatorObserver.observe(previousIndicator)
+ previousIndicator.animate([
+ {
+ transform: 'none',
+ opacity: 1,
+ },
+ {
+ transform: `translate${isMobileView ? 'X' : 'Y'}(${isOnTop ? `${moveBy}px` : `-${moveBy}px`})`,
+ opacity: 0,
+ },
+ ], { ...animOptions, easing: 'ease-in' }).onfinish = () => {
+ previousIndicator.remove()
+ }
+ tempData = {
+ currentActiveElement,
+ currentIndicator,
+ isOnTop,
+ animOptions,
+ moveBy
+ }
+ }
+ previousActiveElement.classList.remove('nav-item--active');
+ currentActiveElement.classList.add('nav-item--active')
+ } else {
+ if (!getRef('main_navbar').classList.contains('hide')) {
+ getRef('main_card').classList.add('nav-hidden')
+ getRef('main_navbar').classList.add('hide-away')
+ getRef('main_navbar').animate([
+ {
+ transform: `none`,
+ opacity: 1,
+ },
+ {
+ transform: isMobileView ? `translateY(100%)` : `translateX(-100%)`,
+ opacity: 0,
+ },
+ ], {
+ duration: 200,
+ fill: 'forwards',
+ easing: 'ease'
+ }).onfinish = () => {
+ getRef('main_navbar').classList.add('hide')
+ }
+ getRef('main_header').classList.add('hide')
+ }
+ }
+ document.querySelectorAll('.page').forEach(page => page.classList.add('hide'))
+ getRef(pageId).closest('.page').classList.remove('hide')
+ document.querySelectorAll('.inner-page').forEach(page => page.classList.add('hide'))
+ getRef(pageId).classList.remove('hide')
+ getRef('main_card').style.overflowY = "hidden";
+ getRef(pageId).animate([
+ {
+ opacity: 0,
+ transform: 'translateY(1rem)'
+ },
+ {
+ opacity: 1,
+ transform: 'translateY(0)'
+ },
+ ],
+ {
+ duration: 300,
+ easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
+ }).onfinish = () => {
+ getRef('main_card').style.overflowY = "";
+ }
pagesData.lastPage = pageId
}
if (params)
pagesData.params = params
- switch (pageId) {
- case 'transactions':
- break;
- default:
+ pagesData.openedPages.add(pageId)
- }
- const animOptions = {
- duration: 100,
- fill: 'forwards',
- }
- let previousActiveElement = getRef('main_navbar').querySelector('.nav-item--active')
- const currentActiveElement = document.querySelector(`.nav-item[href="#/${pageId}"]`)
- if (currentActiveElement) {
- if (getRef('main_navbar').classList.contains('hide')) {
- getRef('main_navbar').classList.remove('hide-away')
- getRef('main_navbar').classList.remove('hide')
- getRef('main_navbar').animate([
- {
- transform: isMobileView ? `translateY(100%)` : `translateX(-100%)`,
- opacity: 0,
- },
- {
- transform: `none`,
- opacity: 1,
- },
- ], {
- duration: 100,
- fill: 'forwards',
- easing: 'ease'
- })
- }
- getRef('main_header').classList.remove('hide')
- const previousActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(previousActiveElement)
- const currentActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(currentActiveElement)
- const isOnTop = previousActiveElementIndex < currentActiveElementIndex
- const currentIndicator = createElement('div', { className: 'nav-item__indicator' });
- let previousIndicator = getRef('main_navbar').querySelector('.nav-item__indicator')
- if (!previousIndicator) {
- previousIndicator = currentIndicator.cloneNode(true)
- previousActiveElement = currentActiveElement
- previousActiveElement.append(previousIndicator)
- } else if (currentActiveElementIndex !== previousActiveElementIndex) {
- const indicatorDimensions = previousIndicator.getBoundingClientRect()
- const currentActiveElementDimensions = currentActiveElement.getBoundingClientRect()
- let moveBy
- if (isMobileView) {
- moveBy = ((currentActiveElementDimensions.width - indicatorDimensions.width) / 2) + indicatorDimensions.width
- } else {
- moveBy = ((currentActiveElementDimensions.height - indicatorDimensions.height) / 2) + indicatorDimensions.height
- }
- indicatorObserver.observe(previousIndicator)
- previousIndicator.animate([
- {
- transform: 'none',
- opacity: 1,
- },
- {
- transform: `translate${isMobileView ? 'X' : 'Y'}(${isOnTop ? `${moveBy}px` : `-${moveBy}px`})`,
- opacity: 0,
- },
- ], { ...animOptions, easing: 'ease-in' }).onfinish = () => {
- previousIndicator.remove()
- }
- tempData = {
- currentActiveElement,
- currentIndicator,
- isOnTop,
- animOptions,
- moveBy
- }
- }
- previousActiveElement.classList.remove('nav-item--active');
- currentActiveElement.classList.add('nav-item--active')
- } else {
- if (!getRef('main_navbar').classList.contains('hide')) {
- getRef('main_navbar').classList.add('hide-away')
- getRef('main_navbar').animate([
- {
- transform: `none`,
- opacity: 1,
- },
- {
- transform: isMobileView ? `translateY(100%)` : `translateX(-100%)`,
- opacity: 0,
- },
- ], {
- duration: 200,
- fill: 'forwards',
- easing: 'ease'
- }).onfinish = () => {
- getRef('main_navbar').classList.add('hide')
- }
- getRef('main_header').classList.add('hide')
- }
- }
- document.querySelectorAll('.page').forEach(page => page.classList.add('hide'))
- getRef(pageId).classList.remove('hide')
- getRef(pageId).animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300, fill: 'forwards', easing: 'ease' })
}
-
const indicatorObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (!entry.isIntersecting) {
@@ -414,7 +522,7 @@ const indicatorObserver = new IntersectionObserver(entries => {
// class based lazy loading
class LazyLoader {
constructor(container, elementsToRender, renderFn, options = {}) {
- const { batchSize = 10, freshRender } = options
+ const { batchSize = 10, freshRender, bottomFirst = false } = options
this.elementsToRender = elementsToRender
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
@@ -423,6 +531,7 @@ class LazyLoader {
this.batchSize = batchSize
this.freshRender = freshRender
+ this.bottomFirst = bottomFirst
this.lazyContainer = document.querySelector(container)
@@ -446,7 +555,11 @@ class LazyLoader {
mutationList.forEach(mutation => {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length) {
- this.intersectionObserver.observe(this.lazyContainer.lastElementChild)
+ if (this.bottomFirst)
+ this.intersectionObserver.observe(this.lazyContainer.firstElementChild)
+ else
+ this.intersectionObserver.observe(this.lazyContainer.lastElementChild)
+
}
}
})
@@ -458,7 +571,6 @@ class LazyLoader {
}
update(elementsToRender) {
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
- this.render()
}
render(options = {}) {
let { lazyLoad = false } = options
@@ -472,10 +584,21 @@ class LazyLoader {
this.updateStartIndex = 0
this.updateEndIndex = this.arrayOfElements.length > this.batchSize ? this.batchSize : this.arrayOfElements.length
}
- for (let index = this.updateStartIndex; index < this.updateEndIndex; index++) {
- frag.append(this.renderFn(this.arrayOfElements[index]))
+ if (this.bottomFirst) {
+ for (let index = this.updateStartIndex; index < this.updateEndIndex; index++) {
+ frag.prepend(this.renderFn(this.arrayOfElements[index]))
+ }
+ this.lazyContainer.prepend(frag)
+ } else {
+ for (let index = this.updateStartIndex; index < this.updateEndIndex; index++) {
+ frag.append(this.renderFn(this.arrayOfElements[index]))
+ }
+ this.lazyContainer.append(frag)
}
- this.lazyContainer.append(frag)
+ if (!lazyLoad && this.bottomFirst)
+ this.lazyContainer.scrollTo({
+ top: this.lazyContainer.scrollHeight,
+ })
// Callback to be called if elements are updated or rendered for first time
if (!lazyLoad && this.freshRender)
this.freshRender()
+
+
+ RanchiMall Pay
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+ Sign out
-
-
- Wallet transactions
- Payment requests
-
-
-
-
-
-
-
-
-
-
- Sign out
-
-
-
-
-
- Change
-
-
-
+
+
+
+
+ Change
+
+
+
+
-
-
+
+
+
+
+
+ Add FLO ID
+
+
+
+
-
- -
-
-
- Balance
- -
-
-
-
+
-
-
-
-
-
-
-
-
- Deposit
-
-
-
-
-
-
- Withdraw
-
+ Scan FLO QR
+
+
+
+
+
+
+
+
+
+
+ Saved FLO IDs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No Saved FLO ID
Requests
-
-
- No requests to process
-Requests
+
+
No requests to process
+Transactions history
--
-
Payments history
+
+
+ No transactions
+
+
+
+
+
+
+
+
+
+
+ Pay
+ Request
+
+ Payment requests
+
+
+ No requests
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wallet
+
+
+
+ Balance
+ +
+
+
+
+
+
+ Top-up wallet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Transfer to bank
+
+
+
+
+
+ Wallet history
+No transactions
Settings
+
+
+
+ My FLO ID
+Activity
-
-
- No transactions
-
-
- No requests
-Settings
-
-
-
- My FLO ID
-Change UPI ID
-Change UPI ID
+Save FLO ID
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+ FLO ID
+
+
+
+
+
+
+
+ Save
+
+
-
+
+
+
+
+
-
-
@@ -362,6 +655,12 @@
${icon}
- ${message}
- `; +${icon}
+ ${message}
+ `; if (pinned) { notification.classList.add('pinned'); composition += ` -