Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,24 @@ You can add accounts to a wallet by:
### 4. Vault Import & Export
- Vault files contain **encrypted wallets and keys** for backup/migration
- Encrypted with **AES-256** using a password provided by the user
- **Minimum password length:** 8 characters (no recovery possible if forgotten)
- **Minimum password length:** 12 characters (no recovery possible if forgotten)
- When exporting, the vault password can be the same as or different from the wallet password
- Vaults can also be imported into the **[Qubic Web Wallet](https://qubic.wallet.org)**

---

### 5. App Message
- Displays static messages fetched from the backend to inform users about maintenance periods, updates, or other announcements.
- The message can be of two types:
- Blocking: Shows a maintenance screen that prevents app use until the period ends.
- Non-blocking: Shows a one-time informational dialog that can be dismissed.
- Each message includes metadata such as:
- Target platform (ios, android, macos, or all)
- Validity period (startDate, endDate)
- Blocking flag (true or false)

---

## RPC Communication

Qubic Wallet interacts with the network using the following RPC endpoints:
Expand Down
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
63 changes: 63 additions & 0 deletions lib/dtos/app_message_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
class AppMessageDto {
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;
final bool disabled;

AppMessageDto({
required this.id,
required this.title,
required this.message,
required this.blocking,
required this.platform,
this.startDate,
this.endDate,
this.disabled = false,
});

factory AppMessageDto.fromJson(Map<String, dynamic> json) {
return AppMessageDto(
id: json['id'],
title: json['title'] ?? '',
message: json['message'] ?? '',
blocking: json['blocking'] ?? false,
platform: json['platform'] ?? 'all',
startDate:
json['startDate'] == null ? null : _parseDate(json['startDate']),
endDate: json['endDate'] == null ? null : _parseDate(json['endDate']),
disabled: json['disabled'] ?? false,
);
}
Copy link
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
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) && !disabled;
}

