/*jshint esversion: 9 */
// Global variables
const { html, render: renderElem,svg } = uhtml;
const domRefs = {};
let rupeeHistoryLoader = null;
let btcHistoryLoader = null;
let walletHistoryLoader = null;
let contactHistoryLoader = null;
let paymentRequestsLoader = null;
//Checks for internet connection status
if (!navigator.onLine)
notify(
"There seems to be a problem connecting to the internet. Please check your internet connection.",
"error"
);
window.addEventListener("offline", () => {
notify(
"There seems to be a problem connecting to the internet. Please check your internet connection.",
"error",
{ pinned: true }
);
});
window.addEventListener("online", () => {
getRef("notification_drawer").clearAll();
notify("We are back online.", "success");
});
// Use instead of document.getElementById
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;
}
}
}
// returns dom with specified element
function createElement(tagName, options = {}) {
const { className, textContent, innerHTML, attributes = {} } = options
const elem = document.createElement(tagName)
for (let attribute in attributes) {
elem.setAttribute(attribute, attributes[attribute])
}
if (className)
elem.className = className
if (textContent)
elem.textContent = textContent
if (innerHTML)
elem.innerHTML = innerHTML
return elem
}
// 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);
};
}
let zIndex = 50
// function required for popups or modals to appear
function openPopup(popupId, pinned) {
zIndex++
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
getRef(popupId).show({ pinned })
return getRef(popupId);
}
// hides the popup or modal
function closePopup() {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide()
}
document.addEventListener('popupopened', async e => {
getRef('main_card').setAttribute('inert', '')
const frag = document.createDocumentFragment()
switch (e.target.id) {
case 'saved_ids_popup':
renderElem(getRef('saved_ids_picker_list'), html`${getArrayOfSavedIds().map(({ floID, details }) => render.savedIdPickerCard(floID, details))}`)
setTimeout(() => {
getRef('search_saved_ids_picker').focusIn()
}, 0);
break;
case 'topup_wallet_popup':
case 'withdraw_wallet_popup':
let hasSavedIds = false
for (const upiId in floGlobals.savedUserData.upiIds) {
frag.append(render.savedUpiIdOption(upiId))
hasSavedIds = true
}
if (hasSavedIds) {
const clone = frag.cloneNode(true)
getRef('select_withdraw_upi_id').append(clone)
getRef('select_withdraw_upi_id').parentNode.classList.remove('hidden')
}
break;
case 'send_btc_popup':
calculateBtcFees();
break;
}
})
document.addEventListener('popupclosed', e => {
zIndex--
switch (e.target.id) {
case 'saved_ids_popup':
getRef('saved_ids_picker_list').innerHTML = ''
getRef('search_saved_ids_picker').value = ''
break;
case 'topup_wallet_popup':
showChildElement('topup_wallet_process', 0)
break;
case 'withdraw_wallet_popup':
getRef('select_withdraw_upi_id').parentNode.classList.add('hidden')
getRef('select_withdraw_upi_id').innerHTML = ''
showChildElement('withdraw_wallet_process', 0)
break;
case 'transfer_to_exchange_popup':
showChildElement('exchange_transfer_process', 0);
break;
case 'confirm_topup_popup':
showChildElement('confirm_topup_wrapper', 0);
break;
}
if (popupStack.items.length === 0) {
getRef('main_card').removeAttribute('inert')
}
})
// displays a popup for asking permission. Use this instead of JS confirm
const getConfirmation = (title, options = {}) => {
return new Promise(resolve => {
const { message = '', cancelText = 'Cancel', confirmText = 'OK' } = options
openPopup('confirmation_popup', true)
getRef('confirm_title').innerText = title;
getRef('confirm_message').innerText = message;
let cancelButton = getRef('confirmation_popup').children[2].children[0],
submitButton = getRef('confirmation_popup').children[2].children[1]
submitButton.textContent = confirmText
cancelButton.textContent = cancelText
submitButton.onclick = () => {
closePopup()
resolve(true);
}
cancelButton.onclick = () => {
closePopup()
resolve(false);
}
})
}
// displays a popup for asking user input. Use this instead of JS prompt
function getPromptInput(title, message = '', options = {}) {
let { placeholder = '', isPassword = false, cancelText = 'Cancel', confirmText = 'OK' } = options
openPopup('prompt_popup', true)
getRef('prompt_title').innerText = title;
getRef('prompt_message').innerText = message;
let buttons = getRef('prompt_popup').querySelectorAll("sm-button");
if (isPassword) {
placeholder = 'Password'
getRef('prompt_input').setAttribute("type", "password")
}
getRef('prompt_input').setAttribute("placeholder", placeholder)
getRef('prompt_input').focusIn()
buttons[0].textContent = cancelText;
buttons[1].textContent = confirmText;
return new Promise((resolve, reject) => {
buttons[0].onclick = () => {
closePopup()
return (null);
}
buttons[1].onclick = () => {
const value = getRef('prompt_input').value;
closePopup()
resolve(value)
}
})
}
//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 = ``
break;
case 'error':
icon = ``
options.pinned = true
break;
}
getRef("notification_drawer").push(message, { icon, ...options });
if (mode === 'error') {
console.error(message)
}
}
function getFormattedTime(timestamp, format) {
try {
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':
// check if timestamp is older than a day
if (Date.now() - new Date(timestamp) < 60 * 60 * 24 * 1000)
return `${finalHours}`;
else
return relativeTime.from(timestamp)
default:
return `${month} ${date}, ${year} at ${finalHours}`;
}
} catch (e) {
console.error(e);
return timestamp;
}
}
// implement event delegation
function delegate(el, event, selector, fn) {
el.addEventListener(event, function (e) {
const potentialTarget = e.target.closest(selector)
if (potentialTarget) {
e.delegateTarget = potentialTarget
fn.call(this, e)
}
})
}
// detect browser version
function detectBrowser() {
let ua = navigator.userAgent,
tem,
M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return 'IE ' + (tem[1] || '');
}
if (M[1] === 'Chrome') {
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera');
}
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
return M.join(' ');
}
window.addEventListener('hashchange', e => showPage(window.location.hash))
window.addEventListener("load", () => {
const [browserName, browserVersion] = detectBrowser().split(' ');
const supportedVersions = {
Chrome: 85,
Firefox: 75,
Safari: 13,
}
if (browserName in supportedVersions) {
if (parseInt(browserVersion) < supportedVersions[browserName]) {
notify(`${browserName} ${browserVersion} is not fully supported, some features may not work properly. Please update to ${supportedVersions[browserName]} or higher.`, 'error')
}
} else {
notify('Browser is not fully compatible, some features may not work. for best experience please use Chrome, Edge, Firefox or Safari', 'error')
}
document.body.classList.remove('hidden')
document.querySelectorAll('sm-input[data-flo-id]').forEach(input => input.customValidation = floCrypto.validateAddr)
document.addEventListener('keyup', (e) => {
if (e.key === 'Escape') {
closePopup()
}
})
document.addEventListener('copy', () => {
notify('copied', 'success')
})
document.addEventListener("pointerdown", (e) => {
if (e.target.closest("button:not([disabled]), sm-button:not([disabled]), .interact")) {
createRipple(e, e.target.closest("button, sm-button, .interact"));
}
});
document.querySelectorAll('.popup__header__close, .close-popup-on-click').forEach(elem => {
elem.addEventListener('click', () => {
closePopup()
})
})
});
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(
[
{
opacity: 1,
transform: `scale(0)`
},
{
transform: "scale(4)",
opacity: 0,
},
],
{
duration: 600,
fill: "forwards",
easing: "ease-out",
}
);
target.append(circle);
rippleAnimation.onfinish = () => {
circle.remove();
};
}
const pagesData = {
params: {},
openedPages: new Set(),
}
async function showPage(targetPage, options = {}) {
const { firstLoad, hashChange } = options
let pageId
let params = {}
let searchParams
if (targetPage === '') {
try {
if(floDapps.user.id)
pageId = 'home'
}catch(e){
pageId = 'sign_in'
}
} else {
if (targetPage.includes('/')) {
if (targetPage.includes('?')) {
const splitAddress = targetPage.split('?')
searchParams = splitAddress.pop()
const pages = splitAddress.pop().split('/')
pageId = pages[1]
subPageId = pages[2]
} else {
const pages = targetPage.split('/')
pageId = pages[1]
subPageId = pages[2]
}
} else {
pageId = targetPage
}
}
try {
if (floDapps.user.id && (['sign_up', 'sign_in', 'loading', 'landing'].includes(pageId))) {
history.replaceState(null, null, '#/home');
pageId = 'home'
}
}catch(e){
if ( !(['sign_up', 'sign_in', 'loading', 'landing'].includes(pageId))) return
}
if (searchParams) {
const urlSearchParams = new URLSearchParams('?' + searchParams);
params = Object.fromEntries(urlSearchParams.entries());
}
switch (pageId) {
case 'sign_in':
setTimeout(() => {
getRef('private_key_field').focusIn()
}, 0);
break;
case 'sign_up':
const { floID, privKey } = floCrypto.generateNewID()
getRef('generated_flo_id').value = floID
getRef('generated_private_key').value = privKey
break;
case 'home':
getExchangeRate().then(rate => {
getRef('conversion_rate').textContent = `1BTC = ${formatAmount(rate.inr)}`;
})
break;
case 'contact':
getRef('contact__title').textContent = getFloIdTitle(params.floId)
getRef('contact__transactions').innerHTML = '