mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Refactor app version handling and update authentication flow
Previously, this was only triggered when logging in to the application. If a user just opened the app, it would just stop working. We also now always check this min version and have removed the option from the android manifest file since disabling this doesn't make much sense and we have many other platforms as well (iOS, flatpak)
This commit is contained in:
@@ -29,10 +29,6 @@
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<meta-data
|
||||
android:name="wger.check_min_app_version"
|
||||
android:value="true" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -31,9 +31,6 @@ const DEFAULT_SERVER_TEST = 'https://wger-master.rge.uber.space/';
|
||||
const TESTSERVER_USER_NAME = 'user';
|
||||
const TESTSERVER_PASSWORD = 'flutteruser';
|
||||
|
||||
/// Keys used in the android manifest
|
||||
const MANIFEST_KEY_CHECK_UPDATE = 'wger.check_min_app_version';
|
||||
|
||||
/// Default impression for a workout session (neutral)
|
||||
const DEFAULT_IMPRESSION = 2;
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import 'package:wger/screens/routine_list_screen.dart';
|
||||
import 'package:wger/screens/routine_logs_screen.dart';
|
||||
import 'package:wger/screens/routine_screen.dart';
|
||||
import 'package:wger/screens/splash_screen.dart';
|
||||
import 'package:wger/screens/update_app_screen.dart';
|
||||
import 'package:wger/screens/weight_screen.dart';
|
||||
import 'package:wger/theme/theme.dart';
|
||||
import 'package:wger/widgets/core/about.dart';
|
||||
@@ -78,11 +79,27 @@ void main() async {
|
||||
await ServiceLocator().configure();
|
||||
|
||||
// Application
|
||||
runApp(const riverpod.ProviderScope(child: MyApp()));
|
||||
runApp(const riverpod.ProviderScope(child: MainApp()));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp();
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp();
|
||||
|
||||
Widget _getHomeScreen(AuthProvider auth) {
|
||||
if (auth.state == AuthState.loggedIn) {
|
||||
return HomeTabsScreen();
|
||||
} else if (auth.state == AuthState.updateRequired) {
|
||||
return const UpdateAppScreen();
|
||||
} else {
|
||||
return FutureBuilder(
|
||||
future: auth.tryAutoLogin(),
|
||||
builder: (ctx, authResultSnapshot) =>
|
||||
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(),
|
||||
|
||||
@@ -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<String, String> 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<bool> applicationUpdateRequired([
|
||||
String? version,
|
||||
Map<String, String>? metadata,
|
||||
]) async {
|
||||
metadata ??= this.metadata;
|
||||
|
||||
if (!metadata.containsKey(MANIFEST_KEY_CHECK_UPDATE) ||
|
||||
metadata[MANIFEST_KEY_CHECK_UPDATE] == 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> 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<bool> tryAutoLogin() async {
|
||||
/// Tries to auto-login the user with the stored token
|
||||
Future<void> 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<void> 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';
|
||||
}
|
||||
|
||||
@@ -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<AuthCard> {
|
||||
|
||||
void _submit(BuildContext context) async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
// Invalid!
|
||||
return;
|
||||
}
|
||||
_formKey.currentState!.save();
|
||||
|
||||
@@ -146,27 +146,8 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> 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<HomeTabsScreen> 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -73,7 +73,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
mockClient = MockClient();
|
||||
authProvider = AuthProvider(mockClient, false);
|
||||
authProvider = AuthProvider(mockClient);
|
||||
authProvider.serverUrl = 'https://wger.de';
|
||||
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
@@ -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<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<bool> applicationUpdateRequired([
|
||||
String? version,
|
||||
Map<String, String>? metadata,
|
||||
]) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#applicationUpdateRequired, [version, metadata]),
|
||||
_i5.Future<bool> applicationUpdateRequired([String? version]) => (super.noSuchMethod(
|
||||
Invocation.method(#applicationUpdateRequired, [version]),
|
||||
returnValue: _i5.Future<bool>.value(false),
|
||||
) as _i5.Future<bool>);
|
||||
|
||||
@@ -308,10 +316,11 @@ class MockAuthProvider extends _i1.Mock implements _i2.AuthProvider {
|
||||
) as _i5.Future<String>);
|
||||
|
||||
@override
|
||||
_i5.Future<bool> tryAutoLogin() => (super.noSuchMethod(
|
||||
_i5.Future<void> tryAutoLogin() => (super.noSuchMethod(
|
||||
Invocation.method(#tryAutoLogin, []),
|
||||
returnValue: _i5.Future<bool>.value(false),
|
||||
) as _i5.Future<bool>);
|
||||
returnValue: _i5.Future<void>.value(),
|
||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||
) as _i5.Future<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<void> logout({bool? shouldNotify = true}) => (super.noSuchMethod(
|
||||
|
||||
@@ -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<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<bool> applicationUpdateRequired([
|
||||
String? version,
|
||||
Map<String, String>? metadata,
|
||||
]) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#applicationUpdateRequired, [version, metadata]),
|
||||
_i5.Future<bool> applicationUpdateRequired([String? version]) => (super.noSuchMethod(
|
||||
Invocation.method(#applicationUpdateRequired, [version]),
|
||||
returnValue: _i5.Future<bool>.value(false),
|
||||
) as _i5.Future<bool>);
|
||||
|
||||
@@ -200,10 +208,11 @@ class MockAuthProvider extends _i1.Mock implements _i3.AuthProvider {
|
||||
) as _i5.Future<String>);
|
||||
|
||||
@override
|
||||
_i5.Future<bool> tryAutoLogin() => (super.noSuchMethod(
|
||||
_i5.Future<void> tryAutoLogin() => (super.noSuchMethod(
|
||||
Invocation.method(#tryAutoLogin, []),
|
||||
returnValue: _i5.Future<bool>.value(false),
|
||||
) as _i5.Future<bool>);
|
||||
returnValue: _i5.Future<void>.value(),
|
||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||
) as _i5.Future<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<void> logout({bool? shouldNotify = true}) => (super.noSuchMethod(
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user