Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ PODS:
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- sqlite3 (3.51.1):
Expand Down Expand Up @@ -41,6 +43,7 @@ DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
Expand All @@ -57,6 +60,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
sqlite3_flutter_libs:
Expand All @@ -70,6 +75,7 @@ SPEC CHECKSUMS:
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
Expand Down
4 changes: 4 additions & 0 deletions app/linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@

#include "generated_plugin_registrant.h"

#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>

void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
Expand Down
1 change: 1 addition & 0 deletions app/linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#

list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
sqlite3_flutter_libs
)
Expand Down
2 changes: 2 additions & 0 deletions app/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation

import device_info_plus
import file_selector_macos
import flutter_secure_storage_macos
import package_info_plus
import sqlite3_flutter_libs
Expand All @@ -14,6 +15,7 @@ import wakelock_plus

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
Expand Down
112 changes: 112 additions & 0 deletions app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ packages:
relative: true
source: path
version: "0.0.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
crypto:
dependency: transitive
description:
Expand Down Expand Up @@ -197,6 +205,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
flutter:
dependency: "direct main"
description: flutter
Expand All @@ -215,6 +255,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
version: "2.0.33"
flutter_riverpod:
dependency: "direct main"
description:
Expand Down Expand Up @@ -337,6 +385,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image_picker:
dependency: transitive
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156
url: "https://pub.dev"
source: hosted
version: "0.8.13+14"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
intl:
dependency: transitive
description:
Expand Down
3 changes: 3 additions & 0 deletions app/windows/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@

#include "generated_plugin_registrant.h"

#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>

void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
Expand Down
1 change: 1 addition & 0 deletions app/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#

list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
sqlite3_flutter_libs
)
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/integrate-user-api/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-23
23 changes: 23 additions & 0 deletions openspec/changes/integrate-user-api/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Context

The `AuthProvider` now uniquely manages the session state as a **Boolean** (`isLoggedIn`). All user metadata (profile, display name, avatar) must be resolved through a separate, reactive pipeline using `UserRepository` and `UserProvider`, which stream raw **`UsersTableData`** from the Drift database.

## Goals

- **Decoupled Auth**: `AuthProvider` (in `core`) only provides the login status (`bool`).
- **Reactive Profile**: Create `UserProvider` (in `profile`) to stream `UsersTableData?`.
- **Database-First**: Cache fetched profile in the `UsersTable` via `UserRepository`.
- **API Integration**: Fetch from `/api/v2.5/me/` and persist to Drift.

## Decisions

1. **AuthProvider is Boolean Only**
- Holds `AsyncData<bool>` representing whether an auth token exists and is valid.
- All router and guard logic depends on this boolean.

2. **UserProvider streams UsersTableData?**
- Monitors the session status.
- Watches the specific user row in `UsersTable` for reactive UI updates.

3. **Drift Caching**
- Profile data is persisted to the local database to support offline viewing and consistent data flow across domain packages.
27 changes: 27 additions & 0 deletions openspec/changes/integrate-user-api/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Why

The app uses a hardcoded `mockCurrentUser` after login. `AuthProvider` stores the token but never fetches real user details from the backend. Profile screens display stale mock data, and edits are local-only. We need to integrate `/api/v2.5/me/` to fetch and update real user profiles.

## What Changes

- Add `getProfile()` and `updateProfile()` to `DataSource` / `HttpDataSource` for `GET` and `PATCH` on `/api/v2.5/me/`.
- Expand `UserDto` with fields the app uses from the API response, plus `fromJson` for parsing.
- Extend `UserRepository` to orchestrate profile fetch and update via `DataSource`.
- Update `AuthProvider.build()` to fetch real user from `UserRepository` instead of returning `mockCurrentUser`.
- Replace `AuthProvider.updateProfile(UserDto)` with an async method that delegates to `UserRepository`.
- Wire Edit Profile screen to handle async save with loading and error feedback.
- Decouple `courses` domain from `profile` by moving `UserProgressRepository` to the `core` package.

## Capabilities

### New Capabilities
- `user-profile-api`: Network integration for fetching and updating user profile via `/api/v2.5/me/`, flowing through `DataSource` → `UserRepository` → `AuthProvider`.

### Modified Capabilities
- `domain-isolation`: The `courses` package now relies exclusively on `core` for identity state and progress data, preventing boundary violations.

## Impact

- Affected code: `DataSource`, `HttpDataSource`, `MockDataSource`, `UserDto`, `UserRepository`, `UserProgressRepository`, `AuthProvider`.
- Dependencies: `dio` (already present).
- Behavior: After login or session restore, the app displays real user data. Profile edits are persisted server-side.
92 changes: 92 additions & 0 deletions openspec/changes/integrate-user-api/specs/user-profile-api/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
## ADDED Requirements

### Requirement: Fetch user profile on session restore
The system SHALL fetch the authenticated user's profile from `GET /api/v2.5/me/` whenever a valid token is present.

#### Scenario: Successful profile fetch after login
- **WHEN** the user successfully logs in (password or OTP)
- **THEN** the system MUST call `GET /api/v2.5/me/` with the stored auth token
- **AND** parse the response into a `UserDto` and persist it to the local database via `UserRepository`
- **AND** ensure the `userProvider` (Stream) emits the updated profile data

#### Scenario: Successful profile fetch on app startup
- **WHEN** the app starts and a stored auth token is found
- **THEN** the system MUST call `GET /api/v2.5/me/` to restore the user session
- **AND** the `authProvider` state MUST transition to `data(true)`
- **AND** the `userProvider` MUST emit the fetched `UserDto` (from cache or network)

#### Scenario: Profile fetch fails with invalid token
- **WHEN** the `GET /api/v2.5/me/` call returns 401
- **THEN** the system MUST clear the stored token
- **AND** the `authProvider` state MUST transition to `data(false)` (unauthenticated)
- **AND** the `userProvider` MUST emit `null`

#### Scenario: Profile fetch fails with network error
- **WHEN** the `GET /api/v2.5/me/` call fails due to a network error
- **THEN** the system MUST set `authProvider` state to `error`
- **AND** the app MUST show a retry option

---

### Requirement: Update user profile via API
The system SHALL persist profile edits to the backend via `PATCH /api/v2.5/me/`.

#### Scenario: Successful profile update
- **WHEN** the user submits valid edits on the Edit Profile screen
- **THEN** the system MUST send a `PATCH /api/v2.5/me/` request with the changed fields
- **AND** the local database MUST be updated with the `UserDto` parsed from the server response
- **AND** the `userProvider` MUST automatically emit the new profile data
- **AND** the system MUST navigate back to the Profile screen

#### Scenario: Profile update fails
- **WHEN** the `PATCH /api/v2.5/me/` request fails
- **THEN** the system MUST display the error message to the user on the Edit Profile screen
- **AND** the form data MUST be preserved so the user can retry

#### Scenario: Loading state during save
- **WHEN** the profile update request is in progress
- **THEN** the Save button MUST show a loading indicator
- **AND** form inputs MUST be disabled to prevent duplicate submissions

---

### Requirement: UserDto field mapping
The system SHALL parse the `/me/` API response into a `UserDto` using only the fields the app needs.

#### Scenario: Field mapping from API response
- **WHEN** the `/me/` response JSON is received
- **THEN** the system MUST map the following fields:
- `id` → `UserDto.id`
- `display_name` → `UserDto.name`
- `first_name` → `UserDto.firstName`
- `last_name` → `UserDto.lastName`
- `email` → `UserDto.email`
- `phone` → `UserDto.phone`
- `username` → `UserDto.username`
- `medium_image` → `UserDto.avatar`
- **AND** all other fields from the API response MUST be ignored

---

### Requirement: Profile data flows through DataSource and UserRepository
The system SHALL route profile operations through the existing `DataSource` → `UserRepository` pipeline.

#### Scenario: DataSource interface contract
- **WHEN** a profile operation is requested
- **THEN** `DataSource` MUST expose `getProfile(String token)` returning `UserDto`
- **AND** `DataSource` MUST expose `updateProfile(String token, Map<String, dynamic> data)` returning `UserDto`

#### Scenario: HttpDataSource implementation
- **WHEN** `HttpDataSource.getProfile()` is called
- **THEN** it MUST send `GET /api/v2.5/me/` with `Authorization: JWT <token>` header
- **AND** parse the response using `UserDto.fromJson`

#### Scenario: MockDataSource implementation
- **WHEN** `MockDataSource.getProfile()` is called
- **THEN** it MUST return `mockCurrentUser` after a simulated delay

#### Scenario: UserRepository orchestration
- **WHEN** `UserRepository.fetchProfile(token)` is called
- **THEN** it MUST delegate to `DataSource.getProfile(token)` and return the result
- **WHEN** `UserRepository.updateProfile(token, data)` is called
- **THEN** it MUST delegate to `DataSource.updateProfile(token, data)` and return the updated `UserDto`
Loading