# Solution Design Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] All context sources are listed with relevance ratings
- [x] Project commands are discovered from actual project files
- [x] Constraints → Strategy → Design → Implementation path is logical
- [x] Architecture pattern is clearly stated with rationale
- [x] Every component in diagram has directory mapping
- [x] Every interface has specification
- [x] Error handling covers all error types
- [x] Quality requirements are specific and measurable
- [x] Every quality requirement has test coverage
- [x] **All architecture decisions confirmed by user**
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design

---

## Constraints

CON-1 **Platform/Framework Requirements**
- Flutter 3.38.4 / Dart 3.10.3
- iOS 12+ and Android API 23+ (existing app minimums)
- Must work on both front-facing and rear cameras
- Maximum upload file size: 1MB (to prevent storage bloat)

CON-2 **Architecture Requirements**
- Clean Architecture with Riverpod 3.x (Notifier pattern)
- Freezed 3.x for data models (abstract class required)
- Equatable for domain entities
- Dio for HTTP with existing interceptors

CON-3 **Security & Permissions**
- Camera permission required (NSCameraUsageDescription / CAMERA)
- Photo library permission for gallery selection (NSPhotoLibraryUsageDescription / READ_EXTERNAL_STORAGE)
- Images compressed client-side before upload
- No content moderation (trust team members)

CON-4 **Backend Dependency**
- Backend must provide `/api/mobile/me/avatar` endpoint for upload
- Backend returns updated `photoUrl` in user data
- Existing `photoUrl` field in UserModel already present

## Implementation Context

### Required Context Sources

- ICO-1 [Application Architecture]
```yaml
# Internal patterns to follow
- file: lib/core/network/api_client.dart
  relevance: HIGH
  why: "HTTP client pattern for avatar upload"

- file: lib/data/models/user_model.dart
  relevance: HIGH
  why: "Existing photoUrl field - no model changes needed"

- file: lib/domain/entities/user.dart
  relevance: HIGH
  why: "User entity with initials fallback pattern"

- file: lib/presentation/providers/auth_provider.dart
  relevance: HIGH
  why: "User state management pattern to follow"

- file: lib/presentation/screens/settings/settings_screen.dart
  relevance: HIGH
  why: "Avatar display location and current implementation"

- file: lib/core/services/storage_service.dart
  relevance: MEDIUM
  why: "Local caching pattern for avatar URL"
```

- ICO-2 [External Libraries]
```yaml
# Flutter packages for camera/image functionality
- package: image_picker
  relevance: HIGH
  why: "Camera capture and gallery selection"
  url: https://pub.dev/packages/image_picker

- package: image_cropper
  relevance: HIGH
  why: "Square crop with circular preview"
  url: https://pub.dev/packages/image_cropper

- package: cached_network_image
  relevance: MEDIUM
  why: "Avatar caching and loading states"
  url: https://pub.dev/packages/cached_network_image

- package: flutter_image_compress
  relevance: HIGH
  why: "Client-side compression before upload"
  url: https://pub.dev/packages/flutter_image_compress
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing UserModel/User entity structure
  - Auth provider state management pattern
  - Settings screen layout and navigation
  - Initials fallback display behavior

- **Can Modify**:
  - Settings screen to add avatar tap interaction
  - User display widgets to show photo when available

- **Must Not Touch**:
  - Authentication flow logic (login, logout, biometric verification)
  - Token management (access/refresh token storage and refresh)
  - Other unrelated provider state structures

- **Allowed Auth Modifications** (for avatar sync):
  - Add `refreshUser()` method to AuthNotifier
  - Add `getCurrentUser()` method to AuthRepository
  - Update cached user data to include `photoUrl`
  - Call `refreshUser()` after login and session restore (non-blocking)

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    User[Team Member] --> App[BuyerKiosk Team App]

    App --> Camera[Device Camera]
    App --> Gallery[Photo Gallery]
    App --> Backend[BuyerKiosk API]

    Backend --> Storage[(Image Storage)]
    Backend --> DB[(User Database)]
```

#### Interface Specifications

```yaml
# Inbound Interfaces (device capabilities)
inbound:
  - name: "Device Camera"
    type: Platform API
    format: image_picker plugin
    authentication: Camera Permission
    data_flow: "Captures image bytes"

  - name: "Photo Gallery"
    type: Platform API
    format: image_picker plugin
    authentication: Photo Library Permission
    data_flow: "Selects existing image"

# Outbound Interfaces (backend API)
outbound:
  - name: "Avatar Upload API"
    type: HTTPS
    format: Multipart Form Data
    authentication: JWT Bearer Token
    endpoint: POST /api/mobile/me/avatar
    data_flow: "Upload compressed image, receive photoUrl"
    criticality: HIGH

  - name: "Avatar Delete API"
    type: HTTPS
    format: REST
    authentication: JWT Bearer Token
    endpoint: DELETE /api/mobile/me/avatar
    data_flow: "Remove avatar, photoUrl set to null"
    criticality: MEDIUM
```

### Project Commands

```bash
# Environment Setup
Install Dependencies: flutter pub get
Start Development: flutter run

# Testing Commands
Unit Tests: flutter test
Widget Tests: flutter test test/widget_test.dart
Integration Tests: flutter test integration_test/

# Code Quality Commands
Linting: flutter analyze
Formatting: dart format .

# Build & Code Generation
Build Runner: dart run build_runner build --delete-conflicting-outputs
Build APK: flutter build apk --debug
Build iOS: flutter build ios --debug --no-codesign
```

## Solution Strategy

- **Architecture Pattern**: Clean Architecture with feature-based organization
  - New `avatar/` feature directory following existing patterns
  - Repository pattern for avatar API operations
  - Notifier pattern for avatar state management

- **Integration Approach**: Extend existing user/auth infrastructure
  - Avatar provider integrates with auth provider (user refresh on upload)
  - Reuse existing ApiClient for multipart upload
  - Leverage existing error handling patterns

- **Justification**:
  - Follows established codebase conventions exactly
  - Minimal surface area - adds feature without disrupting existing code
  - Reuses proven patterns (Riverpod, Dio, Clean Architecture)

- **Key Decisions**:
  1. Use `image_picker` for unified camera/gallery access
  2. Use `image_cropper` for square crop with circular preview
  3. Compress images client-side to <1MB before upload
  4. Optimistic UI update (show image immediately, sync in background)

## Building Block View

### Components

```mermaid
graph TB
    subgraph Presentation
        SS[Settings Screen]
        ABS[Avatar Bottom Sheet]
        APS[Avatar Preview Screen]
        AW[Avatar Widget]
    end

    subgraph Providers
        AP[Avatar Provider]
        AuthP[Auth Provider]
    end

    subgraph Domain
        AR[Avatar Repository Interface]
    end

    subgraph Data
        ARI[Avatar Repository Impl]
        AC[Api Client]
    end

    subgraph Services
        IS[Image Service]
    end

    subgraph External
        Camera[Device Camera]
        Gallery[Photo Gallery]
        Backend[Backend API]
    end

    SS --> AW
    SS --> ABS
    ABS --> APS
    ABS --> IS
    APS --> AP
    AP --> AR
    AP --> AuthP
    AR --> ARI
    ARI --> AC
    AC --> Backend
    IS --> Camera
    IS --> Gallery
```

### Directory Map

```
lib/
├── core/
│   ├── constants/
│   │   └── avatar_constants.dart          # NEW: Max size, compression settings
│   └── services/
│       └── image_service.dart             # NEW: Camera/gallery/compress logic
│
├── data/
│   ├── models/
│   │   └── avatar_upload_response.dart    # NEW: Upload response model
│   └── repositories/
│       └── avatar_repository_impl.dart    # NEW: API implementation
│
├── domain/
│   ├── entities/
│   │   └── avatar_state.dart              # NEW: Sealed state class
│   └── repositories/
│       └── avatar_repository.dart         # NEW: Repository interface
│
├── presentation/
│   ├── providers/
│   │   └── avatar_provider.dart           # NEW: Avatar state notifier
│   ├── screens/
│   │   ├── home/
│   │   │   └── home_screen.dart           # MODIFY: Replace _WelcomeCard avatar with AvatarWidget
│   │   └── settings/
│   │       ├── settings_screen.dart       # MODIFY: Replace profile avatar with AvatarWidget
│   │       └── avatar_preview_screen.dart # NEW: Preview/confirm screen
│   └── widgets/
│       ├── avatar_widget.dart             # NEW: Reusable avatar display
│       └── avatar_bottom_sheet.dart       # NEW: Camera/gallery picker
│
├── ios/Runner/Info.plist                  # MODIFY: Add camera permission
└── android/app/src/main/AndroidManifest.xml # MODIFY: Add camera permission
```

### Interface Specifications

#### Data Storage Changes

```yaml
# No database changes required
# Backend stores image in S3/cloud storage
# Returns photoUrl in existing user data structure

# Local caching (optional optimization)
Storage: SharedPreferences
  Key: "cached_avatar_url"
  Value: String (URL of current avatar)
  Purpose: Display avatar before network fetch completes

# Cache-busting strategy
# The backend MUST return a unique photoUrl per upload to avoid stale cache issues.
# Options (backend should implement one):
#   1. Include timestamp/version in URL: /avatars/user_123_v2.jpg
#   2. Use content hash in filename: /avatars/user_123_abc123.jpg
#   3. Use signed URLs with expiry
#
# If backend returns same URL, client will append cache-bust param:
#   ${photoUrl}?t=${DateTime.now().millisecondsSinceEpoch}
```

#### Internal API Changes

```yaml
# Avatar Upload
Endpoint: Upload Avatar
  Method: POST
  Path: /api/mobile/me/avatar
  Content-Type: multipart/form-data
  Request:
    avatar: File (image/jpeg or image/png, max 1MB)
  Response:
    success:
      photoUrl: string (full URL to uploaded image)
      message: string ("Avatar uploaded successfully")
    error:
      error: string
      message: string

# Avatar Delete
Endpoint: Delete Avatar
  Method: DELETE
  Path: /api/mobile/me/avatar
  Response:
    success:
      message: string ("Avatar removed successfully")
    error:
      error: string
      message: string
```

#### Application Data Models

```dart
// NEW: Avatar upload response (Freezed)
@freezed
abstract class AvatarUploadResponse with _$AvatarUploadResponse {
  const factory AvatarUploadResponse({
    required String photoUrl,
    String? message,
  }) = _AvatarUploadResponse;

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

// Progress callback type for upload tracking
typedef UploadProgressCallback = void Function(int sent, int total);

// NEW: Avatar state (Sealed class - Equatable)
sealed class AvatarState extends Equatable {
  const AvatarState();
  @override
  List<Object?> get props => [];
}

class AvatarInitial extends AvatarState {
  const AvatarInitial();
}

class AvatarCapturing extends AvatarState {
  const AvatarCapturing();
}

class AvatarPreviewing extends AvatarState {
  final String localImagePath;
  const AvatarPreviewing(this.localImagePath);
  @override
  List<Object?> get props => [localImagePath];
}

class AvatarUploading extends AvatarState {
  final String localImagePath;
  final double progress;
  const AvatarUploading(this.localImagePath, {this.progress = 0});
  @override
  List<Object?> get props => [localImagePath, progress];
}

class AvatarSuccess extends AvatarState {
  final String photoUrl;
  const AvatarSuccess(this.photoUrl);
  @override
  List<Object?> get props => [photoUrl];
}

class AvatarError extends AvatarState {
  final String message;
  final String? localImagePath; // Preserve for retry
  const AvatarError({required this.message, this.localImagePath});
  @override
  List<Object?> get props => [message, localImagePath];
}
```

#### Integration Points

```yaml
# Internal Integration
- from: AvatarProvider
  to: AuthProvider
  protocol: Riverpod ref
  data_flow: "Refresh user data after successful upload to get new photoUrl"

- from: AuthProvider (login/restore)
  to: /api/mobile/me
  protocol: HTTP GET
  data_flow: "Fetch full user profile including photoUrl after login"

- from: SettingsScreen
  to: AvatarWidget
  protocol: Widget composition
  data_flow: "Display current avatar from user entity"

# External Integration
Backend_Avatar_API:
  endpoint: /api/mobile/me/avatar
  integration: "Multipart upload via Dio"
  critical_data: [image_bytes, jwt_token]
```

## Runtime View

### Primary Flow

#### Primary Flow: Avatar Capture & Upload

**Entry Points (both trigger same flow):**
- Dashboard: User taps avatar in Welcome Card (`_WelcomeCard`)
- Settings: User taps avatar in profile section

1. User taps avatar in Dashboard OR Settings screen
2. Bottom sheet presents options: "Take Photo" / "Choose from Gallery"
3. User selects option, permission checked/requested
4. Camera/gallery opens, user captures/selects image
5. Image cropped to square with circular preview
6. Preview screen shows result with "Retake" / "Use Photo" options
7. User confirms, upload begins with progress indicator
8. Success: Avatar updates throughout app, user returned to Settings
9. Failure: Error message with retry option

```mermaid
sequenceDiagram
    actor User
    participant Screen as Dashboard/Settings Screen
    participant Sheet as Avatar Bottom Sheet
    participant ImageSvc as Image Service
    participant Preview as Preview Screen
    participant Provider as Avatar Provider
    participant Repo as Avatar Repository
    participant API as Backend API
    participant Auth as Auth Provider

    User->>Screen: Tap avatar (Dashboard or Settings)
    Screen->>Sheet: Show options
    User->>Sheet: Select "Take Photo"
    Sheet->>ImageSvc: captureFromCamera()
    ImageSvc-->>ImageSvc: Check/request permission
    ImageSvc-->>ImageSvc: Open camera
    User->>ImageSvc: Capture photo
    ImageSvc-->>ImageSvc: Crop to square
    ImageSvc-->>Sheet: Return cropped image path
    Sheet->>Preview: Navigate with image
    User->>Preview: Tap "Use Photo"
    Preview->>Provider: uploadAvatar(imagePath)
    Provider->>Provider: state = Uploading
    Provider->>ImageSvc: compressImage()
    ImageSvc-->>Provider: Compressed bytes
    Provider->>Repo: uploadAvatar(bytes)
    Repo->>API: POST /me/avatar (multipart)
    API-->>Repo: {photoUrl: "..."}
    Repo-->>Provider: AvatarUploadResponse
    Provider->>Auth: refreshUser()
    Auth-->>Provider: Updated user with photoUrl
    Provider->>Provider: state = Success
    Provider-->>Preview: Success
    Preview->>Screen: Pop back
    Screen-->>User: Avatar updated!
```

### Error Handling

| Error Type | Handling | User Feedback |
|------------|----------|---------------|
| Camera permission denied | Show message with Settings link | "Camera access needed. Tap to open Settings." |
| Gallery permission denied | Show message with Settings link | "Photo access needed. Tap to open Settings." |
| Image too large after compression | Should not happen (compression targets <1MB) | Fallback: "Image too large. Please try a different photo." |
| Network failure | Preserve image, offer retry | "Upload failed. Check your connection and try again." |
| Backend error (5xx) | Preserve image, offer retry | "Server error. Please try again later." |
| Invalid image format | Inform user | "Unsupported image format. Please use a different photo." |

### Complex Logic

```
ALGORITHM: Image Capture & Compress Flow
INPUT: camera_source OR gallery_source
OUTPUT: compressed_image_bytes (<1MB)

1. REQUEST_PERMISSION:
   IF camera_source: request camera permission
   IF gallery_source: request photo library permission
   IF denied: RETURN permission_error

2. CAPTURE_IMAGE:
   Open image_picker with source
   IF cancelled: RETURN cancelled
   GET raw_image_file

3. CROP_TO_SQUARE:
   Open image_cropper
   SET aspect_ratio = 1:1
   SET crop_style = circular (preview only)
   IF cancelled: RETURN cancelled
   GET cropped_image_file

4. COMPRESS_IMAGE:
   READ cropped_image_file
   WHILE file_size > 1MB AND quality > 20:
     COMPRESS with decreasing quality (90 -> 70 -> 50 -> 30 -> 20)
   IF still > 1MB: RESIZE to max 1024x1024
   RETURN compressed_bytes

5. VALIDATE:
   CHECK format is JPEG or PNG
   CHECK size <= 1MB
   RETURN validated_image_bytes
```

## Deployment View

### Single Application Deployment

- **Environment**: Mobile client (iOS/Android)
- **Configuration**:
  - Camera/photo permissions in platform manifests
  - No new environment variables required
- **Dependencies**:
  - Backend `/api/mobile/me/avatar` endpoint must be available
  - Cloud storage (S3) for image hosting
- **Performance**:
  - Compression happens on-device (may take 1-3 seconds for large images)
  - Upload timeout: 60 seconds (large multipart upload)
  - Avatar images cached via `cached_network_image`

### Platform Configuration

**iOS (Info.plist additions):**
```xml
<key>NSCameraUsageDescription</key>
<string>Take a photo for your profile avatar</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose a photo for your profile avatar</string>
```

**Android (AndroidManifest.xml additions):**
```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
```

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Riverpod Notifier Pattern
  relevance: CRITICAL
  why: "State management for avatar upload flow"

- pattern: Repository Pattern
  relevance: HIGH
  why: "Separation of API logic from business logic"

- pattern: Sealed Class States
  relevance: HIGH
  why: "Type-safe state handling for avatar operations"

# New patterns created
- pattern: Multipart Upload Pattern (NEW)
  relevance: HIGH
  why: "First image upload in the app - establishes pattern for future uploads"
```

### System-Wide Patterns

- **Security**: JWT auth via existing interceptor, no additional auth needed
- **Error Handling**: Follow existing AppException hierarchy, add ImageException if needed
- **Performance**: Client-side compression, cached_network_image for display
- **Logging**: Log upload attempts/failures via existing logging interceptor

### Implementation Patterns

#### Image Service Pattern

**Note:** ImageService is the SOLE owner of image compression. The AvatarProvider receives
already-compressed bytes from ImageService and passes them directly to the repository.

```dart
// Service for image capture, selection, and compression
// This is the SINGLE location where compression happens
class ImageService {
  final ImagePicker _picker = ImagePicker();

  /// Capture from camera (front-facing by default for selfies)
  Future<ImageResult> captureFromCamera({
    CameraDevice preferredCamera = CameraDevice.front,
  }) async {
    // 1. Check camera permission
    final permissionStatus = await Permission.camera.request();
    if (permissionStatus.isDenied) {
      return const ImagePermissionDenied(
        permissionType: 'camera',
        isPermanentlyDenied: false,
      );
    }
    if (permissionStatus.isPermanentlyDenied) {
      return const ImagePermissionDenied(
        permissionType: 'camera',
        isPermanentlyDenied: true, // User must go to Settings
      );
    }

    // 2. Capture with image_picker (front camera for selfies)
    final XFile? image = await _picker.pickImage(
      source: ImageSource.camera,
      preferredCameraDevice: preferredCamera,
    );
    if (image == null) return const ImageCancelled();

    // 3. Crop to square with circular preview
    final croppedFile = await ImageCropper().cropImage(
      sourcePath: image.path,
      aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
      uiSettings: [/* platform-specific settings */],
    );
    if (croppedFile == null) return const ImageCancelled();

    // 4. Compress to under 1MB (this is the ONLY place compression happens)
    final compressedBytes = await _compressImage(croppedFile.path);

    return ImageSuccess(path: croppedFile.path, bytes: compressedBytes);
  }

