Add manual auth screen
This commit is contained in:
		@@ -3,5 +3,8 @@
 | 
			
		||||
         the Flutter tool needs it to communicate with the running application
 | 
			
		||||
         to allow setting breakpoints, to provide hot reload, etc.
 | 
			
		||||
    -->
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
 | 
			
		||||
    <!-- In debug mode, unsecure traffic is permitted -->
 | 
			
		||||
    <application android:usesCleartextTraffic="true" />
 | 
			
		||||
</manifest>
 | 
			
		||||
 
 | 
			
		||||
@@ -42,4 +42,7 @@
 | 
			
		||||
            <data android:mimeType="text/plain"/>
 | 
			
		||||
        </intent>
 | 
			
		||||
    </queries>
 | 
			
		||||
 | 
			
		||||
    <!-- Communication with backend -->
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET"/>
 | 
			
		||||
</manifest>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:logging/logging.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/providers/settings.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/services/router/router.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/services/storage/prefs.dart';
 | 
			
		||||
@@ -18,6 +19,14 @@ Future<void> main() async {
 | 
			
		||||
  // app is inserted to the widget tree.
 | 
			
		||||
  FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
 | 
			
		||||
 | 
			
		||||
  // Configure logger
 | 
			
		||||
  Logger.root.level = Level.ALL; // defaults to Level.INFO
 | 
			
		||||
  Logger.root.onRecord.listen(
 | 
			
		||||
    (record) =>
 | 
			
		||||
        // ignore: avoid_print
 | 
			
		||||
        print('${record.level.name}: ${record.time}: ${record.message}'),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  runApp(
 | 
			
		||||
    ProviderScope(
 | 
			
		||||
      observers: [AppProviderObserver()],
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,15 @@ import '../../services/router/routes_list.dart';
 | 
			
		||||
 | 
			
		||||
class BaseAuthPage extends StatelessWidget {
 | 
			
		||||
  final List<Widget> children;
 | 
			
		||||
  final String? title;
 | 
			
		||||
  final bool? showSettings;
 | 
			
		||||
 | 
			
		||||
  const BaseAuthPage({super.key, required this.children});
 | 
			
		||||
  const BaseAuthPage({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.children,
 | 
			
		||||
    this.title,
 | 
			
		||||
    this.showSettings,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
@@ -16,12 +23,15 @@ class BaseAuthPage extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text('MoneyMgr'),
 | 
			
		||||
        title: Text(title ?? 'MoneyMgr'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
          // Settings button
 | 
			
		||||
          showSettings != false
 | 
			
		||||
              ? IconButton(
 | 
			
		||||
                  onPressed: onSettingsPressed,
 | 
			
		||||
                  icon: const Icon(Icons.settings),
 | 
			
		||||
          ),
 | 
			
		||||
                )
 | 
			
		||||
              : Container(),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      body: SeparatedColumn(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
 | 
			
		||||
part 'login_model.freezed.dart';
 | 
			
		||||
part 'login_model.g.dart';
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
abstract class LoginModel with _$LoginModel {
 | 
			
		||||
  const factory LoginModel({
 | 
			
		||||
    // TODO : replace
 | 
			
		||||
    required String username,
 | 
			
		||||
    required String password,
 | 
			
		||||
  }) = _LoginModel;
 | 
			
		||||
 | 
			
		||||
  factory LoginModel.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$LoginModelFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
@@ -36,7 +36,6 @@ class LoginScreen extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
class _LoginChoice extends StatelessWidget {
 | 
			
		||||
  const _LoginChoice({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.route,
 | 
			
		||||
    required this.label,
 | 
			
		||||
    required this.icon,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										85
									
								
								moneymgr_mobile/lib/routes/login/manual_auth_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								moneymgr_mobile/lib/routes/login/manual_auth_screen.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_gutter/flutter_gutter.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:logging/logging.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/routes/login/base_auth_page.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/services/api/api_token.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/services/auth_state.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/utils/extensions.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/widgets/app_button.dart';
 | 
			
		||||
 | 
			
		||||
class ManualAuthScreen extends HookConsumerWidget {
 | 
			
		||||
  const ManualAuthScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var apiUrlController = useTextEditingController();
 | 
			
		||||
    var tokenIdController = useTextEditingController();
 | 
			
		||||
    var tokenValueController = useTextEditingController();
 | 
			
		||||
 | 
			
		||||
    Future<void> onSubmit() async {
 | 
			
		||||
      try {
 | 
			
		||||
        await ref
 | 
			
		||||
            .read(currentAuthStateProvider.notifier)
 | 
			
		||||
            .setAuthToken(
 | 
			
		||||
              ApiToken(
 | 
			
		||||
                apiUrl: apiUrlController.text,
 | 
			
		||||
                tokenId: int.tryParse(tokenIdController.text) ?? 1,
 | 
			
		||||
                tokenValue: tokenValueController.text,
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
      } catch (e, s) {
 | 
			
		||||
        Logger.root.severe("Failed to authenticate user! $e $s");
 | 
			
		||||
        if (context.mounted) {
 | 
			
		||||
          context.showTextSnackBar("Failed to authenticate user! $e");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return BaseAuthPage(
 | 
			
		||||
      title: "Manual authentication",
 | 
			
		||||
      showSettings: false,
 | 
			
		||||
      children: [
 | 
			
		||||
        Gutter(scaleFactor: 3),
 | 
			
		||||
        Text(
 | 
			
		||||
          "On this screen you can manually enter authentication information.",
 | 
			
		||||
        ),
 | 
			
		||||
        Gutter(),
 | 
			
		||||
        TextField(
 | 
			
		||||
          controller: apiUrlController,
 | 
			
		||||
          keyboardType: TextInputType.url,
 | 
			
		||||
          decoration: const InputDecoration(
 | 
			
		||||
            labelText: 'API URL',
 | 
			
		||||
            helperText:
 | 
			
		||||
                "This URL has usually this format: http://moneymgr.corp.com/api",
 | 
			
		||||
          ),
 | 
			
		||||
          textInputAction: TextInputAction.next,
 | 
			
		||||
        ),
 | 
			
		||||
        Gutter(),
 | 
			
		||||
        TextField(
 | 
			
		||||
          controller: tokenIdController,
 | 
			
		||||
          keyboardType: TextInputType.number,
 | 
			
		||||
          decoration: const InputDecoration(
 | 
			
		||||
            labelText: 'Token ID',
 | 
			
		||||
            helperText: "The ID of the token",
 | 
			
		||||
          ),
 | 
			
		||||
          textInputAction: TextInputAction.next,
 | 
			
		||||
        ),
 | 
			
		||||
        Gutter(),
 | 
			
		||||
        TextField(
 | 
			
		||||
          controller: tokenIdController,
 | 
			
		||||
          keyboardType: TextInputType.text,
 | 
			
		||||
          decoration: const InputDecoration(
 | 
			
		||||
            labelText: 'Token value',
 | 
			
		||||
            helperText: "The value of the token itself",
 | 
			
		||||
          ),
 | 
			
		||||
          textInputAction: TextInputAction.done,
 | 
			
		||||
        ),
 | 
			
		||||
        Gutter(),
 | 
			
		||||
        AppButton(onPressed: onSubmit, label: "Submit"),
 | 
			
		||||
        Gutter(scaleFactor: 3),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								moneymgr_mobile/lib/services/api/account_api.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								moneymgr_mobile/lib/services/api/account_api.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
/// Account API
 | 
			
		||||
class AccountAPI {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								moneymgr_mobile/lib/services/api/api_client.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								moneymgr_mobile/lib/services/api/api_client.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
 | 
			
		||||
/// Client API
 | 
			
		||||
@riverpod
 | 
			
		||||
class ClientAPI {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								moneymgr_mobile/lib/services/api/api_token.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								moneymgr_mobile/lib/services/api/api_token.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
 | 
			
		||||
part 'api_token.freezed.dart';
 | 
			
		||||
part 'api_token.g.dart';
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
abstract class ApiToken with _$ApiToken {
 | 
			
		||||
  const factory ApiToken({
 | 
			
		||||
    required String apiUrl,
 | 
			
		||||
    required int tokenId,
 | 
			
		||||
    required String tokenValue
 | 
			
		||||
  }) = _ApiToken;
 | 
			
		||||
 | 
			
		||||
  factory ApiToken.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$ApiTokenFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import 'package:moneymgr_mobile/routes/login/login_model.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/services/api/api_token.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';
 | 
			
		||||
@@ -8,7 +8,7 @@ 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.
 | 
			
		||||
/// to the storage through the [setAuthToken] and [logout] methods.
 | 
			
		||||
@riverpod
 | 
			
		||||
class CurrentAuthState extends _$CurrentAuthState {
 | 
			
		||||
  @override
 | 
			
		||||
@@ -18,9 +18,9 @@ class CurrentAuthState extends _$CurrentAuthState {
 | 
			
		||||
    return token != null ? AuthState.authenticated : AuthState.unauthenticated;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Attempts to log in with [data] and saves the token and profile info to storage.
 | 
			
		||||
  /// Attempts to authenticate with [data] and saves the token and profile info to storage.
 | 
			
		||||
  /// Will invalidate the state if success.
 | 
			
		||||
  Future<void> login(LoginModel data) async {
 | 
			
		||||
  Future<void> setAuthToken(ApiToken data) async {
 | 
			
		||||
    // TODO : perform login
 | 
			
		||||
    /*final secureStorage = ref.read(secureStorageProvider).requireValue;
 | 
			
		||||
    final token = await ref.read(apiServiceProvider).login(data);
 | 
			
		||||
@@ -53,13 +53,12 @@ class CurrentAuthState extends _$CurrentAuthState {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// The possible authentication states of the app.
 | 
			
		||||
enum AuthState {
 | 
			
		||||
  unknown(redirectPath: homePage, allowedPaths: [homePage]),
 | 
			
		||||
  unauthenticated(
 | 
			
		||||
    redirectPath: authPage,
 | 
			
		||||
    allowedPaths: [authPage, settingsPage],
 | 
			
		||||
    allowedPaths: [authPage, manualAuthPage, settingsPage],
 | 
			
		||||
  ),
 | 
			
		||||
  authenticated(redirectPath: homePage, allowedPaths: null);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,12 @@ 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_screen.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/routes/settings/settings_screen.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.
 | 
			
		||||
@@ -62,9 +61,10 @@ GoRouter router(Ref ref) {
 | 
			
		||||
      GoRoute(path: homePage, builder: (_, __) => const Scaffold()),
 | 
			
		||||
      GoRoute(path: authPage, builder: (_, __) => const LoginScreen()),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: settingsPage,
 | 
			
		||||
        builder: (_, __) => const SettingsScreen(),
 | 
			
		||||
        path: manualAuthPage,
 | 
			
		||||
        builder: (_, __) => const ManualAuthScreen(),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(path: settingsPage, builder: (_, __) => const SettingsScreen()),
 | 
			
		||||
 | 
			
		||||
      // Configuration for the bottom navigation bar routes. The routes themselves
 | 
			
		||||
      // should be defined in [navigationItems]. Modification to this [ShellRoute]
 | 
			
		||||
 
 | 
			
		||||
@@ -6,25 +6,23 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/utils/extensions.dart';
 | 
			
		||||
import 'package:moneymgr_mobile/utils/hooks.dart';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// A button that shows a circular progress indicator when the [onPressed] callback
 | 
			
		||||
/// is pending.
 | 
			
		||||
class AppButton extends HookWidget {
 | 
			
		||||
  const AppButton({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.onPressed,
 | 
			
		||||
    required this.label,
 | 
			
		||||
  });
 | 
			
		||||
  const AppButton({super.key, required this.onPressed, required this.label});
 | 
			
		||||
 | 
			
		||||
  final AsyncCallback? onPressed;
 | 
			
		||||
  final String label;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final (:pending, :snapshot, hasError: _) = useAsyncTask();
 | 
			
		||||
    final (:pending, :snapshot, :hasError) = useAsyncTask();
 | 
			
		||||
 | 
			
		||||
    return FilledButton(
 | 
			
		||||
      onPressed: onPressed == null ? null : () => pending.value = onPressed!(),
 | 
			
		||||
      style: ButtonStyle(
 | 
			
		||||
        backgroundColor: hasError ? WidgetStatePropertyAll(Colors.red) : null,
 | 
			
		||||
      ),
 | 
			
		||||
      child: SeparatedRow(
 | 
			
		||||
        separatorBuilder: () => const GutterSmall(),
 | 
			
		||||
        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
 
 | 
			
		||||
@@ -537,7 +537,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "5.1.1"
 | 
			
		||||
  logging:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: logging
 | 
			
		||||
      sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
 | 
			
		||||
 
 | 
			
		||||
@@ -61,8 +61,12 @@ dependencies:
 | 
			
		||||
  # Help in models building
 | 
			
		||||
  freezed_annotation: ^3.0.0
 | 
			
		||||
 | 
			
		||||
  # For JSON serialization
 | 
			
		||||
  json_annotation: ^4.9.0
 | 
			
		||||
 | 
			
		||||
  # Logger
 | 
			
		||||
  logging: ^1.3.0
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
    sdk: flutter
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user