Skip to main content

UI Specification: Settings & Profile (Phase 7 - FINAL)

Date: 2025-11-10 Version: 1.0 Author: Product Designer Status: Complete - Ready for Development Platform: iOS 15+ (Flutter/Cupertino) Phase: 7 of 7 (FINAL MVP PHASE)

Overview

This is the final phase of the MVP UI design, completing all 28 screens for the AI-Powered Photo Journaling iOS App. Phase 7 delivers the Settings & Profile functionality, giving users control over their account, preferences, and privacy. Screens Designed: 2 screens
  • Settings Screen (Main)
  • Edit Profile Screen
Total MVP Completion: 28/28 screens (100% complete)

Design Goals

  1. iOS-Native Feel: Use grouped list styling (CupertinoFormSection) consistent with iOS Settings app
  2. Clear Organization: Group related settings logically for easy scanning
  3. Data Control: Give users full control over their data, privacy, and account
  4. Accessibility: Full VoiceOver support, Dynamic Type, WCAG 2.1 AA compliance
  5. Safety First: Destructive actions require confirmation dialogs

Screen 1: Settings Screen (Main)

Purpose

Central hub for all user settings, account management, preferences, and app information.

User Flow

Entry Points:
  • Tab Bar → Profile tab (person.circle.fill icon)
  • Entry: User taps Profile icon in bottom tab bar
Exit Points:
  • Edit Profile → Edit Profile screen
  • Change Password → Password change form
  • Subscription → Subscription Options screen (from Phase 6)
  • Export Data → Email with download link
  • Delete Account → Confirmation dialog → Account deletion
  • Sign Out → Confirmation dialog → Welcome screen

Layout Structure

┌────────────────────────────────────────────────┐
│  Settings                                  [×] │ ← Navigation bar (44pt)
├────────────────────────────────────────────────┤
│                                                │
│  ┌──────────────────────────────────────────┐ │
│  │        ┌────────────┐                    │ │
│  │        │            │                    │ │ ← Profile section (120pt)
│  │        │   Photo    │                    │ │   - Photo: 64×64pt circle
│  │        │  64×64pt   │                    │ │   - Name: headline (17pt semibold)
│  │        └────────────┘                    │ │   - Email: callout (16pt gray)
│  │        Ryan Thompson                     │ │   - Premium badge (if applicable)
│  │        [email protected]                  │ │
│  │        👑 Premium                        │ │
│  └──────────────────────────────────────────┘ │
│                                                │
│  ACCOUNT                                       │ ← Section header (footnote, gray, uppercase)
│  ┌──────────────────────────────────────────┐ │
│  │  Edit Profile                         >  │ │ ← Row (44pt min height)
│  │  ───────────────────────────────────────  │ │   Divider (hairline)
│  │  Change Password                      >  │ │
│  │  ───────────────────────────────────────  │ │
│  │  Subscription                         >  │ │
│  └──────────────────────────────────────────┘ │
│                                                │
│  APP SETTINGS                                  │
│  ┌──────────────────────────────────────────┐ │
│  │  Notifications                    ━━━○   │ │ ← Toggle switch (CupertinoSwitch)
│  │  ───────────────────────────────────────  │ │
│  │  Daily Reminder Time       8:00 PM    >  │ │ ← Shows current time
│  │  ───────────────────────────────────────  │ │
│  │  App Theme                Auto         >  │ │ ← Shows current selection
│  └──────────────────────────────────────────┘ │   (Auto / Light / Dark)
│                                                │
│  DATA & PRIVACY                                │
│  ┌──────────────────────────────────────────┐ │
│  │  Export My Data                       >  │ │
│  │  ───────────────────────────────────────  │ │
│  │  Delete My Account                    >  │ │ ← Red text (destructive)
│  └──────────────────────────────────────────┘ │
│                                                │
│  ABOUT                                         │
│  ┌──────────────────────────────────────────┐ │
│  │  Help & Support                       >  │ │
│  │  ───────────────────────────────────────  │ │
│  │  Privacy Policy                       >  │ │
│  │  ───────────────────────────────────────  │ │
│  │  Terms of Service                     >  │ │
│  │  ───────────────────────────────────────  │ │
│  │  App Version         Version 1.0.0 (1)   │ │ ← Non-interactive, gray text
│  └──────────────────────────────────────────┘ │
│                                                │
│                                                │
│  ┌──────────────────────────────────────────┐ │
│  │           Sign Out                       │ │ ← Destructive button (red text)
│  └──────────────────────────────────────────┘ │
│                                                │
└────────────────────────────────────────────────┘
     Safe Area Inset (Tab Bar 49pt)

Component Breakdown

1. Navigation Bar

Specifications:
  • Height: 44pt (standard iOS)
  • Background: Transparent with iOS blur effect
  • Title: “Settings” (headline, 17pt semibold, center-aligned)
  • Right Action: Close button (xmark icon, 24×24pt) - dismisses settings modal (if presented modally)
  • Safe Area: Respects status bar inset
Flutter Implementation:
CupertinoNavigationBar(
  middle: Text('Settings', style: AppTypography.headline),
  trailing: CupertinoButton(
    padding: EdgeInsets.zero,
    child: Icon(CupertinoIcons.xmark, size: 24),
    onPressed: () => Navigator.pop(context),
  ),
)
States:
  • Default: Fully visible
  • Scrolling: Becomes translucent with blur
Accessibility:
  • VoiceOver: “Settings, heading level 1”
  • Close button: “Close settings, button”

2. Profile Section

Specifications:
  • Container: CupertinoFormSection with 16pt padding
  • Background: backgroundSurfaceLight (#F9FAFB) / backgroundSurfaceDark (#2C3135)
  • Border Radius: 12pt (medium)
  • Shadow: Level 1 (subtle)
  • Margin: 16pt horizontal, 16pt top
  • Padding: 16pt internal
Layout:
Center-aligned column:
- Profile Photo: 64×64pt circle
- Gap: 12pt
- Display Name: headline (17pt semibold, textPrimaryLight/Dark)
- Email: callout (16pt regular, textSecondaryLight/Dark)
- Gap: 8pt
- Premium Badge (if applicable): See Premium Badge component from design-system.md
Profile Photo:
  • Size: 64×64pt circle
  • Border: 2pt backgroundPrimaryLight/Dark (creates subtle separation)
  • Placeholder: Person silhouette icon (person.circle.fill, 64×64pt, gray)
  • Tappable: Yes - opens “Change Photo” action sheet (Camera / Library / Remove)
  • Loading State: Circular spinner overlay while uploading
Premium Badge:
  • Component: PremiumBadge from design-system.md
  • Display: Only if user is Premium subscriber
  • Specs: 24pt height, 👑 icon + “Premium” label, golden orange (#FAB1A0) at 15% opacity background
Flutter Implementation:
CupertinoFormSection(
  decoration: BoxDecoration(
    color: AppColors.backgroundSurface(brightness),
    borderRadius: BorderRadius.circular(AppBorderRadius.md),
    boxShadow: [AppShadows.level1(brightness)],
  ),
  children: [
    Container(
      padding: EdgeInsets.all(AppSpacing.md),
      child: Column(
        children: [
          GestureDetector(
            onTap: () => _showChangePhotoSheet(),
            child: CircleAvatar(
              radius: 32,
              backgroundImage: userPhoto != null
                ? NetworkImage(userPhoto)
                : null,
              child: userPhoto == null
                ? Icon(CupertinoIcons.person_circle_fill, size: 64)
                : null,
            ),
          ),
          SizedBox(height: AppSpacing.sm),
          Text(userName, style: AppTypography.headline),
          Text(userEmail, style: AppTypography.callout.copyWith(
            color: AppColors.textSecondary(brightness)
          )),
          if (isPremium) ...[
            SizedBox(height: AppSpacing.xs),
            PremiumBadge(),
          ],
        ],
      ),
    ),
  ],
)
Interactions:
  • Tap Photo: Opens action sheet with options:
    • “Take Photo” → Opens camera
    • “Choose from Library” → Opens photo picker
    • “Remove Photo” → Confirms removal, reverts to placeholder
    • “Cancel”
States:
  • Default: Photo or placeholder visible
  • Uploading: Spinner overlay (32×32pt)
  • Error: Error icon overlay + retry option
Accessibility:
  • Photo: “Profile photo, button, double-tap to change”
  • Name: “Ryan Thompson”
  • Email: “[email protected]
  • Premium Badge: “Premium subscriber”

3. Settings Sections (Grouped Lists)

Section Header:
  • Font: Footnote (13pt regular, uppercase)
  • Color: textSecondaryLight (#636E72) / textSecondaryDark (#B2BEC3)
  • Padding: 16pt horizontal, 8pt top, 4pt bottom
  • Accessibility: VoiceOver announces as “Section header”
Settings Row:
  • Height: 44pt minimum (iOS touch target)
  • Padding: 16pt horizontal, 12pt vertical
  • Background: backgroundSurfaceLight / backgroundSurfaceDark
  • Divider: 0.5pt hairline (borderLight / borderDark) - inset 16pt from left
  • Layout:
    • Leading: Icon (24×24pt, optional) + Label (body, 17pt)
    • Trailing: Value text (callout, 16pt, gray) OR Toggle switch OR Chevron (>)
Row Types: A) Navigation Row (with chevron):
CupertinoFormRow(
  prefix: Text('Edit Profile', style: AppTypography.body),
  child: CupertinoListTileChevron(),
  onTap: () => Navigator.push(context, CupertinoPageRoute(
    builder: (context) => EditProfileScreen(),
  )),
)
  • Examples: Edit Profile, Change Password, Subscription, Export Data, Help & Support, Privacy Policy, Terms of Service
  • Trailing: chevron.right (12×12pt, gray)
  • Full row is tappable (44pt height minimum)
B) Toggle Row:
CupertinoFormRow(
  prefix: Text('Notifications', style: AppTypography.body),
  child: CupertinoSwitch(
    value: notificationsEnabled,
    onChanged: (value) => setState(() => notificationsEnabled = value),
    activeColor: AppColors.primaryCoralLight,
  ),
)
  • Example: Notifications (Daily Journal Reminder)
  • Trailing: CupertinoSwitch (51×31pt, coral when active)
  • Switch is tappable, label also tappable for accessibility
C) Picker Row:
CupertinoFormRow(
  prefix: Text('Daily Reminder Time', style: AppTypography.body),
  child: Row(
    children: [
      Text('8:00 PM', style: AppTypography.callout.copyWith(
        color: AppColors.textSecondary(brightness),
      )),
      Icon(CupertinoIcons.chevron_right, size: 12),
    ],
  ),
  onTap: () => _showTimePicker(),
)
  • Examples: Daily Reminder Time, App Theme
  • Shows current value (e.g., “8:00 PM”, “Auto”)
  • Trailing: Value + chevron
  • Tapping opens picker (time picker or segmented control modal)
