From 6fd531d9c36ab75b3bcad0e8a0998bd193c38fce Mon Sep 17 00:00:00 2001 From: Max Koon <22125083+k2on@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:20:40 -0500 Subject: [PATCH] format: format with biome --- apps/api/src/auth.ts | 16 +- apps/api/src/index.ts | 2 +- apps/api/src/plaid.ts | 12 +- apps/api/src/webhook.ts | 5 +- apps/api/src/zero.ts | 169 ++++++++----- apps/expo/app/[...route].tsx | 4 +- apps/expo/app/_layout.tsx | 37 +-- apps/expo/app/approve.tsx | 8 +- apps/expo/app/auth.tsx | 7 +- apps/expo/app/index.native.tsx | 55 ++++- apps/expo/eslint.config.js | 6 +- apps/expo/lib/auth-client.ts | 12 +- apps/expo/metro.config.js | 4 +- apps/tui/build.ts | 13 +- apps/tui/lib/auth-client.ts | 6 +- apps/tui/src/auth.ts | 241 +++++++++++-------- apps/tui/src/config.ts | 2 +- apps/tui/src/index.tsx | 24 +- apps/tui/src/schema.ts | 7 +- apps/tui/src/store.ts | 6 +- apps/tui/util/qr.ts | 2 - packages/react-native-opentui/index.tsx | 302 +++++++++++++----------- packages/shared/src/const.ts | 1 - packages/shared/src/db/schema/public.ts | 11 +- packages/shared/src/mutators.ts | 17 +- packages/shared/src/queries.ts | 84 ++++--- packages/shared/src/zero-schema.gen.ts | 10 + packages/ui/components/Button.tsx | 25 +- packages/ui/components/Dialog.tsx | 36 ++- packages/ui/components/List.tsx | 36 +-- packages/ui/components/Table.tsx | 187 +++++++++------ packages/ui/src/index.tsx | 64 +++-- packages/ui/src/settings.tsx | 70 ++++-- packages/ui/src/settings/accounts.tsx | 133 +++++++---- packages/ui/src/settings/family.tsx | 3 +- packages/ui/src/settings/general.tsx | 4 +- packages/ui/src/transactions.tsx | 39 +-- packages/ui/src/useKeyboard.ts | 5 +- packages/ui/src/useKeyboard.web.ts | 22 +- scripts/reset-project.js | 4 +- scripts/set-machine-name.ts | 1 - 41 files changed, 1025 insertions(+), 667 deletions(-) diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts index ada728a..e227df7 100644 --- a/apps/api/src/auth.ts +++ b/apps/api/src/auth.ts @@ -20,25 +20,25 @@ export const auth = betterAuth({ "money://", ], advanced: { - crossSubDomainCookies: { - enabled: process.env.NODE_ENV == 'production', - domain: "koon.us", - }, + crossSubDomainCookies: { + enabled: process.env.NODE_ENV == "production", + domain: "koon.us", + }, }, plugins: [ expo(), genericOAuth({ config: [ { - providerId: 'koon-family', + providerId: "koon-family", clientId: process.env.OAUTH_CLIENT_ID!, clientSecret: process.env.OAUTH_CLIENT_SECRET!, discoveryUrl: process.env.OAUTH_DISCOVERY_URL!, scopes: ["profile", "email"], - } - ] + }, + ], }), deviceAuthorization(), bearer(), - ] + ], }); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e9cb3c5..25cee8f 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -12,7 +12,7 @@ const app = getHono(); app.use( "/api/*", cors({ - origin: ['https://money.koon.us', `${BASE_URL}:8081`], + origin: ["https://money.koon.us", `${BASE_URL}:8081`], allowMethods: ["POST", "GET", "OPTIONS"], allowHeaders: ["Content-Type", "Authorization"], credentials: true, diff --git a/apps/api/src/plaid.ts b/apps/api/src/plaid.ts index dcf9249..0df61da 100644 --- a/apps/api/src/plaid.ts +++ b/apps/api/src/plaid.ts @@ -1,13 +1,15 @@ import { Configuration, PlaidApi, PlaidEnvironments } from "plaid"; const configuration = new Configuration({ - basePath: process.env.PLAID_ENV == 'production' ? PlaidEnvironments.production : PlaidEnvironments.sandbox, + basePath: + process.env.PLAID_ENV == "production" + ? PlaidEnvironments.production + : PlaidEnvironments.sandbox, baseOptions: { headers: { - 'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID, - 'PLAID-SECRET': process.env.PLAID_SECRET, - } + "PLAID-CLIENT-ID": process.env.PLAID_CLIENT_ID, + "PLAID-SECRET": process.env.PLAID_SECRET, + }, }, }); export const plaidClient = new PlaidApi(configuration); - diff --git a/apps/api/src/webhook.ts b/apps/api/src/webhook.ts index 8d42f70..267f23f 100644 --- a/apps/api/src/webhook.ts +++ b/apps/api/src/webhook.ts @@ -3,12 +3,9 @@ import { plaidClient } from "./plaid"; // import { LinkSessionFinishedWebhook, WebhookType } from "plaid"; export const webhook = async (c: Context) => { - console.log("Got webhook"); const b = await c.req.text(); console.log("body:", b); - return c.text("Hi"); - -} +}; diff --git a/apps/api/src/zero.ts b/apps/api/src/zero.ts index 616e06a..a59f50e 100644 --- a/apps/api/src/zero.ts +++ b/apps/api/src/zero.ts @@ -8,8 +8,8 @@ import { PushProcessor, ZQLDatabase, } from "@rocicorp/zero/server"; -import { PostgresJSConnection } from '@rocicorp/zero/pg'; -import postgres from 'postgres'; +import { PostgresJSConnection } from "@rocicorp/zero/pg"; +import postgres from "postgres"; import { createMutators as createMutatorsShared, isLoggedIn, @@ -20,11 +20,22 @@ import { } from "@money/shared"; import type { AuthData } from "@money/shared/auth"; import { getHono } from "./hono"; -import { Configuration, CountryCode, PlaidApi, PlaidEnvironments, Products } from "plaid"; +import { + Configuration, + CountryCode, + PlaidApi, + PlaidEnvironments, + Products, +} from "plaid"; import { randomUUID } from "crypto"; import { db } from "./db"; -import { balance, plaidAccessTokens, plaidLink, transaction } from "@money/shared/db"; -import { eq, inArray, sql, type InferInsertModel } from "drizzle-orm"; +import { + balance, + plaidAccessTokens, + plaidLink, + transaction, +} from "@money/shared/db"; +import { and, eq, inArray, sql, type InferInsertModel } from "drizzle-orm"; import { plaidClient } from "./plaid"; const processor = new PushProcessor( @@ -53,7 +64,7 @@ const createMutators = (authData: AuthData | null) => { products: [Products.Transactions], country_codes: [CountryCode.Us], webhook: "https://webhooks.koon.us/api/webhook_receiver", - hosted_link: {} + hosted_link: {}, }); const { link_token, hosted_link_url } = r.data; @@ -70,25 +81,52 @@ const createMutators = (authData: AuthData | null) => { async get(_, { link_token }) { isLoggedIn(authData); - const linkResp = await plaidClient.linkTokenGet({ - link_token, - }); - if (!linkResp) throw Error("No link respo"); - console.log(JSON.stringify(linkResp.data, null, 4)); - const publicToken = linkResp.data.link_sessions?.at(0)?.results?.item_add_results.at(0)?.public_token; + try { + const token = await db.query.plaidLink.findFirst({ + where: and( + eq(plaidLink.token, link_token), + eq(plaidLink.user_id, authData.user.id), + ), + }); + if (!token) throw Error("Link not found"); + if (token.completeAt) return; - if (!publicToken) throw Error("No public token"); - const { data } = await plaidClient.itemPublicTokenExchange({ - public_token: publicToken, - }) + const linkResp = await plaidClient.linkTokenGet({ + link_token, + }); + if (!linkResp) throw Error("No link respo"); - await db.insert(plaidAccessTokens).values({ - id: randomUUID(), - userId: authData.user.id, - token: data.access_token, - logoUrl: "", - name: "" - }); + console.log(JSON.stringify(linkResp.data, null, 4)); + + const item_add_result = linkResp.data.link_sessions + ?.at(0) + ?.results?.item_add_results.at(0); + + // We will assume its not done yet. + if (!item_add_result) return; + + const { data } = await plaidClient.itemPublicTokenExchange({ + public_token: item_add_result.public_token, + }); + + await db.insert(plaidAccessTokens).values({ + id: randomUUID(), + userId: authData.user.id, + token: data.access_token, + logoUrl: "", + name: item_add_result.institution?.name || "Unknown", + }); + + await db + .update(plaidLink) + .set({ + completeAt: new Date(), + }) + .where(eq(plaidLink.token, link_token)); + } catch (e) { + console.error(e); + throw Error("Plaid error"); + } }, async updateTransactions() { @@ -108,29 +146,39 @@ const createMutators = (authData: AuthData | null) => { end_date: new Date().toISOString().split("T")[0], }); - const transactions = data.transactions.map(tx => ({ - id: randomUUID(), - user_id: authData.user.id, - plaid_id: tx.transaction_id, - account_id: tx.account_id, - name: tx.name, - amount: tx.amount as any, - datetime: tx.datetime ? new Date(tx.datetime) : new Date(tx.date), - authorized_datetime: tx.authorized_datetime ? new Date(tx.authorized_datetime) : undefined, - json: JSON.stringify(tx), - } satisfies InferInsertModel)); + const transactions = data.transactions.map( + (tx) => + ({ + id: randomUUID(), + user_id: authData.user.id, + plaid_id: tx.transaction_id, + account_id: tx.account_id, + name: tx.name, + amount: tx.amount as any, + datetime: tx.datetime + ? new Date(tx.datetime) + : new Date(tx.date), + authorized_datetime: tx.authorized_datetime + ? new Date(tx.authorized_datetime) + : undefined, + json: JSON.stringify(tx), + }) satisfies InferInsertModel, + ); - await db.insert(transaction).values(transactions).onConflictDoNothing({ - target: transaction.plaid_id, - }); + await db + .insert(transaction) + .values(transactions) + .onConflictDoNothing({ + target: transaction.plaid_id, + }); const txReplacingPendingIds = data.transactions - .filter(t => t.pending_transaction_id) - .map(t => t.pending_transaction_id!); + .filter((t) => t.pending_transaction_id) + .map((t) => t.pending_transaction_id!); - await db.delete(transaction) + await db + .delete(transaction) .where(inArray(transaction.plaid_id, txReplacingPendingIds)); - } }, @@ -146,26 +194,33 @@ const createMutators = (authData: AuthData | null) => { for (const account of accounts) { const { data } = await plaidClient.accountsBalanceGet({ - access_token: account.token + access_token: account.token, }); - await db.insert(balance).values(data.accounts.map(bal => ({ - id: randomUUID(), - user_id: authData.user.id, - plaid_id: bal.account_id, - avaliable: bal.balances.available as any, - current: bal.balances.current as any, - name: bal.name, - tokenId: account.id, - }))).onConflictDoUpdate({ - target: balance.plaid_id, - set: { current: sql.raw(`excluded.${balance.current.name}`), avaliable: sql.raw(`excluded.${balance.avaliable.name}`) } - }) + await db + .insert(balance) + .values( + data.accounts.map((bal) => ({ + id: randomUUID(), + user_id: authData.user.id, + plaid_id: bal.account_id, + avaliable: bal.balances.available as any, + current: bal.balances.current as any, + name: bal.name, + tokenId: account.id, + })), + ) + .onConflictDoUpdate({ + target: balance.plaid_id, + set: { + current: sql.raw(`excluded.${balance.current.name}`), + avaliable: sql.raw(`excluded.${balance.avaliable.name}`), + }, + }); } - }, - } + }, } as const satisfies Mutators; -} +}; const zero = getHono() .post("/mutate", async (c) => { diff --git a/apps/expo/app/[...route].tsx b/apps/expo/app/[...route].tsx index f8e89dc..1ebd9b5 100644 --- a/apps/expo/app/[...route].tsx +++ b/apps/expo/app/[...route].tsx @@ -5,7 +5,9 @@ import { authClient } from "@/lib/auth-client"; export default function Page() { const { route: initalRoute } = useLocalSearchParams<{ route: string[] }>(); - const [route, setRoute] = useState(initalRoute ? "/" + initalRoute.join("/") : "/"); + const [route, setRoute] = useState( + initalRoute ? "/" + initalRoute.join("/") : "/", + ); const { data } = authClient.useSession(); diff --git a/apps/expo/app/_layout.tsx b/apps/expo/app/_layout.tsx index ea3f929..a8cff7c 100644 --- a/apps/expo/app/_layout.tsx +++ b/apps/expo/app/_layout.tsx @@ -1,17 +1,23 @@ -import { Stack } from 'expo-router'; -import 'react-native-reanimated'; +import { Stack } from "expo-router"; +import "react-native-reanimated"; -import { authClient } from '@/lib/auth-client'; -import { ZeroProvider } from '@rocicorp/zero/react'; -import { useMemo } from 'react'; -import { authDataSchema } from '@money/shared/auth'; -import { Platform } from 'react-native'; -import type { ZeroOptions } from '@rocicorp/zero'; -import { schema, type Schema, createMutators, type Mutators, BASE_URL } from '@money/shared'; +import { authClient } from "@/lib/auth-client"; +import { ZeroProvider } from "@rocicorp/zero/react"; +import { useMemo } from "react"; +import { authDataSchema } from "@money/shared/auth"; +import { Platform } from "react-native"; +import type { ZeroOptions } from "@rocicorp/zero"; +import { + schema, + type Schema, + createMutators, + type Mutators, + BASE_URL, +} from "@money/shared"; import { expoSQLiteStoreProvider } from "@rocicorp/zero/react-native"; export const unstable_settings = { - anchor: 'index', + anchor: "index", }; const kvStore = Platform.OS === "web" ? undefined : expoSQLiteStoreProvider(); @@ -25,19 +31,22 @@ export default function RootLayout() { }, [session]); const cookie = useMemo(() => { - return Platform.OS == 'web' ? undefined : authClient.getCookie(); + return Platform.OS == "web" ? undefined : authClient.getCookie(); }, [session, isPending]); const zeroProps = useMemo(() => { return { - storageKey: 'money', + storageKey: "money", kvStore, - server: process.env.NODE_ENV == 'production' ? 'https://zero.koon.us' : `${BASE_URL}:4848`, + server: + process.env.NODE_ENV == "production" + ? "https://zero.koon.us" + : `${BASE_URL}:4848`, userID: authData?.user.id ?? "anon", schema, mutators: createMutators(authData), auth: cookie, - } as const satisfies ZeroOptions; + } as const satisfies ZeroOptions; }, [authData, cookie]); return ( diff --git a/apps/expo/app/approve.tsx b/apps/expo/app/approve.tsx index c3f29da..13d6d17 100644 --- a/apps/expo/app/approve.tsx +++ b/apps/expo/app/approve.tsx @@ -4,7 +4,7 @@ import { useEffect } from "react"; import { Text } from "react-native"; export default function Page() { - const { code } = useLocalSearchParams<{code: string }>(); + const { code } = useLocalSearchParams<{ code: string }>(); const { isPending, data } = authClient.useSession(); if (isPending) return Loading...; if (!isPending && !data) return Please log in; @@ -13,11 +13,7 @@ export default function Page() { authClient.device.approve({ userCode: code, }); - }, []); - return - Approving: {code} - + return Approving: {code}; } - diff --git a/apps/expo/app/auth.tsx b/apps/expo/app/auth.tsx index 753fa99..75a1bf7 100644 --- a/apps/expo/app/auth.tsx +++ b/apps/expo/app/auth.tsx @@ -6,7 +6,10 @@ export default function Auth() { const onLogin = () => { authClient.signIn.oauth2({ providerId: "koon-family", - callbackURL: process.env.NODE_ENV == 'production' ? 'https://money.koon.us' : `${BASE_URL}:8081`, + callbackURL: + process.env.NODE_ENV == "production" + ? "https://money.koon.us" + : `${BASE_URL}:8081`, }); }; @@ -14,5 +17,5 @@ export default function Auth() { + + - + - + }} + shortcut="y" + > + Delete + - - setIsAddOpen(false)}> - Add Account + Add Account - - + - + - { - if (key.name == 'd') { - setDeleting(selected); - } - }}> + { + if (key.name == "d") { + setDeleting(selected); + } + }} + > @@ -97,34 +115,53 @@ export function Accounts() { ); } - function AddAccount() { const { auth } = use(RouterContext); const [link, details] = useQuery(queries.getPlaidLink(auth)); + const { close } = use(Dialog.Context); const openLink = () => { - if (!link) return + if (!link) return; Linking.openURL(link.link); - } + }; const z = useZero(); useEffect(() => { console.log(link, details); if (details.type != "complete") return; - if (link != undefined) return; - + if (link != undefined) { + if (!link.completeAt) { + const timer = setInterval(() => { + console.log("Checking for link"); + z.mutate.link.get({ link_token: link.token }); + }, 1000 * 5); + return () => clearInterval(timer); + } else { + if (close) close(); + return; + } + } console.log("Creating new link"); z.mutate.link.create(); }, [link, details]); return ( <> - {link ? <> - Please click the button to complete setup. + + {link ? ( + <> + + Please click the button to complete setup. + - - : Loading Plaid Link} + + + ) : ( + Loading Plaid Link + )} ); } diff --git a/packages/ui/src/settings/family.tsx b/packages/ui/src/settings/family.tsx index 16357e8..1a40827 100644 --- a/packages/ui/src/settings/family.tsx +++ b/packages/ui/src/settings/family.tsx @@ -1,6 +1,5 @@ import { Text } from "react-native"; export function Family() { - return Welcome to family + return Welcome to family; } - diff --git a/packages/ui/src/settings/general.tsx b/packages/ui/src/settings/general.tsx index cf2e72b..c69c644 100644 --- a/packages/ui/src/settings/general.tsx +++ b/packages/ui/src/settings/general.tsx @@ -1,7 +1,5 @@ import { Text } from "react-native"; export function General() { - return Welcome to settings + return Welcome to settings; } - - diff --git a/packages/ui/src/transactions.tsx b/packages/ui/src/transactions.tsx index de25d2e..a71f136 100644 --- a/packages/ui/src/transactions.tsx +++ b/packages/ui/src/transactions.tsx @@ -1,11 +1,10 @@ import * as Table from "../components/Table"; import { useQuery } from "@rocicorp/zero/react"; -import { queries, type Transaction } from '@money/shared'; +import { queries, type Transaction } from "@money/shared"; import { use } from "react"; import { View, Text } from "react-native"; import { RouterContext } from "."; - const FORMAT = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, @@ -14,15 +13,18 @@ const FORMAT = new Intl.NumberFormat("en-US", { export type Account = { name: string; createdAt: number; -} +}; const COLUMNS: Table.Column[] = [ - { name: 'createdAt', label: 'Date', render: (n) => new Date(n).toDateString() }, - { name: 'amount', label: 'Amount' }, - { name: 'name', label: 'Name' }, + { + name: "createdAt", + label: "Date", + render: (n) => new Date(n).toDateString(), + }, + { name: "amount", label: "Amount" }, + { name: "name", label: "Name" }, ]; - export function Transactions() { const { auth } = use(RouterContext); const [items] = useQuery(queries.allTransactions(auth)); @@ -30,7 +32,7 @@ export function Transactions() { return ( - + @@ -38,18 +40,18 @@ export function Transactions() { - ) + ); } function Selected() { const { data, idx, selectedFrom } = use(Table.Context); if (selectedFrom == undefined) - return ( - - No items selected - - ); + return ( + + No items selected + + ); const from = Math.min(idx, selectedFrom); const to = Math.max(idx, selectedFrom); @@ -58,10 +60,11 @@ function Selected() { const sum = selected.reduce((prev, curr) => prev + curr.amount, 0); return ( - - {count} transaction{count == 1 ? "" : "s"} selected | ${FORMAT.format(sum)} + + + {count} transaction{count == 1 ? "" : "s"} selected | $ + {FORMAT.format(sum)} + ); } - - diff --git a/packages/ui/src/useKeyboard.ts b/packages/ui/src/useKeyboard.ts index a707a8d..b157c50 100644 --- a/packages/ui/src/useKeyboard.ts +++ b/packages/ui/src/useKeyboard.ts @@ -1,5 +1,8 @@ import { useKeyboard as useOpentuiKeyboard } from "@opentui/react"; -export function useKeyboard(handler: Parameters[0], _deps: any[] = []) { +export function useKeyboard( + handler: Parameters[0], + _deps: any[] = [], +) { return useOpentuiKeyboard(handler); } diff --git a/packages/ui/src/useKeyboard.web.ts b/packages/ui/src/useKeyboard.web.ts index 13f6f48..2dea73a 100644 --- a/packages/ui/src/useKeyboard.web.ts +++ b/packages/ui/src/useKeyboard.web.ts @@ -2,15 +2,17 @@ import { useEffect } from "react"; import type { KeyboardEvent } from "react"; import type { KeyEvent } from "@opentui/core"; - function convertName(keyName: string): string { - const result = keyName.toLowerCase() - if (result == 'arrowdown') return 'down'; - if (result == 'arrowup') return 'up'; + const result = keyName.toLowerCase(); + if (result == "arrowdown") return "down"; + if (result == "arrowup") return "up"; return result; } -export function useKeyboard(handler: (key: KeyEvent) => void, deps: any[] = []) { +export function useKeyboard( + handler: (key: KeyEvent) => void, + deps: any[] = [], +) { useEffect(() => { const handlerWeb = (event: KeyboardEvent) => { // @ts-ignore @@ -20,10 +22,10 @@ export function useKeyboard(handler: (key: KeyEvent) => void, deps: any[] = []) meta: event.metaKey, shift: event.shiftKey, option: event.metaKey, - sequence: '', + sequence: "", number: false, - raw: '', - eventType: 'press', + raw: "", + eventType: "press", source: "raw", code: event.code, super: false, @@ -38,8 +40,8 @@ export function useKeyboard(handler: (key: KeyEvent) => void, deps: any[] = []) // @ts-ignore window.addEventListener("keydown", handlerWeb); return () => { - // @ts-ignore -window.removeEventListener("keydown", handlerWeb); + // @ts-ignore + window.removeEventListener("keydown", handlerWeb); }; }, deps); } diff --git a/scripts/reset-project.js b/scripts/reset-project.js index 51dff15..70724e6 100755 --- a/scripts/reset-project.js +++ b/scripts/reset-project.js @@ -91,7 +91,7 @@ const moveDirectories = async (userInput) => { userInput === "y" ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` : "" - }` + }`, ); } catch (error) { console.error(`❌ Error during script execution: ${error.message}`); @@ -108,5 +108,5 @@ rl.question( console.log("❌ Invalid input. Please enter 'Y' or 'N'."); rl.close(); } - } + }, ); diff --git a/scripts/set-machine-name.ts b/scripts/set-machine-name.ts index b785217..92b2abc 100755 --- a/scripts/set-machine-name.ts +++ b/scripts/set-machine-name.ts @@ -33,4 +33,3 @@ try { console.error("Failed to update .env.dev:", err); process.exit(1); } -