Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions assets/icons/maintenance.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion lib/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ abstract class Config {
static const useNativeSnackbar = false;

static const URL_WebExplorer = "https://explorer.qubic.org";

static const networkQubicMainnet = "Qubic Mainnet";

static const qubicStaticApiUrl =
"https://raw.githubusercontent.com/ahmed-tarek-salem/qubic-appcast-test/refs/heads/main";
static const qubicStaticMessages = "/message.json";

//Qubic Helper Utilities
static final qubicHelper = QubicHelperConfig(
win64: QubicHelperConfigEntry(
Expand Down
13 changes: 9 additions & 4 deletions lib/di.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import 'package:app_links/app_links.dart';
import 'package:get_it/get_it.dart';
import 'package:qubic_wallet/services/screenshot_service.dart';
import 'package:qubic_wallet/services/qr_scanner_service.dart';
import 'package:qubic_wallet/helpers/global_snack_bar.dart';
import 'package:qubic_wallet/models/wallet_connect/wallet_connect_modals_controller.dart';
import 'package:qubic_wallet/resources/apis/archive/qubic_archive_api.dart';
import 'package:qubic_wallet/resources/apis/live/qubic_live_api.dart';
import 'package:qubic_wallet/resources/apis/qubic_helpers_api.dart';
import 'package:qubic_wallet/resources/apis/static/qubic_static_api.dart';
import 'package:qubic_wallet/resources/apis/stats/qubic_stats_api.dart';
import 'package:qubic_wallet/resources/hive_storage.dart';
import 'package:qubic_wallet/resources/qubic_cmd.dart';
import 'package:qubic_wallet/resources/secure_storage.dart';
import 'package:qubic_wallet/services/biometric_service.dart';
import 'package:qubic_wallet/services/qr_scanner_service.dart';
import 'package:qubic_wallet/services/screenshot_service.dart';
import 'package:qubic_wallet/services/wallet_connect_service.dart';
import 'package:qubic_wallet/stores/app_message_store.dart';
import 'package:qubic_wallet/stores/application_store.dart';
import 'package:qubic_wallet/stores/dapp_store.dart';
import 'package:qubic_wallet/stores/network_store.dart';
import 'package:qubic_wallet/stores/root_jailbreak_flag_store.dart';
import 'package:qubic_wallet/stores/settings_store.dart';
import 'package:qubic_wallet/stores/dapp_store.dart';
import 'package:qubic_wallet/timed_controller.dart';
import 'package:qubic_wallet/resources/apis/qubic_helpers_api.dart';

final GetIt getIt = GetIt.instance;

Expand All @@ -31,11 +33,14 @@ Future<void> setupDI() async {
QubicArchiveApi(getIt<NetworkStore>()));
getIt.registerSingleton<QubicStatsApi>(QubicStatsApi(getIt<NetworkStore>()));
getIt.registerSingleton<QubicHelpersApi>(QubicHelpersApi());
getIt.registerSingleton<QubicStaticApi>(QubicStaticApi());

//Stores
getIt.registerSingleton<ApplicationStore>(ApplicationStore());
getIt.registerSingleton<SettingsStore>(SettingsStore());
getIt.registerSingleton<DappStore>(DappStore());
getIt.registerSingleton<AppMessageStore>(AppMessageStore());

getIt.registerSingleton<SecureStorage>(SecureStorage());
await getIt<SecureStorage>().initialize();
getIt.registerSingleton<HiveStorage>(HiveStorage());
Expand Down
59 changes: 59 additions & 0 deletions lib/dtos/app_message_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class AppMessageModel {
final String? id;
final String title;
final String message;
final bool blocking;
final String platform; // e.g. "all", "ios", "android"
final DateTime? startDate;
final DateTime? endDate;

AppMessageModel({
required this.id,
required this.title,
required this.message,
required this.blocking,
required this.platform,
this.startDate,
this.endDate,
});

factory AppMessageModel.fromJson(Map<String, dynamic> json) {
return AppMessageModel(
id: json['id'],
title: json['title'] ?? '',
message: json['message'] ?? '',
blocking: json['blocking'] ?? false,
platform: json['platform'] ?? 'all',
startDate: _parseDate(json['start_date']),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahmed-tarek-salem can we use startDate and endDate instead?
I think we should prefer this format as it could be mapped automatically to object properties.

endDate: _parseDate(json['end_date']),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahmed-tarek-salem how localised message would be handled?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahmed-tarek-salem

  • can we also add the possibility to define the message for specific os version?
    i.e android_os_version: <13
    "Since beginning of next year, our app will only run on devices with Android 13 or upper."
  • can we also add a disabled true/false field to just disable easily a message. Default value would be false, meaning if not present, is assumed is enabled.


/// Check if the message is currently active
bool get isActive {
final now = DateTime.now().toUtc();
if (startDate != null && now.isBefore(startDate!)) return false;
if (endDate != null && now.isAfter(endDate!)) return false;
return true;
}

/// Check if message applies to current platform
bool appliesToPlatform(String currentPlatform) {
final lowerPlatform = platform.toLowerCase();
return lowerPlatform == 'all' ||
lowerPlatform == currentPlatform.toLowerCase();
}

bool isValid(String currentPlatform) {
return isActive && appliesToPlatform(currentPlatform);
}

static DateTime? _parseDate(dynamic value) {
if (value == null) return null;
try {
return DateTime.parse(value).toUtc();
} catch (_) {
return null;
}
}
}
69 changes: 69 additions & 0 deletions lib/pages/main/maintenance_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:qubic_wallet/di.dart';
import 'package:qubic_wallet/dtos/app_message_dto.dart';
import 'package:qubic_wallet/flutter_flow/theme_paddings.dart';
import 'package:qubic_wallet/stores/app_message_store.dart';
import 'package:qubic_wallet/styles/app_icons.dart';
import 'package:qubic_wallet/styles/text_styles.dart';
import 'package:qubic_wallet/styles/themed_controls.dart';

class MaintenanceScreen extends StatelessWidget {
final AppMessageModel? appMessage;
const MaintenanceScreen({super.key, this.appMessage});

@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
child: Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: ThemePaddings.normalPadding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
AppIcons.maintenance,
height: 100,
colorFilter: const ColorFilter.mode(
LightThemeColors.primary40, BlendMode.srcIn),
),
ThemedControls.spacerVerticalNormal(),
Text(appMessage?.title ?? "",
style: TextStyles.alertHeader, textAlign: TextAlign.center),
ThemedControls.spacerVerticalSmall(),
Text(appMessage?.message ?? "",
style: TextStyles.alertText, textAlign: TextAlign.center),
ThemedControls.spacerVerticalNormal(),
Observer(builder: (context) {
bool isLoading = getIt<AppMessageStore>().isLoading;
return ThemedControls.primaryButtonSmall(
onPressed: () async {
final navigator = Navigator.of(context);
final message =
await getIt<AppMessageStore>().getAppMessage();
if (message == null) {
navigator.pop();
}
},
text: isLoading ? "" : "Refresh",
Comment thread
ahmed-tarek-salem marked this conversation as resolved.
Outdated
icon: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: LightThemeColors.background),
)
: null);
}),
],
),
)),
),
);
}
}
28 changes: 28 additions & 0 deletions lib/pages/main/tab_wallet_contents.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import 'package:qubic_wallet/flutter_flow/theme_paddings.dart';
import 'package:qubic_wallet/helpers/app_logger.dart';
import 'package:qubic_wallet/helpers/show_alert_dialog.dart';
import 'package:qubic_wallet/l10n/l10n.dart';
import 'package:qubic_wallet/pages/main/maintenance_screen.dart';
import 'package:qubic_wallet/pages/main/wallet_contents/add_account_modal_bottom_sheet.dart';
import 'package:qubic_wallet/pages/main/wallet_contents/add_wallet_connect/add_wallet_connect.dart';
import 'package:qubic_wallet/stores/app_message_store.dart';
import 'package:qubic_wallet/stores/application_store.dart';
import 'package:qubic_wallet/stores/network_store.dart';
import 'package:qubic_wallet/stores/root_jailbreak_flag_store.dart';
Expand Down Expand Up @@ -67,6 +69,7 @@ class _TabWalletContentsState extends State<TabWalletContents> {
}
}
});
checkAppMessage();

