feat: add local-first private AI digest workflow

Migrate app code into canonical feature slices, add phone-only AI digest scheduling and review, wire local notification/background task support, and cover the flow with tests.
This commit is contained in:
Rijad Zuzo
2026-05-17 00:17:20 +02:00
parent dab50abf0e
commit f655adfbea
212 changed files with 24178 additions and 15895 deletions
+4
View File
@@ -0,0 +1,4 @@
# Auth Application
This layer coordinates auth state and session lifecycle. Start here when you
need to change how the app decides whether a user is signed in.
@@ -0,0 +1,76 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:relationship_saver/core/auth/token_store.dart';
import 'package:relationship_saver/integrations/backend/backend_gateway.dart';
import 'package:relationship_saver/integrations/backend/backend_gateway_provider.dart';
import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
/// Manages authenticated session lifecycle for app-level navigation.
class SessionController extends AsyncNotifier<AuthSession?> {
@override
Future<AuthSession?> build() async {
return ref.read(tokenStoreProvider).read();
}
Future<void> signIn({
required SignInMethod method,
String? email,
String? password,
String? idToken,
}) async {
state = const AsyncLoading<AuthSession?>();
try {
final BackendGateway gateway = ref.read(backendGatewayProvider);
final AuthSession session = await gateway.signIn(
SignInRequest(
method: method,
email: email,
password: password,
idToken: idToken,
),
);
// Ensure fake gateway sessions are also persisted.
await ref.read(tokenStoreProvider).write(session);
state = AsyncData<AuthSession?>(session);
} catch (error, stackTrace) {
state = AsyncError<AuthSession?>(error, stackTrace);
}
}
Future<void> signOut() async {
final BackendGateway gateway = ref.read(backendGatewayProvider);
final TokenStore tokenStore = ref.read(tokenStoreProvider);
try {
await gateway.signOut();
} catch (_) {
// Offline-first sign-out should always clear local session.
}
await tokenStore.clear();
state = const AsyncData<AuthSession?>(null);
}
Future<void> refreshProfile() async {
final AuthSession? current = state.asData?.value;
if (current == null) {
return;
}
try {
final UserProfile profile = await ref.read(backendGatewayProvider).me();
final AuthSession updated = current.copyWith(user: profile);
await ref.read(tokenStoreProvider).write(updated);
state = AsyncData<AuthSession?>(updated);
} catch (_) {
// Keep current state if profile refresh fails.
}
}
}
final AsyncNotifierProvider<SessionController, AuthSession?>
sessionControllerProvider =
AsyncNotifierProvider<SessionController, AuthSession?>(
SessionController.new,
);