162 lines
4.9 KiB
TypeScript
162 lines
4.9 KiB
TypeScript
// TaxPack Background Service Worker
|
|
// Handles Gmail OAuth and message passing
|
|
|
|
import {
|
|
Receipt,
|
|
extractReceiptFromEmail,
|
|
exportToCSV,
|
|
generateTaxSummary,
|
|
} from './lib/receipt-utils';
|
|
|
|
// Store for extracted receipts
|
|
const STORAGE_KEY = 'taxpack_receipts';
|
|
|
|
// Get auth token from Chrome identity API
|
|
export async function getAuthToken(interactive: boolean = true): Promise<string | null> {
|
|
try {
|
|
const token = await chrome.identity.getAuthToken({ interactive });
|
|
return token.token ?? null;
|
|
} catch (error) {
|
|
console.error('Auth error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Fetch Gmail messages with receipt indicators
|
|
export async function fetchReceiptEmails(token: string, maxResults: number = 50): Promise<any[]> {
|
|
const queries = [
|
|
'subject:receipt',
|
|
'subject:invoice',
|
|
'subject:order confirmation',
|
|
'subject:payment confirmation',
|
|
'subject:purchase confirmation',
|
|
'from:receipts',
|
|
'from:noreply',
|
|
];
|
|
|
|
const allMessages: any[] = [];
|
|
const seenIds = new Set<string>();
|
|
|
|
for (const query of queries) {
|
|
try {
|
|
const url = `https://www.googleapis.com/gmail/v1/users/me/messages?q=${encodeURIComponent(query)}&maxResults=${Math.ceil(maxResults / queries.length)}`;
|
|
const response = await fetch(url, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
// Token expired, try to remove and re-auth
|
|
await chrome.identity.removeCachedAuthToken({ token });
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.messages) {
|
|
for (const msg of data.messages) {
|
|
if (!seenIds.has(msg.id)) {
|
|
seenIds.add(msg.id);
|
|
allMessages.push(msg);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`Query failed: ${query}`, error);
|
|
}
|
|
}
|
|
|
|
return allMessages.slice(0, maxResults);
|
|
}
|
|
|
|
// Fetch full message details
|
|
export async function fetchMessageDetails(token: string, messageId: string): Promise<any | null> {
|
|
try {
|
|
const url = `https://www.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=full`;
|
|
const response = await fetch(url, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
if (!response.ok) return null;
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Fetch message error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Save receipts to storage
|
|
export async function saveReceipts(receipts: Receipt[]): Promise<void> {
|
|
const existing = await loadReceipts();
|
|
const merged = [...existing];
|
|
const seen = new Set(existing.map(r => r.id));
|
|
|
|
for (const receipt of receipts) {
|
|
if (!seen.has(receipt.id)) {
|
|
seen.add(receipt.id);
|
|
merged.push(receipt);
|
|
}
|
|
}
|
|
|
|
await chrome.storage.local.set({ [STORAGE_KEY]: merged });
|
|
}
|
|
|
|
// Load receipts from storage
|
|
export async function loadReceipts(): Promise<Receipt[]> {
|
|
const result = await chrome.storage.local.get(STORAGE_KEY);
|
|
return result[STORAGE_KEY] || [];
|
|
}
|
|
|
|
// Clear all receipts
|
|
export async function clearReceipts(): Promise<void> {
|
|
await chrome.storage.local.remove(STORAGE_KEY);
|
|
}
|
|
|
|
// Listen for messages from popup
|
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
|
(async () => {
|
|
try {
|
|
if (request.action === 'scanGmail') {
|
|
const token = await getAuthToken(request.interactive ?? true);
|
|
if (!token) {
|
|
sendResponse({ success: false, error: 'Authentication failed' });
|
|
return;
|
|
}
|
|
|
|
const messages = await fetchReceiptEmails(token, request.maxResults || 50);
|
|
const receipts: Receipt[] = [];
|
|
|
|
for (const msg of messages.slice(0, 20)) {
|
|
const details = await fetchMessageDetails(token, msg.id);
|
|
if (details) {
|
|
const receipt = extractReceiptFromEmail(details);
|
|
if (receipt) receipts.push(receipt);
|
|
}
|
|
}
|
|
|
|
await saveReceipts(receipts);
|
|
sendResponse({ success: true, count: receipts.length, total: messages.length });
|
|
} else if (request.action === 'getReceipts') {
|
|
const receipts = await loadReceipts();
|
|
sendResponse({ success: true, receipts });
|
|
} else if (request.action === 'clearReceipts') {
|
|
await clearReceipts();
|
|
sendResponse({ success: true });
|
|
} else if (request.action === 'exportCSV') {
|
|
const receipts = await loadReceipts();
|
|
const csv = exportToCSV(receipts);
|
|
sendResponse({ success: true, csv });
|
|
} else if (request.action === 'getSummary') {
|
|
const receipts = await loadReceipts();
|
|
const summary = generateTaxSummary(receipts);
|
|
sendResponse({ success: true, summary, total: receipts.reduce((s, r) => s + r.amount, 0) });
|
|
} else {
|
|
sendResponse({ success: false, error: 'Unknown action' });
|
|
}
|
|
} catch (error) {
|
|
sendResponse({ success: false, error: String(error) });
|
|
}
|
|
})();
|
|
return true; // Keep channel open for async
|
|
});
|