Skip to content

Commit e4b550a

Browse files
committed
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)
1 parent cacb89f commit e4b550a

12 files changed

+161
-107
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@
2929
android:enableOnBackInvokedCallback="true"
3030
android:networkSecurityConfig="@xml/network_security_config">
3131

32-
<meta-data
33-
android:name="wger.check_min_app_version"
34-
android:value="true" />
35-
3632
<activity
3733
android:name=".MainActivity"
3834
android:exported="true"

lib/helpers/consts.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ const DEFAULT_SERVER_TEST = 'https://wger-master.rge.uber.space/';
3131
const TESTSERVER_USER_NAME = 'user';
3232
const TESTSERVER_PASSWORD = 'flutteruser';
3333

34-
/// Keys used in the android manifest
35-
const MANIFEST_KEY_CHECK_UPDATE = 'wger.check_min_app_version';
36-
3734
/// Default impression for a workout session (neutral)
3835
const DEFAULT_IMPRESSION = 2;
3936

lib/main.dart

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import 'package:wger/screens/routine_list_screen.dart';
5353
import 'package:wger/screens/routine_logs_screen.dart';
5454
import 'package:wger/screens/routine_screen.dart';
5555
import 'package:wger/screens/splash_screen.dart';
56+
import 'package:wger/screens/update_app_screen.dart';
5657
import 'package:wger/screens/weight_screen.dart';
5758
import 'package:wger/theme/theme.dart';
5859
import 'package:wger/widgets/core/about.dart';
@@ -78,11 +79,27 @@ void main() async {
7879
await ServiceLocator().configure();
7980

8081
// Application
81-
runApp(const riverpod.ProviderScope(child: MyApp()));
82+
runApp(const riverpod.ProviderScope(child: MainApp()));
8283
}
8384

