Skip to content

feat(companion): local due-back reminders for checked-out bookings - #2747

Open
carlosvirreira wants to merge 3 commits into
mainfrom
feat/companion-booking-reminders
Open

feat(companion): local due-back reminders for checked-out bookings#2747
carlosvirreira wants to merge 3 commits into
mainfrom
feat/companion-booking-reminders

Conversation

@carlosvirreira

@carlosvirreira carlosvirreira commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Implements the N3 launch spec: two local notifications per booking checked out from this device — due soon (1h before) and due now — scheduled the moment a checkout succeeds and torn down the moment the booking stops being out. The core promise: reminders can never nag about gear that is already back.

How it holds that promise

  • Derived, never fire-and-forget. Every sync re-fetches the booking and rebuilds its reminders; a reconcile pass runs on every genuine return to the foreground (leaving the foreground resets the debounce). Returned/extended/cancelled on web → healed on next open.
  • Authoritative-gone healing. A 404/403 on the sync fetch (booking deleted on web, workspace access lost) cancels + untracks immediately; only network/timeout failures leave state untouched for the next pass. apiFetch now additionally returns the HTTP status (additive) to make that distinction possible.
  • Sign-out clears everything — after it, fetches would 401 forever and tracked records could never heal.
  • Serialized state, unblocked cancels. Tracked-map mutations run on a promise queue (interleaved read-modify-write would orphan notification ids); the booking fetch and the interactive permission dialog run outside the queue so a check-in's cancel is never stuck behind a timeout or an open prompt. If persisting ids fails after scheduling, the schedules are rolled back — storage failure cannot create uncancellable reminders.
  • Crash-proof on older builds. expo-notifications calls requireNativeModule at module top level, so a static import would crash any pre-notifications binary at startup (verified against the Metro config — inline requires are off). All runtime access goes through a lazy guarded require; pre-notifications dev clients no-op.
  • iOS 64-cap respected — future-dated reminders only, furthest-out dropped first, warned unconditionally (lands in Sentry breadcrumbs).
  • Permission asked in context (first checkout / Settings toggle), never on launch. No app cron — local notifications scheduled at checkout; the OS delivers them.

Wiring

Schedule on: scanner fulfil-and-checkout, detail full + partial checkout (partial may flip RESERVED→ONGOING; sync is idempotent and only schedules when genuinely out).
Cancel on: full check-in, complete partial check-in (scanner + detail), cancel, archive, delete, sign-out.
Tap → opens the booking via pushIntoTab (anchored back-stack), switching workspace first when the reminder belongs to another org; cold-start and warm taps are deduped.
Settings: master toggle mirroring the scan-sound pattern; OFF cancels all but keeps the tracked map so ON restores via reconcile; denied OS permission points to device settings.

Review hardening (pre-PR adversarial pass, 20 agents)

15 confirmed findings fixed before this PR: the import-time crash, 404/403 never healing, sign-out leak, cross-workspace tap, bare router.push vs pushIntoTab, permission-dialog head-of-line blocking, cap counting already-fired reminders, silent production drops, write-failure orphaning, debounce missing the next foreground, a require cycle through auth-context, dead exports, and doc drift.

Verification

  • typecheck, lint, react-doctor (0 errors) all green.
  • Sim/device pass deliberately deferred (needs a dev build containing expo-notifications); test plan from the spec: seed a booking due in ~2 min → check out → background → due-soon fires → check in → getAllScheduledNotificationsAsync shows the pair cancelled; extend due time → reconcile reschedules.

Rollout

Needs one new native build (expo-notifications). Everything after that build is plain JS and OTA-patchable — including a kill-switch via eas update if it misbehaves in the wild (pairs with #2740).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added booking reminders for upcoming and due-back bookings.
    • Added a Settings toggle to enable/disable booking reminders, including an option to open device notification settings when permission is missing.
    • Reminder notifications now schedule/cancel automatically as bookings are checked out/in, cancelled, archived, or deleted.
    • Tapping a reminder opens the relevant booking, including from a cold app launch.
  • Bug Fixes
    • Prevent stale reminders after sign-out or when bookings are no longer available.
    • Improved reminder scheduling and tap handling across app foreground/background transitions.

Two local notifications per booking checked out from this device — due soon
(1h before) and due now — scheduled at checkout and torn down the moment the
booking stops being out. Nothing is fire-and-forget: the scheduled set is
always re-derived from the server, so reminders can never nag about gear
that is already back.

Architecture (lib/reminders/):
- plan.ts: pure, dependency-free planner (single source of truth for what
  fires and when; absolute-date triggers so DST cannot shift a reminder).
- service.ts: schedule/cancel/reconcile runtime. Every sync re-fetches the
  booking; 404/403 is authoritative 'gone' (cancels + untracks, healing web
  deletes and lost workspace access) while network failures leave state
  untouched for the next reconcile. Map mutations are serialized on a
  promise queue; the fetch and the interactive permission dialog run
  OUTSIDE it so a check-in's cancel never waits behind a timeout or an
  open prompt. Persisting-after-scheduling failures roll the schedules
  back so storage errors cannot create uncancellable reminders. Pending
  count stays under iOS's 64-notification cap (future-dated only,
  furthest-out dropped first, never silently).
