From 5b16ca61627d1a275a8cd4b7f35866762ea00a81 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 8 Jul 2025 19:34:40 +0200 Subject: [PATCH] Load profile information on startup --- .../lib/routes/profile/profile_screen.dart | 56 ++++++++++++++++- .../lib/services/api/api_client.dart | 2 + .../lib/services/api/auth_api.dart | 24 ++++++- .../lib/services/api/server_api.dart | 43 ++++++------- .../lib/services/router/router.dart | 4 +- .../lib/services/storage/prefs.dart | 15 +++++ .../lib/widgets/load_startup_data.dart | 62 +++++++++++++++++++ .../widgets/loaders/load_server_config.dart | 35 ----------- 8 files changed, 178 insertions(+), 63 deletions(-) create mode 100644 moneymgr_mobile/lib/widgets/load_startup_data.dart delete mode 100644 moneymgr_mobile/lib/widgets/loaders/load_server_config.dart diff --git a/moneymgr_mobile/lib/routes/profile/profile_screen.dart b/moneymgr_mobile/lib/routes/profile/profile_screen.dart index 94308da..d2fb989 100644 --- a/moneymgr_mobile/lib/routes/profile/profile_screen.dart +++ b/moneymgr_mobile/lib/routes/profile/profile_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:moneymgr_mobile/services/api/api_client.dart'; import 'package:moneymgr_mobile/services/storage/prefs.dart'; class ProfileScreen extends HookConsumerWidget { @@ -8,7 +9,60 @@ class ProfileScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final data = ref.watch(prefsProvider); + final api = ref.watch(apiServiceProvider); if (data.value == null) return CircularProgressIndicator(); - return Text("dd\n\n\n${data.value!.serverConfig()}"); + + final profile = data.value?.authInfo(); + + return Scaffold( + appBar: AppBar(title: Text("Profile")), + body: ListView( + children: [ + ListEntry( + title: "Server URL", + value: api?.token.apiUrl, + icon: Icons.link, + ), + ListEntry( + title: "Token ID", + value: api?.token.tokenId.toString(), + icon: Icons.key, + ), + ListEntry( + title: "User ID", + value: profile?.id.toString(), + icon: Icons.perm_contact_calendar_outlined, + ), + ListEntry( + title: "User name", + value: profile?.name, + icon: Icons.person, + ), + ListEntry(title: "User mail", value: profile?.mail, icon: Icons.mail), + ], + ), + ); + } +} + +class ListEntry extends StatelessWidget { + final String title; + final String? value; + final IconData icon; + + const ListEntry({ + super.key, + required this.title, + required this.value, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + subtitle: Text(value ?? ""), + leading: Icon(icon), + ); } } diff --git a/moneymgr_mobile/lib/services/api/api_client.dart b/moneymgr_mobile/lib/services/api/api_client.dart index cd56928..32cc3bd 100644 --- a/moneymgr_mobile/lib/services/api/api_client.dart +++ b/moneymgr_mobile/lib/services/api/api_client.dart @@ -1,6 +1,7 @@ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; import 'package:moneymgr_mobile/services/api/api_token.dart'; import 'package:moneymgr_mobile/services/storage/prefs.dart'; import 'package:moneymgr_mobile/services/storage/secure_storage.dart'; @@ -24,6 +25,7 @@ class ApiClient { /// Get Dio instance Future> execute(String uri, {String method = "GET"}) async { + Logger.root.fine("Request on ${token.apiUrl} - URI $uri"); return client.request( uri, options: Options( diff --git a/moneymgr_mobile/lib/services/api/auth_api.dart b/moneymgr_mobile/lib/services/api/auth_api.dart index cffa744..d8a5f16 100644 --- a/moneymgr_mobile/lib/services/api/auth_api.dart +++ b/moneymgr_mobile/lib/services/api/auth_api.dart @@ -1,10 +1,12 @@ // ignore_for_file: non_constant_identifier_names import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:logging/logging.dart'; import 'package:moneymgr_mobile/services/api/api_client.dart'; +import 'package:moneymgr_mobile/services/storage/prefs.dart'; -part 'auth_api.g.dart'; part 'auth_api.freezed.dart'; +part 'auth_api.g.dart'; @freezed abstract class AuthInfo with _$AuthInfo { @@ -27,4 +29,24 @@ extension AuthApi on ApiClient { final response = await execute("/auth/info", method: "GET"); return AuthInfo.fromJson(response.data); } + + /// Get authentication information, returning cached information in case of failure + Future authInfoOrCache() async { + try { + final config = await authInfo(); + this.prefs.setAuthInfo(config); + return config; + } catch (e, s) { + Logger.root.warning("Failed to fetch user information! $e $s"); + } + + final cached = this.prefs.authInfo(); + if (cached == null) { + throw Exception( + "Could not fetch user information, cached version is unavailable!", + ); + } + return cached; + } + } diff --git a/moneymgr_mobile/lib/services/api/server_api.dart b/moneymgr_mobile/lib/services/api/server_api.dart index 4fcae9e..7e47747 100644 --- a/moneymgr_mobile/lib/services/api/server_api.dart +++ b/moneymgr_mobile/lib/services/api/server_api.dart @@ -1,10 +1,8 @@ // ignore_for_file: non_constant_identifier_names import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:moneymgr_mobile/services/storage/prefs.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'api_client.dart'; @@ -40,33 +38,30 @@ abstract class ServerConfig with _$ServerConfig { /// Auth API extension ServerApi on ApiClient { - /// Get authentication information from server + /// Get server configuration Future serverConfig() async { final response = await execute("/server/config", method: "GET"); return ServerConfig.fromJson(response.data); } + /// Get server configuration, or retrieve cached information (if available, in + /// case of failure) + Future serverConfigOrCache() async { + try { + final config = await serverConfig(); + this.prefs.setServerConfig(config); + return config; + } catch (e, s) { + Logger.root.warning("Failed to fetch server configuration! $e $s"); + } + final cached = this.prefs.serverConfig(); + if (cached == null) { + throw Exception( + "Could not fetch server configuration, cached version is unavailable!", + ); + } + return cached; + } } -/// Get authentication information from server, or retrieve cached information (if available, in -/// case of failure) -@riverpod -Future serverConfigOrCache(Ref ref) async { - final client = ref.watch(apiServiceProvider)!; - try { - final config = await client.serverConfig(); - client.prefs.setServerConfig(config); - return config; - } catch (e, s) { - Logger.root.warning("Failed to fetch server configuration! $e $s"); - } - - final cached = client.prefs.serverConfig(); - if (cached == null) { - throw Exception( - "Could not fetch server configuration, cached version is unavailable!", - ); - } - return cached; -} \ No newline at end of file diff --git a/moneymgr_mobile/lib/services/router/router.dart b/moneymgr_mobile/lib/services/router/router.dart index a0f3836..23d02bd 100644 --- a/moneymgr_mobile/lib/services/router/router.dart +++ b/moneymgr_mobile/lib/services/router/router.dart @@ -8,7 +8,7 @@ import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart'; import 'package:moneymgr_mobile/routes/profile/profile_screen.dart'; import 'package:moneymgr_mobile/routes/settings/settings_screen.dart'; import 'package:moneymgr_mobile/services/router/routes_list.dart'; -import 'package:moneymgr_mobile/widgets/loaders/load_server_config.dart'; +import 'package:moneymgr_mobile/widgets/load_startup_data.dart'; import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -81,7 +81,7 @@ GoRouter router(Ref ref) { GoRoute( path: item.path, pageBuilder: (context, _) => NoTransitionPage( - child: LoadServerConfig( + child: LoadStartupData( child: ScaffoldWithNavigation( selectedIndex: index, navigationItems: navigationItems, diff --git a/moneymgr_mobile/lib/services/storage/prefs.dart b/moneymgr_mobile/lib/services/storage/prefs.dart index a9b4ae7..445fabd 100644 --- a/moneymgr_mobile/lib/services/storage/prefs.dart +++ b/moneymgr_mobile/lib/services/storage/prefs.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:moneymgr_mobile/services/api/auth_api.dart'; import 'package:moneymgr_mobile/services/api/server_api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -27,4 +28,18 @@ extension MoneyMgrSharedPreferences on SharedPreferencesWithCache { Future clearServerConfig() async { await remove("serverConfig"); } + + AuthInfo? authInfo() { + final json = getString("authInfo"); + if (json != null) return AuthInfo.fromJson(jsonDecode(json)); + return null; + } + + Future setAuthInfo(AuthInfo info) async { + await setString("authInfo", jsonEncode(info)); + } + + Future clearAuthInfo() async { + await remove("authInfo"); + } } diff --git a/moneymgr_mobile/lib/widgets/load_startup_data.dart b/moneymgr_mobile/lib/widgets/load_startup_data.dart new file mode 100644 index 0000000..c8f720d --- /dev/null +++ b/moneymgr_mobile/lib/widgets/load_startup_data.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:moneymgr_mobile/providers/auth_state.dart'; +import 'package:moneymgr_mobile/services/api/api_client.dart'; +import 'package:moneymgr_mobile/services/api/auth_api.dart'; +import 'package:moneymgr_mobile/services/api/server_api.dart'; +import 'package:moneymgr_mobile/widgets/full_screen_error.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'load_startup_data.g.dart'; + +@riverpod +Future _loadStartupElements(Ref ref) async { + final svc = ref.watch(apiServiceProvider); + if (svc == null) { + throw Exception("API client has not be initialized yet!"); + } + + Logger.root.info("Start to load startup elements"); + await svc.serverConfigOrCache(); + await svc.authInfoOrCache(); + Logger.root.info("Finish to load startup elements"); +} + +class LoadStartupData extends HookConsumerWidget { + final Widget child; + + const LoadStartupData({super.key, required this.child}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final serverConfig = ref.watch(_loadStartupElementsProvider); + + tryAgain() { + ref.refresh(_loadStartupElementsProvider); + } + + handleSignOut() { + ref.watch(currentAuthStateProvider.notifier).logout(); + } + + return switch (serverConfig) { + AsyncData() => child, + AsyncError(:final error) => FullScreenError( + message: "Failed to load server configuration!", + error: error.toString(), + actions: [ + MaterialButton( + onPressed: tryAgain, + child: Text("Try again".toUpperCase()), + ), + MaterialButton( + onPressed: handleSignOut, + child: Text("Sign out".toUpperCase()), + ), + ], + ), + _ => const Scaffold(body: Center(child: CircularProgressIndicator())), + }; + } +} diff --git a/moneymgr_mobile/lib/widgets/loaders/load_server_config.dart b/moneymgr_mobile/lib/widgets/loaders/load_server_config.dart deleted file mode 100644 index 292a970..0000000 --- a/moneymgr_mobile/lib/widgets/loaders/load_server_config.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:moneymgr_mobile/providers/auth_state.dart'; -import 'package:moneymgr_mobile/services/api/server_api.dart'; -import 'package:moneymgr_mobile/widgets/full_screen_error.dart'; - -class LoadServerConfig extends HookConsumerWidget { - final Widget child; - - const LoadServerConfig({super.key, required this.child}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final serverConfig = ref.watch(serverConfigOrCacheProvider); - - handleSignOut() { - ref.watch(currentAuthStateProvider.notifier).logout(); - } - - return switch (serverConfig) { - AsyncData() => child, - AsyncError(:final error) => FullScreenError( - message: "Failed to load server configuration!", - error: error.toString(), - actions: [ - MaterialButton( - onPressed: handleSignOut, - child: Text("Sign out".toUpperCase()), - ), - ], - ), - _ => const Scaffold(body: Center(child: CircularProgressIndicator())), - }; - } -}