Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 31 additions & 23 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Scaffolded a Tauri iOS build target alongside the existing Mac/Windows desktop app, so the app can now be run in the iOS Simulator (`pnpm tauri:ios:init`, `pnpm tauri:ios:dev`) and eventually shipped to the App Store. Split `src-tauri/src/main.rs` into a shared `lib.rs` (`#[cfg_attr(mobile, tauri::mobile_entry_point)]`) with a thin binary wrapper, and added a `[lib]` crate target (`crate-type = ["staticlib", "cdylib", "rlib"]`) required for mobile builds. The native menu, self-hosted updater, and updater-triggered restart — none of which apply on iOS — are now compiled out on mobile via `#[cfg(desktop)]`; their Tauri permissions moved to a new desktop-only capabilities file (`capabilities/desktop.json`, scoped via `"platforms"`). Added `src-tauri/tauri.ios.conf.json` to override the Tauri `identifier` to the App Store bundle ID (`com.adamjolicoeur.timetraked`) on iOS only, leaving the existing desktop identifier (and its app-data dir/keychain/updater identity) untouched. Also closed a touch-accessibility gap found during the investigation: the command palette (`Cmd/Ctrl+K`) previously had no on-screen trigger, only a keyboard shortcut and a desktop-only menu item — added a header button so it's reachable on iOS, which has neither. `src/lib/platform.ts` gained an `isIOS` check (touch-point-qualified, since iPadOS spoofs a "Macintosh" user agent) used to fix `isMac` misdetecting iPhone/iPad and to gate the startup silent-update check off on iOS. Corrected `index.html`'s CSP, which allowlisted a stale `capacitor://localhost` left over from an earlier Capacitor-based plan (this project ships via Tauri). CI/App Store Connect signing automation is an explicit follow-up, not included here
— `src-tauri/src/lib.rs` (new), `src-tauri/src/main.rs`, `src-tauri/Cargo.toml`, `src-tauri/tauri.ios.conf.json` (new), `src-tauri/capabilities/default.json`, `src-tauri/capabilities/desktop.json` (new), `src/lib/platform.ts`, `src/lib/tauriElectronApiShim.ts`, `src/App.tsx`, `index.html`, `package.json`
- Manual save now shows a toast confirming the save succeeded, or reporting failure so it isn't silently swallowed. Applies to all three manual-save entry points that share the same handler: the `Cmd/Ctrl+S` keyboard shortcut, the Command Palette's "Save Changes" action, and the Electron menu's Save item. `forceSyncToDatabase` now resolves a `boolean` (`true` on full success, `false` if any save failed) instead of `void`, so callers can react to the outcome
— `src/contexts/TimeTrackingContext.tsx`, `src/App.tsx`, `src/contexts/TimeTrackingContext.forceSync.test.tsx` (new)

Expand Down
22 changes: 14 additions & 8 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,23 @@
<meta name="mobile-web-app-capable" content="yes" />
<meta name="application-name" content="Timetraked" />

<!-- Content Security Policy -->
<!-- capacitor://localhost is included for WKWebView (iOS Capacitor) -->
<!-- Content Security Policy for the web/PWA build. The Tauri desktop/iOS
builds are served over the app's own origin (tauri://localhost on
macOS/iOS, http://tauri.localhost on Windows), which 'self' already
covers — Tauri also injects its own CSP header (from
src-tauri/tauri.conf.json's app.security.csp) at runtime, so no
Tauri-specific scheme needs to be listed here. (This previously
allowlisted capacitor://localhost, left over from an earlier
Capacitor-based plan; this project ships via Tauri, not Capacitor.) -->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self' capacitor://localhost;
script-src 'self' 'unsafe-inline' capacitor://localhost;
style-src 'self' 'unsafe-inline' capacitor://localhost;
connect-src 'self' capacitor://localhost https://*.supabase.co wss://*.supabase.co https://generativelanguage.googleapis.com;
font-src 'self' data: capacitor://localhost;
img-src 'self' data: blob: capacitor://localhost;
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' https://*.supabase.co wss://*.supabase.co https://generativelanguage.googleapis.com;
font-src 'self' data:;
img-src 'self' data: blob:;
frame-src 'none';
object-src 'none';
base-uri 'self';
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:ios:init": "tauri ios init",
"tauri:ios:dev": "tauri ios dev",
"tauri:ios:build": "tauri ios build",
"db:migrate": "tsx server/migrate.ts",
"db:seed": "tsx server/seed.ts",
"server:dev": "tsx watch server/index.ts",
Expand Down
10 changes: 8 additions & 2 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@ version = "1.10.1"
description = "A time tracking application built for freelancers, consultants, and professionals."
edition = "2021"

[lib]
name = "timetraked_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-updater = "2"
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2"
tauri-plugin-process = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
regex = "1"
chrono = "0.4"
tokio = { version = "1", features = ["fs", "sync", "time", "rt", "macros"] }

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2"
tauri-plugin-process = "2"

[dev-dependencies]
tempfile = "3"
tokio = { version = "1", features = ["test-util"] }
10 changes: 2 additions & 8 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capabilities for the main window",
"description": "Cross-platform capabilities for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
"dialog:default",
"updater:default",
"process:allow-restart"
]
"permissions": ["core:default", "opener:default", "dialog:default"]
}
8 changes: 8 additions & 0 deletions src-tauri/capabilities/desktop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop",
"description": "Desktop-only capabilities: self-hosted auto-update and updater-triggered restart, neither of which apply on iOS (App Store owns distribution/updates)",
"windows": ["main"],
"platforms": ["macOS", "windows", "linux"],
"permissions": ["updater:default", "process:allow-restart"]
}
44 changes: 44 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
mod backup;
#[cfg(desktop)]
mod menu;
mod quit_flush;