  Future<ImageResult> selectFromGallery() async {
    // 1. Check photo library permission
    final permissionStatus = await Permission.photos.request();
    if (permissionStatus.isDenied) {
      return const ImagePermissionDenied(
        permissionType: 'photos',
        isPermanentlyDenied: false,
      );
    }
    if (permissionStatus.isPermanentlyDenied) {
      return const ImagePermissionDenied(
        permissionType: 'photos',
        isPermanentlyDenied: true,
      );
    }

    // 2-4. Same flow as camera but with gallery source
    final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
    if (image == null) return const ImageCancelled();

    // Crop and compress...
    final croppedFile = await ImageCropper().cropImage(/* ... */);
    if (croppedFile == null) return const ImageCancelled();

    final compressedBytes = await _compressImage(croppedFile.path);
    return ImageSuccess(path: croppedFile.path, bytes: compressedBytes);
  }

  /// Compress image to under maxSizeKb (default 1MB)
  /// Uses iterative quality reduction, falls back to resize
  Future<Uint8List> _compressImage(String path, {int maxSizeKb = 1024}) async {
    // Implementation per SDD Complex Logic algorithm
  }

  /// Open device settings for permission management
  Future<void> openAppSettings() async {
    await openAppSettings(); // from permission_handler
  }
}

sealed class ImageResult {
  const ImageResult();
}

class ImageSuccess extends ImageResult {
  final String path;
  final Uint8List bytes; // Already compressed!
  const ImageSuccess({required this.path, required this.bytes});
}

class ImageCancelled extends ImageResult {
  const ImageCancelled();
}

class ImagePermissionDenied extends ImageResult {
  final String permissionType; // 'camera' or 'photos'
  final bool isPermanentlyDenied; // If true, user must go to Settings
  const ImagePermissionDenied({
    required this.permissionType,
    required this.isPermanentlyDenied,
  });
}

class ImageError extends ImageResult {
  final String message;
  const ImageError(this.message);
}
```

#### Avatar Provider Pattern

```dart
final avatarProvider = NotifierProvider<AvatarNotifier, AvatarState>(
  AvatarNotifier.new,
);

class AvatarNotifier extends Notifier<AvatarState> {
  AvatarRepository get _avatarRepo => ref.read(avatarRepositoryProvider);

  @override
  AvatarState build() => const AvatarInitial();

  /// Upload avatar from already-compressed image bytes
  /// Note: Compression is done by ImageService BEFORE calling this method
  Future<void> uploadAvatar(String imagePath, Uint8List compressedBytes) async {
    state = AvatarUploading(imagePath, progress: 0);

    try {
      // Upload with progress tracking via Dio onSendProgress
      final response = await _avatarRepo.uploadAvatar(
        compressedBytes,
        onProgress: (sent, total) {
          // Update state with progress (0.0 to 1.0)
          state = AvatarUploading(imagePath, progress: sent / total);
        },
      );

      // Refresh user to get new photoUrl from /api/mobile/me
      await ref.read(authProvider.notifier).refreshUser();

      state = AvatarSuccess(response.photoUrl);
    } catch (e) {
      state = AvatarError(
        message: e.toString(),
        localImagePath: imagePath, // Preserve for retry
      );
    }
  }

  /// Remove avatar with confirmation (UI shows confirmation dialog first)
  Future<void> removeAvatar() async {
    try {
      await _avatarRepo.deleteAvatar();
      await ref.read(authProvider.notifier).refreshUser();
      state = const AvatarInitial();
    } catch (e) {
      state = AvatarError(message: e.toString());
    }
  }