D) Destructive Row:
CupertinoFormRow(
  prefix: Text('Delete My Account', style: AppTypography.body.copyWith(
    color: AppColors.errorLight,
  )),
  child: CupertinoListTileChevron(),
  onTap: () => _showDeleteAccountConfirmation(),
)
  • Example: Delete My Account
  • Text color: errorLight (#D63031) / errorDark (#E74C3C)
  • Trailing: chevron (red)
  • Tapping shows destructive confirmation dialog
E) Info Row (non-interactive):
CupertinoFormRow(
  prefix: Text('App Version', style: AppTypography.body),
  child: Text('Version 1.0.0 (1)', style: AppTypography.callout.copyWith(
    color: AppColors.textSecondary(brightness),
  )),
)
  • Example: App Version
  • Trailing: Value only (no chevron)
  • Not tappable
Flutter Implementation (Full Section):
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Padding(
      padding: EdgeInsets.only(
        left: AppSpacing.md,
        top: AppSpacing.xs,
        bottom: AppSpacing.xxs,
      ),
      child: Text(
        'ACCOUNT',
        style: AppTypography.footnote.copyWith(
          color: AppColors.textSecondary(brightness),
        ),
      ),
    ),
    CupertinoFormSection.insetGrouped(
      margin: EdgeInsets.symmetric(horizontal: AppSpacing.md),
      children: [
        CupertinoFormRow(
          prefix: Text('Edit Profile'),
          child: CupertinoListTileChevron(),
          onTap: () => _navigateToEditProfile(),
        ),
        CupertinoFormRow(
          prefix: Text('Change Password'),
          child: CupertinoListTileChevron(),
          onTap: () => _navigateToChangePassword(),
        ),
        CupertinoFormRow(
          prefix: Text('Subscription'),
          child: CupertinoListTileChevron(),
          onTap: () => _navigateToSubscription(),
        ),
      ],
    ),
  ],
)
Accessibility:
  • Each row: “Edit Profile, button” / “Notifications, switch, on” / “App Version, Version 1.0.0 (1)”
  • VoiceOver reads section headers before rows
  • Focus order: Top to bottom within each section

4. Sign Out Button

Specifications:
  • Position: Below all sections, centered, 24pt top margin
  • Width: Full-width minus 32pt margin (16pt each side)
  • Height: 50pt (standard button height)
  • Background: Transparent (no fill)
  • Border: None
  • Text: “Sign Out” (headline, 17pt semibold)
  • Color: errorLight (#D63031) / errorDark (#E74C3C)
  • Border Radius: 12pt
Flutter Implementation:
Padding(
  padding: EdgeInsets.symmetric(
    horizontal: AppSpacing.md,
    vertical: AppSpacing.lg,
  ),
  child: CupertinoButton.filled(
    padding: EdgeInsets.zero,
    borderRadius: BorderRadius.circular(AppBorderRadius.md),
    child: Text('Sign Out', style: AppTypography.headline),
    color: CupertinoColors.systemRed,
    onPressed: () => _showSignOutConfirmation(),
  ),
)
Interaction:
  • Tap: Shows confirmation dialog:
    • Title: “Sign Out?”
    • Message: “Are you sure you want to sign out?”
    • Actions: “Cancel” (default), “Sign Out” (destructive, red)
  • Confirmed: Signs user out → navigates to Welcome screen (authentication flow from Phase 5)
States:
  • Default: Red text on transparent background
  • Pressed: Scale 0.98, red background at 10% opacity
  • Disabled: Not applicable (always enabled)
Accessibility:
  • VoiceOver: “Sign Out, button, destructive action”
  • Confirmation dialog: “Alert, Sign Out?, Are you sure you want to sign out?, Cancel button, Sign Out button”

Interactive States & Animations

Profile Photo Upload

Sequence:
  1. User taps photo → Action sheet slides up (0.3s easeOut)
  2. User selects “Take Photo” or “Choose from Library”
  3. Photo picker opens (native transition)
  4. Photo selected → Picker dismisses
  5. Upload begins:
    • Circular progress indicator overlays photo (0.2s fade in)
    • Progress: 0% → 100% (indeterminate spinner or determinate progress ring)
  6. Upload complete:
    • Progress indicator fades out (0.2s)
    • New photo crossfades in (0.3s)
    • Success toast: “Profile photo updated” (top of screen, 3s auto-dismiss)
Error Handling:
  • Upload fails → Error icon overlay + “Retry” button
  • Network error → “Upload failed. Check your connection and try again.”
  • File too large → “Photo must be under 10MB.”

Toggle Switch (Notifications)

Interaction:
  1. User taps switch → Immediate visual feedback (switch animates to on/off)
  2. If enabling notifications AND permission not granted:
    • iOS system permission dialog appears
    • User grants → Switch stays on, Daily Reminder Time row becomes enabled
    • User denies → Switch reverts to off, alert appears: “Enable notifications in Settings to receive daily reminders.”
States:
  • Off: Gray background, white knob on left
  • On: Coral background, white knob on right
  • Disabled: 40% opacity (if notifications permission denied at OS level)

Time Picker (Daily Reminder Time)

Interaction:
  1. User taps “Daily Reminder Time” row
  2. Modal bottom sheet slides up (0.3s easeOut) with:
    • Title: “Daily Reminder Time”
    • CupertinoDatePicker (time mode, 12-hour format, wheel picker)
    • Initial value: Current saved time (default 8:00 PM)
    • Actions: “Cancel” (leading), “Save” (trailing, coral)
  3. User spins picker wheels to select time
  4. User taps “Save”:
    • Picker dismisses (0.3s easeIn)
    • Row updates to show new time immediately
    • Success toast: “Reminder set for 8:00 PM”
  5. User taps “Cancel”:
    • Picker dismisses without saving
Validation:
  • No validation needed (time picker ensures valid time)
  • Notifications toggle must be ON to set time (otherwise show alert)
Flutter Implementation:
void _showTimePicker() {
  showCupertinoModalPopup(
    context: context,
    builder: (context) => Container(
      height: 300,
      color: AppColors.backgroundSurface(brightness),
      child: Column(
        children: [
          Container(
            padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CupertinoButton(
                  child: Text('Cancel'),
                  onPressed: () => Navigator.pop(context),
                ),
                Text('Daily Reminder Time', style: AppTypography.headline),
                CupertinoButton(
                  child: Text('Save'),
                  onPressed: () => _saveReminderTime(),
                ),
              ],
            ),
          ),
          Expanded(
            child: CupertinoDatePicker(
              mode: CupertinoDatePickerMode.time,
              initialDateTime: reminderTime,
              onDateTimeChanged: (DateTime newTime) {
                setState(() => reminderTime = newTime);
              },
            ),
          ),
        ],
      ),
    ),
  );
}

