* [AI] Add isNativeMobileApp platform flag and gate web-only UI behind it
The native Capacitor builds were detected ad hoc with 'Capacitor' in window.
Expose a real isNativeMobileApp flag from shared/platform (false on electron
and the API) based on Capacitor.isNativePlatform(), which stays false on the
plain web even once @capacitor/core is bundled there.
Use it to skip what has no meaning in an App Store build: service worker
registration (assets ship in the bundle and update via the store), the
"new version of Actual is available" notification, the "learn how to set
up a server" hint on the server config screen, and the "Use the current
domain" shortcut, whose origin is the capacitor:// scheme.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Gate the GitHub release check itself in the native mobile apps
Skipping the fetch in getLatestVersion covers the update notification, the
Settings "Updates" row and the GitHub rate limit in one place, so the
per-consumer gate in FinancesApp goes away. Also inline the one-use type
alias in platform.ts and tighten the service worker comment.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Unregister any service worker left by earlier native mobile builds
Skipping registration alone leaves a worker installed by a previous native
build in control of page loads, serving the old precached bundle after a
store update. Drop existing registrations on native startup only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Revert "Unregister any service worker left by earlier native mobile builds"
The only native builds that ever registered a service worker were TestFlight
test builds, so there are no store users with a stale worker to clean up.
This reverts commit 2eb95f623f3bc0ba7a8dd7b0e2b94b6a4ebc0c0e.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Sync and re-check the server when the mobile app resumes
The Capacitor apps only synced on an interval or a manual pull, so returning
to the app after a while showed stale data. `useOnVisible` now also listens to
`@capacitor/app` `appStateChange` on native platforms, so the existing
visibility-driven sync and server-version check run on resume too. User data
is refreshed first so the online/offline status reflects the server's
reachability.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Track server reachability from sync events everywhere
The online/offline reconciliation lived in LoggedInUser, which the narrow
(mobile) layout never renders, so the status never updated there. Move it
into the global sync-event listener so every layout keeps the status in step
with sync results, and drop the extra user-data fetch from the visibility
callback. Also fold the native app-state listener into the existing
visibility effect and trim the test mocks.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Use the isNativeMobileApp platform flag for the resume listener
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Apply CodeRabbit review feedback
Use a single foreground event source per platform in useOnVisible so a
resume in the native apps does not run the callback twice.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Coalesce visibility events that arrive during a running callback
A resume event that lands while the previous callback is still running was
dropped, so if that run failed the resume sync was lost. Queue one follow-up
run instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Drop a queued visibility run once the hook is cleaned up
An event coalesced while the callback was still running would start one more
run after the hook unmounted or was disabled. Clear the pending flag in both
cleanup paths.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Ignore native app-state events that land after cleanup
Capacitor removes listeners asynchronously, so an appStateChange arriving
between cleanup and removal could still run the callback. Flag the listener
as removed in cleanup and skip events after that.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Simplify the resume listener and guard the reachability refresh
Collapse useOnVisible into one effect with a shared active flag and a loop
for the coalesced follow-up run, so both platforms share the same cleanup.
Refresh user data one request at a time when sync errors arrive in a burst.
Trim the test scaffolding.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Apply CodeRabbit review feedback
Keep a queued visibility run when the callback rejects, and drop the
in-flight guard on the reachability refresh so a sync error that lands
mid-validation is not lost and a stalled request cannot block later ones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Return useOnVisible to the minimal native listener
Drop the per-platform event source, the coalesced follow-up run and the
cleanup guards added during review. The hook keeps its original logic and
only adds the native app-state listener next to it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Restore LoggedInUser to its original sync handling
Keep the title bar's own user-data refresh so it stays in "Connecting..."
until validation completes. The global sync-events listener still covers the
mobile layout, which never renders this component.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Leave reachability reconciliation to the global sync listener
LoggedInUser only tracks sync start/stop for its loading state; the
re-fetch on a contradicting sync result lives in sync-events alone. The
status text may lag by one validate request until the store updates.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Resolve follow-redirects to 1.16.2 as on master
The merge conflict resolution kept the lockfile entry from the branch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Add native haptic feedback to the Capacitor mobile apps
The calculator keypad relied on navigator.vibrate, which iOS does not
support. Route keypad presses, swipe-action and pull-to-refresh threshold
crossings through a shared helper that uses @capacitor/haptics inside the
native apps and falls back to the Vibration API elsewhere.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eMnLyeJnMJFhQHDcCG
* [AI] Extract pull-to-refresh haptic into a named hook and drop helper test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eMnLyeJnMJFhQHDcCG
* [AI] Keep swipe-action revealed state in sync when the spring start is a no-op
When a swipe overshoots the full reveal (or close), the spring is already
at its target on release, so react-spring treats the final start as a
no-op and never calls onRest. isRevealed then stayed stale, which made the
next drag jump instead of follow the finger and skipped the threshold
haptic. Set the state at release time instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Ignore the haptics plugin dependency in knip
The native app's package.json declares @capacitor/haptics so Capacitor
links the plugin, but no JS in that package imports it, same as the
splash-screen plugin.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Start swipe drags from the spring's live position
Read x.get() when a drag starts instead of tracking an isRevealed flag.
A swipe that interrupts a snap animation now continues from where the
item actually is, and the flag (which could go stale) is no longer needed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* [AI] Rename haptics helpers for clarity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [AI] Run agent hooks and skills in Codex-based harnesses (ChatGPT desktop app)
The ChatGPT desktop app (and Codex CLI/IDE) never picked up the repo's
agent skills or lifecycle hooks:
- Skills: Codex surfaces scan .agents/skills, not .claude/skills. Add a
symlink mirror so both harness families load the same skill files.
- Hooks: Codex lifecycle hooks are feature-flagged; enable them in the
project .codex/config.toml and document the trust steps (project
.codex layer trust + one-time /hooks trust) required for them to run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMdDFdaErx9dqieTGh8ETg
* [AI] Drop .agents/skills/README.md; document the mirror in AGENTS.md instead
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMdDFdaErx9dqieTGh8ETg
* [AI] Ignore .agents/** in knip like .claude/**
The .agents/skills symlink mirror exposes skill reference files under a
path knip scanned, flagging highlight-element.js as an unused file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMdDFdaErx9dqieTGh8ETg
---------
Co-authored-by: Claude <noreply@anthropic.com>
Adds the knip tool with a monorepo-tuned config (knip.json) and a CI gate
in check.yml, plus the dead-code and dependency cleanup it surfaced. The
unused-export/type/duplicate rules are disabled so exporting an unused
symbol is allowed.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>