Focus Mode: In-Session Achievement Rewards
Overview
When users unlock badges or hit streak milestones during a Focus Mode session, the achievement is now surfaced as a dedicated card in the habit-check-in flow, replacing the previous silent "You're all set!" ending. The MilestoneCard appears immediately after the last habit card and before the DoneCard, giving users a moment to celebrate before exiting the session.
This closes the gamification reward loop for Focus Mode — the shortest path between action and recognition — and strengthens the habit formation cycle for daily users.
What triggers the card
| Event | Condition | Surface |
|---|---|---|
| Badge unlock | Common, Rare, or Epic rarity | MilestoneCard displays achievement + rarity-tiered visuals |
| Legendary badge | Any legendary rarity | LegendaryCelebration full-screen modal (existing surface, unchanged) |
| Streak milestone | Day 3, 7, 14, 30, or 100 | MilestoneCard displays day count + rarity-tiered visuals |
Multiple achievements in one session → multiple sequential MilestoneCards in the stack, ordered by rarity descending (epic first, then rare, then common).
v1 scope: Badges and streak milestones only. Level-ups and rank promotions have API stubs in the composable but are not wired to AchievementWatcher and will not appear on the card until a follow-up phase adds user.level and user.rank change detection.
Behavior
Card appearance
The card wraps the existing FocusCard base layout with rarity-specific visual treatment:
- Common: Blue border (
border-primary/30), subtle shadow - Rare: Purple border (
border-primary/50), enhanced glow - Epic: Gold border (
border-gold/60), gold tint background, confetti animation (respectprefers-reduced-motion)
Artwork sizing is rarity-tiered (64px common, 72px rare, 80px epic) and reuses existing badge/streak artwork.
Interaction
- Tap to advance: User tap anywhere on the card advances to the next card in the stack
- Auto-advance: If the user is idle, the card auto-advances after 5 seconds (progress bar visible during the countdown)
- Keyboard support: Enter or Space key also triggers advance; full keyboard accessibility with ARIA labels
Notification suppression
While a MilestoneCard is presenting an achievement:
- Toast notifications for the same badge/streak are suppressed in
useAchievementNotifications.tsviasetFocusCaptureSink - The
StreakCelebrationfull-screen modal (which fires for streak milestones ≥ day 3 outside Focus Mode) is suppressed inside Focus Mode viasetFocusStreakCaptureSink - Legendary-rarity achievements continue to use
LegendaryCelebrationunchanged (they bypass the suppression logic entirely)
This prevents double-firing: the card owns the celebration moment, and existing surfaces do not interrupt it.
Offline & queue behavior
If a user's Focus Mode session ends and then a queued check-in flushes (network returns), unlocking an achievement:
- The
useFocusSessionMilestonescomposable is no longer active (Focus Mode component unmounted) - The capture sink is cleared, and the achievement fires toast as normal
- The card is never rendered because the Focus Mode UI is already closed
- User can see the achievement in the Achievements page or via the regular achievement toast
This is acceptable and documented; the card is a Focus Mode enhancement, not a universal reward surface.
Visual design & accessibility
Rarity tiers
Rarity is conveyed via:
- Border color (
primaryblue orgoldfor epic) - Glow effect (shadow-radius scales with rarity)
- Background tint (gold/5 for epic only, gives a subtle warmth)
Copy & brand voice
All microcopy uses the "coach who's also a friend" tone: action-forward, brief, celebratory without empty praise. Examples:
| Achievement Type | Example Subtitle |
|---|---|
| Streak 30 days | "No excuses needed at this point." |
| Epic badge | "That's not a fluke." |
| Rare badge | "You showed up when it counted." |
Reduced-motion
When prefers-reduced-motion is active:
- Confetti is suppressed (epic rarity still shows the card, just without animation)
- The progress bar is hidden (timer still fires, just invisible)
- Scale animations replaced with opacity fade
- Auto-advance happens faster (not configurable by the user in-session)
WCAG compliance
- Card is
role="article"witharia-label="Achievement unlocked: [title]" - Rarity chips use bordered-pill style (not tinted fills) to achieve 6.2:1 contrast; achieves WCAG AA for all rarity colors
- Streak day number uses
text-foreground(nottext-accent) to ensure 4.5:1 minimum on background - Tap target is the entire card (
min-h-[420px]≥ 44px minimum) - Progress bar is
role="progressbar"witharia-valuenowupdated on 1s ticks - Overflow link:
aria-label="View [N] more achievements in full dashboard"
Implementation
Files
- Component:
ionic_frontend/src/components/dashboard/streamlined/MilestoneCard.vue - Composable (session-scoped):
ionic_frontend/src/composables/useFocusSessionMilestones.ts - Capture sinks (notification muting): Added to
useAchievementNotifications.tsanduseStreakCelebration.ts - Integration:
ionic_frontend/src/components/dashboard/streamlined/StreamlinedDashboard.vue— card injected beforeDoneCard
Capture flow
- Focus Mode component mounts →
useFocusSessionMilestones()initializes and registers capture sinks - User checks in a habit →
AchievementWatcherfires, detects new badge via localStorage diff - Badge rarity is not legendary → Badge is routed to
setFocusCaptureSink(instead of toast) - Composable appends badge to
pendingarray, rarity-sorted StreamlinedDashboardrendersMilestoneCardfor each entry inpending, between habit cards andDoneCard- User taps card →
clearOne(id)removes it from the array, next card renders - Focus Mode component unmounts →
onScopeDisposeclears sinks and resets state
See useFocusSessionMilestones.ts for API surface: captureBadge(key), captureStreak(day), pending: Ref<MilestoneArtifact[]>, clearOne(id).
Testing
All new code covered by unit tests:
- MilestoneCard.vue: 31 tests covering per-rarity rendering, artwork sizing, auto-advance timer, confetti (epic only), tap-to-advance, reduced-motion fallback, keyboard interaction, accessibility attributes
- useFocusSessionMilestones.ts: capture/clear/reset paths, multi-event ordering, rarity sorting, no leakage across sessions
- StreamlinedDashboard.vue: MilestoneCard appears in stack when achievements unlock in-session; toast spy confirms suppression for same badge
- Integration: Focus Mode full flow (check-in → badge unlock → card visible → tap advance → DoneCard)
Statement coverage ≥80% on new components; existing test suites remain green.
Related
- Achievements & Gamification — Full badge catalog, rarity reference, Atelier page
- Gamification Engine — XP awards, level thresholds, rank progression
- Focus Mode — Habit check-in flow, card stack architecture (see the "focus mode" /
StreamlinedDashboard.vuesection)
Last updated: 2026-07-20