Skip to content

feat(reminders): leave alarm via AlarmKit on iOS 26+#64

Open
JamieRuderman wants to merge 9 commits into
feat/go-focused-tripfrom
claude/smart-trip-leave-alarm-lASlY
Open

feat(reminders): leave alarm via AlarmKit on iOS 26+#64
JamieRuderman wants to merge 9 commits into
feat/go-focused-tripfrom
claude/smart-trip-leave-alarm-lASlY

Conversation

@JamieRuderman
Copy link
Copy Markdown
Owner

Summary

Schedules the focused-trip leave reminder as a real Apple AlarmKit alarm on iOS 26+ so it breaks through Silent Mode / Focus, instead of a local notification that's easy to miss. Everywhere else (Android, web, AlarmKit unavailable/denied, or any scheduling failure) it falls back to the existing local notification. The alarm replaces the notification — never both, so the user gets a single alert.

Stacked on #58 (feat/go-focused-trip). This PR ports the AlarmKit logic into that PR's new architecture (notificationScheduler.ts + the focused-trip model), not the old departureReminder.ts. Review/merge after #58.

How it works

  • src/lib/native/leaveAlarm.ts (new) — thin, mockable wrapper over @capgo/capacitor-alarm: availability/auth checks, scheduleLeaveAlarm, cancelLeaveAlarm, and the pure decideReminderChannel. AlarmKit is scoped to iOS only on purpose — the plugin's Android path just hands off to the system Clock app (uncancellable) and doesn't fit our auto-scheduled, per-trip, cancellable model.
  • useFocusedTrip.tsarmAndPersistReminder prefers a Leave Alarm and persists its id on FocusedTripReminder; cancel/clear/disarm retire both channels. notificationScheduler.ts stays a pure primitive (untouched).
  • UI: the active reminder pill shows "Leave alarm" (vs the implicit notification); NSAlarmKitUsageDescription added to Info.plist; en + es copy added.

Why @Capgo (not a hand-rolled Swift plugin)

This is a Capacitor 8 hybrid app; AlarmKit is native-only. @capgo/capacitor-alarm (v8.1.2, Capacitor‑8 aligned, MPL‑2.0, actively maintained) gives AlarmKit + cancelAlarm + permissions with no custom Swift to maintain or verify, and sidesteps the AlarmKit widget-extension question.

Review fixes already applied

  • Date safety: createAlarm only takes a clock time and fires at the next occurrence of that HH:MM — it can't target a calendar date. scheduleLeaveAlarm now bails to the dated notification unless fireAt is the next occurrence of its own clock time (alarmFiresOnIntendedDay), so a weekend trip focused on a weekday no longer fires days early.
  • Stale-focus race: after scheduling (which may block on a permission prompt) the focus is re-read; if it changed, the freshly scheduled channel is rolled back instead of resurrecting a stale/cleared trip.
  • Permission gate: notification permission is requested only on the notification fallback, so an iOS user who denied notifications but authorized AlarmKit can still set a Leave Alarm.

Android

Keeps the existing notification. A true break-through alarm there needs USE_FULL_SCREEN_INTENT, which Android 14+ won't grant a transit app by default.

Test plan

  • npm run typecheck, npm run lint, npm run build
  • npm run test:unit — 99 passing (new coverage: channel decision, day-safety helper, alarm scheduling + off-day/denied/unavailable fallbacks)
  • Manual (Mac + Xcode 26, can't run in CI): npx cap sync ios; verify the wrapper against the installed plugin API; on an iOS 26 device confirm grant → alarm fires at the leave time labeled "Leave for SMART train" → cancel/replace; on iOS 15–25 confirm fallback to a notification with no crash; on Android confirm the notification still schedules/cancels.

Known limitation

For trips not departing within the next-occurrence window, iOS uses the dated notification rather than an alarm (the "Leave alarm" label only appears when a true alarm was scheduled). Supporting alarms for arbitrary future dates would need a custom AlarmKit bridge taking an absolute Date (the plugin can't) — possible follow-up.

https://claude.ai/code/session_015t5VmBqUDZ77s7WHZdvukq


Generated by Claude Code

claude added 2 commits June 6, 2026 14:07
Schedule the focused-trip leave reminder as a real Apple AlarmKit alarm on
iOS 26+ so it breaks through Silent Mode / Focus, instead of a local
notification that's easy to miss. Falls back to the existing notification
everywhere else (Android, web, AlarmKit unavailable/denied, or any
scheduling error). The alarm replaces the notification — never both.

Stacked on the go-focused-trip refactor: the AlarmKit preference lives in
the focused-trip orchestration (useFocusedTrip) plus a thin iOS-only
wrapper; notificationScheduler.ts stays a pure primitive.

- Add @capgo/capacitor-alarm + src/lib/native/leaveAlarm.ts wrapper
  (availability/auth/schedule/cancel + pure decideReminderChannel).
- armAndPersistReminder prefers a Leave Alarm, scheduling the new channel
  before retiring the old one so a failed (re)schedule never leaves the
  user with no reminder; persist the alarm id on FocusedTripReminder.
- Cancel/clear/disarm retire both channels (notification + alarm).
- Label the active pill "Leave alarm" when AlarmKit-backed (en + es).
- Add NSAlarmKitUsageDescription to Info.plist.
- Vitest coverage for the channel decision and alarm scheduling/fallback.

Android keeps the notification: the plugin's Android path only hands off to
the system Clock app (uncancellable), and true break-through alarms require
USE_FULL_SCREEN_INTENT, which Android 14+ won't grant a transit app by
default.
- Date-safety (high): @Capgo's createAlarm only takes a clock time and fires
  at the NEXT occurrence of that HH:MM — it can't target a calendar date. A
  weekend trip focused on a weekday (serviceDate = nextServiceDate) or a
  "tomorrow" departure whose time recurs earlier today would have fired days
  early. scheduleLeaveAlarm now bails (→ dated notification fallback) unless
  fireAt is the next occurrence of its own clock time (alarmFiresOnIntendedDay).
- Stale-focus race: a permission prompt can block long enough for the user to
  Stop / switch trains / the trip to auto-clear. armAndPersistReminder now
  re-reads the focus after scheduling and, if it changed, rolls back the
  freshly scheduled alarm/notification instead of resurrecting the stale trip.
- Permission gate: notification permission is now requested only on the
  notification fallback path, so an iOS user who denied notifications but
  authorized AlarmKit can still set a Leave Alarm.

Adds unit coverage for the day-safety helper and the off-day fallback.
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
smart-trip Ready Ready Preview, Comment Jun 6, 2026 4:03pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants