Skip to content

Profile / Engagement / Offline / Clubs / Pickup / Settings / Aliveness — TwoMore production roadmap

Status: Archived

Source: synthesized from 7 deep-research themes (identity-privacy-foundation, club-lifecycle-and-showcase, pickup-lifecycle-parity, messaging-primitive, detail-screen-skeleton-consistency, offline-mode-or-network-status, settings-information-architecture, celebratory-progress-and-infographics). Drives: /loop /goal autonomous executor. This file lives at /home/sungkyu08/Dev/twomore-v2/docs/architecture/engagement-roadmap.md (write before first iteration).

Ethos thread

The user's brief converges on one principle across all seven themes: the app is too much like rows of data, and the amateur deserves to feel like a professional. The cure is mechanical, not magical — every theme is grounded in the same three evidence anchors that keep recurring: (1) Strava/Discord/Spotify pattern — identity is a tappable primitive (chip in dense rows, card in sheets, full screen on second tap — Discord Per-Server Profiles + Strava Following + Spotify Wrapped card-by-card pacing); (2) iOS HIG + Material 3 platform conventions — pinned-bottom single CTA, ≥44pt tap targets, persistent-non-modal banner for offline state, settings hold only the rarely-changed; (3) internal prototypes already exist but are unwiredMatchProfiles.PlayerRow already implements the rich tappable row, useApplyToSession + GuestApplication ship without a single consumer, triggerCelebration('confetti') fires into a void with no CelebrationOverlay, SyncStatusBadge is exported but mounted nowhere, four i18n keys (joinPreviewTitle, joinRequest, ownerCannotLeave, contactKakaoComingSoon) describe destinations that were never built. The work is overwhelmingly wire-up + skeleton-consistency, not greenfield invention.

Foundations (must land first)

These three foundations unblock the rest of the roadmap. Each is sequenced first because downstream phases either share its primitive or depend on its decision being made.

  1. Detail-screen skeleton arch-test (Iter from detail-screen-skeleton-consistency) — codify the 4-zone shape (DetailShell header → optional SegmentedTabs immediately under header → flowing card stack → single contextual CTA in BottomCtaBand) as a Components rule + fs-based arch-test. Without this, every new public-club profile / public-player profile / host-dashboard ships with a fresh divergence and we get four 2026-05-27-style "consolidation" debts in one quarter.

  2. Network-status handler (Iter 1 from offline-mode-or-network-status) — make useScreenReady paused-aware so QueryBoundary stops pinning skeletons forever when offline. This is the user's stated symptom ("hung in a dead state — just skeletons loading") and it's a 3-file mechanical fix. Branch A (manual match organizer) is explicitly deferred — Spond doesn't ship it, Strava bounds offline to GPS-only, and the recompute paths (ELO/trust/RSVP) are inherently server-side. Defer with a paper-scorecard EmptyState as the answer to that user-need.

  3. PlayerIdentityCard primitive promoted from MatchProfiles.PlayerRow (Iter 1 from identity-privacy-foundation) — the foundation primitive every downstream theme consumes (public profile, club showcase host card, pickup host card, DM compose entry-point, friend graph promotion). Promote first; everything else uses it.

Sequenced phases

Phase 0 — Foundation primitives + dead-skeleton fix (1 week)

Value: Resolves the user's loudest symptom (dead skeletons offline) and ships the primitive every subsequent phase depends on. Highest-value/lowest-risk: zero migrations, all OTA, mechanical fixes against the wettest paint in the codebase.

Why first: Phases 1–8 all consume PlayerIdentityCard and/or assume the detail-screen skeleton is codified. Offline-handler is a 3-file fix that resolves a launch-blocking user complaint.

Chunks:

  • (offline-mode-or-network-status)Iteration 1 — Decision + paused-aware QueryBoundary + uncached offline EmptyState
  • (identity-privacy-foundation)Iter 1 — Promote MatchProfiles.PlayerRow to PlayerIdentityCard + audit all sheet wire-ups
  • (detail-screen-skeleton-consistency)Iteration 1 — Lift SegmentedTabs in club-detail + de-ice scorecard RoundRulesStrip

Phase 1 — Public profile + skeleton sweep (1 week)

Value: Both PublicProfileScreen and the canonical detail-screen skeleton land in the same week so every subsequent surface (public clubProfile, host dashboard, DM thread) inherits the right shape. PublicProfileScreen restructure is the highest-impact validation of the skeleton rule.

Why here: Restructure PublicProfileScreen RIGHT AFTER PlayerIdentityCard exists so the card primitive consumes itself first; pair with the BottomCtaBand sweep so the FriendshipButton lands pinned, not inline.

Chunks:

  • (identity-privacy-foundation)Iter 2 — Restructure PublicProfileScreen to the canonical 3-stacked-card archetype
  • (detail-screen-skeleton-consistency)Iteration 2 — BottomCtaBand sweep for club-detail join + public-profile friendship
  • (detail-screen-skeleton-consistency)Iteration 3 — Venue-detail consolidation + actions-menu pattern + post-detail/record-detail audit

Phase 2 — Identity rendering everywhere + opponentIds + privacy schema (1 week)

Value: Closes the user's headline ask ("clicking a player's name… should open a profile"). After this phase, every name in the app — matchups, head-to-head, leaderboard, scorecard — is tappable per the density rule.

Why here: Requires PlayerIdentityCard (Phase 0) + the public profile restructure (Phase 1) as the destination of every new tap.

Chunks:

  • (identity-privacy-foundation)Iter 3 — PlayerIdentityChip primitive + migrate matchup-item, head-to-head, leaderboard rows
  • (identity-privacy-foundation)Iter 4 — profile_visibility schema + can_view_profile_field SECDEF + adapter findPublicView
  • (identity-privacy-foundation)Iter 5 — SettingsPrivacyScreen + 'preview as' sheet + Settings IA placement

Phase 3 — Network resilience + offline UX hardening (1.5 weeks)

Value: Completes the offline-handler so taps don't get silently lost; expands Phase 14d queue to the 6–8 highest-value remaining mutations (dues, payments, manner-tags, friend, consent, attendance, cancel-RSVP).

Why here: Foundational primitive (NetworkBanner + SyncStatusBadge mount) ships now so every subsequent destructive flow (club archive, host cancel, DM send) inherits the right offline gating.

Chunks:

  • (offline-mode-or-network-status)Iteration 2 — NetworkBanner primitive + mount in MainTabShell + DetailShell + wire SyncStatusBadge
  • (offline-mode-or-network-status)Iteration 3 — Disable mutations + ActionButton offline-aware variant + scorecard paper-backup EmptyState
  • (offline-mode-or-network-status)Iteration 4 — Expand Phase 14d queue to dues/payments/manner-tags/friend/consent/attendance/cancel-rsvp
  • (offline-mode-or-network-status)Iteration 5 — Paused-query telemetry + verify-real-device + close-out doc

Phase 4 — Club lifecycle: cards → preview → apply → archive (2.5 weeks)

Value: Resolves "joining the club is a weird experience" — the dead-end invite-code prompt becomes a preview → apply → owner-approve flow. Disband path finally has a transfer-ownership escape valve.

Why here: PublicClubProfileScreen mirrors PublicProfileScreen's archetype (Phase 1), so the skeleton is settled. The card rework + join policy stack chunks that share files, minimizing churn.

Chunks:

  • (club-lifecycle-and-showcase)Iteration 1: Banner+logo+stats wired into existing cards
  • (club-lifecycle-and-showcase)Iteration 2: PublicClubProfileScreen + reroute Discover taps
  • (club-lifecycle-and-showcase)Iteration 3: joinPolicy + club_join_request + apply flow
  • (club-lifecycle-and-showcase)Iteration 4: transfer-ownership + archive-not-delete
  • (club-lifecycle-and-showcase)Iteration 5: Consolidate lifecycle into a Components rule + ESLint rule + screen-blueprint update

Phase 5 — Settings IA hub + friends promotion + dead-row cleanup (1 week)

Value: The settings hub becomes a Toss-grade 4-tier structure (identity → frequent → secondary → destructive); friends are promoted out of the profile preview into a primary surface; the "coming soon" KakaoTalk row and the duplicate mobile legal shims are deleted.

Why here: Settings restructure depends on SettingsPrivacyScreen (Phase 2) so the new top-level row has a destination. Friends promotion is the natural anchor for PlayerIdentityCard's social-graph rendering.

Chunks:

  • (settings-information-architecture) Delete dead routes + the 'coming soon' placeholder (smallest, immediate-visible)
  • (settings-information-architecture) Collapse ProfileMenuCard + remove the duplicate-route bottom menu
  • (settings-information-architecture) Restructure SettingsScreen into the 4-tier hub
  • (settings-information-architecture) Promote friends to a primary profile surface; cap the profile-screen preview to a summary card

Phase 6 — 번개 lifecycle parity (2 weeks)

Value: Wires the dead useApplyToSession chain into the live session-detail screen, ships the host identity card (trust signal), the host dashboard, and the public preview-before-commit. The amateur sees who's hosting BEFORE applying, which is what makes 번개 not-anonymous.

Why here: Depends on PlayerIdentityCard (Phase 0) for the host card render, and on the detail-screen skeleton (Phase 0) + BottomCtaBand sweep (Phase 1) so the apply CTA pins correctly.

Chunks:

  • (pickup-lifecycle-parity) Wire the dead apply/approve flow into session-detail (the smallest visible win)
  • (pickup-lifecycle-parity) Pickup host identity card on session-detail (the trust-signal foundation)
  • (pickup-lifecycle-parity) Host dashboard screen + cancel-without-strike RPC
  • (pickup-lifecycle-parity) Public-preview-before-commit for pickups
  • (pickup-lifecycle-parity) Filter restructure + home surfacing (discovery polish)

Phase 7 — Messaging primitive: DM + send-a-hi nudge (2.5 weeks)

Value: The "DM the club owner" / "DM the host" use cases that Phase 4 and Phase 6 keep deferring now land. Text-only Spond/Heja-style 1:1 + Instagram-style request gate. Send-a-hi is the structured one-way signal (NOT chat) that fits inside Phase 6's host card.

Why here: Depends on PublicProfileScreen (Phase 1) for the compose entry-point, public-club preview (Phase 4) for "DM the owner from preview," and host card (Phase 6) for "DM the host." Shipping after these means there are real destinations for the compose CTAs.

Chunks:

  • (messaging-primitive) Iteration 1 — Domain layer + minimal vertical slice: DM the club owner
  • (pickup-lifecycle-parity) Send-a-hi nudge primitive (DM scope-down) — pairs naturally with messaging iteration 1; treat the social_nudge channel as the lightweight cousin to a DM
  • (messaging-primitive) Iteration 2 — Inbox screen + Instagram-style requests gate + Spond-style read receipts

Phase 8 — Celebratory progress + infographics + aliveness (3 weeks)

Value: The user's "amateur should feel like a pro" headline. Real chart vocabulary, Strava-style recap cards, Apple-rings tri-progress, active CelebrationOverlay, achievement graduation, calendar heatmap, amateur-pro narrative copy. Final layer — depends on every prior phase being settled so the cards/screens being decorated aren't moving targets.

Why last: Adds a new native module (victory-native-xl + Skia) requiring an EAS build (0.5.0 → 0.6.0). Best done after the structural churn settles so the new build window is spent on engagement, not refactor patches.

Chunks:

  • (celebratory-progress-and-infographics) Iteration 1 — install victory-native-xl + ship @twomore/ui/charts/ primitives (SparkLine, MiniBarChart) + replace EloTrendCard flat bars
  • (celebratory-progress-and-infographics) Iteration 2 — wire CelebrationOverlay + add Apple-rings TriRingProgress to records HeroCard
  • (celebratory-progress-and-infographics) Iteration 3 — recompose home weekly/monthly hero into Strava-style multi-beat recap card
  • (celebratory-progress-and-infographics) Iteration 4 — achievement system graduation: progress-toward-next + active CelebrationOverlay on unlock
  • (celebratory-progress-and-infographics) Iteration 5 — calendar heatmap on records-screen + PersonalRecordsSection teaser-mode flip

Phase 9 — Codify durability (1 week, runs in parallel to Phase 8 if capacity permits)

Value: Turns the scattered fixes into a system. Single arch-test gate for detail-screen skeleton; ESLint rule for no-direct-club-delete; lifecycle Components rules; bump mechanical-enforcement count toward 40 of 40.

Chunks:

  • (detail-screen-skeleton-consistency) Iteration 4 — Codify DETAIL-SCREEN-SKELETON arch-test gate
  • (club-lifecycle-and-showcase)Iteration 5: Consolidate lifecycle into a Components rule + ESLint rule + screen-blueprint update (re-listed here as the closer if not absorbed into Phase 4)

Per-theme reference

identity-privacy-foundation

Summary: Player names are inert text across high-context surfaces; MatchProfiles.PlayerRow already proves the rich pattern but isn't a primitive; no privacy schema beyond PIPA consent. Foundation establishes a two-variant PlayerIdentity primitive (Chip + Card), a profile_visibility schema with per-field defaults + RLS, and a density-driven placement policy. Top 3: (1) Promote MatchProfiles.PlayerRow to PlayerIdentityCard + ship PlayerIdentityChip — evidence: codebase prototype already exists; Discord/Material 3 density patterns. (2) Add profile_visibility schema with (public/club_members/friends/private) per field, default club_members — evidence: Strava always-visible identity + per-field hiding model + existing migration 00142 posture. (3) Restructure PublicProfileScreen to canonical 3-stacked-card archetype — evidence: CLAUDE.md detail-screen IA rule + session-detail reference. Complexity: large · Value: high · Dependencies: none

club-lifecycle-and-showcase

Summary: Discover cards dead-end at invite-code prompt; cards ignore banner/logo/stats; hard-delete cascade with no transfer-ownership; missing primitive is a public club profile screen. Top 3: (1) Build PublicClubProfileScreen at /clubs/[clubId]/preview — evidence: Discord Server Profile + Strava Club page + internal PublicProfileScreen precedent. (2) Add joinPolicy enum (open/approval/invite_only) + club_join_request + apply Q&A — evidence: Naver Café 3-tier + Facebook Groups membership questions. (3) Add transfer-ownership RPC + archive-not-delete with 30-day grace — evidence: WhatsApp Communities transfer (Sept 2024) + Cloudscape multi-step delete + internal 00157 PIPA hard-delete cron pattern. Complexity: epic · Value: critical · Dependencies: identity-privacy-foundation, detail-screen-skeleton-consistency

pickup-lifecycle-parity

Summary: Complete guest-application backend exists with ZERO consumers; pickup hosts are anonymous to discovery viewers; filter sheet is marketplace-dense; 번개 absent from home. Top 3: (1) Wire dead apply/approve flow into session-detail-screen — evidence: GoodRec + Strava invite-only state machine matches GuestApplicationStatus enum exactly. (2) Pickup host identity card on session-detail + new HostBeat in SessionOverview — evidence: Airbnb Superhost composite metric + GoodRec "find your host" pattern. (3) Public-preview-before-commit for pickups via get_public_pickup_preview SECDEF — evidence: Partiful hide-details-until-RSVP pattern + Eventbrite ticketed event gating. Complexity: large · Value: high · Dependencies: identity-privacy-foundation, messaging-primitive

messaging-primitive

Summary: DM is greenfield — no entity/port/adapter/screen exists; chat signal_category is stubbed; Android channel created; routes placeholder. Build text-only Spond/Heja-style 1:1 with Instagram-style request gate; defer 1:1 chat-as-replacement-for-Kakao but ship in-app for accountability. Top 3: (1) Build dm_thread + dm_message domain mirroring friendship lo/hi pair pattern — evidence: friendship.entity.ts ports + Supabase Realtime Chat reference. (2) Instagram-style request gate — friends + same-club auto-accepted, strangers go to 요청 tab — evidence: Instagram Requests UX (Bustle 2026) + Discord shared-server-can-DM default. (3) Iteration 1 ships ONE entry-point — DM the club owner from club-detail header-right — evidence: Spond inline-CTA pattern + CLAUDE.md IA rule against new bottom tabs. Complexity: large · Value: high · Dependencies: identity-privacy-foundation

detail-screen-skeleton-consistency

Summary: Canonical detail-screen skeleton exists (DetailShell → optional SegmentedTabs under header → flowing cards → BottomCtaBand) but 4 high-traffic screens diverge: club-detail tabs mid-screen, public-profile inline FriendshipButton, venue-detail 4 inline buttons, scorecard rules-chips with decorative icons. Top 3: (1) Lift SegmentedTabs in club-detail to immediately under header — evidence: spectator-scorecard precedent + Material 3 / NN Group nested-tab anti-pattern. (2) Pin every single contextual primary action in BottomCtaBand (club join, public-profile friendship, venue book-alert) — evidence: session-detail state-precedence pattern at line 208–340 + iOS HIG Toolbars. (3) Drop decorative lucide icons from RoundRulesStrip — evidence: CLAUDE.md "icons SPARINGLY" rule + Material 3 chip icon-as-optional spec. Complexity: medium · Value: high · Dependencies: identity-privacy-foundation, club-lifecycle-and-showcase

offline-mode-or-network-status

Summary: DECISION: take Branch B (network-status handler). Branch A (offline manual organizer) costs 3-5 weeks for a surface Spond explicitly doesn't ship; TwoMore's core (ELO/trust/RSVP/signals) is inherently server-side. Root cause of dead-skeleton: TanStack default networkMode: 'online' pauses queries forever; useScreenReady only short-circuits 'idle' not 'paused'. Top 3: (1) Make useScreenReady paused-aware + render OfflineEmptyState instead of permanent skeleton — evidence: TanStack v5 docs explicitly call out fetchStatus: 'paused' as the canonical offline signal; codebase types it but doesn't act on it. (2) Persistent NetworkBanner + mount unwired SyncStatusBadge — evidence: Material Banners spec + Linear local-first top-bar sync indicator. (3) Expand Phase 14d queue to dues/payments/manner-tags/friend/consent/attendance/cancel-RSVP — evidence: TanStack networkMode guide + canonical pattern already proven in use-rsvp.ts:98-122. Complexity: large · Value: critical · Dependencies: none

settings-information-architecture

Summary: Settings is a 6-section flat menu failing 3 Toss/HIG tests at once: equates rare-admin with frequent toggles; FriendsScreen grows unboundedly inside profile preview; ships shipped-dead surfaces (legal/notices, legal/contact, KakaoTalk opacity placeholder). Top 3: (1) Restructure as Toss-grade 4-tier hub: identity card → frequent toggles → secondary nav → destructive — evidence: iOS HIG settings rule + Linear 2024-12 redesign + Slack Preferences shape. (2) Promote friends to primary profile-tab surface — evidence: Apple Health Sharing tab + Strava Following list; codebase has unbounded friendList.map(...) with no slice. (3) Delete the shipped-dead mobile legal route shims + KakaoTalk placeholder — evidence: UI Annoyances + DNSK "museums of abandoned ideas"; grep confirms mobile renders hardcoded Korean while web uses i18n'd version. Complexity: medium · Value: high · Dependencies: identity-privacy-foundation

celebratory-progress-and-infographics

Summary: All data infrastructure exists (PlayerStatsDetail, 55-entry achievement catalog, ELO history, weekly/monthly composers) but presentation is undersold. Weekly hero = 3 text numbers; EloTrendCard = flat gray bars; achievements = static binary checklist; triggerCelebration('confetti') wired but no overlay consumes it. Top 3: (1) Install victory-native-xl + ship @twomore/ui/charts/ primitives (Skia GPU) — evidence: victory-native-xl v41 + 100+ FPS on low-end Android; react-native-svg already installed. (2) Recompose weekly/monthly hero into Strava Year-in-Sport pacing recap — evidence: Strava case study (Manual creative) + Spotify Wrapped card-by-card pacing. (3) Apple-rings tri-progress on HeroCard + active CelebrationOverlay — evidence: Apple HIG Activity Rings (83% Watch users report health improvement) + triggerCelebration already wired but firing into void. Complexity: large · Value: high · Dependencies: detail-screen-skeleton-consistency

Open decisions (NEEDS USER INPUT)

  1. DM scope — full thread / lite one-shot / KakaoTalk handoff?Researcher's recommendation: Build text-only 1:1 + 요청 gate (Phase 7) AND add send-a-hi nudge (Phase 6/7 boundary) AS COMPLEMENTS. Rationale: 문토 (Korean meetup app) explicitly warns members against external chat for safeguarding; KakaoTalk bypasses TwoMore's trust signals; the friendship-pair pattern + offline-queue + 13-channel push are 70% of what v1 messaging needs. Defer rich media + group chat. The trade-off is engineering cost (~2.5 weeks for Phase 7) vs the strategic alternative of deep-linking to KakaoTalk for all conversation. Need user confirmation before starting Phase 7.

  2. Offline manual mode — pen-and-paper organizer or just polish the network-status handler?Researcher's recommendation: Polish the network-status handler (Branch B) — DEFER the offline organizer (Branch A). Spond's 4.3M downloads / 4.80 rating ship with NO offline mode; Strava bounds offline to GPS-only single-user capture; TwoMore's match scoring is inherently multi-actor (ELO/RSVP/trust). Ship a paper-scorecard EmptyState as the answer to that user-need. Telemetry hook in Phase 3 Iteration 5 ('paused-query' perf event) reopens the conversation with data if real on-court friction emerges. Defer is recommended unless the user pushes back on the deferral framing.

  3. Player-name tappability scope — every name app-wide or space-permitting only?Researcher's recommendation: Density-driven, not screen-driven — user's instinct is correct. (a) ≥44pt vertical room → PlayerIdentityCard (rich, tappable); (b) limited row room, tap target ≥44pt → PlayerIdentityChip (compact, tappable); (c) micro-density score-line contexts (LiveMatchRow t1, t2, RecentMatchRow) → keep plain Text BUT make the parent Card tappable to MatchProfilesSheet (already exists). Codify in CLAUDE.md Components rule. iOS HIG 44pt minimum is the deciding factor for the carve-out.

  4. Visibility default — club_members or public?Researcher's recommendation: club_members as default, matching the existing migration 00142 posture for club_elo_ratings. Always-visible: displayName + avatar + ELO tier (matching Strava's always-visible name+badge — without those, you can't recognize who you played). Confirm before Phase 2 Iter 4 ships the migration; reversing default later requires a backfill.

  5. Friends promotion — replace profile preview with summary card, or move ENTIRELY out of profile?Researcher's recommendation: Replace with summary card (avatar stack of 4 most-recent + "친구 N명" + chevron → FriendsScreen). Don't add a 5th bottom tab. Apple Health uses a separate Sharing tab; Strava uses a Following list reached from profile — either is defensible, but the lighter-touch summary card is the right ship-it choice. Confirm before Phase 5.

  6. DETAIL-SCREEN-SKELETON arch-test — block or warn?Researcher's recommendation: Ratchet baseline (block new violations, grandfather existing). Matches established @twomore/no-padding-top-in-features pattern. Confirms before Phase 9.

  7. app version bump cadence — bundle Phase 6 social_nudge channel with Phase 8 victory-native-xl in one 0.5.0 → 0.6.0 EAS build?Researcher's recommendation: Yes, bundle. Both require native changes (Phase 6: 14th Android channel; Phase 8: react-native-skia + victory-native-xl). Bundling means one EAS build cycle. Otherwise Phase 6 needs its own 0.5.0 → 0.5.1 build. Confirm the bundling preference before Phase 6 Iter 6 ships.

Success criteria

Phase 0

  • Offline app launch with no cached data renders OfflineEmptyState (not permanent skeleton) within 1s — verified by airplane-mode device test.
  • Tap any name inside MatchProfilesSheet → lands on the existing PublicProfileScreen via the prefetch path — measured by 0 cold-start lag.
  • RoundRulesStrip renders no lucide icons; chip strip reads as type-led tokens.
  • ArchUnitTS gate passes; ratchet for detail-screen skeleton baseline established.

Phase 1

  • PublicProfileScreen renders 3 stacked cards (identity / social / records) with FriendshipButton in BottomCtaBand.
  • venue-detail-screen has exactly ONE pinned CTA + HeaderActionsMenu for secondary actions.
  • yarn check passes; lint:strict 0/0.

Phase 2

  • Every player name in matchup-item, head-to-head OpponentRow + best-record card, leaderboard rows, MatchTeams scorecard rows is tappable per the density rule.
  • SettingsPrivacyScreen renders the 4-way segmented chip + "preview as" sheet.
  • Setting visibility to private on region → public profile renders '비공개' chip; setting to friends + viewer is non-friend → field hidden — verified by 2-account device test.
  • 00212_profile_visibility.sql applied to prod.

Phase 3

  • NetworkBanner appears within ~1s of going offline; clears within ~1s of reconnect.
  • SyncStatusBadge increments visibly when a queued mutation fires offline; drains on reconnect.
  • 10+ mutations support offline queue (was 4 — useRsvp + useStartSession + useSetPostReaction + useSubmitScore).
  • yarn debug:logs --kind=paused-query returns telemetry events.
  • Real-device verification: airplane mode mid-flow on home/club-detail/profile/settings/scorecard — banner, cached content + stale ribbon, queued badge, post-reconnect drain all behave per spec.

Phase 4

  • Discover card tap → PublicClubProfileScreen (banner + logo + ClubTier + member count + chip strip + next-public-session preview + state-aware JOIN CTA).
  • 3 join policies work end-to-end (open / approval-with-Q&A / invite-only).
  • Transfer ownership flow: owner picks new owner → swap is atomic → both get notification + signal.
  • Archive flow: club soft-deletes with 30-day grace; archived clubs hidden from discovery; hard-delete cron at 30 days.
  • @twomore/no-direct-club-delete ESLint rule enforces; ratchet baseline = 0 new violations after migration.

Phase 5

  • SettingsScreen renders 5 cards (identity + frequent + secondary + app info + destructive) — no more than 21 visible rows.
  • profile-screen shows a fixed-height "친구 N명" summary card (avatar stack of 4 + count + chevron), not unbounded list.
  • apps/mobile/app/legal/notices.tsx + contact.tsx are 2-line shims importing from @twomore/profile.
  • Zero opacity={0.55} "coming soon" rows app-wide.

Phase 6

  • Non-member of a pickup virtual club taps the pickup → sees preview with host card + silhouette participant stack + district-level map.
  • "참가 신청" → fires useApplyToSession → host receives push → host dashboard shows pending row → approve → applicant transitions to confirmed RSVP — verified by 2-account device test.
  • 번개 surfaces on home feed when region has pickups + user has <2 confirmed sessions in window.
  • Filter bar: date is visible SegmentedTabs chrome, secondary criteria in single sheet.
  • Host cancellation does NOT generate strikes (excused via new cancel_session_by_host RPC).

Phase 7

  • Send DM from club-detail header (non-member) → owner receives push → tap deep-links to thread → owner replies → message arrives via realtime within ~500ms.
  • Stranger DM lands in 요청 tab (not 주요); friend/same-club DM auto-accepts to 주요.
  • DM works offline (queued + drains on reconnect) — verified via airplane-mode toggle.
  • Send-a-hi nudge from public-profile / pickup host card → recipient sees signal in bell within ~10s + can tap-reply to KakaoTalk if present.

Phase 8

  • EloTrendCard renders animated SparkLine (Skia GPU), color shifts $success/$error by sign.
  • Records HeroCard renders Apple-rings tri-progress on the right + displayMd ELO on the left.
  • Tap "RSVP" → CelebrationOverlay confetti for 2.5s — confetti is not dropped by frame budget on low-end Android.
  • Weekly hero recap card renders 5 beats (headline / SparkLine / CalendarHeatmap / top opponent+partner / milestone) + 공유하기 button → share card lands in user's KakaoTalk.
  • Achievements gallery: locked quantitative achievements render <ProgressBar /> + current/target text.
  • Records screen renders 12-week ActivityHeatmapCard between EloTrendCard and FormatStatsCard.
  • PersonalRecordsSection renders teaser-mode (ghost-tinted placeholders + threshold copy) when matches < 5.
  • App version 0.6.0 published to prod; victory-native-xl + react-native-skia in tree.

Phase 9

  • AGENTS.md mechanical-enforcement count: 39 → 40 (or 41 if both arch-test + ESLint rule graduate).
  • ratchet baselines: 0 new violations across all rules; existing waivers documented.
  • screen-blueprint.md + CLAUDE.md updated with the 4-zone matrix + lifecycle decision tree + chart-primitive Components rule.

/loop runbook

