Initial sign in/sign out workflow with proper state routing

codemagic-setup
Hank Grabowski 2022-11-09 21:02:26 -05:00
rodzic 5d9986121f
commit f647b68881
16 zmienionych plików z 330 dodań i 60 usunięć

Wyświetl plik

@ -1,11 +1,11 @@
import 'dart:convert';
import 'dart:io';
import 'package:result_monad/result_monad.dart';
import 'models/credentials.dart';
import 'models/exec_error.dart';
import 'models/timeline_entry.dart';
import 'package:result_monad/result_monad.dart';
import 'serializers/mastodon/timeline_entry_mastodon_extensions.dart';
class FriendicaClient {
@ -14,6 +14,8 @@ class FriendicaClient {
String get serverName => _credentials.serverName;
Credentials get credentials => _credentials;
FriendicaClient({required Credentials credentials})
: _credentials = credentials {
final authenticationString =

Wyświetl plik

@ -1,12 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_portal/globals.dart';
import 'package:flutter_portal/routes.dart';
import 'package:flutter_portal/screens/sign_in.dart';
import 'package:flutter_portal/services/auth_service.dart';
import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
void main() {
getIt.registerLazySingleton<AuthService>(() => AuthService());
import 'globals.dart';
import 'routes.dart';
import 'screens/sign_in.dart';
import 'services/auth_service.dart';
import 'services/secrets_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final authService = AuthService();
final secretsService = SecretsService();
getIt.registerSingleton<SecretsService>(secretsService);
getIt.registerSingleton<AuthService>(authService);
await secretsService.initialize().andThenSuccessAsync((credentials) async {
if (credentials.isEmpty) {
return;
}
final wasLoggedIn = await authService.getStoredLoginState();
if (wasLoggedIn) {
final result = await authService.signIn(credentials);
print('Startup login result: $result');
} else {
print('Was not logged in');
}
});
runApp(const App());
}
@ -17,22 +38,6 @@ class App extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
// return MaterialApp(
// title: 'Flutter Demo',
// theme: ThemeData(
// // This is the theme of your application.
// //
// // Try running your application with "flutter run". You'll see the
// // application has a blue toolbar. Then, without quitting the app, try
// // changing the primarySwatch below to Colors.green and then invoke
// // "hot reload" (press "r" in the console where you ran "flutter run",
// // or simply save your changes to "hot reload" in a Flutter IDE).
// // Notice that the counter didn't reset back to zero; the application
// // is not restarted.
// primarySwatch: Colors.blue,
// ),
// home: const Home(),
// );
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthService>(

Wyświetl plik

@ -11,6 +11,17 @@ class Credentials {
required this.password,
required this.serverName});
factory Credentials.empty() => Credentials(
username: '',
password: '',
serverName: '',
);
bool get isEmpty =>
username.isEmpty && password.isEmpty && serverName.isEmpty;
String get handle => '$username@$serverName';
Credentials copy({String? username, String? password, String? serverName}) {
return Credentials(
username: username ?? this.username,
@ -31,7 +42,7 @@ class Credentials {
password: password,
serverName: elements[1],
);
print(result);
return Result.ok(result);
}

Wyświetl plik

@ -1,7 +1,10 @@
import 'package:go_router/go_router.dart';
import 'globals.dart';
import 'screens/home.dart';
import 'screens/sign_in.dart';
import 'screens/splash.dart';
import 'services/auth_service.dart';
class ScreenPaths {
static String splash = '/splash';
@ -12,9 +15,33 @@ class ScreenPaths {
}
bool needAuthChangeInitialized = true;
final _authService = getIt<AuthService>();
final allowedLoggedOut = [
ScreenPaths.splash,
ScreenPaths.signin,
ScreenPaths.signup
];
final appRouter = GoRouter(
initialLocation: ScreenPaths.signin,
initialLocation: ScreenPaths.home,
debugLogDiagnostics: true,
refreshListenable: _authService,
redirect: (context, state) async {
print('redirect handler');
final loggedIn = _authService.loggedIn;
print('$loggedIn ${state.location}');
if (!loggedIn && !allowedLoggedOut.contains(state.location)) {
print('Redirecting to sign in');
return ScreenPaths.signin;
}
if (loggedIn && allowedLoggedOut.contains(state.location)) {
print('Redirecting to home');
return ScreenPaths.home;
}
return null;
},
routes: [
GoRoute(
path: ScreenPaths.signin,
@ -26,4 +53,9 @@ final appRouter = GoRouter(
name: ScreenPaths.home,
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: ScreenPaths.splash,
name: ScreenPaths.splash,
builder: (context, state) => SplashScreen(),
),
]);

Wyświetl plik

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
import '../controls/padding.dart';
@ -14,7 +15,7 @@ class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final clientResult = getIt<AuthService>().currentClient;
final clientResult = context.read<AuthService>().currentClient;
final body = clientResult.fold(onSuccess: (client) {
return Column(
children: [
@ -50,6 +51,14 @@ class HomeScreen extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
actions: [
IconButton(
onPressed: () {
getIt<AuthService>().signOut();
},
icon: Icon(Icons.logout),
)
],
),
body: body,
);

Wyświetl plik

@ -1,21 +1,36 @@
import 'package:email_validator/email_validator.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:result_monad/result_monad.dart';
import '../controls/padding.dart';
import '../friendica_client.dart';
import '../globals.dart';
import '../models/credentials.dart';
import '../routes.dart';
import '../services/auth_service.dart';
import '../services/secrets_service.dart';
import '../utils/snackbar_builder.dart';
class SignInScreen extends StatelessWidget {
class SignInScreen extends StatefulWidget {
@override
State<SignInScreen> createState() => _SignInScreenState();
}
class _SignInScreenState extends State<SignInScreen> {
final formKey = GlobalKey<FormState>();
final usernameController = TextEditingController();
final passwordController = TextEditingController();
@override
void initState() {
super.initState();
getIt<SecretsService>().credentials.andThenSuccess((credentials) {
if (credentials.isEmpty) {
return;
}
usernameController.text = credentials.handle;
passwordController.text = credentials.password;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -81,20 +96,16 @@ class SignInScreen extends StatelessWidget {
void _signIn(BuildContext context) async {
if (formKey.currentState?.validate() ?? false) {
print('Attempting login...');
await Credentials.buildFromHandle(
final result = await Credentials.buildFromHandle(
usernameController.text,
passwordController.text,
)
.andThenSuccess((creds) => FriendicaClient(credentials: creds))
.andThenAsync((client) async =>
(await client.getMyProfile()).mapValue((_) => client))
.match(onSuccess: (client) {
print('Logged in');
getIt<AuthService>().updateClient(client);
context.pushNamed(ScreenPaths.home);
}, onError: (error) {
buildSnackbar(context, 'Error logging in: $error');
).andThenSuccess((creds) async {
return await getIt<AuthService>().signIn(creds);
});
if (result.isFailure) {
buildSnackbar(context, 'Error signing in: ${result.error}');
}
}
}
}

Wyświetl plik

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class SplashScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('Friendica Portal'),
],
)),
);
}
}

Wyświetl plik

@ -1,11 +1,16 @@
import 'package:flutter/foundation.dart';
import 'package:result_monad/result_monad.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/exec_error.dart';
import '../friendica_client.dart';
import '../globals.dart';
import '../models/credentials.dart';
import '../models/exec_error.dart';
import 'secrets_service.dart';
class AuthService extends ChangeNotifier {
FriendicaClient? _friendicaClient;
bool _loggedIn = false;
Result<FriendicaClient, ExecError> get currentClient {
if (_friendicaClient == null) {
@ -18,14 +23,44 @@ class AuthService extends ChangeNotifier {
return Result.ok(_friendicaClient!);
}
Result<FriendicaClient, ExecError> updateClient(FriendicaClient newClient) {
_friendicaClient = newClient;
notifyListeners();
return Result.ok(newClient);
bool get loggedIn => _loggedIn && _friendicaClient != null;
Future<bool> getStoredLoginState() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('logged-in') ?? false;
}
void clearCredentials() {
FutureResult<FriendicaClient, ExecError> signIn(
Credentials credentials) async {
final client = FriendicaClient(credentials: credentials);
final result = await client.getMyProfile();
if (result.isFailure) {
return result.errorCast();
}
getIt<SecretsService>().storeCredentials(client.credentials);
await _setLoginState(true);
_friendicaClient = client;
notifyListeners();
return Result.ok(client);
}
Future signOut() async {
print('Sign out');
await _setLoginState(false);
_friendicaClient = null;
notifyListeners();
}
Future clearCredentials() async {
_friendicaClient = null;
await _setLoginState(false);
notifyListeners();
}
Future<void> _setLoginState(bool state) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('logged-in', state);
_loggedIn = state;
}
}

Wyświetl plik

@ -0,0 +1,83 @@
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:result_monad/result_monad.dart';
import '../models/credentials.dart';
import '../models/exec_error.dart';
class SecretsService {
static const _usernameKey = 'username';
static const _passwordKey = 'password';
static const _serverNameKey = 'server-name';
Credentials? _cachedCredentials;
Result<Credentials, ExecError> get credentials => _cachedCredentials != null
? Result.ok(_cachedCredentials!)
: Result.error(
ExecError(
type: ErrorType.localError,
message: 'Credentials not initialized',
),
);
final _secureStorage = const FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock,
),
);
FutureResult<Credentials, ExecError> initialize() async {
return await getCredentials();
}
FutureResult<Credentials, ExecError> clearCredentials() async {
try {
await _secureStorage.delete(key: _usernameKey);
await _secureStorage.read(key: _passwordKey);
await _secureStorage.read(key: _serverNameKey);
return Result.ok(Credentials.empty());
} on PlatformException catch (e) {
return Result.error(ExecError(
type: ErrorType.localError,
message: e.message ?? '',
));
}
}
Result<Credentials, ExecError> storeCredentials(Credentials credentials) {
try {
_secureStorage.write(key: _usernameKey, value: credentials.username);
_secureStorage.write(key: _passwordKey, value: credentials.password);
_secureStorage.write(key: _serverNameKey, value: credentials.serverName);
return Result.ok(credentials);
} on PlatformException catch (e) {
return Result.error(ExecError(
type: ErrorType.localError,
message: e.message ?? '',
));
}
}
FutureResult<Credentials, ExecError> getCredentials() async {
try {
final username = await _secureStorage.read(key: _usernameKey);
final password = await _secureStorage.read(key: _passwordKey);
final serverName = await _secureStorage.read(key: _serverNameKey);
if (username == null || password == null || serverName == null) {
return Result.ok(Credentials.empty());
}
_cachedCredentials = Credentials(
username: username,
password: password,
serverName: serverName,
);
return Result.ok(_cachedCredentials!);
} on PlatformException catch (e) {
return Result.error(ExecError(
type: ErrorType.localError,
message: e.message ?? '',
));
}
}
}

Wyświetl plik

@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h"
#include <desktop_window/desktop_window_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWindowPlugin");
desktop_window_plugin_register_with_registrar(desktop_window_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
}

Wyświetl plik

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_window
flutter_secure_storage_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

Wyświetl plik

@ -6,12 +6,14 @@ import FlutterMacOS
import Foundation
import desktop_window
import flutter_secure_storage_macos
import path_provider_macos
import shared_preferences_macos
import sqflite
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin"))
FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

Wyświetl plik

@ -146,6 +146,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
flutter_test:
dependency: "direct dev"
description: flutter

Wyświetl plik

@ -10,22 +10,23 @@ environment:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
provider: ^6.0.4
email_validator: ^2.1.17
get_it: ^7.2.0
go_router: ^5.1.3
uuid: ^3.0.6
logging: ^1.1.0
get_it_mixin: ^3.1.4
cached_network_image: ^3.2.2
result_monad: ^2.0.2
cupertino_icons: ^1.0.2
desktop_window: ^0.4.0
email_validator: ^2.1.17
flutter_secure_storage: ^6.0.0
flutter_widget_from_html_core: ^0.9.0
get_it: ^7.2.0
get_it_mixin: ^3.1.4
go_router: ^5.1.3
logging: ^1.1.0
markdown: ^6.0.1
metadata_fetch: ^0.4.1
shared_preferences: ^2.0.15
network_to_file_image: ^4.0.1
desktop_window: ^0.4.0
provider: ^6.0.4
result_monad: ^2.0.2
shared_preferences: ^2.0.15
uuid: ^3.0.6
time_machine: ^0.9.17
dev_dependencies:
@ -36,6 +37,17 @@ dev_dependencies:
flutter:
uses-material-design: true
parts:
uet-lms:
source: .
plugin: flutter
flutter-target: lib/main.dart
build-packages:
- libsecret-1-dev
- libjsoncpp-dev
stage-packages:
- libsecret-1-dev
- libjsoncpp1-dev
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg

Wyświetl plik

@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include <desktop_window/desktop_window_plugin.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DesktopWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopWindowPlugin"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
}

Wyświetl plik

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_window
flutter_secure_storage_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST