Merge branch 'signin-refactoring' into 'main'

Refactor bootstrapping process to have more statusing, timeouts and go to sign...

See merge request mysocialportal/relatica!47
This commit is contained in:
HankG 2023-11-17 18:34:31 +00:00
commit 2419cfa164
12 changed files with 223 additions and 113 deletions

View file

@ -32,6 +32,7 @@ import 'services/persistent_info_service.dart';
import 'services/reshared_via_service.dart'; import 'services/reshared_via_service.dart';
import 'services/secrets_service.dart'; import 'services/secrets_service.dart';
import 'services/setting_service.dart'; import 'services/setting_service.dart';
import 'services/status_service.dart';
import 'services/timeline_entry_filter_service.dart'; import 'services/timeline_entry_filter_service.dart';
import 'services/timeline_manager.dart'; import 'services/timeline_manager.dart';
import 'update_timer_initialization.dart'; import 'update_timer_initialization.dart';
@ -40,6 +41,8 @@ import 'utils/active_profile_selector.dart';
final _logger = Logger('DI_Init'); final _logger = Logger('DI_Init');
Future<void> dependencyInjectionInitialization() async { Future<void> dependencyInjectionInitialization() async {
getIt.registerSingleton<StatusService>(StatusService());
final appSupportdir = await getApplicationSupportDirectory(); final appSupportdir = await getApplicationSupportDirectory();
getIt.registerSingleton<ActiveProfileSelector<PersistentInfoService>>( getIt.registerSingleton<ActiveProfileSelector<PersistentInfoService>>(
ActiveProfileSelector( ActiveProfileSelector(

View file

@ -781,7 +781,7 @@ class ProfileClient extends FriendicaClient {
_logger.finest(() => 'Getting logged in user profile'); _logger.finest(() => 'Getting logged in user profile');
final request = final request =
Uri.parse('https://$serverName/api/v1/accounts/verify_credentials'); Uri.parse('https://$serverName/api/v1/accounts/verify_credentials');
return (await _getApiRequest(request)) return (await _getApiRequest(request, timeout: oauthTimeout))
.mapValue((json) => ConnectionMastodonExtensions.fromJson( .mapValue((json) => ConnectionMastodonExtensions.fromJson(
json, json,
defaultServerName: serverName, defaultServerName: serverName,
@ -1083,25 +1083,40 @@ abstract class FriendicaClient {
} }
FutureResult<PagedResponse<List<dynamic>>, ExecError> _getApiListRequest( FutureResult<PagedResponse<List<dynamic>>, ExecError> _getApiListRequest(
Uri url) async { Uri url, {
return await getUrl(url, headers: _headers).transformAsync( Duration? timeout,
}) async {
return await getUrl(
url,
headers: _headers,
timeout: timeout,
).transformAsync(
(response) async { (response) async {
return response.map((data) => jsonDecode(data) as List<dynamic>); return response.map((data) => jsonDecode(data) as List<dynamic>);
}, },
).execErrorCastAsync(); ).execErrorCastAsync();
} }
FutureResult<PagedResponse<dynamic>, ExecError> _getApiPagedRequest( FutureResult<PagedResponse<dynamic>, ExecError> _getApiPagedRequest(Uri url,
Uri url) async { {Duration? timeout}) async {
return await getUrl(url, headers: _headers).transformAsync( return await getUrl(
url,
headers: _headers,
timeout: timeout,
).transformAsync(
(response) async { (response) async {
return response.map((data) => jsonDecode(data)); return response.map((data) => jsonDecode(data));
}, },
).execErrorCastAsync(); ).execErrorCastAsync();
} }
FutureResult<dynamic, ExecError> _getApiRequest(Uri url) async { FutureResult<dynamic, ExecError> _getApiRequest(Uri url,
return await getUrl(url, headers: _headers).transformAsync( {Duration? timeout}) async {
return await getUrl(
url,
headers: _headers,
timeout: timeout,
).transformAsync(
(response) async { (response) async {
return jsonDecode(response.data); return jsonDecode(response.data);
}, },
@ -1111,6 +1126,5 @@ abstract class FriendicaClient {
Map<String, String> get _headers => { Map<String, String> get _headers => {
'Authorization': _profile.credentials.authHeaderValue, 'Authorization': _profile.credentials.authHeaderValue,
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
if (usePhpDebugging) 'Cookie': 'XDEBUG_SESSION=PHPSTORM;path=/',
}; };
} }

View file

@ -15,7 +15,7 @@ final platformHasCamera = Platform.isIOS || Platform.isAndroid;
final useImagePicker = kIsWeb || Platform.isAndroid || Platform.isIOS; final useImagePicker = kIsWeb || Platform.isAndroid || Platform.isIOS;
const usePhpDebugging = true; const usePhpDebugging = false;
const maxViewPortalHeight = 750.0; const maxViewPortalHeight = 750.0;
const maxViewPortalWidth = 750.0; const maxViewPortalWidth = 750.0;
@ -23,6 +23,7 @@ const maxViewPortalWidth = 750.0;
const maxProcessingMillis = 3; const maxProcessingMillis = 3;
const processingSleep = Duration(milliseconds: 1); const processingSleep = Duration(milliseconds: 1);
const apiCallTimeout = Duration(seconds: 60); const apiCallTimeout = Duration(seconds: 60);
const oauthTimeout = Duration(seconds: 15);
Future<bool?> showConfirmDialog(BuildContext context, String caption) { Future<bool?> showConfirmDialog(BuildContext context, String caption) {
return showDialog<bool>( return showDialog<bool>(

View file

@ -21,6 +21,7 @@ import 'services/hashtag_service.dart';
import 'services/interactions_manager.dart'; import 'services/interactions_manager.dart';
import 'services/notifications_manager.dart'; import 'services/notifications_manager.dart';
import 'services/setting_service.dart'; import 'services/setting_service.dart';
import 'services/status_service.dart';
import 'services/timeline_entry_filter_service.dart'; import 'services/timeline_entry_filter_service.dart';
import 'services/timeline_manager.dart'; import 'services/timeline_manager.dart';
import 'utils/active_profile_selector.dart'; import 'utils/active_profile_selector.dart';
@ -62,6 +63,10 @@ class App extends StatelessWidget {
return Portal( return Portal(
child: MultiProvider( child: MultiProvider(
providers: [ providers: [
ChangeNotifierProvider<StatusService>(
create: (_) => getIt<StatusService>(),
lazy: true,
),
ChangeNotifierProvider<SettingsService>( ChangeNotifierProvider<SettingsService>(
create: (_) => getIt<SettingsService>(), create: (_) => getIt<SettingsService>(),
lazy: true, lazy: true,

View file

@ -2,11 +2,13 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart'; import 'package:result_monad/result_monad.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../../globals.dart';
import '../../services/status_service.dart';
import '../../utils/network_utils.dart';
import '../exec_error.dart'; import '../exec_error.dart';
import 'credentials_intf.dart'; import 'credentials_intf.dart';
@ -91,30 +93,36 @@ class OAuthCredentials implements ICredentials {
'Client ID and Client Secret are already set, skipping ID fetching'); 'Client ID and Client Secret are already set, skipping ID fetching');
return Result.ok(true); return Result.ok(true);
} }
getIt<StatusService>().setStatus('Attempting to contact $serverName...');
final idEndpoint = Uri.parse('https://$serverName/api/v1/apps'); final idEndpoint = Uri.parse('https://$serverName/api/v1/apps');
final response = await http.post(idEndpoint, body: { final response = await postUrl(
'client_name': 'Relatica', idEndpoint,
'redirect_uris': redirectUrl, {
'scopes': 'read write follow push', 'client_name': 'Relatica',
'website': 'https://myportal.social', 'redirect_uris': redirectUrl,
'scopes': 'read write follow push',
'website': 'https://myportal.social',
},
timeout: oauthTimeout,
);
response.match(onSuccess: (body) {
getIt<StatusService>().setStatus('Connected to $serverName...');
final json = jsonDecode(body);
clientId = json['client_id'];
clientSecret = json['client_secret'];
}, onError: (error) {
getIt<StatusService>()
.setStatus('Error contacting $serverName...: $error');
_logger.severe('Error contacting $serverName...: $error');
}); });
if (response.statusCode != 200) { return response.mapValue((_) => true);
_logger.severe('Error: ${response.statusCode}: ${response.body}');
return buildErrorResult(
type: ErrorType.serverError,
message: 'Error: ${response.statusCode}: ${response.body}',
);
}
final json = jsonDecode(response.body);
clientId = json['client_id'];
clientSecret = json['client_secret'];
return Result.ok(true);
} }
FutureResult<bool, ExecError> _login() async { FutureResult<bool, ExecError> _login() async {
if (accessToken.isNotEmpty) { if (accessToken.isNotEmpty) {
getIt<StatusService>().setStatus('Logged into $serverName');
_logger.info('Already have access token, skipping'); _logger.info('Already have access token, skipping');
return Result.ok(true); return Result.ok(true);
} }
@ -127,12 +135,20 @@ class OAuthCredentials implements ICredentials {
}); });
try { try {
getIt<StatusService>()
.setStatus('Attempting getting authorization to $serverName');
final result = await FlutterWebAuth2.authenticate( final result = await FlutterWebAuth2.authenticate(
url: url.toString(), callbackUrlScheme: redirectScheme); url: url.toString(),
callbackUrlScheme: redirectScheme,
);
final code = Uri.parse(result).queryParameters['code']; final code = Uri.parse(result).queryParameters['code'];
if (code == null) { if (code == null) {
_logger.severe( _logger.severe(
'Error code was not returned with the query parameters: $result'); 'Error code was not returned with the query parameters: $result');
getIt<StatusService>().setStatus(
'Error getting the response code during authentication to $serverName');
return buildErrorResult( return buildErrorResult(
type: ErrorType.serverError, type: ErrorType.serverError,
message: 'Error getting the response code during authentication', message: 'Error getting the response code during authentication',
@ -146,16 +162,18 @@ class OAuthCredentials implements ICredentials {
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': code, 'code': code,
}; };
final response = await http.post(url2, body: body); final response = await postUrl(url2, body);
if (response.statusCode != 200) { response.match(onSuccess: (body) {
_logger.severe('Error: ${response.statusCode}: ${response.body}'); getIt<StatusService>().setStatus('Logged into $serverName');
return buildErrorResult( accessToken = jsonDecode(body)['access_token'];
type: ErrorType.serverError, }, onError: (error) {
message: 'Error: ${response.statusCode}: ${response.body}', getIt<StatusService>()
); .setStatus('Error getting authorization to $serverName');
} _logger.severe('Error doing OAUth processing: $error');
accessToken = jsonDecode(response.body)['access_token']; });
} catch (e) { } catch (e) {
getIt<StatusService>()
.setStatus('Error getting authorization to $serverName');
_logger.severe('Exception while Doing OAuth Process: $e'); _logger.severe('Exception while Doing OAuth Process: $e');
return buildErrorResult( return buildErrorResult(
type: ErrorType.serverError, type: ErrorType.serverError,

View file

@ -23,7 +23,7 @@ class SignInScreen extends StatefulWidget {
class _SignInScreenState extends State<SignInScreen> { class _SignInScreenState extends State<SignInScreen> {
static final _logger = Logger('$SignInScreen'); static final _logger = Logger('$SignInScreen');
static const usernamePasswordType = 'Username/Password'; static const usernamePasswordType = 'Username/Password';
static const oauthType = 'OAuth'; static const oauthType = 'Standard Login';
static final authTypes = [usernamePasswordType, oauthType]; static final authTypes = [usernamePasswordType, oauthType];
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final usernameController = TextEditingController(); final usernameController = TextEditingController();
@ -31,9 +31,12 @@ class _SignInScreenState extends State<SignInScreen> {
final passwordController = TextEditingController(); final passwordController = TextEditingController();
var authType = oauthType; var authType = oauthType;
var hidePassword = true; var hidePassword = true;
var showUsernameAndPasswordFields = false;
var signInButtonEnabled = false; var signInButtonEnabled = false;
var existingAccount = false; Profile? existingProfile;
bool get showUsernameAndPasswordFields => authType == usernamePasswordType;
bool get existingAccount => existingProfile != null;
@override @override
void initState() { void initState() {
@ -47,32 +50,29 @@ class _SignInScreenState extends State<SignInScreen> {
} }
void newProfile() { void newProfile() {
usernameController.text = ''; usernameController.clear();
passwordController.text = ''; passwordController.clear();
serverNameController.text = ''; serverNameController.clear();
showUsernameAndPasswordFields = false;
authType = oauthType; authType = oauthType;
signInButtonEnabled = true; signInButtonEnabled = true;
existingAccount = false; existingProfile = null;
} }
void setBasicCredentials(BasicCredentials credentials) { void setBasicCredentials(BasicCredentials credentials) {
usernameController.text = credentials.username; usernameController.text = credentials.username;
passwordController.text = credentials.password; passwordController.text = credentials.password;
serverNameController.text = credentials.serverName; serverNameController.text = credentials.serverName;
showUsernameAndPasswordFields = true;
authType = usernamePasswordType; authType = usernamePasswordType;
} }
void setOauthCredentials(OAuthCredentials credentials) { void setOauthCredentials(OAuthCredentials credentials) {
serverNameController.text = credentials.serverName; serverNameController.text = credentials.serverName;
showUsernameAndPasswordFields = false;
authType = oauthType; authType = oauthType;
} }
void setCredentials(BuildContext? context, Profile profile) { void setCredentials(BuildContext? context, Profile profile) {
final ICredentials credentials = profile.credentials; final ICredentials credentials = profile.credentials;
existingAccount = true; existingProfile = profile;
signInButtonEnabled = !profile.loggedIn; signInButtonEnabled = !profile.loggedIn;
if (credentials is BasicCredentials) { if (credentials is BasicCredentials) {
setBasicCredentials(credentials); setBasicCredentials(credentials);
@ -99,7 +99,20 @@ class _SignInScreenState extends State<SignInScreen> {
final loggedOutProfiles = service.loggedOutProfiles; final loggedOutProfiles = service.loggedOutProfiles;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Sign In'), title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Sign In'),
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Add new account',
onPressed: () {
newProfile();
setState(() {});
},
)
],
),
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@ -115,26 +128,17 @@ class _SignInScreenState extends State<SignInScreen> {
.map( .map(
(a) => DropdownMenuItem(value: a, child: Text(a))) (a) => DropdownMenuItem(value: a, child: Text(a)))
.toList(), .toList(),
onChanged: (value) { onChanged: existingAccount
if (existingAccount) { ? null
buildSnackbar(context, : (value) {
"Can't change the type on an existing account"); if (existingAccount) {
return; buildSnackbar(context,
} "Can't change the type on an existing account");
setState(() { return;
authType = value ?? ''; }
switch (value) { authType = value!;
case usernamePasswordType: setState(() {});
showUsernameAndPasswordFields = true; }),
break;
case oauthType:
showUsernameAndPasswordFields = false;
break;
default:
print("Don't know this");
}
});
}),
), ),
const VerticalPadding(), const VerticalPadding(),
TextFormField( TextFormField(
@ -156,6 +160,15 @@ class _SignInScreenState extends State<SignInScreen> {
), ),
), ),
const VerticalPadding(), const VerticalPadding(),
if (!showUsernameAndPasswordFields) ...[
Text(
existingAccount
? 'Configured to sign in as user ${existingProfile?.handle}'
: 'Relatica will open the requested Friendica site in a web browser where you will be asked to authorize this client.',
softWrap: true,
),
const VerticalPadding(),
],
if (showUsernameAndPasswordFields) ...[ if (showUsernameAndPasswordFields) ...[
TextFormField( TextFormField(
readOnly: existingAccount, readOnly: existingAccount,
@ -194,6 +207,13 @@ class _SignInScreenState extends State<SignInScreen> {
readOnly: existingAccount, readOnly: existingAccount,
obscureText: hidePassword, obscureText: hidePassword,
controller: passwordController, controller: passwordController,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password field cannot be empty';
}
return null;
},
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.password), prefixIcon: const Icon(Icons.password),
suffixIcon: IconButton( suffixIcon: IconButton(
@ -223,14 +243,7 @@ class _SignInScreenState extends State<SignInScreen> {
onPressed: () => _signIn(context), onPressed: () => _signIn(context),
child: const Text('Signin'), child: const Text('Signin'),
) )
: ElevatedButton( : SizedBox(),
onPressed: () {
setState(() {
newProfile();
});
},
child: const Text('New'),
),
const VerticalPadding(), const VerticalPadding(),
Text( Text(
'Logged out:', 'Logged out:',
@ -256,8 +269,8 @@ class _SignInScreenState extends State<SignInScreen> {
}, },
title: Text(p.handle), title: Text(p.handle),
subtitle: Text(p.credentials is BasicCredentials subtitle: Text(p.credentials is BasicCredentials
? 'Username/Password' ? usernamePasswordType
: 'OAuth Login'), : oauthType),
trailing: ElevatedButton( trailing: ElevatedButton(
onPressed: () async { onPressed: () async {
final confirm = await showYesNoDialog(context, final confirm = await showYesNoDialog(context,
@ -265,6 +278,7 @@ class _SignInScreenState extends State<SignInScreen> {
if (confirm ?? false) { if (confirm ?? false) {
await service.removeProfile(p); await service.removeProfile(p);
} }
setState(() {});
}, },
child: const Text('Remove'), child: const Text('Remove'),
), ),
@ -297,16 +311,6 @@ class _SignInScreenState extends State<SignInScreen> {
onTap: () async { onTap: () async {
setCredentials(context, p); setCredentials(context, p);
setState(() {}); setState(() {});
final confirm = await showYesNoDialog(
context, 'Switch to account?');
if (confirm ?? false) {
service.setActiveProfile(p);
if (mounted) {
context.goNamed(ScreenPaths.timelines);
}
}
}, },
title: Text( title: Text(
p.handle, p.handle,
@ -318,8 +322,8 @@ class _SignInScreenState extends State<SignInScreen> {
), ),
subtitle: Text( subtitle: Text(
p.credentials is BasicCredentials p.credentials is BasicCredentials
? 'Username/Password' ? usernamePasswordType
: 'OAuth Login', : oauthType,
style: active style: active
? const TextStyle( ? const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -332,6 +336,7 @@ class _SignInScreenState extends State<SignInScreen> {
context, 'Log out account?'); context, 'Log out account?');
if (confirm == true) { if (confirm == true) {
await getIt<AccountsService>().signOut(p); await getIt<AccountsService>().signOut(p);
setState(() {});
} }
}, },
child: const Text('Sign out'), child: const Text('Sign out'),
@ -349,8 +354,17 @@ class _SignInScreenState extends State<SignInScreen> {
} }
void _signIn(BuildContext context) async { void _signIn(BuildContext context) async {
if (formKey.currentState?.validate() ?? false) { final valid = formKey.currentState?.validate() ?? false;
ICredentials? creds; if (!valid) {
buildSnackbar(
context, 'Cannot login. Fix highlighted errors and try again.');
return;
}
ICredentials? creds;
if (existingProfile != null) {
creds = existingProfile?.credentials;
} else {
switch (authType) { switch (authType) {
case usernamePasswordType: case usernamePasswordType:
creds = BasicCredentials( creds = BasicCredentials(
@ -365,22 +379,22 @@ class _SignInScreenState extends State<SignInScreen> {
buildSnackbar(context, 'Unknown authorization type: $authType'); buildSnackbar(context, 'Unknown authorization type: $authType');
break; break;
} }
}
if (creds == null) { if (creds == null) {
return; return;
} }
print('Sign in credentials: ${creds.toJson()}'); print('Sign in credentials: ${creds.toJson()}');
final result = await getIt<AccountsService>().signIn(creds); final result = await getIt<AccountsService>().signIn(creds);
if (mounted && result.isFailure) { if (mounted && result.isFailure) {
buildSnackbar(context, 'Error signing in: ${result.error}'); buildSnackbar(context, 'Error signing in: ${result.error}');
return; return;
} }
await getIt<AccountsService>().setActiveProfile(result.value); await getIt<AccountsService>().setActiveProfile(result.value);
if (mounted) { if (mounted) {
context.goNamed(ScreenPaths.timelines); context.goNamed(ScreenPaths.timelines);
}
} }
} }
} }

View file

@ -1,15 +1,25 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../controls/padding.dart'; import '../controls/padding.dart';
import '../globals.dart'; import '../globals.dart';
import '../routes.dart';
import '../services/auth_service.dart'; import '../services/auth_service.dart';
import '../services/status_service.dart';
class SplashScreen extends StatelessWidget { class SplashScreen extends StatelessWidget {
const SplashScreen({super.key}); const SplashScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final statusService = context.watch<StatusService>();
final accountsService = context.watch<AccountsService>();
if (!accountsService.initializing && !accountsService.loggedIn) {
context.pushNamed(ScreenPaths.signin);
}
return Scaffold( return Scaffold(
body: Center( body: Center(
child: Column( child: Column(
@ -27,7 +37,13 @@ class SplashScreen extends StatelessWidget {
const CircularProgressIndicator(), const CircularProgressIndicator(),
const VerticalPadding(), const VerticalPadding(),
const Text('Logging in accounts...'), const Text('Logging in accounts...'),
const VerticalPadding(),
], ],
Text(
statusService.status,
softWrap: true,
textAlign: TextAlign.center,
),
], ],
)), )),
); );

View file

@ -7,11 +7,13 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../di_initialization.dart'; import '../di_initialization.dart';
import '../friendica_client/friendica_client.dart'; import '../friendica_client/friendica_client.dart';
import '../globals.dart';
import '../models/auth/credentials_intf.dart'; import '../models/auth/credentials_intf.dart';
import '../models/auth/profile.dart'; import '../models/auth/profile.dart';
import '../models/exec_error.dart'; import '../models/exec_error.dart';
import '../update_timer_initialization.dart'; import '../update_timer_initialization.dart';
import 'secrets_service.dart'; import 'secrets_service.dart';
import 'status_service.dart';
class AccountsService extends ChangeNotifier { class AccountsService extends ChangeNotifier {
static final _logger = Logger('$AccountsService'); static final _logger = Logger('$AccountsService');
@ -80,6 +82,8 @@ class AccountsService extends ChangeNotifier {
final client = final client =
ProfileClient(Profile.credentialsOnly(signedInCredentials)); ProfileClient(Profile.credentialsOnly(signedInCredentials));
credentialsCache = signedInCredentials; credentialsCache = signedInCredentials;
getIt<StatusService>().setStatus(
'Getting user profile from ${signedInCredentials.serverName}');
return await client.getMyProfile(); return await client.getMyProfile();
}).andThenAsync((profileData) async { }).andThenAsync((profileData) async {
final loginProfile = Profile( final loginProfile = Profile(
@ -91,6 +95,8 @@ class AccountsService extends ChangeNotifier {
loggedIn: true, loggedIn: true,
); );
getIt<StatusService>()
.setStatus('Loaded user profile ${profileData.handle}');
if (_loggedInProfiles.isEmpty) { if (_loggedInProfiles.isEmpty) {
await setActiveProfile(loginProfile, await setActiveProfile(loginProfile,
withNotification: withNotification); withNotification: withNotification);
@ -106,6 +112,7 @@ class AccountsService extends ChangeNotifier {
}); });
if (result.isFailure) { if (result.isFailure) {
getIt<StatusService>().setStatus('Error signing in: ${result.error}');
_logger.severe('Error signing in: ${result.error}'); _logger.severe('Error signing in: ${result.error}');
} }

View file

@ -0,0 +1,16 @@
import 'package:flutter/widgets.dart';
class StatusService extends ChangeNotifier {
String _lastStatus = 'None';
DateTime _lastStatusTime = DateTime.now();
String get status => _lastStatus;
DateTime get statusTime => _lastStatusTime;
void setStatus(String status) {
_lastStatus = status;
_lastStatusTime = DateTime.now();
notifyListeners();
}
}

View file

@ -18,6 +18,12 @@ final _logger = Logger('UpdateTimer');
void setupUpdateTimers() { void setupUpdateTimers() {
Timer.periodic(_timerRefresh, (_) async { Timer.periodic(_timerRefresh, (_) async {
final service = getIt<AccountsService>();
if (!service.loggedIn) {
_logger
.info('Trying to do update when no logged in accounts. Skipping...');
return;
}
executeUpdatesForProfile(getIt<AccountsService>().currentProfile); executeUpdatesForProfile(getIt<AccountsService>().currentProfile);
}); });
} }

View file

@ -2,10 +2,10 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import '../globals.dart';
import 'package:result_monad/result_monad.dart'; import 'package:result_monad/result_monad.dart';
import '../friendica_client/paged_response.dart'; import '../friendica_client/paged_response.dart';
import '../globals.dart';
import '../models/exec_error.dart'; import '../models/exec_error.dart';
final _logger = Logger('NetworkUtils'); final _logger = Logger('NetworkUtils');
@ -15,16 +15,21 @@ http.Response requestTimeout() => http.Response('Client side timeout', 408);
FutureResult<PagedResponse<String>, ExecError> getUrl( FutureResult<PagedResponse<String>, ExecError> getUrl(
Uri url, { Uri url, {
Map<String, String>? headers, Map<String, String>? headers,
Duration? timeout,
}) async { }) async {
_logger.finer('GET: $url'); _logger.finer('GET: $url');
final requestHeaders = headers ?? {};
if (usePhpDebugging) {
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
}
try { try {
final request = http.get( final request = http.get(
url, url,
headers: headers, headers: requestHeaders,
); );
final response = await request.timeout( final response = await request.timeout(
apiCallTimeout, timeout ?? apiCallTimeout,
onTimeout: requestTimeout, onTimeout: requestTimeout,
); );
@ -47,17 +52,22 @@ FutureResult<String, ExecError> postUrl(
Uri url, Uri url,
Map<String, dynamic> body, { Map<String, dynamic> body, {
Map<String, String>? headers, Map<String, String>? headers,
Duration? timeout,
}) async { }) async {
_logger.finer('POST: $url \n Body: $body'); _logger.finer('POST: $url \n Body: $body');
final requestHeaders = headers ?? {};
if (usePhpDebugging) {
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
}
try { try {
final request = http.post( final request = http.post(
url, url,
headers: headers, headers: requestHeaders,
body: jsonEncode(body), body: jsonEncode(body),
); );
final response = await request.timeout( final response = await request.timeout(
apiCallTimeout, timeout ?? apiCallTimeout,
onTimeout: requestTimeout, onTimeout: requestTimeout,
); );

View file

@ -2,7 +2,7 @@ name: relatica
description: A mobile and desktop client for interacting with the Friendica social network description: A mobile and desktop client for interacting with the Friendica social network
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.7.2+1 version: 0.8.0
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'