- notifications-native.ts: lazy guarded require — expo-notifications calls
  requireNativeModule at module top level, so a static import would crash
  pre-notifications builds (older dev clients) at startup.
- use-booking-reminders.ts: init + foreground reconcile (leaving the
  foreground marks the debounce stale so every genuine return reconciles)
  + tap handling with cold/warm dedupe, workspace switch when the reminder
  belongs to another org, and pushIntoTab-anchored navigation.

Wiring:
- Schedule on checkout success (scanner fulfil-and-checkout, detail full
  and partial checkout). Cancel on full check-in, complete partial
  check-in, cancel, archive, delete, and on sign-out (fetches would 401
  forever after it, so tracked records could never heal).
- Settings: master 'Booking reminders' toggle mirroring the scan-sound
  pattern; off cancels everything but keeps the tracked map so on restores
  via reconcile; a denied OS permission points at device settings.
- apiFetch now also returns the HTTP status (additive) so callers can tell
  an authoritative 404/403 from a network failure.
- Permission is asked in context (first checkout / the toggle), never on
  launch. No app cron: due-times are local notifications scheduled at
  checkout; the OS delivers them.

Needs a new native build (expo-notifications); all logic after that build
is plain JS and OTA-patchable.
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

🩺 React Doctor — companion

Findings on the files changed by this PR:

  • 0 errors
  • 10 warnings — advisory
⚠️ 10 warnings (click to expand)
  • react-doctor/rn-no-legacy-expo-packages (3)
    • apps/companion/app/(tabs)/settings.tsx:15
    • apps/companion/app/(tabs)/scanner.tsx:20
    • apps/companion/app/(tabs)/bookings/[id].tsx:24
  • react-doctor/no-giant-component (3)
    • apps/companion/app/(tabs)/settings.tsx:49
    • apps/companion/app/(tabs)/scanner.tsx:175
    • apps/companion/app/(tabs)/bookings/[id].tsx:108
  • react-doctor/prefer-useReducer (2)
    • apps/companion/app/(tabs)/scanner.tsx:175
    • apps/companion/app/(tabs)/bookings/[id].tsx:108
  • react-doctor/no-cascading-set-state (1)
    • apps/companion/lib/auth-context.tsx:42
  • react-doctor/rn-prefer-reanimated (1)
    • apps/companion/app/(tabs)/scanner.tsx:10

Run locally with pnpm companion:doctor for a full scan, or cd apps/companion && pnpm exec react-doctor . --diff for the same diff-only view.

Comment thread apps/companion/lib/reminders/service.ts Fixed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9c0ea19a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/companion/lib/reminders/use-booking-reminders.ts Outdated
Comment thread apps/companion/lib/reminders/service.ts
Comment thread apps/companion/lib/auth-context.tsx
…, tap timing

- Generation guard: cancellations (check-in, sign-out) bump a per-booking /
  global counter synchronously; a sync captures both before its fetch and
  its queued apply aborts if either moved. Closes the race where a
  reconcile fetch reading ONGOING lands after a check-in's cancel and
  resurrects reminders for returned gear. isEnabled is also re-checked at
  apply time for the toggle-off flavour of the same race.
- Clear reminders on EVERY authenticated -> signed-out transition (session
  expiry, revocation, refresh failure via onAuthStateChange), not only the
  explicit sign-out button — after any of them reconciles 401 forever and a
  later user of the device must not see the previous account's bookings.
- Cold-start reminder taps: wait for the workspace list and mark the
  response handled only after navigation ran, so a tap arriving before
  OrgProvider finishes loading is retried (previously it was consumed and
  the booking never opened). Warm taps during loading are left unhandled
  for the cold-start path to pick up.
- Drop an always-true negation flagged by code scanning.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5987f7a1-d213-491a-91f9-9851eec375bc

📥 Commits

Reviewing files that changed from the base of the PR and between d16d841 and b351b13.

📒 Files selected for processing (2)
  • apps/companion/lib/auth-context.tsx
  • apps/companion/lib/reminders/service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/companion/lib/auth-context.tsx
  • apps/companion/lib/reminders/service.ts

Walkthrough

Adds booking due-back reminders to the companion app, including planning, native notification scheduling, persisted preferences, lifecycle reconciliation, notification-tap navigation, booking mutation integration, sign-out cleanup, and Expo configuration.

Changes

Booking reminders