use backup::BackupState;
use quit_flush::QuitState;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.manage(BackupState::default())
.manage(QuitState::default())
.invoke_handler(tauri::generate_handler![
backup::backup_write,
backup::backup_list,
backup::backup_read,
quit_flush::before_quit_flush_done,
])
.on_window_event(quit_flush::handle_window_event);

// Native menu bar, self-hosted updater, and process-restart-on-update are
// desktop concepts with no iOS analog: there's no menu bar on iOS, and
// App Store review requires distribution/update flows to go through the
// App Store rather than a self-hosted updater plugin.
#[cfg(desktop)]
let builder = builder
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
let app_menu = menu::build_menu(app)?;
app.set_menu(app_menu)?;
Ok(())
})
.on_menu_event(menu::handle_menu_event);

let app = builder
.build(tauri::generate_context!())
.expect("error while building tauri application");

app.run(quit_flush::handle_run_event);
}
32 changes: 1 addition & 31 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod backup;
mod menu;
mod quit_flush;

use backup::BackupState;
use quit_flush::QuitState;

fn main() {
let app = tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.manage(BackupState::default())
.manage(QuitState::default())
.invoke_handler(tauri::generate_handler![
backup::backup_write,
backup::backup_list,
backup::backup_read,
quit_flush::before_quit_flush_done,
])
.setup(|app| {
let app_menu = menu::build_menu(app)?;
app.set_menu(app_menu)?;
Ok(())
})
.on_menu_event(menu::handle_menu_event)
.on_window_event(quit_flush::handle_window_event)
.build(tauri::generate_context!())
.expect("error while building tauri application");

app.run(quit_flush::handle_run_event);
timetraked_lib::run();
}
4 changes: 4 additions & 0 deletions src-tauri/tauri.ios.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://schema.tauri.app/config/2",
"identifier": "com.adamjolicoeur.timetraked"
}
12 changes: 11 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { AppSidebar } from "@/components/AppSidebar";
import { CommandPalette } from "@/components/CommandPalette";
import { KeyboardShortcutsDialog } from "@/components/KeyboardShortcutsDialog";
import { Button } from "@/components/ui/button";
import { Keyboard } from "lucide-react";
import { Keyboard, Search } from "lucide-react";
import {
SidebarInset,
SidebarProvider,
Expand Down Expand Up @@ -114,6 +114,16 @@ const AppShell = () => {
{badge}
<div className="ml-auto flex items-center gap-2">
{actions}
<Button
variant="ghost"
size="icon"
className="size-7"
onClick={openCommandPalette}
aria-label="Command palette"
title="Command palette (Cmd/Ctrl+K)"
>
<Search className="size-4" />
</Button>
<Button
variant="ghost"
size="icon"
Expand Down
11 changes: 9 additions & 2 deletions src/lib/platform.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
const maxTouchPoints = typeof navigator !== "undefined" ? navigator.maxTouchPoints : 0;

// iPadOS has spoofed a "Macintosh" user agent since iPadOS 13, so telling an
// iPad apart from a real Mac requires the touch-point check too (real Macs
// report 0 touch points).
export const isIOS = /iPhone|iPad|iPod/.test(userAgent) || (/Macintosh/.test(userAgent) && maxTouchPoints > 1);

// Detects macOS so shortcut hints can render the platform-appropriate modifier
// glyph (⌘ vs Ctrl) in the command palette and shortcuts help dialog.
export const isMac =
typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
export const isMac = /Mac/.test(userAgent) && !isIOS;

export const modKey = isMac ? "⌘" : "Ctrl";
5 changes: 4 additions & 1 deletion src/lib/tauriElectronApiShim.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { checkForUpdatesManual, checkForUpdatesSilent } from "@/lib/tauriUpdater";
import { isIOS } from "@/lib/platform";

// Populates window.electronAPI with the same shape electron/preload.ts used
// to expose, so useElectronBackup.ts and useElectronMenuActions.ts work
Expand Down Expand Up @@ -53,7 +54,9 @@ export function installTauriElectronApiShim(): void {
},
};

if (import.meta.env.PROD) {
// Self-hosted updates have no iOS equivalent — App Store review requires
// distribution/updates to go through the App Store instead.
if (import.meta.env.PROD && !isIOS) {
void checkForUpdatesSilent();
}
}
Loading