App Theme Selection

Interaction:
  1. User taps “App Theme” row
  2. Modal bottom sheet slides up with:
    • Title: “App Theme”
    • CupertinoSegmentedControl with 3 options:
      • “Auto” (follows system setting)
      • “Light” (always light mode)
      • “Dark” (always dark mode)
    • Current selection highlighted in coral
    • “Done” button (coral, top-right)
  3. User taps an option → Immediate preview (app theme changes in real-time)
  4. User taps “Done”:
    • Modal dismisses (0.3s easeIn)
    • Theme preference saved
    • No toast needed (change is obvious)
Flutter Implementation:
CupertinoSegmentedControl<AppThemeMode>(
  children: {
    AppThemeMode.auto: Text('Auto'),
    AppThemeMode.light: Text('Light'),
    AppThemeMode.dark: Text('Dark'),
  },
  groupValue: currentTheme,
  onValueChanged: (AppThemeMode value) {
    setState(() => currentTheme = value);
    _applyTheme(value); // Immediate preview
  },
)

Delete Account Confirmation

Sequence:
  1. User taps “Delete My Account” (red text row)
  2. Alert dialog appears (0.2s scale + fade in):
    • Title: “Delete Account?”
    • Message: “This will permanently delete all your entries, photos, and data. This action cannot be undone.”
    • TextField: “Enter your password to confirm” (password field, obscured)
    • Actions:
      • “Cancel” (default style, left)
      • “Delete Account” (destructive style, red, right)
  3. User enters password and taps “Delete Account”:
    • Validate password (show error if incorrect: “Incorrect password”)
    • If valid:
      • Loading spinner overlays dialog
      • API call to delete account (with 30-day soft delete grace period)
      • Success:
        • Dialog dismisses
        • User signed out
        • Navigate to Welcome screen
        • Toast (on Welcome screen): “Your account has been deleted. You have 30 days to recover it by signing in again.”
      • Error:
        • Spinner removed
        • Error alert: “Something went wrong. Please try again.”
Dialog Specifications:
  • Width: 270pt (iOS standard)
  • Padding: 16pt
  • Border Radius: 14pt
  • Background: backgroundSurfaceLight/Dark
  • Overlay: 40% black dim
Flutter Implementation:
showCupertinoDialog(
  context: context,
  builder: (context) => CupertinoAlertDialog(
    title: Text('Delete Account?'),
    content: Column(
      children: [
        Text(
          'This will permanently delete all your entries, photos, and data. This action cannot be undone.',
          style: AppTypography.callout,
        ),
        SizedBox(height: AppSpacing.md),
        CupertinoTextField(
          placeholder: 'Enter your password to confirm',
          obscureText: true,
          controller: passwordController,
        ),
      ],
    ),
    actions: [
      CupertinoDialogAction(
        child: Text('Cancel'),
        onPressed: () => Navigator.pop(context),
      ),
      CupertinoDialogAction(
        isDestructiveAction: true,
        child: Text('Delete Account'),
        onPressed: () => _confirmDeleteAccount(),
      ),
    ],
  ),
);
Accessibility:
  • Alert announced immediately by VoiceOver
  • Focus moves to password field
  • VoiceOver: “Alert, Delete Account?, This will permanently delete…”
  • Password field: “Enter your password to confirm, secure text field”

Sign Out Confirmation

Sequence:
  1. User taps “Sign Out” button
  2. Alert dialog appears:
    • Title: “Sign Out?”
    • Message: “Are you sure you want to sign out?”
    • Actions:
      • “Cancel” (default)
      • “Sign Out” (destructive, red)
  3. User taps “Sign Out”:
    • Dialog dismisses
    • User session cleared
    • Navigate to Welcome screen (authentication flow)
Dialog Specifications:
  • Same as Delete Account dialog specs
  • Simpler (no password field)
Flutter Implementation:
showCupertinoDialog(
  context: context,
  builder: (context) => CupertinoAlertDialog(
    title: Text('Sign Out?'),
    content: Text('Are you sure you want to sign out?'),
    actions: [
      CupertinoDialogAction(
        child: Text('Cancel'),
        onPressed: () => Navigator.pop(context),
      ),
      CupertinoDialogAction(
        isDestructiveAction: true,
        child: Text('Sign Out'),
        onPressed: () => _confirmSignOut(),
      ),
    ],
  ),
);

Responsive Behavior

Portrait (iPhone 12-15, 390pt width):
  • Full layout as shown above
  • Sections stack vertically
  • Horizontal margins: 16pt
  • Profile photo: 64×64pt
Landscape (iPhone rotated):
  • Same layout (vertical scroll)
  • No special landscape adaptations for Settings
Large Text (Dynamic Type AX5):
  • Row heights increase to accommodate larger text
  • Labels may wrap to 2 lines
  • Minimum touch target: 44pt maintained
  • Profile section expands vertically
  • Section headers remain uppercase but scale with Dynamic Type
Small Devices (iPhone SE, 320pt width):
  • Horizontal margins: 12pt (reduce from 16pt)
  • Profile photo: 56×56pt (reduce from 64pt)
  • Tighter spacing where possible
  • All content remains readable

Accessibility

VoiceOver Navigation:
  • Tab to Settings → “Settings, heading level 1”
  • Swipe right through elements:
    • “Profile photo, Ryan Thompson, button, double-tap to change”
    • “Email, [email protected]
    • “Premium subscriber”
    • “Account, section header”
    • “Edit Profile, button, double-tap to navigate”
    • “Change Password, button, double-tap to navigate”
    • (Continue through all rows…)
    • “Sign Out, button, destructive action, double-tap to sign out”
Dynamic Type Support:
  • All text uses iOS Dynamic Type styles
  • Test at smallest (xSmall) and largest (AX5) sizes
  • Rows expand to fit larger text
  • No text truncation
Color Contrast:
  • Text on background: 13.1:1 (AAA) for primary text
  • Gray text: 5.74:1 (AA) for secondary text
  • Red text (destructive): 4.5:1 (AA) minimum
  • Switch active color (coral): 4.5:1 on white background
Keyboard Navigation (iPad, future):
  • Tab order: Top to bottom, section by section
  • Focus indicators: 2pt blue ring around focused row
  • Enter key: Activates row (same as tap)
Reduce Motion:
  • Action sheets: Fade in instead of slide up
  • Dialogs: Fade + scale down to 0.9 instead of spring animation
  • Toggle switches: Instant state change (no sliding animation)

Edge Cases & Error Handling

1. Photo Upload Failures:
  • Network Error:
    • Show error icon on photo
    • “Upload failed. Check your connection and try again.”
    • “Retry” button overlays photo
  • File Too Large (>10MB):
    • Alert: “Photo must be under 10MB. Please choose a smaller photo.”
    • Photo not uploaded, reverts to previous photo
  • Invalid Format:
    • Alert: “This file type is not supported. Please use JPG, PNG, or HEIC.”
2. Notifications Permission Denied:
  • At OS Level (user denied in iOS Settings):
    • Toggle switch: Disabled (40% opacity)
    • Tapping toggle shows alert: “Notifications are disabled. Open Settings to enable them.”
    • Alert has “Open Settings” button → deep link to iOS Settings app
  • Never Prompted:
    • Tapping toggle requests iOS permission
    • If granted: Switch turns on, Daily Reminder Time row enabled
    • If denied: Switch stays off, alert shown
3. Delete Account Failures:
  • Incorrect Password:
    • Error below password field: “Incorrect password” (red text)
    • Password field border turns red
    • “Delete Account” button remains enabled (allow retry)
  • Network Error:
    • Alert: “Something went wrong. Please check your connection and try again.”
    • Dialog remains open, user can retry
  • Server Error:
    • Alert: “We couldn’t delete your account. Please contact support.”
    • Link to support email
4. Export Data Failures:
  • Network Error:
    • Alert: “Export failed. Please check your connection and try again.”
    • “Retry” button
  • Success:
    • Alert: “Your data export is ready. We’ve sent a download link to your email.”
    • User receives email with JSON/CSV file + photos ZIP
5. Premium User Navigates to Subscription:
  • Subscription Screen Behavior:
    • Show “Manage Subscription” section (not purchase options)
    • “View Receipt”, “Cancel Subscription”, “Contact Support”
    • Link to App Store subscription management
6. No Internet Connection:
  • Settings screen loads normally (cached user data)
  • Actions requiring network (Export Data, Delete Account) show:
    • Alert: “No internet connection. Please try again when connected.”

Design Tokens Used

Colors:
  • primaryCoralLight / primaryCoralDark - Toggle switch active, Save buttons
  • textPrimaryLight / textPrimaryDark - Row labels, section content
  • textSecondaryLight / textSecondaryDark - Email, value text, section headers
  • backgroundPrimaryLight / backgroundPrimaryDark - Screen background
  • backgroundSurfaceLight / backgroundSurfaceDark - Profile section, settings rows
  • borderLight / borderDark - Row dividers
  • errorLight / errorDark - Delete Account, Sign Out (destructive actions)
Typography:
  • headline (17pt semibold) - Navigation title, row labels, button text, user name
  • callout (16pt regular) - Email, value text, dialog messages
  • footnote (13pt regular) - Section headers (uppercase)
  • body (17pt regular) - Row labels
Spacing:
  • xxs (4pt) - Section header bottom padding
  • xs (8pt) - Section header top padding, premium badge gap
  • sm (12pt) - Profile photo to name gap, reduced margins on iPhone SE
  • md (16pt) - Screen horizontal margins, section internal padding
  • lg (24pt) - Sign Out button top margin
Border Radius:
  • md (12pt) - Profile section, settings sections, buttons
Shadows:
  • level1 - Profile section (subtle elevation)
Animations:
  • standard (300ms) - Action sheet slide up/down, dialog fade in/out
  • fast (200ms) - Profile photo crossfade, progress indicator fade in/out
  • instant (100ms) - Button press, toggle switch

Screen 2: Edit Profile Screen

Purpose

Allow users to update their profile photo, display name, and bio.

User Flow

Entry Point:
  • Settings Screen → Tap “Edit Profile” row
Exit Points:
  • Save → Validates and saves changes → Returns to Settings (with success toast)
  • Cancel (with unsaved changes) → Confirmation dialog → Returns to Settings or stays on screen
  • Cancel (no changes) → Returns to Settings immediately

Layout Structure

┌────────────────────────────────────────────────┐
│  Cancel          Edit Profile           Save   │ ← Navigation bar (44pt)
├────────────────────────────────────────────────┤
│                                                │
│                                                │
│                  ┌──────────┐                  │
│                  │          │                  │ ← Profile photo (120×120pt)
│                  │  Photo   │                  │   Center-aligned
│                  │ 120×120pt│                  │
│                  │          │                  │
│                  └──────────┘                  │
│                                                │
│              Change Photo                      │ ← Button (callout, coral, center)
│                                                │
│  ┌──────────────────────────────────────────┐ │
│  │  Display Name                            │ │ ← Section header (footnote, gray)
│  │  ┌────────────────────────────────────┐  │ │
│  │  │ Ryan Thompson                      │  │ │ ← Text field (body, 17pt)
│  │  └────────────────────────────────────┘  │ │   Border: 1pt gray
│  │  This name will appear on your profile.  │ │ ← Helper text (footnote, gray)
│  └──────────────────────────────────────────┘ │
│                                                │
│  ┌──────────────────────────────────────────┐ │
│  │  Bio (Optional)                          │ │
│  │  ┌────────────────────────────────────┐  │ │
│  │  │ Tell us about yourself...          │  │ │ ← Multiline text field
│  │  │                                    │  │ │   Min height: 80pt
│  │  │                                    │  │ │   Max: 150 chars
│  │  │                                    │  │ │   Scrollable if longer
│  │  └────────────────────────────────────┘  │ │
│  │  142 / 150                               │ │ ← Character count (footnote, gray)
│  └──────────────────────────────────────────┘ │
│                                                │
│                                                │
└────────────────────────────────────────────────┘
     Safe Area Inset (Tab Bar if present)

Component Breakdown

1. Navigation Bar

Specifications:
  • Height: 44pt
  • Background: Transparent with iOS blur effect
  • Title: “Edit Profile” (headline, 17pt semibold, center)
  • Left Action: “Cancel” button (callout, 16pt, coral)
  • Right Action: “Save” button (callout, 16pt semibold, coral)
Cancel Button:
  • Text: “Cancel”
  • Color: primaryCoralLight / primaryCoralDark
  • Behavior:
    • If no changes made: Dismiss immediately
    • If unsaved changes: Show confirmation dialog:
      • Title: “Discard Changes?”
      • Message: “Your changes will be lost if you don’t save them.”
      • Actions: “Keep Editing” (default), “Discard” (destructive)
Save Button:
  • Text: “Save”
  • Color: primaryCoralLight / primaryCoralDark (enabled), gray (disabled)
  • States:
    • Disabled: No changes made OR display name empty (validation fail)
    • Enabled: Changes made AND display name valid
    • Loading: Replaced with spinner (20×20pt) while saving
Flutter Implementation:
CupertinoNavigationBar(
  middle: Text('Edit Profile', style: AppTypography.headline),
  leading: CupertinoButton(
    padding: EdgeInsets.zero,
    child: Text('Cancel', style: AppTypography.callout),
    onPressed: () => _handleCancel(),
  ),
  trailing: CupertinoButton(
    padding: EdgeInsets.zero,
    child: isLoading
      ? CupertinoActivityIndicator()
      : Text(
          'Save',
          style: AppTypography.callout.copyWith(
            fontWeight: FontWeight.w600,
            color: isSaveEnabled
              ? AppColors.primaryCoral(brightness)
              : CupertinoColors.inactiveGray,
          ),
        ),
    onPressed: isSaveEnabled ? () => _saveProfile() : null,
  ),
)
Accessibility:
  • Cancel: “Cancel, button, double-tap to cancel editing”
  • Save: “Save, button, enabled/disabled, double-tap to save changes”
  • Title: “Edit Profile, heading level 1”

2. Profile Photo Section

Specifications:
  • Photo Size: 120×120pt circle (larger than Settings screen for better editing visibility)
  • Position: Center-aligned horizontally, 32pt from top of content area
  • Border: 2pt backgroundPrimaryLight/Dark (subtle separation)
  • Placeholder: Person silhouette icon (person.circle.fill, 120×120pt, gray)
Change Photo Button:
  • Position: 16pt below photo, center-aligned
  • Text: “Change Photo” (callout, 16pt, coral)
  • Background: Transparent (text button)
  • Height: 44pt (touch target)
Interaction:
  1. User taps “Change Photo”
  2. Action sheet slides up (0.3s easeOut):
    • Title: “Change Profile Photo”
    • Options:
      • “Take Photo” → Opens camera
      • “Choose from Library” → Opens photo picker (PHPickerViewController)
      • “Remove Photo” → Confirms removal, reverts to placeholder
      • “Cancel” (destructive appearance, but not destructive action)
  3. User selects photo:
    • Photo picker dismisses
    • Selected photo displays in circle (cropped to square if needed)
    • Photo NOT uploaded yet (upload happens on Save)
    • UI marks changes as unsaved (enables Save button)
Flutter Implementation:
Column(
  children: [
    GestureDetector(
      onTap: () => _showChangePhotoSheet(),
      child: Stack(
        children: [
          CircleAvatar(
            radius: 60,
            backgroundImage: selectedPhoto != null
              ? FileImage(selectedPhoto!)
              : (currentPhoto != null
                  ? NetworkImage(currentPhoto!)
                  : null),
            child: (selectedPhoto == null && currentPhoto == null)
              ? Icon(CupertinoIcons.person_circle_fill, size: 120)
              : null,
          ),
          if (isUploadingPhoto)
            Positioned.fill(
              child: Container(
                decoration: BoxDecoration(
                  color: Colors.black54,
                  shape: BoxShape.circle,
                ),
                child: Center(
                  child: CupertinoActivityIndicator(
                    radius: 16,
                    color: Colors.white,
                  ),
                ),
              ),
            ),
        ],
      ),
    ),
    SizedBox(height: AppSpacing.md),
    CupertinoButton(
      padding: EdgeInsets.zero,
      child: Text('Change Photo', style: AppTypography.callout.copyWith(
        color: AppColors.primaryCoral(brightness),
      )),
      onPressed: () => _showChangePhotoSheet(),
    ),
  ],
)
States:
  • Default: Current photo or placeholder
  • Selected (not saved): New photo displayed, Save button enabled
  • Uploading: Spinner overlay (32×32pt, white on 50% black background)
  • Error: Error icon overlay + retry option
Accessibility:
  • Photo: “Profile photo, current photo: Ryan Thompson, button, double-tap to change”
  • Change Photo button: “Change Photo, button, double-tap to select new photo”

3. Display Name Field

Specifications:
  • Section Header: “Display Name” (footnote, 13pt regular, uppercase, gray)
  • Text Field:
    • Height: 44pt
    • Padding: 12pt horizontal, 8pt vertical
    • Border: 1pt solid borderLight / borderDark
    • Border Radius: 8pt (small)
    • Background: backgroundSurfaceLight / backgroundSurfaceDark
    • Font: body (17pt regular)
    • Placeholder: “Your name” (gray)
    • Max Length: 50 characters
  • Helper Text: “This name will appear on your profile.” (footnote, 13pt, gray, 4pt below field)
