Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
64435a4
feat(wallet): restrict wallet name characters
CharlVS Jun 17, 2025
5790d28
Merge branch 'dev' into codex/add-wallet-name-validation
CharlVS Jul 8, 2025
03b2ca0
feat(wallet): allow hyphens in names (#2889)
CharlVS Jul 8, 2025
1a6f92d
Merge branch 'dev' of https://github.com/KomodoPlatform/komodo-wallet…
CharlVS Jul 21, 2025
2647aac
fix(merge): misc merge fixes
CharlVS Jul 21, 2025
85d9f80
Merge branch 'dev' into codex/add-wallet-name-validation
takenagain Sep 22, 2025
4b301ef
fix(wallet-file-import): remove legacy wallet name validation
takenagain Sep 22, 2025
af834a7
feat(wallets): add unicode alphanumeric wallet name support
takenagain Sep 22, 2025
c46c133
refactor(wallet-rename): replace deprecated dispatcher with appdialog
takenagain Sep 23, 2025
42333c0
Merge remote-tracking branch 'origin/dev' into codex/add-wallet-name-…
takenagain Sep 24, 2025
81943dc
fix(screenshot-sensitivity): notify listeners in postFrameCallback
takenagain Sep 24, 2025
053e73d
fix(wallet-import): defer wallet file name validation to click handler
takenagain Sep 24, 2025
510b1bc
Refactor wallet name validation and renaming logic
cursoragent Sep 24, 2025
d5b3285
fix(ci): unblock Android release build by including integration_test …
CharlVS Sep 25, 2025
56e60fe
chore: update SDK submodule to latest dev commit
CharlVS Sep 25, 2025
b3225e0
refactor(wallets-manager): fetch wallet names on demand; remove pre-c…
CharlVS Sep 25, 2025
8ad4598
refactor(wallets-manager): remove no-op initState overrides in import…
CharlVS Sep 25, 2025
8286b75
chore(sdk): update submodule to latest dev
CharlVS Sep 25, 2025
31c5203
refactor: fix minor context warning
CharlVS Sep 25, 2025
1706574
fix(wallets): avoid mutating Wallet instances during login and export…
CharlVS Sep 25, 2025
e162ca3
fix: check both validation and uniqueness before prompting for wallet…
CharlVS Sep 25, 2025
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
7 changes: 5 additions & 2 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"walletCreationTitle": "Create wallet",
"walletImportTitle": "Import wallet",
"walletImportByFileTitle": "Importing seed phrase file",
"invalidWalletNameError": "Invalid wallet name, please remove special chars",
"invalidWalletNameError": "Invalid wallet name. Allowed: letters, numbers, spaces, underscores (_), hyphens (-)",
"invalidWalletFileNameError": "Invalid filename, please rename it to remove special chars",
"walletImportCreatePasswordTitle": "Create a password for \"{}\" wallet",
"walletImportByFileDescription": "Create a password of your seed phrase file to decrypt it. This password will be used to log in to your wallet",
Expand All @@ -127,9 +127,12 @@
"walletCreationUploadFile": "Upload seed phrase file",
"walletCreationEmptySeedError": "Seed phrase should not be empty",
"walletCreationExistNameError": "Wallet name exists",
"walletCreationNameLengthError": "Name length should be between 1 and 40",
"walletCreationNameLengthError": "Name must be 1–40 characters with no leading or trailing spaces",
"walletCreationFormatPasswordError": "Password must contain at least 8 characters, with at least one digit, one lower-case, one upper-case and one special symbol. The password can't contain the same character 3 times in a row. The password can't contain the word 'password'",
"walletCreationConfirmPasswordError": "Your passwords do not match. Please try again.",
"walletCreationNameCharactersError": "Name can contain letters, numbers, spaces, underscores (_), and hyphens (-)",
"renameWalletDescription": "Wallet name is invalid. Please enter a new name.",
"renameWalletConfirm": "Rename",
"incorrectPassword": "Incorrect password",
"oneClickLogin": "Quick Login",
"quickLoginTooltip": "Tip: Save your password to your device's password manager for true one-click login.",
Expand Down
89 changes: 71 additions & 18 deletions lib/blocs/wallets_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ class WalletsRepository {
final FileLoader _fileLoader;

List<Wallet>? _cachedWallets;
List<Wallet>? _cachedLegacyWallets;
List<Wallet>? get wallets => _cachedWallets;
bool get isCacheLoaded =>
_cachedWallets != null && _cachedLegacyWallets != null;

Future<List<Wallet>> getWallets() async {
final legacyWallets = await _getLegacyWallets();
Expand All @@ -44,6 +47,7 @@ class WalletsRepository {
!wallet.name.toLowerCase().startsWith(trezorWalletNamePrefix),
)
.toList();
_cachedLegacyWallets = legacyWallets;
return [..._cachedWallets!, ...legacyWallets];
}

Expand Down Expand Up @@ -99,33 +103,41 @@ class WalletsRepository {

String? validateWalletName(String name) {
// Disallow special characters except letters, digits, space, underscore and hyphen
if (RegExp(r'[^\w\- ]').hasMatch(name)) {
if (RegExp(r'[^\p{L}\p{M}\p{N}\s\-_]', unicode: true).hasMatch(name)) {
return LocaleKeys.invalidWalletNameError.tr();
}
// This shouldn't happen, but just in case.
if (_cachedWallets == null) {
getWallets().ignore();
return null;
}

final trimmedName = name.trim();

// Check if the trimmed name is empty (prevents space-only names)
if (trimmedName.isEmpty) {
// Reject leading/trailing spaces explicitly to avoid confusion/duplicates
if (trimmedName != name) {
return LocaleKeys.walletCreationNameLengthError.tr();
}

// Check if trimmed name exceeds length limit
if (trimmedName.length > 40) {
// Check empty and length limits on trimmed input
if (trimmedName.isEmpty || trimmedName.length > 40) {
return LocaleKeys.walletCreationNameLengthError.tr();
}

// Check for duplicates using the exact input name (not trimmed)
// This preserves backward compatibility with existing wallets that might have spaces
if (_cachedWallets!.firstWhereOrNull((w) => w.name == name) != null) {
return LocaleKeys.walletCreationExistNameError.tr();
}
return null;
}

/// Async uniqueness check: verifies that no existing wallet (SDK or legacy)
/// has the same trimmed name. Returns a localized error string if taken,
/// or null if available or if wallets can't be loaded.
Future<String?> validateWalletNameUniqueness(String name) async {
final String trimmedName = name.trim();
try {
final List<Wallet> allWallets = await getWallets();
final bool taken =
allWallets.firstWhereOrNull((w) => w.name.trim() == trimmedName) !=
null;
if (taken) {
return LocaleKeys.walletCreationExistNameError.tr();
}
} catch (_) {
// Non-blocking on failure to fetch wallets; treat as no conflict found.
}
return null;
}

Expand All @@ -141,19 +153,23 @@ class WalletsRepository {
@Deprecated('Use the KomodoDefiSdk.auth.getMnemonicEncrypted method instead.')
Future<void> downloadEncryptedWallet(Wallet wallet, String password) async {
try {
Wallet workingWallet = wallet.copy();
if (wallet.config.seedPhrase.isEmpty) {
final mnemonic = await _kdfSdk.auth.getMnemonicPlainText(password);
wallet.config.seedPhrase = await _encryptionTool.encryptData(
final String encryptedSeed = await _encryptionTool.encryptData(
password,
mnemonic.plaintextMnemonic ?? '',
);
workingWallet = workingWallet.copyWith(
config: workingWallet.config.copyWith(seedPhrase: encryptedSeed),
);
}
final String data = jsonEncode(wallet.config);
final String data = jsonEncode(workingWallet.config);
final String encryptedData = await _encryptionTool.encryptData(
password,
data,
);
final String sanitizedFileName = _sanitizeFileName(wallet.name);
final String sanitizedFileName = _sanitizeFileName(workingWallet.name);
await _fileLoader.save(
fileName: sanitizedFileName,
data: encryptedData,
Expand All @@ -167,4 +183,41 @@ class WalletsRepository {
String _sanitizeFileName(String fileName) {
return fileName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
}

Future<void> renameLegacyWallet({
required String walletId,
required String newName,
}) async {
final String trimmed = newName.trim();
// Persist to legacy storage
final List<Map<String, dynamic>> rawLegacyWallets =
(await _legacyWalletStorage.read(allWalletsStorageKey) as List?)
?.cast<Map<String, dynamic>>() ??
[];
bool updated = false;
for (int i = 0; i < rawLegacyWallets.length; i++) {
final Map<String, dynamic> data = rawLegacyWallets[i];
if ((data['id'] as String? ?? '') == walletId) {
data['name'] = trimmed;
rawLegacyWallets[i] = data;
updated = true;
break;
}
}
if (updated) {
await _legacyWalletStorage.write(allWalletsStorageKey, rawLegacyWallets);
}

// Update in-memory legacy cache if available
if (_cachedLegacyWallets != null) {
final index = _cachedLegacyWallets!.indexWhere(
(element) => element.id == walletId,
);
if (index != -1) {
_cachedLegacyWallets![index] = _cachedLegacyWallets![index].copyWith(
name: trimmed,
);
}
}
}
}
9 changes: 5 additions & 4 deletions lib/generated/codegen_loader.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,11 @@ abstract class LocaleKeys {
static const walletCreationEmptySeedError = 'walletCreationEmptySeedError';
static const walletCreationExistNameError = 'walletCreationExistNameError';
static const walletCreationNameLengthError = 'walletCreationNameLengthError';
static const walletCreationFormatPasswordError =
'walletCreationFormatPasswordError';
static const walletCreationConfirmPasswordError =
'walletCreationConfirmPasswordError';
static const walletCreationFormatPasswordError = 'walletCreationFormatPasswordError';
static const walletCreationConfirmPasswordError = 'walletCreationConfirmPasswordError';
static const walletCreationNameCharactersError = 'walletCreationNameCharactersError';
static const renameWalletDescription = 'renameWalletDescription';
static const renameWalletConfirm = 'renameWalletConfirm';
static const incorrectPassword = 'incorrectPassword';
static const oneClickLogin = 'oneClickLogin';
static const quickLoginTooltip = 'quickLoginTooltip';
Expand Down
45 changes: 35 additions & 10 deletions lib/shared/screenshot/screenshot_sensitivity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,48 @@ class ScreenshotSensitivityController extends ChangeNotifier {

void enter() {
_depth += 1;
notifyListeners();
_safeNotifyListeners();
}

void exit() {
if (_depth > 0) {
_depth -= 1;
notifyListeners();
_safeNotifyListeners();
}
}

/// Safely notify listeners, avoiding calls during widget tree locked phases
/// and calling build during a build or dismount.
void _safeNotifyListeners() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (hasListeners) {
notifyListeners();
}
});
}
}

/// Inherited notifier providing access to the ScreenshotSensitivityController.
class ScreenshotSensitivity extends InheritedNotifier<ScreenshotSensitivityController> {
class ScreenshotSensitivity
extends InheritedNotifier<ScreenshotSensitivityController> {
const ScreenshotSensitivity({
super.key,
required ScreenshotSensitivityController controller,
required Widget child,
}) : super(notifier: controller, child: child);
required super.child,
}) : super(notifier: controller);

static ScreenshotSensitivityController? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ScreenshotSensitivity>()?.notifier;
return context
.dependOnInheritedWidgetOfExactType<ScreenshotSensitivity>()
?.notifier;
}

static ScreenshotSensitivityController of(BuildContext context) {
final controller = maybeOf(context);
assert(controller != null, 'ScreenshotSensitivity not found in widget tree');
assert(
controller != null,
'ScreenshotSensitivity not found in widget tree',
);
return controller!;
}
}
Expand All @@ -51,21 +67,31 @@ class ScreenshotSensitive extends StatefulWidget {

class _ScreenshotSensitiveState extends State<ScreenshotSensitive> {
ScreenshotSensitivityController? _controller;
bool _hasCalledEnter = false;

@override
void didChangeDependencies() {
super.didChangeDependencies();
final controller = ScreenshotSensitivity.maybeOf(context);
if (!identical(controller, _controller)) {
_controller?.exit();
// Exit the old controller if we were using it
if (_hasCalledEnter) {
_controller?.exit();
}
_controller = controller;
_hasCalledEnter = false;
// Enter the new controller - this is safe now due to deferred notification
_controller?.enter();
_hasCalledEnter = true;
}
}

@override
void dispose() {
_controller?.exit();
// Exit the controller - this is safe now due to deferred notification
if (_hasCalledEnter) {
_controller?.exit();
}
super.dispose();
}

Expand All @@ -77,4 +103,3 @@ extension ScreenshotSensitivityContextExt on BuildContext {
bool get isScreenshotSensitive =>
ScreenshotSensitivity.maybeOf(this)?.isSensitive ?? false;
}

Loading
Loading