Skip to main content

๐Ÿ“ฑ Frontend Overview

Cross-Platform Flutter Application

:::tip TL;DR

  • One Flutter codebase โ†’ iOS, Android, Web
  • Provider for state โ€ข Firebase SDK for auth/storage/functions/messaging
  • Videos are uploaded through requestUploadPath as a single file today; client-side chunking is deferred
  • All Cloud Function calls go to europe-west1 โ€” don't forget the region
  • 17 Ace components + semantic design tokens (colors, typography, spacing, motion) โ€” see Design System
  • New users see sample data until their first upload โ€” see Key Features :::

The AceSense frontend is a cross-platform application built with Flutter, supporting iOS, Android, and Web from a single codebase.


๐ŸŽฏ Application Overviewโ€‹

:::info Tech Stack

RepositoryCommitLast Updated
acesense-frontendfff006e2026-05-28 (v1.0.4+1)
  • Framework: Flutter 3.10+ / Dart SDK ^3.10.3
  • State: Provider
  • Backend: Firebase (Auth, Firestore, Storage, Functions, Messaging)
  • Video: video_player metadata extraction + Firebase Storage upload
  • Charts: fl_chart for analytics visualization
  • UI: Material Design 3 with custom theming :::

๐Ÿ—๏ธ Application Architectureโ€‹


๐Ÿ“ฑ Screen Mapโ€‹


๐Ÿงญ Navigation Structureโ€‹


๐Ÿ“‚ Project Structureโ€‹

