# Solution Design Document: Login & Authentication Flow

**Spec ID:** 002-login-auth-flow
**Created:** 2024-12-24
**Status:** Draft
**Author:** Engineering Team

---

## 1. Overview

### 1.1 Purpose

This document describes the technical architecture and implementation approach for the BuyerKiosk Team mobile app's login and authentication flow. It covers the user-scoped JWT authentication system, store selection, biometric authentication, token management, and clock-in integration.

### 1.2 Scope

**In Scope (P0 Requirements):**
- FR-001: Splash Screen with branding
- FR-002: Email/Password Login
- FR-003: Store Selection (Multi-Store Users)
- FR-004: Store Switcher
- FR-005: Biometric Authentication
- FR-006: Token Management
- FR-007: Home Screen with Clock Status
- FR-008: Geofence Detection (foreground-only for v1)
- FR-009: Bottom Navigation
- FR-010: Access Revocation Handling

**Out of Scope (v1):**
- Background geofence monitoring (P1)
- Smart store detection banners (P1)
- Geofence entry notifications (P1)
- Session activity indicator (P2)

### 1.3 Context Sources

| Source | File | Relevance |
|--------|------|-----------|
| PRD | `docs/specs/002-login-auth-flow/product-requirements.md` | Primary requirements |
| API Spec | `docs/employee-api.yaml` | API contracts |
| Style Guide | `docs/STYLE_GUIDE.md` | UI design tokens |
| CLAUDE.md | `CLAUDE.md` | Project conventions |
| Existing Code | `lib/**/*.dart` | Current patterns |

---

## 2. Architecture Decisions

### ADR-1: User-Scoped JWT Authentication
**Decision:** Login returns a user-scoped JWT with stores array; user selects store after authentication.

**Rationale:**
- Single token works across all assigned stores
- No re-authentication needed when switching stores
- Simpler mobile UX for multi-store employees
- Aligns with PRD user journeys

**Trade-offs:**
- Token payload doesn't include store context
- Store-specific permissions checked server-side on each request

### ADR-2: Global Riverpod Provider for Store Context
**Decision:** Use a global `storeProvider` (Notifier) for reactive store selection.

**Rationale:**
- All screens reactively update when store changes
- Consistent with existing Riverpod patterns
- Easy to persist last-selected store

**Implementation:**
```dart
final storeProvider = NotifierProvider<StoreNotifier, StoreState>(StoreNotifier.new);
```

### ADR-3: Foreground-Only Geofencing for v1
**Decision:** Check location only when app is in foreground; no background monitoring.

**Rationale:**
- Reduces complexity for initial release
- Avoids paid plugin dependencies (flutter_background_geolocation)
- Sufficient for clock-in validation use case
- Can iterate to background monitoring in P1

### ADR-4: Repository per Feature
**Decision:** Create `AuthRepository` and `ClockRepository` for v1.

**Rationale:**
- Separates API calls from state management
- Enables unit testing with mocked repositories
- Follows Clean Architecture already in project
- Easy to add caching or offline support later

### ADR-5: Token Refresh on 401 Response
**Decision:** Intercept 401 responses in Dio interceptor, attempt refresh, retry original request.

**Rationale:**
- Simpler than proactive refresh
- No need to track token expiry timestamps
- Handles edge cases (clock drift, early expiry)

### ADR-6: Bottom Sheet for Store Picker
**Decision:** Use a bottom sheet modal for store selection.

**Rationale:**
- Common mobile pattern, familiar to users
- Quick access without leaving current screen
- Supports the store switcher in app bar (FR-004)

### ADR-7: Client + Server Geofence Validation
**Decision:** Check geofence client-side for UX, server validates for security.

**Rationale:**
- Client check enables/disables clock button immediately
- Server check prevents tampering or GPS spoofing
- Belt-and-suspenders approach for accuracy

---

## 3. System Architecture