84-
class MyApp extends StatelessWidget {
85-
const MyApp();
85+
class MainApp extends StatelessWidget {
86+
const MainApp();
87+
88+
Widget _getHomeScreen(AuthProvider auth) {
89+
if (auth.state == AuthState.loggedIn) {
90+
return HomeTabsScreen();
91+
} else if (auth.state == AuthState.updateRequired) {
92+
return const UpdateAppScreen();
93+
} else {
94+
return FutureBuilder(
95+
future: auth.tryAutoLogin(),
96+
builder: (ctx, authResultSnapshot) =>
97+
authResultSnapshot.connectionState == ConnectionState.waiting
98+
? const SplashScreen()
99+
: const AuthScreen(),
100+
);
101+
}
102+
}
86103

87104
@override
88105
Widget build(BuildContext context) {
@@ -157,15 +174,7 @@ class MyApp extends StatelessWidget {
157174
highContrastTheme: wgerLightThemeHc,
158175
highContrastDarkTheme: wgerDarkThemeHc,
159176
themeMode: user.themeMode,
160-
home: auth.isAuth
161-
? HomeTabsScreen()
162-
: FutureBuilder(
163-
future: auth.tryAutoLogin(),
164-
builder: (ctx, authResultSnapshot) =>
165-
authResultSnapshot.connectionState == ConnectionState.waiting
166-
? const SplashScreen()
167-
: const AuthScreen(),
168-
),
177+
home: _getHomeScreen(auth),
169178
routes: {
170179
DashboardScreen.routeName: (ctx) => const DashboardScreen(),
171180
FormScreen.routeName: (ctx) => const FormScreen(),

lib/providers/auth.dart

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ enum LoginActions {
3838
proceed,
3939
}
4040

41+
enum AuthState {
42+
updateRequired,
43+
loggedIn,
44+
loggedOut,
45+
}
46+
4147
class AuthProvider with ChangeNotifier {
4248
final _logger = Logger('AuthProvider');
4349

@@ -46,6 +52,7 @@ class AuthProvider with ChangeNotifier {
4652
String? serverVersion;
4753
PackageInfo? applicationVersion;
4854
Map<String, String> metadata = {};
55+
AuthState state = AuthState.loggedOut;
4956

5057
static const MIN_APP_VERSION_URL = 'min-app-version';
5158
static const SERVER_VERSION_URL = 'version';
@@ -54,7 +61,7 @@ class AuthProvider with ChangeNotifier {
5461

5562
late http.Client client;
5663

57-
AuthProvider([http.Client? client, bool? checkMetadata]) {
64+
AuthProvider([http.Client? client]) {
5865
this.client = client ?? http.Client();
5966
}
6067

@@ -83,23 +90,16 @@ class AuthProvider with ChangeNotifier {
8390
}
8491

8592
/// Checking if there is a new version of the application.
86-
Future<bool> applicationUpdateRequired([
87-
String? version,
88-
Map<String, String>? metadata,
89-
]) async {
90-
metadata ??= this.metadata;
91-
92-
if (!metadata.containsKey(MANIFEST_KEY_CHECK_UPDATE) ||
93-
metadata[MANIFEST_KEY_CHECK_UPDATE] == 'false') {
94-
return false;
95-
}
96-
93+
Future<bool> applicationUpdateRequired([String? version]) async {
9794
final applicationCurrentVersion = version ?? applicationVersion!.version;
9895
final response = await client.get(makeUri(serverUrl!, MIN_APP_VERSION_URL));
9996
final currentVersion = Version.parse(applicationCurrentVersion);
10097
final requiredAppVersion = Version.parse(jsonDecode(response.body));
10198

102-
return requiredAppVersion > currentVersion;
99+
final result = requiredAppVersion > currentVersion;
100+
_logger.fine('Application update required: $result');
101+
102+
return result;
103103
}
104104

105105
/// Registers a new user
@@ -160,15 +160,13 @@ class AuthProvider with ChangeNotifier {
160160
await initVersions(serverUrl);
161161

162162
// If update is required don't log in user
163-
if (await applicationUpdateRequired(
164-
applicationVersion!.version,
165-
{MANIFEST_KEY_CHECK_UPDATE: 'true'},
166-
)) {
163+
if (await applicationUpdateRequired()) {
167164
return {'action': LoginActions.update};
168165
}
169166

170167
// Log user in
171168
token = responseData['token'];
169+
state = AuthState.loggedIn;
172170
notifyListeners();
173171

174172
// store login data in shared preferences
@@ -195,30 +193,66 @@ class AuthProvider with ChangeNotifier {
195193
return userData['serverUrl'] as String;
196194
}
197195

198-
Future<bool> tryAutoLogin() async {
196+
/// Tries to auto-login the user with the stored token
197+
Future<void> tryAutoLogin() async {
199198
final prefs = await SharedPreferences.getInstance();
200199
if (!prefs.containsKey(PREFS_USER)) {
201-
_logger.info('autologin failed');
202-
return false;
200+
_logger.info('autologin failed, no saved user data');
201+
state = AuthState.loggedOut;
202+
return;
203+
}
204+
205+
final userData = json.decode(prefs.getString(PREFS_USER)!);
206+
207+
if (!userData.containsKey('token') || !userData.containsKey('serverUrl')) {
208+
_logger.info('autologin failed, no token or serverUrl');
209+
state = AuthState.loggedOut;
210+
return;
211+
}
212+
213+
token = userData['token'];
214+
serverUrl = userData['serverUrl'];
215+
216+
if (token == null || serverUrl == null) {
217+
_logger.info('autologin failed, token or serverUrl is null');
218+
state = AuthState.loggedOut;
219+
return;
203220
}
204-
final extractedUserData = json.decode(prefs.getString(PREFS_USER)!);
205221

206-
token = extractedUserData['token'];
207-
serverUrl = extractedUserData['serverUrl'];
222+
// // Try to talk to a URL using the token, if this doesn't work, log out
223+
final response = await client.head(
224+
makeUri(serverUrl!, 'routine'),
225+
headers: {
226+
HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8',
227+
HttpHeaders.userAgentHeader: getAppNameHeader(),
228+
HttpHeaders.authorizationHeader: 'Token $token'
229+
},
230+
);
231+
if (response.statusCode != 200) {
232+
_logger.info('autologin failed, statusCode: ${response.statusCode}');
233+
await logout();
234+
return;
235+
}
236+
237+
await initVersions(serverUrl!);
238+
239+
// If update is required don't log in user
240+
if (await applicationUpdateRequired()) {
241+
state = AuthState.updateRequired;
242+
} else {
243+
state = AuthState.loggedIn;
244+
_logger.info('autologin successful');
245+
}
208246

209-
_logger.info('autologin successful');
210-
setApplicationVersion();
211-
setServerVersion();
212247
notifyListeners();
213-
//_autoLogout();
214-
return true;
215248
}
216249

217250
Future<void> logout({bool shouldNotify = true}) async {
218251
_logger.fine('logging out');
219252
token = null;
220253
serverUrl = null;
221254
dataInit = false;
255+
state = AuthState.loggedOut;
222256

223257
if (shouldNotify) {
224258
notifyListeners();
@@ -236,7 +270,8 @@ class AuthProvider with ChangeNotifier {
236270
if (applicationVersion != null) {
237271
out = '/${applicationVersion!.version} '
238272
'(${applicationVersion!.packageName}; '
239-
'build: ${applicationVersion!.buildNumber})';
273+
'build: ${applicationVersion!.buildNumber})'
274+
' - https://github.com/wger-project';
240275
}
241276
return 'wger App$out';
242277
}

lib/screens/auth_screen.dart

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,6 @@ class AuthScreen extends StatelessWidget {
8989
),
9090
),
9191
),
92-
// Positioned(
93-
// top: 0.4 * deviceSize.height,
94-
// left: 15,
95-
// right: 15,
96-
// child: const ,
97-
// ),
9892
],
9993
),
10094
);
@@ -166,7 +160,6 @@ class _AuthCardState extends State<AuthCard> {
166160

167161
void _submit(BuildContext context) async {
168162
if (!_formKey.currentState!.validate()) {
169-
// Invalid!
170163
return;
171164
}
172165
_formKey.currentState!.save();

lib/screens/home_tabs_screen.dart

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -146,27 +146,8 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
146146
future: _initialData,
147147
builder: (context, snapshot) {
148148
if (snapshot.connectionState != ConnectionState.done) {
149-
return Scaffold(
150-
body: Center(
151-
child: Column(
152-
mainAxisAlignment: MainAxisAlignment.center,
153-
children: [
154-
const Center(
155-
child: SizedBox(
156-
height: 70,
157-
child: RiveAnimation.asset(
158-
'assets/animations/wger_logo.riv',
159-
animations: ['idle_loop2'],
160-
),
161-
),
162-
),
163-
Text(
164-
AppLocalizations.of(context).loadingText,
165-
style: Theme.of(context).textTheme.headlineSmall,
166-
),
167-
],
168-
),
169-
),
149+
return const Scaffold(
150+
body: LoadingWidget(),
170151
);
171152
} else {
172153
return Scaffold(
@@ -204,3 +185,33 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
204185
);
205186
}
206187
}
188+
189+
class LoadingWidget extends StatelessWidget {
190+
const LoadingWidget({
191+
super.key,
192+
});
193+
194+
@override
195+
Widget build(BuildContext context) {
196+
return Center(
197+
child: Column(
198+
mainAxisAlignment: MainAxisAlignment.center,
199+
children: [
200+
const Center(
201+
child: SizedBox(
202+
height: 70,
203+
child: RiveAnimation.asset(
204+
'assets/animations/wger_logo.riv',
205+
animations: ['idle_loop2'],
206+
),
207+
),
208+
),
209+
Text(
210+
AppLocalizations.of(context).loadingText,
211+
style: Theme.of(context).textTheme.headlineSmall,
212+
),
213+
],
214+
),
215+
);
216+
}
217+
}

lib/screens/update_app_screen.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@ class UpdateAppScreen extends StatelessWidget {
3030
AppLocalizations.of(context).appUpdateTitle,
3131
style: Theme.of(context).textTheme.headlineSmall,
3232
),
33-
content: Column(
34-
mainAxisSize: MainAxisSize.min,
35-
children: [Text(AppLocalizations.of(context).appUpdateContent)],
36-
),
33+
content: Text(AppLocalizations.of(context).appUpdateContent),
3734
actions: null,
3835
),
3936
);

test/auth/auth_provider_test.dart

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,17 @@ void main() {
1515
path: 'api/v2/min-app-version/',
1616
);
1717

18-
final testMetadata = {'wger.check_min_app_version': 'true'};
19-
2018
setUp(() {
2119
mockClient = MockClient();
22-
authProvider = AuthProvider(mockClient, false);
20+
authProvider = AuthProvider(mockClient);
2321
authProvider.serverUrl = 'http://localhost';
2422
});
2523

2624
group('min application version check', () {
2725
test('app version higher than min version', () async {
2826
// arrange
2927
when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.2.0"', 200)));
30-
final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0', testMetadata);
28+
final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0');
3129

3230
// assert
3331
expect(updateNeeded, false);
@@ -36,7 +34,7 @@ void main() {
3634
test('app version higher than min version - 1', () async {
3735
// arrange
3836
when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.3"', 200)));
39-
final updateNeeded = await authProvider.applicationUpdateRequired('1.1', testMetadata);
37+
final updateNeeded = await authProvider.applicationUpdateRequired('1.1');
4038

4139
// assert
4240
expect(updateNeeded, true);
@@ -45,7 +43,7 @@ void main() {
4543
test('app version higher than min version - 2', () async {
4644
// arrange
4745
when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.3.0"', 200)));
48-
final updateNeeded = await authProvider.applicationUpdateRequired('1.1', testMetadata);
46+
final updateNeeded = await authProvider.applicationUpdateRequired('1.1');
4947

5048
// assert
5149
expect(updateNeeded, true);
@@ -54,7 +52,7 @@ void main() {
5452
test('app version equal as min version', () async {
5553
// arrange
5654
when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.3.0"', 200)));
57-
final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0', testMetadata);
55+
final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0');
5856

5957
// assert
6058
expect(updateNeeded, false);

test/auth/auth_screen_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ void main() {
7373

7474
setUp(() {
7575
mockClient = MockClient();
76-
authProvider = AuthProvider(mockClient, false);
76+
authProvider = AuthProvider(mockClient);
7777
authProvider.serverUrl = 'https://wger.de';
7878

7979
SharedPreferences.setMockInitialValues({});

0 commit comments

Comments
 (0)