Skip to content

feat: add Gmail API expense loader with Google OAuth#4

Open
ankitk26 wants to merge 1 commit into
mainfrom
feat/gmail-api-expense-loader
Open

feat: add Gmail API expense loader with Google OAuth#4
ankitk26 wants to merge 1 commit into
mainfrom
feat/gmail-api-expense-loader

Conversation

@ankitk26

@ankitk26 ankitk26 commented Jun 17, 2026

Copy link
Copy Markdown
Owner
  • Add Google social provider to better-auth with gmail.readonly scope
  • Store per-user Google refresh token via better-auth account table
  • Create Gmail API-based loader (replaces IMAP TCP dependency)
  • Add Google sign-in button alongside GitHub
  • Wire load expenses button to new Gmail API loader

Summary by cubic

Add a Gmail API–based expense loader with Google OAuth, replacing the IMAP approach. Users can now link Google and load expenses directly from Gmail.

  • New Features

    • Added Google provider to better-auth with offline access and gmail.readonly; per-user refresh tokens stored.
    • New load-expenses-gmail-api server fn: fetches mails after last expense, parses UPI notifications, upserts payees, inserts expenses; negates amounts for “paid to me” using IDS.
    • Login page adds “Sign in with Google”; Load Expenses button now calls the Gmail loader. Optional sender filter via CHECK_MAIL.
  • Migration

    • Set env vars: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, CHECK_MAIL (sender), IDS (comma-separated UPI IDs).
    • Configure Google OAuth credentials and authorized redirect for better-auth; ensure offline access is allowed.
    • Users must sign in with Google once to link their account and store the refresh token.

Written for commit 6879468. Summary will update on new commits.

Review in cubic

- Add Google social provider to better-auth with gmail.readonly scope
- Store per-user Google refresh token via better-auth account table
- Create Gmail API-based loader (replaces IMAP TCP dependency)
- Add Google sign-in button alongside GitHub
- Wire load expenses button to new Gmail API loader
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
iledge-tanstack Ready Ready Preview, Comment Jun 17, 2026 9:09am

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/routes/login.tsx">

<violation number="1" location="src/routes/login.tsx:31">
P1: No error handling in social sign-in onClick. If authClient.signIn.social rejects (network, popup blocked, etc.), setIsLoading(false) is never called — button stays disabled forever, user must refresh.</violation>
</file>

<file name="src/server-fns/load-expenses-gmail-api.ts">

<violation number="1" location="src/server-fns/load-expenses-gmail-api.ts:270">
P2: Loader only fetches the first page of Gmail message IDs, so matching emails after the first 100 are never processed.</violation>

<violation number="2" location="src/server-fns/load-expenses-gmail-api.ts:353">
P1: When two users share a payee UPI ID, `persistPayees`'s upsert updates the existing row's name but NOT its `user_id`. Then `fetchPayeeIdMap(user.id)` filters by the current user's ID and won't find that payee, so `payeeIdMap.get()` returns undefined → `?? 0` substitutes 0 as payee_id. Since `payee.id` is a serial starting at 1 and `expense.payee_id` has a FK constraint, this will throw a PostgreSQL foreign key violation.</violation>

<violation number="3" location="src/server-fns/load-expenses-gmail-api.ts:380">
P3: Typo: "up-to date" missing hyphen in user-facing message.</violation>

<violation number="4" location="src/server-fns/load-expenses-gmail-api.ts:406">
P0: No full-load code path. When `getLatestExpenseDate` returns null (first-time user with no expenses), the handler returns early with `"Please do a full load"` — but this IS the only load handler. There's no fallback date or alternate path to proceed, so first-time users can never use this API.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}

const latestDate = await getLatestExpenseDate(user.id);
if (!latestDate) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: No full-load code path. When getLatestExpenseDate returns null (first-time user with no expenses), the handler returns early with "Please do a full load" — but this IS the only load handler. There's no fallback date or alternate path to proceed, so first-time users can never use this API.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/server-fns/load-expenses-gmail-api.ts, line 406:

