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
8 changes: 4 additions & 4 deletions lib/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ abstract class Config {
/// Returns a supported language code, falling back to English if not supported
/// Uses the app's l10n configuration automatically - no manual maintenance needed
static String getSupportedLocale(String locale) {
final supportedLocaleCodes = AppLocalizations.supportedLocales
.map((l) => l.languageCode)
.toList();
final supportedLocaleCodes =
AppLocalizations.supportedLocales.map((l) => l.languageCode).toList();
return supportedLocaleCodes.contains(locale) ? locale : 'en';
}

// Qubic static API endpoints
// Wallet-app specific endpoints (can be versioned independently in future)
static const dapps = "/wallet-app/dapps/dapps.json";
static dappLocale(String locale) => "/wallet-app/dapps/locales/$locale.json";
static const appVersionCheck = "/wallet-app/version-check.json";

// General/ecosystem endpoints (shared across Qubic ecosystem)
static const smartContracts = "/general/data/smart_contracts.json";
Expand Down Expand Up @@ -96,7 +96,7 @@ abstract class Config {
static const String proxyIP = '192.168.1.1'; // Replace with actual proxy IP
static const int proxyPort = 8888; // Replace with actual proxy port
static const DeviceIntegrityResponse deviceIntegrityResponse =
DeviceIntegrityResponse.restrict;
DeviceIntegrityResponse.none;

// Configuration for Wallet Connect
static const walletConnectProjectId = "b2ace378845f0e4806ef23d2732f77a4";
Expand Down
2 changes: 2 additions & 0 deletions lib/di.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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/wallet_connect_service.dart';
import 'package:qubic_wallet/stores/app_update_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 @@ -42,6 +43,7 @@ Future<void> setupDI() async {
await getIt<SecureStorage>().initialize();
getIt.registerSingleton<HiveStorage>(HiveStorage());
await getIt<HiveStorage>().initialize();
getIt.registerSingleton<AppUpdateStore>(AppUpdateStore());

//Providers
getIt.registerSingleton<GlobalSnackBar>(GlobalSnackBar());
Expand Down
12 changes: 11 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -961,5 +961,15 @@
"type": "String"
}
}
}
},
"updateRequiredTitle": "Update Required",
"updateRequiredMessage": "A new version of Qubic Wallet is required to continue. Please update to the latest version.",
"updateAvailableTitle": "Update Available",
"updateAvailableMessage": "A new version of Qubic Wallet is available with improvements and bug fixes.",
"updateButton": "Update Now",
"laterButton": "Remind Me Later",
"ignoreVersionButton": "Skip This Version",
"updateScreenCurrentVersion": "Current Version",
"updateScreenNewVersion": "New Version",
"updateScreenWhatsNew": "What's New"
}
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:qubic_wallet/stores/application_store.dart';
import 'package:qubic_wallet/stores/wallet_content_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/app_update_store.dart';
import 'package:qubic_wallet/styles/button_styles.dart';
import 'package:universal_platform/universal_platform.dart';

Expand All @@ -41,6 +42,7 @@ Future<void> main() async {
getIt.get<SettingsStore>().setBuildNumber(packageInfo.buildNumber);

getIt.get<ApplicationStore>().checkWalletIsInitialized();
await getIt.get<AppUpdateStore>().checkForUpdate();
} catch (e) {
appLogger.e(e.toString());
}
Expand Down
80 changes: 80 additions & 0 deletions lib/models/app_version_check_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:universal_platform/universal_platform.dart';

enum UpdateType {
force,
flexible,
none,
}

class AppVersionCheckModel {
final String version;
final String? releaseNotes;
final bool showLaterButton;
final bool showIgnoreButton;
final Map<String, String> updateUrls;
final List<String> platforms;

static const List<String> _defaultPlatforms = [
'android',
'ios',
];

static const Map<String, String> _defaultUpdateUrls = {
'ios': 'https://apps.apple.com/app/qubic-wallet/id6502265811',
'android': 'https://play.google.com/store/apps/details?id=org.qubic.wallet',
};

AppVersionCheckModel({
required this.version,
this.releaseNotes,
this.showLaterButton = false,
this.showIgnoreButton = false,
this.updateUrls = _defaultUpdateUrls,
this.platforms = _defaultPlatforms,
});

/// Derive update type from button visibility:
/// - If both buttons are hidden -> force update
/// - If any button is visible -> flexible update
UpdateType get updateType {
if (!showLaterButton && !showIgnoreButton) {
return UpdateType.force;
}
return UpdateType.flexible;
}

static AppVersionCheckModel? fromJson(Map<String, dynamic>? json) {
if (json == null || json.isEmpty) {
return null;
}

final updateUrls = json['update_urls'] != null
? Map<String, String>.from(json['update_urls'])
: _defaultUpdateUrls;

final platforms = json['platforms'] != null
? List<String>.from(json['platforms'])
: _defaultPlatforms;

return AppVersionCheckModel(
version: json['version'],
releaseNotes: json['release_notes'],
showLaterButton: json['show_later_button'] ?? false,
showIgnoreButton: json['show_ignore_button'] ?? false,
updateUrls: updateUrls,
platforms: platforms,
);
}

bool isApplicableForCurrentPlatform() {
if (UniversalPlatform.isIOS) return platforms.contains('ios');
if (UniversalPlatform.isAndroid) return platforms.contains('android');
return false;
}

String? getUpdateUrlForPlatform() {
if (UniversalPlatform.isIOS) return updateUrls['ios'];
if (UniversalPlatform.isAndroid) return updateUrls['android'];
return null;
}
}
99 changes: 99 additions & 0 deletions lib/pages/update/app_update_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:go_router/go_router.dart';
import 'package:qubic_wallet/di.dart';
import 'package:qubic_wallet/flutter_flow/theme_paddings.dart';
import 'package:qubic_wallet/helpers/app_logger.dart';
import 'package:qubic_wallet/l10n/l10n.dart';
import 'package:qubic_wallet/models/app_version_check_model.dart';
import 'package:qubic_wallet/stores/app_update_store.dart';
import 'package:qubic_wallet/stores/settings_store.dart';
import 'package:qubic_wallet/styles/text_styles.dart';
import 'package:qubic_wallet/styles/themed_controls.dart';
import 'package:url_launcher/url_launcher_string.dart';