Validation:
  • Required: Cannot be empty
  • Max Length: 50 characters
  • Real-time Validation:
    • Empty: Save button disabled
    • Valid (1-50 chars): Save button enabled (if changes made)
    • Invalid (>50 chars): Character count turns red, Save button disabled
Flutter Implementation:
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Padding(
      padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
      child: Text(
        'DISPLAY NAME',
        style: AppTypography.footnote.copyWith(
          color: AppColors.textSecondary(brightness),
        ),
      ),
    ),
    SizedBox(height: AppSpacing.xxs),
    Padding(
      padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
      child: CupertinoTextField(
        controller: nameController,
        placeholder: 'Your name',
        maxLength: 50,
        style: AppTypography.body,
        decoration: BoxDecoration(
          color: AppColors.backgroundSurface(brightness),
          border: Border.all(
            color: nameError != null
              ? AppColors.error(brightness)
              : AppColors.border(brightness),
            width: AppBorderWidth.thin,
          ),
          borderRadius: BorderRadius.circular(AppBorderRadius.sm),
        ),
        onChanged: (value) => _validateName(value),
      ),
    ),
    SizedBox(height: AppSpacing.xxs),
    Padding(
      padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
      child: Text(
        'This name will appear on your profile.',
        style: AppTypography.footnote.copyWith(
          color: AppColors.textSecondary(brightness),
        ),
      ),
    ),
  ],
)
States:
  • Default: Border gray, no error
  • Focus: Border coral (2pt)
  • Error (empty after blur): Border red, error text “Name is required”
  • Valid: Border gray (green checkmark NOT shown - keep clean)
Accessibility:
  • VoiceOver: “Display Name, text field, double-tap to edit, Ryan Thompson”
  • Helper text read after label
  • Error announced immediately when validation fails

4. Bio Field (Optional)

Specifications:
  • Section Header: “Bio (Optional)” (footnote, 13pt regular, uppercase, gray)
  • Text Field (Multiline):
    • Min Height: 80pt (3-4 lines)
    • Max Height: 120pt (scrollable if longer)
    • Padding: 12pt horizontal, 8pt vertical
    • Border: 1pt solid borderLight / borderDark
    • Border Radius: 8pt
    • Background: backgroundSurfaceLight / backgroundSurfaceDark
    • Font: body (17pt regular)
    • Placeholder: “Tell us about yourself…” (gray, multiline)
    • Max Length: 150 characters
  • Character Count: “0 / 150” (footnote, 13pt, gray, 4pt below field)
    • Shows at all times (not just near limit)
    • Turns red when approaching limit (>145 chars)
    • Prevents input at 150 chars
Flutter Implementation:
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Padding(
      padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
      child: Text(
        'BIO (OPTIONAL)',
        style: AppTypography.footnote.copyWith(
          color: AppColors.textSecondary(brightness),
        ),
      ),
    ),
    SizedBox(height: AppSpacing.xxs),
    Padding(
      padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
      child: CupertinoTextField(
        controller: bioController,
        placeholder: 'Tell us about yourself...',
        maxLines: 5,
        minLines: 3,
        maxLength: 150,
        style: AppTypography.body,
        decoration: BoxDecoration(
          color: AppColors.backgroundSurface(brightness),
          border: Border.all(
            color: AppColors.border(brightness),
            width: AppBorderWidth.thin,
          ),
          borderRadius: BorderRadius.circular(AppBorderRadius.sm),
        ),
        onChanged: (value) => setState(() => bioCharCount = value.length),
      ),
    ),
    SizedBox(height: AppSpacing.xxs),
    Padding(
      padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
      child: Text(
        '$bioCharCount / 150',
        style: AppTypography.footnote.copyWith(
          color: bioCharCount > 145
            ? AppColors.error(brightness)
            : AppColors.textSecondary(brightness),
        ),
      ),
    ),
  ],
)
States:
  • Default: Border gray, counter “0 / 150”
  • Focus: Border coral (2pt)
  • Typing: Counter updates in real-time
  • Near Limit (>145 chars): Counter text turns red
  • At Limit (150 chars): No more input allowed, counter red
Accessibility:
  • VoiceOver: “Bio, optional, text field, multiline, double-tap to edit”
  • Character count announced as “142 of 150 characters”
  • Focus: Cursor placed at end of existing text

Interactive States & Animations

Save Profile Flow

Sequence:
  1. User taps “Save” button (top-right)
  2. Validation runs:
    • Display name: Not empty, max 50 chars → Pass
    • Bio: Max 150 chars (optional) → Pass
  3. If validation passes:
    • Save button replaced with spinner (20×20pt, 0.2s crossfade)
    • API calls:
      • If photo changed: Upload photo to cloud storage (S3/GCS)
      • Update profile (name, bio, photo URL)
    • Success (within 2-3 seconds):
      • Spinner fades out (0.2s)
      • Screen dismisses with transition (0.3s slide down)
      • Settings screen updates profile section immediately
      • Success toast appears (top): “Profile updated” (3s auto-dismiss)
  4. If validation fails:
    • Shake animation on invalid field (0.3s)
    • Error text appears below field
    • Focus moves to first invalid field
    • Save button remains enabled (allow fixing and retry)
Error Handling:
  • Network Error:
    • Spinner removed
    • Alert: “Update failed. Please check your connection and try again.”
    • “Retry” and “Cancel” buttons
  • Photo Upload Fails:
    • Alert: “Photo upload failed. Your name and bio were saved.”
    • User can retry photo upload from Settings screen
  • Server Error:
    • Alert: “Something went wrong. Please try again later.”
Flutter Implementation:
Future<void> _saveProfile() async {
  if (!_validateForm()) return;

  setState(() => isLoading = true);

  try {
    String? photoUrl;

    // Upload photo if changed
    if (selectedPhoto != null) {
      photoUrl = await _uploadPhoto(selectedPhoto!);
    }

    // Update profile
    await _updateProfile(
      name: nameController.text,
      bio: bioController.text,
      photoUrl: photoUrl ?? currentPhotoUrl,
    );

    // Success
    Navigator.pop(context);
    _showSuccessToast('Profile updated');

  } catch (e) {
    setState(() => isLoading = false);
    _showErrorDialog('Update failed. Please check your connection and try again.');
  }
}

Cancel with Unsaved Changes

Sequence:
  1. User taps “Cancel” (top-left)
  2. Check for unsaved changes:
    • Compare current values with initial values
    • If identical: Dismiss immediately (no dialog)
    • If different: Show confirmation dialog
  3. Dialog appears (0.2s scale + fade):
    • Title: “Discard Changes?”
    • Message: “Your changes will be lost if you don’t save them.”
    • Actions:
      • “Keep Editing” (default, left)
      • “Discard” (destructive, red, right)
  4. User taps “Discard”:
    • Dialog dismisses
    • Screen dismisses (returns to Settings)
    • No changes saved
  5. User taps “Keep Editing”:
    • Dialog dismisses
    • Remains on Edit Profile screen
Flutter Implementation:
void _handleCancel() {
  if (_hasUnsavedChanges()) {
    showCupertinoDialog(
      context: context,
      builder: (context) => CupertinoAlertDialog(
        title: Text('Discard Changes?'),
        content: Text("Your changes will be lost if you don't save them."),
        actions: [
          CupertinoDialogAction(
            child: Text('Keep Editing'),
            onPressed: () => Navigator.pop(context),
          ),
          CupertinoDialogAction(
            isDestructiveAction: true,
            child: Text('Discard'),
            onPressed: () {
              Navigator.pop(context); // Dismiss dialog
              Navigator.pop(context); // Dismiss Edit Profile
            },
          ),
        ],
      ),
    );
  } else {
    Navigator.pop(context);
  }
}

bool _hasUnsavedChanges() {
  return nameController.text != initialName ||
         bioController.text != initialBio ||
         selectedPhoto != null;
}

Photo Upload Progress

Sequence:
  1. User selects photo from picker → Photo displays in UI (not uploaded yet)
  2. User taps Save:
    • Photo upload begins
    • Progress indicator overlays photo (circular spinner or progress ring)
    • Progress: 0% → 100% (if determinate, show percentage; if indeterminate, spinner)
  3. Upload complete:
    • Progress indicator fades out (0.2s)
    • Photo remains displayed
    • Continue with profile update API call
  4. Upload fails:
    • Progress indicator replaced with error icon
    • Alert: “Photo upload failed. Please try again.”
    • User can retry
Flutter Implementation:
Future<String> _uploadPhoto(File photo) async {
  setState(() => isUploadingPhoto = true);

  try {
    // Compress photo
    final compressed = await _compressPhoto(photo);

    // Upload to S3/GCS
    final url = await uploadToCloudStorage(compressed);

    setState(() => isUploadingPhoto = false);
    return url;

  } catch (e) {
    setState(() => isUploadingPhoto = false);
    throw Exception('Photo upload failed');
  }
}