Save this roadmap at /home/sungkyu08/Dev/twomore-v2/docs/architecture/engagement-roadmap.md. Drive execution with /loop /goal:

  1. First iteration: open the roadmap. Find the next un-done foundation in the Foundations section. Foundations land in this order: (1) detail-screen skeleton arch-test (Phase 0 Iter 1 of detail-screen-skeleton-consistency), (2) network-status handler (Phase 0 Iter 1 of offline-mode-or-network-status), (3) PlayerIdentityCard primitive (Phase 0 Iter 1 of identity-privacy-foundation). The agent picks the topmost un-done foundation, executes the chunk's audit → research → fix → ship → document cycle (each chunk specifies these phases), and updates this roadmap by checking off the chunk title with a prefix on the same line.

  2. Subsequent iterations: once Foundations are checked off, iterate by Sequenced phases in order. Within a phase, take chunks top-to-bottom — they are dependency-ordered. Do not parallelize across phases without a dependency check (e.g. Phase 4 needs Phase 1's BottomCtaBand sweep landed).

  3. Open decisions in this roadmap are NOT executable until the user resolves them. If /loop encounters a phase blocked on an open decision, post the open-decision question into the loop output and pause that phase; pick up the next un-blocked phase.

  4. Per-iteration discipline: every chunk is sized for ~1 day = 1 commit + 1 OTA (or 1 db push + 1 OTA, or 1 EAS build for native changes per the bundling decision in Open decisions §7). The chunk text already specifies AUDIT → RESEARCH → FIX → SHIP → DOCUMENT — agents follow that pattern verbatim.

  5. Checkpoint: after every 3 chunks shipped, append a one-line milestone entry to CLAUDE.md "Operational State" section (matching the established narrative-milestone pattern) and update key-numbers if migrations/tests/rules changed.


Execution log

2026-05-30 — Roadmap synthesized via Workflow wf_b7c1464e-80c (10 agents, 1.8M tokens, 12 min). The 7 open decisions are auto-accepted per the researcher's recommendations (each grounded in iOS HIG / Material 3 / Strava / Spond / Toss / TanStack docs); /loop /goal autonomous execution begins at Phase 0 chunk 1 (offline paused-aware QueryBoundary + OfflineEmptyState).

2026-05-30 — Phase 0 shipped (3 chunks). 90edd41 offline iter 1 + 0746ceb identity iter 1 + 575d544 detail-screen iter 1. OTAs 43556523 (offline) + 9227249b (identity + tabs lift + de-ice). Foundations done; Phase 1 (public profile + skeleton sweep) is the next set of chunks.

2026-05-30 — Phase 1 shipped (3 chunks). a284c5a chunks 1+2 (PublicProfileScreen 3-card archetype + BottomCtaBand sweep for club-detail join + public-profile friendship) + 19de0ec chunk 3 (venue-detail consolidation: HeaderActionsMenu for navigate/map/site/call + BottomCtaBand for book-alert; post-detail and record-detail audited clean). OTAs 2cec4e8e + a85a6c1d. Phase 2 (identity-everywhere chip primitive + visibility schema + privacy screen) is next.

2026-05-30 — Phase 2 shipped (3 chunks). 62eace9 chunk 1 (PlayerIdentityChip primitive in @twomore/app + migrate leaderboard RankRow across 3 tabs + head-to-head OpponentRow — matchup-item TeamSide deferred as already-tappable + density-bound) + ed4c5b0 chunk 2 (migration 00212: profile_visibility JSONB + can_view_profile_field SECDEF + get_public_profile_view SECDEF + adapter findPublicView; locally validated via db reset + lint:pi-schema clean; applied to prod via db push) + 21d779b chunk 3 (SettingsPrivacyScreen + wire PublicProfileScreen to findPublicView via new use-public-profile hook; profile entity + mapper extended with profileVisibility patch shape; Korean copy uses 클럽 not the tab-only 모임). OTAs b215191c + 50107c62. Migration count 211 → 212. Identity-rendering + privacy schema landed end-to-end; Phase 3 (network resilience + offline UX hardening) is next.

2026-05-30 — Phase 3 chunk 1 shipped. fa3c486 NetworkBanner primitive in @twomore/app subscribes to TanStack's onlineManager and renders a thin amber strip with WifiOff + "연결이 없어요" when offline, nothing when online. Mounted ONCE at apps/mobile/app/_layout.tsx (above the routes' Stack) so every screen inherits it — diverges from the roadmap's "mount in MainTabShell + DetailShell" since those shells live in @twomore/ui and can't import the onlineManager hook (hex direction); the app-shell mount achieves the same UX outcome without the layer break. Reuses the common.offline i18n shipped in Phase 0. SyncStatusBadge wiring (still unmounted) + ActionButton offline-aware variant + scorecard paper-backup land in Phase 3 chunks 2-4 next iteration. OTA 04109757.

