Convert Beenvoice to a Turborepo monorepo
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# beenvoice-app — agent notes
|
||||
# Beenvoice mobile — agent notes
|
||||
|
||||
Expo SDK **57**. Read [Expo v57 docs](https://docs.expo.dev/versions/v57.0.0/) before changing native config.
|
||||
|
||||
@@ -10,7 +10,8 @@ Expo SDK **57**. Read [Expo v57 docs](https://docs.expo.dev/versions/v57.0.0/) b
|
||||
## Conventions
|
||||
|
||||
- **Package manager**: Bun only
|
||||
- **API types**: import `AppRouter` from `beenvoice/server/api/root` (tsconfig path `../beenvoice-web/src/*`)
|
||||
- **API types**: import `AppRouter` from `beenvoice/server/api/root` (tsconfig path `../web/src/*`)
|
||||
- **Shared domain**: import platform-neutral behavior from `@beenvoice/domain`
|
||||
- **Styling**: `useAppTheme()` + `useThemedStyles()`; tokens in `lib/theme-palette.ts`
|
||||
- **Forms**: `lib/form-validation.ts`; show errors only after blur/submit (`useFieldVisibility`)
|
||||
- **Auth**: never remount account without migrating SecureStore session (`lib/auth-storage.ts`)
|
||||
@@ -28,6 +29,6 @@ Expo SDK **57**. Read [Expo v57 docs](https://docs.expo.dev/versions/v57.0.0/) b
|
||||
| App lock | `lib/app-lock.ts`, `contexts/AppLockContext.tsx` |
|
||||
| Time clock | `components/time-clock/TimeClockPanel.tsx` |
|
||||
|
||||
## Server repo
|
||||
## Server workspace
|
||||
|
||||
Sibling `../beenvoice-web` — run `bun run dev` on :3000 before mobile dev.
|
||||
Sibling `../web` — run `bun run dev` at the repository root to start both apps.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+10
-10
@@ -1,20 +1,20 @@
|
||||
# beenvoice Mobile
|
||||
|
||||
Expo companion for [beenvoice-web](../beenvoice-web) — dashboard, time clock, invoices, clients, businesses, and settings. Shares the **same tRPC API** and **better-auth** sessions as the web app.
|
||||
Expo companion for the [Beenvoice web app](../web/README.md) — dashboard, time clock, invoices, clients, businesses, and settings. Shares the **same tRPC API**, Better Auth sessions, and platform-neutral domain package.
|
||||
|
||||
**Architecture (dense):** [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh) 1.3+
|
||||
- beenvoice API running ([setup](../beenvoice-web/README.md))
|
||||
- Beenvoice API running ([setup](../web/README.md))
|
||||
- Xcode + iOS Simulator (or device) for native dev build
|
||||
- **Not Expo Go** — widgets, SecureStore auth, and biometrics need `expo-dev-client`
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd beenvoice-app
|
||||
cd apps/mobile
|
||||
bun install
|
||||
cp .env.example .env
|
||||
```
|
||||
@@ -36,11 +36,11 @@ Server must enable `@better-auth/expo` in `beenvoice/src/lib/auth.ts` with `been
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# Terminal 1 — API
|
||||
cd ../beenvoice-web && bun run dev
|
||||
# Terminal 1, from the repository root — API + Metro
|
||||
bun run dev
|
||||
|
||||
# Terminal 2 — mobile (builds native app if needed)
|
||||
cd beenvoice-app && bun run ios
|
||||
# Terminal 2, from the repository root — native app
|
||||
bun run --filter @beenvoice/mobile ios
|
||||
```
|
||||
|
||||
Metro uses port **8082** (avoids other Expo projects on 8081).
|
||||
@@ -145,6 +145,6 @@ widgets/ # iOS Live Activity (TimeClockActivity)
|
||||
|
||||
## Related
|
||||
|
||||
- [beenvoice-web README](../beenvoice-web/README.md)
|
||||
- [beenvoice-web ARCHITECTURE](../beenvoice-web/docs/ARCHITECTURE.md)
|
||||
- [Workspace root README](../README.md)
|
||||
- [Web README](../web/README.md)
|
||||
- [Web architecture](../web/docs/ARCHITECTURE.md)
|
||||
- [Workspace root README](../../README.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -247,7 +247,7 @@ Optional: iPad 12.9" if `supportsTablet: true` — use iPad simulator or “Run
|
||||
See **[IOS_LOCAL_RELEASE.md](./IOS_LOCAL_RELEASE.md)** for the full guide.
|
||||
|
||||
```bash
|
||||
cd beenvoice-app
|
||||
cd apps/mobile
|
||||
cp .ios-release.env.example .ios-release.env # once — add Team ID + API key
|
||||
bun run ios:release:upload # archive + upload to TestFlight
|
||||
```
|
||||
@@ -257,7 +257,7 @@ Requires Xcode on macOS, Apple Developer membership, and an App Store Connect AP
|
||||
### Option B — EAS (Expo cloud build)
|
||||
|
||||
```bash
|
||||
cd beenvoice-app
|
||||
cd apps/mobile
|
||||
|
||||
# Production iOS build (auto-increments build number)
|
||||
eas build --platform ios --profile production
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# beenvoice-app architecture
|
||||
# Beenvoice mobile architecture
|
||||
|
||||
Dense reference for the Expo 57 mobile companion. Talks to **beenvoice** over tRPC + better-auth. Requires a **development build** (not Expo Go) for widgets, SecureStore auth, and biometrics.
|
||||
|
||||
@@ -10,7 +10,8 @@ Dense reference for the Expo 57 mobile companion. Talks to **beenvoice** over tR
|
||||
| UI | React Native 0.85, `@expo/ui` (SwiftUI widgets) |
|
||||
| API | tRPC 11 + TanStack Query, SuperJSON |
|
||||
| Auth | better-auth + `@better-auth/expo` → `expo-secure-store` |
|
||||
| Types | `AppRouter` imported from `../beenvoice-web/src/server/api/root` |
|
||||
| Types | `AppRouter` imported type-only from sibling `apps/web` |
|
||||
| Shared domain | `@beenvoice/domain` workspace package |
|
||||
|
||||
## Boot sequence
|
||||
|
||||
@@ -271,6 +272,6 @@ Requires beenvoice with:
|
||||
|
||||
- `@better-auth/expo` in `src/lib/auth.ts`
|
||||
- `trustedOrigins` including `beenvoice://` and `exp://`
|
||||
- Postgres running (`docker compose -f docker-compose.dev.yml up -d db`)
|
||||
- Postgres running (`bun run --filter @beenvoice/web docker:up` from the repository root)
|
||||
|
||||
See [beenvoice-web/docs/ARCHITECTURE.md](../../beenvoice-web/docs/ARCHITECTURE.md).
|
||||
See the [web architecture](../../web/docs/ARCHITECTURE.md).
|
||||
|
||||
@@ -13,7 +13,7 @@ Archive and upload **beenvoice** to App Store Connect using Xcode on your Mac
|
||||
## One-time setup
|
||||
|
||||
```bash
|
||||
cd beenvoice-app
|
||||
cd apps/mobile
|
||||
cp .ios-release.env.example .ios-release.env
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# beenvoice-app documentation
|
||||
# Beenvoice mobile documentation
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
## Related
|
||||
|
||||
- [beenvoice-web docs](../../beenvoice-web/docs/README.md) — server API and web app
|
||||
- [Workspace README](../../README.md) — full-stack layout
|
||||
- [Web docs](../../web/docs/README.md) — server API and web app
|
||||
- [Workspace README](../../../README.md) — full-stack layout
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
export const EXPENSE_CATEGORIES = [
|
||||
"Travel",
|
||||
"Meals & Entertainment",
|
||||
"Software & Subscriptions",
|
||||
"Hardware & Equipment",
|
||||
"Office Supplies",
|
||||
"Marketing",
|
||||
"Professional Services",
|
||||
"Utilities",
|
||||
"Other",
|
||||
] as const;
|
||||
export {
|
||||
EXPENSE_CATEGORIES,
|
||||
type ExpenseCategory,
|
||||
} from "@beenvoice/domain/expense-categories";
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
export type InvoiceStatus = "draft" | "sent" | "paid" | "overdue";
|
||||
import { getEffectiveInvoiceStatus } from "@beenvoice/domain/invoice-status";
|
||||
import type { EffectiveInvoiceStatus } from "@beenvoice/domain/invoice-status";
|
||||
|
||||
export type InvoiceStatus = EffectiveInvoiceStatus;
|
||||
|
||||
export function getInvoiceStatus(invoice: {
|
||||
status: string;
|
||||
dueDate: Date | string;
|
||||
}): InvoiceStatus {
|
||||
if (invoice.status === "paid") return "paid";
|
||||
if (invoice.status === "draft") return "draft";
|
||||
|
||||
const today = new Date();
|
||||
const due = new Date(invoice.dueDate);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
due.setHours(0, 0, 0, 0);
|
||||
|
||||
if (due < today) return "overdue";
|
||||
return "sent";
|
||||
if (invoice.status === "paid" || invoice.status === "draft") {
|
||||
return invoice.status;
|
||||
}
|
||||
return getEffectiveInvoiceStatus("sent", invoice.dueDate);
|
||||
}
|
||||
|
||||
export const statusLabels: Record<InvoiceStatus, string> = {
|
||||
|
||||
@@ -1,128 +1,5 @@
|
||||
export type ReceiptParseResult = {
|
||||
amount: number | null;
|
||||
date: Date | null;
|
||||
subtotal: number | null;
|
||||
tax: number | null;
|
||||
vendor: string | null;
|
||||
items: ReceiptLineItem[];
|
||||
rawLines: string[];
|
||||
};
|
||||
|
||||
export type ReceiptLineItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
rawLine: string;
|
||||
};
|
||||
|
||||
const AMOUNT_PATTERNS = [
|
||||
/(?:total|amount due|balance due|grand total)[:\s]*\$?\s*([\d,]+\.\d{2})/i,
|
||||
/\$\s*([\d,]+\.\d{2})\s*(?:total|due)?/i,
|
||||
/(?:USD|CAD|EUR)\s*([\d,]+\.\d{2})/i,
|
||||
];
|
||||
|
||||
const DATE_PATTERNS = [
|
||||
/(\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4})/,
|
||||
/(\d{4}[/.-]\d{1,2}[/.-]\d{1,2})/,
|
||||
];
|
||||
|
||||
const SUBTOTAL_PATTERNS = [
|
||||
/(?:sub\s?total|subtotal)[:\s]*\$?\s*([\d,]+\.\d{2})/i,
|
||||
];
|
||||
|
||||
const TAX_PATTERNS = [
|
||||
/(?:tax|sales tax|hst|gst|pst|vat)[:\s]*\$?\s*([\d,]+\.\d{2})/i,
|
||||
];
|
||||
|
||||
const NON_ITEM_LINE =
|
||||
/(?:total|subtotal|sub total|tax|tip|gratuity|change|cash|visa|mastercard|amex|discover|card|credit|debit|balance|amount due|auth|approval|terminal|merchant|receipt|order|invoice|thank|powered by)/i;
|
||||
|
||||
function parseAmount(text: string): number | null {
|
||||
for (const pattern of AMOUNT_PATTERNS) {
|
||||
const match = text.match(pattern);
|
||||
if (!match?.[1]) continue;
|
||||
const value = Number(match[1].replace(/,/g, ""));
|
||||
if (Number.isFinite(value) && value > 0) return value;
|
||||
}
|
||||
|
||||
const amounts = [...text.matchAll(/\$\s*([\d,]+\.\d{2})/g)]
|
||||
.map((m) => Number(m[1]!.replace(/,/g, "")))
|
||||
.filter((n) => Number.isFinite(n) && n > 0);
|
||||
|
||||
return amounts.length > 0 ? Math.max(...amounts) : null;
|
||||
}
|
||||
|
||||
function parseFirstMatchingAmount(
|
||||
text: string,
|
||||
patterns: RegExp[],
|
||||
): number | null {
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (!match?.[1]) continue;
|
||||
const value = Number(match[1].replace(/,/g, ""));
|
||||
if (Number.isFinite(value) && value >= 0) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseDate(text: string): Date | null {
|
||||
for (const pattern of DATE_PATTERNS) {
|
||||
const match = text.match(pattern);
|
||||
if (!match?.[1]) continue;
|
||||
const parsed = new Date(match[1]);
|
||||
if (!Number.isNaN(parsed.getTime())) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseVendor(lines: string[]): string | null {
|
||||
const candidate = lines.find((line) => line.trim().length >= 3);
|
||||
return candidate?.trim().slice(0, 120) ?? null;
|
||||
}
|
||||
|
||||
function parseLineItems(lines: string[]): ReceiptLineItem[] {
|
||||
const items: ReceiptLineItem[] = [];
|
||||
|
||||
for (const [index, rawLine] of lines.entries()) {
|
||||
const line = rawLine.replace(/\s+/g, " ").trim();
|
||||
if (line.length < 5 || NON_ITEM_LINE.test(line)) continue;
|
||||
|
||||
const match = line.match(/^(.{2,}?)\s+\$?(-?[\d,]+\.\d{2})$/);
|
||||
if (!match?.[1] || !match[2]) continue;
|
||||
|
||||
const amount = Number(match[2].replace(/,/g, ""));
|
||||
const name = match[1]
|
||||
.replace(/^\d+\s*[xX]\s+/, "")
|
||||
.replace(/\s+\d+\s*[xX]\s*$/, "")
|
||||
.trim();
|
||||
|
||||
if (!Number.isFinite(amount) || amount <= 0 || name.length < 2) continue;
|
||||
|
||||
items.push({
|
||||
id: `${index}-${name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${amount.toFixed(2)}`,
|
||||
name: name.slice(0, 80),
|
||||
amount,
|
||||
rawLine,
|
||||
});
|
||||
}
|
||||
|
||||
return items.slice(0, 30);
|
||||
}
|
||||
|
||||
export function parseReceiptText(text: string): ReceiptParseResult {
|
||||
const normalized = text.replace(/\r/g, "\n").trim();
|
||||
const rawLines = normalized
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
amount: parseAmount(normalized),
|
||||
date: parseDate(normalized),
|
||||
subtotal: parseFirstMatchingAmount(normalized, SUBTOTAL_PATTERNS),
|
||||
tax: parseFirstMatchingAmount(normalized, TAX_PATTERNS),
|
||||
vendor: parseVendor(rawLines),
|
||||
items: parseLineItems(rawLines),
|
||||
rawLines,
|
||||
};
|
||||
}
|
||||
export {
|
||||
parseReceiptText,
|
||||
type ReceiptLineItem,
|
||||
type ReceiptParseResult,
|
||||
} from "@beenvoice/domain/receipt-parse";
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
export type ClockOutOutcome =
|
||||
| "linked_to_invoice"
|
||||
| "saved_no_invoice"
|
||||
| "saved_no_client"
|
||||
| "zero_hours";
|
||||
import {
|
||||
DEFAULT_CLOCK_DESCRIPTION,
|
||||
LEGACY_DEFAULT_CLOCK_DESCRIPTION,
|
||||
} from "@beenvoice/domain/time-clock";
|
||||
import type { ClockOutOutcome } from "@beenvoice/domain/time-clock";
|
||||
|
||||
export const DEFAULT_CLOCK_DESCRIPTION = "Clock In";
|
||||
|
||||
/** Stored on entries clocked in before empty descriptions were allowed. */
|
||||
export const LEGACY_DEFAULT_CLOCK_DESCRIPTION = "Professional services";
|
||||
export {
|
||||
DEFAULT_CLOCK_DESCRIPTION,
|
||||
formatElapsedHoursMinutes,
|
||||
formatElapsedSeconds,
|
||||
LEGACY_DEFAULT_CLOCK_DESCRIPTION,
|
||||
type ClockOutOutcome,
|
||||
} from "@beenvoice/domain/time-clock";
|
||||
|
||||
export function resolveClockDescription(description: string | null | undefined): string {
|
||||
const trimmed = description?.trim();
|
||||
@@ -26,20 +29,6 @@ export function formatRunningTimerLabel(description?: string | null): string {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function formatElapsedSeconds(seconds: number): string {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
return [h, m, s].map((v) => String(v).padStart(2, "0")).join(":");
|
||||
}
|
||||
|
||||
/** Hours and minutes only — for Live Activity / compact displays. */
|
||||
export function formatElapsedHoursMinutes(seconds: number): string {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
return `${h}:${String(m).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function resolveEffectiveHourlyRate(
|
||||
rateText: string,
|
||||
clientDefaultRate?: number | null,
|
||||
|
||||
+52
-42
@@ -1,67 +1,77 @@
|
||||
{
|
||||
"name": "beenvoice-app",
|
||||
"name": "@beenvoice/mobile",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@better-auth/expo": "^1.6.19",
|
||||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
"@expo-google-fonts/playfair-display": "^0.4.2",
|
||||
"@expo/ui": "~57.0.11",
|
||||
"@expo/vector-icons": "^15.1.1",
|
||||
"@beenvoice/domain": "workspace:*",
|
||||
"@better-auth/expo": "1.6.19",
|
||||
"@expo-google-fonts/inter": "0.4.2",
|
||||
"@expo-google-fonts/playfair-display": "0.4.2",
|
||||
"@expo/ui": "57.0.11",
|
||||
"@expo/vector-icons": "15.1.1",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/datetimepicker": "9.1.0",
|
||||
"@react-native-picker/picker": "^2.11.4",
|
||||
"@tanstack/react-query": "^5.101.0",
|
||||
"@trpc/client": "^11.17.0",
|
||||
"@trpc/react-query": "^11.17.0",
|
||||
"better-auth": "^1.6.19",
|
||||
"expo": "^57.0.9",
|
||||
"expo-blur": "~57.0.2",
|
||||
"expo-build-properties": "~57.0.11",
|
||||
"expo-constants": "~57.0.11",
|
||||
"expo-dev-client": "~57.0.12",
|
||||
"expo-file-system": "~57.0.4",
|
||||
"expo-font": "~57.0.1",
|
||||
"expo-image": "~57.0.3",
|
||||
"expo-image-picker": "~57.0.10",
|
||||
"expo-linear-gradient": "~57.0.1",
|
||||
"expo-linking": "~57.0.6",
|
||||
"expo-local-authentication": "~57.0.2",
|
||||
"expo-mlkit-ocr": "^0.2.7",
|
||||
"expo-network": "~57.0.1",
|
||||
"expo-notifications": "~57.0.11",
|
||||
"expo-router": "~57.0.13",
|
||||
"expo-secure-store": "~57.0.1",
|
||||
"expo-sharing": "~57.0.12",
|
||||
"expo-splash-screen": "~57.0.6",
|
||||
"expo-status-bar": "~57.0.1",
|
||||
"expo-symbols": "~57.0.2",
|
||||
"expo-web-browser": "~57.0.2",
|
||||
"expo-widgets": "~57.0.10",
|
||||
"@react-native-picker/picker": "2.11.4",
|
||||
"@tanstack/react-query": "5.101.0",
|
||||
"@trpc/client": "11.17.0",
|
||||
"@trpc/react-query": "11.17.0",
|
||||
"@trpc/server": "11.17.0",
|
||||
"better-auth": "1.6.19",
|
||||
"expo": "57.0.13",
|
||||
"expo-blur": "57.0.2",
|
||||
"expo-build-properties": "57.0.11",
|
||||
"expo-constants": "57.0.11",
|
||||
"expo-dev-client": "57.0.12",
|
||||
"expo-file-system": "57.0.4",
|
||||
"expo-font": "57.0.1",
|
||||
"expo-image": "57.0.3",
|
||||
"expo-image-picker": "57.0.10",
|
||||
"expo-linear-gradient": "57.0.1",
|
||||
"expo-linking": "57.0.6",
|
||||
"expo-local-authentication": "57.0.2",
|
||||
"expo-mlkit-ocr": "0.2.7",
|
||||
"expo-modules-core": "57.0.11",
|
||||
"expo-network": "57.0.1",
|
||||
"expo-notifications": "57.0.11",
|
||||
"expo-router": "57.0.13",
|
||||
"expo-secure-store": "57.0.1",
|
||||
"expo-sharing": "57.0.12",
|
||||
"expo-splash-screen": "57.0.6",
|
||||
"expo-status-bar": "57.0.1",
|
||||
"expo-symbols": "57.0.2",
|
||||
"expo-web-browser": "57.0.2",
|
||||
"expo-widgets": "57.0.10",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.86.2",
|
||||
"react-native-gesture-handler": "3.2.1",
|
||||
"react-native-reanimated": "4.5.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-safe-area-context": "5.7.0",
|
||||
"react-native-screens": "4.26.2",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-web": "0.21.2",
|
||||
"react-native-webview": "13.16.1",
|
||||
"react-native-worklets": "0.10.1",
|
||||
"superjson": "^2.2.6"
|
||||
"superjson": "2.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/react": "~19.2.2",
|
||||
"react-native-svg-transformer": "^1.5.3",
|
||||
"typescript": "~6.0.3"
|
||||
"@expo/config-plugins": "57.0.8",
|
||||
"@types/bun": "1.3.14",
|
||||
"@types/react": "19.2.17",
|
||||
"react-native-svg-transformer": "1.5.3",
|
||||
"typescript": "6.0.3",
|
||||
"xcode": "3.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "expo start --dev-client --port 8082",
|
||||
"start": "expo start --dev-client --port 8082",
|
||||
"android": "expo run:android --port 8082",
|
||||
"ios": "expo run:ios --port 8082",
|
||||
"ios:release": "bash scripts/ios-release.sh",
|
||||
"ios:release:upload": "bash scripts/ios-release.sh --upload",
|
||||
"build": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "tsc --noEmit",
|
||||
"test": "bun test",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
/// <reference types="bun" />
|
||||
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
EXPENSE_CATEGORIES as domainExpenseCategories,
|
||||
formatElapsedSeconds as formatDomainElapsedSeconds,
|
||||
getEffectiveInvoiceStatus,
|
||||
} from "@beenvoice/domain";
|
||||
|
||||
import { fetchAuthCapabilities } from "../lib/auth-capabilities";
|
||||
import { EXPENSE_CATEGORIES as appExpenseCategories } from "../lib/expense-categories";
|
||||
import { getInvoiceStatus } from "../lib/invoice-status";
|
||||
import { formatElapsedSeconds as formatAppElapsedSeconds } from "../lib/time-clock";
|
||||
import { EXPENSE_CATEGORIES as webExpenseCategories } from "../../beenvoice-web/src/lib/expense-categories";
|
||||
import { getEffectiveInvoiceStatus } from "../../beenvoice-web/src/lib/invoice-status";
|
||||
import { safeCallbackPath } from "../../beenvoice-web/src/lib/safe-callback-url";
|
||||
import { safeCallbackPath } from "../../web/src/lib/safe-callback-url";
|
||||
import {
|
||||
formatElapsedSeconds as formatWebElapsedSeconds,
|
||||
normalizeOptionalId,
|
||||
} from "../../beenvoice-web/src/lib/time-clock";
|
||||
} from "../../web/src/lib/time-clock";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -85,7 +87,7 @@ describe("timer parity", () => {
|
||||
test("elapsed time formatting is identical", () => {
|
||||
for (const seconds of [0, 59, 60, 3_661, 86_399]) {
|
||||
expect(formatAppElapsedSeconds(seconds)).toBe(
|
||||
formatWebElapsedSeconds(seconds),
|
||||
formatDomainElapsedSeconds(seconds),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -99,6 +101,6 @@ describe("timer parity", () => {
|
||||
|
||||
describe("expense parity", () => {
|
||||
test("web and mobile expose the same category vocabulary", () => {
|
||||
expect([...appExpenseCategories]).toEqual([...webExpenseCategories]);
|
||||
expect([...appExpenseCategories]).toEqual([...domainExpenseCategories]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"~/*": ["../beenvoice-web/src/*"],
|
||||
"src/*": ["../beenvoice-web/src/*"],
|
||||
"beenvoice/*": ["../beenvoice-web/src/*"]
|
||||
"~/*": ["../web/src/*"],
|
||||
"src/*": ["../web/src/*"],
|
||||
"beenvoice/*": ["../web/src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user