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
35 changes: 22 additions & 13 deletions mobile-app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -325,21 +325,30 @@
}
}
},
"accountsSheetSubheaderTransparent": "Transparent Accounts",
"@accountsSheetSubheaderTransparent": {
"description": "Sub-segment header for transparent accounts"
"accountsScreenActiveWallet": "Active Wallet",
"@accountsScreenActiveWallet": {
"description": "Badge on the wallet header containing the active account; rendered uppercase"
},
"accountsSheetSubheaderEncrypted": "Encrypted Account",
"@accountsSheetSubheaderEncrypted": {
"description": "Sub-segment header for the encrypted account"
"accountsScreenAccountCount": "{count, plural, =1{1 Account} other{{count} Accounts}}",
"@accountsScreenAccountCount": {
"description": "Account count on a collapsed wallet row",
"placeholders": {
"count": {
"type": "int"
}
}
},
"walletNameTitle": "Wallet Name",
"@walletNameTitle": {
"description": "App bar title of the wallet naming screen"
},
"accountsSheetSubheaderKeystone": "Keystone Accounts",
"@accountsSheetSubheaderKeystone": {
"description": "Sub-segment header for keystone accounts"
"walletNameSubtitle": "Naming your wallets makes it easier to tell their accounts apart. Each wallet has its own encrypted account.",
"@walletNameSubtitle": {
"description": "Explainer text on the wallet naming screen"
},
"accountsSheetSubheaderMultisig": "Multisig Accounts",
"@accountsSheetSubheaderMultisig": {
"description": "Sub-segment header for multisig accounts"
"walletNameHint": "Enter a name for your wallet",
"@walletNameHint": {
"description": "Placeholder of the wallet name field"
},

"addAccountMenuTitle": "Add Account",
Expand All @@ -358,7 +367,7 @@
"@addAccountMenuMoreTitle": {
"description": "Title of the add account more options action sheet"
},
"addAccountMenuImportKeystoneTitle": "Add Keystone Account",
"addAccountMenuImportKeystoneTitle": "Add Keystone Wallet",
"@addAccountMenuImportKeystoneTitle": {
"description": "Add keystone hardware account menu row title"
},
Expand Down
11 changes: 6 additions & 5 deletions mobile-app/lib/l10n/app_id.arb
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,17 @@
"accountsSheetBalance": "{balance} {symbol}",
"accountsSheetWallet": "Wallet {number}",
"accountsSheetKeystoneWallet": "{number, plural, =1{Wallet Perangkat Keras Keystone} other{Wallet Perangkat Keras Keystone {number}}}",
"accountsSheetSubheaderTransparent": "Akun Transparan",
"accountsSheetSubheaderEncrypted": "Akun Terenkripsi",
"accountsSheetSubheaderKeystone": "Akun Keystone",
"accountsSheetSubheaderMultisig": "Akun Multisig",
"accountsScreenActiveWallet": "Wallet Aktif",
"accountsScreenAccountCount": "{count, plural, =1{1 Akun} other{{count} Akun}}",
"walletNameTitle": "Nama Wallet",
"walletNameSubtitle": "Memberi nama wallet memudahkan Anda membedakan akun-akunnya. Setiap wallet memiliki akun terenkripsi sendiri.",
"walletNameHint": "Masukkan nama untuk wallet Anda",

"addAccountMenuTitle": "Tambah Akun",
"addAccountMenuCreateTitle": "Tambah Akun Transparan",
"addAccountMenuCreateSubtitle": "Tambahkan akun publik lainnya",
"addAccountMenuMoreTitle": "Lanjutan",
"addAccountMenuImportKeystoneTitle": "Tambah Akun Keystone",
"addAccountMenuImportKeystoneTitle": "Tambah Wallet Keystone",
"addAccountMenuImportKeystoneSubtitle": "Penandatanganan air-gap melalui kode QR",
"addAccountMenuImportTitle": "Impor Akun",
"addAccountMenuImportSubtitle": "Pulihkan dari recovery phrase",
Expand Down
32 changes: 19 additions & 13 deletions mobile-app/lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -494,29 +494,35 @@ abstract class AppLocalizations {
/// **'{number, plural, =1{Keystone Hardware Wallet} other{Keystone Hardware Wallet {number}}}'**
String accountsSheetKeystoneWallet(int number);

/// Sub-segment header for transparent accounts
/// Badge on the wallet header containing the active account; rendered uppercase
///
/// In en, this message translates to:
/// **'Transparent Accounts'**
String get accountsSheetSubheaderTransparent;
/// **'Active Wallet'**
String get accountsScreenActiveWallet;

/// Sub-segment header for the encrypted account
/// Account count on a collapsed wallet row
///
/// In en, this message translates to:
/// **'Encrypted Account'**
String get accountsSheetSubheaderEncrypted;
/// **'{count, plural, =1{1 Account} other{{count} Accounts}}'**
String accountsScreenAccountCount(int count);

/// Sub-segment header for keystone accounts
/// App bar title of the wallet naming screen
///
/// In en, this message translates to:
/// **'Keystone Accounts'**
String get accountsSheetSubheaderKeystone;
/// **'Wallet Name'**
String get walletNameTitle;

/// Sub-segment header for multisig accounts
/// Explainer text on the wallet naming screen
///
/// In en, this message translates to:
/// **'Multisig Accounts'**
String get accountsSheetSubheaderMultisig;
/// **'Naming your wallets makes it easier to tell their accounts apart. Each wallet has its own encrypted account.'**
String get walletNameSubtitle;

/// Placeholder of the wallet name field
///
/// In en, this message translates to:
/// **'Enter a name for your wallet'**
String get walletNameHint;

/// App bar title on add account menu
///
Expand Down Expand Up @@ -545,7 +551,7 @@ abstract class AppLocalizations {
/// Add keystone hardware account menu row title
///
/// In en, this message translates to:
/// **'Add Keystone Account'**
/// **'Add Keystone Wallet'**
String get addAccountMenuImportKeystoneTitle;

/// Add keystone hardware account menu row subtitle
Expand Down
17 changes: 12 additions & 5 deletions mobile-app/lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -255,16 +255,23 @@ class AppLocalizationsEn extends AppLocalizations {
}

@override
String get accountsSheetSubheaderTransparent => 'Transparent Accounts';
String get accountsScreenActiveWallet => 'Active Wallet';

@override
String get accountsSheetSubheaderEncrypted => 'Encrypted Account';
String accountsScreenAccountCount(int count) {
String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count Accounts', one: '1 Account');
return '$_temp0';
}

@override
String get walletNameTitle => 'Wallet Name';

@override
String get accountsSheetSubheaderKeystone => 'Keystone Accounts';
String get walletNameSubtitle =>
'Naming your wallets makes it easier to tell their accounts apart. Each wallet has its own encrypted account.';

@override
String get accountsSheetSubheaderMultisig => 'Multisig Accounts';
String get walletNameHint => 'Enter a name for your wallet';

@override
String get addAccountMenuTitle => 'Add Account';
Expand All @@ -279,7 +286,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get addAccountMenuMoreTitle => 'Advanced';

@override
String get addAccountMenuImportKeystoneTitle => 'Add Keystone Account';
String get addAccountMenuImportKeystoneTitle => 'Add Keystone Wallet';

@override
String get addAccountMenuImportKeystoneSubtitle => 'Air-gapped signing via QR code';
Expand Down
17 changes: 12 additions & 5 deletions mobile-app/lib/l10n/app_localizations_id.dart
Original file line number Diff line number Diff line change
Expand Up @@ -250,16 +250,23 @@ class AppLocalizationsId extends AppLocalizations {
}

@override
String get accountsSheetSubheaderTransparent => 'Akun Transparan';
String get accountsScreenActiveWallet => 'Wallet Aktif';

@override
String get accountsSheetSubheaderEncrypted => 'Akun Terenkripsi';
String accountsScreenAccountCount(int count) {
String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count Akun', one: '1 Akun');
return '$_temp0';
}

@override
String get walletNameTitle => 'Nama Wallet';

@override
String get accountsSheetSubheaderKeystone => 'Akun Keystone';
String get walletNameSubtitle =>
'Memberi nama wallet memudahkan Anda membedakan akun-akunnya. Setiap wallet memiliki akun terenkripsi sendiri.';

@override
String get accountsSheetSubheaderMultisig => 'Akun Multisig';
String get walletNameHint => 'Masukkan nama untuk wallet Anda';

@override
String get addAccountMenuTitle => 'Tambah Akun';
Expand All @@ -274,7 +281,7 @@ class AppLocalizationsId extends AppLocalizations {
String get addAccountMenuMoreTitle => 'Lanjutan';

@override
String get addAccountMenuImportKeystoneTitle => 'Tambah Akun Keystone';
String get addAccountMenuImportKeystoneTitle => 'Tambah Wallet Keystone';

@override
String get addAccountMenuImportKeystoneSubtitle => 'Penandatanganan air-gap melalui kode QR';
Expand Down
6 changes: 6 additions & 0 deletions mobile-app/lib/providers/wallet_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,12 @@ final walletOriginProvider = Provider.family<WalletOrigin?, int>((ref, walletInd
return ref.watch(settingsServiceProvider).getWalletOrigin(walletIndex);
});

/// Optional user-set wallet name; null falls back to "Wallet {n}" display copy.
/// Invalidate after [SettingsService.setWalletName].
final walletNameProvider = Provider.family<String?, int>((ref, walletIndex) {
return ref.watch(settingsServiceProvider).getWalletName(walletIndex);
});

/// 0.0001 tokens in smallest units; dust below this doesn't warrant a backup nudge.
final _backupNudgeBalanceThreshold = BigInt.from(10).pow(AppConstants.decimals - 4);

Expand Down
141 changes: 65 additions & 76 deletions mobile-app/lib/shared/utils/accounts_grouping.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,54 @@ import 'package:quantus_sdk/quantus_sdk.dart';

enum WalletKind { software, keystone }

enum AccountSegment { transparent, encrypted, keystone, multisig }

sealed class AccountListItem {
const AccountListItem();
}

/// A wallet group header, e.g. "Wallet 1" or "Keystone Hardware Wallet 1".
/// [number] is the 1-based position within its [kind].
class WalletHeaderItem extends AccountListItem {
/// One wallet as displayed on the Accounts screen: its transparent (or
/// keystone) accounts, the single encrypted account (software wallets only),
/// and any multisigs owned by one of its accounts. [number] is the 1-based
/// position within its [kind], used for "Wallet {n}" fallback naming.
class WalletGroup {
final int walletIndex;
final WalletKind kind;
final int number;
const WalletHeaderItem({required this.kind, required this.number});
}

class SegmentHeaderItem extends AccountListItem {
final AccountSegment segment;
const SegmentHeaderItem(this.segment);
final List<Account> accounts;
final Account? encryptedAccount;
final List<MultisigAccount> multisigs;

const WalletGroup({
required this.walletIndex,
required this.kind,
required this.number,
required this.accounts,
required this.encryptedAccount,
required this.multisigs,
});

int get accountCount => accounts.length + (encryptedAccount == null ? 0 : 1) + multisigs.length;

bool contains(String accountId) =>
accounts.any((a) => a.accountId == accountId) ||
encryptedAccount?.accountId == accountId ||
multisigs.any((m) => m.accountId == accountId);
}

class AccountRowItem extends AccountListItem {
final BaseAccount account;
const AccountRowItem(this.account);
/// Result of grouping accounts for the Accounts screen. The first wallet is
/// the active wallet (falling back to the lowest software walletIndex);
/// multisigs whose member account is unknown trail in [standaloneMultisigs].
class WalletsGrouping {
final List<WalletGroup> wallets;
final List<MultisigAccount> standaloneMultisigs;
const WalletsGrouping({required this.wallets, required this.standaloneMultisigs});
}

/// Result of grouping accounts for the Accounts popup. When [segmented] is
/// false, [items] is a flat list of [AccountRowItem] with no headers.
class AccountsGrouping {
final bool segmented;
final List<AccountListItem> items;
const AccountsGrouping({required this.segmented, required this.items});
}

/// Pure mapping of accounts + multisigs to an ordered, segmented list.
///
/// Order: software wallets (by walletIndex) then keystone wallets (by
/// walletIndex). Within each wallet, sub-segments appear in order and are
/// skipped when empty: transparent, encrypted (software) / keystone (hardware),
/// then multisig. Multisigs are owned by the wallet of the account matching
/// [MultisigAccount.myMemberAccountId]; unresolved ones trail in a standalone
/// multisig segment. A single software wallet of only transparent accounts with
/// no multisigs renders flat (no headers).
AccountsGrouping groupAccounts({required List<Account> accounts, required List<MultisigAccount> multisigs}) {
/// Pure mapping of accounts + multisigs to wallet groups, ordered software
/// wallets (by walletIndex) then keystone wallets (by walletIndex). The wallet
/// containing [activeAccountId] moves to the front; the rest keep their order
/// and their "Wallet {n}" numbering. Multisigs are owned by the wallet of the
/// account matching [MultisigAccount.myMemberAccountId].
WalletsGrouping groupWallets({
required List<Account> accounts,
required List<MultisigAccount> multisigs,
String? activeAccountId,
}) {
final byWallet = <int, List<Account>>{};
for (final a in accounts) {
byWallet.putIfAbsent(a.walletIndex, () => []).add(a);
Expand Down Expand Up @@ -72,58 +78,41 @@ AccountsGrouping groupAccounts({required List<Account> accounts, required List<M
softwareIndices.sort();
keystoneIndices.sort();

final atMostOneSoftwareWallet = softwareIndices.length <= 1 && keystoneIndices.isEmpty;
final hasEncrypted = accounts.any((a) => a.accountType == AccountType.encrypted);
final segmented = !(atMostOneSoftwareWallet && !hasEncrypted && multisigs.isEmpty);

if (!segmented) {
final flat = [...accounts]..sort(_compareAccounts);
return AccountsGrouping(segmented: false, items: [for (final a in flat) AccountRowItem(a)]);
}

final items = <AccountListItem>[];

void addSegment(AccountSegment segment, List<BaseAccount> rows) {
if (rows.isEmpty) return;
items.add(SegmentHeaderItem(segment));
items.addAll(rows.map(AccountRowItem.new));
}

void addWallet(WalletKind kind, int number, int walletIndex) {
WalletGroup buildGroup(WalletKind kind, int number, int walletIndex) {
final group = byWallet[walletIndex] ?? [];
final regular = group.where((a) => a.accountType != AccountType.encrypted).toList()..sort(_compareAccounts);
final encrypted = group.where((a) => a.accountType == AccountType.encrypted).toList()..sort(_compareAccounts);
final msigs = [...?multisigsByWallet[walletIndex]]..sort(_compareMultisigs);
items.add(WalletHeaderItem(kind: kind, number: number));

if (kind == WalletKind.software) {
final transparent = group.where((a) => a.accountType != AccountType.encrypted).toList()..sort(_compareAccounts);
final encrypted = group.where((a) => a.accountType == AccountType.encrypted).toList()..sort(_compareAccounts);
addSegment(AccountSegment.transparent, transparent);
addSegment(AccountSegment.encrypted, encrypted);
} else {
final keystone = [...group]..sort(_compareAccounts);
addSegment(AccountSegment.keystone, keystone);
}
addSegment(AccountSegment.multisig, msigs);
return WalletGroup(
walletIndex: walletIndex,
kind: kind,
number: number,
accounts: regular,
encryptedAccount: encrypted.isEmpty ? null : encrypted.first,
multisigs: msigs,
);
}

for (var i = 0; i < softwareIndices.length; i++) {
addWallet(WalletKind.software, i + 1, softwareIndices[i]);
}
for (var i = 0; i < keystoneIndices.length; i++) {
addWallet(WalletKind.keystone, i + 1, keystoneIndices[i]);
}
final wallets = <WalletGroup>[
for (var i = 0; i < softwareIndices.length; i++) buildGroup(WalletKind.software, i + 1, softwareIndices[i]),
for (var i = 0; i < keystoneIndices.length; i++) buildGroup(WalletKind.keystone, i + 1, keystoneIndices[i]),
];

final standalone = [...standaloneMultisigs]..sort(_compareMultisigs);
addSegment(AccountSegment.multisig, standalone);
if (activeAccountId != null) {
final activePos = wallets.indexWhere((w) => w.contains(activeAccountId));
if (activePos > 0) {
wallets.insert(0, wallets.removeAt(activePos));
}
}

return AccountsGrouping(segmented: true, items: items);
return WalletsGrouping(wallets: wallets, standaloneMultisigs: [...standaloneMultisigs]..sort(_compareMultisigs));
}

bool _isKeystoneWallet(List<Account> group) =>
group.isNotEmpty && group.every((a) => a.accountType == AccountType.keystone);

/// 1-based display number of the software wallet at [walletIndex], matching the
/// numbering used by [groupAccounts]. Returns null when [walletIndex] is not a
/// numbering used by [groupWallets]. Returns null when [walletIndex] is not a
/// software wallet.
int? softwareWalletNumber(List<Account> accounts, int walletIndex) {
final byWallet = <int, List<Account>>{};
Expand Down
Loading
Loading