  void reset() {
    state = const AvatarInitial();
  }
}
```

#### AuthProvider Integration (refreshUser method)

The existing `AuthProvider` must be extended with a `refreshUser()` method that:
1. Calls `/api/mobile/me` to fetch fresh user data including `photoUrl`
2. Updates the cached user data in storage
3. Updates the `AuthAuthenticated` state with the new user

```dart
// Add to AuthNotifier in auth_provider.dart
Future<void> refreshUser() async {
  if (state is! AuthAuthenticated) return;

  try {
    // Call /api/mobile/me to get fresh user data
    final freshUser = await _authRepo.getCurrentUser();

    // Update cached user data (including photoUrl)
    final currentState = state as AuthAuthenticated;
    await _storage.setUserData({
      'user': {
        'id': freshUser.id,
        'email': freshUser.email,
        'firstName': freshUser.firstName,
        'lastName': freshUser.lastName,
        'displayName': freshUser.displayName,
        'photoUrl': freshUser.photoUrl, // NEW: Include photoUrl in cache
      },
      'stores': /* existing stores data */,
    });

    // Update state with fresh user (triggers UI rebuild)
    state = AuthAuthenticated(
      user: freshUser,
      stores: currentState.stores,
      biometricEnabled: currentState.biometricEnabled,
      requiresStoreSelection: currentState.requiresStoreSelection,
    );
  } catch (e) {
    // Silently fail - don't disrupt the session
  }
}
```

**When to call refreshUser():**
- After successful avatar upload (to get new photoUrl)
- After login (non-blocking, to fetch photoUrl not in login response)
- After session restore from cache (non-blocking, to sync any server-side changes)

#### Avatar Widget Pattern

```dart
class AvatarWidget extends ConsumerWidget {
  final User? user;
  final double radius;
  final VoidCallback? onTap;

