Skip to content

Instantly share code, notes, and snippets.

@ibnIrshad
Created July 26, 2025 20:43
Show Gist options
  • Save ibnIrshad/5fed6c90f96a3c90e63083a8ed0ecdab to your computer and use it in GitHub Desktop.
Save ibnIrshad/5fed6c90f96a3c90e63083a8ed0ecdab to your computer and use it in GitHub Desktop.
// 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