Conversation
Introduces the type system for owner-scoped auto-share rules. A rule captures "share every trip I have (and future trips) with X at permission Y." Each share spawned by a rule carries the rule's id via the new optional originRuleId field, so the server can later cascade revoke / cascade update those shares without disturbing manually created shares for the same recipient. Adds Zod schemas tripShareRuleSchema, createShareRuleSchema, and updateShareRuleSchema (all required fields validated; updates require at least one mutating field). originRuleId is optional on tripShareSchema so already-persisted shares parse cleanly.
Adds list/get/save/deleteShareRule to the StorageProvider interface, backed by a Map in InMemoryStorage (with clear() resetting it for tests). DriveStorage stubs throw "not implemented" — Drive persistence for share-rules.json will land alongside the rest of DriveStorage; until then dev / tests use the in-memory path and production trips don't reach this code. Storage returns deep-cloned rules to keep callers from mutating in-place, matching the Trip / settings handling.
Pulls the share-creation side effects (push to trip.shares, register in ShareRegistry, store unfurl snapshot, record share.create history, fire push notification) out of POST /trips/:tripId/share into a reusable helper at services/share-fanout.ts. Same behaviour as before — all 486 server tests still pass — but upcoming auto-share rule paths (rule create backfill, trip-create fan-out) now have a single seam to plug into. The helper accepts a suppressNotification flag so rule paths can fire one consolidated push for the whole rule application instead of N per-trip pushes, plus an originRuleId pass-through for cascade-revoke.
Adds the four share-rule endpoints: - GET /share-rules — owner's rules - POST /share-rules — creates rule, backfills shares across existing trips. Conflict policy is "upgrade only if stricter": a manual view-share for the recipient gets upgraded to edit when the rule grants edit, but never downgraded. Equal permission is skipped. Self-share (owner email == recipient) rejected 400; duplicate rule for the same recipient rejected 409. - PUT /share-rules/:ruleId — edits cascade onto every share with originRuleId === ruleId. Manual shares for the same recipient (no originRuleId) are left untouched. - DELETE /share-rules/:ruleId?cascade=true|false — cascade is required (400 if missing). cascade=true revokes only originRuleId-matching shares; cascade=false deletes the rule and leaves shares as orphans. Notifications collapse to one push per rule application: a single "now auto-shares with you" / "updated your access" / "stopped auto-sharing" push instead of N per-trip pushes — avoids spamming the recipient on the first activation against a busy account. Wired in app.ts behind requireAuth in drive mode. 18 new tests cover the conflict matrix, cascade cases, and ownership boundary.
POST /trips and POST /trips/import-xlsx now iterate the owner's active rules after the initial saveTrip, spawn a TripShare per rule (skipping recipients that already have a share on the trip), and re-save once. Each spawned share carries originRuleId so cascade revoke / cascade update can find it later. Behavior decisions baked in: - Per-share push fires here (not the rule-level collapse) — a single new trip shared with N recipients via N rules is N legitimate "trip shared with you" pushes to N different people. - No suppression of recipients who previously left an earlier trip; the rule means "always share with X", and a fresh trip is a fresh decision. Test locks this in.
Adds listShareRules / createShareRule / updateShareRule / deleteShareRule on ApiClient, returning the typed envelopes the server emits (rule + spawnedShareCount, rule + updatedShareCount, revokedShareCount). React Query layer: - queryKeys.shareRules - useShareRules() — list query - useCreateShareRule() — invalidates rules + trips after spawn - useUpdateShareRule() — optimistic patch of the rules cache (per CLAUDE.md), invalidates trips after cascade - useDeleteShareRule() — optimistic remove; only invalidates trips when cascade=true (no need to refetch shares when shares are intentionally left behind).
Demo mode (?demo=true) now mirrors the real backend's share-rule behaviour: - listShareRules / createShareRule / updateShareRule / deleteShareRule overrides keep state in a per-MockApiClient Map - createShareRule backfills across the demo's owned trips (skips trips already flagged via SHARED_DEMO_OVERRIDES — those are "shared with you" trips the demo user doesn't own) - The conflict policy matches the server: edit upgrades view, view-on-edit is skipped, equal permission left alone - Cascade=true on delete removes only originRuleId-tagged shares - createTrip spawns shares for every active rule so the "create a new trip with rules active" demo flow lands the same way it would in production.
Adds AutoShareRulesPanel — a card on the home dashboard with: - "Add" → CreateRuleDialog (email + permission radio + showCosts / showTodos toggles, success toast reports the spawned/upgraded count) - One row per rule (recipient + permission + an inline toggle to flip view↔edit; cascades to existing spawned shares via useUpdateShareRule's optimistic update) - Trash → DeleteRuleDialog with two destructive options: "Keep existing shares" (cascade=false) vs. "Also revoke from existing trips" (cascade=true). Required cascade query param matches the server contract. share-trip-dialog gets a small "Via auto-share rule" pill on every share row whose originRuleId is set, so the owner can see at a glance which shares come from a rule and would be cascade-affected. useCreateShareRule / useUpdateShareRule / useDeleteShareRule exported from @travel-app/api-client (was missing — caught the build).
Mirrors the desktop auto-share UI on the mobile shell: - MobileAutoShareSheet — full bottom sheet with rule list, inline permission toggle (view↔edit cascades to existing spawned shares), and an in-sheet create form (no nested dialog — the form expands inside the sheet so the user stays in flow). The destructive delete confirmation also lives inline as a panel that replaces the rule list, with the same "Keep" / "Also revoke" choice as desktop. - Entry from MobileUserMenu via a new "Auto-share with people" item; the trigger only renders when the parent passes onAutoShare, so trip-detail menus that reuse MobileUserMenu don't surface it. - mobile-share-sheet share-row gets the same "Via auto-share rule" pill as desktop (using --status-info-* tokens + text-kicker). Per CLAUDE.md desktop+mobile parity, every affordance added to the home dashboard now lands on /m too.
- README: extend the Sharing one-liner, add the four /share-rules endpoints to the API table, and append a new "Auto-share rules" bullet under the Sharing section that captures the conflict policy, cascade behaviour, and surfaces (panel on desktop, sheet on mobile, pill on the per-trip share dialog). - Welcome page: rename the share card to "Share — view, edit, or auto-share" and add a sentence about pinning a person to every trip in one tap. - Intro tour dialog: extend the share step with a sentence about auto-share rules so first-run users discover the feature.
- Convert AutoShareRulesPanel to AutoShareRulesDialog and remove it from the dashboard so it doesn't take prime real-estate from the trip list. Open it via a new "Auto-share…" item in the desktop user menu — same affordance pattern as the mobile shell. - Rename the mobile menu item to "Auto-share…" and drop the "With people" subtitle from the mobile sheet header — title is just "Auto-share" now. - Drop "in one tap" from the empty-state copy on both surfaces; it was inaccurate (creating a rule is several taps).
- Welcome share card: replace "Auto-share rules pin a person to every existing trip and every new one you create, in one tap." with "Setup Auto-share rules for all existing and future trips." - Intro tour share step: replace the auto-share sentence with "Add an auto-share rule to share all existing and future trips with someone." and call out that share links can be read-only or editable so the choice is visible to first-run users.
feat: auto-share rules — share every (existing & future) trip with someone
) * feat(server): persist auto-share rules to Drive Replaces the four DriveStorage share-rule stubs with a real implementation that reads and writes a single share-rules.json file in the user's Itinly app folder, alongside settings.json and processed-emails.json. Rules are owner-scoped and low-cardinality — always read and written together — so a single-file array is the right granularity (no per-rule file like trips). - listShareRules: reads share-rules.json, returns [] when the file doesn't exist (back-compat with users who never created a rule). - getShareRule: list + find by id. - saveShareRule: list, replace-or-append by id, write back. The upsert means re-saving an edited rule never duplicates. - deleteShareRule: list, filter, write back; returns false (without writing) when the rule was already absent so the caller can 404. Drops the "not implemented" comments and the throw stubs. The share-rule routes work unchanged — they only see StorageProvider. Adds 9 new drive-storage.test.ts cases covering empty file, list + get, create vs. update branching, replace-vs-append, and the no-op delete path. Total server test count: 516. * chore(ci): run CI on the preview branch too Adds `preview` to the CI workflow's pull_request and push branch filters so PRs targeting preview (and pushes after merge) get the same test + lint + typecheck pass that PRs targeting main get. Until now the green PR check on a preview-targeted PR was Vercel-only; this closes that gap so I can't merge a broken preview branch. version-bump stays main-only — it's tied to release tagging. --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(web): advertise auto-share inside the per-trip share dialog Adds a small CTA banner at the bottom of ShareTripDialog (desktop) and MobileShareSheet (mobile). Tapping it closes the share UI and opens the auto-share dialog/sheet with the recipient email the user already typed pre-filled into the create form — so they don't have to retype the address. Threads an `initialEmail?: string` prop through: - AutoShareRulesDialog → CreateRuleDialog (auto-opens the inner create dialog when set, useEffect picks up fresh values on each open transition) - MobileAutoShareSheet → CreateForm Banner uses the --status-info-* tokens (cyan info hue) so it reads as a discovery hint, not a warning. Hidden in the desktop dialog's "share link ready" success state to keep that moment focused. * fix(web): disable auto-share submit + delete buttons while pending Rule create + delete are not optimistic (they need server-assigned ids / a confirmed cascade count), so a user double-tapping the CTAs fires N parallel requests against the same rule. Result: rule create hits 409 on the second attempt; rule delete double-cascades / 404s. Disable both flows while the underlying mutation is in flight and swap the label to "Creating…" / "Removing…" so the action feels acknowledged. Applies to both desktop dialogs and the mobile sheet: - desktop: AutoShareRulesPanel CreateRuleDialog "Auto-share" button, DeleteRuleDialog "Keep" + "Also revoke" buttons - mobile: MobileAutoShareSheet CreateForm "Auto-share" button, DeleteRulePanel "Keep" + "Also revoke" + "Cancel" buttons * fix(web): close user menu when opening Auto-share dialog The DropdownMenuItem onSelect handler called event.preventDefault() which blocked Radix's default menu-close behaviour, leaving the dropdown visible behind the dialog. Other items in the same menu (Use mobile site, Sign out) use plain onClick and close cleanly — match their pattern. preventDefault is only useful for items that need to keep the menu state alive across an async UI step (e.g. iOS PWA install prompts), which doesn't apply here. * perf(server): parallelize Drive reads + share-rule fan-out / cascade The auto-share rule create + delete cascade were hitting Drive sequentially: ~3N round-trips for backfill (N trips), all serial. With N=15 and ~300ms per Drive op, that's the ~15-second wait we were seeing on preview. Switch to bounded-concurrency parallel writes/reads via a small mapWithConcurrency helper (cap=6, comfortably under Drive's per-user quota even when chained writes follow): - DriveStorage.listTrips: N sequential file reads → ceil(N/6) round-trips of 6-wide parallel reads - POST /share-rules backfill loop: per-trip work runs in parallel pool of 6 instead of one-at-a-time - PUT /share-rules cascade-update loop: same - DELETE /share-rules?cascade=true revoke loop: same For a 15-trip user this drops the create/revoke wait from ~15s to ~2-3s. No behaviour change — each trip remains an independent saveTrip and the registry/snapshot side effects are atomic in-memory ops. Also fix two double-tap-vulnerable buttons surfaced in the audit: - email-scan-dialog "Start Scan" — now disables on scanEmails.isPending and labels "Scanning…". Prevents accidental re-fires that burn Anthropic quota on a slow scan. - html-import-dialog "Parse" — now disables on importMutation.isPending and labels "Parsing…". Same rationale. --------- Co-authored-by: Claude <noreply@anthropic.com>
The service worker was intercepting /auth/callback navigations and re-issuing them via fetch(req), which (a) cached URLs containing the single-use OAuth `code` and (b) made the cross-site Google → callback request look like an SW retry pattern to Vercel's DDoS mitigation, contributing to firewall denials. Bumped SW_VERSION to v8 to evict any previously-cached callback HTML carrying a stale code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(mobile): tapping auto-share ad opens the auto-share sheet immediately
The auto-share advertisement inside the mobile share sheet was rendered
as a child of MobileBottomSheet. Tapping the ad called both
setAutoShareEmail("") and onClose(); the latter flips the bottom sheet
to open=false, which makes MobileBottomSheet return null and unmount
all its children — including the just-opened MobileAutoShareSheet. The
auto-share sheet's state was preserved on the parent component, so the
*next* time the share sheet opened, the auto-share sheet would mount
already-open, producing the reported "click ad → nothing → click share
→ Auto-share appears" sequence.
Move MobileAutoShareSheet to be a sibling of MobileBottomSheet so the
two sheets mount independently. Also reset autoShareEmail in the
on-open effect to defend against any future state leak.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(share): leave-trip is instant, margin under share-sheet ad, drop "with someone"
Three issues reported on the auto-share / share flow:
1. **Leaving an auto-shared trip looked like a no-op.** `useDeleteShare`
only optimistically removed from the trip's `shares` list (the
owner's view) and only invalidated that key. The recipient leaving
from their trip card got no immediate UI change — the list query
stayed stale until the next refetch, with no toast, looking like
nothing happened. Extend the hook to also drop any TripSummary with
a matching `sharedShareId` from the trips-list cache on mutate,
restore on error, and invalidate `queryKeys.trips` on settled.
2. **No spacing between the share-sheet body and the auto-share ad.**
The ad button sat flush against the toggles. Add `mt-5` for breathing
room.
3. **"with someone" is unnecessary.** It's already implicit in the rule
(the recipient is configured when you create the rule). Drop it
from both the desktop dialog title/description and the mobile sheet
empty state.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): mirror mobile trip groupings, center mobile login, clear cache on logout
Three parity / hygiene issues that landed together:
- **Desktop My Trips: Now / Upcoming / Past sections.** The desktop list
was a flat grid sorted by start date; mobile already buckets into Now,
Upcoming, and Past with the past collapsed by default. Extract the
bucketing helpers to `lib/trip-buckets.ts` so both surfaces share one
rule (today comparison against startDate/endDate, ascending for now /
upcoming, descending for past). Mobile now imports the shared helpers
instead of duplicating them.
- **Mobile login is vertically centered.** Previous layout pinned the
wordmark to `pt-16` and pushed the desktop-site link to the bottom
with `mt-auto`, leaving the buttons block in a top-anchored gap.
Adding `mt-auto` to the wordmark group splits the leftover space
evenly with the link's `mt-auto`, centering the (wordmark + buttons)
block while keeping the link at the bottom.
- **Sign-out clears cached trips.** `logout()` only reset auth state;
the React Query in-memory cache and the persisted localStorage
snapshot survived, so the next user (or the same user re-signing in)
briefly saw the previous account's trips on first paint until the
refetch completed. Add an effect in `ApiProviderSwitcher` that
watches `accessToken` and on truthy→null transition runs
`queryClient.clear()` + removes `CACHE_STORAGE_KEY` from localStorage.
Catches both manual logout and refresh-token failure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: stable preview alias setup + cite trip-buckets in parity rule
- vercel-setup.md: new "Stable preview alias" section walks through DNS,
Vercel branch-domain assignment, OAuth-relay regex extension, and the
Railway preview env CORS update needed to back a stable
preview.itinly.app URL with the `preview` branch.
- .env.example + README env table: example regex now shows the
alternation pattern (per-deploy hostnames OR a stable alias) so
future readers don't have to reverse-engineer the right shape.
- CLAUDE.md parity rule: cite `lib/trip-buckets.ts` alongside the
`useTripPermission` / `useShareLinkOwnerRedirect` hook examples — a
pure helper module also keeps desktop + mobile in sync.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): import TripSummary from api-client, not shared
`TripSummary` lives in `@travel-app/api-client`, not
`@travel-app/shared`, so the production build failed type-check with
"Module has no exported member 'TripSummary'". Other consumers
(`m/page.tsx`, `trip-list.tsx`) already use the api-client path —
match them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…apse (#246) Desktop trip list still had the legacy status-based filter (`status === "completed" || "cancelled"`) hiding trips behind a "Show completed trips (N)" footer button. Mobile already shows every trip in its date-bucket regardless of status — the collapsed Past section is enough to keep finished trips out of the way without a separate toggle. Drop the filter, the toggle, and the empty-state hint about hidden completed trips so the two surfaces match. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
2 tasks
justmarks
added a commit
that referenced
this pull request
May 10, 2026
…etachars (#248) The "Determine version bump type" step interpolated the commit message via `${{ github.event.head_commit.message }}` directly inside the run: script. GitHub substitutes that as raw text BEFORE bash parses, so any commit body containing parens, quotes, or backticks (e.g. a squash- merge body listing "(#246)" PR refs, like the one for PR #247) crashed the step with "syntax error near unexpected token '('". Pass the value via `env:` instead. Env vars route through getenv so bash never re-parses them — `"$COMMIT_MSG"` expands cleanly even when the body contains arbitrary characters. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges 18 commits accumulated on
previewsince the last main release. Shipped behind the same UI surface; no migrations required.Headlines
Auto-share rules (PRs #242, #243, #244)
TripShareRule,originRuleIdonTripShare(packages/shared)/api/v1/share-rulesCRUD + backfill + cascade, fan-out on trip create, Drive persistence (share-rules.json), in-memory + Drive storage methods,applyShareToTripextractionMockApiClientoverrides so the demo experience covers rules end-to-endService worker —
/auth/*bypass/auth/callbacknavigations viafetch(req)and caching the URL containing the single-use OAuthcode. Vercel's DDoS mitigation flagged the cross-site Google → callback round-trip as suspicious, denying withx-vercel-mitigated: deny.v8SW returns early for same-origin/auth/*so OAuth navigations hit the network as plain browser nav. Eviction also drops any cached callback HTML carrying a stale code.Trip list parity (desktop ↔ mobile)
apps/web/src/lib/trip-buckets.ts— Now / Upcoming / Past, today-vs-startDate/endDate, ascending for current/upcoming, descending for past.Share / leave-trip UX fixes
MobileBottomSheet, which returnsnullwhen closed and unmounted the sibling sheet that just opened.useDeleteShareoptimistically drops the trip fromqueryKeys.trips(matched bysharedShareId), restores on error. Previously felt like a no-op until the next stale refetch.Auth + login polish
Docs
docs/vercel-setup.mdsection: Stable preview alias — DNS CNAME, Vercel branch-domain assignment topreview, OAuth-relay regex extension, Railway preview-env CORS, optional Google direct registration..env.example+README.mdenv table:NEXT_PUBLIC_PREVIEW_ORIGIN_PATTERNexample shows alternation pattern (per-deploy hostnames OR stable alias).CLAUDE.mdparity rule citeslib/trip-buckets.tsalongsideuseTripPermission/useShareLinkOwnerRedirectas a fresh shared-helper example.Test plan
pnpm --filter @travel-app/web build)pnpm --filter @travel-app/web lint)Reviewer notes
apps/web/src/lib/trip-buckets.tsextraction is the canonical place to change bucketing logic — both surfaces consume it.ApiProviderSwitcher(providers.tsx) sinceAuthProvideritself sits outsideQueryClientProvider. It watchesaccessTokenfor truthy → null transition.package.json/ lockfile changes; if Vercel reports thelstat .pnpm/node_modules/reactENOENT seen on a recent preview deploy, redeploy with build cache disabled — CI install reproduces clean every run.🤖 Generated with Claude Code