<comment>No full-load code path. When `getLatestExpenseDate` returns null (first-time user with no expenses), the handler returns early with `"Please do a full load"` — but this IS the only load handler. There's no fallback date or alternate path to proceed, so first-time users can never use this API.</comment>

<file context>
@@ -0,0 +1,431 @@
+		}
+
+		const latestDate = await getLatestExpenseDate(user.id);
+		if (!latestDate) {
+			return { message: "No expenses added yet. Please do a full load" };
+		}
</file context>

Comment thread src/routes/login.tsx
className="w-60"
size="lg"
disabled={isLoading}
onClick={async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: No error handling in social sign-in onClick. If authClient.signIn.social rejects (network, popup blocked, etc.), setIsLoading(false) is never called — button stays disabled forever, user must refresh.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/login.tsx, line 31:

<comment>No error handling in social sign-in onClick. If authClient.signIn.social rejects (network, popup blocked, etc.), setIsLoading(false) is never called — button stays disabled forever, user must refresh.</comment>

<file context>
@@ -2,40 +2,74 @@ import { SpinnerIcon } from "@phosphor-icons/react";
+			className="w-60"
+			size="lg"
+			disabled={isLoading}
+			onClick={async () => {
+				setIsLoading(true);
+				await authClient.signIn.social({
</file context>

sender_upi_id: mail.sender_upi_id,
amount: mail.amount,
transaction_date: mail.transaction_date,
payee_id: payeeIdMap.get(mail.payee_upi_id) ?? 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When two users share a payee UPI ID, persistPayees's upsert updates the existing row's name but NOT its user_id. Then fetchPayeeIdMap(user.id) filters by the current user's ID and won't find that payee, so payeeIdMap.get() returns undefined → ?? 0 substitutes 0 as payee_id. Since payee.id is a serial starting at 1 and expense.payee_id has a FK constraint, this will throw a PostgreSQL foreign key violation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/server-fns/load-expenses-gmail-api.ts, line 353:

<comment>When two users share a payee UPI ID, `persistPayees`'s upsert updates the existing row's name but NOT its `user_id`. Then `fetchPayeeIdMap(user.id)` filters by the current user's ID and won't find that payee, so `payeeIdMap.get()` returns undefined → `?? 0` substitutes 0 as payee_id. Since `payee.id` is a serial starting at 1 and `expense.payee_id` has a FK constraint, this will throw a PostgreSQL foreign key violation.</comment>

<file context>
@@ -0,0 +1,431 @@
+		sender_upi_id: mail.sender_upi_id,
+		amount: mail.amount,
+		transaction_date: mail.transaction_date,
+		payee_id: payeeIdMap.get(mail.payee_upi_id) ?? 0,
+		user_id: userId,
+	}));
</file context>

const query = buildGmailSearchQuery(afterSeconds);
const url = new URL("https://gmail.googleapis.com/gmail/v1/users/me/messages");
url.searchParams.set("q", query);
url.searchParams.set("maxResults", "100");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Loader only fetches the first page of Gmail message IDs, so matching emails after the first 100 are never processed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/server-fns/load-expenses-gmail-api.ts, line 270:

<comment>Loader only fetches the first page of Gmail message IDs, so matching emails after the first 100 are never processed.</comment>

<file context>
@@ -0,0 +1,431 @@
+	const query = buildGmailSearchQuery(afterSeconds);
+	const url = new URL("https://gmail.googleapis.com/gmail/v1/users/me/messages");
+	url.searchParams.set("q", query);
+	url.searchParams.set("maxResults", "100");
+
+	const res = await fetch(url, {
</file context>


function buildLoadResult(insertedRowCount: number): LoadResult {
if (insertedRowCount === 0) {
return { message: "Data up-to date" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Typo: "up-to date" missing hyphen in user-facing message.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/server-fns/load-expenses-gmail-api.ts, line 380:

<comment>Typo: "up-to date" missing hyphen in user-facing message.</comment>

<file context>
@@ -0,0 +1,431 @@
+
+function buildLoadResult(insertedRowCount: number): LoadResult {
+	if (insertedRowCount === 0) {
+		return { message: "Data up-to date" };
+	}
+
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant