Skip to content

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

EventConditionSurface
Badge unlockCommon, Rare, or Epic rarityMilestoneCard displays achievement + rarity-tiered visuals
Legendary badgeAny legendary rarityLegendaryCelebration full-screen modal (existing surface, unchanged)
Streak milestoneDay 3, 7, 14, 30, or 100MilestoneCard 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 (respect prefers-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.ts via setFocusCaptureSink
  • The StreakCelebration full-screen modal (which fires for streak milestones ≥ day 3 outside Focus Mode) is suppressed inside Focus Mode via setFocusStreakCaptureSink
  • Legendary-rarity achievements continue to use LegendaryCelebration unchanged (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 useFocusSessionMilestones composable 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 (primary blue or gold for 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 TypeExample 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" with aria-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 (not text-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" with aria-valuenow updated 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.ts and useStreakCelebration.ts
  • Integration: ionic_frontend/src/components/dashboard/streamlined/StreamlinedDashboard.vue — card injected before DoneCard

Capture flow

  1. Focus Mode component mounts → useFocusSessionMilestones() initializes and registers capture sinks
  2. User checks in a habit → AchievementWatcher fires, detects new badge via localStorage diff
  3. Badge rarity is not legendary → Badge is routed to setFocusCaptureSink (instead of toast)
  4. Composable appends badge to pending array, rarity-sorted
  5. StreamlinedDashboard renders MilestoneCard for each entry in pending, between habit cards and DoneCard
  6. User taps card → clearOne(id) removes it from the array, next card renders
  7. Focus Mode component unmounts → onScopeDispose clears 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.


Last updated: 2026-07-20

Loading…