_scrollController.addListener(() {
if (_scrollController.offset > sliverExpanded) {
Expand Down Expand Up @@ -150,6 +153,30 @@ class _TabWalletContentsState extends State<TabWalletContents> {
appStore.triggerAddAccountModal();
}

void checkAppMessage() async {
final message = await getIt<AppMessageStore>().getAppMessage();
if (message == null || !mounted) return;
if (message.blocking) {
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
pushScreenWithoutNavBar(
context, MaintenanceScreen(appMessage: message));
}
});
} else {
showDialog(
context: context,
builder: (context) {
return getAlertDialog(
message.title,
message.message,
primaryButtonLabel: l10nWrapper.l10n!.generalButtonOK,
primaryButtonFunction: () => Navigator.of(context).pop(),
);
});
}
}

@override
Widget build(BuildContext context) {
final l10n = l10nOf(context);
Expand All @@ -159,6 +186,7 @@ class _TabWalletContentsState extends State<TabWalletContents> {
edgeOffset: kToolbarHeight,
onRefresh: () async {
await _timedController.interruptFetchTimer();
checkAppMessage();
},
backgroundColor: LightThemeColors.refreshIndicatorBackground,
child: Container(
Expand Down
27 changes: 27 additions & 0 deletions lib/resources/apis/static/qubic_static_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:qubic_wallet/config.dart';
import 'package:qubic_wallet/dtos/app_message_dto.dart';
import 'package:qubic_wallet/models/app_error.dart';
import 'package:qubic_wallet/services/dio_client.dart';

class QubicStaticApi {
late Dio _dio;

QubicStaticApi() {
_dio = DioClient.getDio(baseUrl: Config.qubicStaticApiUrl);
}

Future<AppMessageModel?> getAppMessage() async {
try {
final response = await _dio.get(Config.qubicStaticMessages);
final decodedResponse = json.decode(response.data);
return decodedResponse["id"] == null
? null
: AppMessageModel.fromJson(decodedResponse);
} catch (error) {
throw ErrorHandler.handleError(error);
}
}
}
34 changes: 34 additions & 0 deletions lib/stores/app_message_store.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:mobx/mobx.dart';
import 'package:qubic_wallet/di.dart';
import 'package:qubic_wallet/dtos/app_message_dto.dart';
import 'package:qubic_wallet/helpers/app_logger.dart';
import 'package:qubic_wallet/resources/apis/static/qubic_static_api.dart';
import 'package:universal_platform/universal_platform.dart';

part 'app_message_store.g.dart';

// ignore: library_private_types_in_public_api
class AppMessageStore = _AppMessageStore with _$AppMessageStore;

abstract class _AppMessageStore with Store {
@observable
bool isLoading = false;

@action
Future<AppMessageModel?> getAppMessage() async {
isLoading = true;
try {
final message = await getIt<QubicStaticApi>().getAppMessage();
if (message != null &&
message.isValid(UniversalPlatform.operatingSystem)) {
return message;
}
return null;
} catch (e) {
appLogger.e(e);
return null;
} finally {
isLoading = false;
}
}
}
42 changes: 42 additions & 0 deletions lib/stores/app_message_store.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions lib/stores/settings_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,6 @@ abstract class _SettingsStore with Store {
await secureStorage.setWalletSettings(settings);
}

@action
Future<void> setTOTPKey(String key) async {
settings.TOTPKey = key;
settings = Settings.clone(settings);
await secureStorage.setWalletSettings(settings);
}

@action
Future<void> clearTOTPKey() async {
settings.TOTPKey = null;
settings = Settings.clone(settings);
await secureStorage.setWalletSettings(settings);
}

@action
Future<void> setAutoLockTimeout(int value) async {
settings.autoLockTimeout = value;
Expand Down
Loading