diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1e198ad9..4e50bad4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,10 +29,6 @@ android:enableOnBackInvokedCallback="true" android:networkSecurityConfig="@xml/network_security_config"> - - + authResultSnapshot.connectionState == ConnectionState.waiting + ? const SplashScreen() + : const AuthScreen(), + ); + } + } @override Widget build(BuildContext context) { @@ -157,15 +174,7 @@ class MyApp extends StatelessWidget { highContrastTheme: wgerLightThemeHc, highContrastDarkTheme: wgerDarkThemeHc, themeMode: user.themeMode, - home: auth.isAuth - ? HomeTabsScreen() - : FutureBuilder( - future: auth.tryAutoLogin(), - builder: (ctx, authResultSnapshot) => - authResultSnapshot.connectionState == ConnectionState.waiting - ? const SplashScreen() - : const AuthScreen(), - ), + home: _getHomeScreen(auth), routes: { DashboardScreen.routeName: (ctx) => const DashboardScreen(), FormScreen.routeName: (ctx) => const FormScreen(), diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index f4ca6f39..a419d25d 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -38,6 +38,12 @@ enum LoginActions { proceed, } +enum AuthState { + updateRequired, + loggedIn, + loggedOut, +} + class AuthProvider with ChangeNotifier { final _logger = Logger('AuthProvider'); @@ -46,6 +52,7 @@ class AuthProvider with ChangeNotifier { String? serverVersion; PackageInfo? applicationVersion; Map metadata = {}; + AuthState state = AuthState.loggedOut; static const MIN_APP_VERSION_URL = 'min-app-version'; static const SERVER_VERSION_URL = 'version'; @@ -54,7 +61,7 @@ class AuthProvider with ChangeNotifier { late http.Client client; - AuthProvider([http.Client? client, bool? checkMetadata]) { + AuthProvider([http.Client? client]) { this.client = client ?? http.Client(); } @@ -83,23 +90,16 @@ class AuthProvider with ChangeNotifier { } /// Checking if there is a new version of the application. - Future applicationUpdateRequired([ - String? version, - Map? metadata, - ]) async { - metadata ??= this.metadata; - - if (!metadata.containsKey(MANIFEST_KEY_CHECK_UPDATE) || - metadata[MANIFEST_KEY_CHECK_UPDATE] == 'false') { - return false; - } - + Future applicationUpdateRequired([String? version]) async { final applicationCurrentVersion = version ?? applicationVersion!.version; final response = await client.get(makeUri(serverUrl!, MIN_APP_VERSION_URL)); final currentVersion = Version.parse(applicationCurrentVersion); final requiredAppVersion = Version.parse(jsonDecode(response.body)); - return requiredAppVersion > currentVersion; + final result = requiredAppVersion > currentVersion; + _logger.fine('Application update required: $result'); + + return result; } /// Registers a new user @@ -160,15 +160,13 @@ class AuthProvider with ChangeNotifier { await initVersions(serverUrl); // If update is required don't log in user - if (await applicationUpdateRequired( - applicationVersion!.version, - {MANIFEST_KEY_CHECK_UPDATE: 'true'}, - )) { + if (await applicationUpdateRequired()) { return {'action': LoginActions.update}; } // Log user in token = responseData['token']; + state = AuthState.loggedIn; notifyListeners(); // store login data in shared preferences @@ -195,23 +193,58 @@ class AuthProvider with ChangeNotifier { return userData['serverUrl'] as String; } - Future tryAutoLogin() async { + /// Tries to auto-login the user with the stored token + Future tryAutoLogin() async { final prefs = await SharedPreferences.getInstance(); if (!prefs.containsKey(PREFS_USER)) { - _logger.info('autologin failed'); - return false; + _logger.info('autologin failed, no saved user data'); + state = AuthState.loggedOut; + return; } - final extractedUserData = json.decode(prefs.getString(PREFS_USER)!); - token = extractedUserData['token']; - serverUrl = extractedUserData['serverUrl']; + final userData = json.decode(prefs.getString(PREFS_USER)!); + + if (!userData.containsKey('token') || !userData.containsKey('serverUrl')) { + _logger.info('autologin failed, no token or serverUrl'); + state = AuthState.loggedOut; + return; + } + + token = userData['token']; + serverUrl = userData['serverUrl']; + + if (token == null || serverUrl == null) { + _logger.info('autologin failed, token or serverUrl is null'); + state = AuthState.loggedOut; + return; + } + + // // Try to talk to a URL using the token, if this doesn't work, log out + final response = await client.head( + makeUri(serverUrl!, 'routine'), + headers: { + HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + HttpHeaders.userAgentHeader: getAppNameHeader(), + HttpHeaders.authorizationHeader: 'Token $token' + }, + ); + if (response.statusCode != 200) { + _logger.info('autologin failed, statusCode: ${response.statusCode}'); + await logout(); + return; + } + + await initVersions(serverUrl!); + + // If update is required don't log in user + if (await applicationUpdateRequired()) { + state = AuthState.updateRequired; + } else { + state = AuthState.loggedIn; + _logger.info('autologin successful'); + } - _logger.info('autologin successful'); - setApplicationVersion(); - setServerVersion(); notifyListeners(); - //_autoLogout(); - return true; } Future logout({bool shouldNotify = true}) async { @@ -219,6 +252,7 @@ class AuthProvider with ChangeNotifier { token = null; serverUrl = null; dataInit = false; + state = AuthState.loggedOut; if (shouldNotify) { notifyListeners(); @@ -236,7 +270,8 @@ class AuthProvider with ChangeNotifier { if (applicationVersion != null) { out = '/${applicationVersion!.version} ' '(${applicationVersion!.packageName}; ' - 'build: ${applicationVersion!.buildNumber})'; + 'build: ${applicationVersion!.buildNumber})' + ' - https://github.com/wger-project'; } return 'wger App$out'; } diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index c4c73eca..085f29e2 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -89,12 +89,6 @@ class AuthScreen extends StatelessWidget { ), ), ), - // Positioned( - // top: 0.4 * deviceSize.height, - // left: 15, - // right: 15, - // child: const , - // ), ], ), ); @@ -166,7 +160,6 @@ class _AuthCardState extends State { void _submit(BuildContext context) async { if (!_formKey.currentState!.validate()) { - // Invalid! return; } _formKey.currentState!.save(); diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index b038a942..22ac70c5 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -146,27 +146,8 @@ class _HomeTabsScreenState extends State with SingleTickerProvid future: _initialData, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Center( - child: SizedBox( - height: 70, - child: RiveAnimation.asset( - 'assets/animations/wger_logo.riv', - animations: ['idle_loop2'], - ), - ), - ), - Text( - AppLocalizations.of(context).loadingText, - style: Theme.of(context).textTheme.headlineSmall, - ), - ], - ), - ), + return const Scaffold( + body: LoadingWidget(), ); } else { return Scaffold( @@ -204,3 +185,33 @@ class _HomeTabsScreenState extends State with SingleTickerProvid ); } } + +class LoadingWidget extends StatelessWidget { + const LoadingWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Center( + child: SizedBox( + height: 70, + child: RiveAnimation.asset( + 'assets/animations/wger_logo.riv', + animations: ['idle_loop2'], + ), + ), + ), + Text( + AppLocalizations.of(context).loadingText, + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + ); + } +} diff --git a/lib/screens/update_app_screen.dart b/lib/screens/update_app_screen.dart index 8b1996c7..2b01eeec 100644 --- a/lib/screens/update_app_screen.dart +++ b/lib/screens/update_app_screen.dart @@ -30,10 +30,7 @@ class UpdateAppScreen extends StatelessWidget { AppLocalizations.of(context).appUpdateTitle, style: Theme.of(context).textTheme.headlineSmall, ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [Text(AppLocalizations.of(context).appUpdateContent)], - ), + content: Text(AppLocalizations.of(context).appUpdateContent), actions: null, ), ); diff --git a/test/auth/auth_provider_test.dart b/test/auth/auth_provider_test.dart index 8c7d9db4..cea67ef8 100644 --- a/test/auth/auth_provider_test.dart +++ b/test/auth/auth_provider_test.dart @@ -15,11 +15,9 @@ void main() { path: 'api/v2/min-app-version/', ); - final testMetadata = {'wger.check_min_app_version': 'true'}; - setUp(() { mockClient = MockClient(); - authProvider = AuthProvider(mockClient, false); + authProvider = AuthProvider(mockClient); authProvider.serverUrl = 'http://localhost'; }); @@ -27,7 +25,7 @@ void main() { test('app version higher than min version', () async { // arrange when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.2.0"', 200))); - final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0', testMetadata); + final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0'); // assert expect(updateNeeded, false); @@ -36,7 +34,7 @@ void main() { test('app version higher than min version - 1', () async { // arrange when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.3"', 200))); - final updateNeeded = await authProvider.applicationUpdateRequired('1.1', testMetadata); + final updateNeeded = await authProvider.applicationUpdateRequired('1.1'); // assert expect(updateNeeded, true); @@ -45,7 +43,7 @@ void main() { test('app version higher than min version - 2', () async { // arrange when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.3.0"', 200))); - final updateNeeded = await authProvider.applicationUpdateRequired('1.1', testMetadata); + final updateNeeded = await authProvider.applicationUpdateRequired('1.1'); // assert expect(updateNeeded, true); @@ -54,7 +52,7 @@ void main() { test('app version equal as min version', () async { // arrange when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.3.0"', 200))); - final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0', testMetadata); + final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0'); // assert expect(updateNeeded, false); diff --git a/test/auth/auth_screen_test.dart b/test/auth/auth_screen_test.dart index 953fd535..ece5cfaa 100644 --- a/test/auth/auth_screen_test.dart +++ b/test/auth/auth_screen_test.dart @@ -73,7 +73,7 @@ void main() { setUp(() { mockClient = MockClient(); - authProvider = AuthProvider(mockClient, false); + authProvider = AuthProvider(mockClient); authProvider.serverUrl = 'https://wger.de'; SharedPreferences.setMockInitialValues({}); diff --git a/test/nutrition/nutritional_plan_screen_test.mocks.dart b/test/nutrition/nutritional_plan_screen_test.mocks.dart index 09ca8fcb..694d7827 100644 --- a/test/nutrition/nutritional_plan_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plan_screen_test.mocks.dart @@ -202,6 +202,18 @@ class MockAuthProvider extends _i1.Mock implements _i2.AuthProvider { returnValueForMissingStub: null, ); + @override + _i2.AuthState get state => (super.noSuchMethod( + Invocation.getter(#state), + returnValue: _i2.AuthState.updateRequired, + ) as _i2.AuthState); + + @override + set state(_i2.AuthState? _state) => super.noSuchMethod( + Invocation.setter(#state, _state), + returnValueForMissingStub: null, + ); + @override _i3.Client get client => (super.noSuchMethod( Invocation.getter(#client), @@ -253,12 +265,8 @@ class MockAuthProvider extends _i1.Mock implements _i2.AuthProvider { ) as _i5.Future); @override - _i5.Future applicationUpdateRequired([ - String? version, - Map? metadata, - ]) => - (super.noSuchMethod( - Invocation.method(#applicationUpdateRequired, [version, metadata]), + _i5.Future applicationUpdateRequired([String? version]) => (super.noSuchMethod( + Invocation.method(#applicationUpdateRequired, [version]), returnValue: _i5.Future.value(false), ) as _i5.Future); @@ -308,10 +316,11 @@ class MockAuthProvider extends _i1.Mock implements _i2.AuthProvider { ) as _i5.Future); @override - _i5.Future tryAutoLogin() => (super.noSuchMethod( + _i5.Future tryAutoLogin() => (super.noSuchMethod( Invocation.method(#tryAutoLogin, []), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future logout({bool? shouldNotify = true}) => (super.noSuchMethod( diff --git a/test/nutrition/nutritional_plans_screen_test.mocks.dart b/test/nutrition/nutritional_plans_screen_test.mocks.dart index dff174c1..a135ff8e 100644 --- a/test/nutrition/nutritional_plans_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plans_screen_test.mocks.dart @@ -94,6 +94,18 @@ class MockAuthProvider extends _i1.Mock implements _i3.AuthProvider { returnValueForMissingStub: null, ); + @override + _i3.AuthState get state => (super.noSuchMethod( + Invocation.getter(#state), + returnValue: _i3.AuthState.updateRequired, + ) as _i3.AuthState); + + @override + set state(_i3.AuthState? _state) => super.noSuchMethod( + Invocation.setter(#state, _state), + returnValueForMissingStub: null, + ); + @override _i2.Client get client => (super.noSuchMethod( Invocation.getter(#client), @@ -145,12 +157,8 @@ class MockAuthProvider extends _i1.Mock implements _i3.AuthProvider { ) as _i5.Future); @override - _i5.Future applicationUpdateRequired([ - String? version, - Map? metadata, - ]) => - (super.noSuchMethod( - Invocation.method(#applicationUpdateRequired, [version, metadata]), + _i5.Future applicationUpdateRequired([String? version]) => (super.noSuchMethod( + Invocation.method(#applicationUpdateRequired, [version]), returnValue: _i5.Future.value(false), ) as _i5.Future); @@ -200,10 +208,11 @@ class MockAuthProvider extends _i1.Mock implements _i3.AuthProvider { ) as _i5.Future); @override - _i5.Future tryAutoLogin() => (super.noSuchMethod( + _i5.Future tryAutoLogin() => (super.noSuchMethod( Invocation.method(#tryAutoLogin, []), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future logout({bool? shouldNotify = true}) => (super.noSuchMethod( diff --git a/test/utils.dart b/test/utils.dart index 9244b4da..db590b38 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -23,7 +23,7 @@ import 'measurements/measurement_provider_test.mocks.dart'; import 'other/base_provider_test.mocks.dart'; // Test Auth provider -final AuthProvider testAuthProvider = AuthProvider(MockClient(), false) +final AuthProvider testAuthProvider = AuthProvider(MockClient()) ..token = 'FooBar' ..serverUrl = 'https://localhost';