Regenerated from the tree on 2026-08-16. This section was previously wrong in a way that mattered: it documented screens/, widgets/, providers/, navigation/ and billing/ as top-level directories and an l10n/messages/*.json catalogue. None of those exist. The app is organised by feature, state lives in state/, routing is go_router, and localisation is ARB-based.

lib/
โ”œโ”€โ”€ main.dart # Entry point + MaterialApp
โ”œโ”€โ”€ firebase_options.dart # FlutterFire multi-platform config
โ”œโ”€โ”€ config/ # AppConfig + build flags (kUseMockData, App Check)
โ”œโ”€โ”€ data/ # mock.dart โ€” drill catalogue, sample notifications
โ”œโ”€โ”€ dev/ # Developer-only preview harness (never shipped)
โ”œโ”€โ”€ ds/ # Design system
โ”‚ โ”œโ”€โ”€ theme.dart
โ”‚ โ”œโ”€โ”€ tokens/ # DsColors, DsType, DsSpacing, DsRadius, DsMotion
โ”‚ โ””โ”€โ”€ widgets/ # DsButton, DsCard, DsBadge, DsAppShell, โ€ฆ
โ”œโ”€โ”€ features/ # One directory per feature, screens live here
โ”‚ โ”œโ”€โ”€ analysis/ # match_analysis_screen, tabs/, mini_player, court_minimap
โ”‚ โ”œโ”€โ”€ auth/ # welcome journey, sign in/up, onboarding, auth_layout
โ”‚ โ”œโ”€โ”€ billing/ # billing_screen, upgrade_prompt
โ”‚ โ”œโ”€โ”€ body/ # training-load + prevention
โ”‚ โ”œโ”€โ”€ coach/ # player-facing coach dashboard
โ”‚ โ”œโ”€โ”€ coach_mode/ # coach roster + student notes
โ”‚ โ”œโ”€โ”€ flows/ # app_flows.dart โ€” upload/record/consent/share flows
โ”‚ โ”œโ”€โ”€ help/ how_it_works/ legal/ notifications/ settings/ update/
โ”‚ โ”œโ”€โ”€ home/ sessions/ new_session/ practice/ profile/
โ”‚ โ”œโ”€โ”€ progression/ # badges, roadmap
โ”‚ โ””โ”€โ”€ wrapped/ # weekly recap
โ”œโ”€โ”€ l10n/
โ”‚ โ”œโ”€โ”€ app_localizations.dart # Re-export shim
โ”‚ โ”œโ”€โ”€ arb/ # app_{en,de,es,fr,it,sv}.arb โ€” 1542 keys each
โ”‚ โ””โ”€โ”€ generated/ # flutter gen-l10n output, do not hand-edit
โ”œโ”€โ”€ models/ # job, notification, tennis_analysis, user_stats
โ”œโ”€โ”€ routing/ # app_router.dart (go_router) + NotFoundScreen
โ”œโ”€โ”€ services/ # Firebase + domain services (see below)
โ””โ”€โ”€ state/ # Riverpod providers
โ”œโ”€โ”€ locale_provider.dart
โ”œโ”€โ”€ notifications_provider.dart
โ”œโ”€โ”€ player_coach_provider.dart
โ””โ”€โ”€ progress_provider.dart

117 Dart files under lib/, 34 test files under test/.

Localisationโ€‹

Six locales: en de es fr it sv, 1542 keys each, no drift. Source of truth is lib/l10n/arb/app_*.arb; lib/l10n/generated/ is output โ€” never hand-edit it. Regenerate with flutter gen-l10n.

Two rules the codebase now enforces:

  • tool/l10n_audit.py runs in CI and fails the build on a hardcoded user-facing string. Excuse a string only in its ALLOW_EXACT/ALLOW_SITES list, with the reason.
  • Data layers carry facts, not words. PlayerCoachingService, DrillItem and ShotType are reachable from cached providers, so any copy resolved there would survive a language switch. They expose ids and numbers; the widget layer resolves copy per render. ShotType.stableName is the deliberate exception โ€” a locale-independent key for the one comparison that needs it.

๐Ÿ”ง Key Servicesโ€‹

AuthServiceโ€‹

VideoServiceโ€‹

Other servicesโ€‹

ServicePurpose
FirestoreServiceThin data layer over the users/, jobs/, feedback/ collections
StorageServiceVideo preprocess + single-file upload pipeline + signed URL retrieval
AiConsentServiceFirst-upload AI analysis consent (App Store Guideline 5.1.1)

๐ŸŒ Localizationโ€‹

LanguageCodeStatus
๐Ÿ‡ฌ๐Ÿ‡ง Englishenโœ… Complete
๐Ÿ‡ฉ๐Ÿ‡ช Germandeโœ… Complete
๐Ÿ‡ซ๐Ÿ‡ท Frenchfrโœ… Complete
๐Ÿ‡ช๐Ÿ‡ธ Spanishesโœ… Complete
๐Ÿ‡ฎ๐Ÿ‡น Italianitโœ… Complete
๐Ÿ‡ธ๐Ÿ‡ช Swedishsvโœ… Complete

๐Ÿ“Š State Managementโ€‹


๐Ÿงช Testingโ€‹

TypeToolCoverage Target
Unit Testsflutter_test80%
Widget Testsflutter_test70%
Integrationintegration_testCritical paths
E2EPatrolHappy paths
# Run all tests
flutter test

# Run with coverage
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html

๐Ÿš€ Build & Deployโ€‹

Developmentโ€‹

# Run on device
flutter run

# Run on web
flutter run -d chrome

# Run on specific device
flutter run -d <device_id>

Productionโ€‹

# iOS
flutter build ios --release

# Android
flutter build appbundle --release

# Web
flutter build web --release
firebase deploy --only hosting:app

โ˜๏ธ Cloud Functions Integrationโ€‹

The app integrates with Firebase Cloud Functions for video processing.

Callable Functionsโ€‹

FunctionPurpose
requestUploadPathValidate video metadata, create job doc, return upload path
reportUploadFailureMark a still-pending job failed if the client upload fails before Storage finalization
mergeChunkResultsDeferred chunk result merge support; not called by the current upload flow

Example Usageโ€‹

final functions = FirebaseFunctions.instanceFor(region: 'europe-west1');

// Request upload path
final callable = functions.httpsCallable('requestUploadPath');
final result = await callable.call({
'fileName': 'match.mp4',
'fileSize': 52428800,
'duration': 180,
'width': 1920,
'height': 1080,
'format': 'mp4',
});

final uploadPath = result.data['uploadPath'];
final jobId = result.data['jobId'];

try {
// Upload file bytes to uploadPath with Firebase Storage.
} catch (error) {
await functions.httpsCallable('reportUploadFailure').call({
'jobId': jobId,
'errorMessage': error.toString(),
});
rethrow;
}

Storage Triggerโ€‹

The processVideoOnUpload function automatically triggers when videos are uploaded to the videos/{uid}/{sessionId}/ folder. If the upload fails before Storage finalization, the client calls reportUploadFailure so the job does not remain stuck in pending and the user's concurrent slot is released.


๐ŸŽฏ Next Stepsโ€‹