Add base skeleton
This commit is contained in:
75
moneymgr_mobile/lib/services/auth_state.dart
Normal file
75
moneymgr_mobile/lib/services/auth_state.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:moneymgr_mobile/routes/login/login_model.dart';
|
||||
import 'package:moneymgr_mobile/services/router/routes_list.dart';
|
||||
import 'package:moneymgr_mobile/services/storage/secure_storage.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'auth_state.g.dart';
|
||||
|
||||
/// The current authentication state of the app.
|
||||
///
|
||||
/// This notifier is responsible for saving/removing the token and profile info
|
||||
/// to the storage through the [login] and [logout] methods.
|
||||
@riverpod
|
||||
class CurrentAuthState extends _$CurrentAuthState {
|
||||
@override
|
||||
AuthState build() {
|
||||
final secureStorage = ref.watch(secureStorageProvider).requireValue;
|
||||
final token = secureStorage.get('token');
|
||||
return token != null ? AuthState.authenticated : AuthState.unauthenticated;
|
||||
}
|
||||
|
||||
/// Attempts to log in with [data] and saves the token and profile info to storage.
|
||||
/// Will invalidate the state if success.
|
||||
Future<void> login(LoginModel data) async {
|
||||
// TODO : perform login
|
||||
/*final secureStorage = ref.read(secureStorageProvider).requireValue;
|
||||
final token = await ref.read(apiServiceProvider).login(data);
|
||||
|
||||
// Save the new [token] and [profile] to secure storage.
|
||||
secureStorage.set('token', token);
|
||||
|
||||
ref
|
||||
// Invalidate the state so the auth state will be updated to authenticated.
|
||||
..invalidateSelf()
|
||||
// Invalidate the token provider so the API service will use the new token.
|
||||
..invalidate(tokenProvider);*/
|
||||
}
|
||||
|
||||
/// Logs out, deletes the saved token and profile info from storage, and invalidates
|
||||
/// the state.
|
||||
void logout() {
|
||||
// TODO : implement logic
|
||||
/*final secureStorage = ref.read(secureStorageProvider).requireValue;
|
||||
|
||||
// Delete the current [token] and [profile] from secure storage.
|
||||
secureStorage.remove('token');
|
||||
|
||||
ref
|
||||
// Invalidate the state so the auth state will be updated to unauthenticated.
|
||||
..invalidateSelf()
|
||||
// Invalidate the token provider so the API service will no longer use the
|
||||
// previous token.
|
||||
..invalidate(tokenProvider);*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The possible authentication states of the app.
|
||||
enum AuthState {
|
||||
unknown(redirectPath: homePath, allowedPaths: [homePath]),
|
||||
unauthenticated(
|
||||
redirectPath: authPath,
|
||||
allowedPaths: [authPath, settingsPath],
|
||||
),
|
||||
authenticated(redirectPath: homePath, allowedPaths: null);
|
||||
|
||||
const AuthState({required this.redirectPath, required this.allowedPaths});
|
||||
|
||||
/// The target path to redirect when the current route is not allowed in this
|
||||
/// auth state.
|
||||
final String redirectPath;
|
||||
|
||||
/// List of paths allowed when the app is in this auth state. May be set to null if there is no
|
||||
/// restriction applicable
|
||||
final List<String>? allowedPaths;
|
||||
}
|
107
moneymgr_mobile/lib/services/router/router.dart
Normal file
107
moneymgr_mobile/lib/services/router/router.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:moneymgr_mobile/routes/login/login_screens.dart';
|
||||
import 'package:moneymgr_mobile/services/auth_state.dart';
|
||||
import 'package:moneymgr_mobile/services/router/routes_list.dart';
|
||||
import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
|
||||
part 'router.g.dart';
|
||||
|
||||
/// The router config for the app.
|
||||
@riverpod
|
||||
GoRouter router(Ref ref) {
|
||||
// Local notifier for the current auth state. The purpose of this notifier is
|
||||
// to provide a [Listenable] to the [GoRouter] exposed by this provider.
|
||||
final authStateNotifier = ValueNotifier(AuthState.unknown);
|
||||
ref
|
||||
..onDispose(authStateNotifier.dispose)
|
||||
..listen(currentAuthStateProvider, (_, value) {
|
||||
authStateNotifier.value = value;
|
||||
});
|
||||
|
||||
// This is the only place you need to define your navigation items. The items
|
||||
// will be propagated automatically to the router and the navigation bar/rail
|
||||
// of the scaffold.
|
||||
//
|
||||
// To configure the authentication state needed to access a particular item,
|
||||
// see [AuthState] enum.
|
||||
final navigationItems = [
|
||||
NavigationItem(
|
||||
path: '/products',
|
||||
body: (_) => const Text("product screen"),
|
||||
icon: Icons.widgets_outlined,
|
||||
selectedIcon: Icons.widgets,
|
||||
label: 'Products',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':id',
|
||||
builder: (_, state) {
|
||||
final id = int.parse(state.pathParameters['id']!);
|
||||
return Text("product screen $id");
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
NavigationItem(
|
||||
path: '/profile',
|
||||
body: (_) => const Text("Profile"),
|
||||
icon: Icons.person_outline,
|
||||
selectedIcon: Icons.person,
|
||||
label: 'Profile',
|
||||
),
|
||||
];
|
||||
|
||||
final router = GoRouter(
|
||||
debugLogDiagnostics: true,
|
||||
initialLocation: navigationItems.first.path,
|
||||
routes: [
|
||||
GoRoute(path: homePath, builder: (_, __) => const Scaffold()),
|
||||
GoRoute(path: authPath, builder: (_, __) => const LoginScreen()),
|
||||
GoRoute(
|
||||
path: settingsPath,
|
||||
builder: (_, __) => const Text("settings screen"),
|
||||
),
|
||||
|
||||
// Configuration for the bottom navigation bar routes. The routes themselves
|
||||
// should be defined in [navigationItems]. Modification to this [ShellRoute]
|
||||
// config is rarely needed.
|
||||
ShellRoute(
|
||||
builder: (_, __, child) => child,
|
||||
routes: [
|
||||
for (final (index, item) in navigationItems.indexed)
|
||||
GoRoute(
|
||||
path: item.path,
|
||||
pageBuilder: (context, _) => NoTransitionPage(
|
||||
child: ScaffoldWithNavigation(
|
||||
selectedIndex: index,
|
||||
navigationItems: navigationItems,
|
||||
child: item.body(context),
|
||||
),
|
||||
),
|
||||
routes: item.routes,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
refreshListenable: authStateNotifier,
|
||||
redirect: (_, state) {
|
||||
// Get the current auth state.
|
||||
final authState = ref.read(currentAuthStateProvider);
|
||||
|
||||
// Check if the current path is allowed for the current auth state. If not,
|
||||
// redirect to the redirect target of the current auth state.
|
||||
if (authState.allowedPaths?.contains(state.fullPath) == false) {
|
||||
return authState.redirectPath;
|
||||
}
|
||||
|
||||
// If the current path is allowed for the current auth state, don't redirect.
|
||||
return null;
|
||||
},
|
||||
);
|
||||
ref.onDispose(router.dispose);
|
||||
|
||||
return router;
|
||||
}
|
7
moneymgr_mobile/lib/services/router/routes_list.dart
Normal file
7
moneymgr_mobile/lib/services/router/routes_list.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
const homePath = "/";
|
||||
|
||||
/// Authentication path
|
||||
const authPath = "/login";
|
||||
|
||||
/// Settings path
|
||||
const settingsPath = "/settings";
|
42
moneymgr_mobile/lib/services/storage/secure_storage.dart
Normal file
42
moneymgr_mobile/lib/services/storage/secure_storage.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'secure_storage.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<SecureStorage> secureStorage(Ref ref) =>
|
||||
SecureStorage.getInstance(keys: {'token'});
|
||||
|
||||
class SecureStorage {
|
||||
SecureStorage._(this._flutterSecureStorage, this._cache);
|
||||
|
||||
late final FlutterSecureStorage _flutterSecureStorage;
|
||||
|
||||
late final Map<String, String> _cache;
|
||||
|
||||
static Future<SecureStorage> getInstance({required Set<String> keys}) async {
|
||||
const flutterSecureStorage = FlutterSecureStorage();
|
||||
final cache = <String, String>{};
|
||||
await keys
|
||||
.map((key) => flutterSecureStorage.read(key: key).then((value) {
|
||||
if (value != null) {
|
||||
cache[key] = value;
|
||||
}
|
||||
}))
|
||||
.wait;
|
||||
return SecureStorage._(flutterSecureStorage, cache);
|
||||
}
|
||||
|
||||
String? get(String key) => _cache[key];
|
||||
|
||||
Future<void> set(String key, String value) {
|
||||
_cache[key] = value;
|
||||
return _flutterSecureStorage.write(key: key, value: value);
|
||||
}
|
||||
|
||||
Future<void> remove(String key) {
|
||||
_cache.remove(key);
|
||||
return _flutterSecureStorage.delete(key: key);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user