Created
July 26, 2025 20:43
-
-
Save ibnIrshad/5fed6c90f96a3c90e63083a8ed0ecdab to your computer and use it in GitHub Desktop.
source code for fintable chrome extension at https://chromewebstore.google.com/detail/fintable-check-all-your-b/kjbglnedcpajmegbgibjbflpblhllffc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Fintable Chrome Extension - Combined Popup Script | |
// Base URLs and constants | |
const FINTABLE_BASE_URL = 'https://fintable.io'; | |
const FINTABLE_API = `${FINTABLE_BASE_URL}/api`; | |
const DASHBOARD_URL = `${FINTABLE_BASE_URL}/dashboard`; | |
// DOM Elements - grouped by container | |
const containers = { | |
email: document.getElementById('email-container'), | |
code: document.getElementById('code-container'), | |
dashboard: document.getElementById('dashboard-container') | |
}; | |
const elements = { | |
// Email view | |
emailInput: document.getElementById('email-input'), | |
submitEmailBtn: document.getElementById('submit-email-btn'), | |
emailStatus: document.getElementById('email-status'), | |
// Code view | |
codeInput: document.getElementById('code-input'), | |
submitCodeBtn: document.getElementById('submit-code-btn'), | |
backToEmailBtn: document.getElementById('back-to-email-btn'), | |
codeStatus: document.getElementById('code-status'), | |
verificationEmail: document.getElementById('verification-email'), | |
// Dashboard view | |
userNameEl: document.getElementById('user-name'), | |
connectionsListEl: document.getElementById('connections-list'), | |
dashboardLink: document.getElementById('dashboard-link'), | |
refreshBtn: document.getElementById('refresh-btn'), | |
logoutBtn: document.getElementById('logout-btn') | |
}; | |
// Initialize the extension | |
document.addEventListener('DOMContentLoaded', async () => { | |
try { | |
const authStatus = await checkAuthentication(); | |
if (authStatus.isAuthenticated) { | |
showView('dashboard'); | |
if (authStatus.userData && authStatus.userData.user_name) { | |
elements.userNameEl.textContent = authStatus.userData.user_name; | |
} | |
await fetchAndDisplayBankData(); | |
} else if (authStatus.authStage === 'code_verification') { | |
// If user has entered email but not verified code yet | |
showView('code'); | |
// Display the user's email in the verification message | |
const email = authStatus.email || 'your email'; | |
elements.verificationEmail.textContent = email; | |
updateStatus(elements.codeStatus, 'Please enter the verification code sent to your email', 'success'); | |
// Pre-populate email field for when going back | |
elements.emailInput.value = authStatus.email || ''; | |
} else { | |
showView('email'); | |
} | |
} catch (error) { | |
console.error('Initialization error:', error); | |
showView('email'); | |
updateStatus(elements.emailStatus, 'Error checking login status', 'error'); | |
} | |
}); | |
// Event Listeners | |
elements.submitEmailBtn.addEventListener('click', async () => { | |
const email = elements.emailInput.value.trim(); | |
if (!isValidEmail(email)) { | |
updateStatus(elements.emailStatus, 'Please enter a valid email address', 'error'); | |
return; | |
} | |
updateStatus(elements.emailStatus, 'Sending verification code...', 'success'); | |
elements.submitEmailBtn.disabled = true; | |
try { | |
const result = await submitEmail(email); | |
if (result.success) { | |
// Display the email address in the verification screen | |
elements.verificationEmail.textContent = email; | |
showView('code'); | |
updateStatus(elements.codeStatus, 'Verification code sent to your email', 'success'); | |
} else { | |
updateStatus(elements.emailStatus, result.error || 'Failed to send verification code', 'error'); | |
} | |
} catch (error) { | |
updateStatus(elements.emailStatus, 'Error sending verification code', 'error'); | |
} finally { | |
elements.submitEmailBtn.disabled = false; | |
} | |
}); | |
elements.submitCodeBtn.addEventListener('click', async () => { | |
const code = elements.codeInput.value.trim(); | |
if (!code) { | |
updateStatus(elements.codeStatus, 'Please enter the verification code', 'error'); | |
return; | |
} | |
updateStatus(elements.codeStatus, 'Verifying code...', 'success'); | |
elements.submitCodeBtn.disabled = true; | |
try { | |
const storage = await chrome.storage.local.get(['email']); | |
const email = storage.email; | |
if (!email) { | |
updateStatus(elements.codeStatus, 'Email not found, please go back and try again', 'error'); | |
return; | |
} | |
const result = await verifyCode(email, code); | |
if (result.success) { | |
showView('dashboard'); | |
await fetchAndDisplayBankData(); | |
} else { | |
updateStatus(elements.codeStatus, result.error || 'Invalid verification code', 'error'); | |
} | |
} catch (error) { | |
updateStatus(elements.codeStatus, 'Error verifying code', 'error'); | |
} finally { | |
elements.submitCodeBtn.disabled = false; | |
} | |
}); | |
elements.backToEmailBtn.addEventListener('click', async (e) => { | |
e.preventDefault(); | |
// Clear the code verification stage when going back to email input | |
try { | |
await chrome.storage.local.remove(['auth_stage']); | |
} catch (error) { | |
console.error('Error clearing auth stage:', error); | |
} | |
showView('email'); | |
elements.codeInput.value = ''; | |
updateStatus(elements.codeStatus, '', ''); | |
}); | |
elements.refreshBtn.addEventListener('click', async (e) => { | |
e.preventDefault(); | |
await fetchAndDisplayBankData(); | |
}); | |
elements.logoutBtn.addEventListener('click', async () => { | |
try { | |
await chrome.storage.local.remove(['email', 'finkey', 'auth_stage']); | |
showView('email'); | |
updateStatus(elements.emailStatus, 'Successfully logged out', 'success'); | |
} catch (error) { | |
updateStatus(elements.emailStatus, 'Error logging out', 'error'); | |
} | |
}); | |
// API Functions | |
async function checkAuthentication() { | |
try { | |
const result = await chrome.storage.local.get(['email', 'finkey', 'auth_stage']); | |
// Check if we have a finkey - means user is fully authenticated | |
if (result.finkey) { | |
const response = await fetch(`${FINTABLE_API}/airext/main/chrome-ext`, { | |
method: 'GET', | |
headers: { | |
'Accept': 'application/json', | |
'Content-Type': 'application/json', | |
'X-Finkey': result.finkey | |
} | |
}); | |
if (response.ok) { | |
const data = await response.json(); | |
// Update dashboard link if available | |
if (data.dashboard_url) { | |
elements.dashboardLink.href = data.dashboard_url; | |
} | |
return { | |
isAuthenticated: true, | |
userData: data, | |
email: result.email | |
}; | |
} else { | |
// If authentication failed, clear stored data | |
if (response.status === 401) { | |
await chrome.storage.local.remove(['email', 'finkey', 'auth_stage']); | |
} | |
return { isAuthenticated: false }; | |
} | |
} | |
// Check if we're in code verification stage | |
if (result.auth_stage === 'code_verification' && result.email) { | |
return { | |
isAuthenticated: false, | |
email: result.email, | |
authStage: 'code_verification' | |
}; | |
} | |
// Not authenticated at all | |
return { isAuthenticated: false }; | |
} catch (error) { | |
console.error('Auth check error:', error); | |
return { isAuthenticated: false, error: error.message }; | |
} | |
} | |
async function submitEmail(email) { | |
try { | |
const response = await fetch(`${FINTABLE_API}/airext/setEmail`, { | |
method: 'POST', | |
headers: { | |
'Accept': 'application/json', | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ email }) | |
}); | |
if (response.ok) { | |
// Store email and also set auth_stage to track progress | |
await chrome.storage.local.set({ | |
email: email, | |
auth_stage: 'code_verification' | |
}); | |
return { success: true }; | |
} else { | |
const errorData = await response.json(); | |
return { | |
success: false, | |
error: errorData.message || 'Failed to send verification code' | |
}; | |
} | |
} catch (error) { | |
console.error('Email submission error:', error); | |
return { success: false, error: error.message }; | |
} | |
} | |
async function verifyCode(email, code) { | |
try { | |
const response = await fetch(`${FINTABLE_API}/airext/exchangeFinkey`, { | |
method: 'POST', | |
headers: { | |
'Accept': 'application/json', | |
'Content-Type': 'application/json', | |
'X-Finkey': code | |
}, | |
body: JSON.stringify({ email }) | |
}); | |
if (response.ok) { | |
const data = await response.json(); | |
// Store finkey and email, and remove auth_stage since authentication is complete | |
await chrome.storage.local.set({ | |
finkey: data.finkey, | |
email: email, | |
auth_stage: null // Clear the auth stage as we're now fully authenticated | |
}); | |
return { success: true }; | |
} else { | |
const errorData = await response.json(); | |
return { | |
success: false, | |
error: errorData.message || 'Invalid verification code' | |
}; | |
} | |
} catch (error) { | |
console.error('Code verification error:', error); | |
return { success: false, error: error.message }; | |
} | |
} | |
async function fetchBankData() { | |
try { | |
const result = await chrome.storage.local.get(['finkey']); | |
if (!result.finkey) { | |
return { success: false, error: 'Not authenticated' }; | |
} | |
const response = await fetch(`${FINTABLE_API}/airext/main/chrome-ext`, { | |
method: 'GET', | |
headers: { | |
'Accept': 'application/json', | |
'Content-Type': 'application/json', | |
'X-Finkey': result.finkey | |
} | |
}); | |
if (response.ok) { | |
const data = await response.json(); | |
return { success: true, data }; | |
} else { | |
if (response.status === 401) { | |
await chrome.storage.local.remove(['email', 'finkey']); | |
return { success: false, error: 'Authentication expired' }; | |
} | |
const errorData = await response.json(); | |
return { | |
success: false, | |
error: errorData.message || 'Failed to fetch bank data' | |
}; | |
} | |
} catch (error) { | |
console.error('Fetch bank data error:', error); | |
return { success: false, error: error.message }; | |
} | |
} | |
// Helper Functions | |
async function fetchAndDisplayBankData() { | |
elements.connectionsListEl.innerHTML = '<div class="loader"></div>'; | |
try { | |
const result = await fetchBankData(); | |
if (result.success && result.data) { | |
// Update user name from API response if available | |
if (result.data.user_name) { | |
elements.userNameEl.textContent = result.data.user_name; | |
} | |
renderBankConnections(result.data); | |
} else { | |
if (result.error === 'Authentication expired') { | |
showView('email'); | |
updateStatus(elements.emailStatus, 'Your session has expired. Please login again.', 'error'); | |
} else { | |
elements.connectionsListEl.innerHTML = `<p class="status-message error">${result.error || 'Failed to load bank connections'}</p>`; | |
} | |
} | |
} catch (error) { | |
elements.connectionsListEl.innerHTML = '<p class="status-message error">Error loading bank connections</p>'; | |
} | |
} | |
function renderBankConnections(data) { | |
if (!data.bank_items || data.bank_items.length === 0) { | |
const connectUrl = data.secure_new_conn_url || DASHBOARD_URL; | |
elements.connectionsListEl.innerHTML = ` | |
<p>No bank connections found.</p> | |
<p><a href="${connectUrl}" target="_blank">Click here</a> to connect a bank.</p> | |
`; | |
return; | |
} | |
let html = ''; | |
data.bank_items.forEach(bank => { | |
html += ` | |
<div class="connection-item"> | |
<div class="connection-header"> | |
<div class="connection-name">${bank.institution_name}</div> | |
<a href="${bank.secure_sync_url}" target="_blank" class="text-link">Sync↗</a> | |
</div> | |
<div class="connection-status">Last update: ${bank.last_read_pretty}, ${bank.text_status}</div> | |
<div class="connection-accounts"> | |
`; | |
bank.accounts.forEach(account => { | |
html += ` | |
<div class="account-item"> | |
<div class="account-name">${account.pretty_name} (${account.type_text})</div> | |
<div class="account-balance">${account.balance_text}</div> | |
</div> | |
`; | |
}); | |
html += ` | |
</div> | |
</div> | |
`; | |
}); | |
elements.connectionsListEl.innerHTML = html; | |
} | |
function showView(viewName) { | |
// Hide all containers | |
Object.values(containers).forEach(container => { | |
container.style.display = 'none'; | |
}); | |
// Show the requested container | |
containers[viewName].style.display = 'block'; | |
// Focus on input if it's email or code view | |
if (viewName === 'email') { | |
setTimeout(() => elements.emailInput.focus(), 100); | |
} else if (viewName === 'code') { | |
setTimeout(() => elements.codeInput.focus(), 100); | |
} | |
} | |
function updateStatus(element, message, type = '') { | |
element.textContent = message; | |
element.className = 'status-message'; | |
if (type) element.classList.add(type); | |
} | |
function isValidEmail(email) { | |
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; | |
return re.test(String(email).toLowerCase()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment