feat: replace IP with tailscale url
This commit is contained in:
@@ -16,3 +16,5 @@ ZERO_MUTATE_FORWARD_COOKIES="true"
|
||||
PLAID_CLIENT_ID=
|
||||
PLAID_SECRET=
|
||||
PLAID_ENV=sandbox
|
||||
|
||||
EXPO_PUBLIC_TAILSCALE_MACHINE=laptop
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
103
app/index.tsx
103
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<Schema, Mutators>();
|
||||
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 (
|
||||
<View>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<View style={{ backgroundColor: '' }}>
|
||||
{balances.map((bal, i) => <View key={bal.id} style={{ backgroundColor: i == accountIdx ? 'black' : undefined}}>
|
||||
<Text style={{ fontFamily: 'mono', color: i == accountIdx ? 'white' : undefined }}>{bal.name}: {bal.current} ({bal.avaliable})</Text>
|
||||
</View>)}
|
||||
</View>
|
||||
|
||||
<View>
|
||||
{filteredTransactions.map((t, i) => <Pressable onHoverIn={() => {
|
||||
setIdx(i);
|
||||
}} style={{ backgroundColor: i == idx ? 'black' : undefined, cursor: 'default' as 'auto' }} key={t.id}>
|
||||
<Text style={{ fontFamily: 'mono', color: i == idx ? 'white' : undefined }}>
|
||||
{new Date(t.datetime!).toDateString()}
|
||||
<Text style={{ color: t.amount > 0 ? 'red' : 'green' }}> {lpad(t.amount)}</Text>
|
||||
<Image style={{ width: 15, height: 15, marginHorizontal: 10 }} source={{ uri: uuu(t) || "" }} />
|
||||
{t.name.substring(0, 50)}
|
||||
</Text>
|
||||
</Pressable>)}
|
||||
</View>
|
||||
<ScrollView>
|
||||
<Text style={{ fontFamily: 'mono' }}>{JSON.stringify(JSON.parse(filteredTransactions.at(idx)?.json || "null"), null, 4)}</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
<>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
<ScrollView contentContainerStyle={{ paddingTop: StatusBar.currentHeight, flexGrow: 1 }} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />} style={{ paddingHorizontal: 10 }}>
|
||||
{balances.map(balance => <Balance key={balance.id} balance={balance} />)}
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Balance({ balance }: { balance: { name: string, current: number, avaliable: number } }) {
|
||||
return <View style={{ backgroundColor: "#eee", borderColor: "#ddd", borderWidth: 1, marginBottom: 10, borderRadius: 10 }}>
|
||||
<Text style={{ fontSize: 15, textAlign: "center" }}>{balance.name}</Text>
|
||||
<Text style={{ fontSize: 30, textAlign: "center" }}>{balance.current}</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
99
app/index.web.tsx
Normal file
99
app/index.web.tsx
Normal file
@@ -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<Schema, Mutators>();
|
||||
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 (
|
||||
<View>
|
||||
<Link prefetch href="/settings">
|
||||
<Button title="Settings" />
|
||||
</Link>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<View style={{ backgroundColor: '' }}>
|
||||
{balances.map((bal, i) => <View key={bal.id} style={{ backgroundColor: i == accountIdx ? 'black' : undefined}}>
|
||||
<Text style={{ fontFamily: 'mono', color: i == accountIdx ? 'white' : undefined }}>{bal.name}: {bal.current} ({bal.avaliable})</Text>
|
||||
</View>)}
|
||||
</View>
|
||||
|
||||
<View>
|
||||
{filteredTransactions.map((t, i) => <Pressable onHoverIn={() => {
|
||||
setIdx(i);
|
||||
}} style={{ backgroundColor: i == idx ? 'black' : undefined, cursor: 'default' as 'auto' }} key={t.id}>
|
||||
<Text style={{ fontFamily: 'mono', color: i == idx ? 'white' : undefined }}>
|
||||
{new Date(t.datetime!).toDateString()}
|
||||
<Text style={{ color: t.amount > 0 ? 'red' : 'green' }}> {lpad(t.amount)}</Text>
|
||||
<Image style={{ width: 15, height: 15, marginHorizontal: 10 }} source={{ uri: uuu(t) || "" }} />
|
||||
{t.name.substring(0, 50)}
|
||||
</Text>
|
||||
</Pressable>)}
|
||||
</View>
|
||||
<ScrollView>
|
||||
<Text style={{ fontFamily: 'mono' }}>{JSON.stringify(JSON.parse(filteredTransactions.at(idx)?.json || "null"), null, 4)}</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import { createAuthClient } from "better-auth/react";
|
||||
import { genericOAuthClient } from "better-auth/client/plugins";
|
||||
import { expoClient } from "@better-auth/expo/client";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import { BASE_URL } from "@money/shared";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NODE_ENV == 'production' ? 'https://money-api.koon.us' : "http://localhost:3000",
|
||||
baseURL: process.env.NODE_ENV == 'production' ? 'https://money-api.koon.us' : `${BASE_URL}:3000`,
|
||||
plugins: [
|
||||
expoClient({
|
||||
scheme: "money",
|
||||
|
||||
@@ -25,10 +25,14 @@ processes:
|
||||
initial_delay_seconds: 2
|
||||
period_seconds: 1
|
||||
|
||||
tailscale_machine_name:
|
||||
command: "pnpm tsx ./scripts/set-machine-name.ts"
|
||||
|
||||
expo:
|
||||
command: "pnpm start"
|
||||
environment:
|
||||
- "DATABASE_URL=postgresql://postgres@localhost:5432/money"
|
||||
depends_on:
|
||||
tailscale_machine_name:
|
||||
condition: process_completed_successfully
|
||||
|
||||
api:
|
||||
command: "pnpm run dev"
|
||||
|
||||
36
scripts/set-machine-name.ts
Executable file
36
scripts/set-machine-name.ts
Executable file
@@ -0,0 +1,36 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
|
||||
const ENV_FILE = ".env.dev";
|
||||
const VARIABLE_NAME = "EXPO_PUBLIC_TAILSCALE_MACHINE";
|
||||
|
||||
try {
|
||||
const json = execSync("tailscale status --self --json", { encoding: "utf8" });
|
||||
const data = JSON.parse(json);
|
||||
const machine = data?.Self?.DNSName.split(".")[0];
|
||||
|
||||
if (!machine) {
|
||||
console.error("Error: could not retrieve Tailscale machine name");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let content = "";
|
||||
if (fs.existsSync(ENV_FILE)) {
|
||||
content = fs.readFileSync(ENV_FILE, "utf8");
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`^${VARIABLE_NAME}=.*`, "m");
|
||||
if (pattern.test(content)) {
|
||||
content = content.replace(pattern, `${VARIABLE_NAME}=${machine}`);
|
||||
} else {
|
||||
if (content && !content.endsWith("\n")) content += "\n";
|
||||
content += `${VARIABLE_NAME}=${machine}\n`;
|
||||
}
|
||||
|
||||
fs.writeFileSync(ENV_FILE, content, "utf8");
|
||||
console.log(`Updated ${ENV_FILE} with ${VARIABLE_NAME}=${machine}`);
|
||||
} catch (err) {
|
||||
console.error("Failed to update .env.dev:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
3
shared/src/const.ts
Normal file
3
shared/src/const.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const HOST = process.env.EXPO_PUBLIC_TAILSCALE_MACHINE || "localhost";
|
||||
export const BASE_URL = `http://${HOST}`;
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./queries";
|
||||
export * from "./mutators";
|
||||
export * from "./zero-schema.gen";
|
||||
export * from "./zql";
|
||||
export * from "./const";
|
||||
|
||||
Reference in New Issue
Block a user