Skip to content

Commit 0bbb703

Browse files
feat: check for supported server version (#218)
1 parent 741d642 commit 0bbb703

File tree

16 files changed

+440
-102
lines changed

16 files changed

+440
-102
lines changed

lib/core/di/network_provider.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'package:flutter_riverpod/flutter_riverpod.dart';
21
import 'package:riverpod_annotation/riverpod_annotation.dart';
32
import 'package:vikunja_app/core/network/client.dart';
43
import 'package:vikunja_app/domain/entities/auth_model.dart';

lib/core/utils/constants.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import 'package:flutter/material.dart';
22
import 'package:intl/intl.dart';
3+
import 'package:vikunja_app/domain/entities/version.dart';
4+
5+
var supportedServerVersion = Version(1, 0, 0);
36

47
const vPrimary = Color(0xFF0c86ff);
58
const vLabelLight = Color(0xFFf2f2f2);

lib/core/utils/validator.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ bool isEmail(String? email) {
77
return _emailRegex.hasMatch(email);
88
}
99

10-
final RegExp _url = new RegExp(
10+
final RegExp _urlRegex = new RegExp(
1111
r'https?:\/\/((([a-zA-Z0-9.\-\_]+)\.[a-zA-Z]+)|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:[0-9]+)?',
1212
);
1313

1414
bool isUrl(String? url) {
1515
if (url == null) return false;
16-
return _url.hasMatch(url);
16+
return _urlRegex.hasMatch(url);
1717
}
1818

19+
final RegExp versionRegex = new RegExp(
20+
r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$',
21+
);
22+
1923
bool isURLValid(String? url) {
2024
if (url == null || url.isEmpty) return true;
2125
final trimmed = url.trim();
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:vikunja_app/data/data_sources/version_data_source.dart';
2+
import 'package:vikunja_app/domain/entities/version.dart';
23
import 'package:vikunja_app/domain/repositories/version_repository.dart';
34

45
class VersionRepositoryImpl extends VersionRepository {
@@ -7,19 +8,17 @@ class VersionRepositoryImpl extends VersionRepository {
78
VersionRepositoryImpl(this._dataSource);
89

910
@override
10-
Future<String?> getLatestVersionTag() async {
11-
return _dataSource.getLatestVersionTag();
12-
}
11+
Future<Version?> getLatestVersionTag() async {
12+
var latestVersionTag = await _dataSource.getLatestVersionTag();
1313

14-
@override
15-
Future<String> getCurrentVersionTag() async {
16-
return _dataSource.getCurrentVersionTag();
14+
return latestVersionTag != null
15+
? Version.fromString(latestVersionTag)
16+
: null;
1717
}
1818

1919
@override
20-
Future<bool> isUpToDate() async {
21-
String? latest = await getLatestVersionTag();
22-
String current = await getCurrentVersionTag();
23-
return latest == null || latest == current;
20+
Future<Version?> getCurrentVersionTag() async {
21+
var currentVersionTag = await _dataSource.getCurrentVersionTag();
22+
return Version.fromString(currentVersionTag);
2423
}
2524
}

lib/domain/entities/settings_page_state.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:vikunja_app/core/theming/theme_mode.dart';
22
import 'package:vikunja_app/domain/entities/project.dart';
33
import 'package:vikunja_app/domain/entities/user.dart';
4+
import 'package:vikunja_app/domain/entities/version.dart';
45

56
class SettingsPageState {
67
User user;
@@ -15,7 +16,7 @@ class SettingsPageState {
1516
FlutterThemeMode themeMode;
1617
bool dynamicColors;
1718

18-
String currentVersion;
19+
Version? currentVersion;
1920

2021
SettingsPageState(
2122
this.user,

lib/domain/entities/version.dart

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import 'package:vikunja_app/core/utils/validator.dart';
2+
3+
class Version {
4+
int major;
5+
int minor;
6+
int patch;
7+
String? label;
8+
String? label2;
9+
10+
Version(this.major, this.minor, this.patch, [this.label, this.label2]);
11+
12+
static Version? fromString(String versionString) {
13+
if (versionRegex.hasMatch(versionString)) {
14+
Iterable<RegExpMatch> matches = versionRegex.allMatches(versionString);
15+
16+
RegExpMatch version = matches.elementAt(0);
17+
var major = int.tryParse(version.group(1) ?? "");
18+
var minor = int.tryParse(version.group(2) ?? "");
19+
var patch = int.tryParse(version.group(3) ?? "");
20+
var label = version.group(4);
21+
var label2 = version.group(5);
22+
23+
return Version(major ?? -1, minor ?? -1, patch ?? -1, label, label2);
24+
}
25+
26+
return null;
27+
}
28+
29+
static Version? fromServerString(String versionString) {
30+
versionString = versionString.substring(1);
31+
if (versionRegex.hasMatch(versionString)) {
32+
Iterable<RegExpMatch> matches = versionRegex.allMatches(versionString);
33+
34+
RegExpMatch version = matches.elementAt(0);
35+
var major = int.tryParse(version.group(1) ?? "");
36+
var minor = int.tryParse(version.group(2) ?? "");
37+
var patch = int.tryParse(version.group(3) ?? "");
38+
var label = version.group(4);
39+
var label2 = version.group(5);
40+
41+
return Version(major ?? -1, minor ?? -1, patch ?? -1, label, label2);
42+
}
43+
44+
return null;
45+
}
46+
47+
bool isNewerThan(Version other) {
48+
try {
49+
if (major > other.major) return true;
50+
if (major < other.major) return false;
51+
if (minor > other.minor) return true;
52+
if (minor < other.minor) return false;
53+
if (patch > other.patch) return true;
54+
if (patch < other.patch) return false;
55+
56+
if (label != null && label == other.label) {
57+
if (label == "beta") {
58+
return int.parse(label2 ?? "0") > int.parse(other.label2 ?? "0");
59+
}
60+
} else if (label?.startsWith("rc") == true &&
61+
other.label?.startsWith("rc") == true) {
62+
return int.parse(label?.substring(2) ?? "0") >
63+
int.parse(other.label?.substring(2) ?? "0");
64+
} else if (label != null && other.label != null) {
65+
if (label?.startsWith("rc") == true && other.label == "beta") {
66+
return true;
67+
}
68+
} else if (label == null && other.label != null) {
69+
return true;
70+
}
71+
} catch (e) {}
72+
73+
return false;
74+
}
75+
76+
@override
77+
bool operator ==(Object other) =>
78+
identical(this, other) ||
79+
other is Version &&
80+
runtimeType == other.runtimeType &&
81+
major == other.major &&
82+
minor == other.minor &&
83+
patch == other.patch &&
84+
label == other.label &&
85+
label2 == other.label2;
86+
87+
@override
88+
int get hashCode => Object.hash(major, minor, patch, label, label2);
89+
90+
@override
91+
String toString() {
92+
var versionString = '$major.$minor.$patch';
93+
94+
if (label != null) {
95+
versionString += '-$label';
96+
}
97+
98+
if (label2 != null && int.tryParse(label2!) != null) {
99+
versionString += '+$label2';
100+
}
101+
102+
return versionString;
103+
}
104+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
abstract class VersionRepository {
2-
Future<String?> getLatestVersionTag();
1+
import 'package:vikunja_app/domain/entities/version.dart';
32

4-
Future<String> getCurrentVersionTag();
3+
abstract class VersionRepository {
4+
Future<Version?> getLatestVersionTag();
55

6-
Future<bool> isUpToDate();
6+
Future<Version?> getCurrentVersionTag();
77
}

lib/init_page.dart

Lines changed: 82 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@ import 'package:sentry_flutter/sentry_flutter.dart';
44
import 'package:vikunja_app/core/di/network_provider.dart';
55
import 'package:vikunja_app/core/di/repository_provider.dart';
66
import 'package:vikunja_app/core/network/response.dart';
7+
import 'package:vikunja_app/core/utils/constants.dart';
78
import 'package:vikunja_app/domain/entities/auth_model.dart';
89
import 'package:vikunja_app/domain/entities/server.dart';
10+
import 'package:vikunja_app/domain/entities/user.dart';
11+
import 'package:vikunja_app/domain/entities/version.dart';
12+
import 'package:vikunja_app/l10n/gen/app_localizations.dart';
913
import 'package:vikunja_app/main.dart';
1014
import 'package:vikunja_app/presentation/pages/error_widget.dart';
1115
import 'package:vikunja_app/presentation/pages/loading_widget.dart';
12-
import 'package:vikunja_app/l10n/gen/app_localizations.dart';
16+
import 'package:vikunja_app/presentation/widgets/version_mismatch_dialog.dart';
1317

1418
class InitPage extends ConsumerWidget {
1519
const InitPage({super.key});
1620

1721
@override
1822
Widget build(BuildContext context, WidgetRef ref) {
1923
return FutureBuilder(
20-
future: checkLogin(ref),
24+
future: checkLoginToken(ref),
2125
builder: (context, asyncSnapshot) {
2226
if (asyncSnapshot.connectionState == ConnectionState.done &&
2327
asyncSnapshot.data != null) {
@@ -31,57 +35,90 @@ class InitPage extends ConsumerWidget {
3135
);
3236
}
3337

34-
Future<Object?> checkLogin(WidgetRef ref) async {
38+
Future<Object?> checkLoginToken(WidgetRef ref) async {
3539
var server = await ref.read(settingsRepositoryProvider).getServer();
3640
var token = await ref.read(settingsRepositoryProvider).getUserToken();
3741

3842
if (server != null && token != null) {
39-
ref.read(authDataProvider.notifier).set(AuthModel(server, token));
40-
41-
Response<Server> info = await ref
42-
.read(serverRepositoryProvider)
43-
.getInfo();
44-
if (info.isSuccessful) {
45-
Sentry.configureScope(
46-
(scope) => scope.setTag(
47-
'server.version',
48-
info.toSuccess().body.version ?? "-",
49-
),
50-
);
51-
}
52-
53-
var userResponse = await ref
54-
.read(userRepositoryProvider)
55-
.getCurrentUser();
56-
if (userResponse.isSuccessful) {
57-
ref
58-
.read(currentUserProvider.notifier)
59-
.set(userResponse.toSuccess().body);
60-
61-
globalNavigatorKey.currentState?.pushReplacementNamed("/home");
62-
} else if (userResponse.isError) {
63-
if (userResponse.toError().statusCode == 401) {
64-
ref.read(settingsRepositoryProvider).saveUserToken(null);
65-
66-
ScaffoldMessenger.of(ref.context).showSnackBar(
67-
SnackBar(
68-
content: Text(
69-
AppLocalizations.of(ref.context).loginExpiredMessage,
70-
),
71-
),
72-
);
43+
return checkServer(ref, server, token);
44+
}
7345

74-
globalNavigatorKey.currentState?.pushReplacementNamed("/login");
75-
} else {
76-
return userResponse.toError().error["message"];
77-
}
78-
} else {
79-
return userResponse.toException().exception;
80-
}
46+
globalNavigatorKey.currentState?.pushReplacementNamed("/login");
47+
return null;
48+
}
49+
50+
Future<Object?> checkServer(
51+
WidgetRef ref,
52+
String server,
53+
String token,
54+
) async {
55+
ref.read(authDataProvider.notifier).set(AuthModel(server, token));
56+
57+
Version? serverVersion;
58+
59+
Response<Server> info = await ref.read(serverRepositoryProvider).getInfo();
60+
if (info.isSuccessful) {
61+
Sentry.configureScope(
62+
(scope) => scope.setTag(
63+
'server.version',
64+
info.toSuccess().body.version ?? "-",
65+
),
66+
);
67+
68+
serverVersion = Version.fromServerString(
69+
info.toSuccess().body.version ?? "-",
70+
);
71+
}
72+
73+
return checkUser(ref, serverVersion);
74+
}
75+
76+
Future<Object?> checkUser(WidgetRef ref, Version? serverVersion) async {
77+
var userResponse = await ref.read(userRepositoryProvider).getCurrentUser();
78+
if (userResponse.isSuccessful) {
79+
ref.read(currentUserProvider.notifier).set(userResponse.toSuccess().body);
80+
81+
onLoginSuccess(ref, serverVersion);
82+
} else if (userResponse.isError) {
83+
onLoginError(ref, userResponse.toError());
8184
} else {
82-
globalNavigatorKey.currentState?.pushReplacementNamed("/login");
85+
return userResponse.toException().exception;
8386
}
8487

8588
return null;
8689
}
90+
91+
Future<void> onLoginSuccess(WidgetRef ref, Version? serverVersion) async {
92+
if (serverVersion != null && serverVersion != supportedServerVersion) {
93+
await showDialog<void>(
94+
context: ref.context,
95+
barrierDismissible: false,
96+
builder: (BuildContext context) {
97+
return VersionMismatchDialog(serverVersion: serverVersion);
98+
},
99+
);
100+
}
101+
102+
globalNavigatorKey.currentState?.pushReplacementNamed("/home");
103+
}
104+
105+
Future<Object?> onLoginError(
106+
WidgetRef ref,
107+
ErrorResponse<User> userResponse,
108+
) async {
109+
if (userResponse.statusCode == 401) {
110+
ref.read(settingsRepositoryProvider).saveUserToken(null);
111+
112+
ScaffoldMessenger.of(ref.context).showSnackBar(
113+
SnackBar(
114+
content: Text(AppLocalizations.of(ref.context).loginExpiredMessage),
115+
),
116+
);
117+
118+
globalNavigatorKey.currentState?.pushReplacementNamed("/login");
119+
return null;
120+
}
121+
122+
return userResponse.error["message"];
123+
}
87124
}

0 commit comments

Comments
 (0)