Add beenvoice mobile companion app with full dark mode support.

Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 22:36:37 -04:00
parent 8a7a8df477
commit 14c880123c
93 changed files with 8849 additions and 7849 deletions
+67
View File
@@ -0,0 +1,67 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
const ACCOUNTS_KEY = "beenvoice:accounts";
const ACTIVE_ACCOUNT_KEY = "beenvoice:active-account-id";
const DRAFT_INSTANCE_URL_KEY = "beenvoice:draft-instance-url";
export type SavedAccount = {
id: string;
instanceUrl: string;
userId: string;
email: string;
name: string;
lastUsedAt: number;
};
export function buildAccountId(instanceUrl: string, userId: string) {
const host = instanceUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
return `${host}::${userId}`;
}
export function authStoragePrefix(accountId: string) {
return `beenvoice:auth:${accountId}`;
}
export async function loadAccounts(): Promise<SavedAccount[]> {
const raw = await AsyncStorage.getItem(ACCOUNTS_KEY);
if (!raw) return [];
try {
const parsed = JSON.parse(raw) as SavedAccount[];
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
export async function saveAccounts(accounts: SavedAccount[]) {
await AsyncStorage.setItem(ACCOUNTS_KEY, JSON.stringify(accounts));
}
export async function loadActiveAccountId(): Promise<string | null> {
return AsyncStorage.getItem(ACTIVE_ACCOUNT_KEY);
}
export async function saveActiveAccountId(accountId: string | null) {
if (accountId) {
await AsyncStorage.setItem(ACTIVE_ACCOUNT_KEY, accountId);
} else {
await AsyncStorage.removeItem(ACTIVE_ACCOUNT_KEY);
}
}
export async function loadDraftInstanceUrl(): Promise<string | null> {
return AsyncStorage.getItem(DRAFT_INSTANCE_URL_KEY);
}
export async function saveDraftInstanceUrl(url: string | null) {
if (url) {
await AsyncStorage.setItem(DRAFT_INSTANCE_URL_KEY, url);
} else {
await AsyncStorage.removeItem(DRAFT_INSTANCE_URL_KEY);
}
}
export async function hasConfiguredInstanceUrl(): Promise<boolean> {
const [accounts, draft] = await Promise.all([loadAccounts(), loadDraftInstanceUrl()]);
return accounts.length > 0 || Boolean(draft);
}