-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## Description This PR introduces force update flow on version mismatch ## Additional Notes Versions config re-fetch every 8 hours and on app launch Showing of Update App modal on versions mismatch Support of all platforms Re-fetch config and recheck versions/hide popup after app update ## Type of Change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [x] Refactoring - [ ] Documentation - [ ] Chore ## Screenshots (if applicable) <img width="180" alt="Screenshot 2024-12-24 at 09 58 10" src="https://github.com/user-attachments/assets/5c2d7e28-d98b-43ad-8e9e-6e1a3eb20e32" />
- Loading branch information
1 parent
fcabfc7
commit 642161a
Showing
12 changed files
with
292 additions
and
47 deletions.
There are no files selected for viewing
36 changes: 36 additions & 0 deletions
36
lib/app/components/app_update_handler/hooks/use_app_update.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// SPDX-License-Identifier: ice License 1.0 | ||
|
||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_hooks/flutter_hooks.dart'; | ||
import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||
import 'package:ion/app/features/config/providers/force_update_provider.c.dart'; | ||
import 'package:ion/app/features/core/model/app_update_type.dart'; | ||
import 'package:ion/app/features/core/views/pages/app_update_modal.dart'; | ||
import 'package:ion/app/router/app_routes.c.dart'; | ||
import 'package:ion/app/router/utils/show_simple_bottom_sheet.dart'; | ||
|
||
void useAppUpdate(WidgetRef ref) { | ||
final isUpdateModalVisible = useState(false); | ||
final forceUpdateState = ref.watch(forceUpdateProvider); | ||
|
||
useEffect( | ||
() { | ||
if (forceUpdateState.shouldShowUpdateModal && !isUpdateModalVisible.value) { | ||
isUpdateModalVisible.value = true; | ||
|
||
showSimpleBottomSheet<void>( | ||
isDismissible: false, | ||
context: rootNavigatorKey.currentContext!, | ||
child: const AppUpdateModal( | ||
appUpdateType: AppUpdateType.updateRequired, | ||
), | ||
); | ||
} else if (!forceUpdateState.shouldShowUpdateModal && isUpdateModalVisible.value) { | ||
Navigator.of(rootNavigatorKey.currentContext!).pop(); | ||
isUpdateModalVisible.value = false; | ||
} | ||
return null; | ||
}, | ||
[forceUpdateState], | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// SPDX-License-Identifier: ice License 1.0 | ||
|
||
import 'dart:io'; | ||
|
||
import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||
import 'package:ion/app/exceptions/exceptions.dart'; | ||
import 'package:ion/app/features/core/providers/dio_provider.c.dart'; | ||
import 'package:ion/app/features/core/providers/env_provider.c.dart'; | ||
import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||
|
||
part 'config_provider.c.g.dart'; | ||
|
||
enum ConfigName { | ||
requiredAndroidAppVersion, | ||
requiredIosAppVersion, | ||
requiredMacosAppVersion, | ||
requiredWindowsAppVersion, | ||
requiredLinuxAppVersion; | ||
|
||
@override | ||
String toString() => switch (this) { | ||
ConfigName.requiredAndroidAppVersion => 'required_android_app_version', | ||
ConfigName.requiredIosAppVersion => 'required_ios_app_version', | ||
ConfigName.requiredMacosAppVersion => 'required_macos_app_version', | ||
ConfigName.requiredWindowsAppVersion => 'required_windows_app_version', | ||
ConfigName.requiredLinuxAppVersion => 'required_linux_app_version', | ||
}; | ||
} | ||
|
||
@riverpod | ||
Future<String> configForPlatform(Ref ref) async { | ||
final dio = ref.watch(dioProvider); | ||
|
||
ConfigName getPlatformConfigName() { | ||
if (Platform.isAndroid) { | ||
return ConfigName.requiredAndroidAppVersion; | ||
} else if (Platform.isIOS) { | ||
return ConfigName.requiredIosAppVersion; | ||
} else if (Platform.isMacOS) { | ||
return ConfigName.requiredMacosAppVersion; | ||
} else if (Platform.isWindows) { | ||
return ConfigName.requiredWindowsAppVersion; | ||
} else if (Platform.isLinux) { | ||
return ConfigName.requiredLinuxAppVersion; | ||
} else { | ||
throw ConfigPlatformNotSupportException(); | ||
} | ||
} | ||
|
||
final baseUrl = ref.watch(envProvider.notifier).get<String>(EnvVariable.ION_ORIGIN); | ||
final configName = getPlatformConfigName(); | ||
|
||
final path = '$baseUrl/v1/config/$configName'; | ||
|
||
try { | ||
final response = await dio.get<String>(path); | ||
if (response.data == null) { | ||
throw ForceUpdateFetchConfigException(); | ||
} | ||
return response.data!; | ||
} catch (e) { | ||
throw ForceUpdateFetchConfigException(); | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
lib/app/features/config/providers/force_update_last_sync_date_provider.c.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// SPDX-License-Identifier: ice License 1.0 | ||
|
||
import 'package:ion/app/services/storage/local_storage.c.dart'; | ||
import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||
|
||
part 'force_update_last_sync_date_provider.c.g.dart'; | ||
|
||
@Riverpod(keepAlive: true) | ||
class ForceUpdateLastSyncDateNotifier extends _$ForceUpdateLastSyncDateNotifier { | ||
static const String forceUpdateLastSyncDateKey = 'forceUpdateLastSyncDateKey'; | ||
|
||
@override | ||
DateTime? build() { | ||
final storedDate = ref.watch(localStorageProvider).getString(forceUpdateLastSyncDateKey); | ||
if (storedDate != null) { | ||
return DateTime.parse(storedDate); | ||
} | ||
return null; | ||
} | ||
|
||
void updateLastSyncDate(DateTime date) { | ||
state = date; | ||
ref.read(localStorageProvider).setString(forceUpdateLastSyncDateKey, date.toIso8601String()); | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
lib/app/features/config/providers/force_update_provider.c.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// SPDX-License-Identifier: ice License 1.0 | ||
|
||
import 'dart:async'; | ||
|
||
import 'package:flutter/material.dart'; | ||
import 'package:freezed_annotation/freezed_annotation.dart'; | ||
import 'package:ion/app/features/config/providers/config_provider.c.dart'; | ||
import 'package:ion/app/features/config/providers/force_update_last_sync_date_provider.c.dart'; | ||
import 'package:ion/app/features/config/providers/force_update_util_provider.c.dart'; | ||
import 'package:ion/app/features/core/providers/app_lifecycle_provider.c.dart'; | ||
import 'package:ion/app/features/core/providers/env_provider.c.dart'; | ||
import 'package:package_info_plus/package_info_plus.dart'; | ||
import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||
|
||
part 'force_update_provider.c.freezed.dart'; | ||
part 'force_update_provider.c.g.dart'; | ||
|
||
@freezed | ||
class ForceUpdateState with _$ForceUpdateState { | ||
const factory ForceUpdateState({ | ||
@Default(false) bool shouldShowUpdateModal, | ||
}) = _ForceUpdateState; | ||
|
||
const ForceUpdateState._(); | ||
} | ||
|
||
@Riverpod(keepAlive: true) | ||
class ForceUpdate extends _$ForceUpdate { | ||
@override | ||
ForceUpdateState build() { | ||
ref.listen<AppLifecycleState>( | ||
appLifecycleProvider, | ||
(previous, next) { | ||
if (next == AppLifecycleState.resumed) { | ||
_checkAndUpdateConfig(); | ||
} | ||
}, | ||
fireImmediately: true, | ||
); | ||
|
||
return const ForceUpdateState(); | ||
} | ||
|
||
Future<void> _checkAndUpdateConfig() async { | ||
await ref.read(envProvider.future); | ||
|
||
final refetchIntervalInMilliseconds = | ||
ref.read(envProvider.notifier).get<int>(EnvVariable.VERSIONS_CONFIG_REFETCH_INTERVAL); | ||
|
||
final lastSyncDate = ref.read(forceUpdateLastSyncDateNotifierProvider); | ||
|
||
if (lastSyncDate == null || | ||
DateTime.now().difference(lastSyncDate).inMilliseconds >= refetchIntervalInMilliseconds) { | ||
final remoteVersion = await ref.read(configForPlatformProvider.future); | ||
|
||
final packageInfo = await PackageInfo.fromPlatform(); | ||
final localVersion = packageInfo.version; | ||
|
||
if (localVersion == remoteVersion) { | ||
_updateState(showUpdateModal: false); | ||
|
||
ref | ||
.read(forceUpdateLastSyncDateNotifierProvider.notifier) | ||
.updateLastSyncDate(DateTime.now()); | ||
} else if (ref | ||
.read(forceUpdateServiceProvider) | ||
.isVersionOutdated(localVersion, remoteVersion)) { | ||
_updateState(showUpdateModal: true); | ||
} | ||
} | ||
} | ||
|
||
void _updateState({required bool showUpdateModal}) { | ||
state = state.copyWith(shouldShowUpdateModal: showUpdateModal); | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
lib/app/features/config/providers/force_update_util_provider.c.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// SPDX-License-Identifier: ice License 1.0 | ||
|
||
import 'dart:io'; | ||
|
||
import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||
import 'package:ion/app/exceptions/exceptions.dart'; | ||
import 'package:ion/app/features/core/providers/env_provider.c.dart'; | ||
import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||
import 'package:url_launcher/url_launcher.dart'; | ||
|
||
part 'force_update_util_provider.c.g.dart'; | ||
|
||
class ForceUpdateUtil { | ||
const ForceUpdateUtil(this.env); | ||
|
||
final Env env; | ||
|
||
Future<void> handleForceUpdateRedirect() async { | ||
if (Platform.isAndroid) { | ||
final androidAppId = env.get<String>(EnvVariable.ION_ANDROID_APP_ID); | ||
final androidUrl = 'https://play.google.com/store/apps/details?id=$androidAppId'; | ||
await _openUrl(androidUrl); | ||
} else if (Platform.isIOS) { | ||
final iosAppId = env.get<String>(EnvVariable.ION_IOS_APP_ID); | ||
final iosUrl = 'https://apps.apple.com/app/id$iosAppId'; | ||
await _openUrl(iosUrl); | ||
} else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { | ||
const fallbackWebsite = 'https://example.com'; //TODO: Replace with the actual website | ||
await _openUrl(fallbackWebsite); | ||
} | ||
} | ||
|
||
Future<void> _openUrl(String url) async { | ||
final uri = Uri.parse(url); | ||
if (await canLaunchUrl(uri)) { | ||
await launchUrl(uri, mode: LaunchMode.externalApplication); | ||
} else { | ||
throw ForceUpdateCouldntLaunchUrlException(url: url); | ||
} | ||
} | ||
|
||
bool isVersionOutdated(String localVersion, String remoteVersion) { | ||
final localParts = localVersion.split('.').map(int.parse).toList(); | ||
final remoteParts = remoteVersion.split('.').map(int.parse).toList(); | ||
|
||
for (var i = 0; i < remoteParts.length; i++) { | ||
if (localParts.length <= i || localParts[i] < remoteParts[i]) { | ||
return true; | ||
} else if (localParts[i] > remoteParts[i]) { | ||
return false; | ||
} | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
@riverpod | ||
ForceUpdateUtil forceUpdateService(Ref ref) { | ||
return ForceUpdateUtil(ref.watch(envProvider.notifier)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.