mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 20:39:51 +00:00
* Add theme base v2 * Initial new theme base files * Typos * Fixes * Update theme files * feat: Migrate to Material 3 Theming Foundation, Theme Data Refactor, and First Extension Cleanup Batch. This commit completes the first major phase of migrating to Material 3 theming by setting up the foundations for material 3 integration and begins the initial migration, removing custom theme extensions, updating theme data, and refactoring all relevant widget and page theming to use Material 3’s built-in color and typography tokens. These changes: - Lays the groundwork for Material 3 theming by restructuring the app’s theme configuration to use Material 3’s ColorScheme and TextTheme as the primary sources of color and typography throughout the app. - Refactors the core theme config files by removing legacy custom color roles ensuring all color definitions now map directly to Material 3’s role. - Begins the first batch migration of custom theme extensions (InfoTheme, PlaceholderTheme, KeyboardTheme, PinCodeTheme) and updates all affected widgets and pages to use Material 3 color and typography tokens instead of the custom properties. - Cleans up the codebase by deleting the files of the initial set of migrated extensions and eliminating all related imports and usages. * feat: Migrate to Material 3 Theming. This change: - Updates the themes README.md file to reflect the current structure and give more information based on the first major phase that was completed. * feat: Migrate to Material 3 Theming Deleting previous theme extensions * feat: Migrate to Material 3 Theming Another batch of migrations from existing extensions * feat: Migration to Material 3 Theming Third Migration batch for theme extensions * fwat: Migration to Material 3 Theming Final Migration batch for previous theme extensions * Update onboarding hero * Update button radius * Add surfaceContainer to light theme * feat(themes): Migrate to Material 3 Theming This change: - Adds new set of hero images - Modifies the core structure for the themes - Add missing color tokens to the theme classes - Adds a CustomThemeColor class for custom color tokens - Modifies the themelist to have a fall back for previous theme implementation - Adds localization for some texts - Modifies the flow for loading the theme on app start - Add a WidgetsBindingObserver that listens for changes in the device theme and updates the app theme when there is a change - Registers the themeStore as a Singleton for codebase wide use * feat(themes): Migrate to Material 3 theming This change: - Migrates UI flows across the app to the new themes - Confirms styling and typography of components across the app uses the new themes - Remove instances of Palette use - Switch TextStyles across the app to use theme text styles * feat(themes): Migrate to Material 3 Theming. This change: - Adjusts bottomsheets styling and removes duplicate close button - Removes more themedata extensions from the previous implementation * - Remove outlines from cards and dock - Update menu colors - Update padding/divider size for cards * - Update PIN screen - Fix navigation dock shadow - Update wallet screen colors * Update border radius --skip-ci * feat(themes): Migrate to Material 3 Theming. This change: - Adds gradient backgrounds to the dashbaord and balance cards. - Migrates the input fields across the app to BaseTextFormFields. - Removes dependence of input fields on individual styling, focusing instead on using theme defined InputDecoration styling with adjustments on individual components where needed. - Applies new theme styling to BaseTextFormField, AddressTextField and CurrencyAmountTextField. * - Switch some hero images to PNG - Fix nagivation_dock shadow - Minor fixes * feat: Add fallback to previous underline styling in central widgets This change: - Adds a fallback to CurrencyAmountTextField, AddressTextField, and BaseTextFormField, allowing them use the previous theme styling. - Adds localization for new texts * feat(themes): Update warning box colors for dark and light themes * feat(themes): Relaod themes when user restores from backup, ensuring the user previous theme preference is used. * feat(themes): Handle themes logic during restore from backup This change: - Refactors theme loading logic to handle backup restore edgecase - Refreshes the theme based on the user saved preference during restore from backup flow * Fix card gradients and spacing * Fix even more radiuses Test new icons for navigation_dock.dart * Update onboarding flow backgrounds Fix swap icon clipping Fix some text colors Add more hero images * Fix incorrect color for light theme * Fix more hero images and cleanup * Update text field icons Fix info box CTA colors Fix sync indicator colors * Update toggle colors Update dark theme colors (minor) Update crypto_balance_widget.dart icon * Update page transitions in router.dart Fix some colors * feat(themes): Display label by default for filled textfields * feat(themes): Refactor theme handling across various components This change: - Fixes issue with themeMode resetting to system mode when app is restarted causing a UI glitch - Updates theme checks from `currentTheme.type == ThemeType.dark` to `currentTheme.isDark` for consistency - Adjusts UI components to use the theme directly from the themeStore * feat(themes): Add animating tagline to the create pin welcome screen * Revert text fields label temporarily, fix a couple colors, and cleanup some images --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com> Co-authored-by: Blazebrain <davidadegoke16@gmail.com>
349 lines
13 KiB
Dart
349 lines
13 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:cake_wallet/core/secure_storage.dart';
|
|
import 'package:cake_wallet/entities/get_encryption_key.dart';
|
|
import 'package:cake_wallet/entities/transaction_description.dart';
|
|
import 'package:cake_wallet/themes/utils/theme_list.dart';
|
|
import 'package:cw_core/root_dir.dart';
|
|
import 'package:cake_wallet/utils/device_info.dart';
|
|
import 'package:cw_core/utils/print_verbose.dart';
|
|
import 'package:cw_core/wallet_type.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:hive/hive.dart';
|
|
import 'package:cryptography/cryptography.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:archive/archive_io.dart';
|
|
import 'package:cw_core/cake_hive.dart';
|
|
import 'package:cake_wallet/core/key_service.dart';
|
|
import 'package:cake_wallet/entities/encrypt.dart';
|
|
import 'package:cake_wallet/entities/preferences_key.dart';
|
|
import 'package:cake_wallet/entities/secret_store_key.dart';
|
|
import 'package:cw_core/wallet_info.dart';
|
|
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
|
import 'package:cake_wallet/wallet_types.g.dart';
|
|
import 'package:cake_backup/backup.dart' as cake_backup;
|
|
|
|
class $BackupService {
|
|
$BackupService(this._secureStorage, this.walletInfoSource, this.transactionDescriptionBox,
|
|
this.keyService, this.sharedPreferences)
|
|
: cipher = Cryptography.instance.chacha20Poly1305Aead(),
|
|
correctWallets = <WalletInfo>[];
|
|
|
|
static const currentVersion = _v3;
|
|
|
|
static const _v2 = 2;
|
|
static const _v3 = 3;
|
|
|
|
final Cipher cipher;
|
|
final SecureStorage _secureStorage;
|
|
final SharedPreferences sharedPreferences;
|
|
final Box<WalletInfo> walletInfoSource;
|
|
final Box<TransactionDescription> transactionDescriptionBox;
|
|
final KeyService keyService;
|
|
List<WalletInfo> correctWallets;
|
|
|
|
|
|
Future<void> importBackupV1(Uint8List data, String password, {required String nonce}) async {
|
|
final appDir = await getAppDir();
|
|
final decryptedData = await _decryptV1(data, password, nonce);
|
|
final zip = ZipDecoder().decodeBytes(decryptedData);
|
|
|
|
for (var file in zip.files) {
|
|
final filename = file.name;
|
|
|
|
if (file.isFile) {
|
|
final content = file.content as List<int>;
|
|
File('${appDir.path}/' + filename)
|
|
..createSync(recursive: true)
|
|
..writeAsBytesSync(content, flush: true);
|
|
} else {
|
|
Directory('${appDir.path}/' + filename)..create(recursive: true);
|
|
}
|
|
};
|
|
|
|
await verifyWallets();
|
|
await _importKeychainDumpV1(password, nonce: nonce);
|
|
await importPreferencesDump();
|
|
}
|
|
|
|
// checked with .endsWith - so this should be the last part of the filename
|
|
static const ignoreFiles = [
|
|
"flutter_assets/kernel_blob.bin",
|
|
"flutter_assets/vm_snapshot_data",
|
|
"flutter_assets/isolate_snapshot_data",
|
|
"README.txt",
|
|
".lock",
|
|
];
|
|
|
|
Future<void> importBackupV2(Uint8List data, String password) async {
|
|
final appDir = await getAppDir();
|
|
final decryptedData = await decryptV2(data, password);
|
|
final zip = ZipDecoder().decodeBytes(decryptedData);
|
|
|
|
outer:
|
|
for (var file in zip.files) {
|
|
final filename = file.name;
|
|
for (var ignore in ignoreFiles) {
|
|
if (filename.endsWith(ignore) && !filename.contains("wallets/")) {
|
|
printV("ignoring backup file: $filename");
|
|
continue outer;
|
|
}
|
|
}
|
|
printV("restoring: $filename");
|
|
if (file.isFile) {
|
|
final content = file.content as List<int>;
|
|
File('${appDir.path}/' + filename)
|
|
..createSync(recursive: true)
|
|
..writeAsBytesSync(content, flush: true);
|
|
} else {
|
|
final dir = Directory('${appDir.path}/' + filename);
|
|
if (!dir.existsSync()) {
|
|
dir.createSync(recursive: true);
|
|
}
|
|
}
|
|
};
|
|
|
|
await verifyWallets();
|
|
await importKeychainDumpV2(password);
|
|
await importPreferencesDump();
|
|
await importTransactionDescriptionDump(); // HiveError: Box has already been closed
|
|
}
|
|
|
|
Future<void> verifyWallets() async {
|
|
final walletInfoSource = await _reloadHiveWalletInfoBox();
|
|
correctWallets =
|
|
walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList();
|
|
|
|
if (correctWallets.isEmpty) {
|
|
throw Exception('Correct wallets not detected');
|
|
}
|
|
}
|
|
|
|
Future<Box<WalletInfo>> _reloadHiveWalletInfoBox() async {
|
|
final appDir = await getAppDir();
|
|
await CakeHive.close();
|
|
CakeHive.init(appDir.path);
|
|
|
|
if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) {
|
|
CakeHive.registerAdapter(WalletInfoAdapter());
|
|
}
|
|
|
|
return await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
|
|
}
|
|
|
|
Future<void> importTransactionDescriptionDump() async {
|
|
final appDir = await getAppDir();
|
|
final transactionDescriptionFile = File('${appDir.path}/~_transaction_descriptions_dump');
|
|
|
|
if (!transactionDescriptionFile.existsSync()) {
|
|
return;
|
|
}
|
|
|
|
final jsonData =
|
|
json.decode(transactionDescriptionFile.readAsStringSync()) as Map<String, dynamic>;
|
|
final descriptionsMap = jsonData.map((key, value) =>
|
|
MapEntry(key, TransactionDescription.fromJson(value as Map<String, dynamic>)));
|
|
var box = transactionDescriptionBox;
|
|
if (!box.isOpen) {
|
|
final transactionDescriptionsBoxKey =
|
|
await getEncryptionKey(secureStorage: _secureStorage, forKey: TransactionDescription.boxKey);
|
|
box = await CakeHive.openBox<TransactionDescription>(
|
|
TransactionDescription.boxName,
|
|
encryptionKey: transactionDescriptionsBoxKey);
|
|
}
|
|
await box.putAll(descriptionsMap);
|
|
}
|
|
|
|
Future<void> importPreferencesDump() async {
|
|
final appDir = await getAppDir();
|
|
final preferencesFile = File('${appDir.path}/~_preferences_dump');
|
|
|
|
if (!preferencesFile.existsSync()) {
|
|
return;
|
|
}
|
|
|
|
final data = json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>;
|
|
|
|
try { // shouldn't throw an error but just in case, so it doesn't stop the backup restore
|
|
for (var entry in data.entries) {
|
|
String key = entry.key;
|
|
dynamic value = entry.value;
|
|
|
|
// Check the type of the value and save accordingly
|
|
if (value is String) {
|
|
await sharedPreferences.setString(key, value);
|
|
} else if (value is int) {
|
|
await sharedPreferences.setInt(key, value);
|
|
} else if (value is double) {
|
|
await sharedPreferences.setDouble(key, value);
|
|
} else if (value is bool) {
|
|
await sharedPreferences.setBool(key, value);
|
|
} else if (value is List<String>) {
|
|
await sharedPreferences.setStringList(key, value);
|
|
} else {
|
|
if (kDebugMode) {
|
|
printV('Skipping individual save for key "$key": Unsupported type (${value.runtimeType}). Value: $value');
|
|
}
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
|
|
String currentWalletName = data[PreferencesKey.currentWalletName] as String;
|
|
int currentWalletType = data[PreferencesKey.currentWalletType] as int;
|
|
|
|
final isCorrentCurrentWallet = correctWallets
|
|
.any((info) => info.name == currentWalletName && info.type.index == currentWalletType);
|
|
|
|
if (!isCorrentCurrentWallet) {
|
|
currentWalletName = correctWallets.first.name;
|
|
currentWalletType = serializeToInt(correctWallets.first.type);
|
|
}
|
|
|
|
if (DeviceInfo.instance.isDesktop) {
|
|
await sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw);
|
|
}
|
|
|
|
await preferencesFile.delete();
|
|
}
|
|
|
|
Future<void> _importKeychainDumpV1(String password,
|
|
{required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async {
|
|
final appDir = await getAppDir();
|
|
final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
|
|
final decryptedKeychainDumpFileData =
|
|
await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce);
|
|
final keychainJSON =
|
|
json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
|
|
final keychainWalletsInfo = keychainJSON['wallets'] as List;
|
|
final decodedPin = keychainJSON['pin'] as String;
|
|
final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
|
|
final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
|
|
final backupPassword = keychainJSON[backupPasswordKey] as String;
|
|
|
|
await _secureStorage.write(key: backupPasswordKey, value: backupPassword);
|
|
|
|
keychainWalletsInfo.forEach((dynamic rawInfo) async {
|
|
final info = rawInfo as Map<String, dynamic>;
|
|
await importWalletKeychainInfo(info);
|
|
});
|
|
|
|
await _secureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
|
|
|
|
keychainDumpFile.deleteSync();
|
|
}
|
|
|
|
Future<void> importKeychainDumpV2(String password,
|
|
{String keychainSalt = secrets.backupKeychainSalt}) async {
|
|
final appDir = await getAppDir();
|
|
final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
|
|
final decryptedKeychainDumpFileData =
|
|
await decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
|
|
final keychainJSON =
|
|
json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
|
|
final keychainWalletsInfo = keychainJSON['wallets'] as List;
|
|
final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
|
|
final backupPassword = keychainJSON[backupPasswordKey] as String;
|
|
|
|
await _secureStorage.write(key: backupPasswordKey, value: backupPassword);
|
|
|
|
keychainWalletsInfo.forEach((dynamic rawInfo) async {
|
|
final info = rawInfo as Map<String, dynamic>;
|
|
await importWalletKeychainInfo(info);
|
|
});
|
|
|
|
if (keychainJSON['_all'] is Map<String, dynamic>) {
|
|
for (var key in (keychainJSON['_all'] as Map<String, dynamic>).keys) {
|
|
try {
|
|
if (!key.startsWith('MONERO_WALLET_')) continue;
|
|
final decodedPassword = decodeWalletPassword(
|
|
password: keychainJSON['_all'][key].toString());
|
|
final walletName = key.split('_WALLET_')[1];
|
|
final walletType = key.split('_WALLET_')[0].toLowerCase();
|
|
await importWalletKeychainInfo({
|
|
'name': walletName,
|
|
'type': "WalletType.$walletType",
|
|
'password': decodedPassword,
|
|
});
|
|
} catch (e) {
|
|
printV('Error importing wallet ($key) password: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
keychainDumpFile.deleteSync();
|
|
}
|
|
|
|
Future<void> importWalletKeychainInfo(Map<String, dynamic> info) async {
|
|
final name = info['name'] as String;
|
|
final password = info['password'] as String;
|
|
|
|
await keyService.saveWalletPassword(walletName: name, password: password);
|
|
}
|
|
|
|
Future<Uint8List> exportKeychainDumpV2(String password,
|
|
{String keychainSalt = secrets.backupKeychainSalt}) async {
|
|
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
|
|
final wallets = await Future.wait(walletInfoSource.values.map((walletInfo) async {
|
|
try {
|
|
return {
|
|
'name': walletInfo.name,
|
|
'type': walletInfo.type.toString(),
|
|
'password': await keyService.getWalletPassword(walletName: walletInfo.name)
|
|
};
|
|
} catch (e) {
|
|
return {
|
|
'name': walletInfo.name,
|
|
'type': walletInfo.type.toString(),
|
|
'password': ''
|
|
};
|
|
}
|
|
}));
|
|
final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
|
|
final backupPassword = await _secureStorage.read(key: backupPasswordKey);
|
|
final data = utf8.encode(
|
|
json.encode({'wallets': wallets, backupPasswordKey: backupPassword, '_all': await _secureStorage.readAll()}));
|
|
final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password');
|
|
|
|
return encrypted;
|
|
}
|
|
|
|
static const List<String> _excludedPrefsKeys = [
|
|
PreferencesKey.currentPinLength,
|
|
PreferencesKey.showCameraConsent,
|
|
PreferencesKey.lastSeenAppVersion,
|
|
PreferencesKey.failedTotpTokenTrials,
|
|
];
|
|
|
|
Future<String> exportPreferencesJSON() async {
|
|
final preferences = <String, dynamic>{};
|
|
sharedPreferences.getKeys().forEach((key) => preferences[key] = sharedPreferences.get(key));
|
|
|
|
_excludedPrefsKeys.forEach((key) => preferences.remove(key));
|
|
|
|
return json.encode(preferences);
|
|
}
|
|
|
|
int getVersion(Uint8List data) => data.toList().first;
|
|
|
|
Uint8List setVersion(Uint8List data, int version) {
|
|
final bytes = data.toList()..insert(0, version);
|
|
return Uint8List.fromList(bytes);
|
|
}
|
|
|
|
Future<Uint8List> _decryptV1(Uint8List data, String secretKeySource, String nonceBase64,
|
|
{int macLength = 16}) async {
|
|
final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource));
|
|
final secretKey = SecretKey(secretKeyHash.bytes);
|
|
final nonce = base64.decode(nonceBase64).toList();
|
|
final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(),
|
|
nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength)));
|
|
final plainData = await cipher.decrypt(box, secretKey: secretKey);
|
|
return Uint8List.fromList(plainData);
|
|
}
|
|
|
|
Future<Uint8List> _encryptV2(Uint8List data, String passphrase) async =>
|
|
cake_backup.encrypt(passphrase, data, version: _v2);
|
|
|
|
Future<Uint8List> decryptV2(Uint8List data, String passphrase) async =>
|
|
cake_backup.decrypt(passphrase, data);
|
|
}
|