You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Background: Full-screen tinted with the card's bg color (primaryContainer → tertiaryContainer → secondaryContainer) — each card has its own color theme, creating a strong visual rhythm as the user swipes
✅ didUpdateWidget syncs PageController to external button-driven index changes — no double-animation bug
✅ "Skip intro" only skips marketing, not permission setup — correct and intentional
✅ ConstrainedBox + SingleChildScrollView prevents overflow on short devices / large text scale
⚠️Issue: No "swipe left" affordance on the last card — user might swipe right infinitely. The PageView is bounded (itemCount: 3) so left-swipe beyond card 0 is naturally dead — good. But there's no visual indicator that card 3 is the last (only the absence of "Skip" + "Next"→"Continue" label)
Accessibility
✅ MediaQuery.disableAnimationsOf() guards all animations
⚠️ExpressiveFlower icons have no semantic labels — screen reader won't describe the alarm/map/groups icons
⚠️PageView has no Semantics page count/index — TalkBack won't announce "slide 1 of 3"
Architecture Adherence (Architecture Skill)
✅ View is UI-only (StatefulWidget, no business logic)
✅ _OnboardingPageState is the single source of truth for card index, matching MVVM pattern
✅ _MarketingCarousel receives callbacks, not repositories — correct separation
Responsive Layout (Responsive Skill)
✅ LayoutBuilder + ConstrainedBox(minHeight) — short-content centering on all screen heights
✅ ConstrainedBox(maxWidth: 290) — limits reading width on tablets
⚠️ On tablets (>600dp), the ConstrainedBox(maxWidth: 290) is tight — text reads fine but the 150px flower is disproportionately small on a 10" screen. Consider scaling ExpressiveFlower size with MediaQuery.sizeOf()
✅ Never blocks onboarding on denial outcome — correct: permission denial → still advances
✅ Loading state on "Continue" button while OS dialog is up — prevents user confusion
⚠️Issue: The explanation text references "full-screen-intent" and "silently blocks" — slightly technical for a general audience. Could soften: "Without this, your alarm might not appear on screen when you're sleeping."
Accessibility
✅ IconButton back button with tooltip: 'Back'
✅ Button disabled state communicated via onPressed: requesting ? null : onContinue
⚠️ Loading state (CircularProgressIndicator replacing "Continue" text) has no semantic label — screen reader won't know what's happening
Architecture Adherence
✅ Pure UI widget — permission logic is in _OnboardingPageState._continueFromNotificationRationale()
✅ StatelessWidget — all state lives in the parent (correct)
Responsive Layout
✅ SafeArea + Column with Expanded for body — text won't overflow under floating keyboard
⚠️ No maxWidth constraint — on tablets the Padding(20, 0, 20, 0) stretches the body text to the full tablet width, which can exceed the 75-char ideal line length
Not fully read in this review, but structure follows same _NotificationRationaleCard pattern.
Key Notes
OEM-specific instructions (Xiaomi, Samsung, etc.) — most important practical step for alarm reliability
Back navigation implemented to notification rationale step
✅ Reachable from Profile page post-onboarding for users who skipped or need to re-do it
Screen 4: Home Page (HomePage)
Visual Design
App bar:AppleGlassContainer pill (22dp radius) — glassmorphic header with BackdropFilter
Left: greeting text + "Awaken" app name
Right: streak flame pill (animated) + StreakTierAvatarRing around CurrentUserAvatarButton
Stats row: 3 StatTiles in a Row — streak, owned area, rank — with hasError state for all
Wake-up tax:WakeUpTaxMeter shown conditionally when multiplier > 1.0
Next alarm card:_NextAlarmCardBloc — a distinct card showing next upcoming alarm
Activity feed: List of _ActivityEntry items with relative timestamps
UX Flow
✅ RefreshIndicator with AlwaysScrollableScrollPhysics — pull to refresh works on short content
✅ Timer.periodic refreshes relative timestamps ("5m ago") without full page rebuilds
✅ New user: "Dismiss an alarm or capture territory to build your stats." — helpful empty state for zero stats
✅ CTAs (onOpenAlarms, onOpenTerritory, onOpenSquad) navigate via shell callbacks — no coupling to Navigator
⚠️Issue: The greeting text + "Awaken" name takes two lines; on small phones (360dp wide) the streak pill + avatar row might crowd the right side. No width constraint on the left column, so if greeting is long it could push against the right row
Accessibility
✅ StreakTierAvatarRing has Semantics(label: '${tier.label} streak tier')
✅ CurrentUserAvatarButton has Semantics(label: 'Profile', button: true)
✅ StatTilehasError state exposes "Couldn't load" + errorContainer color
⚠️ Streak flame pill has no semantic label — reads as plain text number "5" with no context for screen readers
⚠️_ActivityEntry items — unclear if each row has a meaningful semantic label
Architecture Adherence
✅ HomeCubit drives all state — no direct data-fetching in build()
✅ BlocProvider<HomeCubit> scoped to this page — not global, correct for page-local state
⚠️Timer.periodic for relative timestamp refresh lives in _RecentActivitySection widget — this is a presentational concern but could be argued to belong in the Cubit
Responsive Layout
✅ SingleChildScrollView with bottom padding (120dp) — clears nav bar
✅ StatTile uses FittedBox(scaleDown) to prevent number overflow
⚠️ Stats row doesn't adapt to tablet: 3 tiles at equal Expanded flex look fine on mobile but could become wide, awkward blocks on a 10" tablet. Consider a GridView.builder or maxWidth constraint at ≥600dp
⚠️ No LayoutBuilder breakpoint switch for the activity feed — on tablets the list stretches edge-to-edge, violating the "max 75 chars per line" guideline
✅ Bottom padding 120dp — list scrolls clear of FAB + glassmorphic nav bar
✅ LayoutBuilder — compact vs expanded card layout
✅ "tap to preview" subtitle — discoverable preview flow
⚠️Issue: No swipe-to-delete gesture on alarm cards — common iOS/Android pattern users expect. Delete is only accessible via card tap → edit sheet → delete button — requires 3 taps
⚠️Issue: The "bug_report" icon in the app bar for the self-test is confusing to non-technical users — it suggests "report a bug" not "test alarm reliability"
⚠️Issue: The "battery_charging_full" icon in the app bar maps to battery exemption settings, which is not obvious
Accessibility
✅ All icon buttons are 44×44 with Tooltip
✅ ExpressiveSwitch has Semantics(toggled, label: 'Alarm on/off', button: true)
✅ ExpressiveSwitch tap target is 56×48 (exceeds 44×44)
⚠️_AlarmCard as a whole has no semantic label that describes the entire alarm (time + exercise + enabled/disabled) to screen readers who might not explore all child elements
Architecture Adherence
✅ AlarmCubit is provided by AppShellPage's ancestor — alarm list is a pure observer
✅ All mutations (create/delete/toggle) go through AlarmCubit use cases
✅ _AlarmCard is a pure display widget — all callbacks go up to page
Responsive Layout
✅ LayoutBuilder inside the card adapts layout
✅ Bottom padding 120dp prevents FAB overlap
⚠️ On tablets (≥600dp): The single-column alarm list with wide cards reads as an unoptimized phone layout. Consider a 2-column GridView or centered ConstrainedBox(maxWidth: 600) for tablets
Screen 6: Alarm Ring Page (AlarmRingPage)
Visual Design
Background: Full-screen scheme.errorContainer — deliberate urgency color (red/danger family)
Time display: 16px tabular-figures clock in onErrorContainer — subtle but present
Ringing bell animation:
ExpressiveFlower(128px) in scheme.error with bell icon
Dual expanding-ring pulse animation (two staggered _ringAt painters at 0° and 180° offset)
Wake-up tax pill:scheme.error colored pill with warning icon — only shown if tax > 1.0x
CTA button (_StartWorkoutButton): Shape-morphs pill→rounded-rect on press (animateContainer border-radius 24→999)
UX Flow
✅ PopScope(canPop: isPreview) — back blocked for real alarms, allowed for preview
✅ Double-tap guard via _startingWorkout synchronous flag — prevents double camera sessions
✅ cubit.setVerificationInProgress(true/false) — steps overlay aside so VerificationPage is visible
✅ Loading state ("Opening camera…") while camera/ML Kit warms up — no silent blank screen
✅ Haptic feedback (mediumImpact()) on successful dismissal
⚠️Issue: "Time to squat!" and "Time to push up!" headers are grammatically inconsistent — should be "Time to squat!" / "Time to do push-ups!" or both in noun form ("Squats" / "Push-ups")
⚠️Issue: No visible escape hatch from the real-alarm ring screen for users who genuinely cannot exercise — the only exit is completing verification or the physical device's OS-level alarm stop. A "Can't exercise today?" option (logged but allowed) should exist even on real alarms
Accessibility
✅ _StartWorkoutButton has Semantics(button: true, label: 'Start workout to dismiss')
⚠️ No liveRegion on the clock or rep count — screen readers won't announce the time or rep count changes
⚠️ The pulse-ring animation is purely visual — correct, it adds urgency but has audio/haptic fallbacks (ringtone + vibration)
Architecture Adherence
✅ AlarmRingPage reads from AlarmCubit via context.watch — reactive, no direct use case calls
✅ Business logic (complete workout, dismiss) flows through AlarmCubit.completeWorkout()
✅ _RingingBell, _StartWorkoutButton are private widget classes — not helper methods returning Widget (correct per rules)
Responsive Layout
✅ SafeArea + centered Column — adapts to all screen heights
✅ Flexible on the header text — won't overflow on small screens
⚠️ 128px flower + 84px rep count + button may be cramped on very short devices (e.g., Galaxy Fold outer screen). The Stack+Positioned(bottom: 8) button approach doesn't guarantee the central content won't overlap on screens shorter than ~600px
Screen 7: Verification Page (VerificationPage)
Visual Design
Background: Full-screen black (Colors.black) — maximizes camera contrast
Camera layer:CameraPreview mirrored horizontally (front camera "mirror" expectation) via Matrix4.rotationY(π)
Skeleton overlay:SkeletonPainter (CustomPainter) — joint dots + bone lines on detected pose
Status banner (_StatusBanner): Semi-transparent frosted pill with pulsing red recording dot + status text
Rep segments (_RepSegments): Row of ≤20 animated fill segments — smart bucketing for >20 target reps
Rep counter (_RepCounter): 176px circular progress ring — _RingPainter fills clockwise, contains large rep digit + "of N" label
✅ buildWhen on top-level BlocBuilder — only rebuilds view kind on status category changes (not every frame)
✅ RepaintBoundary isolates camera texture from skeleton/overlay repaints
✅ _RepCounter uses BlocConsumer — bump animation triggers as side effect of rep increase, not on rebuild
✅ _StatusBanner is Semantics(liveRegion: true) — screen reader announces status changes
✅ _RepCounter is Semantics(liveRegion: true, label: 'N of M squats') — rep count announced
✅ "I can't do this exercise today" escape hatch always visible (except when complete)
✅ PopScope with confirmation dialog when mid-progress
✅ openAppSettings() shortcut in permission-denied state
⚠️Issue:_StatusBanner message "Can't see you clearly — step back or find better light." uses circular(18) — wraps to 2 lines on narrow phones. The fix (non-pill radius) was applied but the message itself is still quite long. Consider shortening to "Step back & find better light."
⚠️Issue: No live audio/haptic confirmation per rep for users holding the phone at arm's length — the _triggerBump() haptic + scale bump are good, but the visual counter is face-down during a push-up
Accessibility
✅ liveRegion on both status banner and rep counter
✅ ExcludeSemantics on visual ring/digits (semantic label covers it)
✅ AppLifecycleState pause/resume — camera yields properly on background
⚠️ Skeleton painter has no semantic description of pose quality (good/bad form)
⚠️ Dark background with white text — contrast is fine, but the frosted banner (Colors.black.withOpacity(0.54)) against a bright camera background could drop below 4.5:1 in some lighting
Architecture Adherence
✅ VerificationCubit drives all state — camera/ML Kit are injected via DI
✅ _CameraView is a StatelessWidget that reads from DI singleton — not tightly coupled to state
✅ SkeletonPainter is a pure CustomPainter — pure presentation, no logic
✅ App lifecycle management in _VerificationViewState (not in Cubit) — correct: UI concern
Responsive Layout
✅ Stack(fit: StackFit.expand) — camera fills entire screen correctly on all aspect ratios
✅ SafeArea wraps the overlay column — safe on notched screens
✅ _RepSegments uses Row + Expanded — adapts to any width
⚠️ The 176px rep-counter circle is fixed-size — on tablets it will appear small and off-center. Consider scaling with MediaQuery.sizeOf() on larger screens
Screen 8: Territory Page (TerritoryPage)
Visual Design
Full-screen MapLibre map — OpenFreeMap vector tiles with Pokémon GO-inspired basemap recolor
✅ "Steal back" CTA opens ActiveRunPage focused on rival's location
⚠️Issue: Map UI controls (3D toggle, squad heatmap toggle, rival toggle) need clear iconography — these are non-obvious to first-time users
⚠️Issue: Territory map has no legend — user can't distinguish owned vs rival vs at-risk without prior knowledge of the color system
⚠️Issue:OSM attribution must be visible per OpenStreetMap/OpenFreeMap licensing — verify it's always legible against the map background
Accessibility
❌ Major gap: Map content (polygons, bounty zones, squad heatmap) has no semantic overlay — completely invisible to screen readers. This is a platform limitation of maplibre_gl (native map view), but there's no fallback text description of what the map shows
✅ Sync badge has Tooltip with descriptive message
⚠️ Floating action buttons/controls need Semantics labels
Architecture Adherence
✅ Use cases injected via getIt<WatchTerritories>() etc. — no direct Supabase calls in page
✅ Map-layer state (_territoryLayersReady, _styleReady) is page-local — correct (UI concern)
⚠️TerritoryPage is a 2177-line StatefulWidget — extremely large. Per architecture best practices (SOLID, single responsibility), this should be decomposed into:
TerritoryMapController (map lifecycle)
TerritoryLayerManager (layer add/update logic)
SquadHeatmapManager
BountyZoneManager
RivalController
Responsive Layout
✅ MapLibreMap fills available space via Expanded/Stack
✅ SafeArea wraps UI chrome
⚠️ At ≥600dp (NavigationRail mode), the map's right-edge controls may be covered by the rail — the Stack-positioned overlays need to account for the rail's width in the medium-window layout
Screen 9: Active Run Page (ActiveRunPage)
Visual Design
Full-screen map with route Line annotation + start/current Circle markers
Stats overlay:AppleGlassContainer pill chips for elapsed time / distance / pace
Auto-follow indicator: Camera auto-lock icon — tappable to unlock
Stop button: Large pill FilledButton at bottom
Style: Same Pokémon GO-inspired basemap recolor as TerritoryPage
UX Flow
✅ Map built once, BlocListener updates annotations imperatively — no rebuild on every GPS tick
✅ focusLocation param — "Steal back" CTA opens page pre-centered on rival's zone
✅ _autoFollow toggle — user can pan freely, camera stops auto-following
✅ TerritoryCaptureSheet shown after submit_run() success
⚠️Issue: No "End run early?" confirmation dialog — user could accidentally tap "Stop" mid-run with no undo
Accessibility
⚠️ Stat chip values read as plain numbers — no context ("00:42" reads as "42" not "42 seconds")
⚠️ Map has no semantic description of the route
Architecture Adherence
✅ RunTrackingCubit drives all run state
✅ submit_run() called through repository use case — not directly from page
Responsive Layout
✅ Full-screen map fills all form factors
⚠️ Stat chips are positioned with fixed padding — on very wide tablets they may cluster to the left unnecessarily
⚠️Issue: No way to leave a squad from this screen — leave/remove requires digging into settings (if it exists at all)
⚠️Issue: Invite code not visible from the Squad tab — user needs to navigate to create-squad flow to find it. An invite code share button should be prominent on the main squad view
Accessibility
✅ Leaderboard icon target is 44×44
✅ ExpressiveLoader with reduce-motion guard for loading state
⚠️ Squad member cards likely lack descriptive semantic labels combining all member info (name + area + tier)
Architecture Adherence
✅ SquadCubit scoped inside SquadPage via BlocProvider — not global
✅ _WeeklyResetCeremonyGate is a separate widget class — correct (not a helper method)
✅ getIt<SquadCubit>() — DI registered cubit
Responsive Layout
⚠️ Member list is a Column in a SingleChildScrollView — works on mobile but on tablets with NavigationRail, there's no width constraint; member cards stretch edge-to-edge
⚠️ Consider a GridView.builder for member cards on ≥600dp screens
Screen 11: Profile Page (ProfilePage)
Visual Design
App bar:AppleGlassContainer pill — back arrow (iOS chevron) + "Profile" title
User card: Glassmorphic card with StreakTierAvatarRing avatar + name (editable) + email
✅ Delete account: destructive, should require confirmation — verify this is implemented
✅ ProfileCubit watches current user + streak + area + rank
⚠️Issue: "Progress syncs across devices" subtitle for linked accounts is positive but doesn't appear until you scroll — the most valuable user-education moment (why link an account?) is below the fold on small phones
Accessibility
✅ All icon buttons 44×44 with Tooltip
✅ Avatar fallback uses avatarInitial() — safe for empty strings
✅ ProfileCubit drives all user data — no direct Supabase calls in UI
✅ Auth use cases injected: SignInWithGoogle, LinkWithEmail, DeleteAccount, etc.
✅ ThemeModeCubit for theme toggle — correct separation
Responsive Layout
✅ SingleChildScrollView — all content scrollable
⚠️ On tablets: the user card and settings list stretch full width. Wrapping in Center + ConstrainedBox(maxWidth: 560) would produce a much better tablet layout
Medium+ (≥600dp): Standard NavigationRail — no glassmorphism, solid background
UX Flow
✅ IndexedStack preserves scroll/animation state across tab switches
✅ late final List<Widget> _pages built in initState — widgets not recreated on tab switch
✅ _ShellSyncBadge stream updates independently — no shell rebuild for sync state changes
Responsive Layout
✅ MediaQuery.sizeOf(context).width >= 600 breakpoint — correct per responsive skill (uses MediaQuery.sizeOf, not OrientationBuilder)
✅ NavigationRail on medium+ with Expanded(child: body) — correct rail+content layout
⚠️ The glassmorphic nav bar Padding(16, 0, 16, 8) creates side margins — on very narrow screens (<375dp), this leaves only ~343dp content width, which could cause overflow in some edge cases
Cross-Cutting Architecture Review
Adherence to Architecture Skill
Principle
Status
Notes
Domain layer (use cases)
✅ Fully implemented
All features have use case classes
Repository pattern
✅ Implemented
Local (Drift) + remote (Supabase) sources
MVVM pattern
✅ via Cubit/Bloc
Cubits = ViewModel role
DI with constructor injection
✅ get_it + injectable
All registered
Private widget classes (not helper methods)
✅ All screens use class _FooWidget extends StatelessWidget
Correct
Build methods lean (no logic)
✅ Generally respected
TerritoryPage has complex build that warrants decomposition
Separation UI/business logic
✅
Only minor Timer.periodic case in Home
Adherence to Responsive Layout Skill
Check
Status
Notes
MediaQuery.sizeOf() for breakpoints
✅
Used in AdaptiveNavScaffold
LayoutBuilder for parent-space decisions
✅
Alarm list, onboarding
No OrientationBuilder for layout switching
✅
Not used
No device-type checks
✅
Width-based breakpoints only
Expanded/Flexible for space distribution
✅
Used correctly throughout
ConstrainedBox maxWidth on large screens
⚠️
Applied in onboarding; missing in Squad, Profile, Home activity feed
ListView.builder for long lists
⚠️
Activity feed uses Column — for short lists this is acceptable, but should use ListView.builder if feed can grow
No orientation lock
✅
Not locked
Multi-input support (mouse/keyboard)
⚠️
No explicit keyboard shortcut support; mouse hover works via Material ink
Summary of Key UI/UX Issues & Recommendations
High Priority
Issue
Screen(s)
Recommendation
No escape hatch from real alarm ring
AlarmRingPage
Add "Can't exercise today?" with streak penalty warning
TerritoryPage 2177-line monolith
TerritoryPage
Decompose into sub-components per SRP
Map has no accessibility description
Territory, ActiveRun
Add a text-mode summary panel or screen-reader-only description
No swipe-to-delete on alarm cards
AlarmListPage
Add Dismissible widget wrapping each alarm card
Invite code not visible on Squad tab
SquadPage
Add a share/copy invite code button on the squad header
Medium Priority
Issue
Screen(s)
Recommendation
No ConstrainedBox(maxWidth) on tablets
Profile, Squad, Home
Add Center + ConstrainedBox(maxWidth: 600)
Stat row not adapted for tablets
Home, Profile
Switch to GridView or Row with constrained tile width at ≥600dp
App bar icons not intuitive
AlarmListPage
Replace bug_report with fact_check or alarm_on; battery_charging_full with settings
ExpressiveFlower icons have no semantic labels
Onboarding, empty states
Add Semantics(label: '...') wrappers
No territory map legend
TerritoryPage
Add a collapsible legend chip (owned/rival/at-risk/bounty)
Low Priority
Issue
Screen(s)
Recommendation
Marketing card flower size fixed on tablets
Onboarding
Scale with MediaQuery.sizeOf().width * 0.35
Grammar inconsistency "push up!"
AlarmRingPage
Normalize to "Time to push up!" or "Push-ups!"
Notification rationale text too technical
Onboarding step 2
Soften for general audience
Leaderboard empty state for no-run users
LeaderboardsSheet
Add "Start a run to appear on the nearby leaderboard"