Layer / File(s) Summary
Reminder contracts and native boundary
apps/companion/lib/reminders/*, apps/companion/app.json, apps/companion/package.json
Defines reminder plans and payloads, lazily loads native notifications, exposes the reminders API, and configures expo-notifications.
HTTP status propagation
apps/companion/lib/api/client.ts
Adds HTTP status values to apiFetch responses across success and error paths.
Reminder scheduling runtime
apps/companion/lib/reminders/service.ts
Persists tracked bookings and preferences, schedules and cancels reminders, reconciles foreground state, handles permissions, and enforces notification caps.
App lifecycle and notification taps
apps/companion/lib/reminders/use-booking-reminders.ts, apps/companion/app/_layout.tsx
Initializes reminders, reconciles on app-state changes, and routes cold- and warm-start notification taps to bookings.
Booking controls and reminder settings
apps/companion/app/(tabs)/bookings/[id].tsx, apps/companion/app/(tabs)/scanner.tsx, apps/companion/app/(tabs)/settings.tsx
Synchronizes or cancels reminders across booking mutations and adds a persisted reminders toggle with OS settings access.
Sign-out reminder cleanup
apps/companion/lib/auth-context.tsx
Clears scheduled reminders before explicit or detected authentication sign-out.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BookingScreen
  participant ReminderService
  participant API
  participant Notifications
  BookingScreen->>ReminderService: sync after checkout
  ReminderService->>API: fetch booking state
  API-->>ReminderService: booking and HTTP status
  ReminderService->>Notifications: schedule due-back reminders
  Notifications-->>BookingScreen: reminder tap
  BookingScreen->>BookingScreen: open booking route
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding local due-back reminders for checked-out bookings in the companion app.
Docstring Coverage ✅ Passed Docstring coverage is 86.96% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/companion-booking-reminders

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/companion/app/(tabs)/settings.tsx (1)

308-361: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Switch state can drift from actual OS permission.

remindersOn only reflects the persisted preference, not the live OS notification permission. If a user grants permission once and later revokes it from device Settings without reopening this screen, the switch still shows ON while no reminders will actually fire, and the mismatch only surfaces the next time the user explicitly toggles.

Consider re-querying permission status on screen focus (exporting a lightweight permission-check from service.ts) so the switch can reflect reality without requiring a toggle round-trip.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/companion/app/`(tabs)/settings.tsx around lines 308 - 361, Update the
settings screen’s focus handling around remindersOn to re-query live OS
notification permission whenever the screen gains focus, using a lightweight
exported permission-check helper from service.ts. Synchronize the switch state
with the permission result while preserving the existing persisted preference
and toggle behavior in setRemindersEnabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/companion/lib/auth-context.tsx`:
- Around line 65-68: Update clearAllBookingReminders and the signOut flow in
auth-context so reminder cancellation or storage failures are not swallowed as
success. Propagate the failure or return an explicit success result, and make
signOut handle that outcome before completing authentication sign-out, ensuring
cleanup is confirmed before the session is cleared.

---

Nitpick comments:
In `@apps/companion/app/`(tabs)/settings.tsx:
- Around line 308-361: Update the settings screen’s focus handling around
remindersOn to re-query live OS notification permission whenever the screen
gains focus, using a lightweight exported permission-check helper from
service.ts. Synchronize the switch state with the permission result while
preserving the existing persisted preference and toggle behavior in
setRemindersEnabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b7d24e1-527e-4817-91fc-dcf26355869f

📥 Commits

Reviewing files that changed from the base of the PR and between 94d86ea and a9c0ea1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • apps/companion/app.json
  • apps/companion/app/(tabs)/bookings/[id].tsx
  • apps/companion/app/(tabs)/scanner.tsx
  • apps/companion/app/(tabs)/settings.tsx
  • apps/companion/app/_layout.tsx
  • apps/companion/lib/api/client.ts
  • apps/companion/lib/auth-context.tsx
  • apps/companion/lib/reminders/index.ts
  • apps/companion/lib/reminders/notifications-native.ts
  • apps/companion/lib/reminders/plan.ts
  • apps/companion/lib/reminders/service.ts
  • apps/companion/lib/reminders/use-booking-reminders.ts
  • apps/companion/package.json

Comment thread apps/companion/lib/auth-context.tsx
Comment thread apps/companion/lib/reminders/service.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/companion/lib/auth-context.tsx`:
- Around line 42-48: Register the Supabase auth state listener before calling
getSession in the useEffect, and use the listener’s INITIAL_SESSION payload to
initialize session and hadSessionRef when available. Prevent the asynchronous
getSession result from overwriting a newer auth event, while preserving the
existing setSession and reminder-transition behavior in both initialization and
subsequent auth events.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ad261d04-f27b-424c-b596-413417857dcf

📥 Commits

Reviewing files that changed from the base of the PR and between a9c0ea1 and d16d841.

📒 Files selected for processing (3)
  • apps/companion/lib/auth-context.tsx
  • apps/companion/lib/reminders/service.ts
  • apps/companion/lib/reminders/use-booking-reminders.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/companion/lib/reminders/use-booking-reminders.ts
  • apps/companion/lib/reminders/service.ts

Comment thread apps/companion/lib/auth-context.tsx
…d var

- Register onAuthStateChange BEFORE getSession() and never let the stale
  initial snapshot clobber a fresher auth event, so a sign-out/refresh
  landing during startup can't be overwritten and its reminder cleanup
  can't be missed (Supabase's INITIAL_SESSION is caught by the listener).
- Remove the now-unused destructured error in the sync fetch.
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.

1 participant