Responsive Behavior

Portrait (iPhone 12-15, 390pt width):
  • Full layout as shown
  • Horizontal margins: 16pt
  • Profile photo: 120×120pt
Landscape (iPhone rotated):
  • Same layout (vertical scroll)
  • No special landscape adaptations
Large Text (Dynamic Type AX5):
  • Field heights increase to accommodate larger text
  • Labels scale with Dynamic Type
  • Helper text may wrap to 2 lines
  • Character counts remain inline (not wrapped)
Small Devices (iPhone SE, 320pt width):
  • Horizontal margins: 12pt
  • Profile photo: 100×100pt (reduce from 120pt)
  • Tighter spacing

Accessibility

VoiceOver Navigation:
  • Swipe right through elements:
    • “Edit Profile, heading level 1”
    • “Cancel, button, double-tap to cancel”
    • “Save, button, disabled/enabled, double-tap to save”
    • “Profile photo, Ryan Thompson, button, double-tap to change”
    • “Change Photo, button, double-tap to select new photo”
    • “Display Name, text field, Ryan Thompson, double-tap to edit”
    • “This name will appear on your profile”
    • “Bio, optional, text field, multiline, double-tap to edit”
    • “142 of 150 characters”
Dynamic Type Support:
  • All text uses iOS Dynamic Type
  • Fields expand to fit larger text
  • Test at AX5 (largest size)
Color Contrast:
  • All text meets WCAG 2.1 AA (4.5:1 minimum)
  • Error text (red): 4.5:1 on background
  • Placeholder text (gray): 4.5:1 on background
Keyboard Navigation (iPad):
  • Tab order: Photo → Change Photo → Display Name → Bio → Save
  • Enter key: Saves profile (if valid)
  • Escape key: Cancels (with confirmation if unsaved changes)
Reduce Motion:
  • Dialogs: Fade in instead of scale
  • Screen transitions: Fade instead of slide
  • Field shake: Skip animation, just show error text

Edge Cases & Error Handling

1. Empty Display Name:
  • Validation: Real-time validation after blur
  • Error: “Name is required” (red text below field)
  • Save Button: Disabled until valid
2. Display Name Too Long (>50 chars):
  • Validation: Real-time character count
  • Prevention: Text field enforces maxLength (Flutter handles)
  • UI: Character counter doesn’t show (enforced at input level)
3. Bio Too Long (>150 chars):
  • Validation: Real-time character count
  • Prevention: Text field enforces maxLength
  • UI: Counter turns red at >145 chars
4. Photo Upload Fails:
  • Error: Alert: “Photo upload failed. Your name and bio were saved.”
  • Retry: User can retry from Settings screen (Change Photo)
  • Fallback: Profile updated without new photo
5. Network Error During Save:
  • Error: Alert: “Update failed. Please check your connection and try again.”
  • Retry: “Retry” button in alert
  • Cancel: “Cancel” button dismisses alert, stays on Edit Profile
6. User Changes Photo Multiple Times:
  • Behavior: Only the most recent selection is kept
  • Upload: Only upload once on Save (not on each selection)
7. User Taps Cancel Then Discard:
  • Behavior: All changes lost, returns to Settings
  • Photo: Selected photo not uploaded, reverts to original
8. User Saves With Same Values:
  • Behavior: API call still made (in case of sync issues)
  • UI: Success toast shown
  • Optimization (future): Detect no changes, skip API call

Design Tokens Used

Colors:
  • primaryCoralLight / primaryCoralDark - Save button, Change Photo button, focus borders
  • textPrimaryLight / textPrimaryDark - Field text, labels
  • textSecondaryLight / textSecondaryDark - Helper text, character count, section headers
  • backgroundPrimaryLight / backgroundPrimaryDark - Screen background
  • backgroundSurfaceLight / backgroundSurfaceDark - Text fields
  • borderLight / borderDark - Field borders
  • errorLight / errorDark - Error text, Discard button
Typography:
  • headline (17pt semibold) - Navigation title, Save button
  • callout (16pt regular) - Cancel button, Change Photo button, helper text, field values
  • footnote (13pt regular) - Section headers (uppercase), character count, helper text
  • body (17pt regular) - Text field input
Spacing:
  • xxs (4pt) - Helper text top margin, character count top margin
  • xs (8pt) - Section header bottom margin
  • sm (12pt) - Text field padding (horizontal), reduced margins on iPhone SE
  • md (16pt) - Screen horizontal margins, profile photo bottom margin, section vertical spacing
  • xl (32pt) - Profile photo top margin
Border Radius:
  • sm (8pt) - Text fields
Border Width:
  • thin (1pt) - Text field borders
  • medium (2pt) - Focus borders
Animations:
  • instant (100ms) - Button press
  • fast (200ms) - Spinner crossfade, progress indicator fade
  • standard (300ms) - Screen dismiss, dialog appear, field shake

Additional Specifications

Photo Upload Flow (Camera / Library)

Action Sheet Options:
showCupertinoModalPopup(
  context: context,
  builder: (context) => CupertinoActionSheet(
    title: Text('Change Profile Photo'),
    actions: [
      CupertinoActionSheetAction(
        child: Text('Take Photo'),
        onPressed: () {
          Navigator.pop(context);
          _openCamera();
        },
      ),
      CupertinoActionSheetAction(
        child: Text('Choose from Library'),
        onPressed: () {
          Navigator.pop(context);
          _openPhotoLibrary();
        },
      ),
      if (currentPhoto != null || selectedPhoto != null)
        CupertinoActionSheetAction(
          isDestructiveAction: true,
          child: Text('Remove Photo'),
          onPressed: () {
            Navigator.pop(context);
            _removePhoto();
          },
        ),
    ],
    cancelButton: CupertinoActionSheetAction(
      child: Text('Cancel'),
      onPressed: () => Navigator.pop(context),
    ),
  ),
);
Camera Flow:
  1. Action sheet → “Take Photo” → Dismiss sheet
  2. Camera opens (iOS native UIImagePickerController or image_picker package)
  3. User takes photo → Photo captured
  4. Camera dismisses
  5. Photo displays in Edit Profile screen (cropped to square circle)
  6. UI marks changes as unsaved
Library Flow:
  1. Action sheet → “Choose from Library” → Dismiss sheet
  2. Photo picker opens (PHPickerViewController or image_picker)
  3. User selects photo
  4. Picker dismisses
  5. Photo displays in Edit Profile screen (cropped to square)
  6. UI marks changes as unsaved
Remove Photo Flow:
  1. Action sheet → “Remove Photo” (red) → Dismiss sheet
  2. Photo immediately removed (reverts to placeholder silhouette)
  3. UI marks changes as unsaved
  4. On Save: Photo URL set to null in database
Photo Processing:
  • Crop: Square crop (center-aligned) if aspect ratio not 1:1
  • Resize: Max 1024×1024px (reduce file size)
  • Compression: JPEG quality 85% (balance quality and size)
  • Format: JPEG (convert HEIC/PNG if needed)
  • Max Size: 10MB (before compression)
Flutter Implementation (Camera):
import 'package:image_picker/image_picker.dart';

Future<void> _openCamera() async {
  final picker = ImagePicker();
  final XFile? photo = await picker.pickImage(
    source: ImageSource.camera,
    maxWidth: 1024,
    maxHeight: 1024,
    imageQuality: 85,
  );

  if (photo != null) {
    setState(() {
      selectedPhoto = File(photo.path);
      hasUnsavedChanges = true;
    });
  }
}

Future<void> _openPhotoLibrary() async {
  final picker = ImagePicker();
  final XFile? photo = await picker.pickImage(
    source: ImageSource.gallery,
    maxWidth: 1024,
    maxHeight: 1024,
    imageQuality: 85,
  );

  if (photo != null) {
    setState(() {
      selectedPhoto = File(photo.path);
      hasUnsavedChanges = true;
    });
  }
}

void _removePhoto() {
  setState(() {
    selectedPhoto = null;
    currentPhoto = null;
    hasUnsavedChanges = true;
  });
}
Permissions:
  • Camera: Request on first use via iOS system dialog
  • Photo Library: Request on first use
  • Permission Denied:
    • Camera: Alert “Camera access is required. Please enable it in Settings.”
    • Library: Alert “Photo library access is required. Please enable it in Settings.”
    • Both alerts have “Open Settings” button (deep link to iOS Settings)

Settings Screen: Additional Flows

Daily Reminder Time Picker (Detailed)

Modal Specifications:
  • Height: 300pt (fixed)
  • Background: backgroundSurfaceLight / backgroundSurfaceDark
  • Border Radius: 12pt (top corners only)
  • Position: Bottom of screen (sheet slides up from bottom)