### 3.1 High-Level Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                     Flutter Mobile App                          │
├─────────────────────────────────────────────────────────────────┤
│  Presentation Layer                                             │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐            │
│  │ SplashScreen │ │ LoginScreen  │ │ HomeScreen   │            │
│  └──────────────┘ └──────────────┘ └──────────────┘            │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐            │
│  │BiometricScreen│ │StorePicker   │ │BottomNav    │            │
│  └──────────────┘ └──────────────┘ └──────────────┘            │
├─────────────────────────────────────────────────────────────────┤
│  State Management (Riverpod)                                    │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐            │
│  │ AuthNotifier │ │StoreNotifier │ │ClockNotifier │            │
│  └──────────────┘ └──────────────┘ └──────────────┘            │
├─────────────────────────────────────────────────────────────────┤
│  Domain Layer                                                   │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐            │
│  │     User     │ │StoreAssignment│ │ ClockStatus │            │
│  └──────────────┘ └──────────────┘ └──────────────┘            │
├─────────────────────────────────────────────────────────────────┤
│  Repository Layer                                               │
│  ┌──────────────┐ ┌──────────────┐                              │
│  │AuthRepository│ │ClockRepository│                             │
│  └──────────────┘ └──────────────┘                              │
├─────────────────────────────────────────────────────────────────┤
│  Data Layer                                                     │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐            │
│  │  ApiClient   │ │StorageService│ │GeolocationSvc│            │
│  └──────────────┘ └──────────────┘ └──────────────┘            │
│  ┌──────────────┐ ┌──────────────┐                              │
│  │ UserModel    │ │ ClockModels  │                              │
│  └──────────────┘ └──────────────┘                              │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    BuyerKiosk Backend API                       │
│                 /api/mobile/scheduling/*                        │
└─────────────────────────────────────────────────────────────────┘
```

### 3.2 Directory Structure

```
lib/
├── core/
│   ├── constants/
│   │   ├── api_constants.dart      # API URLs, endpoints (UPDATE)
│   │   ├── app_constants.dart      # Storage keys, etc.
│   │   └── ably_constants.dart     # Real-time config (future)
│   ├── network/
│   │   ├── api_client.dart         # Dio client (EXISTS)
│   │   └── api_interceptors.dart   # Auth + refresh (UPDATE)
│   ├── services/
│   │   ├── storage_service.dart    # Secure storage (UPDATE)
│   │   ├── biometric_service.dart  # Face ID/Touch ID (EXISTS)
│   │   └── geolocation_service.dart # Location (UPDATE)
│   ├── theme/
│   │   ├── app_colors.dart         # Color palette (EXISTS)
│   │   └── app_theme.dart          # Theme config (EXISTS)
│   └── errors/
│       └── app_exceptions.dart     # Custom exceptions (EXISTS)
│
├── data/
│   ├── models/
│   │   ├── user_model.dart         # User + LoginResponse (UPDATE)
│   │   └── clock_model.dart        # ClockStatus, ClockIn (NEW)
│   ├── mappers/
│   │   ├── user_mapper.dart        # Model → Entity (UPDATE)
│   │   └── clock_mapper.dart       # Model → Entity (NEW)
│   └── repositories/
│       ├── auth_repository.dart    # Auth API calls (NEW)
│       └── clock_repository.dart   # Clock API calls (NEW)
│
├── domain/
│   └── entities/
│       ├── user.dart               # User entity (UPDATE)
│       ├── auth_state.dart         # Auth states (EXISTS)
│       ├── store.dart              # Store entity (NEW)
│       └── clock_status.dart       # Clock entity (NEW)
│
├── presentation/
│   ├── providers/
│   │   ├── auth_provider.dart      # Auth state (UPDATE)
│   │   ├── store_provider.dart     # Store selection (NEW)
│   │   └── clock_provider.dart     # Clock state (NEW)
│   ├── screens/
│   │   ├── auth/
│   │   │   ├── login_screen.dart   # Email/password (UPDATE)
│   │   │   └── biometric_screen.dart # Biometric auth (UPDATE)
│   │   ├── home/
│   │   │   └── home_screen.dart    # Dashboard (UPDATE)
│   │   ├── schedule/
│   │   │   └── schedule_screen.dart # Schedule (EXISTS)
│   │   └── settings/
│   │       └── settings_screen.dart # Settings (UPDATE)
│   └── widgets/
│       ├── splash_screen.dart      # Branded splash (NEW)
│       ├── store_picker.dart       # Bottom sheet picker (NEW)
│       ├── clock_status_card.dart  # Clock display (NEW)
│       └── store_header.dart       # App bar with store (NEW)
│
├── router/
│   └── app_router.dart             # GoRouter config (UPDATE)
│
├── app.dart                        # App widget (EXISTS)
└── main.dart                       # Entry point (EXISTS)

assets/
├── images/
│   ├── logo_white.svg              # Splash logo (EXISTS)
│   └── logo_dark.svg               # Login logo (ADD)
└── fonts/
    └── Inter/                      # Font files (ADD if not using Google Fonts)
```

---

## 4. Component Design

### 4.1 Authentication Flow

#### 4.1.1 State Machine

```
┌─────────────┐
│  AuthInitial│ ─── app launch ──▶ checkAuthStatus()
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ AuthLoading │ ─── checking ───▶
└──────┬──────┘
       │
   ┌───┴───────────────────┐
   │                       │
   ▼                       ▼
No tokens              Has tokens
   │                       │
   ▼                       ▼
┌─────────────────┐   ┌────────────────────┐
│AuthUnauthenticated│   │Biometric enabled?  │
└────────┬────────┘   └─────────┬──────────┘
         │                      │
         │                 ┌────┴────┐
         │                 │         │
         │                Yes        No
         │                 │         │
         │                 ▼         ▼
         │        ┌──────────────┐ Validate token
         │        │AuthBiometric │     │
         │        │   Required   │     │
         │        └──────┬───────┘     │
         │               │             │
         │          authenticate()     │
         │               │             │
         │         ┌─────┴─────┐       │
         │         │           │       │
         │       Success     Fail      │
         │         │           │       │
         │         ▼           ▼       │
         │         │    Back to prompt │
         │         │           or      │
         │         │    skip to login  │
         ▼         ▼                   ▼
    ┌────────────────────────────────────┐
    │         AuthAuthenticated          │
    │  (user, stores, biometricEnabled) │
    └────────────────────────────────────┘
               │
               ▼
    ┌──────────────────────┐
    │  Multi-store user?   │
    └──────────┬───────────┘
               │
          ┌────┴────┐
          │         │
         Yes        No
          │         │
          ▼         ▼
    Show picker   Auto-select
          │         │
          └────┬────┘
               │
               ▼
    ┌──────────────────────┐
    │    Store Selected    │
    │    Navigate Home     │
    └──────────────────────┘
```

#### 4.1.2 AuthNotifier (Updated)

**File:** `lib/presentation/providers/auth_provider.dart`

```dart
/// Enhanced AuthState to include stores
sealed class AuthState extends Equatable {
  const AuthState();
}

class AuthInitial extends AuthState { ... }
class AuthLoading extends AuthState { ... }

class AuthAuthenticated extends AuthState {
  final User user;
  final List<Store> stores;        // NEW: List of assigned stores
  final bool biometricEnabled;
  final bool requiresStoreSelection; // NEW: true if stores.length > 1

  const AuthAuthenticated({
    required this.user,
    required this.stores,
    this.biometricEnabled = false,
    this.requiresStoreSelection = false,
  });
}

class AuthUnauthenticated extends AuthState { ... }
class AuthBiometricRequired extends AuthState { ... }
class AuthError extends AuthState { ... }
```

**Key Methods:**
```dart
class AuthNotifier extends Notifier<AuthState> {
  /// Check auth status on app launch
  Future<void> checkAuthStatus();

  /// Login with email/password
  Future<void> login({required String email, required String password});

  /// Authenticate with biometrics
  Future<void> authenticateWithBiometric();

  /// Refresh access token (called by interceptor)
  Future<bool> refreshToken();

  /// Logout and clear all tokens
  Future<void> logout();

  /// Enable biometric for future logins
  Future<bool> enableBiometric();

  /// Skip biometric and go to login
  void skipBiometricToLogin();

  /// Handle store access revocation (403)
  Future<void> handleAccessRevoked();
}
```

### 4.2 Store Management

#### 4.2.1 Store Entity

**File:** `lib/domain/entities/store.dart`

```dart
class Store extends Equatable {
  final String typeNum;       // e.g., "ou00"
  final String storeName;     // e.g., "Ottawa Store"
  final String role;          // e.g., "manager", "buyer"
  final int roleId;           // 1=Owner, 2=Manager, 4=Buyer
  final bool isManager;
  final int employeeId;       // Employee ID at this store
  final StoreGeofence? geofence; // Optional geofence data

  bool get isOwner => roleId == 1;
  bool get hasManagerAccess => roleId <= 2;
}

class StoreGeofence extends Equatable {
  final double latitude;
  final double longitude;
  final double radiusMeters;  // Default: 200m
}
```

#### 4.2.2 StoreNotifier

**File:** `lib/presentation/providers/store_provider.dart`

```dart
/// Store selection state
sealed class StoreState extends Equatable {
  const StoreState();
}

class StoreInitial extends StoreState { ... }
class StoreSelected extends StoreState {
  final Store store;
  const StoreSelected(this.store);
}
class StoreNone extends StoreState { ... } // No stores available

/// Provider for store selection
final storeProvider = NotifierProvider<StoreNotifier, StoreState>(
  StoreNotifier.new,
);

/// Convenience provider for current store
final currentStoreProvider = Provider<Store?>((ref) {
  final storeState = ref.watch(storeProvider);
  return storeState is StoreSelected ? storeState.store : null;
});

class StoreNotifier extends Notifier<StoreState> {
  @override
  StoreState build() => const StoreInitial();

  /// Select a store (after login or from picker)
  Future<void> selectStore(Store store) async {
    state = StoreSelected(store);
    // Persist to storage
    await _storage.setLastStore(store.typeNum);
  }

  /// Load last selected store
  Future<void> loadLastStore(List<Store> availableStores) async {
    final lastTypeNum = await _storage.getLastStore();
    if (lastTypeNum != null) {
      final store = availableStores.firstWhere(
        (s) => s.typeNum == lastTypeNum,
        orElse: () => availableStores.first,
      );
      state = StoreSelected(store);
    } else if (availableStores.length == 1) {
      state = StoreSelected(availableStores.first);
    } else {
      state = const StoreNone();
    }
  }

  /// Clear store (on logout)
  void clear() {
    state = const StoreInitial();
  }
}
```

### 4.3 Clock Status Management

#### 4.3.1 Clock Entities

**File:** `lib/domain/entities/clock_status.dart`

```dart
/// Current clock status for an employee at a store
class ClockStatus extends Equatable {
  final bool isClockedIn;
  final DateTime? lastPunchTime;
  final int? elapsedMinutes;
  final bool withinGeofence;
  final double? geofenceDistance;  // meters from store
  final bool canClockIn;
  final bool canClockOut;
  final String? clockWindowMessage; // e.g., "Too early to clock in"
  final Shift? currentShift;        // Today's shift if any
}

/// A scheduled shift
class Shift extends Equatable {
  final int shiftId;
  final DateTime startTime;
  final DateTime endTime;
  final String position;
  final String? positionColor;
  final double totalHours;
}

/// Result of a clock in/out action
class ClockResult extends Equatable {
  final bool success;
  final int? punchId;
  final DateTime? punchTime;
  final double? totalHours;  // For clock out
  final String? errorMessage;
}
```

#### 4.3.2 ClockNotifier

**File:** `lib/presentation/providers/clock_provider.dart`

```dart
/// Clock state
sealed class ClockState extends Equatable {
  const ClockState();
}

class ClockInitial extends ClockState { ... }
class ClockLoading extends ClockState { ... }

class ClockReady extends ClockState {
  final ClockStatus status;
  final bool isRefreshing;
  const ClockReady(this.status, {this.isRefreshing = false});
}

class ClockError extends ClockState {
  final String message;
  const ClockError(this.message);
}

class ClockActionSuccess extends ClockState {
  final ClockResult result;
  final bool wasClockIn;  // true for clock in, false for clock out
  const ClockActionSuccess(this.result, {required this.wasClockIn});
}

class ClockNotifier extends Notifier<ClockState> {
  @override
  ClockState build() {
    // Auto-fetch when store changes
    ref.listen(currentStoreProvider, (prev, next) {
      if (next != null && prev?.typeNum != next.typeNum) {
        fetchStatus();
      }
    });
    return const ClockInitial();
  }

  /// Fetch current clock status for selected store
  Future<void> fetchStatus() async {
    final store = ref.read(currentStoreProvider);
    if (store == null) return;

    state = const ClockLoading();

    try {
      // Get current location
      final position = await _geolocation.getCurrentPosition();

      // Fetch status from API
      final status = await _clockRepo.getStatus(
        typeNum: store.typeNum,
        latitude: position.latitude,
        longitude: position.longitude,
      );

      state = ClockReady(status);
    } catch (e) {
      state = ClockError(e.toString());
    }
  }

  /// Clock in at current location
  Future<void> clockIn() async {
    final store = ref.read(currentStoreProvider);
    if (store == null) return;

    state = const ClockLoading();

    try {
      final position = await _geolocation.getCurrentPosition();

      // Client-side geofence pre-check
      final inGeofence = _isWithinGeofence(position, store.geofence);
      if (!inGeofence) {
        state = const ClockError('You must be at the store to clock in');
        return;
      }

      final result = await _clockRepo.clockIn(
        typeNum: store.typeNum,
        latitude: position.latitude,
        longitude: position.longitude,
        accuracy: position.accuracy,
      );

      state = ClockActionSuccess(result, wasClockIn: true);

      // Refresh status after action
      await fetchStatus();
    } catch (e) {
      state = ClockError(e.toString());
    }
  }

  /// Clock out
  Future<void> clockOut() async { ... }
}
```

### 4.4 Repository Layer

#### 4.4.1 AuthRepository

**File:** `lib/data/repositories/auth_repository.dart`

```dart
abstract class AuthRepository {
  /// Login with email and password
  Future<LoginResult> login({
    required String email,
    required String password,
    String? deviceName,
  });

  /// Refresh access token
  Future<RefreshResult> refreshToken(String refreshToken);

  /// Logout (revoke refresh token)
  Future<void> logout(String refreshToken);

  /// Get current user profile
  Future<User> getMe();
}

class AuthRepositoryImpl implements AuthRepository {
  final ApiClient _api;

  AuthRepositoryImpl(this._api);

  @override
  Future<LoginResult> login({
    required String email,
    required String password,
    String? deviceName,
  }) async {
    final response = await _api.post<Map<String, dynamic>>(
      ApiConstants.login,
      data: {
        'email': email,
        'password': password,
        if (deviceName != null) 'deviceName': deviceName,
      },
    );

    return LoginResult.fromJson(response.data!);
  }

  // ... other methods
}
```

#### 4.4.2 ClockRepository

**File:** `lib/data/repositories/clock_repository.dart`

```dart
abstract class ClockRepository {
  /// Get clock status for a store
  Future<ClockStatus> getStatus({
    required String typeNum,
    required double latitude,
    required double longitude,
  });

  /// Clock in
  Future<ClockResult> clockIn({
    required String typeNum,
    required double latitude,
    required double longitude,
    required double accuracy,
  });

  /// Clock out
  Future<ClockResult> clockOut({
    required String typeNum,
    double? latitude,
    double? longitude,
    double? accuracy,
  });
}

class ClockRepositoryImpl implements ClockRepository {
  final ApiClient _api;

  ClockRepositoryImpl(this._api);

  @override
  Future<ClockStatus> getStatus({
    required String typeNum,
    required double latitude,
    required double longitude,
  }) async {
    final response = await _api.post<Map<String, dynamic>>(
      '/$typeNum/clock/status',
      data: {
        'latitude': latitude,
        'longitude': longitude,
      },
    );

    return ClockStatusMapper.fromJson(response.data!['data']);
  }

  // ... other methods
}
```

---

## 5. Data Models

### 5.1 Updated User Model

**File:** `lib/data/models/user_model.dart`

```dart
@freezed
abstract class LoginRequest with _$LoginRequest {
  const factory LoginRequest({
    required String email,     // Changed from username
    required String password,
    String? deviceName,
    String? deviceFingerprint,
  }) = _LoginRequest;

  factory LoginRequest.fromJson(Map<String, dynamic> json) =>
      _$LoginRequestFromJson(json);
}

@freezed
abstract class LoginResponse with _$LoginResponse {
  const factory LoginResponse({
    required String accessToken,
    required String refreshToken,
    required int expiresIn,
    required DateTime refreshExpiresAt,
    required LoginUserModel user,
    required List<LoginStoreModel> stores,
    @Default(false) bool requiresStoreSelection,
  }) = _LoginResponse;

  factory LoginResponse.fromJson(Map<String, dynamic> json) =>
      _$LoginResponseFromJson(json);
}

@freezed
abstract class LoginUserModel with _$LoginUserModel {
  const factory LoginUserModel({
    required int userId,
    required String firstName,
    required String lastName,
    String? email,
  }) = _LoginUserModel;

  factory LoginUserModel.fromJson(Map<String, dynamic> json) =>
      _$LoginUserModelFromJson(json);
}

@freezed
abstract class LoginStoreModel with _$LoginStoreModel {
  const factory LoginStoreModel({
    required String typeNum,
    required String storeName,
    required String role,
    required int roleId,
    required bool isManager,
    required int employeeId,
  }) = _LoginStoreModel;

  factory LoginStoreModel.fromJson(Map<String, dynamic> json) =>
      _$LoginStoreModelFromJson(json);
}
```

### 5.2 Clock Models

**File:** `lib/data/models/clock_model.dart`

```dart
@freezed
abstract class ClockStatusModel with _$ClockStatusModel {
  const factory ClockStatusModel({
    required bool isClockedIn,
    DateTime? lastPunchTime,
    int? elapsedMinutes,
    required bool withinGeofence,
    double? geofenceDistance,
    required bool canClockIn,
    required bool canClockOut,
    String? clockWindowMessage,
    ShiftModel? currentShift,
  }) = _ClockStatusModel;

  factory ClockStatusModel.fromJson(Map<String, dynamic> json) =>
      _$ClockStatusModelFromJson(json);
}

@freezed
abstract class ClockInRequest with _$ClockInRequest {
  const factory ClockInRequest({
    required double latitude,
    required double longitude,
    required double accuracy,
    required String deviceId,
  }) = _ClockInRequest;

  factory ClockInRequest.fromJson(Map<String, dynamic> json) =>
      _$ClockInRequestFromJson(json);
}

@freezed
abstract class ClockResultModel with _$ClockResultModel {
  const factory ClockResultModel({
    required int punchId,
    required DateTime punchTime,
    int? shiftId,
    @Default(false) bool isUnscheduled,
    double? totalHours,  // For clock out
  }) = _ClockResultModel;

  factory ClockResultModel.fromJson(Map<String, dynamic> json) =>
      _$ClockResultModelFromJson(json);
}
```

---

## 6. API Integration

### 6.1 Endpoint Updates

**File:** `lib/core/constants/api_constants.dart`

```dart
class ApiConstants {
  // Base URL
  static const String baseUrl = 'https://try.buyerkiosk.com';
  static const String apiBasePath = '/api/mobile/scheduling';

  // Auth endpoints (no typeNum needed)
  static const String login = '$apiBasePath/auth/login';
  static const String refresh = '$apiBasePath/auth/refresh';
  static const String logout = '$apiBasePath/auth/logout';

  // User endpoints
  static const String me = '$apiBasePath/me';

  // Store-scoped endpoints (need typeNum)
  static String clockStatus(String typeNum) =>
      '$apiBasePath/$typeNum/clock/status';
  static String clockIn(String typeNum) =>
      '$apiBasePath/$typeNum/clock/in';
  static String clockOut(String typeNum) =>
      '$apiBasePath/$typeNum/clock/out';
  static String todayShift(String typeNum) =>
      '$apiBasePath/$typeNum/schedule/today';
}
```

### 6.2 Auth Interceptor Update

**File:** `lib/core/network/api_interceptors.dart`

```dart
class AuthInterceptor extends Interceptor {
  final Ref _ref;
  bool _isRefreshing = false;
  final _pendingRequests = <RequestOptions, Completer<Response>>[];

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401 && !_isRefreshing) {
      _isRefreshing = true;

      try {
        final refreshed = await ref.read(authProvider.notifier).refreshToken();

        if (refreshed) {
          // Retry original request with new token
          final newToken = await _storage.getAccessToken();
          final options = err.requestOptions;
          options.headers['Authorization'] = 'Bearer $newToken';

          final response = await _dio.fetch(options);
          return handler.resolve(response);
        } else {
          // Refresh failed - logout
          await ref.read(authProvider.notifier).handleAccessRevoked();
        }
      } finally {
        _isRefreshing = false;
      }
    }

    // Handle 403 - store access revoked
    if (err.response?.statusCode == 403) {
      final data = err.response?.data;
      if (data is Map && data['code'] == 'STORE_ACCESS_DENIED') {
        await ref.read(authProvider.notifier).handleAccessRevoked();
      }
    }

    return handler.next(err);
  }
}
```

---

## 7. UI Components

### 7.1 Splash Screen

**File:** `lib/presentation/widgets/splash_screen.dart`

```dart
class SplashScreen extends ConsumerStatefulWidget {
  const SplashScreen({super.key});

  @override
  ConsumerState<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends ConsumerState<SplashScreen> {
  @override
  void initState() {
    super.initState();
    Future.microtask(() {
      ref.read(authProvider.notifier).checkAuthStatus();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          gradient: AppColors.primaryGradient,
        ),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // White logo
              SvgPicture.asset(
                'assets/images/logo_white.svg',
                width: 180,
                height: 80,
              ),
              const SizedBox(height: 32),
              // Loading indicator
              const _LoadingDots(),
            ],
          ),
        ),
      ),
    );
  }
}

class _LoadingDots extends StatefulWidget {
  const _LoadingDots();

  @override
  State<_LoadingDots> createState() => _LoadingDotsState();
}

class _LoadingDotsState extends State<_LoadingDots>
    with SingleTickerProviderStateMixin {
  // Animated dots implementation
}
```

### 7.2 Store Picker Bottom Sheet

**File:** `lib/presentation/widgets/store_picker.dart`

```dart
class StorePickerSheet extends ConsumerWidget {
  final List<Store> stores;
  final Store? currentStore;
  final ValueChanged<Store> onSelect;

  const StorePickerSheet({
    required this.stores,
    this.currentStore,
    required this.onSelect,
    super.key,
  });

  static Future<Store?> show(
    BuildContext context, {
    required List<Store> stores,
    Store? currentStore,
  }) {
    return showModalBottomSheet<Store>(
      context: context,
      isScrollControlled: true,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
      ),
      builder: (context) => StorePickerSheet(
        stores: stores,
        currentStore: currentStore,
        onSelect: (store) => Navigator.pop(context, store),
      ),
    );
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return SafeArea(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Select a Store',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 16),
            ...stores.map((store) => _StoreListTile(
              store: store,
              isSelected: store.typeNum == currentStore?.typeNum,
              onTap: () => onSelect(store),
            )),
          ],
        ),
      ),
    );
  }
}

class _StoreListTile extends StatelessWidget {
  final Store store;
  final bool isSelected;
  final VoidCallback onTap;

  const _StoreListTile({
    required this.store,
    required this.isSelected,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: const Icon(Icons.store),
      title: Text(store.storeName),
      subtitle: Text(store.role),
      trailing: isSelected
          ? Icon(Icons.check, color: AppColors.primary)
          : null,
      onTap: onTap,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
      tileColor: isSelected ? AppColors.primary50 : null,
    );
  }
}
```

### 7.3 Store Header Widget

**File:** `lib/presentation/widgets/store_header.dart`

```dart
class StoreHeader extends ConsumerWidget implements PreferredSizeWidget {
  const StoreHeader({super.key});

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final storeState = ref.watch(storeProvider);
    final authState = ref.watch(authProvider);

    final store = storeState is StoreSelected ? storeState.store : null;
    final stores = authState is AuthAuthenticated ? authState.stores : <Store>[];
    final hasMultipleStores = stores.length > 1;

    return AppBar(
      title: GestureDetector(
        onTap: hasMultipleStores
            ? () => _showStorePicker(context, stores, store)
            : null,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(store?.storeName ?? 'Select Store'),
            if (hasMultipleStores) ...[
              const SizedBox(width: 4),
              const Icon(Icons.arrow_drop_down, size: 20),
            ],
          ],
        ),
      ),
      actions: [
        IconButton(
          icon: const Icon(Icons.settings_outlined),
          onPressed: () => context.push('/settings'),
        ),
      ],
    );
  }

  void _showStorePicker(
    BuildContext context,
    List<Store> stores,
    Store? currentStore,
  ) async {
    final selected = await StorePickerSheet.show(
      context,
      stores: stores,
      currentStore: currentStore,
    );

    if (selected != null && context.mounted) {
      context.read(storeProvider.notifier).selectStore(selected);
    }
  }
}
```

### 7.4 Clock Status Card

**File:** `lib/presentation/widgets/clock_status_card.dart`

```dart
class ClockStatusCard extends ConsumerWidget {
  const ClockStatusCard({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final clockState = ref.watch(clockProvider);

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // Status icon and text
            _buildStatusIndicator(clockState),
            const SizedBox(height: 16),

            // Clock action button (if within geofence)
            _buildActionButton(context, ref, clockState),
          ],
        ),
      ),
    );
  }

  Widget _buildStatusIndicator(ClockState state) {
    if (state is ClockReady) {
      final status = state.status;

      if (status.isClockedIn) {
        return Column(
          children: [
            const Icon(Icons.timer, size: 48, color: AppColors.success),
            const SizedBox(height: 8),
            Text(
              'Clocked In',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
                color: AppColors.success,
              ),
            ),
            if (status.elapsedMinutes != null)
              Text(
                _formatElapsedTime(status.elapsedMinutes!),
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
          ],
        );
      } else {
        return Column(
          children: [
            Icon(Icons.timer_off, size: 48, color: AppColors.neutral400),
            const SizedBox(height: 8),
            Text(
              'Not Clocked In',
              style: TextStyle(
                fontSize: 18,
                color: AppColors.textSecondary,
              ),
            ),
          ],
        );
      }
    }

    return const CircularProgressIndicator();
  }

  Widget _buildActionButton(
    BuildContext context,
    WidgetRef ref,
    ClockState state,
  ) {
    if (state is ClockReady) {
      final status = state.status;

      if (!status.withinGeofence) {
        return Text(
          'You must be at the store to clock in/out',
          style: TextStyle(color: AppColors.textSecondary),
          textAlign: TextAlign.center,
        );
      }

      if (status.canClockIn) {
        return ElevatedButton.icon(
          onPressed: () => ref.read(clockProvider.notifier).clockIn(),
          icon: const Icon(Icons.login),
          label: const Text('Clock In'),
          style: ElevatedButton.styleFrom(
            backgroundColor: AppColors.success,
            foregroundColor: Colors.white,
            minimumSize: const Size(double.infinity, 48),
          ),
        );
      }

      if (status.canClockOut) {
        return ElevatedButton.icon(
          onPressed: () => ref.read(clockProvider.notifier).clockOut(),
          icon: const Icon(Icons.logout),
          label: const Text('Clock Out'),
          style: ElevatedButton.styleFrom(
            backgroundColor: AppColors.primary,
            foregroundColor: Colors.white,
            minimumSize: const Size(double.infinity, 48),
          ),
        );
      }
    }

    return const SizedBox.shrink();
  }

  String _formatElapsedTime(int minutes) {
    final hours = minutes ~/ 60;
    final mins = minutes % 60;
    return '${hours}h ${mins}m';
  }
}
```

---

## 8. Router Configuration

### 8.1 Updated Router

**File:** `lib/router/app_router.dart`

```dart
class AppRouter {
  final Ref _ref;

  AppRouter(this._ref);

  static const String splash = '/';
  static const String login = '/login';
  static const String biometric = '/biometric';
  static const String storePicker = '/store-picker';
  static const String home = '/home';
  static const String schedule = '/schedule';
  static const String settings = '/settings';

  GoRouter get router => GoRouter(
    initialLocation: splash,
    redirect: (context, state) {
      final authState = _ref.read(authProvider);
      final storeState = _ref.read(storeProvider);
      final currentPath = state.matchedLocation;

      // During initial load, stay on splash
      if (authState is AuthInitial || authState is AuthLoading) {
        return currentPath == splash ? null : splash;
      }

      // Not authenticated → login
      if (authState is AuthUnauthenticated || authState is AuthError) {
        return currentPath == login ? null : login;
      }

      // Biometric required → biometric screen
      if (authState is AuthBiometricRequired) {
        return currentPath == biometric ? null : biometric;
      }

      // Authenticated but needs store selection
      if (authState is AuthAuthenticated) {
        if (authState.requiresStoreSelection &&
            storeState is! StoreSelected) {
          return currentPath == storePicker ? null : storePicker;
        }

        // If on auth screens, redirect to home
        if ([splash, login, biometric, storePicker].contains(currentPath)) {
          return home;
        }
      }

      return null;
    },
    routes: [
      GoRoute(
        path: splash,
        builder: (_, __) => const SplashScreen(),
      ),
      GoRoute(
        path: login,
        builder: (_, __) => const LoginScreen(),
      ),
      GoRoute(
        path: biometric,
        builder: (_, __) => const BiometricScreen(),
      ),
      GoRoute(
        path: storePicker,
        builder: (_, __) => const StorePickerScreen(),
      ),
      GoRoute(
        path: home,
        builder: (_, __) => const HomeScreen(),
      ),
      GoRoute(
        path: schedule,
        builder: (_, __) => const ScheduleScreen(),
      ),
      GoRoute(
        path: settings,
        builder: (_, __) => const SettingsScreen(),
      ),
    ],
  );
}
```

---

## 9. Error Handling

### 9.1 Error Scenarios

| Scenario | HTTP Code | Handler |
|----------|-----------|---------|
| Invalid credentials | 401 | Show error on login form |
| Account disabled | 403 | Show "Account disabled" message |
| Network error | N/A | Show retry option |
| Token expired | 401 | Auto-refresh, retry request |
| Refresh failed | 401 | Clear tokens, redirect to login |
| Store access revoked | 403 | Clear tokens, show message, login |
| Outside geofence | 403 | Show location message, disable button |
| Clock window closed | 403 | Show time-based message |
| Server error | 500 | Show generic error, offer retry |

### 9.2 User-Friendly Messages

| Error Code | User Message |
|------------|--------------|
| `INVALID_CREDENTIALS` | "Invalid email or password. Please try again." |
| `ACCOUNT_DISABLED` | "Your account has been disabled. Contact your manager." |
| `SESSION_EXPIRED` | "Your session has expired. Please sign in again." |
| `STORE_ACCESS_DENIED` | "Your access has been updated. Please sign in again." |
| `OUTSIDE_GEOFENCE` | "You must be at the store to clock in." |
| `TOO_EARLY` | "You can clock in starting at [time]." |
| `NETWORK_ERROR` | "Unable to connect. Check your internet connection." |
| `SERVER_ERROR` | "Something went wrong. Please try again later." |

---

## 10. Testing Strategy

### 10.1 Unit Tests

| Component | Test Focus |
|-----------|------------|
| `AuthNotifier` | State transitions, login/logout, token refresh |
| `StoreNotifier` | Store selection, persistence, clear on logout |
| `ClockNotifier` | Status fetch, clock in/out, geofence checks |
| `AuthRepository` | API calls, response parsing |
| `ClockRepository` | API calls, response parsing |
| Mappers | Model → Entity conversion |

### 10.2 Widget Tests

| Widget | Test Focus |
|--------|------------|
| `SplashScreen` | Shows gradient, logo, triggers auth check |
| `LoginScreen` | Form validation, error display, loading state |
| `StorePickerSheet` | Store list display, selection callback |
| `ClockStatusCard` | Status display, button states |
| `BiometricScreen` | Prompt display, fallback to login |

### 10.3 Integration Tests

| Flow | Test Steps |
|------|------------|
| First Login | Launch → Splash → Login → Enter creds → Store picker → Home |
| Returning User | Launch → Splash → Biometric → Home |
| Store Switch | Home → Tap store → Picker → Select → Home refreshes |
| Clock In | Home → Clock In → Success → Status updates |
| Session Expiry | API call → 401 → Refresh → Retry → Success |
| Access Revoked | API call → 403 → Clear tokens → Login |

---

## 11. Quality Requirements

### 11.1 Performance

| Metric | Target | How to Achieve |
|--------|--------|----------------|
| Splash → Login | < 2s | Minimal async work, preload assets |
| Login → Home | < 3s | Optimize API response |
| Biometric → Home | < 2s | Local verification, parallel token check |
| Store switch | < 1s | No API call, local state change |
| Clock status fetch | < 2s | Lightweight API, location cached |

### 11.2 Security

- Tokens stored in platform Keychain/Keystore (flutter_secure_storage)
- No sensitive data logged (LoggingInterceptor sanitizes)
- HTTPS only
- Biometric uses platform APIs, no custom crypto
- Location data only sent for clock actions

### 11.3 Accessibility

- Minimum touch targets: 48x48 dp
- Color contrast: 4.5:1 for text
- Screen reader labels on all interactive elements
- Focus order follows visual layout

---

## 12. Implementation Plan Summary

This SDD supports the following implementation phases:

1. **Phase 1: Core Auth & Splash** (FR-001, FR-002, FR-006)
   - Branded splash screen
   - Updated login with email/password
   - Token storage and refresh interceptor

2. **Phase 2: Store Management** (FR-003, FR-004, FR-012)
   - StoreNotifier and provider
   - Store picker bottom sheet
   - Store header with switcher
   - Last store persistence

3. **Phase 3: Biometric Auth** (FR-005)
   - Biometric prompt flow
   - Enable/disable in settings
   - Fallback to password

4. **Phase 4: Clock & Home** (FR-007, FR-008)
   - ClockNotifier and ClockRepository
   - Clock status card with live updates
   - Foreground geofence checks
   - Updated home screen layout

5. **Phase 5: Navigation & Polish** (FR-009, FR-010)
   - Bottom navigation bar
   - Access revocation handling
   - Error messaging
   - Final UI polish

---

*Last updated: 2024-12-24*