  const AvatarWidget({
    required this.user,
    this.radius = 30,
    this.onTap,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Watch avatar state for optimistic local preview
    final avatarState = ref.watch(avatarProvider);

    // Determine image source (priority: local preview > network > initials)
    ImageProvider? imageProvider;
    if (avatarState is AvatarUploading || avatarState is AvatarPreviewing) {
      // Show local image during preview/upload (optimistic update)
      final localPath = avatarState is AvatarUploading
          ? avatarState.localImagePath
          : (avatarState as AvatarPreviewing).localImagePath;
      imageProvider = FileImage(File(localPath));
    } else if (user?.photoUrl != null) {
      // Show network image with cache-busting
      imageProvider = CachedNetworkImageProvider(user!.photoUrl!);
    }

    return GestureDetector(
      onTap: onTap,
      child: CircleAvatar(
        radius: radius,
        backgroundColor: AppColors.primary100,
        backgroundImage: imageProvider,
        child: imageProvider == null
            ? Text(
                user?.initials ?? 'U',
                style: TextStyle(
                  color: AppColors.primary,
                  fontWeight: FontWeight.bold,
                  fontSize: radius * 0.6,
                ),
              )
            : null,
      ),
    );
  }
}
```

## Architecture Decisions

- [x] **ADR-1: Use image_picker + image_cropper combination**
  - Rationale: Standard Flutter packages with excellent platform support, widely used and maintained
  - Alternatives: camera package (more control but more complexity), custom native implementation
  - Trade-offs: Less control over camera UI, but faster implementation and proven reliability
  - User confirmed: **Yes** (2025-12-30)

- [x] **ADR-2: Client-side compression before upload**
  - Rationale: Reduces upload time, saves bandwidth, reduces storage costs
  - Alternatives: Server-side compression, no compression
  - Trade-offs: Slight delay during compression (1-3 seconds), but significantly faster uploads
  - User confirmed: **Yes** (2025-12-30)

- [x] **ADR-3: Optimistic UI update with background sync**
  - Rationale: Immediate feedback improves UX, follows existing notification preferences pattern
  - Alternatives: Wait for server confirmation before updating UI
  - Trade-offs: Possible state mismatch if upload fails (mitigated by error handling)
  - User confirmed: **Yes** (2025-12-30)

- [x] **ADR-4: Square crop only (1:1 aspect ratio)**
  - Rationale: Consistent circular display throughout app, simpler cropping logic
  - Alternatives: Allow any aspect ratio and crop server-side
  - Trade-offs: Less flexibility but better consistency
  - User confirmed: **Yes** (via PRD decision log - 2025-12-30)

- [x] **ADR-5: Separate AvatarProvider from AuthProvider**
  - Rationale: Single responsibility - avatar operations are distinct from auth
  - Alternatives: Extend AuthProvider with avatar methods
  - Trade-offs: Two providers to coordinate, but cleaner separation of concerns
  - User confirmed: **Yes** (2025-12-30)

## Quality Requirements

| Requirement | Target | Measurement |
|-------------|--------|-------------|
| Upload success rate | >95% | Analytics: avatar_upload_completed / avatar_upload_started |
| Compression time | <3 seconds | Logging: time from capture to compressed bytes |
| Upload time | <10 seconds on 3G | Logging: avatar_upload_completed duration_ms |
| Permission grant rate | >80% | Analytics: avatar_permission_result.granted |
| Feature adoption | 40% of users within 30 days | Analytics: unique users with avatar_upload_completed |
| Flow completion rate | >80% | Analytics: avatar_upload_completed / avatar_flow_started |

## Risks and Technical Debt

### Known Technical Issues
- None in avatar feature area (greenfield implementation)

### Technical Debt
- Current Settings screen avatar display is inline - will be extracted to AvatarWidget
- No existing image upload pattern - this establishes the first one

### Implementation Gotchas
- **iOS Simulator**: Camera not available, must test on real device
- **Android Emulator**: Camera works but quality differs from real devices
- **Large images**: Some device cameras produce 10MB+ images - compression is critical
- **Permission timing**: Must request permission at moment of use, not app launch
- **image_cropper Android**: Requires UCrop activity in AndroidManifest.xml

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Happy Path - Camera Capture**
```gherkin
Given: User is on Settings screen with no avatar
And: Camera permission is granted
When: User taps avatar, selects "Take Photo"
And: User captures and confirms photo
Then: Avatar uploads successfully
And: Avatar displays throughout the app
And: Success message is shown
```

**Scenario 2: Happy Path - Gallery Selection**
```gherkin
Given: User is on Settings screen with existing avatar
And: Photo library permission is granted
When: User taps avatar, selects "Choose from Gallery"
And: User selects and confirms photo
Then: New avatar uploads successfully
And: Old avatar is replaced
```

**Scenario 3: Permission Denied - Camera**
```gherkin
Given: User is on Settings screen
And: Camera permission is denied
When: User taps avatar, selects "Take Photo"
Then: Permission explanation is shown
And: Link to device Settings is provided
And: User can dismiss and choose gallery instead
```

**Scenario 4: Upload Failure - Network Error**
```gherkin
Given: User has captured and confirmed a photo
When: Upload fails due to network error
Then: Error message is displayed
And: Captured photo is preserved
And: "Retry" button is available
And: User can retry without re-capturing
```

**Scenario 5: Remove Avatar**
```gherkin
Given: User has an existing avatar
When: User taps avatar, selects "Remove Photo"
And: User confirms removal
Then: Avatar is deleted from server
And: Display reverts to initials
```

### Test Coverage Requirements

- **Unit Tests**:
  - ImageService: compression logic, permission handling
  - AvatarRepository: API request/response mapping
  - AvatarNotifier: state transitions, error handling

- **Widget Tests**:
  - AvatarWidget: displays photo or initials correctly
  - AvatarBottomSheet: shows correct options based on avatar state
  - AvatarPreviewScreen: retake/confirm buttons work

- **Integration Tests**:
  - Full capture → crop → upload → display flow
  - Permission request flows
  - Error recovery flows

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Avatar | Profile photo representing a team member | Displayed in circular format throughout app |
| Initials | Two-letter fallback when no avatar | Computed from user's name (e.g., "JD" for John Doe) |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| Multipart upload | HTTP request with file attachment | Used for avatar image upload |
| Client-side compression | Reducing image size on device | Before upload to meet 1MB limit |
| Optimistic update | Update UI before server confirmation | Show avatar immediately after capture |
| Sealed class | Dart pattern for type-safe union types | Used for AvatarState variants |