Layout:
┌────────────────────────────────────────────────┐
│  Cancel    Daily Reminder Time         Save   │ ← Header (44pt)
├────────────────────────────────────────────────┤
│                                                │
│             ┌────────┬────────┬──────┐         │
│             │   8    │   00   │  PM  │         │ ← CupertinoDatePicker
│             │   9    │   15   │      │         │   (time mode, 12-hour)
│             │  10    │   30   │      │         │   Height: 216pt
│             │  11    │   45   │      │         │
│             └────────┴────────┴──────┘         │
│                                                │
└────────────────────────────────────────────────┘
Interaction:
  1. User taps “Daily Reminder Time” row on Settings screen
  2. Sheet slides up (0.3s easeOut)
  3. Picker displays current time (or default 8:00 PM if never set)
  4. User spins wheels to select time
  5. Options:
    • Save: Validates time, saves to preferences, dismisses sheet (0.3s easeIn), updates Settings row, shows success toast “Reminder set for 8:00 PM”
    • Cancel: Dismisses sheet without saving
Validation:
  • No validation needed (picker ensures valid time)
  • Notifications must be enabled:
    • If notifications OFF: Alert “Enable notifications first to set a reminder time.”
Flutter Implementation:
void _showTimePicker() {
  // Check notifications enabled
  if (!notificationsEnabled) {
    _showAlert('Enable notifications first to set a reminder time.');
    return;
  }

  showCupertinoModalPopup(
    context: context,
    builder: (context) => Container(
      height: 300,
      decoration: BoxDecoration(
        color: AppColors.backgroundSurface(brightness),
        borderRadius: BorderRadius.vertical(
          top: Radius.circular(AppBorderRadius.md),
        ),
      ),
      child: Column(
        children: [
          // Header
          Container(
            height: 44,
            padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CupertinoButton(
                  padding: EdgeInsets.zero,
                  child: Text('Cancel'),
                  onPressed: () => Navigator.pop(context),
                ),
                Text('Daily Reminder Time', style: AppTypography.headline),
                CupertinoButton(
                  padding: EdgeInsets.zero,
                  child: Text('Save', style: AppTypography.callout.copyWith(
                    fontWeight: FontWeight.w600,
                  )),
                  onPressed: () => _saveReminderTime(),
                ),
              ],
            ),
          ),
          // Picker
          Expanded(
            child: CupertinoDatePicker(
              mode: CupertinoDatePickerMode.time,
              use24hFormat: false,
              initialDateTime: reminderTime,
              onDateTimeChanged: (DateTime newTime) {
                setState(() => reminderTime = newTime);
              },
            ),
          ),
        ],
      ),
    ),
  );
}

void _saveReminderTime() {
  Navigator.pop(context);

  // Schedule local notification
  _scheduleNotification(reminderTime);

  // Update UI
  setState(() {});

  // Show success toast
  _showToast('Reminder set for ${_formatTime(reminderTime)}');
}
Accessibility:
  • Header: “Daily Reminder Time, heading”
  • Picker: VoiceOver announces hours, minutes, AM/PM as user swipes
  • Save: “Save, button, double-tap to save time”
  • Cancel: “Cancel, button, double-tap to cancel”

App Theme Selection (Detailed)

Modal Specifications:
  • Height: 220pt (fixed)
  • Background: backgroundSurfaceLight / backgroundSurfaceDark
  • Border Radius: 12pt (top corners)
  • Position: Bottom sheet
Layout:
┌────────────────────────────────────────────────┐
│              App Theme                  Done   │ ← Header (44pt)
├────────────────────────────────────────────────┤
│                                                │
│  ┌──────────────────────────────────────────┐ │
│  │  Auto  │  Light  │  Dark                │ │ ← Segmented control
│  └──────────────────────────────────────────┘ │   Height: 32pt
│                                                │   Full-width with padding
│                                                │
│  The app theme will follow your system        │ ← Helper text
│  setting when Auto is selected.               │   (footnote, gray)
│                                                │
└────────────────────────────────────────────────┘
Interaction:
  1. User taps “App Theme” row on Settings screen
  2. Sheet slides up (0.3s easeOut)
  3. Segmented control displays current selection (highlighted in coral)
  4. User taps a segment:
    • Auto: App theme follows iOS system setting (light/dark mode)
    • Light: App always uses light theme
    • Dark: App always uses dark theme
    • Immediate preview: App theme changes in real-time (no Save needed)
  5. User taps “Done”: Sheet dismisses (0.3s easeIn)