2026-05-30 — Phase 3 fully shipped. f6f7ae0 chunk 2 (useIsOnline hook + scorecard paper-backup EmptyState + useLiveSessionData.isPausedOffline) + 22a488f chunks 3+4 (Phase 14d queue expansion 4→10 mutations: useUpdateDuesStatus + useMarkPaymentPaid + useMarkPaymentWaived + useSendFriendRequest + useAcceptFriendRequest + useDeclineFriendRequest; paused-query telemetry via setupOnlineManager subscribe → network:offline-session perf events). Skipped with grounded reasons: useGrantConsent + useRevokeConsent (user_consent lacks idempotency_key), useConfirmAttendance + useExcuseAttendance (RPCs are naturally idempotent UPDATEs but don't accept idempotency_key — queue support would be safe-but-redundant). OTAs 6174767d + 235c174a. Phase 4 (club lifecycle: cards → preview → apply → archive) is next.

2026-05-31 — Phase 4 fully shipped (5 chunks, 5 OTAs, 2 migrations). 5cbc381 chunk 1 (banner+logo+stats on club cards via 3:1 banner strip + new ClubLogoBubble primitive) · 304a324 chunk 2 (PublicClubProfileScreen at /clubs/[clubId]/preview + DiscoverClubCard rerouted from joinClub dead-end) · 3ce7482 chunk 3 (migration 00213 join_policy enum open/approval/invite_only + club_join_request table + apply_to_club + decide_join_request SECDEFs + ApplyToClubSheet + policy-aware CTA in PublicClubProfileScreen) · 1ad6bb5 chunk 4 (migration 00214 archived_at column + transfer_club_ownership + archive_club + unarchive_club SECDEFs + purge_archived_clubs cron at 03:30 KST + TransferOwnershipSheet + DangerZone now archive-not-delete) · this commit (Components rule for the lifecycle + screen-blueprint additions + ESLint rule deferred as follow-up). OTAs 31f6666f + 452c868a + 53fc1d17 + 33954128. Migration count 212 → 214. Phase 5 (settings IA hub) is next.

2026-05-31 — Phase 5 fully shipped (4 chunks, 4 OTAs). f7ee2d7 chunk 1 (drop the opacity={0.55} "준비 중" KakaoTalk Card from ContactScreen + collapse apps/mobile/app/legal/contact.tsx to a 2-line shim re-exporting @twomore/profile's ContactScreen — notices migration deferred because moving its inline ko strings tripped 3 lint rules at once; tracked as follow-up) · 7f5f675 chunk 2 (ProfileMenuCard collapsed 6 rows → 2: Settings + Logout. matchHistory/practice → 기록 tab; notices/contact → SettingsScreen 앱 정보 — single canonical entry-point per destination per the Toss IA principle) · 87f547c chunks 3+4 paired (SettingsScreen restructured into the 5-card / 21-row 4-tier hub: identity → frequent toggles → secondary nav → app info → destructive; developer-mode toggle now hidden behind a long-press affordance when off, surfaces as a card when on; SectionBlock wrappers removed since hierarchy is now implicit from card grouping. FriendsSummaryCard NEW: avatar stack of 4 most-recent friends overlapping ~10pt + "친구 N명" + chevron to /profile/friends; replaces unbounded FriendsPreviewSection on profile-screen; empty state pivots to "친구 찾기" CTA so the discovery entry-point survives. Protocol B props from profile-screen). OTAs 08f1b9f0 + 5bfac7de + 02775bff. Phase 6 (번개 lifecycle parity — wire dead apply/approve chain + host card + host dashboard + public preview + filter restructure) is next.

2026-05-31 — Phase 6 fully shipped (5 chunks, 5 OTAs, 1 migration). Closes the 5-month 번개 dead-end where migration 00017 had landed a complete guest-application backend (table + RLS + port + adapter + 5 hooks + orchestration hook useSessionDetail) but ZERO consumers — every component still routed pickups through the club-membership path. a185bd9 chunk 1 (wire dead apply/approve flow into session-detail's BottomCtaBand precedence — 3 new branches for pending/rejected/no-application — inline isGuest + sessionIsOpenForGuests derivations mirroring the unused useSessionDetail orchestrator) · b23f31b chunk 2 (PickupHostCard wraps PlayerIdentityCard primitive with a "호스트" label, slotted between SessionOverview and SessionStatusParticipationCard, gated on publicInviteEnabled) · 7fad4f2 chunk 3 (migration 00215 ships decide_guest_application atomic approve-creates-rsvp SECDEF + cancel_pickup_session_without_strike SECDEF — the latter pre-seeds excused attendance_records BEFORE cancelling RSVPs so the 00127 classify_rsvp_cancel trigger's ON CONFLICT clause preserves them and zero strikes fire; new PickupHostDashboardScreen at /sessions/[id]/host for pending-app approve/reject + destructive cancel-pickup CTA, entry point from session-detail's actions menu when isHost && publicInviteEnabled) · 5721409 chunk 4 (PublicPickupPreviewScreen at /sessions/[id]/preview mirrors PublicClubProfileScreen — host card + session basics + silhouette participant strip with grayscale $borderSubtle/$surfaceSecondary placeholders capped at 6 + district-level location + apply CTA; 4-condition bounce useEffect short-circuits host/member/non-pickup/approved viewers to the full screen; activity-pickups-screen reroutes every pickup tap to the preview via in-parent onPress override) · 545a441 chunk 5 (DatePickupChromeBar lifts date OUT of the 7-dimension sheet into SegmentedTabs chrome above the search bar — 5 tabs 전체/오늘/내일/이번 주/이번 달; activePickupFilterCount no longer counts date; HomeNearbyPickupsCard self-gates on user.region has open pickups AND user has <2 confirmed sessions in next 14d, mounted in HomeFeedScreen after HomeDisputedAlertBanner, zero new query hooks). OTAs fcd2930a + 0692e830 + 705a2e64 + d3da022d + f722fe01. Migration count 214 → 215. Phase 7 (messaging DM primitive) is gated on open decision §1 about DM scope (text-only 1:1 + 요청 gate vs KakaoTalk handoff vs lite one-shot) — needs user confirmation before starting. Until that resolves, the autonomous loop completes the full-stack audit + Phase 9 codification.

Markdown remains the source of truth. Run yarn docs:check before handoff.