static DateTime? _parseDate(dynamic value) {
if (value == null) return null;
try {
return DateTime.parse(value).toUtc();
} catch (_) {
return null;
}
}
}
3 changes: 2 additions & 1 deletion lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -838,5 +838,6 @@
"revealSeedCopiedToClipboardMessage": "Privater Seed wurde vorübergehend in die Zwischenablage kopiert (für 1 Minute gespeichert)",
"revealSeedWarningTitle": "Teile deinen privaten Seed nicht!",
"revealSeedWarningDescription": "Wenn jemand deinen privaten Seed hat, hat er vollständige Kontrolle über dein Konto",
"blockedScreenshotWarning": "Screenshot geschützt — dieser Bildschirm bleibt privat."
"blockedScreenshotWarning": "Screenshot geschützt — dieser Bildschirm bleibt privat.",
"maintenanceRefreshButton": "Aktualisieren"
}
3 changes: 2 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -901,5 +901,6 @@
"revealSeedCopiedToClipboardMessage": "Private seed temporarily copied to clipboard (stored for 1 minute)",
"revealSeedWarningTitle": "Do not share your Private Seed!",
"revealSeedWarningDescription": "If someone has your private seed, they will have full control of your account",
"blockedScreenshotWarning": "Screenshot protected — this screen is private."
"blockedScreenshotWarning": "Screenshot protected — this screen is private.",
"maintenanceRefreshButton": "Refresh"
}
3 changes: 2 additions & 1 deletion lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -837,5 +837,6 @@
"revealSeedCopiedToClipboardMessage": "La semilla privada se copió temporalmente (por 1 minuto) al portapapeles",
"revealSeedWarningTitle": "¡No compartas tu semilla privada!",
"revealSeedWarningDescription": "Si alguien tiene tu semilla privada, tendrá control total de tu cuenta",
"blockedScreenshotWarning": "Captura protegida: esta pantalla es privada."
"blockedScreenshotWarning": "Captura protegida: esta pantalla es privada.",
"maintenanceRefreshButton": "Actualizar"
}
3 changes: 2 additions & 1 deletion lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -843,5 +843,6 @@
"revealSeedCopiedToClipboardMessage": "La phrase secrète a été copiée temporairement (pendant 1 minute) dans le presse-papiers ",
"revealSeedWarningTitle": "Ne partagez pas votre phrase secrète!",
"revealSeedWarningDescription": "Si quelqu’un possède votre phrase secrète, il aura un contrôle total sur votre compte",
"blockedScreenshotWarning": "Capture protégée — cet écran reste privé."
"blockedScreenshotWarning": "Capture protégée — cet écran reste privé.",
"maintenanceRefreshButton": "Rafraîchir"
}
3 changes: 2 additions & 1 deletion lib/l10n/app_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -838,5 +838,6 @@
"revealSeedCopiedToClipboardMessage": "Приватная сид-фраза временно скопирована в буфер обмена (хранится 1 минуту)",
"revealSeedWarningTitle": "Никогда не делитесь своей приватной сид-фразой!",
"revealSeedWarningDescription": "Если кто-то получит вашу приватную сид-фразу, он получит полный доступ к вашему аккаунту",
"blockedScreenshotWarning": "Скриншот защищён — экран остаётся приватным."
"blockedScreenshotWarning": "Скриншот защищён — экран остаётся приватным.",
"maintenanceRefreshButton": "Обновить"
}
3 changes: 2 additions & 1 deletion lib/l10n/app_tr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -838,5 +838,6 @@
"revealSeedCopiedToClipboardMessage": "Özel anahtar geçici olarak panoya kopyalandı (1 dakika saklanır)",
"revealSeedWarningTitle": "Özel Anahtarınızı Paylaşmayın!",
"revealSeedWarningDescription": "Birisi özel anahtarınıza sahipse, hesabınız üzerinde tam kontrol sahibi olur",
"blockedScreenshotWarning": "Ekran görüntüsü korundu — bu ekran gizli kalır."
"blockedScreenshotWarning": "Ekran görüntüsü korundu — bu ekran gizli kalır.",
"maintenanceRefreshButton": "Yenile"
}
5 changes: 3 additions & 2 deletions lib/l10n/app_vi.arb
Original file line number Diff line number Diff line change
Expand Up @@ -901,5 +901,6 @@
"revealSeedCopiedToClipboardMessage": "Seed riêng tư đã được sao chép tạm thời vào clipboard (được lưu trong 1 phút)",
"revealSeedWarningTitle": "Đừng chia sẻ seed riêng tư của bạn!",
"revealSeedWarningDescription": "Nếu ai đó có seed riêng tư của bạn, họ sẽ có toàn quyền kiểm soát tài khoản của bạn",
"blockedScreenshotWarning": "Chụp màn hình được bảo vệ — màn hình này là riêng tư."
}
"blockedScreenshotWarning": "Chụp màn hình được bảo vệ — màn hình này là riêng tư.",
"maintenanceRefreshButton": "Làm mới"
}
3 changes: 2 additions & 1 deletion lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -838,5 +838,6 @@
"revealSeedCopiedToClipboardMessage": "私密助记词已临时复制到剪贴板(保存 1 分钟)",
"revealSeedWarningTitle": "请勿泄露您的私密助记词!",
"revealSeedWarningDescription": "如果他人获取了您的私密助记词,他们将完全控制您的账户",
"blockedScreenshotWarning": "螢幕截圖已保護,此畫面保持隱私。"
"blockedScreenshotWarning": "螢幕截圖已保護,此畫面保持隱私。",
"maintenanceRefreshButton": "重新整理"
}
71 changes: 71 additions & 0 deletions lib/pages/main/maintenance_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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/l10n/l10n.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 AppMessageDto? appMessage;
const MaintenanceScreen({super.key, this.appMessage});

@override
Widget build(BuildContext context) {
final l10n = l10nOf(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 ? "" : l10n.maintenanceRefreshButton,
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
Loading