refactor: riverpod 3
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
// Types are used through app_providers.dart
|
// Types are used through app_providers.dart
|
||||||
import '../providers/app_providers.dart';
|
import '../providers/app_providers.dart';
|
||||||
import '../models/user.dart';
|
import '../models/user.dart';
|
||||||
@@ -7,6 +9,8 @@ import 'token_validator.dart';
|
|||||||
import 'auth_cache_manager.dart';
|
import 'auth_cache_manager.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
|
part 'auth_state_manager.g.dart';
|
||||||
|
|
||||||
/// Comprehensive auth state representation
|
/// Comprehensive auth state representation
|
||||||
@immutable
|
@immutable
|
||||||
class AuthState {
|
class AuthState {
|
||||||
@@ -78,25 +82,41 @@ enum AuthStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Unified auth state manager - single source of truth for all auth operations
|
/// Unified auth state manager - single source of truth for all auth operations
|
||||||
class AuthStateManager extends Notifier<AuthState> {
|
@Riverpod(keepAlive: true)
|
||||||
|
class AuthStateManager extends _$AuthStateManager {
|
||||||
final AuthCacheManager _cacheManager = AuthCacheManager();
|
final AuthCacheManager _cacheManager = AuthCacheManager();
|
||||||
// Prevent overlapping silent-login attempts from multiple triggers
|
|
||||||
Future<bool>? _silentLoginFuture;
|
Future<bool>? _silentLoginFuture;
|
||||||
bool _initialized = false;
|
|
||||||
|
AuthState get _current =>
|
||||||
|
state.asData?.value ?? const AuthState(status: AuthStatus.initial);
|
||||||
|
|
||||||
|
void _set(AuthState next, {bool cache = false}) {
|
||||||
|
state = AsyncValue.data(next);
|
||||||
|
if (cache) {
|
||||||
|
_cacheManager.cacheAuthState(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _update(
|
||||||
|
AuthState Function(AuthState current) transform, {
|
||||||
|
bool cache = false,
|
||||||
|
}) {
|
||||||
|
final next = transform(_current);
|
||||||
|
_set(next, cache: cache);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AuthState build() {
|
Future<AuthState> build() async {
|
||||||
if (!_initialized) {
|
await _initialize();
|
||||||
_initialized = true;
|
return _current;
|
||||||
Future.microtask(_initialize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const AuthState(status: AuthStatus.initial);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize auth state from storage
|
/// Initialize auth state from storage
|
||||||
Future<void> _initialize() async {
|
Future<void> _initialize() async {
|
||||||
state = state.copyWith(status: AuthStatus.loading, isLoading: true);
|
_update(
|
||||||
|
(current) =>
|
||||||
|
current.copyWith(status: AuthStatus.loading, isLoading: true),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final storage = ref.read(optimizedStorageServiceProvider);
|
final storage = ref.read(optimizedStorageServiceProvider);
|
||||||
@@ -107,11 +127,14 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
// Fast path: trust token format to avoid blocking startup on network
|
// Fast path: trust token format to avoid blocking startup on network
|
||||||
final formatOk = _isValidTokenFormat(token);
|
final formatOk = _isValidTokenFormat(token);
|
||||||
if (formatOk) {
|
if (formatOk) {
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.authenticated,
|
(current) => current.copyWith(
|
||||||
token: token,
|
status: AuthStatus.authenticated,
|
||||||
isLoading: false,
|
token: token,
|
||||||
clearError: true,
|
isLoading: false,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
|
cache: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update API service with token and load user data in background
|
// Update API service with token and load user data in background
|
||||||
@@ -132,27 +155,33 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
// Token format invalid; clear and require login
|
// Token format invalid; clear and require login
|
||||||
DebugLogger.auth('Token format invalid, deleting token');
|
DebugLogger.auth('Token format invalid, deleting token');
|
||||||
await storage.deleteAuthToken();
|
await storage.deleteAuthToken();
|
||||||
state = state.copyWith(
|
_update(
|
||||||
|
(current) => current.copyWith(
|
||||||
|
status: AuthStatus.unauthenticated,
|
||||||
|
isLoading: false,
|
||||||
|
clearToken: true,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_update(
|
||||||
|
(current) => current.copyWith(
|
||||||
status: AuthStatus.unauthenticated,
|
status: AuthStatus.unauthenticated,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
clearToken: true,
|
clearToken: true,
|
||||||
clearError: true,
|
clearError: true,
|
||||||
);
|
),
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state = state.copyWith(
|
|
||||||
status: AuthStatus.unauthenticated,
|
|
||||||
isLoading: false,
|
|
||||||
clearToken: true,
|
|
||||||
clearError: true,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.error('auth-init-failed', scope: 'auth/state', error: e);
|
DebugLogger.error('auth-init-failed', scope: 'auth/state', error: e);
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.error,
|
(current) => current.copyWith(
|
||||||
error: 'Failed to initialize auth: $e',
|
status: AuthStatus.error,
|
||||||
isLoading: false,
|
error: 'Failed to initialize auth: $e',
|
||||||
|
isLoading: false,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,10 +191,12 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
String apiKey, {
|
String apiKey, {
|
||||||
bool rememberCredentials = false,
|
bool rememberCredentials = false,
|
||||||
}) async {
|
}) async {
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.loading,
|
(current) => current.copyWith(
|
||||||
isLoading: true,
|
status: AuthStatus.loading,
|
||||||
clearError: true,
|
isLoading: true,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -216,19 +247,19 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update state (without user data initially)
|
// Update state (without user data initially)
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.authenticated,
|
(current) => current.copyWith(
|
||||||
token: tokenStr,
|
status: AuthStatus.authenticated,
|
||||||
isLoading: false,
|
token: tokenStr,
|
||||||
clearError: true,
|
isLoading: false,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
|
cache: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update API service with token
|
// Update API service with token
|
||||||
_updateApiServiceToken(tokenStr);
|
_updateApiServiceToken(tokenStr);
|
||||||
|
|
||||||
// Cache the successful auth state
|
|
||||||
_cacheManager.cacheAuthState(state);
|
|
||||||
|
|
||||||
// Load user data in background (consistent with credentials method)
|
// Load user data in background (consistent with credentials method)
|
||||||
_loadUserData();
|
_loadUserData();
|
||||||
|
|
||||||
@@ -240,11 +271,13 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.error('api-key-login-failed', scope: 'auth/state', error: e);
|
DebugLogger.error('api-key-login-failed', scope: 'auth/state', error: e);
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.error,
|
(current) => current.copyWith(
|
||||||
error: e.toString(),
|
status: AuthStatus.error,
|
||||||
isLoading: false,
|
error: e.toString(),
|
||||||
clearToken: true,
|
isLoading: false,
|
||||||
|
clearToken: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -256,10 +289,12 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
String password, {
|
String password, {
|
||||||
bool rememberCredentials = false,
|
bool rememberCredentials = false,
|
||||||
}) async {
|
}) async {
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.loading,
|
(current) => current.copyWith(
|
||||||
isLoading: true,
|
status: AuthStatus.loading,
|
||||||
clearError: true,
|
isLoading: true,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -302,18 +337,18 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update state and API service
|
// Update state and API service
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.authenticated,
|
(current) => current.copyWith(
|
||||||
token: tokenStr,
|
status: AuthStatus.authenticated,
|
||||||
isLoading: false,
|
token: tokenStr,
|
||||||
clearError: true,
|
isLoading: false,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
|
cache: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
_updateApiServiceToken(tokenStr);
|
_updateApiServiceToken(tokenStr);
|
||||||
|
|
||||||
// Cache the successful auth state
|
|
||||||
_cacheManager.cacheAuthState(state);
|
|
||||||
|
|
||||||
// Load user data in background
|
// Load user data in background
|
||||||
_loadUserData();
|
_loadUserData();
|
||||||
|
|
||||||
@@ -321,11 +356,13 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.error('login-failed', scope: 'auth/state', error: e);
|
DebugLogger.error('login-failed', scope: 'auth/state', error: e);
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.error,
|
(current) => current.copyWith(
|
||||||
error: e.toString(),
|
status: AuthStatus.error,
|
||||||
isLoading: false,
|
error: e.toString(),
|
||||||
clearToken: true,
|
isLoading: false,
|
||||||
|
clearToken: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -361,10 +398,12 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _performSilentLogin() async {
|
Future<bool> _performSilentLogin() async {
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.loading,
|
(current) => current.copyWith(
|
||||||
isLoading: true,
|
status: AuthStatus.loading,
|
||||||
clearError: true,
|
isLoading: true,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -372,10 +411,12 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
final savedCredentials = await storage.getSavedCredentials();
|
final savedCredentials = await storage.getSavedCredentials();
|
||||||
|
|
||||||
if (savedCredentials == null) {
|
if (savedCredentials == null) {
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.unauthenticated,
|
(current) => current.copyWith(
|
||||||
isLoading: false,
|
status: AuthStatus.unauthenticated,
|
||||||
clearError: true,
|
isLoading: false,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -394,11 +435,13 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
ref.invalidate(serverConfigsProvider);
|
ref.invalidate(serverConfigsProvider);
|
||||||
ref.invalidate(activeServerProvider);
|
ref.invalidate(activeServerProvider);
|
||||||
|
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.error,
|
(current) => current.copyWith(
|
||||||
error:
|
status: AuthStatus.error,
|
||||||
'Saved server configuration is no longer available. Please reconnect.',
|
error:
|
||||||
isLoading: false,
|
'Saved server configuration is no longer available. Please reconnect.',
|
||||||
|
isLoading: false,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -411,10 +454,12 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
final activeServer = await ref.read(activeServerProvider.future);
|
final activeServer = await ref.read(activeServerProvider.future);
|
||||||
if (activeServer == null) {
|
if (activeServer == null) {
|
||||||
await storage.setActiveServerId(null);
|
await storage.setActiveServerId(null);
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.error,
|
(current) => current.copyWith(
|
||||||
error: 'Server configuration not found',
|
status: AuthStatus.error,
|
||||||
isLoading: false,
|
error: 'Server configuration not found',
|
||||||
|
isLoading: false,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -439,11 +484,13 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
await storage.deleteSavedCredentials();
|
await storage.deleteSavedCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.unauthenticated,
|
(current) => current.copyWith(
|
||||||
error: e.toString(),
|
status: AuthStatus.unauthenticated,
|
||||||
isLoading: false,
|
error: e.toString(),
|
||||||
clearToken: true,
|
isLoading: false,
|
||||||
|
clearToken: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -463,11 +510,13 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
_updateApiServiceToken(null);
|
_updateApiServiceToken(null);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.tokenExpired,
|
(current) => current.copyWith(
|
||||||
clearToken: true,
|
status: AuthStatus.tokenExpired,
|
||||||
clearUser: true,
|
clearToken: true,
|
||||||
clearError: true,
|
clearUser: true,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Attempt silent re-login if credentials are available
|
// Attempt silent re-login if credentials are available
|
||||||
@@ -482,7 +531,10 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
|
|
||||||
/// Logout user
|
/// Logout user
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
state = state.copyWith(status: AuthStatus.loading, isLoading: true);
|
_update(
|
||||||
|
(current) =>
|
||||||
|
current.copyWith(status: AuthStatus.loading, isLoading: true),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call server logout if possible
|
// Call server logout if possible
|
||||||
@@ -505,24 +557,28 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
_updateApiServiceToken(null);
|
_updateApiServiceToken(null);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.unauthenticated,
|
(current) => current.copyWith(
|
||||||
isLoading: false,
|
status: AuthStatus.unauthenticated,
|
||||||
clearToken: true,
|
isLoading: false,
|
||||||
clearUser: true,
|
clearToken: true,
|
||||||
clearError: true,
|
clearUser: true,
|
||||||
|
clearError: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
DebugLogger.auth('Logout complete');
|
DebugLogger.auth('Logout complete');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.error('logout-failed', scope: 'auth/state', error: e);
|
DebugLogger.error('logout-failed', scope: 'auth/state', error: e);
|
||||||
// Even if logout fails, clear local state
|
// Even if logout fails, clear local state
|
||||||
state = state.copyWith(
|
_update(
|
||||||
status: AuthStatus.unauthenticated,
|
(current) => current.copyWith(
|
||||||
isLoading: false,
|
status: AuthStatus.unauthenticated,
|
||||||
clearToken: true,
|
isLoading: false,
|
||||||
clearUser: true,
|
clearToken: true,
|
||||||
error: 'Logout error: $e',
|
clearUser: true,
|
||||||
|
error: 'Logout error: $e',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
_updateApiServiceToken(null);
|
_updateApiServiceToken(null);
|
||||||
}
|
}
|
||||||
@@ -532,13 +588,14 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
Future<void> _loadUserData() async {
|
Future<void> _loadUserData() async {
|
||||||
try {
|
try {
|
||||||
// First try to extract user info from JWT token if available
|
// First try to extract user info from JWT token if available
|
||||||
if (state.token != null) {
|
final current = _current;
|
||||||
final jwtUserInfo = TokenValidator.extractUserInfo(state.token!);
|
if (current.token != null) {
|
||||||
|
final jwtUserInfo = TokenValidator.extractUserInfo(current.token!);
|
||||||
if (jwtUserInfo != null) {
|
if (jwtUserInfo != null) {
|
||||||
final userFromJwt = _userFromJwtClaims(jwtUserInfo);
|
final userFromJwt = _userFromJwtClaims(jwtUserInfo);
|
||||||
if (userFromJwt != null) {
|
if (userFromJwt != null) {
|
||||||
DebugLogger.auth('Extracted user info from JWT token');
|
DebugLogger.auth('Extracted user info from JWT token');
|
||||||
state = state.copyWith(user: userFromJwt);
|
_update((current) => current.copyWith(user: userFromJwt));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Still try to load from server in background for complete data
|
// Still try to load from server in background for complete data
|
||||||
@@ -563,15 +620,16 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
Future<void> _loadServerUserData() async {
|
Future<void> _loadServerUserData() async {
|
||||||
try {
|
try {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
if (api != null && state.isAuthenticated) {
|
final current = _current;
|
||||||
|
if (api != null && current.isAuthenticated) {
|
||||||
// Check if we already have user data from token validation
|
// Check if we already have user data from token validation
|
||||||
if (state.user != null) {
|
if (current.user != null) {
|
||||||
DebugLogger.auth('user-data-present-from-token', scope: 'auth/state');
|
DebugLogger.auth('user-data-present-from-token', scope: 'auth/state');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final user = await api.getCurrentUser();
|
final user = await api.getCurrentUser();
|
||||||
state = state.copyWith(user: user);
|
_update((current) => current.copyWith(user: user));
|
||||||
DebugLogger.auth('Loaded complete user data from server');
|
DebugLogger.auth('Loaded complete user data from server');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -647,8 +705,8 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
// Store the user data if validation was successful
|
// Store the user data if validation was successful
|
||||||
if (serverResult.isValid &&
|
if (serverResult.isValid &&
|
||||||
validationUser != null &&
|
validationUser != null &&
|
||||||
state.isAuthenticated) {
|
_current.isAuthenticated) {
|
||||||
state = state.copyWith(user: validationUser);
|
_update((current) => current.copyWith(user: validationUser));
|
||||||
DebugLogger.auth('Cached user data from token validation');
|
DebugLogger.auth('Cached user data from token validation');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,31 +812,3 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
extension _StringFallbackExtension on String {
|
extension _StringFallbackExtension on String {
|
||||||
String ifEmptyReturn(String fallback) => isEmpty ? fallback : this;
|
String ifEmptyReturn(String fallback) => isEmpty ? fallback : this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provider for the unified auth state manager
|
|
||||||
final authStateManagerProvider = NotifierProvider<AuthStateManager, AuthState>(
|
|
||||||
AuthStateManager.new,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Computed providers for common auth state queries
|
|
||||||
final isAuthenticatedProvider = Provider<bool>((ref) {
|
|
||||||
return ref.watch(
|
|
||||||
authStateManagerProvider.select((state) => state.isAuthenticated),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
final authTokenProvider2 = Provider<String?>((ref) {
|
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.token));
|
|
||||||
});
|
|
||||||
|
|
||||||
final authUserProvider = Provider<User?>((ref) {
|
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.user));
|
|
||||||
});
|
|
||||||
|
|
||||||
final authErrorProvider2 = Provider<String?>((ref) {
|
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.error));
|
|
||||||
});
|
|
||||||
|
|
||||||
final isAuthLoadingProvider = Provider<bool>((ref) {
|
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.isLoading));
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../services/storage_service.dart';
|
import '../services/storage_service.dart';
|
||||||
// (removed duplicate) import '../services/optimized_storage_service.dart';
|
// (removed duplicate) import '../services/optimized_storage_service.dart';
|
||||||
@@ -22,6 +25,8 @@ import '../services/optimized_storage_service.dart';
|
|||||||
import '../services/socket_service.dart';
|
import '../services/socket_service.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
|
part 'app_providers.g.dart';
|
||||||
|
|
||||||
// Storage providers
|
// Storage providers
|
||||||
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
|
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
@@ -189,45 +194,171 @@ final apiServiceProvider = Provider<ApiService?>((ref) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Socket.IO service provider
|
// Socket.IO service provider
|
||||||
final socketServiceProvider = Provider<SocketService?>((ref) {
|
@Riverpod(keepAlive: true)
|
||||||
final reviewerMode = ref.watch(reviewerModeProvider);
|
class SocketServiceManager extends _$SocketServiceManager {
|
||||||
if (reviewerMode) return null;
|
SocketService? _service;
|
||||||
|
ProviderSubscription<String?>? _tokenSubscription;
|
||||||
|
|
||||||
final activeServer = ref.watch(activeServerProvider);
|
@override
|
||||||
final token = ref.watch(authTokenProvider3.select((t) => t));
|
FutureOr<SocketService?> build() async {
|
||||||
final transportMode = ref.watch(
|
final reviewerMode = ref.watch(reviewerModeProvider);
|
||||||
appSettingsProvider.select((s) => s.socketTransportMode),
|
if (reviewerMode) {
|
||||||
);
|
_disposeService();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return activeServer.maybeWhen(
|
final server = await ref.watch(activeServerProvider.future);
|
||||||
data: (server) {
|
if (server == null) {
|
||||||
if (server == null) return null;
|
_disposeService();
|
||||||
final s = SocketService(
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final transportMode = ref.watch(
|
||||||
|
appSettingsProvider.select((settings) => settings.socketTransportMode),
|
||||||
|
);
|
||||||
|
final websocketOnly = transportMode == 'ws';
|
||||||
|
final token = ref.watch(authTokenProvider3);
|
||||||
|
|
||||||
|
final requiresNewService =
|
||||||
|
_service == null ||
|
||||||
|
_service!.serverConfig.id != server.id ||
|
||||||
|
_service!.websocketOnly != websocketOnly;
|
||||||
|
if (requiresNewService) {
|
||||||
|
_disposeService();
|
||||||
|
_service = SocketService(
|
||||||
serverConfig: server,
|
serverConfig: server,
|
||||||
authToken: token,
|
authToken: token,
|
||||||
websocketOnly: transportMode == 'ws',
|
websocketOnly: websocketOnly,
|
||||||
);
|
);
|
||||||
// best-effort connect, but defer to post-frame with a small delay
|
_scheduleConnect(_service!);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
} else {
|
||||||
await Future.delayed(const Duration(milliseconds: 150));
|
_service!.updateAuthToken(token);
|
||||||
// ignore: discarded_futures
|
}
|
||||||
s.connect();
|
|
||||||
});
|
_tokenSubscription ??= ref.listen<String?>(authTokenProvider3, (
|
||||||
// Keep socket token up-to-date without reconstructing the service
|
previous,
|
||||||
ref.listen<String?>(authTokenProvider3, (prev, next) {
|
next,
|
||||||
s.updateAuthToken(next);
|
) {
|
||||||
});
|
_service?.updateAuthToken(next);
|
||||||
ref.onDispose(() {
|
});
|
||||||
try {
|
|
||||||
s.dispose();
|
ref.onDispose(() {
|
||||||
} catch (_) {}
|
_tokenSubscription?.close();
|
||||||
});
|
_tokenSubscription = null;
|
||||||
return s;
|
_disposeService();
|
||||||
},
|
});
|
||||||
orElse: () => null,
|
|
||||||
);
|
return _service;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleConnect(SocketService service) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 150));
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
try {
|
||||||
|
unawaited(service.connect());
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disposeService() {
|
||||||
|
if (_service == null) return;
|
||||||
|
try {
|
||||||
|
_service!.dispose();
|
||||||
|
} catch (_) {}
|
||||||
|
_service = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final socketServiceProvider = Provider<SocketService?>((ref) {
|
||||||
|
final asyncService = ref.watch(socketServiceManagerProvider);
|
||||||
|
return asyncService.maybeWhen(data: (service) => service, orElse: () => null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
enum SocketConnectionState { disconnected, connecting, connected }
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class SocketConnectionStream extends _$SocketConnectionStream {
|
||||||
|
StreamController<SocketConnectionState>? _controller;
|
||||||
|
ProviderSubscription<AsyncValue<SocketService?>>? _serviceSubscription;
|
||||||
|
void Function()? _cancelConnectListener;
|
||||||
|
void Function()? _cancelDisconnectListener;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<SocketConnectionState> build() {
|
||||||
|
final controller = StreamController<SocketConnectionState>.broadcast();
|
||||||
|
_controller = controller;
|
||||||
|
|
||||||
|
void emitState(SocketService? service) {
|
||||||
|
if (service == null) {
|
||||||
|
controller.add(SocketConnectionState.disconnected);
|
||||||
|
_unbindSocket();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.add(
|
||||||
|
service.isConnected
|
||||||
|
? SocketConnectionState.connected
|
||||||
|
: SocketConnectionState.connecting,
|
||||||
|
);
|
||||||
|
_bindSocket(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitState(
|
||||||
|
ref
|
||||||
|
.watch(socketServiceManagerProvider)
|
||||||
|
.maybeWhen(data: (service) => service, orElse: () => null),
|
||||||
|
);
|
||||||
|
|
||||||
|
_serviceSubscription = ref.listen<AsyncValue<SocketService?>>(
|
||||||
|
socketServiceManagerProvider,
|
||||||
|
(previous, next) {
|
||||||
|
emitState(
|
||||||
|
next.maybeWhen(data: (service) => service, orElse: () => null),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
_serviceSubscription?.close();
|
||||||
|
_serviceSubscription = null;
|
||||||
|
_unbindSocket();
|
||||||
|
_controller?.close();
|
||||||
|
_controller = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _bindSocket(SocketService service) {
|
||||||
|
_unbindSocket();
|
||||||
|
|
||||||
|
void handleConnect(dynamic _) {
|
||||||
|
_controller?.add(SocketConnectionState.connected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDisconnect(dynamic _) {
|
||||||
|
_controller?.add(SocketConnectionState.disconnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
service.socket?.on('connect', handleConnect);
|
||||||
|
service.socket?.on('disconnect', handleDisconnect);
|
||||||
|
|
||||||
|
_cancelConnectListener = () {
|
||||||
|
service.socket?.off('connect', handleConnect);
|
||||||
|
};
|
||||||
|
_cancelDisconnectListener = () {
|
||||||
|
service.socket?.off('disconnect', handleDisconnect);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unbindSocket() {
|
||||||
|
_cancelConnectListener?.call();
|
||||||
|
_cancelDisconnectListener?.call();
|
||||||
|
_cancelConnectListener = null;
|
||||||
|
_cancelDisconnectListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attachment upload queue provider
|
// Attachment upload queue provider
|
||||||
final attachmentUploadQueueProvider = Provider<AttachmentUploadQueue?>((ref) {
|
final attachmentUploadQueueProvider = Provider<AttachmentUploadQueue?>((ref) {
|
||||||
final api = ref.watch(apiServiceProvider);
|
final api = ref.watch(apiServiceProvider);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
import '../providers/app_providers.dart';
|
import '../providers/app_providers.dart';
|
||||||
import '../../features/auth/providers/unified_auth_providers.dart';
|
import '../../features/auth/providers/unified_auth_providers.dart';
|
||||||
@@ -16,6 +17,8 @@ import '../services/connectivity_service.dart';
|
|||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
import '../models/server_config.dart';
|
import '../models/server_config.dart';
|
||||||
|
|
||||||
|
part 'app_startup_providers.g.dart';
|
||||||
|
|
||||||
enum _ConversationWarmupStatus { idle, warming, complete }
|
enum _ConversationWarmupStatus { idle, warming, complete }
|
||||||
|
|
||||||
final _conversationWarmupStatusProvider =
|
final _conversationWarmupStatusProvider =
|
||||||
@@ -116,149 +119,174 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) {
|
|||||||
|
|
||||||
/// App-level startup/background task flow orchestrator.
|
/// App-level startup/background task flow orchestrator.
|
||||||
///
|
///
|
||||||
/// Moves background initialization out of widgets and into a Riverpod provider,
|
/// Moves background initialization out of widgets and into a Riverpod controller,
|
||||||
/// keeping UI lean and business logic centralized.
|
/// keeping UI lean and business logic centralized while avoiding side effects
|
||||||
final appStartupFlowProvider = Provider<void>((ref) {
|
/// during provider build.
|
||||||
// Ensure token integration listeners are active
|
@Riverpod(keepAlive: true)
|
||||||
ref.watch(authApiIntegrationProvider);
|
class AppStartupFlow extends _$AppStartupFlow {
|
||||||
ref.watch(apiTokenUpdaterProvider);
|
bool _started = false;
|
||||||
ref.watch(silentLoginCoordinatorProvider);
|
|
||||||
|
|
||||||
// Kick background model loading flow (non-blocking)
|
@override
|
||||||
ref.watch(backgroundModelLoadProvider);
|
FutureOr<void> build() {}
|
||||||
|
|
||||||
// If authenticated, keep socket service alive and connected
|
void start() {
|
||||||
final navState = ref.watch(authNavigationStateProvider);
|
if (_started) return;
|
||||||
if (navState == AuthNavigationState.authenticated) {
|
_started = true;
|
||||||
ref.watch(socketServiceProvider);
|
state = const AsyncValue<void>.data(null);
|
||||||
|
_activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure resume-triggered foreground refresh is active
|
void _activate() {
|
||||||
ref.watch(foregroundRefreshProvider);
|
final ref = this.ref;
|
||||||
|
|
||||||
// Keep Socket.IO connection alive in background within platform limits
|
// Ensure token integration listeners are active
|
||||||
ref.watch(socketPersistenceProvider);
|
ref.watch(authApiIntegrationProvider);
|
||||||
|
ref.watch(apiTokenUpdaterProvider);
|
||||||
|
ref.watch(silentLoginCoordinatorProvider);
|
||||||
|
|
||||||
// Ensure persistent streaming uses the shared connectivity service
|
// Kick background model loading flow (non-blocking)
|
||||||
final connectivityService = ref.watch(connectivityServiceProvider);
|
ref.watch(backgroundModelLoadProvider);
|
||||||
PersistentStreamingService().attachConnectivityService(connectivityService);
|
|
||||||
|
|
||||||
// Warm the conversations list in the background as soon as possible,
|
// If authenticated, keep socket service alive and connected
|
||||||
// but avoid doing so on poor connectivity to reduce startup load.
|
final navState = ref.watch(authNavigationStateProvider);
|
||||||
// Apply a small randomized delay to smooth load spikes across app wakes.
|
if (navState == AuthNavigationState.authenticated) {
|
||||||
Future.microtask(() async {
|
ref.watch(socketServiceProvider);
|
||||||
final online = ref.read(isOnlineProvider);
|
}
|
||||||
if (!online) return;
|
|
||||||
// Slightly increase jitter to reduce contention on startup
|
|
||||||
final jitter = Duration(
|
|
||||||
milliseconds: 150 + (DateTime.now().millisecond % 200),
|
|
||||||
);
|
|
||||||
// Defer until after first frame to keep first paint smooth
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
await Future.delayed(jitter);
|
|
||||||
_scheduleConversationWarmup(ref);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// One-time, post-frame system UI polish: set status bar icon brightness to
|
// Ensure resume-triggered foreground refresh is active
|
||||||
// match theme after the first frame. Avoids flicker at startup.
|
ref.watch(foregroundRefreshProvider);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
try {
|
// Keep Socket.IO connection alive in background within platform limits
|
||||||
final context = NavigationService.context;
|
ref.watch(socketPersistenceProvider);
|
||||||
final view = context != null ? View.maybeOf(context) : null;
|
ref.watch(socketConnectionStreamProvider);
|
||||||
final dispatcher = WidgetsBinding.instance.platformDispatcher;
|
|
||||||
final platformBrightness =
|
// Ensure persistent streaming uses the shared connectivity service
|
||||||
view?.platformDispatcher.platformBrightness ??
|
final connectivityService = ref.watch(connectivityServiceProvider);
|
||||||
dispatcher.platformBrightness;
|
PersistentStreamingService().attachConnectivityService(connectivityService);
|
||||||
final isDark = platformBrightness == Brightness.dark;
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
// Warm the conversations list in the background as soon as possible,
|
||||||
SystemUiOverlayStyle(
|
// but avoid doing so on poor connectivity to reduce startup load.
|
||||||
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
// Apply a small randomized delay to smooth load spikes across app wakes.
|
||||||
systemNavigationBarIconBrightness: isDark
|
Future.microtask(() async {
|
||||||
? Brightness.light
|
final online = ref.read(isOnlineProvider);
|
||||||
: Brightness.dark,
|
if (!online) return;
|
||||||
),
|
// Slightly increase jitter to reduce contention on startup
|
||||||
|
final jitter = Duration(
|
||||||
|
milliseconds: 150 + (DateTime.now().millisecond % 200),
|
||||||
);
|
);
|
||||||
} catch (_) {}
|
// Defer until after first frame to keep first paint smooth
|
||||||
});
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await Future.delayed(jitter);
|
||||||
// Watch for auth transitions to trigger warmup and other background work
|
_scheduleConversationWarmup(ref);
|
||||||
ref.listen<AuthNavigationState>(authNavigationStateProvider, (prev, next) {
|
|
||||||
if (next == AuthNavigationState.authenticated) {
|
|
||||||
// Schedule microtask so we don't perform side-effects inside build
|
|
||||||
Future.microtask(() async {
|
|
||||||
try {
|
|
||||||
final api = ref.read(apiServiceProvider);
|
|
||||||
if (api == null) {
|
|
||||||
DebugLogger.warning('API service not available for startup flow');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure API has the latest token immediately
|
|
||||||
final authToken = ref.read(authTokenProvider3);
|
|
||||||
if (authToken != null && authToken.isNotEmpty) {
|
|
||||||
api.updateAuthToken(authToken);
|
|
||||||
DebugLogger.auth('StartupFlow: Applied auth token to API');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload default model in background (best-effort) with an adaptive
|
|
||||||
// delay based on network latency to avoid hammering poor networks.
|
|
||||||
final latency = ref.read(connectivityServiceProvider).lastLatencyMs;
|
|
||||||
final delayMs = latency < 0
|
|
||||||
? 300
|
|
||||||
: latency > 800
|
|
||||||
? 600
|
|
||||||
: 200 + (latency ~/ 2);
|
|
||||||
Future.delayed(Duration(milliseconds: delayMs), () async {
|
|
||||||
try {
|
|
||||||
await ref.read(defaultModelProvider.future);
|
|
||||||
} catch (e) {
|
|
||||||
DebugLogger.warning(
|
|
||||||
'model-preload-failed',
|
|
||||||
scope: 'startup',
|
|
||||||
data: {'error': e},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Kick background chat warmup now that we're authenticated
|
|
||||||
_scheduleConversationWarmup(ref, force: true);
|
|
||||||
|
|
||||||
// Show onboarding once when user reaches chat and hasn't seen it yet
|
|
||||||
await _maybeShowOnboarding(ref);
|
|
||||||
} catch (e) {
|
|
||||||
DebugLogger.error('startup-flow-failed', scope: 'startup', error: e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
// Reset warmup state when leaving authenticated flow
|
|
||||||
ref
|
|
||||||
.read(_conversationWarmupStatusProvider.notifier)
|
|
||||||
.set(_ConversationWarmupStatus.idle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retry warmup when connectivity is restored
|
// One-time, post-frame system UI polish: set status bar icon brightness to
|
||||||
ref.listen<bool>(isOnlineProvider, (prev, next) {
|
// match theme after the first frame. Avoids flicker at startup.
|
||||||
if (next == true) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_scheduleConversationWarmup(ref);
|
try {
|
||||||
}
|
final context = NavigationService.context;
|
||||||
});
|
final view = context != null ? View.maybeOf(context) : null;
|
||||||
|
final dispatcher = WidgetsBinding.instance.platformDispatcher;
|
||||||
|
final platformBrightness =
|
||||||
|
view?.platformDispatcher.platformBrightness ??
|
||||||
|
dispatcher.platformBrightness;
|
||||||
|
final isDark = platformBrightness == Brightness.dark;
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
SystemUiOverlayStyle(
|
||||||
|
statusBarIconBrightness: isDark
|
||||||
|
? Brightness.light
|
||||||
|
: Brightness.dark,
|
||||||
|
systemNavigationBarIconBrightness: isDark
|
||||||
|
? Brightness.light
|
||||||
|
: Brightness.dark,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
|
||||||
// When conversations reload (e.g., manual refresh), ensure warmup runs again
|
// Watch for auth transitions to trigger warmup and other background work
|
||||||
ref.listen<AsyncValue<List<Conversation>>>(conversationsProvider, (
|
ref.listen<AuthNavigationState>(authNavigationStateProvider, (prev, next) {
|
||||||
previous,
|
if (next == AuthNavigationState.authenticated) {
|
||||||
next,
|
// Schedule microtask so we don't perform side-effects inside build
|
||||||
) {
|
Future.microtask(() async {
|
||||||
final wasReady = previous?.hasValue == true || previous?.hasError == true;
|
try {
|
||||||
if (wasReady && next.isLoading) {
|
final api = ref.read(apiServiceProvider);
|
||||||
ref
|
if (api == null) {
|
||||||
.read(_conversationWarmupStatusProvider.notifier)
|
DebugLogger.warning('API service not available for startup flow');
|
||||||
.set(_ConversationWarmupStatus.idle);
|
return;
|
||||||
Future.microtask(() => _scheduleConversationWarmup(ref, force: true));
|
}
|
||||||
}
|
|
||||||
});
|
// Ensure API has the latest token immediately
|
||||||
});
|
final authToken = ref.read(authTokenProvider3);
|
||||||
|
if (authToken != null && authToken.isNotEmpty) {
|
||||||
|
api.updateAuthToken(authToken);
|
||||||
|
DebugLogger.auth('StartupFlow: Applied auth token to API');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preload default model in background (best-effort) with an adaptive
|
||||||
|
// delay based on network latency to avoid hammering poor networks.
|
||||||
|
final latency = ref.read(connectivityServiceProvider).lastLatencyMs;
|
||||||
|
final delayMs = latency < 0
|
||||||
|
? 300
|
||||||
|
: latency > 800
|
||||||
|
? 600
|
||||||
|
: 200 + (latency ~/ 2);
|
||||||
|
Future.delayed(Duration(milliseconds: delayMs), () async {
|
||||||
|
try {
|
||||||
|
await ref.read(defaultModelProvider.future);
|
||||||
|
} catch (e) {
|
||||||
|
DebugLogger.warning(
|
||||||
|
'model-preload-failed',
|
||||||
|
scope: 'startup',
|
||||||
|
data: {'error': e},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kick background chat warmup now that we're authenticated
|
||||||
|
_scheduleConversationWarmup(ref, force: true);
|
||||||
|
|
||||||
|
// Show onboarding once when user reaches chat and hasn't seen it yet
|
||||||
|
await _maybeShowOnboarding(ref);
|
||||||
|
} catch (e) {
|
||||||
|
DebugLogger.error(
|
||||||
|
'startup-flow-failed',
|
||||||
|
scope: 'startup',
|
||||||
|
error: e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Reset warmup state when leaving authenticated flow
|
||||||
|
ref
|
||||||
|
.read(_conversationWarmupStatusProvider.notifier)
|
||||||
|
.set(_ConversationWarmupStatus.idle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry warmup when connectivity is restored
|
||||||
|
ref.listen<bool>(isOnlineProvider, (prev, next) {
|
||||||
|
if (next == true) {
|
||||||
|
_scheduleConversationWarmup(ref);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When conversations reload (e.g., manual refresh), ensure warmup runs again
|
||||||
|
ref.listen<AsyncValue<List<Conversation>>>(conversationsProvider, (
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
) {
|
||||||
|
final wasReady = previous?.hasValue == true || previous?.hasError == true;
|
||||||
|
if (wasReady && next.isLoading) {
|
||||||
|
ref
|
||||||
|
.read(_conversationWarmupStatusProvider.notifier)
|
||||||
|
.set(_ConversationWarmupStatus.idle);
|
||||||
|
Future.microtask(() => _scheduleConversationWarmup(ref, force: true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tracks whether we've already attempted a silent login for the current app session.
|
// Tracks whether we've already attempted a silent login for the current app session.
|
||||||
final _silentLoginAttemptedProvider =
|
final _silentLoginAttemptedProvider =
|
||||||
|
|||||||
@@ -18,13 +18,10 @@ class AuthActions {
|
|||||||
String password, {
|
String password, {
|
||||||
bool rememberCredentials = false,
|
bool rememberCredentials = false,
|
||||||
}) {
|
}) {
|
||||||
// Defer mutation to a microtask to avoid provider-build side-effects
|
return _auth.login(
|
||||||
return Future(
|
username,
|
||||||
() => _auth.login(
|
password,
|
||||||
username,
|
rememberCredentials: rememberCredentials,
|
||||||
password,
|
|
||||||
rememberCredentials: rememberCredentials,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,24 +29,22 @@ class AuthActions {
|
|||||||
String apiKey, {
|
String apiKey, {
|
||||||
bool rememberCredentials = false,
|
bool rememberCredentials = false,
|
||||||
}) {
|
}) {
|
||||||
return Future(
|
return _auth.loginWithApiKey(
|
||||||
() => _auth.loginWithApiKey(
|
apiKey,
|
||||||
apiKey,
|
rememberCredentials: rememberCredentials,
|
||||||
rememberCredentials: rememberCredentials,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> silentLogin() {
|
Future<bool> silentLogin() {
|
||||||
return Future(() => _auth.silentLogin());
|
return _auth.silentLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() {
|
Future<void> logout() {
|
||||||
return Future(() => _auth.logout());
|
return _auth.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() {
|
Future<void> refresh() {
|
||||||
return Future(() => _auth.refresh());
|
return _auth.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,29 +62,43 @@ final hasSavedCredentialsProvider2 = FutureProvider<bool>((ref) async {
|
|||||||
/// These automatically update when auth state changes
|
/// These automatically update when auth state changes
|
||||||
|
|
||||||
final isAuthenticatedProvider2 = Provider<bool>((ref) {
|
final isAuthenticatedProvider2 = Provider<bool>((ref) {
|
||||||
return ref.watch(
|
final authState = ref.watch(authStateManagerProvider);
|
||||||
authStateManagerProvider.select((state) => state.isAuthenticated),
|
return authState.maybeWhen(
|
||||||
|
data: (state) => state.isAuthenticated,
|
||||||
|
orElse: () => false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final authTokenProvider3 = Provider<String?>((ref) {
|
final authTokenProvider3 = Provider<String?>((ref) {
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.token));
|
final authState = ref.watch(authStateManagerProvider);
|
||||||
|
return authState.maybeWhen(data: (state) => state.token, orElse: () => null);
|
||||||
});
|
});
|
||||||
|
|
||||||
final currentUserProvider2 = Provider<User?>((ref) {
|
final currentUserProvider2 = Provider<User?>((ref) {
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.user));
|
final authState = ref.watch(authStateManagerProvider);
|
||||||
|
return authState.maybeWhen(data: (state) => state.user, orElse: () => null);
|
||||||
});
|
});
|
||||||
|
|
||||||
final authErrorProvider3 = Provider<String?>((ref) {
|
final authErrorProvider3 = Provider<String?>((ref) {
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.error));
|
final authState = ref.watch(authStateManagerProvider);
|
||||||
|
return authState.maybeWhen(data: (state) => state.error, orElse: () => null);
|
||||||
});
|
});
|
||||||
|
|
||||||
final isAuthLoadingProvider2 = Provider<bool>((ref) {
|
final isAuthLoadingProvider2 = Provider<bool>((ref) {
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.isLoading));
|
final authState = ref.watch(authStateManagerProvider);
|
||||||
|
if (authState.isLoading) return true;
|
||||||
|
return authState.maybeWhen(
|
||||||
|
data: (state) => state.isLoading,
|
||||||
|
orElse: () => false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final authStatusProvider = Provider<AuthStatus>((ref) {
|
final authStatusProvider = Provider<AuthStatus>((ref) {
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.status));
|
final authState = ref.watch(authStateManagerProvider);
|
||||||
|
return authState.maybeWhen(
|
||||||
|
data: (state) => state.status,
|
||||||
|
orElse: () => AuthStatus.loading,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use `ref.read(authActionsProvider).refresh()` instead of refresh providers
|
// Use `ref.read(authActionsProvider).refresh()` instead of refresh providers
|
||||||
@@ -107,19 +116,24 @@ final authApiIntegrationProvider = Provider<void>((ref) {
|
|||||||
/// Navigation helper provider - determines where user should go
|
/// Navigation helper provider - determines where user should go
|
||||||
final authNavigationStateProvider = Provider<AuthNavigationState>((ref) {
|
final authNavigationStateProvider = Provider<AuthNavigationState>((ref) {
|
||||||
final authState = ref.watch(authStateManagerProvider);
|
final authState = ref.watch(authStateManagerProvider);
|
||||||
|
return authState.when(
|
||||||
switch (authState.status) {
|
data: (state) {
|
||||||
case AuthStatus.initial:
|
switch (state.status) {
|
||||||
case AuthStatus.loading:
|
case AuthStatus.initial:
|
||||||
return AuthNavigationState.loading;
|
case AuthStatus.loading:
|
||||||
case AuthStatus.authenticated:
|
return AuthNavigationState.loading;
|
||||||
return AuthNavigationState.authenticated;
|
case AuthStatus.authenticated:
|
||||||
case AuthStatus.unauthenticated:
|
return AuthNavigationState.authenticated;
|
||||||
case AuthStatus.tokenExpired:
|
case AuthStatus.unauthenticated:
|
||||||
return AuthNavigationState.needsLogin;
|
case AuthStatus.tokenExpired:
|
||||||
case AuthStatus.error:
|
return AuthNavigationState.needsLogin;
|
||||||
return AuthNavigationState.error;
|
case AuthStatus.error:
|
||||||
}
|
return AuthNavigationState.error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: () => AuthNavigationState.loading,
|
||||||
|
error: (_, stack) => AuthNavigationState.error,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
enum AuthNavigationState { loading, authenticated, needsLogin, error }
|
enum AuthNavigationState { loading, authenticated, needsLogin, error }
|
||||||
|
|||||||
@@ -124,10 +124,15 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Listen for auth state changes to navigate on successful login
|
// Listen for auth state changes to navigate on successful login
|
||||||
ref.listen<AuthState>(authStateManagerProvider, (previous, next) {
|
ref.listen<AsyncValue<AuthState>>(authStateManagerProvider, (
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
) {
|
||||||
|
final nextState = next.asData?.value;
|
||||||
|
final prevState = previous?.asData?.value;
|
||||||
if (mounted &&
|
if (mounted &&
|
||||||
next.isAuthenticated &&
|
nextState?.isAuthenticated == true &&
|
||||||
previous?.isAuthenticated != true) {
|
prevState?.isAuthenticated != true) {
|
||||||
DebugLogger.auth(
|
DebugLogger.auth(
|
||||||
'Authentication successful, initializing background resources',
|
'Authentication successful, initializing background resources',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import 'package:flutter_animate/flutter_animate.dart';
|
|||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../core/auth/auth_state_manager.dart';
|
|
||||||
import '../providers/chat_providers.dart';
|
import '../providers/chat_providers.dart';
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
import '../../../core/utils/user_display_name.dart';
|
import '../../../core/utils/user_display_name.dart';
|
||||||
import '../../../core/utils/model_icon_utils.dart';
|
import '../../../core/utils/model_icon_utils.dart';
|
||||||
|
import '../../auth/providers/unified_auth_providers.dart';
|
||||||
|
|
||||||
import '../widgets/modern_chat_input.dart';
|
import '../widgets/modern_chat_input.dart';
|
||||||
import '../widgets/user_message_bubble.dart';
|
import '../widgets/user_message_bubble.dart';
|
||||||
@@ -901,7 +901,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
data: (user) => user,
|
data: (user) => user,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final authUser = ref.watch(authUserProvider);
|
final authUser = ref.watch(currentUserProvider2);
|
||||||
final user = userFromProfile ?? authUser;
|
final user = userFromProfile ?? authUser;
|
||||||
final greetingName = deriveUserDisplayName(user);
|
final greetingName = deriveUserDisplayName(user);
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import '../../chat/providers/chat_providers.dart' as chat;
|
|||||||
import '../../../shared/utils/ui_utils.dart';
|
import '../../../shared/utils/ui_utils.dart';
|
||||||
import '../../../core/services/navigation_service.dart';
|
import '../../../core/services/navigation_service.dart';
|
||||||
import '../../../shared/widgets/themed_dialogs.dart';
|
import '../../../shared/widgets/themed_dialogs.dart';
|
||||||
import '../../../core/auth/auth_state_manager.dart';
|
|
||||||
import 'package:conduit/l10n/app_localizations.dart';
|
import 'package:conduit/l10n/app_localizations.dart';
|
||||||
import '../../../core/utils/user_display_name.dart';
|
import '../../../core/utils/user_display_name.dart';
|
||||||
import '../../../core/utils/model_icon_utils.dart';
|
import '../../../core/utils/model_icon_utils.dart';
|
||||||
|
import '../../auth/providers/unified_auth_providers.dart';
|
||||||
import '../../../core/utils/user_avatar_utils.dart';
|
import '../../../core/utils/user_avatar_utils.dart';
|
||||||
import '../../../shared/utils/conversation_context_menu.dart';
|
import '../../../shared/utils/conversation_context_menu.dart';
|
||||||
import '../../../shared/widgets/user_avatar.dart';
|
import '../../../shared/widgets/user_avatar.dart';
|
||||||
@@ -1218,7 +1218,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
data: (u) => u,
|
data: (u) => u,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final authUser = ref.watch(authUserProvider);
|
final authUser = ref.watch(currentUserProvider2);
|
||||||
final user = userFromProfile ?? authUser;
|
final user = userFromProfile ?? authUser;
|
||||||
final api = ref.watch(apiServiceProvider);
|
final api = ref.watch(apiServiceProvider);
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:conduit/l10n/app_localizations.dart';
|
import 'package:conduit/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../../../core/auth/auth_state_manager.dart';
|
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../core/utils/user_display_name.dart';
|
import '../../../core/utils/user_display_name.dart';
|
||||||
import '../../../shared/theme/theme_extensions.dart';
|
import '../../../shared/theme/theme_extensions.dart';
|
||||||
import '../../../shared/widgets/sheet_handle.dart';
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
|
import '../../auth/providers/unified_auth_providers.dart';
|
||||||
|
|
||||||
class OnboardingSheet extends ConsumerStatefulWidget {
|
class OnboardingSheet extends ConsumerStatefulWidget {
|
||||||
const OnboardingSheet({super.key});
|
const OnboardingSheet({super.key});
|
||||||
@@ -73,7 +73,7 @@ class _OnboardingSheetState extends ConsumerState<OnboardingSheet> {
|
|||||||
data: (user) => user,
|
data: (user) => user,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final authUser = ref.watch(authUserProvider);
|
final authUser = ref.watch(currentUserProvider2);
|
||||||
final user = userFromProfile ?? authUser;
|
final user = userFromProfile ?? authUser;
|
||||||
final greetingName = deriveUserDisplayName(user);
|
final greetingName = deriveUserDisplayName(user);
|
||||||
final pages = _buildPages(l10n, greetingName);
|
final pages = _buildPages(l10n, greetingName);
|
||||||
|
|||||||
@@ -116,19 +116,12 @@ class ConduitApp extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ConduitAppState extends ConsumerState<ConduitApp> {
|
class _ConduitAppState extends ConsumerState<ConduitApp> {
|
||||||
ProviderSubscription<void>? _startupFlowSubscription;
|
|
||||||
Brightness? _lastAppliedOverlayBrightness;
|
Brightness? _lastAppliedOverlayBrightness;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Defer heavy provider initialization to after first frame to render UI sooner
|
// Defer heavy provider initialization to after first frame to render UI sooner
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _initializeAppState());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _initializeAppState());
|
||||||
|
|
||||||
// Activate app startup flow without tying it to root widget rebuilds
|
|
||||||
_startupFlowSubscription = ref.listenManual<void>(
|
|
||||||
appStartupFlowProvider,
|
|
||||||
(previous, next) {},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeAppState() {
|
void _initializeAppState() {
|
||||||
@@ -138,11 +131,11 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
|
|||||||
ref.read(authApiIntegrationProvider);
|
ref.read(authApiIntegrationProvider);
|
||||||
ref.read(defaultModelAutoSelectionProvider);
|
ref.read(defaultModelAutoSelectionProvider);
|
||||||
ref.read(shareReceiverInitializerProvider);
|
ref.read(shareReceiverInitializerProvider);
|
||||||
|
ref.read(appStartupFlowProvider.notifier).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_startupFlowSubscription?.close();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
import '../../core/services/connectivity_service.dart';
|
import '../../core/services/connectivity_service.dart';
|
||||||
|
import '../../core/providers/app_providers.dart';
|
||||||
import '../theme/theme_extensions.dart';
|
import '../theme/theme_extensions.dart';
|
||||||
import 'package:conduit/l10n/app_localizations.dart';
|
import 'package:conduit/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -20,7 +21,12 @@ class OfflineIndicator extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final connectivityStatus = ref.watch(connectivityStatusProvider);
|
final connectivityStatus = ref.watch(connectivityStatusProvider);
|
||||||
|
final socketConnection = ref.watch(socketConnectionStreamProvider);
|
||||||
final wasOffline = ref.watch(_wasOfflineProvider);
|
final wasOffline = ref.watch(_wasOfflineProvider);
|
||||||
|
final socketOffline = socketConnection.maybeWhen(
|
||||||
|
data: (state) => state == SocketConnectionState.disconnected,
|
||||||
|
orElse: () => false,
|
||||||
|
);
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -28,7 +34,7 @@ class OfflineIndicator extends ConsumerWidget {
|
|||||||
if (showBanner)
|
if (showBanner)
|
||||||
connectivityStatus.when(
|
connectivityStatus.when(
|
||||||
data: (status) {
|
data: (status) {
|
||||||
if (status == ConnectivityStatus.offline) {
|
if (status == ConnectivityStatus.offline || socketOffline) {
|
||||||
return _OfflineBanner();
|
return _OfflineBanner();
|
||||||
}
|
}
|
||||||
// Announce back-online briefly if we were previously offline
|
// Announce back-online briefly if we were previously offline
|
||||||
|
|||||||
72
pubspec.lock
72
pubspec.lock
@@ -13,10 +13,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
|
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.7.1"
|
version: "7.6.0"
|
||||||
|
analyzer_buffer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer_buffer
|
||||||
|
sha256: f7833bee67c03c37241c67f8741b17cc501b69d9758df7a5a4a13ed6c947be43
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.10"
|
||||||
|
analyzer_plugin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer_plugin
|
||||||
|
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.4"
|
||||||
ansicolor:
|
ansicolor:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -241,6 +257,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
custom_lint_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: custom_lint_core
|
||||||
|
sha256: cc4684d22ca05bf0a4a51127e19a8aea576b42079ed2bc9e956f11aaebe35dd1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.0"
|
||||||
|
custom_lint_visitor:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: custom_lint_visitor
|
||||||
|
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0+7.7.0"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -773,6 +805,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
mockito:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mockito
|
||||||
|
sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.5.0"
|
||||||
node_preamble:
|
node_preamble:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -997,6 +1037,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
|
riverpod_analyzer_utils:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: riverpod_analyzer_utils
|
||||||
|
sha256: c971e50f678d55c87d9e3b5015df7fcaadb2302a84ea101247cf99387b4554fe
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0-dev.6"
|
||||||
|
riverpod_annotation:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: riverpod_annotation
|
||||||
|
sha256: "1f9c1bc10b13f68ebc83ab391e77a865ff796880461d2a60c5ce301e1fb65f51"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
riverpod_generator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: riverpod_generator
|
||||||
|
sha256: "178d6bfa6de66d7db97db2466e5fad2da91a678ab376a7309235eec731755046"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1174,10 +1238,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "800f12fb87434defa13432ab37e33051b43b290a174e15259563b043cda40c46"
|
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "3.1.0"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ dependencies:
|
|||||||
wakelock_plus: ^1.4.0
|
wakelock_plus: ^1.4.0
|
||||||
share_plus: ^12.0.0
|
share_plus: ^12.0.0
|
||||||
share_handler: ^0.0.19
|
share_handler: ^0.0.19
|
||||||
|
riverpod_annotation: ^3.0.0
|
||||||
|
|
||||||
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ dev_dependencies:
|
|||||||
freezed: ^3.2.0
|
freezed: ^3.2.0
|
||||||
json_serializable: ^6.11.1
|
json_serializable: ^6.11.1
|
||||||
flutter_native_splash: ^2.4.6
|
flutter_native_splash: ^2.4.6
|
||||||
|
riverpod_generator: ^3.0.0
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user