Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04032a6dbc | ||
|
|
6b6614f482 | ||
|
|
265ca5959d |
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.1.16] - 2026-01-05
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Theme toggle causing logout due to AuthInterceptor auto-signout on 401
|
||||||
|
- Removed aggressive `signOut()` call in `AuthInterceptor.onError`
|
||||||
|
- 401 errors now propagate to calling code for graceful handling
|
||||||
|
- Preferences API 401 no longer triggers full logout redirect
|
||||||
|
|
||||||
|
## [1.1.15] - 2026-01-05
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Theme toggle causing auth issues due to AuthNotifier auto-dispose
|
||||||
|
- Applied `@persistentRiverpod` annotation to AuthNotifier
|
||||||
|
- AuthProvider now persists for app lifetime, preventing rebuild on theme change
|
||||||
|
|
||||||
|
## [1.1.14] - 2026-01-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Theme toggle causing auth issues due to ThemeProvider auto-dispose
|
||||||
|
- Added `@persistentRiverpod` annotation for providers that need keepAlive
|
||||||
|
- ThemeProvider now persists for app lifetime
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `@persistentRiverpod` annotation in `core/providers/annotations.dart`
|
||||||
|
- Reusable annotation for providers that should not auto-dispose
|
||||||
|
- Documented in ARCHITECTURE.md
|
||||||
|
|
||||||
## [1.1.13] - 2026-01-04
|
## [1.1.13] - 2026-01-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -420,6 +420,41 @@ ContainerRepository containerRepository(Ref ref) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Persistent Providers
|
||||||
|
|
||||||
|
By default, `@riverpod` generates providers with `isAutoDispose: true`, meaning they dispose when no longer watched. This causes issues for:
|
||||||
|
|
||||||
|
- **API clients** with interceptors that store a `Ref`
|
||||||
|
- **App-level state** like theme, auth, config
|
||||||
|
- **Providers with listeners** to other providers
|
||||||
|
|
||||||
|
Use `@persistentRiverpod` from `core/providers/annotations.dart` for these cases:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:tatlock_ui/core/providers/annotations.dart';
|
||||||
|
|
||||||
|
// ✅ Correct - persists for app lifetime
|
||||||
|
@persistentRiverpod
|
||||||
|
Dio coreApiClient(Ref ref) { ... }
|
||||||
|
|
||||||
|
@persistentRiverpod
|
||||||
|
class ThemeNotifier extends _$ThemeNotifier { ... }
|
||||||
|
|
||||||
|
// ❌ Wrong - auto-dispose can invalidate stored Ref
|
||||||
|
@riverpod
|
||||||
|
Dio coreApiClient(Ref ref) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use `@persistentRiverpod`:**
|
||||||
|
|
||||||
|
| Use Case | Annotation | Example |
|
||||||
|
|----------|------------|---------|
|
||||||
|
| API clients with interceptors | `@persistentRiverpod` | `coreApiClient`, `tatlockApiClient` |
|
||||||
|
| Auth state provider | `@persistentRiverpod` | `AuthNotifier` |
|
||||||
|
| Theme/config providers | `@persistentRiverpod` | `ThemeNotifier` |
|
||||||
|
| Feature data providers | `@riverpod` (default) | `ContainersNotifier` |
|
||||||
|
| UI state providers | `@riverpod` (default) | `SearchFilterNotifier` |
|
||||||
|
|
||||||
## File Naming Conventions
|
## File Naming Conventions
|
||||||
|
|
||||||
| Type | Convention | Example |
|
| Type | Convention | Example |
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:tatlock_ui/core/api/api_interceptors.dart';
|
import 'package:tatlock_ui/core/api/api_interceptors.dart';
|
||||||
import 'package:tatlock_ui/core/config/app_config.dart';
|
import 'package:tatlock_ui/core/config/app_config.dart';
|
||||||
|
import 'package:tatlock_ui/core/providers/annotations.dart';
|
||||||
|
|
||||||
import 'api_client_native.dart' if (dart.library.html) 'api_client_web.dart'
|
import 'api_client_native.dart' if (dart.library.html) 'api_client_web.dart'
|
||||||
as platform;
|
as platform;
|
||||||
@@ -9,10 +10,7 @@ import 'api_client_native.dart' if (dart.library.html) 'api_client_web.dart'
|
|||||||
part 'api_client.g.dart';
|
part 'api_client.g.dart';
|
||||||
|
|
||||||
/// Provides the Dio instance for Core API.
|
/// Provides the Dio instance for Core API.
|
||||||
///
|
@persistentRiverpod
|
||||||
/// Uses keepAlive to prevent auto-dispose - the AuthInterceptor stores
|
|
||||||
/// a Ref that must remain valid for the lifetime of API requests.
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
Dio coreApiClient(Ref ref) {
|
Dio coreApiClient(Ref ref) {
|
||||||
final options = BaseOptions(
|
final options = BaseOptions(
|
||||||
baseUrl: AppConfig.coreApiUrl,
|
baseUrl: AppConfig.coreApiUrl,
|
||||||
@@ -36,10 +34,7 @@ Dio coreApiClient(Ref ref) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Provides the Dio instance for Tatlock API.
|
/// Provides the Dio instance for Tatlock API.
|
||||||
///
|
@persistentRiverpod
|
||||||
/// Uses keepAlive to prevent auto-dispose - the AuthInterceptor stores
|
|
||||||
/// a Ref that must remain valid for the lifetime of API requests.
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
Dio tatlockApiClient(Ref ref) {
|
Dio tatlockApiClient(Ref ref) {
|
||||||
final options = BaseOptions(
|
final options = BaseOptions(
|
||||||
baseUrl: AppConfig.tatlockApiUrl,
|
baseUrl: AppConfig.tatlockApiUrl,
|
||||||
|
|||||||
@@ -38,16 +38,9 @@ class AuthInterceptor extends Interceptor {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
// Skip auth error handling for LAN development
|
// Don't auto-signout on 401 - let calling code handle auth errors gracefully.
|
||||||
if (!AppConfig.requiresAuth) {
|
// Auto-signout was causing issues (e.g., theme toggle triggering logout when
|
||||||
handler.next(err);
|
// preferences API returned 401).
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.response?.statusCode == 401) {
|
|
||||||
// Token expired - trigger re-authentication
|
|
||||||
_ref.read(authProvider.notifier).signOut();
|
|
||||||
}
|
|
||||||
handler.next(err);
|
handler.next(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../config/app_config.dart';
|
import '../config/app_config.dart';
|
||||||
|
import '../providers/annotations.dart';
|
||||||
import 'auth_datasource.dart';
|
import 'auth_datasource.dart';
|
||||||
import 'auth_state.dart';
|
import 'auth_state.dart';
|
||||||
import 'oidc_service.dart';
|
import 'oidc_service.dart';
|
||||||
@@ -24,7 +25,7 @@ part 'auth_provider.g.dart';
|
|||||||
///
|
///
|
||||||
/// After OIDC authentication, syncs with core-api via POST /auth/sync
|
/// After OIDC authentication, syncs with core-api via POST /auth/sync
|
||||||
/// to get user profile, roles, and preferences.
|
/// to get user profile, roles, and preferences.
|
||||||
@riverpod
|
@persistentRiverpod
|
||||||
class AuthNotifier extends _$AuthNotifier {
|
class AuthNotifier extends _$AuthNotifier {
|
||||||
// Storage keys
|
// Storage keys
|
||||||
static const _accessTokenKey = 'auth_access_token';
|
static const _accessTokenKey = 'auth_access_token';
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
/// Riverpod annotation for providers that should persist for the app lifetime.
|
||||||
|
///
|
||||||
|
/// Use this instead of `@riverpod` when:
|
||||||
|
/// - The provider holds app-level state (theme, auth, config)
|
||||||
|
/// - The provider stores a Ref that must remain valid (API clients with interceptors)
|
||||||
|
/// - Disposing would cause flickering or re-initialization issues
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// @persistentRiverpod
|
||||||
|
/// class ThemeNotifier extends _$ThemeNotifier { ... }
|
||||||
|
/// ```
|
||||||
|
const persistentRiverpod = Riverpod(keepAlive: true);
|
||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:tatlock_ui/core/auth/auth_provider.dart';
|
import 'package:tatlock_ui/core/auth/auth_provider.dart';
|
||||||
|
import 'package:tatlock_ui/core/providers/annotations.dart';
|
||||||
|
|
||||||
part 'theme_provider.g.dart';
|
part 'theme_provider.g.dart';
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ enum ThemeSetting {
|
|||||||
///
|
///
|
||||||
/// Syncs with API preferences when user is authenticated. On login, the theme
|
/// Syncs with API preferences when user is authenticated. On login, the theme
|
||||||
/// from API preferences takes precedence over local storage.
|
/// from API preferences takes precedence over local storage.
|
||||||
@riverpod
|
@persistentRiverpod
|
||||||
class ThemeNotifier extends _$ThemeNotifier {
|
class ThemeNotifier extends _$ThemeNotifier {
|
||||||
static const _prefsKey = 'theme_setting';
|
static const _prefsKey = 'theme_setting';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.13+1
|
version: 1.1.16+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.4
|
sdk: ^3.10.4
|
||||||
|
|||||||
Reference in New Issue
Block a user