Flutter Implementation:
void _showThemePicker() {
  showCupertinoModalPopup(
    context: context,
    builder: (context) => Container(
      height: 220,
      decoration: BoxDecoration(
        color: AppColors.backgroundSurface(brightness),
        borderRadius: BorderRadius.vertical(
          top: Radius.circular(AppBorderRadius.md),
        ),
      ),
      child: Column(
        children: [
          // Header
          Container(
            height: 44,
            padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                SizedBox(width: 60), // Balance "Done" button
                Text('App Theme', style: AppTypography.headline),
                CupertinoButton(
                  padding: EdgeInsets.zero,
                  child: Text('Done', style: AppTypography.callout.copyWith(
                    fontWeight: FontWeight.w600,
                  )),
                  onPressed: () => Navigator.pop(context),
                ),
              ],
            ),
          ),
          SizedBox(height: AppSpacing.lg),
          // Segmented control
          Padding(
            padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
            child: CupertinoSegmentedControl<AppThemeMode>(
              children: {
                AppThemeMode.auto: Padding(
                  padding: EdgeInsets.symmetric(horizontal: 20),
                  child: Text('Auto'),
                ),
                AppThemeMode.light: Padding(
                  padding: EdgeInsets.symmetric(horizontal: 20),
                  child: Text('Light'),
                ),
                AppThemeMode.dark: Padding(
                  padding: EdgeInsets.symmetric(horizontal: 20),
                  child: Text('Dark'),
                ),
              },
              groupValue: currentThemeMode,
              onValueChanged: (AppThemeMode value) {
                setState(() => currentThemeMode = value);
                _applyTheme(value); // Immediate preview
                _saveThemePreference(value);
              },
            ),
          ),
          SizedBox(height: AppSpacing.md),
          // Helper text
          Padding(
            padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
            child: Text(
              'The app theme will follow your system setting when Auto is selected.',
              style: AppTypography.footnote.copyWith(
                color: AppColors.textSecondary(brightness),
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ],
      ),
    ),
  );
}

void _applyTheme(AppThemeMode mode) {
  // Update app theme immediately (via provider or setState)
  // This provides instant visual feedback
}
States:
  • Auto: Selected (coral background), app follows system theme
  • Light: Selected, app always light
  • Dark: Selected, app always dark
Accessibility:
  • Segmented control: “Theme mode, segmented control, Auto selected, Light, Dark”
  • Each segment: “Auto, button, selected” / “Light, button, not selected”
  • Helper text read after selection

Export My Data Flow

Sequence:
  1. User taps “Export My Data” row on Settings screen
  2. Alert appears:
    • Title: “Export Your Data”
    • Message: “We’ll send a download link to your email with all your journal entries and photos.”
    • Actions: “Cancel”, “Export”
  3. User taps “Export”:
    • Alert dismisses
    • Loading indicator appears on “Export My Data” row (spinner, trailing)
    • API call to generate export (backend creates JSON/CSV + ZIP of photos)
    • Success (within 10-30 seconds):
      • Spinner removed
      • Success toast: “Export ready! Check your email for the download link.”
      • User receives email with:
        • Subject: “Your Journal Export is Ready”
        • Body: “Your data export is ready. Click the link below to download. This link expires in 7 days.”
        • Link: Signed download URL (expires in 7 days)
    • Error:
      • Spinner removed
      • Alert: “Export failed. Please try again.”
Export Contents:
  • JSON File: All entries with metadata:
    {
      "user": {
        "name": "Ryan Thompson",
        "email": "[email protected]"
      },
      "entries": [
        {
          "id": "entry-123",
          "date": "2025-11-10T14:30:00Z",
          "photo_url": "https://...",
          "text": "What a beautiful day...",
          "emotions": ["happy", "grateful"],
          "location": "San Francisco, CA"
        }
      ]
    }
    
  • CSV File (alternative): Same data in CSV format
  • Photos ZIP: All photos in original resolution (organized by date)
Flutter Implementation:
Future<void> _exportData() async {
  showCupertinoDialog(
    context: context,
    builder: (context) => CupertinoAlertDialog(
      title: Text('Export Your Data'),
      content: Text(
        "We'll send a download link to your email with all your journal entries and photos.",
      ),
      actions: [
        CupertinoDialogAction(
          child: Text('Cancel'),
          onPressed: () => Navigator.pop(context),
        ),
        CupertinoDialogAction(
          child: Text('Export'),
          onPressed: () {
            Navigator.pop(context);
            _startExport();
          },
        ),
      ],
    ),
  );
}

Future<void> _startExport() async {
  setState(() => isExporting = true);

  try {
    // API call to generate export
    await apiClient.requestDataExport();

    setState(() => isExporting = false);
    _showToast('Export ready! Check your email for the download link.');

  } catch (e) {
    setState(() => isExporting = false);
    _showAlert('Export failed. Please try again.');
  }
}
Accessibility:
  • Export row: “Export My Data, button, double-tap to export”
  • During export: “Export My Data, button, exporting, loading”
  • Alert announced immediately when displayed

Delete Account Flow (Full Sequence)

Sequence:
  1. User taps “Delete My Account” row (red text) on Settings screen
  2. First Alert (Warning):
    • Title: “Delete Account?”
    • Message: “This will permanently delete all your entries, photos, and data. This action cannot be undone.”
    • Actions: “Cancel” (default), “Continue” (destructive, red)
  3. User taps “Continue”
  4. Second Alert (Password Confirmation):
    • Title: “Enter Password”
    • Message: “To confirm deletion, please enter your password.”
    • TextField: Password field (obscured)
    • Actions: “Cancel”, “Delete Account” (destructive, red)
  5. User enters password and taps “Delete Account”:
    • Validation:
      • If password incorrect: Error below field “Incorrect password”, allow retry
      • If password correct: Proceed
    • Loading spinner overlays dialog
    • API call to delete account:
      • Backend marks account for deletion (30-day soft delete)
      • Deletes user session
    • Success:
      • Dialog dismisses
      • User signed out
      • Navigate to Welcome screen
      • Toast on Welcome screen: “Your account has been deleted. You have 30 days to recover it by signing in again.”
    • Error:
      • Spinner removed
      • Alert: “Something went wrong. Please contact support.”
Flutter Implementation:
Future<void> _deleteAccount() async {
  // First alert: Warning
  final continueDelete = await showCupertinoDialog<bool>(
    context: context,
    builder: (context) => CupertinoAlertDialog(
      title: Text('Delete Account?'),
      content: Text(
        'This will permanently delete all your entries, photos, and data. This action cannot be undone.',
      ),
      actions: [
        CupertinoDialogAction(
          child: Text('Cancel'),
          onPressed: () => Navigator.pop(context, false),
        ),
        CupertinoDialogAction(
          isDestructiveAction: true,
          child: Text('Continue'),
          onPressed: () => Navigator.pop(context, true),
        ),
      ],
    ),
  );

  if (continueDelete != true) return;

  // Second alert: Password confirmation
  final passwordController = TextEditingController();

  showCupertinoDialog(
    context: context,
    builder: (context) => StatefulBuilder(
      builder: (context, setState) => CupertinoAlertDialog(
        title: Text('Enter Password'),
        content: Column(
          children: [
            Text('To confirm deletion, please enter your password.'),
            SizedBox(height: AppSpacing.md),
            CupertinoTextField(
              controller: passwordController,
              placeholder: 'Password',
              obscureText: true,
            ),
            if (passwordError != null) ...[
              SizedBox(height: AppSpacing.xs),
              Text(
                passwordError!,
                style: AppTypography.footnote.copyWith(
                  color: AppColors.error(brightness),
                ),
              ),
            ],
          ],
        ),
        actions: [
          CupertinoDialogAction(
            child: Text('Cancel'),
            onPressed: () => Navigator.pop(context),
          ),
          CupertinoDialogAction(
            isDestructiveAction: true,
            child: isDeleting
              ? CupertinoActivityIndicator()
              : Text('Delete Account'),
            onPressed: isDeleting
              ? null
              : () => _confirmDeleteAccount(passwordController.text, setState),
          ),
        ],
      ),
    ),
  );
}

Future<void> _confirmDeleteAccount(String password, StateSetter setState) async {
  setState(() => isDeleting = true);

  try {
    // Verify password
    final isValid = await apiClient.verifyPassword(password);

    if (!isValid) {
      setState(() {
        passwordError = 'Incorrect password';
        isDeleting = false;
      });
      return;
    }

    // Delete account
    await apiClient.deleteAccount();

    // Sign out and navigate
    Navigator.of(context).popUntil((route) => route.isFirst);
    Navigator.pushReplacement(
      context,
      CupertinoPageRoute(builder: (context) => WelcomeScreen()),
    );

    _showToast('Your account has been deleted. You have 30 days to recover it by signing in again.');

  } catch (e) {
    setState(() => isDeleting = false);
    Navigator.pop(context);
    _showAlert('Something went wrong. Please contact support.');
  }
}
Accessibility:
  • Both alerts announced immediately
  • Password field: “Password, secure text field, double-tap to edit”
  • Error announced when password incorrect
  • Loading state: “Deleting account, loading”

Summary: Phase 7 Complete

Screens Delivered: 2
  1. Settings Screen (Main) - 9 sections, 14+ interactive elements
  2. Edit Profile Screen - Photo upload, 2 form fields, save/cancel
Key Features:
  • ✅ Account management (edit profile, change password, delete account)
  • ✅ App settings (notifications, daily reminder time, theme selection)
  • ✅ Data & privacy (export data, delete account)
  • ✅ Help & support (FAQ, privacy policy, terms)
  • ✅ Sign out functionality
  • ✅ Profile photo upload (camera/library)
  • ✅ Form validation (name required, bio max length)
  • ✅ Unsaved changes confirmation
  • ✅ iOS-native feel (grouped lists, segmented controls, action sheets, alerts)
Total MVP UI Design: 28/28 screens (100% complete)

Next Steps for Development

Priority Order:

  1. Settings Screen: Implement all rows, sections, and navigation
  2. Edit Profile Screen: Implement form, photo upload, validation
  3. Time Picker Modal: Daily reminder time selection
  4. Theme Picker Modal: App theme selection (auto/light/dark)
  5. Export Data Flow: Backend integration for data export
  6. Delete Account Flow: Multi-step confirmation with password verification
  7. Sign Out Flow: Session management and navigation

Critical Integrations:

  • Photo Upload: Use image_picker package for camera/library access
  • Local Notifications: Use flutter_local_notifications for daily reminders
  • Theme Management: Use provider or riverpod for app-wide theme state
  • API Endpoints:
    • PATCH /api/users/profile - Update profile
    • POST /api/users/profile/photo - Upload photo
    • POST /api/users/export - Request data export
    • DELETE /api/users/account - Delete account (soft delete)

Testing Requirements:

  • ✅ All toggle switches functional
  • ✅ Time picker saves correctly and schedules notifications
  • ✅ Theme changes apply immediately
  • ✅ Photo upload/remove works on all devices
  • ✅ Form validation prevents empty name
  • ✅ Unsaved changes confirmation shows when needed
  • ✅ Delete account requires password and shows confirmations
  • ✅ Sign out clears session and returns to Welcome
  • ✅ VoiceOver navigation works for all elements
  • ✅ Dynamic Type scales properly at all sizes

END OF PHASE 7 UI SPECIFICATION Status: Ready for @frontend-developer handoff Next Agent: @software-architect (final architecture review) → @frontend-developer (implementation)

Handoff to @frontend-developer

Implementation Notes

Flutter Packages Needed:
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  image_picker: ^1.0.4  # Photo upload
  flutter_local_notifications: ^16.1.0  # Daily reminders
  provider: ^6.0.5  # State management (theme)
File Structure:
lib/
├── screens/
│   ├── settings/
│   │   ├── settings_screen.dart
│   │   ├── edit_profile_screen.dart
│   │   └── widgets/
│   │       ├── profile_section.dart
│   │       ├── settings_section.dart
│   │       ├── settings_row.dart
│   │       └── premium_badge.dart
├── services/
│   ├── photo_upload_service.dart
│   ├── notification_service.dart
│   └── theme_service.dart
└── models/
    └── user_profile.dart
Implementation Priority:
  1. Settings screen layout (grouped lists)
  2. Edit Profile screen (form + validation)
  3. Photo upload functionality (camera/library)
  4. Theme selection (immediate preview)
  5. Notifications (permission + scheduling)
  6. Data export (API integration)
  7. Delete account (multi-step confirmation)
Design QA Checklist:
  • All spacing matches design tokens
  • All colors match design tokens (light + dark mode)
  • All typography matches (Dynamic Type support)
  • All animations use correct durations and curves
  • All interactive states work (default, pressed, disabled, loading, error)
  • All edge cases handled (network errors, permission denied, validation failures)
  • VoiceOver navigation tested
  • Dynamic Type tested at AX5 (largest size)
  • Reduce Motion respected
Good luck with the final MVP implementation! 🎉