part 'components/app_update_logo.dart';
part 'components/app_update_header.dart';
part 'components/app_update_info_card.dart';
part 'components/app_update_buttons.dart';

class AppUpdateScreen extends StatelessWidget {
const AppUpdateScreen({super.key});

@override
Widget build(BuildContext context) {
final appUpdateStore = getIt<AppUpdateStore>();
final settingsStore = getIt<SettingsStore>();

return Observer(
builder: (context) {
final versionInfo = appUpdateStore.currentVersionInfo;

if (versionInfo == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
context.go('/');
}
});
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

final isForceUpdate = versionInfo.updateType == UpdateType.force;

return Scaffold(
backgroundColor: LightThemeColors.background,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(ThemePaddings.hugePadding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Spacer(),
const _AppUpdateLogo(),
_AppUpdateHeader(isForceUpdate: isForceUpdate),
ThemedControls.spacerVerticalBig(),
_AppUpdateInfoCard(
versionInfo: versionInfo,
currentVersion: settingsStore.versionInfo,
onUpdatePressed: () => _launchUpdateUrl(versionInfo),
),
const Spacer(),
_AppUpdateButtons(
versionInfo: versionInfo,
onUpdatePressed: () => _launchUpdateUrl(versionInfo),
onLaterPressed: () {
appUpdateStore.handleLaterAction();
context.go('/');
},
onIgnorePressed: () {
appUpdateStore.handleIgnoreAction(versionInfo.version);
context.go('/');
},
),
],
),
),
),
);
},
);
}

Future<void> _launchUpdateUrl(AppVersionCheckModel versionInfo) async {
final url = versionInfo.getUpdateUrlForPlatform();
if (url == null) {
appLogger.e('[AppUpdateScreen] No update URL available for platform');
return;
}

try {
await launchUrlString(url, mode: LaunchMode.externalApplication);
} catch (e) {
appLogger.e('[AppUpdateScreen] Failed to launch update URL: $e');
return;
}
}
}
82 changes: 82 additions & 0 deletions lib/pages/update/components/app_update_buttons.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
part of '../app_update_screen.dart';

class _AppUpdateButtons extends StatelessWidget {
final AppVersionCheckModel versionInfo;
final VoidCallback onUpdatePressed;
final VoidCallback onLaterPressed;
final VoidCallback onIgnorePressed;

const _AppUpdateButtons({
required this.versionInfo,
required this.onUpdatePressed,
required this.onLaterPressed,
required this.onIgnorePressed,
});

bool get _hasAllOptions =>
versionInfo.showLaterButton && versionInfo.showIgnoreButton;

@override
Widget build(BuildContext context) {
final l10n = l10nOf(context);

// Scenario 1: Force update - only Update button at bottom
if (versionInfo.updateType == UpdateType.force) {
return ThemedControls.primaryButtonBig(
onPressed: onUpdatePressed,
text: l10n.updateButton,
);
}

final showLater = versionInfo.showLaterButton;
final showIgnore = versionInfo.showIgnoreButton;

// Scenario 3: All three options - Update in card, Later & Skip in row
if (_hasAllOptions) {
return Row(
children: [
Expanded(
child: ThemedControls.transparentButtonNormal(
onPressed: onLaterPressed,
text: l10n.laterButton,
),
),
ThemedControls.spacerHorizontalNormal(),
Expanded(
child: ThemedControls.dangerButtonBigWithClild(
onPressed: onIgnorePressed,
child: Text(
l10n.ignoreVersionButton,
style: TextStyles.destructiveButtonText,
),
),
),
],
);
}

// Scenario 2: Two options - Update first, then the other option
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ThemedControls.primaryButtonBig(
onPressed: onUpdatePressed,
text: l10n.updateButton,
),
if (showLater)
ThemedControls.transparentButtonNormal(
onPressed: onLaterPressed,
text: l10n.laterButton,
),
if (showIgnore)
ThemedControls.dangerButtonBigWithClild(
onPressed: onIgnorePressed,
child: Text(
l10n.ignoreVersionButton,
style: TextStyles.destructiveButtonText,
),
),
],
);
}
}
35 changes: 35 additions & 0 deletions lib/pages/update/components/app_update_header.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
part of '../app_update_screen.dart';

class _AppUpdateHeader extends StatelessWidget {
final bool isForceUpdate;

const _AppUpdateHeader({required this.isForceUpdate});

@override
Widget build(BuildContext context) {
final l10n = l10nOf(context);

return Column(
children: [
Text(
isForceUpdate ? l10n.updateRequiredTitle : l10n.updateAvailableTitle,
style: TextStyles.textEnormous.copyWith(
fontWeight: FontWeight.bold,
color: LightThemeColors.primary,
),
textAlign: TextAlign.center,
),
ThemedControls.spacerVerticalBig(),
Text(
isForceUpdate
? l10n.updateRequiredMessage
: l10n.updateAvailableMessage,
style: TextStyles.textNormal.copyWith(
color: LightThemeColors.textColorSecondary,
),
textAlign: TextAlign.center,
),
],
);
}
}
Loading