Stop overpaying.

Upload your bank statement and we'll find every subscription, bill, and fee you could be paying less for.

πŸ“„
Drop your bank statements here
or click to browse Β· PDF only Β· upload from multiple accounts
How it works
1

Upload

Drop your bank statement PDF. We read it right in your browser session.

2

AI Analyzes

Our engine identifies recurring charges and finds cheaper Australian alternatives.

3

See Savings

Get a clear breakdown of how much you can save β€” and how to switch.

Your data is processed securely. Sign in to save your results. πŸ”’

Analyzing your statement…
Reading transactions
Identifying recurring charges
Finding cheaper alternatives
πŸ˜•

Something went wrong

We couldn't process your statement. Please try again with a different file.

+ tsTotal.toFixed(2) + ''; html += '
'; for (var t = 0; t < tsSorted.length; t++) { var tKey = tsSorted[t]; var tCat = getCat(tKey); var tAmt = tsCats[tKey]; var tPct = tsTotal > 0 ? Math.round((tAmt / tsTotal) * 100) : 0; var tBarW = Math.max(8, Math.round((tAmt / tsMax) * 100)); html += '
'; html += '
' + tCat.icon + '
'; html += '
'; html += '
' + esc(tCat.label) + '
'; html += '
/* ── render results ── */ function renderResults(data, isSaved) { var s = data.summary; var charges = data.charges; var html = ''; /* saved data banner */ if (isSaved && isLoggedIn()) { var updStr = savedData && savedData.updatedAt ? new Date(savedData.updatedAt).toLocaleDateString() : ''; var stmtNames = (data.statements || []).join(', ') || 'your statements'; html += '
πŸ“Š Your saved profile Β· ' + esc(s.total_recurring) + ' charges from ' + esc(stmtNames); if (updStr) html += ' Β· last updated ' + esc(updStr); html += '
+ Add statement
'; } /* header */ var _stmtCount = s.statements_processed || (data.statements || []).length || 1; var _rTitle = _stmtCount > 1 ? 'Your Combined Savings Report' : 'Your Savings Report'; var _rSub = _stmtCount > 1 ? esc(s.total_recurring) + ' charges analyzed across ' + _stmtCount + ' statements' : esc(s.total_recurring) + ' recurring charges analyzed'; html += '

' + _rTitle + '

' + _rSub + '

'; /* summary cards */ var _tsTotalDebits = data.total_spending && data.total_spending.monthly_total_debits ? data.total_spending.monthly_total_debits : null; html += '
'; if (_tsTotalDebits) { html += '
Total Monthly Spend
$' + esc(_tsTotalDebits.toFixed(2)) + '/mo
'; } html += '
Recurring Bills
$' + esc(s.total_monthly_spend.toFixed(2)) + '/mo
'; html += '
You Could Save
$' + esc(s.total_potential_monthly_savings.toFixed(2)) + '/mo
'; html += '
Annual Savings
$' + esc(s.total_potential_annual_savings.toFixed(2)) + '/yr
'; html += '
'; /* spending stats */ if (charges.length > 1 || data.total_spending) { html += buildSpendingStats(charges, data.total_spending); } /* save nudge for non-logged-in users */ if (!isSaved && !isLoggedIn()) { html += '
'; html += '
πŸ”’
'; html += '

Save your savings profile

'; html += '

Sign in with your email to save these results, add more bank statements later, and build a complete picture of your spending.

'; html += ''; html += 'Maybe later'; html += '
'; } /* charge cards */ for (var i = 0; i < charges.length; i++) { var c = charges[i]; var cat = getCat(c.category); var price = c.current_price != null ? c.current_price : c.current_amount; html += '
'; html += '
'; var logoUrl = getMerchantLogo(c.merchant); html += ''; html += '
' + esc(c.merchant) + '
' + esc(c.frequency || 'Monthly') + ' Β· ' + esc(CATEGORY_LABELS[c.category] || c.category.replace(/_/g, ' ')) + '
'; html += '
' + esc(fmt(price)) + '/mo
'; html += '
'; if (c.alternatives && c.alternatives.length > 0) { html += '
'; for (var j = 0; j < c.alternatives.length; j++) { var a = c.alternatives[j]; var aProvider = a.provider || a.name; var aPrice = a.price; var saving = (aPrice != null && price != null) ? (price - aPrice) : null; if (saving !== null && saving < 0) saving = null; var savingText = saving != null ? ('Save $' + saving.toFixed(2) + '/mo') : 'Compare'; var badgeClass = saving != null ? 'green' : 'blue'; var priceDisplay = aPrice === 0 ? 'Free' : (aPrice == null ? 'Get quote' : '$' + aPrice.toFixed(2) + '/mo'); html += '
'; html += '
β†’
'; html += '
'; html += '
' + esc(aProvider) + ' Β· ' + esc(priceDisplay) + '
'; if (a.note) html += '
' + esc(a.note) + '
'; if (a.specs) html += '
' + esc(a.specs) + '
'; html += '
'; html += '
'; html += '' + esc(savingText) + ''; var switchSpecs = a.specs ? esc(a.specs) : ''; var switchCurPrice = price != null ? esc(price.toFixed(2)) : ''; var switchNewPrice = aPrice != null ? esc(aPrice.toFixed(2)) : ''; html += ''; html += '
'; } html += '
'; } html += '
'; } /* total bar */ html += '
Total potential savings
$' + esc(s.total_potential_annual_savings.toFixed(2)) + '/yr
'; /* actions */ html += '
'; html += ''; html += ''; html += '
'; html += ''; document.getElementById('results-container').innerHTML = html; } /* ── mock data ── */ var MOCK_DATA = { summary: { total_recurring: 9, total_monthly_spend: 823.48, total_potential_monthly_savings: 118.50, total_potential_annual_savings: 1422.00 }, charges: [ { merchant: 'Telstra', category: 'mobile', frequency: 'Monthly', current_price: 65, alternatives: [ { provider: 'Boost Mobile', price: 40, note: 'Runs on Telstra network', specs: '70GB data, unlimited calls & texts, Telstra 4G/5G' }, { provider: 'Felix Mobile', price: 35, note: 'Unlimited data with speed cap', specs: 'Unlimited data (50GB full speed), Vodafone 4G' } ]}, { merchant: 'Optus', category: 'internet', frequency: 'Monthly', current_price: 89, alternatives: [ { provider: 'Superloop', price: 69, note: 'Top NBN value', specs: 'NBN 50, 50/20 Mbps, unlimited data, no lock-in' }, { provider: 'Spintel', price: 59, note: 'Budget-friendly', specs: 'NBN 50, unlimited data, 6-month intro discount' } ]}, { merchant: 'Origin Energy', category: 'energy', frequency: 'Monthly', current_price: 210, alternatives: [ { provider: 'Alinta Energy', price: 190, note: 'Pay-on-time discount', specs: '16% discount, no lock-in' }, { provider: 'Red Energy', price: 195, note: '100% GreenPower', specs: '100% carbon neutral, 12c/kWh solar feed-in' } ]}, { merchant: 'AHM', category: 'insurance_health', frequency: 'Monthly', current_price: 180, alternatives: [ { provider: 'HCF', price: 152, note: 'Top-rated claims satisfaction', specs: 'Hospital + extras, $500 excess, no-gap dental' }, { provider: 'Teachers Health', price: 160, note: 'Not-for-profit, open to everyone', specs: 'Hospital + extras, $500 excess' } ]}, { merchant: 'Netflix', category: 'streaming', frequency: 'Monthly', current_price: 22.99, alternatives: [ { provider: 'Stan', price: 16, note: 'Includes Paramount+', specs: 'Stan originals + Paramount+, 3 screens' }, { provider: 'Binge', price: 18, note: 'HBO + Warner Bros', specs: 'Full HBO catalogue, 2 screens' } ]}, { merchant: 'Allianz', category: 'insurance_car', frequency: 'Monthly', current_price: 125, alternatives: [ { provider: 'Budget Direct', price: 98, note: 'Online-only, low cost', specs: 'Comprehensive, $750 excess' }, { provider: 'AAMI', price: 108, note: 'Trusted brand', specs: 'Comprehensive, $650 excess, lifetime repair guarantee' } ]}, { merchant: 'ANZ', category: 'credit_card', frequency: 'Monthly', current_price: 12.50, alternatives: [ { provider: 'Bankwest Zero', price: 0, note: 'Zero fee card', specs: '$0 annual fee, 0% FX fee' }, { provider: 'ING Orange One', price: 0, note: 'Low rate card', specs: '$0 annual fee (conditional), 12.99% rate' } ]}, { merchant: 'ANZ', category: 'banking', frequency: 'Monthly', current_price: 5, alternatives: [ { provider: 'ING', price: 0, note: 'No fees anywhere', specs: '$0 monthly fee, 5.50% savings rate' }, { provider: 'Up Bank', price: 0, note: 'Modern banking app', specs: '$0 monthly fee, 5.00% savings rate' } ]}, { merchant: 'Spotify', category: 'streaming', frequency: 'Monthly', current_price: 13.99, alternatives: [ { provider: 'Apple Music', price: 12.99, note: 'Lossless audio included', specs: '100M+ songs, lossless & spatial audio' }, { provider: 'YouTube Music', price: null, note: 'Free tier available', specs: 'Free with ads, background play on Premium' } ]} ], statements: ['example-statement.pdf'] }; /* ── multi-file queue state ── */ var pendingFiles = []; var _analyzedFileCount = 1; function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(1) + ' MB'; } function renderFileChips() { var container = document.getElementById('file-chips'); if (!container) return; if (pendingFiles.length === 0) { container.innerHTML = ''; return; } var html = ''; for (var i = 0; i < pendingFiles.length; i++) { html += '
' + esc(pendingFiles[i].name) + '' + formatFileSize(pendingFiles[i].size) + '
'; } container.innerHTML = html; } function updateAnalyzeBtn() { var btn = document.getElementById('btn-analyze'); if (!btn) return; if (pendingFiles.length === 0) { btn.style.display = 'none'; return; } btn.style.display = 'block'; btn.textContent = pendingFiles.length === 1 ? 'Analyze 1 statement' : 'Analyze ' + pendingFiles.length + ' statements'; } document.addEventListener('click', function(e) { var removeBtn = e.target.closest('.file-chip-remove'); if (removeBtn) { var idx = parseInt(removeBtn.getAttribute('data-idx')); pendingFiles.splice(idx, 1); renderFileChips(); updateAnalyzeBtn(); } }); document.getElementById('btn-analyze').addEventListener('click', function() { if (pendingFiles.length === 0) return; var filesToAnalyze = pendingFiles.slice(); pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); analyzeFiles(filesToAnalyze); }); /* ── file upload handler ── */ function handleFiles(fileList) { var added = 0; for (var i = 0; i < fileList.length; i++) { var f = fileList[i]; if (f.type !== 'application/pdf') continue; if (f.size > 10 * 1024 * 1024) { alert(f.name + ' exceeds 10 MB limit.'); continue; } pendingFiles.push(f); added++; } if (added === 0 && fileList.length > 0) alert('Please upload PDF files only.'); renderFileChips(); updateAnalyzeBtn(); } /* ── API call ── */ function analyzeFiles(files) { _analyzedFileCount = files.length; showState('processing'); var procTitle = document.getElementById('proc-title'); if (procTitle) procTitle.textContent = files.length > 1 ? 'Analyzing ' + files.length + ' statements…' : 'Analyzing your statement…'; var pendingData = null; var stepsFinished = false; runProcessingSteps(function() { stepsFinished = true; if (pendingData) showResults(pendingData, false); }); var formData = new FormData(); for (var i = 0; i < files.length; i++) { formData.append('file', files[i]); } fetch(API + '/api/analyze', { method: 'POST', body: formData }) .then(function(res) { if (!res.ok) throw new Error('API returned ' + res.status); return res.json(); }) .then(function(data) { if (!data || !data.summary || !data.charges) throw new Error('Invalid response'); lastAnalysis = data; if (stepsFinished) showResults(data, false); else pendingData = data; }) .catch(function(err) { showState('landing'); var errMsg = err && err.message ? err.message : 'Something went wrong'; alert('Analysis failed: ' + errMsg + '. Please try again.'); }); } function normalizeData(data) { if (!data || !data.charges) return data; data.charges = data.charges.map(function(c) { return { merchant: c.merchant, category: c.category, frequency: c.frequency || 'Monthly', current_price: c.current_price != null ? c.current_price : c.current_amount, current_amount: c.current_amount != null ? c.current_amount : c.current_price, best_savings: c.best_savings || 0, alternatives: (c.alternatives || []).map(function(a) { return { provider: a.provider || a.name, name: a.name || a.provider, price: a.price, note: a.note, specs: a.specs, url: a.url, monthly_savings: a.monthly_savings, annual_savings: a.annual_savings }; }) }; }); return data; } function showResults(data, isSaved) { data = normalizeData(data); lastAnalysis = data; renderResults(data, isSaved); showState('results'); window.scrollTo({ top: 0, behavior: 'smooth' }); /* auto-save if logged in & fresh analysis */ if (!isSaved && isLoggedIn()) { saveAnalysis(data); } /* bind buttons */ var uploadAnother = document.getElementById('btn-upload-another'); if (uploadAnother) { uploadAnother.addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); } var addMore = document.getElementById('btn-add-more'); if (addMore) { addMore.addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); } var shareBtn = document.getElementById('btn-share'); if (shareBtn) { shareBtn.addEventListener('click', function() { var text = 'I just found $' + data.summary.total_potential_annual_savings.toFixed(2) + '/yr in savings on my Australian bills using Switcheroo! πŸ‡¦πŸ‡Ί'; if (navigator.share) { navigator.share({ title: 'Switcheroo Savings', text: text }).catch(function() {}); } else { navigator.clipboard.writeText(text).then(function() { shareBtn.textContent = 'Copied!'; setTimeout(function() { shareBtn.textContent = 'Share results'; }, 2000); }); } }); } /* "Save results" button for non-logged-in users */ var savePrompt = document.getElementById('btn-save-prompt'); if (savePrompt) { savePrompt.addEventListener('click', function() { openAuthModal(function() { saveAnalysis(data); showResults(data, false); }); }); } /* save nudge */ var nudgeSignin = document.getElementById('nudge-signin-btn'); if (nudgeSignin) { nudgeSignin.addEventListener('click', function() { openAuthModal(function() { saveAnalysis(data); showResults(data, false); }); }); } var nudgeSkip = document.getElementById('nudge-skip'); if (nudgeSkip) { nudgeSkip.addEventListener('click', function() { var nudge = document.getElementById('save-nudge'); if (nudge) nudge.style.display = 'none'; }); } } /* ── error state ── */ function showError(msg) { document.getElementById('error-msg').textContent = msg || 'We couldn\'t process your statement. Please try again.'; showState('error'); } document.getElementById('retry-btn').addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); /* ── modal system (switch plans) ── */ function openModal(merchant, provider, category, saving, priceDisplay, specs, currentPrice, newPrice) { var html = ''; html += ''; html += '

Switch from ' + esc(merchant) + ' to ' + esc(provider) + '

'; if (saving) { html += ''; } html += '

Generating your personalized switching plan…

'; html += ''; document.getElementById('modal-content').innerHTML = html; document.getElementById('modal-content').className = 'modal'; var overlay = document.getElementById('modal-overlay'); overlay.style.display = 'flex'; void overlay.offsetWidth; overlay.classList.add('open'); document.getElementById('modal-close-btn').addEventListener('click', closeModal); fetch(API + '/api/switch-plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_provider: merchant, new_provider: provider, category: category, current_price: currentPrice || null, new_price: newPrice || null, specs: specs || null }) }) .then(function(res) { return res.json(); }) .then(function(data) { if (!data.success || !data.plan) throw new Error('No plan'); renderPlan(data.plan, merchant, provider); }) .catch(function() { renderFallbackPlan(merchant, provider, category, saving); }); } function renderPlan(plan, merchant, provider) { var loadEl = document.getElementById('plan-loading'); var contentEl = document.getElementById('plan-content'); if (!loadEl || !contentEl) return; var html = ''; if (plan.before && plan.before.length) { html += '
⚑ Before You Switch
    '; for (var i = 0; i < plan.before.length; i++) html += '
  • ' + esc(plan.before[i]) + '
  • '; html += '
'; } if (plan.steps && plan.steps.length) { html += '
πŸ“‹ Step-by-Step Plan
'; } if (plan.cancellation) { var c = plan.cancellation; html += '
βœ‚οΈ Cancellation
'; html += '
'; if (c.method) html += '
via ' + esc(c.method) + '
'; if (c.details) html += '
' + esc(c.details) + '
'; if (c.contact) html += '
' + esc(c.contact) + '
'; if (c.email_template) { html += ''; var subject = 'Cancellation Request - ' + merchant; var mailto = 'mailto:' + (c.contact ? encodeURIComponent(c.contact) : '') + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(c.email_template); html += ''; } html += '
'; } if (plan.tips && plan.tips.length) { html += '
πŸ’‘ Money-Saving Tips
    '; for (var i = 0; i < plan.tips.length; i++) html += '
  • ' + esc(plan.tips[i]) + '
  • '; html += '
'; } if (plan.timeline) { html += '
⏱ ' + esc(plan.timeline) + '
'; } loadEl.style.display = 'none'; contentEl.innerHTML = html; contentEl.style.display = 'block'; var copyBtn = document.getElementById('btn-copy-email'); if (copyBtn && plan.cancellation && plan.cancellation.email_template) { copyBtn.addEventListener('click', function() { navigator.clipboard.writeText(plan.cancellation.email_template).then(function() { copyBtn.textContent = 'Copied!'; setTimeout(function() { copyBtn.textContent = 'Copy template'; }, 2000); }); }); } } function renderFallbackPlan(merchant, provider, category, saving) { var loadEl = document.getElementById('plan-loading'); var contentEl = document.getElementById('plan-content'); if (!loadEl || !contentEl) return; var SWITCH_STEPS = { streaming: ['Log in to your current provider and cancel from Account Settings.', 'Sign up for ALT_NAME at their website β€” most offer a free trial.', 'Done! Your new service is ready immediately.'], internet: ['Send a cancellation email to your current provider.', 'Sign up with ALT_NAME and choose your plan.', 'New connection active within 5–10 business days.'], mobile: ['Sign up with ALT_NAME and request to port your existing number.', 'ALT_NAME sends a porting request β€” your old plan auto-cancels.', 'Porting takes 1–3 hours.'], energy: ['Sign up with ALT_NAME online with your NMI (on your bill).', 'ALT_NAME handles the switchover β€” no disconnection.', 'Switch takes 1–3 business days.'], insurance_health: ['Sign up with ALT_NAME first for continuous cover.', 'Cancel your old insurer. Waiting periods transfer within 30 days.'], insurance_car: ['Get a quote from ALT_NAME.', 'Cancel old policy via email. Activate new before old expires.'], insurance_home: ['Get a quote from ALT_NAME.', 'Cancel old policy timed so new one starts seamlessly.'], banking: ['Open new account with ALT_NAME (minutes online).', 'Transfer direct debits, salary, payments to new account.', 'Close old account once everything is moved.'], credit_card: ['Apply for ALT_NAME card online.', 'Transfer recurring payments to new card.', 'Pay off and close old card.'], home_loan: ['Apply for pre-approval with ALT_NAME.', 'New lender handles refinance + discharge.', 'Settlement takes 4–6 weeks.'], car_loan: ['Apply with ALT_NAME with current loan + vehicle info.', 'New lender pays out old loan.', 'Check for early exit fees first.'], other: ['Review current contract terms.', 'Cancel following provider process.', 'Sign up with ALT_NAME.'] }; var steps = SWITCH_STEPS[category] || SWITCH_STEPS.other; var html = '
πŸ“‹ How to Switch
'; loadEl.style.display = 'none'; contentEl.innerHTML = html; contentEl.style.display = 'block'; } function closeModal() { var overlay = document.getElementById('modal-overlay'); overlay.classList.remove('open'); setTimeout(function() { overlay.style.display = 'none'; }, 300); } document.getElementById('modal-overlay').addEventListener('click', function(e) { if (e.target === this) closeModal(); }); /* ── event delegation for switch buttons ── */ document.addEventListener('click', function(e) { var btn = e.target.closest('.switch-btn'); if (!btn) return; openModal( btn.getAttribute('data-merchant'), btn.getAttribute('data-provider'), btn.getAttribute('data-category'), btn.getAttribute('data-saving'), btn.getAttribute('data-price'), btn.getAttribute('data-specs'), btn.getAttribute('data-current-price'), btn.getAttribute('data-new-price') ); }); /* ── file upload ── */ var uploadZone = document.getElementById('upload-zone'); var fileInput = document.getElementById('file-input'); uploadZone.addEventListener('click', function(e) { e.stopPropagation(); fileInput.click(); }); uploadZone.addEventListener('dragover', function(e) { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', function(e) { e.preventDefault(); uploadZone.classList.remove('dragover'); }); uploadZone.addEventListener('drop', function(e) { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', function() { if (fileInput.files.length > 0) { handleFiles(fileInput.files); fileInput.value = ''; } }); /* ── nav home click ── */ document.getElementById('nav-home').addEventListener('click', function() { if (savedData && savedData.charges && savedData.charges.length > 0) { showResults(savedData, true); } else { showState('landing'); } window.scrollTo({ top: 0, behavior: 'smooth' }); }); /* ── keyboard ── */ document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); }); /* ── init ── */ renderNavAuth(); if (isLoggedIn()) loadSavedData(); + tAmt.toFixed(2) + '/mo
'; html += '
' + tPct + '% of total
'; html += '
'; html += '
'; } html += '
'; } } /* Recurring charges breakdown (switchable subscriptions) */ var cats = {}; var totalSpend = 0; for (var i = 0; i < charges.length; i++) { var c = charges[i]; var amt = c.current_price || c.current_amount || 0; totalSpend += amt; if (!cats[c.category]) cats[c.category] = { amount: 0, count: 0 }; cats[c.category].amount += amt; cats[c.category].count++; } var sorted = Object.keys(cats).sort(function(a, b) { return cats[b].amount - cats[a].amount; }); var maxAmt = sorted.length > 0 ? cats[sorted[0]].amount : 1; html += '
'; html += '
Recurring charges by category
'; html += '
'; for (var j = 0; j < sorted.length; j++) { var catKey = sorted[j]; var cat = getCat(catKey); var data = cats[catKey]; var pct = Math.round((data.amount / totalSpend) * 100); var barW = Math.max(8, Math.round((data.amount / maxAmt) * 100)); html += '
'; html += '
' + cat.icon + '
'; html += '
'; html += '
' + esc(cat.label) + '
'; html += '
/* ── render results ── */ function renderResults(data, isSaved) { var s = data.summary; var charges = data.charges; var html = ''; /* saved data banner */ if (isSaved && isLoggedIn()) { var updStr = savedData && savedData.updatedAt ? new Date(savedData.updatedAt).toLocaleDateString() : ''; var stmtNames = (data.statements || []).join(', ') || 'your statements'; html += '
πŸ“Š Your saved profile Β· ' + esc(s.total_recurring) + ' charges from ' + esc(stmtNames); if (updStr) html += ' Β· last updated ' + esc(updStr); html += '
+ Add statement
'; } /* header */ var _stmtCount = s.statements_processed || (data.statements || []).length || 1; var _rTitle = _stmtCount > 1 ? 'Your Combined Savings Report' : 'Your Savings Report'; var _rSub = _stmtCount > 1 ? esc(s.total_recurring) + ' charges analyzed across ' + _stmtCount + ' statements' : esc(s.total_recurring) + ' recurring charges analyzed'; html += '

' + _rTitle + '

' + _rSub + '

'; /* summary cards */ var _tsTotalDebits = data.total_spending && data.total_spending.monthly_total_debits ? data.total_spending.monthly_total_debits : null; html += '
'; if (_tsTotalDebits) { html += '
Total Monthly Spend
$' + esc(_tsTotalDebits.toFixed(2)) + '/mo
'; } html += '
Recurring Bills
$' + esc(s.total_monthly_spend.toFixed(2)) + '/mo
'; html += '
You Could Save
$' + esc(s.total_potential_monthly_savings.toFixed(2)) + '/mo
'; html += '
Annual Savings
$' + esc(s.total_potential_annual_savings.toFixed(2)) + '/yr
'; html += '
'; /* spending stats */ if (charges.length > 1 || data.total_spending) { html += buildSpendingStats(charges, data.total_spending); } /* save nudge for non-logged-in users */ if (!isSaved && !isLoggedIn()) { html += '
'; html += '
πŸ”’
'; html += '

Save your savings profile

'; html += '

Sign in with your email to save these results, add more bank statements later, and build a complete picture of your spending.

'; html += ''; html += 'Maybe later'; html += '
'; } /* charge cards */ for (var i = 0; i < charges.length; i++) { var c = charges[i]; var cat = getCat(c.category); var price = c.current_price != null ? c.current_price : c.current_amount; html += '
'; html += '
'; var logoUrl = getMerchantLogo(c.merchant); html += ''; html += '
' + esc(c.merchant) + '
' + esc(c.frequency || 'Monthly') + ' Β· ' + esc(CATEGORY_LABELS[c.category] || c.category.replace(/_/g, ' ')) + '
'; html += '
' + esc(fmt(price)) + '/mo
'; html += '
'; if (c.alternatives && c.alternatives.length > 0) { html += '
'; for (var j = 0; j < c.alternatives.length; j++) { var a = c.alternatives[j]; var aProvider = a.provider || a.name; var aPrice = a.price; var saving = (aPrice != null && price != null) ? (price - aPrice) : null; if (saving !== null && saving < 0) saving = null; var savingText = saving != null ? ('Save $' + saving.toFixed(2) + '/mo') : 'Compare'; var badgeClass = saving != null ? 'green' : 'blue'; var priceDisplay = aPrice === 0 ? 'Free' : (aPrice == null ? 'Get quote' : '$' + aPrice.toFixed(2) + '/mo'); html += '
'; html += '
β†’
'; html += '
'; html += '
' + esc(aProvider) + ' Β· ' + esc(priceDisplay) + '
'; if (a.note) html += '
' + esc(a.note) + '
'; if (a.specs) html += '
' + esc(a.specs) + '
'; html += '
'; html += '
'; html += '' + esc(savingText) + ''; var switchSpecs = a.specs ? esc(a.specs) : ''; var switchCurPrice = price != null ? esc(price.toFixed(2)) : ''; var switchNewPrice = aPrice != null ? esc(aPrice.toFixed(2)) : ''; html += ''; html += '
'; } html += '
'; } html += '
'; } /* total bar */ html += '
Total potential savings
$' + esc(s.total_potential_annual_savings.toFixed(2)) + '/yr
'; /* actions */ html += '
'; html += ''; html += ''; html += '
'; html += ''; document.getElementById('results-container').innerHTML = html; } /* ── mock data ── */ var MOCK_DATA = { summary: { total_recurring: 9, total_monthly_spend: 823.48, total_potential_monthly_savings: 118.50, total_potential_annual_savings: 1422.00 }, charges: [ { merchant: 'Telstra', category: 'mobile', frequency: 'Monthly', current_price: 65, alternatives: [ { provider: 'Boost Mobile', price: 40, note: 'Runs on Telstra network', specs: '70GB data, unlimited calls & texts, Telstra 4G/5G' }, { provider: 'Felix Mobile', price: 35, note: 'Unlimited data with speed cap', specs: 'Unlimited data (50GB full speed), Vodafone 4G' } ]}, { merchant: 'Optus', category: 'internet', frequency: 'Monthly', current_price: 89, alternatives: [ { provider: 'Superloop', price: 69, note: 'Top NBN value', specs: 'NBN 50, 50/20 Mbps, unlimited data, no lock-in' }, { provider: 'Spintel', price: 59, note: 'Budget-friendly', specs: 'NBN 50, unlimited data, 6-month intro discount' } ]}, { merchant: 'Origin Energy', category: 'energy', frequency: 'Monthly', current_price: 210, alternatives: [ { provider: 'Alinta Energy', price: 190, note: 'Pay-on-time discount', specs: '16% discount, no lock-in' }, { provider: 'Red Energy', price: 195, note: '100% GreenPower', specs: '100% carbon neutral, 12c/kWh solar feed-in' } ]}, { merchant: 'AHM', category: 'insurance_health', frequency: 'Monthly', current_price: 180, alternatives: [ { provider: 'HCF', price: 152, note: 'Top-rated claims satisfaction', specs: 'Hospital + extras, $500 excess, no-gap dental' }, { provider: 'Teachers Health', price: 160, note: 'Not-for-profit, open to everyone', specs: 'Hospital + extras, $500 excess' } ]}, { merchant: 'Netflix', category: 'streaming', frequency: 'Monthly', current_price: 22.99, alternatives: [ { provider: 'Stan', price: 16, note: 'Includes Paramount+', specs: 'Stan originals + Paramount+, 3 screens' }, { provider: 'Binge', price: 18, note: 'HBO + Warner Bros', specs: 'Full HBO catalogue, 2 screens' } ]}, { merchant: 'Allianz', category: 'insurance_car', frequency: 'Monthly', current_price: 125, alternatives: [ { provider: 'Budget Direct', price: 98, note: 'Online-only, low cost', specs: 'Comprehensive, $750 excess' }, { provider: 'AAMI', price: 108, note: 'Trusted brand', specs: 'Comprehensive, $650 excess, lifetime repair guarantee' } ]}, { merchant: 'ANZ', category: 'credit_card', frequency: 'Monthly', current_price: 12.50, alternatives: [ { provider: 'Bankwest Zero', price: 0, note: 'Zero fee card', specs: '$0 annual fee, 0% FX fee' }, { provider: 'ING Orange One', price: 0, note: 'Low rate card', specs: '$0 annual fee (conditional), 12.99% rate' } ]}, { merchant: 'ANZ', category: 'banking', frequency: 'Monthly', current_price: 5, alternatives: [ { provider: 'ING', price: 0, note: 'No fees anywhere', specs: '$0 monthly fee, 5.50% savings rate' }, { provider: 'Up Bank', price: 0, note: 'Modern banking app', specs: '$0 monthly fee, 5.00% savings rate' } ]}, { merchant: 'Spotify', category: 'streaming', frequency: 'Monthly', current_price: 13.99, alternatives: [ { provider: 'Apple Music', price: 12.99, note: 'Lossless audio included', specs: '100M+ songs, lossless & spatial audio' }, { provider: 'YouTube Music', price: null, note: 'Free tier available', specs: 'Free with ads, background play on Premium' } ]} ], statements: ['example-statement.pdf'] }; /* ── multi-file queue state ── */ var pendingFiles = []; var _analyzedFileCount = 1; function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(1) + ' MB'; } function renderFileChips() { var container = document.getElementById('file-chips'); if (!container) return; if (pendingFiles.length === 0) { container.innerHTML = ''; return; } var html = ''; for (var i = 0; i < pendingFiles.length; i++) { html += '
' + esc(pendingFiles[i].name) + '' + formatFileSize(pendingFiles[i].size) + '
'; } container.innerHTML = html; } function updateAnalyzeBtn() { var btn = document.getElementById('btn-analyze'); if (!btn) return; if (pendingFiles.length === 0) { btn.style.display = 'none'; return; } btn.style.display = 'block'; btn.textContent = pendingFiles.length === 1 ? 'Analyze 1 statement' : 'Analyze ' + pendingFiles.length + ' statements'; } document.addEventListener('click', function(e) { var removeBtn = e.target.closest('.file-chip-remove'); if (removeBtn) { var idx = parseInt(removeBtn.getAttribute('data-idx')); pendingFiles.splice(idx, 1); renderFileChips(); updateAnalyzeBtn(); } }); document.getElementById('btn-analyze').addEventListener('click', function() { if (pendingFiles.length === 0) return; var filesToAnalyze = pendingFiles.slice(); pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); analyzeFiles(filesToAnalyze); }); /* ── file upload handler ── */ function handleFiles(fileList) { var added = 0; for (var i = 0; i < fileList.length; i++) { var f = fileList[i]; if (f.type !== 'application/pdf') continue; if (f.size > 10 * 1024 * 1024) { alert(f.name + ' exceeds 10 MB limit.'); continue; } pendingFiles.push(f); added++; } if (added === 0 && fileList.length > 0) alert('Please upload PDF files only.'); renderFileChips(); updateAnalyzeBtn(); } /* ── API call ── */ function analyzeFiles(files) { _analyzedFileCount = files.length; showState('processing'); var procTitle = document.getElementById('proc-title'); if (procTitle) procTitle.textContent = files.length > 1 ? 'Analyzing ' + files.length + ' statements…' : 'Analyzing your statement…'; var pendingData = null; var stepsFinished = false; runProcessingSteps(function() { stepsFinished = true; if (pendingData) showResults(pendingData, false); }); var formData = new FormData(); for (var i = 0; i < files.length; i++) { formData.append('file', files[i]); } fetch(API + '/api/analyze', { method: 'POST', body: formData }) .then(function(res) { if (!res.ok) throw new Error('API returned ' + res.status); return res.json(); }) .then(function(data) { if (!data || !data.summary || !data.charges) throw new Error('Invalid response'); lastAnalysis = data; if (stepsFinished) showResults(data, false); else pendingData = data; }) .catch(function(err) { showState('landing'); var errMsg = err && err.message ? err.message : 'Something went wrong'; alert('Analysis failed: ' + errMsg + '. Please try again.'); }); } function normalizeData(data) { if (!data || !data.charges) return data; data.charges = data.charges.map(function(c) { return { merchant: c.merchant, category: c.category, frequency: c.frequency || 'Monthly', current_price: c.current_price != null ? c.current_price : c.current_amount, current_amount: c.current_amount != null ? c.current_amount : c.current_price, best_savings: c.best_savings || 0, alternatives: (c.alternatives || []).map(function(a) { return { provider: a.provider || a.name, name: a.name || a.provider, price: a.price, note: a.note, specs: a.specs, url: a.url, monthly_savings: a.monthly_savings, annual_savings: a.annual_savings }; }) }; }); return data; } function showResults(data, isSaved) { data = normalizeData(data); lastAnalysis = data; renderResults(data, isSaved); showState('results'); window.scrollTo({ top: 0, behavior: 'smooth' }); /* auto-save if logged in & fresh analysis */ if (!isSaved && isLoggedIn()) { saveAnalysis(data); } /* bind buttons */ var uploadAnother = document.getElementById('btn-upload-another'); if (uploadAnother) { uploadAnother.addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); } var addMore = document.getElementById('btn-add-more'); if (addMore) { addMore.addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); } var shareBtn = document.getElementById('btn-share'); if (shareBtn) { shareBtn.addEventListener('click', function() { var text = 'I just found $' + data.summary.total_potential_annual_savings.toFixed(2) + '/yr in savings on my Australian bills using Switcheroo! πŸ‡¦πŸ‡Ί'; if (navigator.share) { navigator.share({ title: 'Switcheroo Savings', text: text }).catch(function() {}); } else { navigator.clipboard.writeText(text).then(function() { shareBtn.textContent = 'Copied!'; setTimeout(function() { shareBtn.textContent = 'Share results'; }, 2000); }); } }); } /* "Save results" button for non-logged-in users */ var savePrompt = document.getElementById('btn-save-prompt'); if (savePrompt) { savePrompt.addEventListener('click', function() { openAuthModal(function() { saveAnalysis(data); showResults(data, false); }); }); } /* save nudge */ var nudgeSignin = document.getElementById('nudge-signin-btn'); if (nudgeSignin) { nudgeSignin.addEventListener('click', function() { openAuthModal(function() { saveAnalysis(data); showResults(data, false); }); }); } var nudgeSkip = document.getElementById('nudge-skip'); if (nudgeSkip) { nudgeSkip.addEventListener('click', function() { var nudge = document.getElementById('save-nudge'); if (nudge) nudge.style.display = 'none'; }); } } /* ── error state ── */ function showError(msg) { document.getElementById('error-msg').textContent = msg || 'We couldn\'t process your statement. Please try again.'; showState('error'); } document.getElementById('retry-btn').addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); /* ── modal system (switch plans) ── */ function openModal(merchant, provider, category, saving, priceDisplay, specs, currentPrice, newPrice) { var html = ''; html += ''; html += '

Switch from ' + esc(merchant) + ' to ' + esc(provider) + '

'; if (saving) { html += ''; } html += '

Generating your personalized switching plan…

'; html += ''; document.getElementById('modal-content').innerHTML = html; document.getElementById('modal-content').className = 'modal'; var overlay = document.getElementById('modal-overlay'); overlay.style.display = 'flex'; void overlay.offsetWidth; overlay.classList.add('open'); document.getElementById('modal-close-btn').addEventListener('click', closeModal); fetch(API + '/api/switch-plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_provider: merchant, new_provider: provider, category: category, current_price: currentPrice || null, new_price: newPrice || null, specs: specs || null }) }) .then(function(res) { return res.json(); }) .then(function(data) { if (!data.success || !data.plan) throw new Error('No plan'); renderPlan(data.plan, merchant, provider); }) .catch(function() { renderFallbackPlan(merchant, provider, category, saving); }); } function renderPlan(plan, merchant, provider) { var loadEl = document.getElementById('plan-loading'); var contentEl = document.getElementById('plan-content'); if (!loadEl || !contentEl) return; var html = ''; if (plan.before && plan.before.length) { html += '
⚑ Before You Switch
    '; for (var i = 0; i < plan.before.length; i++) html += '
  • ' + esc(plan.before[i]) + '
  • '; html += '
'; } if (plan.steps && plan.steps.length) { html += '
πŸ“‹ Step-by-Step Plan
'; } if (plan.cancellation) { var c = plan.cancellation; html += '
βœ‚οΈ Cancellation
'; html += '
'; if (c.method) html += '
via ' + esc(c.method) + '
'; if (c.details) html += '
' + esc(c.details) + '
'; if (c.contact) html += '
' + esc(c.contact) + '
'; if (c.email_template) { html += ''; var subject = 'Cancellation Request - ' + merchant; var mailto = 'mailto:' + (c.contact ? encodeURIComponent(c.contact) : '') + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(c.email_template); html += ''; } html += '
'; } if (plan.tips && plan.tips.length) { html += '
πŸ’‘ Money-Saving Tips
    '; for (var i = 0; i < plan.tips.length; i++) html += '
  • ' + esc(plan.tips[i]) + '
  • '; html += '
'; } if (plan.timeline) { html += '
⏱ ' + esc(plan.timeline) + '
'; } loadEl.style.display = 'none'; contentEl.innerHTML = html; contentEl.style.display = 'block'; var copyBtn = document.getElementById('btn-copy-email'); if (copyBtn && plan.cancellation && plan.cancellation.email_template) { copyBtn.addEventListener('click', function() { navigator.clipboard.writeText(plan.cancellation.email_template).then(function() { copyBtn.textContent = 'Copied!'; setTimeout(function() { copyBtn.textContent = 'Copy template'; }, 2000); }); }); } } function renderFallbackPlan(merchant, provider, category, saving) { var loadEl = document.getElementById('plan-loading'); var contentEl = document.getElementById('plan-content'); if (!loadEl || !contentEl) return; var SWITCH_STEPS = { streaming: ['Log in to your current provider and cancel from Account Settings.', 'Sign up for ALT_NAME at their website β€” most offer a free trial.', 'Done! Your new service is ready immediately.'], internet: ['Send a cancellation email to your current provider.', 'Sign up with ALT_NAME and choose your plan.', 'New connection active within 5–10 business days.'], mobile: ['Sign up with ALT_NAME and request to port your existing number.', 'ALT_NAME sends a porting request β€” your old plan auto-cancels.', 'Porting takes 1–3 hours.'], energy: ['Sign up with ALT_NAME online with your NMI (on your bill).', 'ALT_NAME handles the switchover β€” no disconnection.', 'Switch takes 1–3 business days.'], insurance_health: ['Sign up with ALT_NAME first for continuous cover.', 'Cancel your old insurer. Waiting periods transfer within 30 days.'], insurance_car: ['Get a quote from ALT_NAME.', 'Cancel old policy via email. Activate new before old expires.'], insurance_home: ['Get a quote from ALT_NAME.', 'Cancel old policy timed so new one starts seamlessly.'], banking: ['Open new account with ALT_NAME (minutes online).', 'Transfer direct debits, salary, payments to new account.', 'Close old account once everything is moved.'], credit_card: ['Apply for ALT_NAME card online.', 'Transfer recurring payments to new card.', 'Pay off and close old card.'], home_loan: ['Apply for pre-approval with ALT_NAME.', 'New lender handles refinance + discharge.', 'Settlement takes 4–6 weeks.'], car_loan: ['Apply with ALT_NAME with current loan + vehicle info.', 'New lender pays out old loan.', 'Check for early exit fees first.'], other: ['Review current contract terms.', 'Cancel following provider process.', 'Sign up with ALT_NAME.'] }; var steps = SWITCH_STEPS[category] || SWITCH_STEPS.other; var html = '
πŸ“‹ How to Switch
'; loadEl.style.display = 'none'; contentEl.innerHTML = html; contentEl.style.display = 'block'; } function closeModal() { var overlay = document.getElementById('modal-overlay'); overlay.classList.remove('open'); setTimeout(function() { overlay.style.display = 'none'; }, 300); } document.getElementById('modal-overlay').addEventListener('click', function(e) { if (e.target === this) closeModal(); }); /* ── event delegation for switch buttons ── */ document.addEventListener('click', function(e) { var btn = e.target.closest('.switch-btn'); if (!btn) return; openModal( btn.getAttribute('data-merchant'), btn.getAttribute('data-provider'), btn.getAttribute('data-category'), btn.getAttribute('data-saving'), btn.getAttribute('data-price'), btn.getAttribute('data-specs'), btn.getAttribute('data-current-price'), btn.getAttribute('data-new-price') ); }); /* ── file upload ── */ var uploadZone = document.getElementById('upload-zone'); var fileInput = document.getElementById('file-input'); uploadZone.addEventListener('click', function(e) { e.stopPropagation(); fileInput.click(); }); uploadZone.addEventListener('dragover', function(e) { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', function(e) { e.preventDefault(); uploadZone.classList.remove('dragover'); }); uploadZone.addEventListener('drop', function(e) { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', function() { if (fileInput.files.length > 0) { handleFiles(fileInput.files); fileInput.value = ''; } }); /* ── nav home click ── */ document.getElementById('nav-home').addEventListener('click', function() { if (savedData && savedData.charges && savedData.charges.length > 0) { showResults(savedData, true); } else { showState('landing'); } window.scrollTo({ top: 0, behavior: 'smooth' }); }); /* ── keyboard ── */ document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); }); /* ── init ── */ renderNavAuth(); if (isLoggedIn()) loadSavedData(); + data.amount.toFixed(2) + '/mo
'; html += '
' + data.count + ' charge' + (data.count > 1 ? 's' : '') + ' Β· ' + pct + '%
'; html += '
'; html += '
'; } html += '
'; return html; } /* ── render results ── */ function renderResults(data, isSaved) { var s = data.summary; var charges = data.charges; var html = ''; /* saved data banner */ if (isSaved && isLoggedIn()) { var updStr = savedData && savedData.updatedAt ? new Date(savedData.updatedAt).toLocaleDateString() : ''; var stmtNames = (data.statements || []).join(', ') || 'your statements'; html += '
πŸ“Š Your saved profile Β· ' + esc(s.total_recurring) + ' charges from ' + esc(stmtNames); if (updStr) html += ' Β· last updated ' + esc(updStr); html += '
+ Add statement
'; } /* header */ var _stmtCount = s.statements_processed || (data.statements || []).length || 1; var _rTitle = _stmtCount > 1 ? 'Your Combined Savings Report' : 'Your Savings Report'; var _rSub = _stmtCount > 1 ? esc(s.total_recurring) + ' charges analyzed across ' + _stmtCount + ' statements' : esc(s.total_recurring) + ' recurring charges analyzed'; html += '

' + _rTitle + '

' + _rSub + '

'; /* summary cards */ var _tsTotalDebits = data.total_spending && data.total_spending.monthly_total_debits ? data.total_spending.monthly_total_debits : null; html += '
'; if (_tsTotalDebits) { html += '
Total Monthly Spend
$' + esc(_tsTotalDebits.toFixed(2)) + '/mo
'; } html += '
Recurring Bills
$' + esc(s.total_monthly_spend.toFixed(2)) + '/mo
'; html += '
You Could Save
$' + esc(s.total_potential_monthly_savings.toFixed(2)) + '/mo
'; html += '
Annual Savings
$' + esc(s.total_potential_annual_savings.toFixed(2)) + '/yr
'; html += '
'; /* spending stats */ if (charges.length > 1 || data.total_spending) { html += buildSpendingStats(charges, data.total_spending); } /* save nudge for non-logged-in users */ if (!isSaved && !isLoggedIn()) { html += '
'; html += '
πŸ”’
'; html += '

Save your savings profile

'; html += '

Sign in with your email to save these results, add more bank statements later, and build a complete picture of your spending.

'; html += ''; html += 'Maybe later'; html += '
'; } /* charge cards */ for (var i = 0; i < charges.length; i++) { var c = charges[i]; var cat = getCat(c.category); var price = c.current_price != null ? c.current_price : c.current_amount; html += '
'; html += '
'; var logoUrl = getMerchantLogo(c.merchant); html += ''; html += '
' + esc(c.merchant) + '
' + esc(c.frequency || 'Monthly') + ' Β· ' + esc(CATEGORY_LABELS[c.category] || c.category.replace(/_/g, ' ')) + '
'; html += '
' + esc(fmt(price)) + '/mo
'; html += '
'; if (c.alternatives && c.alternatives.length > 0) { html += '
'; for (var j = 0; j < c.alternatives.length; j++) { var a = c.alternatives[j]; var aProvider = a.provider || a.name; var aPrice = a.price; var saving = (aPrice != null && price != null) ? (price - aPrice) : null; if (saving !== null && saving < 0) saving = null; var savingText = saving != null ? ('Save $' + saving.toFixed(2) + '/mo') : 'Compare'; var badgeClass = saving != null ? 'green' : 'blue'; var priceDisplay = aPrice === 0 ? 'Free' : (aPrice == null ? 'Get quote' : '$' + aPrice.toFixed(2) + '/mo'); html += '
'; html += '
β†’
'; html += '
'; html += '
' + esc(aProvider) + ' Β· ' + esc(priceDisplay) + '
'; if (a.note) html += '
' + esc(a.note) + '
'; if (a.specs) html += '
' + esc(a.specs) + '
'; html += '
'; html += '
'; html += '' + esc(savingText) + ''; var switchSpecs = a.specs ? esc(a.specs) : ''; var switchCurPrice = price != null ? esc(price.toFixed(2)) : ''; var switchNewPrice = aPrice != null ? esc(aPrice.toFixed(2)) : ''; html += ''; html += '
'; } html += '
'; } html += '
'; } /* total bar */ html += '
Total potential savings
$' + esc(s.total_potential_annual_savings.toFixed(2)) + '/yr
'; /* actions */ html += '
'; html += ''; html += ''; html += '
'; html += ''; document.getElementById('results-container').innerHTML = html; } /* ── mock data ── */ var MOCK_DATA = { summary: { total_recurring: 9, total_monthly_spend: 823.48, total_potential_monthly_savings: 118.50, total_potential_annual_savings: 1422.00 }, charges: [ { merchant: 'Telstra', category: 'mobile', frequency: 'Monthly', current_price: 65, alternatives: [ { provider: 'Boost Mobile', price: 40, note: 'Runs on Telstra network', specs: '70GB data, unlimited calls & texts, Telstra 4G/5G' }, { provider: 'Felix Mobile', price: 35, note: 'Unlimited data with speed cap', specs: 'Unlimited data (50GB full speed), Vodafone 4G' } ]}, { merchant: 'Optus', category: 'internet', frequency: 'Monthly', current_price: 89, alternatives: [ { provider: 'Superloop', price: 69, note: 'Top NBN value', specs: 'NBN 50, 50/20 Mbps, unlimited data, no lock-in' }, { provider: 'Spintel', price: 59, note: 'Budget-friendly', specs: 'NBN 50, unlimited data, 6-month intro discount' } ]}, { merchant: 'Origin Energy', category: 'energy', frequency: 'Monthly', current_price: 210, alternatives: [ { provider: 'Alinta Energy', price: 190, note: 'Pay-on-time discount', specs: '16% discount, no lock-in' }, { provider: 'Red Energy', price: 195, note: '100% GreenPower', specs: '100% carbon neutral, 12c/kWh solar feed-in' } ]}, { merchant: 'AHM', category: 'insurance_health', frequency: 'Monthly', current_price: 180, alternatives: [ { provider: 'HCF', price: 152, note: 'Top-rated claims satisfaction', specs: 'Hospital + extras, $500 excess, no-gap dental' }, { provider: 'Teachers Health', price: 160, note: 'Not-for-profit, open to everyone', specs: 'Hospital + extras, $500 excess' } ]}, { merchant: 'Netflix', category: 'streaming', frequency: 'Monthly', current_price: 22.99, alternatives: [ { provider: 'Stan', price: 16, note: 'Includes Paramount+', specs: 'Stan originals + Paramount+, 3 screens' }, { provider: 'Binge', price: 18, note: 'HBO + Warner Bros', specs: 'Full HBO catalogue, 2 screens' } ]}, { merchant: 'Allianz', category: 'insurance_car', frequency: 'Monthly', current_price: 125, alternatives: [ { provider: 'Budget Direct', price: 98, note: 'Online-only, low cost', specs: 'Comprehensive, $750 excess' }, { provider: 'AAMI', price: 108, note: 'Trusted brand', specs: 'Comprehensive, $650 excess, lifetime repair guarantee' } ]}, { merchant: 'ANZ', category: 'credit_card', frequency: 'Monthly', current_price: 12.50, alternatives: [ { provider: 'Bankwest Zero', price: 0, note: 'Zero fee card', specs: '$0 annual fee, 0% FX fee' }, { provider: 'ING Orange One', price: 0, note: 'Low rate card', specs: '$0 annual fee (conditional), 12.99% rate' } ]}, { merchant: 'ANZ', category: 'banking', frequency: 'Monthly', current_price: 5, alternatives: [ { provider: 'ING', price: 0, note: 'No fees anywhere', specs: '$0 monthly fee, 5.50% savings rate' }, { provider: 'Up Bank', price: 0, note: 'Modern banking app', specs: '$0 monthly fee, 5.00% savings rate' } ]}, { merchant: 'Spotify', category: 'streaming', frequency: 'Monthly', current_price: 13.99, alternatives: [ { provider: 'Apple Music', price: 12.99, note: 'Lossless audio included', specs: '100M+ songs, lossless & spatial audio' }, { provider: 'YouTube Music', price: null, note: 'Free tier available', specs: 'Free with ads, background play on Premium' } ]} ], statements: ['example-statement.pdf'] }; /* ── multi-file queue state ── */ var pendingFiles = []; var _analyzedFileCount = 1; function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(1) + ' MB'; } function renderFileChips() { var container = document.getElementById('file-chips'); if (!container) return; if (pendingFiles.length === 0) { container.innerHTML = ''; return; } var html = ''; for (var i = 0; i < pendingFiles.length; i++) { html += '
' + esc(pendingFiles[i].name) + '' + formatFileSize(pendingFiles[i].size) + '
'; } container.innerHTML = html; } function updateAnalyzeBtn() { var btn = document.getElementById('btn-analyze'); if (!btn) return; if (pendingFiles.length === 0) { btn.style.display = 'none'; return; } btn.style.display = 'block'; btn.textContent = pendingFiles.length === 1 ? 'Analyze 1 statement' : 'Analyze ' + pendingFiles.length + ' statements'; } document.addEventListener('click', function(e) { var removeBtn = e.target.closest('.file-chip-remove'); if (removeBtn) { var idx = parseInt(removeBtn.getAttribute('data-idx')); pendingFiles.splice(idx, 1); renderFileChips(); updateAnalyzeBtn(); } }); document.getElementById('btn-analyze').addEventListener('click', function() { if (pendingFiles.length === 0) return; var filesToAnalyze = pendingFiles.slice(); pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); analyzeFiles(filesToAnalyze); }); /* ── file upload handler ── */ function handleFiles(fileList) { var added = 0; for (var i = 0; i < fileList.length; i++) { var f = fileList[i]; if (f.type !== 'application/pdf') continue; if (f.size > 10 * 1024 * 1024) { alert(f.name + ' exceeds 10 MB limit.'); continue; } pendingFiles.push(f); added++; } if (added === 0 && fileList.length > 0) alert('Please upload PDF files only.'); renderFileChips(); updateAnalyzeBtn(); } /* ── API call ── */ function analyzeFiles(files) { _analyzedFileCount = files.length; showState('processing'); var procTitle = document.getElementById('proc-title'); if (procTitle) procTitle.textContent = files.length > 1 ? 'Analyzing ' + files.length + ' statements…' : 'Analyzing your statement…'; var pendingData = null; var stepsFinished = false; runProcessingSteps(function() { stepsFinished = true; if (pendingData) showResults(pendingData, false); }); var formData = new FormData(); for (var i = 0; i < files.length; i++) { formData.append('file', files[i]); } fetch(API + '/api/analyze', { method: 'POST', body: formData }) .then(function(res) { if (!res.ok) throw new Error('API returned ' + res.status); return res.json(); }) .then(function(data) { if (!data || !data.summary || !data.charges) throw new Error('Invalid response'); lastAnalysis = data; if (stepsFinished) showResults(data, false); else pendingData = data; }) .catch(function(err) { showState('landing'); var errMsg = err && err.message ? err.message : 'Something went wrong'; alert('Analysis failed: ' + errMsg + '. Please try again.'); }); } function normalizeData(data) { if (!data || !data.charges) return data; data.charges = data.charges.map(function(c) { return { merchant: c.merchant, category: c.category, frequency: c.frequency || 'Monthly', current_price: c.current_price != null ? c.current_price : c.current_amount, current_amount: c.current_amount != null ? c.current_amount : c.current_price, best_savings: c.best_savings || 0, alternatives: (c.alternatives || []).map(function(a) { return { provider: a.provider || a.name, name: a.name || a.provider, price: a.price, note: a.note, specs: a.specs, url: a.url, monthly_savings: a.monthly_savings, annual_savings: a.annual_savings }; }) }; }); return data; } function showResults(data, isSaved) { data = normalizeData(data); lastAnalysis = data; renderResults(data, isSaved); showState('results'); window.scrollTo({ top: 0, behavior: 'smooth' }); /* auto-save if logged in & fresh analysis */ if (!isSaved && isLoggedIn()) { saveAnalysis(data); } /* bind buttons */ var uploadAnother = document.getElementById('btn-upload-another'); if (uploadAnother) { uploadAnother.addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); } var addMore = document.getElementById('btn-add-more'); if (addMore) { addMore.addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); } var shareBtn = document.getElementById('btn-share'); if (shareBtn) { shareBtn.addEventListener('click', function() { var text = 'I just found $' + data.summary.total_potential_annual_savings.toFixed(2) + '/yr in savings on my Australian bills using Switcheroo! πŸ‡¦πŸ‡Ί'; if (navigator.share) { navigator.share({ title: 'Switcheroo Savings', text: text }).catch(function() {}); } else { navigator.clipboard.writeText(text).then(function() { shareBtn.textContent = 'Copied!'; setTimeout(function() { shareBtn.textContent = 'Share results'; }, 2000); }); } }); } /* "Save results" button for non-logged-in users */ var savePrompt = document.getElementById('btn-save-prompt'); if (savePrompt) { savePrompt.addEventListener('click', function() { openAuthModal(function() { saveAnalysis(data); showResults(data, false); }); }); } /* save nudge */ var nudgeSignin = document.getElementById('nudge-signin-btn'); if (nudgeSignin) { nudgeSignin.addEventListener('click', function() { openAuthModal(function() { saveAnalysis(data); showResults(data, false); }); }); } var nudgeSkip = document.getElementById('nudge-skip'); if (nudgeSkip) { nudgeSkip.addEventListener('click', function() { var nudge = document.getElementById('save-nudge'); if (nudge) nudge.style.display = 'none'; }); } } /* ── error state ── */ function showError(msg) { document.getElementById('error-msg').textContent = msg || 'We couldn\'t process your statement. Please try again.'; showState('error'); } document.getElementById('retry-btn').addEventListener('click', function() { pendingFiles = []; renderFileChips(); updateAnalyzeBtn(); showState('landing'); window.scrollTo({ top: 0, behavior: 'smooth' }); }); /* ── modal system (switch plans) ── */ function openModal(merchant, provider, category, saving, priceDisplay, specs, currentPrice, newPrice) { var html = ''; html += ''; html += '

Switch from ' + esc(merchant) + ' to ' + esc(provider) + '

'; if (saving) { html += ''; } html += '

Generating your personalized switching plan…

'; html += ''; document.getElementById('modal-content').innerHTML = html; document.getElementById('modal-content').className = 'modal'; var overlay = document.getElementById('modal-overlay'); overlay.style.display = 'flex'; void overlay.offsetWidth; overlay.classList.add('open'); document.getElementById('modal-close-btn').addEventListener('click', closeModal); fetch(API + '/api/switch-plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_provider: merchant, new_provider: provider, category: category, current_price: currentPrice || null, new_price: newPrice || null, specs: specs || null }) }) .then(function(res) { return res.json(); }) .then(function(data) { if (!data.success || !data.plan) throw new Error('No plan'); renderPlan(data.plan, merchant, provider); }) .catch(function() { renderFallbackPlan(merchant, provider, category, saving); }); } function renderPlan(plan, merchant, provider) { var loadEl = document.getElementById('plan-loading'); var contentEl = document.getElementById('plan-content'); if (!loadEl || !contentEl) return; var html = ''; if (plan.before && plan.before.length) { html += '
⚑ Before You Switch
'; } if (plan.steps && plan.steps.length) { html += '
πŸ“‹ Step-by-Step Plan
'; } if (plan.cancellation) { var c = plan.cancellation; html += '
βœ‚οΈ Cancellation
'; html += '
'; if (c.method) html += '
via ' + esc(c.method) + '
'; if (c.details) html += '
' + esc(c.details) + '
'; if (c.contact) html += '
' + esc(c.contact) + '
'; if (c.email_template) { html += ''; var subject = 'Cancellation Request - ' + merchant; var mailto = 'mailto:' + (c.contact ? encodeURIComponent(c.contact) : '') + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(c.email_template); html += ''; } html += '
'; } if (plan.tips && plan.tips.length) { html += '
πŸ’‘ Money-Saving Tips
'; } if (plan.timeline) { html += '
⏱ ' + esc(plan.timeline) + '
'; } loadEl.style.display = 'none'; contentEl.innerHTML = html; contentEl.style.display = 'block'; var copyBtn = document.getElementById('btn-copy-email'); if (copyBtn && plan.cancellation && plan.cancellation.email_template) { copyBtn.addEventListener('click', function() { navigator.clipboard.writeText(plan.cancellation.email_template).then(function() { copyBtn.textContent = 'Copied!'; setTimeout(function() { copyBtn.textContent = 'Copy template'; }, 2000); }); }); } } function renderFallbackPlan(merchant, provider, category, saving) { var loadEl = document.getElementById('plan-loading'); var contentEl = document.getElementById('plan-content'); if (!loadEl || !contentEl) return; var SWITCH_STEPS = { streaming: ['Log in to your current provider and cancel from Account Settings.', 'Sign up for ALT_NAME at their website β€” most offer a free trial.', 'Done! Your new service is ready immediately.'], internet: ['Send a cancellation email to your current provider.', 'Sign up with ALT_NAME and choose your plan.', 'New connection active within 5–10 business days.'], mobile: ['Sign up with ALT_NAME and request to port your existing number.', 'ALT_NAME sends a porting request β€” your old plan auto-cancels.', 'Porting takes 1–3 hours.'], energy: ['Sign up with ALT_NAME online with your NMI (on your bill).', 'ALT_NAME handles the switchover β€” no disconnection.', 'Switch takes 1–3 business days.'], insurance_health: ['Sign up with ALT_NAME first for continuous cover.', 'Cancel your old insurer. Waiting periods transfer within 30 days.'], insurance_car: ['Get a quote from ALT_NAME.', 'Cancel old policy via email. Activate new before old expires.'], insurance_home: ['Get a quote from ALT_NAME.', 'Cancel old policy timed so new one starts seamlessly.'], banking: ['Open new account with ALT_NAME (minutes online).', 'Transfer direct debits, salary, payments to new account.', 'Close old account once everything is moved.'], credit_card: ['Apply for ALT_NAME card online.', 'Transfer recurring payments to new card.', 'Pay off and close old card.'], home_loan: ['Apply for pre-approval with ALT_NAME.', 'New lender handles refinance + discharge.', 'Settlement takes 4–6 weeks.'], car_loan: ['Apply with ALT_NAME with current loan + vehicle info.', 'New lender pays out old loan.', 'Check for early exit fees first.'], other: ['Review current contract terms.', 'Cancel following provider process.', 'Sign up with ALT_NAME.'] }; var steps = SWITCH_STEPS[category] || SWITCH_STEPS.other; var html = '
πŸ“‹ How to Switch
'; loadEl.style.display = 'none'; contentEl.innerHTML = html; contentEl.style.display = 'block'; } function closeModal() { var overlay = document.getElementById('modal-overlay'); overlay.classList.remove('open'); setTimeout(function() { overlay.style.display = 'none'; }, 300); } document.getElementById('modal-overlay').addEventListener('click', function(e) { if (e.target === this) closeModal(); }); /* ── event delegation for switch buttons ── */ document.addEventListener('click', function(e) { var btn = e.target.closest('.switch-btn'); if (!btn) return; openModal( btn.getAttribute('data-merchant'), btn.getAttribute('data-provider'), btn.getAttribute('data-category'), btn.getAttribute('data-saving'), btn.getAttribute('data-price'), btn.getAttribute('data-specs'), btn.getAttribute('data-current-price'), btn.getAttribute('data-new-price') ); }); /* ── file upload ── */ var uploadZone = document.getElementById('upload-zone'); var fileInput = document.getElementById('file-input'); uploadZone.addEventListener('click', function(e) { e.stopPropagation(); fileInput.click(); }); uploadZone.addEventListener('dragover', function(e) { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', function(e) { e.preventDefault(); uploadZone.classList.remove('dragover'); }); uploadZone.addEventListener('drop', function(e) { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', function() { if (fileInput.files.length > 0) { handleFiles(fileInput.files); fileInput.value = ''; } }); /* ── nav home click ── */ document.getElementById('nav-home').addEventListener('click', function() { if (savedData && savedData.charges && savedData.charges.length > 0) { showResults(savedData, true); } else { showState('landing'); } window.scrollTo({ top: 0, behavior: 'smooth' }); }); /* ── keyboard ── */ document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); }); /* ── init ── */ renderNavAuth(); if (isLoggedIn()) loadSavedData();