diff --git a/.env.example b/.env.example index 81c1e87..b24b72a 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,5 @@ ZERO_MUTATE_FORWARD_COOKIES="true" PLAID_CLIENT_ID= PLAID_SECRET= PLAID_ENV=sandbox + +EXPO_PUBLIC_TAILSCALE_MACHINE=laptop diff --git a/api/src/auth.ts b/api/src/auth.ts index ea14337..1a654a9 100644 --- a/api/src/auth.ts +++ b/api/src/auth.ts @@ -4,6 +4,7 @@ import { genericOAuth } from "better-auth/plugins"; import { expo } from "@better-auth/expo"; import { drizzleSchema } from "@money/shared/db"; import { db } from "./db"; +import { BASE_URL, HOST } from "@money/shared"; export const auth = betterAuth({ database: drizzleAdapter(db, { @@ -11,10 +12,16 @@ export const auth = betterAuth({ provider: "pg", usePlural: true, }), - trustedOrigins: ["money://", "http://localhost:8081", "https://money.koon.us"], + trustedOrigins: [ + "http://localhost:8081", + `exp://${HOST}:8081`, + `${BASE_URL}:8081`, + "https://money.koon.us", + "money://", + ], advanced: { crossSubDomainCookies: { - enabled: true, + enabled: process.env.NODE_ENV == 'production', domain: "koon.us", }, }, diff --git a/api/src/index.ts b/api/src/index.ts index d67b4ac..197613b 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,5 +1,6 @@ import { serve } from "@hono/node-server"; import { authDataSchema } from "@money/shared/auth"; +import { BASE_URL } from "@money/shared"; import { cors } from "hono/cors"; import { auth } from "./auth"; import { getHono } from "./hono"; @@ -10,7 +11,7 @@ const app = getHono(); app.use( "/api/*", cors({ - origin: ['https://money.koon.us','http://localhost:8081'], + origin: ['https://money.koon.us', `${BASE_URL}:8081`], allowMethods: ["POST", "GET", "OPTIONS"], allowHeaders: ["Content-Type", "Authorization"], credentials: true, diff --git a/app/_layout.tsx b/app/_layout.tsx index 380d8d3..adf90f7 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import { authDataSchema } from '@/shared/src/auth'; import { Platform } from 'react-native'; import type { ZeroOptions } from '@rocicorp/zero'; -import { schema, type Schema, createMutators, type Mutators } from '@/shared/src'; +import { schema, type Schema, createMutators, type Mutators, BASE_URL } from '@/shared/src'; import { expoSQLiteStoreProvider } from "@rocicorp/zero/react-native"; export const unstable_settings = { @@ -32,7 +32,7 @@ export default function RootLayout() { return { storageKey: 'money', kvStore, - server: process.env.NODE_ENV == 'production' ? 'https://zero.koon.us' : 'http://localhost:4848', + server: process.env.NODE_ENV == 'production' ? 'https://zero.koon.us' : `${BASE_URL}:4848`, userID: authData?.user.id ?? "anon", schema, mutators: createMutators(authData), diff --git a/app/auth.tsx b/app/auth.tsx index 7a24f69..753fa99 100644 --- a/app/auth.tsx +++ b/app/auth.tsx @@ -1,11 +1,12 @@ import { Button, View } from "react-native"; import { authClient } from "@/lib/auth-client"; +import { BASE_URL } from "@money/shared"; export default function Auth() { const onLogin = () => { authClient.signIn.oauth2({ providerId: "koon-family", - callbackURL: process.env.NODE_ENV == 'production' ? 'https://money.koon.us' : "http://localhost:8081" + callbackURL: process.env.NODE_ENV == 'production' ? 'https://money.koon.us' : `${BASE_URL}:8081`, }); }; diff --git a/app/index.tsx b/app/index.tsx index a4967fb..6d721eb 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,94 +1,35 @@ import { authClient } from '@/lib/auth-client'; -import { Image, Pressable, ScrollView, Text, View } from 'react-native'; +import { RefreshControl, ScrollView, StatusBar, Text, View } from 'react-native'; import { useQuery, useZero } from "@rocicorp/zero/react"; import { queries, type Mutators, type Schema } from '@money/shared'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; export default function HomeScreen() { const { data: session } = authClient.useSession(); - const z = useZero(); - const [transactions] = useQuery(queries.allTransactions(session)); const [balances] = useQuery(queries.getBalances(session)); + const [refreshing, setRefreshing] = useState(false); - const [idx, setIdx] = useState(0); - const [accountIdx, setAccountIdx] = useState(0); - - const account = balances.at(accountIdx)!; - - const filteredTransactions = transactions - .filter(t => t.account_id == account.plaid_id) - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "j") { - setIdx((prevIdx) => { - if (prevIdx + 1 == filteredTransactions.length) return prevIdx; - return prevIdx + 1 - }); - } else if (event.key === "k") { - setIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1); - } else if (event.key == 'g') { - setIdx(0); - } else if (event.key == "G") { - setIdx(transactions.length - 1); - } else if (event.key == 'R') { - z.mutate.link.updateTransactions(); - z.mutate.link.updateBalences(); - } else if (event.key == 'h') { - setAccountIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1); - } else if (event.key == 'l') { - setAccountIdx((prevIdx) => { - if (prevIdx + 1 == balances.length) return prevIdx; - return prevIdx + 1 - }); - } - }; - - window.addEventListener("keydown", handleKeyDown); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [filteredTransactions, balances]); - - function lpad(n: number): string { - const LEN = 9; - const nstr = n.toFixed(2).toLocaleString(); - return Array.from({ length: LEN - nstr.length }).join(" ") + nstr; - } - - function uuu(t: typeof filteredTransactions[number]): string | undefined { - if (!t.json) return; - const j = JSON.parse(t.json); - return j.counterparties.filter((c: any) => !!c.logo_url).at(0)?.logo_url || j.personal_finance_category_icon_url; - } + const onRefresh = async () => { + setRefreshing(true); + // simulate async work + await new Promise((resolve) => setTimeout(resolve, 1000)); + setRefreshing(false); + }; return ( - - - - {balances.map((bal, i) => - {bal.name}: {bal.current} ({bal.avaliable}) - )} - - - - {filteredTransactions.map((t, i) => { - setIdx(i); - }} style={{ backgroundColor: i == idx ? 'black' : undefined, cursor: 'default' as 'auto' }} key={t.id}> - - {new Date(t.datetime!).toDateString()} - 0 ? 'red' : 'green' }}> {lpad(t.amount)} - - {t.name.substring(0, 50)} - - )} - - - {JSON.stringify(JSON.parse(filteredTransactions.at(idx)?.json || "null"), null, 4)} - - - + <> + + } style={{ paddingHorizontal: 10 }}> + {balances.map(balance => )} + + ); } + +function Balance({ balance }: { balance: { name: string, current: number, avaliable: number } }) { + return + {balance.name} + {balance.current} + +} diff --git a/app/index.web.tsx b/app/index.web.tsx new file mode 100644 index 0000000..a63108c --- /dev/null +++ b/app/index.web.tsx @@ -0,0 +1,99 @@ +import { authClient } from '@/lib/auth-client'; +import { Button, Image, Platform, Pressable, ScrollView, Text, View } from 'react-native'; +import { useQuery, useZero } from "@rocicorp/zero/react"; +import { queries, type Mutators, type Schema } from '@money/shared'; +import { useEffect, useState } from 'react'; +import { Link } from 'expo-router'; + +export default function HomeScreen() { + const { data: session } = authClient.useSession(); + + const z = useZero(); + const [transactions] = useQuery(queries.allTransactions(session)); + const [balances] = useQuery(queries.getBalances(session)); + + const [idx, setIdx] = useState(0); + const [accountIdx, setAccountIdx] = useState(0); + + const account = balances.at(accountIdx)!; + + const filteredTransactions = transactions + .filter(t => t.account_id == account.plaid_id) + + useEffect(() => { + if (Platform.OS != 'web') return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "j") { + setIdx((prevIdx) => { + if (prevIdx + 1 == filteredTransactions.length) return prevIdx; + return prevIdx + 1 + }); + } else if (event.key === "k") { + setIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1); + } else if (event.key == 'g') { + setIdx(0); + } else if (event.key == "G") { + setIdx(transactions.length - 1); + } else if (event.key == 'R') { + z.mutate.link.updateTransactions(); + z.mutate.link.updateBalences(); + } else if (event.key == 'h') { + setAccountIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1); + } else if (event.key == 'l') { + setAccountIdx((prevIdx) => { + if (prevIdx + 1 == balances.length) return prevIdx; + return prevIdx + 1 + }); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [filteredTransactions, balances]); + + function lpad(n: number): string { + const LEN = 9; + const nstr = n.toFixed(2).toLocaleString(); + return Array.from({ length: LEN - nstr.length }).join(" ") + nstr; + } + + function uuu(t: typeof filteredTransactions[number]): string | undefined { + if (!t.json) return; + const j = JSON.parse(t.json); + return j.counterparties.filter((c: any) => !!c.logo_url).at(0)?.logo_url || j.personal_finance_category_icon_url; + } + + return ( + + +