CW-228 Auto generate monero subaddress (#902)

* Add UI and setting logic for subaddresses * Enable auto generate subaddresses * Rename variable * Add comment to unused code * Fix issue with initial state change * Fix observable for isAppSecure * Filter sub account contacts * Fix select account use unused address * Use add address if last address is unused * Fix auto generate wallet issues * Fix button color * Add translation and refactored naming * Fix PR review * Remove unused code * Remove unused overrides in electrum * Fix address info null check * CW-228 Fix ContactListViewModel condition * CW-228 Fix Account Tile; Rework updateAddressesInBox; Fix _getAllUnusedAddresses * CW-228 Fix unintentional address_page.dart regression * CW-228 Fix Merge Conflicts * CW-228 Add more translation Tools * CW-228 More merge conflict fixes * CW-228 Fix Merge Conflicts * CW-228 Auto Translation improvements * CW-228 Resolve requested Changes --------- Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>

Godwin Asuquo committed Aug 29, 2023 at 19:11 UTC fff5a1c41982ba449132bd20e5f6d71d2318acda
54 files changed +604 -201
cw_core/lib/address_info.dart new
+21
@@ -0,0 +1,21 @@
1 +import 'package:cw_core/hive_type_ids.dart';
2 +import 'package:hive/hive.dart';
3 +
4 +part 'address_info.g.dart';
5 +
6 +@HiveType(typeId: ADDRESS_INFO_TYPE_ID)
7 +class AddressInfo extends HiveObject {
8 + AddressInfo({required this.address, this.accountIndex, required this.label});
9 +
10 + static const typeId = ADDRESS_INFO_TYPE_ID;
11 + static const boxName = 'AddressInfo';
12 +
13 + @HiveField(0)
14 + int? accountIndex;
15 +
16 + @HiveField(1, defaultValue: '')
17 + String address;
18 +
19 + @HiveField(2, defaultValue: '')
20 + String label;
21 +}
cw_core/lib/hive_type_ids.dart
+1 -1
@@ -9,5 +9,5 @@ const EXCHANGE_TEMPLATE_TYPE_ID = 7;
9 const ORDER_TYPE_ID = 8;
10 const UNSPENT_COINS_INFO_TYPE_ID = 9;
11 const ANONPAY_INVOICE_INFO_TYPE_ID = 10;
12 -
12 +const ADDRESS_INFO_TYPE_ID = 11;
13 const ERC20_TOKEN_TYPE_ID = 12;
cw_core/lib/wallet_addresses.dart
+9 -1
@@ -1,8 +1,10 @@
1 +import 'package:cw_core/address_info.dart';
2 import 'package:cw_core/wallet_info.dart';
3
4 abstract class WalletAddresses {
5 WalletAddresses(this.walletInfo)
5 - : addressesMap = {};
6 + : addressesMap = {},
7 + addressInfos = {};
8
9 final WalletInfo walletInfo;
10
@@ -12,6 +14,10 @@ abstract class WalletAddresses {
14
15 Map<String, String> addressesMap;
16
17 + Map<int, List<AddressInfo>> addressInfos;
18 +
19 + Set<String> usedAddresses = {};
20 +
21 Future<void> init();
22
23 Future<void> updateAddressesInBox();
@@ -20,6 +26,8 @@ abstract class WalletAddresses {
26 try {
27 walletInfo.address = address;
28 walletInfo.addresses = addressesMap;
29 + walletInfo.addressInfos = addressInfos;
30 + walletInfo.usedAddresses = usedAddresses.toList();
31
32 if (walletInfo.isInBox) {
33 await walletInfo.save();
cw_core/lib/wallet_base.dart
+4
@@ -52,6 +52,10 @@ abstract class WalletBase<
52
53 late HistoryType transactionHistory;
54
55 + set isEnabledAutoGenerateSubaddress(bool value) {}
56 +
57 + bool get isEnabledAutoGenerateSubaddress => false;
58 +
59 Future<void> connectToNode({required Node node});
60
61 Future<void> startSync();
cw_core/lib/wallet_info.dart
+7
@@ -1,4 +1,5 @@
1 import 'dart:async';
2 +import 'package:cw_core/address_info.dart';
3 import 'package:cw_core/hive_type_ids.dart';
4 import 'package:cw_core/wallet_type.dart';
5 import 'package:hive/hive.dart';
@@ -72,6 +73,12 @@ class WalletInfo extends HiveObject {
73 @HiveField(13)
74 bool? showIntroCakePayCard;
75
76 + @HiveField(14)
77 + Map<int, List<AddressInfo>>? addressInfos;
78 +
79 + @HiveField(15)
80 + List<String>? usedAddresses;
81 +
82 String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
83
84 set yatLastUsedAddress(String address) {
cw_haven/lib/haven_wallet.dart
+34 -48
@@ -12,8 +12,7 @@ import 'package:cw_core/monero_wallet_utils.dart';
12 import 'package:cw_haven/api/structs/pending_transaction.dart';
13 import 'package:flutter/foundation.dart';
14 import 'package:mobx/mobx.dart';
15 -import 'package:cw_haven/api/transaction_history.dart'
16 - as haven_transaction_history;
15 +import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history;
16 //import 'package:cw_haven/wallet.dart';
17 import 'package:cw_haven/api/wallet.dart' as haven_wallet;
18 import 'package:cw_haven/api/transaction_history.dart' as transaction_history;
@@ -37,8 +36,8 @@ const moneroBlockSize = 1000;
36
37 class HavenWallet = HavenWalletBase with _$HavenWallet;
38
40 -abstract class HavenWalletBase extends WalletBase<MoneroBalance,
41 - HavenTransactionHistory, HavenTransactionInfo> with Store {
39 +abstract class HavenWalletBase
40 + extends WalletBase<MoneroBalance, HavenTransactionHistory, HavenTransactionInfo> with Store {
41 HavenWalletBase({required WalletInfo walletInfo})
42 : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)),
43 _isTransactionUpdating = false,
@@ -47,8 +46,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
46 syncStatus = NotConnectedSyncStatus(),
47 super(walletInfo) {
48 transactionHistory = HavenTransactionHistory();
50 - _onAccountChangeReaction = reaction((_) => walletAddresses.account,
51 - (Account? account) {
49 + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) {
50 if (account == null) {
51 return;
52 }
@@ -96,14 +94,12 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
94 haven_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery);
95
96 if (haven_wallet.getCurrentHeight() <= 1) {
99 - haven_wallet.setRefreshFromBlockHeight(
100 - height: walletInfo.restoreHeight);
97 + haven_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight);
98 }
99 }
100
104 - _autoSaveTimer = Timer.periodic(
105 - Duration(seconds: _autoSaveInterval),
106 - (_) async => await save());
101 + _autoSaveTimer =
102 + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
103 }
104
105 @override
@@ -115,7 +111,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
111 _onAccountChangeReaction?.reaction.dispose();
112 _autoSaveTimer?.cancel();
113 }
118 -
114 +
115 @override
116 Future<void> connectToNode({required Node node}) async {
117 try {
@@ -170,26 +166,25 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
166 }
167
168 if (hasMultiDestination) {
173 - if (outputs.any((item) => item.sendAll
174 - || (item.formattedCryptoAmount ?? 0) <= 0)) {
175 - throw HavenTransactionCreationException('You do not have enough coins to send this amount.');
169 + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
170 + throw HavenTransactionCreationException(
171 + 'You do not have enough coins to send this amount.');
172 }
173
178 - final int totalAmount = outputs.fold(0, (acc, value) =>
179 - acc + (value.formattedCryptoAmount ?? 0));
174 + final int totalAmount =
175 + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
176
177 if (unlockedBalance < totalAmount) {
182 - throw HavenTransactionCreationException('You do not have enough coins to send this amount.');
178 + throw HavenTransactionCreationException(
179 + 'You do not have enough coins to send this amount.');
180 }
181
185 - final moneroOutputs = outputs.map((output) =>
186 - MoneroOutput(
187 - address: output.address,
188 - amount: output.cryptoAmount!.replaceAll(',', '.')))
182 + final moneroOutputs = outputs
183 + .map((output) => MoneroOutput(
184 + address: output.address, amount: output.cryptoAmount!.replaceAll(',', '.')))
185 .toList();
186
191 - pendingTransactionDescription =
192 - await transaction_history.createTransactionMultDest(
187 + pendingTransactionDescription = await transaction_history.createTransactionMultDest(
188 outputs: moneroOutputs,
189 priorityRaw: _credentials.priority.serialize(),
190 accountIndex: walletAddresses.account!.id);
@@ -198,12 +193,8 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
193 final address = output.isParsedAddress && (output.extractedAddress?.isNotEmpty ?? false)
194 ? output.extractedAddress!
195 : output.address;
201 - final amount = output.sendAll
202 - ? null
203 - : output.cryptoAmount!.replaceAll(',', '.');
204 - final int? formattedAmount = output.sendAll
205 - ? null
206 - : output.formattedCryptoAmount;
196 + final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
197 + final int? formattedAmount = output.sendAll ? null : output.formattedCryptoAmount;
198
199 if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
200 (formattedAmount == null && unlockedBalance <= 0)) {
@@ -213,8 +204,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
204 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
205 }
206
216 - pendingTransactionDescription =
217 - await transaction_history.createTransaction(
207 + pendingTransactionDescription = await transaction_history.createTransaction(
208 address: address,
209 assetType: _credentials.assetType,
210 amount: amount,
@@ -307,16 +297,14 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
297 }
298
299 String getTransactionAddress(int accountIndex, int addressIndex) =>
310 - haven_wallet.getAddress(
311 - accountIndex: accountIndex,
312 - addressIndex: addressIndex);
300 + haven_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex);
301
302 @override
303 Future<Map<String, HavenTransactionInfo>> fetchTransactions() async {
304 haven_transaction_history.refreshTransactions();
317 - return _getAllTransactions(null).fold<Map<String, HavenTransactionInfo>>(
318 - <String, HavenTransactionInfo>{},
319 - (Map<String, HavenTransactionInfo> acc, HavenTransactionInfo tx) {
305 + return _getAllTransactions(null)
306 + .fold<Map<String, HavenTransactionInfo>>(<String, HavenTransactionInfo>{},
307 + (Map<String, HavenTransactionInfo> acc, HavenTransactionInfo tx) {
308 acc[tx.id] = tx;
309 return acc;
310 });
@@ -340,9 +328,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
328 }
329
330 List<HavenTransactionInfo> _getAllTransactions(dynamic _) => haven_transaction_history
343 - .getAllTransations()
344 - .map((row) => HavenTransactionInfo.fromRow(row))
345 - .toList();
331 + .getAllTransations()
332 + .map((row) => HavenTransactionInfo.fromRow(row))
333 + .toList();
334
335 void _setListeners() {
336 _listener?.stop();
@@ -364,8 +352,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
352 }
353
354 int _getHeightDistance(DateTime date) {
367 - final distance =
368 - DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
355 + final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
356 final daysTmp = (distance / 86400).round();
357 final days = daysTmp < 1 ? 1 : daysTmp;
358
@@ -386,8 +373,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
373 void _askForUpdateBalance() =>
374 balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id));
375
389 - Future<void> _askForUpdateTransactionHistory() async =>
390 - await updateTransactions();
376 + Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
377
378 void _onNewBlock(int height, int blocksLeft, double ptc) async {
379 try {
@@ -404,9 +390,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
390 syncStatus = SyncedSyncStatus();
391
392 if (!_hasSyncAfterStartup) {
407 - _hasSyncAfterStartup = true;
408 - await save();
409 - }
393 + _hasSyncAfterStartup = true;
394 + await save();
395 + }
396
397 if (walletInfo.isRecovery) {
398 await setAsRecovered();
cw_monero/lib/monero_subaddress_list.dart
+74 -14
@@ -6,14 +6,15 @@ import 'package:cw_core/subaddress.dart';
6
7 part 'monero_subaddress_list.g.dart';
8
9 -class MoneroSubaddressList = MoneroSubaddressListBase
10 - with _$MoneroSubaddressList;
9 +class MoneroSubaddressList = MoneroSubaddressListBase with _$MoneroSubaddressList;
10
11 abstract class MoneroSubaddressListBase with Store {
12 MoneroSubaddressListBase()
14 - : _isRefreshing = false,
15 - _isUpdating = false,
16 - subaddresses = ObservableList<Subaddress>();
13 + : _isRefreshing = false,
14 + _isUpdating = false,
15 + subaddresses = ObservableList<Subaddress>();
16 +
17 + final List<String> _usedAddresses = [];
18
19 @observable
20 ObservableList<Subaddress> subaddresses;
@@ -49,20 +50,24 @@ abstract class MoneroSubaddressListBase with Store {
50 subaddresses = [primary] + rest.toList();
51 }
52
52 - return subaddresses
53 - .map((subaddressRow) => Subaddress(
53 + return subaddresses.map((subaddressRow) {
54 + final hasDefaultAddressName =
55 + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() ||
56 + subaddressRow.getLabel().toLowerCase() == 'Untitled account'.toLowerCase();
57 + final isPrimaryAddress = subaddressRow.getId() == 0 && hasDefaultAddressName;
58 + return Subaddress(
59 id: subaddressRow.getId(),
60 address: subaddressRow.getAddress(),
56 - label: subaddressRow.getId() == 0 &&
57 - subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase()
58 - ? 'Primary address'
59 - : subaddressRow.getLabel()))
60 - .toList();
61 + label: isPrimaryAddress
62 + ? 'Primary address'
63 + : hasDefaultAddressName
64 + ? ''
65 + : subaddressRow.getLabel());
66 + }).toList();
67 }
68
69 Future<void> addSubaddress({required int accountIndex, required String label}) async {
64 - await subaddress_list.addSubaddress(
65 - accountIndex: accountIndex, label: label);
70 + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label);
71 update(accountIndex: accountIndex);
72 }
73
@@ -88,4 +93,59 @@ abstract class MoneroSubaddressListBase with Store {
93 rethrow;
94 }
95 }
96 +
97 + Future<void> updateWithAutoGenerate({
98 + required int accountIndex,
99 + required String defaultLabel,
100 + required List<String> usedAddresses,
101 + }) async {
102 + _usedAddresses.addAll(usedAddresses);
103 + if (_isUpdating) {
104 + return;
105 + }
106 +
107 + try {
108 + _isUpdating = true;
109 + refresh(accountIndex: accountIndex);
110 + subaddresses.clear();
111 + final newSubAddresses =
112 + await _getAllUnusedAddresses(accountIndex: accountIndex, label: defaultLabel);
113 + subaddresses.addAll(newSubAddresses);
114 + } catch (e) {
115 + rethrow;
116 + } finally {
117 + _isUpdating = false;
118 + }
119 + }
120 +
121 + Future<List<Subaddress>> _getAllUnusedAddresses(
122 + {required int accountIndex, required String label}) async {
123 + final allAddresses = subaddress_list.getAllSubaddresses();
124 +
125 + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last.getAddress())) {
126 + final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label);
127 + if (!isAddressUnused) {
128 + return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label);
129 + }
130 + }
131 +
132 + return allAddresses
133 + .map((subaddressRow) => Subaddress(
134 + id: subaddressRow.getId(),
135 + address: subaddressRow.getAddress(),
136 + label: subaddressRow.getId() == 0 &&
137 + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase()
138 + ? 'Primary address'
139 + : subaddressRow.getLabel()))
140 + .toList();
141 + }
142 +
143 + Future<bool> _newSubaddress({required int accountIndex, required String label}) async {
144 + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label);
145 +
146 + return subaddress_list
147 + .getAllSubaddresses()
148 + .where((subaddressRow) => !_usedAddresses.contains(subaddressRow.getAddress()))
149 + .isNotEmpty;
150 + }
151 }
cw_monero/lib/monero_wallet.dart
+32 -3
@@ -48,12 +48,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
48 }),
49 _isTransactionUpdating = false,
50 _hasSyncAfterStartup = false,
51 - walletAddresses = MoneroWalletAddresses(walletInfo),
51 + isEnabledAutoGenerateSubaddress = false,
52 syncStatus = NotConnectedSyncStatus(),
53 unspentCoins = [],
54 this.unspentCoinsInfo = unspentCoinsInfo,
55 super(walletInfo) {
56 transactionHistory = MoneroTransactionHistory();
57 + walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory);
58 +
59 _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) {
60 if (account == null) {
61 return;
@@ -64,7 +66,11 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
66 fullBalance: monero_wallet.getFullBalance(accountIndex: account.id),
67 unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id))
68 });
67 - walletAddresses.updateSubaddressList(accountIndex: account.id);
69 + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account);
70 + });
71 +
72 + reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
73 + _updateSubAddress(enabled, account: walletAddresses.account);
74 });
75 }
76
@@ -73,7 +79,11 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
79 Box<UnspentCoinsInfo> unspentCoinsInfo;
80
81 @override
76 - MoneroWalletAddresses walletAddresses;
82 + late MoneroWalletAddresses walletAddresses;
83 +
84 + @override
85 + @observable
86 + bool isEnabledAutoGenerateSubaddress;
87
88 @override
89 @observable
@@ -287,6 +297,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
297
298 @override
299 Future<void> save() async {
300 + await walletAddresses.updateUsedSubaddress();
301 +
302 + if (isEnabledAutoGenerateSubaddress) {
303 + walletAddresses.updateUnusedSubaddress(
304 + accountIndex: walletAddresses.account?.id ?? 0,
305 + defaultLabel: walletAddresses.account?.label ?? '');
306 + }
307 +
308 await walletAddresses.updateAddressesInBox();
309 await backupWalletFiles(name);
310 await monero_wallet.store();
@@ -610,4 +628,15 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
628 print(e.toString());
629 }
630 }
631 +
632 + void _updateSubAddress(bool enableAutoGenerate, {Account? account}) {
633 + if (enableAutoGenerate) {
634 + walletAddresses.updateUnusedSubaddress(
635 + accountIndex: account?.id ?? 0,
636 + defaultLabel: account?.label ?? '',
637 + );
638 + } else {
639 + walletAddresses.updateSubaddressList(accountIndex: account?.id ?? 0);
640 + }
641 + }
642 }
cw_monero/lib/monero_wallet_addresses.dart
+41 -13
@@ -1,27 +1,32 @@
1 +import 'package:cw_core/address_info.dart';
2 import 'package:cw_core/wallet_addresses.dart';
3 import 'package:cw_core/wallet_info.dart';
4 import 'package:cw_core/account.dart';
5 +import 'package:cw_monero/api/wallet.dart';
6 import 'package:cw_monero/monero_account_list.dart';
7 import 'package:cw_monero/monero_subaddress_list.dart';
8 import 'package:cw_core/subaddress.dart';
9 +import 'package:cw_monero/monero_transaction_history.dart';
10 import 'package:mobx/mobx.dart';
11
12 part 'monero_wallet_addresses.g.dart';
13
11 -class MoneroWalletAddresses = MoneroWalletAddressesBase
12 - with _$MoneroWalletAddresses;
14 +class MoneroWalletAddresses = MoneroWalletAddressesBase with _$MoneroWalletAddresses;
15
16 abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
15 - MoneroWalletAddressesBase(WalletInfo walletInfo)
16 - : accountList = MoneroAccountList(),
17 - subaddressList = MoneroSubaddressList(),
18 - address = '',
19 - super(walletInfo);
20 -
17 + MoneroWalletAddressesBase(
18 + WalletInfo walletInfo, MoneroTransactionHistory moneroTransactionHistory)
19 + : accountList = MoneroAccountList(),
20 + _moneroTransactionHistory = moneroTransactionHistory,
21 + subaddressList = MoneroSubaddressList(),
22 + address = '',
23 + super(walletInfo);
24 +
25 + final MoneroTransactionHistory _moneroTransactionHistory;
26 @override
27 @observable
28 String address;
24 -
29 +
30 @observable
31 Account? account;
32
@@ -36,7 +41,6 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
41 Future<void> init() async {
42 accountList.update();
43 account = accountList.accounts.first;
39 - updateSubaddressList(accountIndex: account?.id ?? 0);
44 await updateAddressesInBox();
45 }
46
@@ -46,11 +50,15 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
50 final _subaddressList = MoneroSubaddressList();
51
52 addressesMap.clear();
53 + addressInfos.clear();
54
55 accountList.accounts.forEach((account) {
56 _subaddressList.update(accountIndex: account.id);
57 _subaddressList.subaddresses.forEach((subaddress) {
58 addressesMap[subaddress.address] = subaddress.label;
59 + addressInfos[account.id] ??= [];
60 + addressInfos[account.id]?.add(AddressInfo(
61 + address: subaddress.address, label: subaddress.label, accountIndex: account.id));
62 });
63 });
64
@@ -62,14 +70,14 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
70
71 bool validate() {
72 accountList.update();
65 - final accountListLength = accountList.accounts.length ?? 0;
73 + final accountListLength = accountList.accounts.length;
74
75 if (accountListLength <= 0) {
76 return false;
77 }
78
79 subaddressList.update(accountIndex: accountList.accounts.first.id);
72 - final subaddressListLength = subaddressList.subaddresses.length ?? 0;
80 + final subaddressListLength = subaddressList.subaddresses.length;
81
82 if (subaddressListLength <= 0) {
83 return false;
@@ -83,4 +91,24 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
91 subaddress = subaddressList.subaddresses.first;
92 address = subaddress!.address;
93 }
86 -}
\ No newline at end of file
94 +
95 + Future<void> updateUsedSubaddress() async {
96 + final transactions = _moneroTransactionHistory.transactions.values.toList();
97 +
98 + transactions.forEach((element) {
99 + final accountIndex = element.accountIndex;
100 + final addressIndex = element.addressIndex;
101 + usedAddresses.add(getAddress(accountIndex: accountIndex, addressIndex: addressIndex));
102 + });
103 + }
104 +
105 + Future<void> updateUnusedSubaddress(
106 + {required int accountIndex, required String defaultLabel}) async {
107 + await subaddressList.updateWithAutoGenerate(
108 + accountIndex: accountIndex,
109 + defaultLabel: defaultLabel,
110 + usedAddresses: usedAddresses.toList());
111 + subaddress = subaddressList.subaddresses.last;
112 + address = subaddress!.address;
113 + }
114 +}
lib/core/backup_service.dart
+6
@@ -246,6 +246,7 @@ class BackupService {
246 final useEtherscan = data[PreferencesKey.useEtherscan] as bool?;
247 final syncAll = data[PreferencesKey.syncAllKey] as bool?;
248 final syncMode = data[PreferencesKey.syncModeKey] as int?;
249 + final autoGenerateSubaddressStatus = data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?;
250
251 await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
252
@@ -296,6 +297,9 @@ class BackupService {
297
298 if (fiatApiMode != null)
299 await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode);
300 + if (autoGenerateSubaddressStatus != null)
301 + await _sharedPreferences.setInt(PreferencesKey.autoGenerateSubaddressStatusKey,
302 + autoGenerateSubaddressStatus);
303
304 if (currentPinLength != null)
305 await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength);
@@ -523,6 +527,8 @@ class BackupService {
527 _sharedPreferences.getInt(PreferencesKey.syncModeKey),
528 PreferencesKey.syncAllKey:
529 _sharedPreferences.getBool(PreferencesKey.syncAllKey),
530 + PreferencesKey.autoGenerateSubaddressStatusKey:
531 + _sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey),
532 };
533
534 return json.encode(preferences);
lib/di.dart
+1 -1
@@ -5,6 +5,7 @@ import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
5 import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart';
6 import 'package:cake_wallet/core/yat_service.dart';
7 import 'package:cake_wallet/entities/background_tasks.dart';
8 +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
9 import 'package:cake_wallet/entities/exchange_api_mode.dart';
10 import 'package:cake_wallet/entities/parse_address_from_domain.dart';
11 import 'package:cake_wallet/entities/receive_page_option.dart';
@@ -245,7 +246,6 @@ Future<void> setup({
246 if (!_isSetupFinished) {
247 getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance());
248 }
248 -
249 if (!_isSetupFinished) {
250 getIt.registerFactory(() => BackgroundTasks());
251 }
lib/entities/auto_generate_subaddress_status.dart new
+13
@@ -0,0 +1,13 @@
1 +
2 +enum AutoGenerateSubaddressStatus {
3 + initialized(1),
4 + enabled(2),
5 + disabled(3);
6 +
7 + const AutoGenerateSubaddressStatus(this.value);
8 + final int value;
9 +
10 + static AutoGenerateSubaddressStatus deserialize({required int raw}) =>
11 + AutoGenerateSubaddressStatus.values.firstWhere((e) => e.value == raw);
12 +
13 +}
\ No newline at end of file
lib/entities/preferences_key.dart
+1
@@ -50,6 +50,7 @@ class PreferencesKey {
50 '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
51
52 static const exchangeProvidersSelection = 'exchange-providers-selection';
53 + static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status';
54 static const clearnetDonationLink = 'clearnet_donation_link';
55 static const onionDonationLink = 'onion_donation_link';
56 static const lastSeenAppVersion = 'last_seen_app_version';
lib/main.dart
+5
@@ -6,6 +6,7 @@ import 'package:cake_wallet/buy/order.dart';
6 import 'package:cake_wallet/locales/locale.dart';
7 import 'package:cake_wallet/store/yat/yat_store.dart';
8 import 'package:cake_wallet/utils/exception_handler.dart';
9 +import 'package:cw_core/address_info.dart';
10 import 'package:cake_wallet/utils/responsive_layout_util.dart';
11 import 'package:cw_core/hive_type_ids.dart';
12 import 'package:flutter/foundation.dart';
@@ -89,6 +90,10 @@ Future<void> initializeAppConfigs() async {
90 CakeHive.registerAdapter(TradeAdapter());
91 }
92
93 + if (!CakeHive.isAdapterRegistered(AddressInfo.typeId)) {
94 + CakeHive.registerAdapter(AddressInfoAdapter());
95 + }
96 +
97 if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) {
98 CakeHive.registerAdapter(WalletInfoAdapter());
99 }
lib/reactions/on_current_wallet_change.dart
+48 -33
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
2 import 'package:cake_wallet/entities/fiat_api_mode.dart';
3 import 'package:cake_wallet/entities/update_haven_rate.dart';
4 import 'package:cake_wallet/ethereum/ethereum.dart';
@@ -21,36 +22,36 @@ ReactionDisposer? _onCurrentWalletChangeReaction;
22 ReactionDisposer? _onCurrentWalletChangeFiatRateUpdateReaction;
23 //ReactionDisposer _onCurrentWalletAddressChangeReaction;
24
24 -void startCurrentWalletChangeReaction(AppStore appStore,
25 - SettingsStore settingsStore, FiatConversionStore fiatConversionStore) {
25 +void startCurrentWalletChangeReaction(
26 + AppStore appStore, SettingsStore settingsStore, FiatConversionStore fiatConversionStore) {
27 _onCurrentWalletChangeReaction?.reaction.dispose();
28 _onCurrentWalletChangeFiatRateUpdateReaction?.reaction.dispose();
29 //_onCurrentWalletAddressChangeReaction?.reaction?dispose();
30
31 //_onCurrentWalletAddressChangeReaction = reaction((_) => appStore.wallet.walletAddresses.address,
31 - //(String address) async {
32 - //if (address == appStore.wallet.walletInfo.yatLastUsedAddress) {
33 - // return;
34 - //}
32 + //(String address) async {
33 + //if (address == appStore.wallet.walletInfo.yatLastUsedAddress) {
34 + // return;
35 + //}
36
36 - //try {
37 - // final yatStore = getIt.get<YatStore>();
38 - // await updateEmojiIdAddress(
39 - // appStore.wallet.walletInfo.yatEmojiId,
40 - // appStore.wallet.walletAddresses.address,
41 - // yatStore.apiKey,
42 - // appStore.wallet.type
43 - // );
44 - // appStore.wallet.walletInfo.yatLastUsedAddress = address;
45 - // await appStore.wallet.walletInfo.save();
46 - //} catch (e) {
47 - // print(e.toString());
48 - //}
37 + //try {
38 + // final yatStore = getIt.get<YatStore>();
39 + // await updateEmojiIdAddress(
40 + // appStore.wallet.walletInfo.yatEmojiId,
41 + // appStore.wallet.walletAddresses.address,
42 + // yatStore.apiKey,
43 + // appStore.wallet.type
44 + // );
45 + // appStore.wallet.walletInfo.yatLastUsedAddress = address;
46 + // await appStore.wallet.walletInfo.save();
47 + //} catch (e) {
48 + // print(e.toString());
49 + //}
50 //});
51
51 - _onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, (WalletBase<
52 - Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
53 - wallet) async {
52 + _onCurrentWalletChangeReaction = reaction((_) => appStore.wallet,
53 + (WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
54 + wallet) async {
55 try {
56 if (wallet == null) {
57 return;
@@ -59,11 +60,13 @@ void startCurrentWalletChangeReaction(AppStore appStore,
60 final node = settingsStore.getCurrentNode(wallet.type);
61 startWalletSyncStatusChangeReaction(wallet, fiatConversionStore);
62 startCheckConnectionReaction(wallet, settingsStore);
63 + await getIt.get<SharedPreferences>().setString(PreferencesKey.currentWalletName, wallet.name);
64 await getIt
65 .get<SharedPreferences>()
64 - .setString(PreferencesKey.currentWalletName, wallet.name);
65 - await getIt.get<SharedPreferences>().setInt(
66 - PreferencesKey.currentWalletType, serializeToInt(wallet.type));
66 + .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type));
67 + if (wallet.type == WalletType.monero) {
68 + _setAutoGenerateSubaddressStatus(wallet, settingsStore);
69 + }
70 await wallet.connectToNode(node: node);
71
72 if (wallet.type == WalletType.haven) {
@@ -82,9 +85,8 @@ void startCurrentWalletChangeReaction(AppStore appStore,
85 }
86 });
87
85 - _onCurrentWalletChangeFiatRateUpdateReaction =
86 - reaction((_) => appStore.wallet, (WalletBase<Balance,
87 - TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
88 + _onCurrentWalletChangeFiatRateUpdateReaction = reaction((_) => appStore.wallet,
89 + (WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
90 wallet) async {
91 try {
92 if (wallet == null || settingsStore.fiatApiMode == FiatApiMode.disabled) {
@@ -92,11 +94,10 @@ void startCurrentWalletChangeReaction(AppStore appStore,
94 }
95
96 fiatConversionStore.prices[wallet.currency] = 0;
95 - fiatConversionStore.prices[wallet.currency] =
96 - await FiatConversionService.fetchPrice(
97 - crypto: wallet.currency,
98 - fiat: settingsStore.fiatCurrency,
99 - torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
97 + fiatConversionStore.prices[wallet.currency] = await FiatConversionService.fetchPrice(
98 + crypto: wallet.currency,
99 + fiat: settingsStore.fiatCurrency,
100 + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
101
102 if (wallet.type == WalletType.ethereum) {
103 final currencies =
@@ -116,3 +117,17 @@ void startCurrentWalletChangeReaction(AppStore appStore,
117 }
118 });
119 }
120 +
121 +void _setAutoGenerateSubaddressStatus(
122 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet,
123 + SettingsStore settingsStore,
124 +) async {
125 + final walletHasAddresses = await wallet.walletAddresses.addressesMap.length > 1;
126 + if (settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized &&
127 + walletHasAddresses) {
128 + settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled;
129 + }
130 + wallet.isEnabledAutoGenerateSubaddress =
131 + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled ||
132 + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized;
133 +}
lib/src/screens/dashboard/widgets/address_page.dart
+27 -13
@@ -1,8 +1,10 @@
1 import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
2 +import 'package:cake_wallet/di.dart';
3 +import 'package:cake_wallet/src/screens/base_page.dart';
4 +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart';
5 import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
6 import 'package:cake_wallet/entities/preferences_key.dart';
7 import 'package:cake_wallet/entities/receive_page_option.dart';
5 -import 'package:cake_wallet/src/screens/base_page.dart';
8 import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart';
9 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
10 import 'package:cake_wallet/src/widgets/gradient_background.dart';
@@ -24,7 +26,6 @@ import 'package:flutter_mobx/flutter_mobx.dart';
26 import 'package:keyboard_actions/keyboard_actions.dart';
27 import 'package:mobx/mobx.dart';
28 import 'package:shared_preferences/shared_preferences.dart';
27 -import 'package:cake_wallet/di.dart';
29 import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
30 import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
31
@@ -174,7 +175,11 @@ class AddressPage extends BasePage {
175 Observer(builder: (_) {
176 if (addressListViewModel.hasAddressList) {
177 return GestureDetector(
177 - onTap: () => Navigator.of(context).pushNamed(Routes.receive),
178 + onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled
179 + ? await showPopUp<void>(
180 + context: context,
181 + builder: (_) => getIt.get<MoneroAccountListPage>())
182 + : Navigator.of(context).pushNamed(Routes.receive),
183 child: Container(
184 height: 50,
185 padding: EdgeInsets.only(left: 24, right: 12),
@@ -193,17 +198,26 @@ class AddressPage extends BasePage {
198 mainAxisAlignment: MainAxisAlignment.spaceBetween,
199 children: <Widget>[
200 Observer(
196 - builder: (_) => Text(
197 - addressListViewModel.hasAccounts
198 - ? S.of(context).accounts_subaddresses
199 - : S.of(context).addresses,
200 - style: TextStyle(
201 - fontSize: 14,
202 - fontWeight: FontWeight.w500,
203 - color: Theme.of(context)
201 + builder: (_) {
202 + String label = addressListViewModel.hasAccounts
203 + ? S.of(context).accounts_subaddresses
204 + : S.of(context).addresses;
205 +
206 + if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) {
207 + label = addressListViewModel.hasAccounts
208 + ? S.of(context).accounts
209 + : S.of(context).account;
210 + }
211 + return Text(
212 + label,
213 + style: TextStyle(
214 + fontSize: 14,
215 + fontWeight: FontWeight.w500,
216 + color: Theme.of(context)
217 .extension<SyncIndicatorTheme>()!
218 .textColor),
206 - )),
219 + );
220 + },),
221 Icon(
222 Icons.arrow_forward_ios,
223 size: 14,
@@ -213,7 +227,7 @@ class AddressPage extends BasePage {
227 ),
228 ),
229 );
216 - } else if (addressListViewModel.showElectrumAddressDisclaimer) {
230 + } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || addressListViewModel.showElectrumAddressDisclaimer) {
231 return Text(S.of(context).electrum_address_disclaimer,
232 textAlign: TextAlign.center,
233 style: TextStyle(
lib/src/screens/monero_accounts/widgets/account_tile.dart
+8 -7
@@ -5,13 +5,14 @@ import 'package:flutter_slidable/flutter_slidable.dart';
5 import 'package:cake_wallet/generated/i18n.dart';
6
7 class AccountTile extends StatelessWidget {
8 - AccountTile(
9 - {required this.isCurrent,
10 - required this.accountName,
11 - this.accountBalance,
12 - required this.currency,
13 - required this.onTap,
14 - required this.onEdit});
8 + AccountTile({
9 + required this.isCurrent,
10 + required this.accountName,
11 + this.accountBalance,
12 + required this.currency,
13 + required this.onTap,
14 + required this.onEdit,
15 + });
16
17 final bool isCurrent;
18 final String accountName;
lib/src/screens/restore/restore_options_page.dart
+1 -1
@@ -73,7 +73,7 @@ class RestoreOptionsPage extends BasePage {
73 await restoreFromQRViewModel.create(restoreWallet: restoreWallet);
74 if (restoreFromQRViewModel.state is FailureState) {
75 _onWalletCreateFailure(context,
76 - 'Create wallet state: ${restoreFromQRViewModel.state.runtimeType.toString()}');
76 + 'Create wallet state: ${(restoreFromQRViewModel.state as FailureState).error}');
77 }
78 } catch (e) {
79 _onWalletCreateFailure(context, e.toString());
lib/src/screens/restore/widgets/backup_file_button.dart
lib/src/screens/settings/privacy_page.dart
+8
@@ -50,6 +50,14 @@ class PrivacyPage extends BasePage {
50 onValueChange: (BuildContext _, bool value) {
51 _privacySettingsViewModel.setShouldSaveRecipientAddress(value);
52 }),
53 + if (_privacySettingsViewModel.isAutoGenerateSubaddressesVisible)
54 + SettingsSwitcherCell(
55 + title: S.current.auto_generate_subaddresses,
56 + value: _privacySettingsViewModel.isAutoGenerateSubaddressesEnabled,
57 + onValueChange: (BuildContext _, bool value) {
58 + _privacySettingsViewModel.setAutoGenerateSubaddresses(value);
59 + },
60 + ),
61 if (DeviceInfo.instance.isMobile)
62 SettingsSwitcherCell(
63 title: S.current.prevent_screenshots,
lib/store/settings_store.dart
+25 -2
@@ -1,6 +1,7 @@
1 import 'dart:io';
2
3 import 'package:cake_wallet/bitcoin/bitcoin.dart';
4 +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
5 import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
6 import 'package:cake_wallet/entities/background_tasks.dart';
7 import 'package:cake_wallet/entities/exchange_api_mode.dart';
@@ -42,6 +43,7 @@ abstract class SettingsStoreBase with Store {
43 required FiatCurrency initialFiatCurrency,
44 required BalanceDisplayMode initialBalanceDisplayMode,
45 required bool initialSaveRecipientAddress,
46 + required AutoGenerateSubaddressStatus initialAutoGenerateSubaddressStatus,
47 required bool initialAppSecure,
48 required bool initialDisableBuy,
49 required bool initialDisableSell,
@@ -87,6 +89,7 @@ abstract class SettingsStoreBase with Store {
89 fiatCurrency = initialFiatCurrency,
90 balanceDisplayMode = initialBalanceDisplayMode,
91 shouldSaveRecipientAddress = initialSaveRecipientAddress,
92 + autoGenerateSubaddressStatus = initialAutoGenerateSubaddressStatus,
93 fiatApiMode = initialFiatMode,
94 allowBiometricalAuthentication = initialAllowBiometricalAuthentication,
95 selectedCake2FAPreset = initialCake2FAPresetOptions,
@@ -197,6 +200,11 @@ abstract class SettingsStoreBase with Store {
200 (bool disableSell) =>
201 sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
202
203 + reaction(
204 + (_) => autoGenerateSubaddressStatus,
205 + (AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt(
206 + PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus.value));
207 +
208 reaction(
209 (_) => fiatApiMode,
210 (FiatApiMode mode) =>
@@ -337,6 +345,7 @@ abstract class SettingsStoreBase with Store {
345 static const defaultPinLength = 4;
346 static const defaultActionsMode = 11;
347 static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes;
348 + static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized;
349
350 @observable
351 FiatCurrency fiatCurrency;
@@ -359,6 +368,9 @@ abstract class SettingsStoreBase with Store {
368 @observable
369 bool shouldSaveRecipientAddress;
370
371 + @observable
372 + AutoGenerateSubaddressStatus autoGenerateSubaddressStatus;
373 +
374 @observable
375 bool isAppSecure;
376
@@ -602,7 +614,12 @@ abstract class SettingsStoreBase with Store {
614 final packageInfo = await PackageInfo.fromPlatform();
615 final deviceName = await _getDeviceName() ?? '';
616 final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true;
617 + final generateSubaddresses =
618 + sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey);
619
620 + final autoGenerateSubaddressStatus = generateSubaddresses != null
621 + ? AutoGenerateSubaddressStatus.deserialize(raw: generateSubaddresses)
622 + : defaultAutoGenerateSubaddressStatus;
623 final nodes = <WalletType, Node>{};
624
625 if (moneroNode != null) {
@@ -640,6 +657,7 @@ abstract class SettingsStoreBase with Store {
657 initialFiatCurrency: currentFiatCurrency,
658 initialBalanceDisplayMode: currentBalanceDisplayMode,
659 initialSaveRecipientAddress: shouldSaveRecipientAddress,
660 + initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus,
661 initialAppSecure: isAppSecure,
662 initialDisableBuy: disableBuy,
663 initialDisableSell: disableSell,
@@ -709,6 +727,13 @@ abstract class SettingsStoreBase with Store {
727 priority[WalletType.ethereum]!;
728 }
729
730 + final generateSubaddresses =
731 + sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey);
732 +
733 + autoGenerateSubaddressStatus = generateSubaddresses != null
734 + ? AutoGenerateSubaddressStatus.deserialize(raw: generateSubaddresses)
735 + : defaultAutoGenerateSubaddressStatus;
736 +
737 balanceDisplayMode = BalanceDisplayMode.deserialize(
738 raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!);
739 shouldSaveRecipientAddress =
@@ -719,8 +744,6 @@ abstract class SettingsStoreBase with Store {
744
745 numberOfFailedTokenTrials =
746 sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
722 - sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ??
723 - shouldSaveRecipientAddress;
747 isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
748 disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
749 disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
lib/view_model/contact_list/contact_list_view_model.dart
+26 -5
@@ -1,4 +1,5 @@
1 import 'dart:async';
2 +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
3 import 'package:cake_wallet/entities/contact_base.dart';
4 import 'package:cake_wallet/entities/wallet_contact.dart';
5 import 'package:cake_wallet/store/settings_store.dart';
@@ -10,6 +11,7 @@ import 'package:cake_wallet/entities/contact_record.dart';
11 import 'package:cake_wallet/entities/contact.dart';
12 import 'package:cake_wallet/utils/mobx.dart';
13 import 'package:cw_core/crypto_currency.dart';
14 +import 'package:collection/collection.dart';
15
16 part 'contact_list_view_model.g.dart';
17
@@ -20,12 +22,26 @@ abstract class ContactListViewModelBase with Store {
22 ContactListViewModelBase(this.contactSource, this.walletInfoSource,
23 this._currency, this.settingsStore)
24 : contacts = ObservableList<ContactRecord>(),
23 - walletContacts = [] {
25 + walletContacts = [],
26 + isAutoGenerateEnabled =
27 + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled {
28 walletInfoSource.values.forEach((info) {
25 - if (info.addresses?.isNotEmpty ?? false) {
26 - info.addresses?.forEach((address, label) {
27 - final name = label.isNotEmpty ? info.name + ' ($label)' : info.name;
28 -
29 + if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) {
30 + info.addressInfos!.forEach((key, value) {
31 + final nextUnusedAddress = value.firstWhereOrNull(
32 + (addressInfo) => !(info.usedAddresses?.contains(addressInfo.address) ?? false));
33 + if (nextUnusedAddress != null) {
34 + final name = _createName(info.name, nextUnusedAddress.label);
35 + walletContacts.add(WalletContact(
36 + nextUnusedAddress.address,
37 + name,
38 + walletTypeToCryptoCurrency(info.type),
39 + ));
40 + }
41 + });
42 + } else if (info.addresses?.isNotEmpty == true) {
43 + info.addresses!.forEach((address, label) {
44 + final name = _createName(info.name, label);
45 walletContacts.add(WalletContact(
46 address,
47 name,
@@ -40,6 +56,11 @@ abstract class ContactListViewModelBase with Store {
56 initialFire: true);
57 }
58
59 + String _createName(String walletName, String label) {
60 + return label.isNotEmpty ? '$walletName ($label)' : walletName;
61 + }
62 +
63 + final bool isAutoGenerateEnabled;
64 final Box<Contact> contactSource;
65 final Box<WalletInfo> walletInfoSource;
66 final ObservableList<ContactRecord> contacts;
lib/view_model/dashboard/dashboard_view_model.dart
+5
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
2 import 'package:cake_wallet/entities/exchange_api_mode.dart';
3 import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
4 import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
@@ -235,6 +236,10 @@ abstract class DashboardViewModelBase with Store {
236 @computed
237 double get price => balanceViewModel.price;
238
239 + @computed
240 + bool get isAutoGenerateSubaddressesEnabled =>
241 + settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled;
242 +
243 @computed
244 List<ActionListItem> get items {
245 final _items = <ActionListItem>[];
lib/view_model/settings/privacy_settings_view_model.dart
+21 -1
@@ -1,6 +1,10 @@
1 +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
2 import 'package:cake_wallet/entities/exchange_api_mode.dart';
3 import 'package:cake_wallet/ethereum/ethereum.dart';
4 import 'package:cake_wallet/store/settings_store.dart';
5 +import 'package:cw_core/balance.dart';
6 +import 'package:cw_core/transaction_history.dart';
7 +import 'package:cw_core/transaction_info.dart';
8 import 'package:cw_core/wallet_base.dart';
9 import 'package:cw_core/wallet_type.dart';
10 import 'package:mobx/mobx.dart';
@@ -14,11 +18,27 @@ abstract class PrivacySettingsViewModelBase with Store {
18 PrivacySettingsViewModelBase(this._settingsStore, this._wallet);
19
20 final SettingsStore _settingsStore;
17 - final WalletBase _wallet;
21 + final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
22
23 @computed
24 ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus;
25
26 + @computed
27 + bool get isAutoGenerateSubaddressesEnabled =>
28 + _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled;
29 +
30 + @action
31 + void setAutoGenerateSubaddresses(bool value) {
32 + _wallet.isEnabledAutoGenerateSubaddress = value;
33 + if (value) {
34 + _settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.enabled;
35 + } else {
36 + _settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled;
37 + }
38 + }
39 +
40 + bool get isAutoGenerateSubaddressesVisible => _wallet.type == WalletType.monero;
41 +
42 @computed
43 bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress;
44
res/values/strings_ar.arb
+3 -1
@@ -682,5 +682,7 @@
682 "support_title_other_links": "روابط دعم أخرى",
683 "support_description_other_links": "انضم إلى مجتمعاتنا أو تصل إلينا شركائنا من خلال أساليب أخرى",
684 "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ",
685 - "save_to_downloads": "ﺕﻼﻳﺰﻨﺘﻟﺍ ﻲﻓ ﻆﻔﺣ"
685 + "save_to_downloads": "ﺕﻼﻳﺰﻨﺘﻟﺍ ﻲﻓ ﻆﻔﺣ",
686 + "support_description_other_links": "انضم إلى مجتمعاتنا أو تصل إلينا شركائنا من خلال أساليب أخرى",
687 + "auto_generate_subaddresses": "تلقائي توليد subddresses"
688 }
res/values/strings_bg.arb
+1
@@ -626,6 +626,7 @@
626 "setup_totp_recommended": "Настройка на TOTP (препоръчително)",
627 "disable_buy": "Деактивирайте действието за покупка",
628 "disable_sell": "Деактивирайте действието за продажба",
629 + "auto_generate_subaddresses": "Автоматично генериране на подадреси",
630 "cake_2fa_preset": "Торта 2FA Preset",
631 "narrow": "Тесен",
632 "normal": "нормално",
res/values/strings_cs.arb
+1
@@ -626,6 +626,7 @@
626 "setup_totp_recommended": "Nastavit TOTP (doporučeno)",
627 "disable_buy": "Zakázat akci nákupu",
628 "disable_sell": "Zakázat akci prodeje",
629 + "auto_generate_subaddresses": "Automaticky generovat podadresy",
630 "cake_2fa_preset": "Předvolba Cake 2FA",
631 "narrow": "Úzký",
632 "normal": "Normální",
res/values/strings_de.arb
+1
@@ -640,6 +640,7 @@
640 "high_contrast_theme": "Kontrastreiches Thema",
641 "matrix_green_dark_theme": "Matrix Green Dark Theme",
642 "monero_light_theme": "Monero Light-Thema",
643 + "auto_generate_subaddresses": "Unteradressen automatisch generieren",
644 "cake_2fa_preset" : "Cake 2FA-Voreinstellung",
645 "narrow": "Eng",
646 "normal": "Normal",
res/values/strings_en.arb
+1
@@ -568,6 +568,7 @@
568 "privacy": "Privacy",
569 "display_settings": "Display settings",
570 "other_settings": "Other settings",
571 + "auto_generate_subaddresses": "Auto generate subaddresses",
572 "require_pin_after": "Require PIN after",
573 "always": "Always",
574 "minutes_to_pin_code": "${minute} minutes",
res/values/strings_es.arb
+1
@@ -640,6 +640,7 @@
640 "high_contrast_theme": "Tema de alto contraste",
641 "matrix_green_dark_theme": "Matrix verde oscuro tema",
642 "monero_light_theme": "Tema ligero de Monero",
643 + "auto_generate_subaddresses": "Generar subdirecciones automáticamente",
644 "cake_2fa_preset": "Pastel 2FA preestablecido",
645 "narrow": "Angosto",
646 "normal": "Normal",
res/values/strings_fr.arb
+1
@@ -640,6 +640,7 @@
640 "high_contrast_theme": "Thème à contraste élevé",
641 "matrix_green_dark_theme": "Thème Matrix Green Dark",
642 "monero_light_theme": "Thème de lumière Monero",
643 + "auto_generate_subaddresses": "Générer automatiquement des sous-adresses",
644 "cake_2fa_preset": "Gâteau 2FA prédéfini",
645 "narrow": "Étroit",
646 "normal": "Normal",
res/values/strings_ha.arb
+1
@@ -618,6 +618,7 @@
618 "high_contrast_theme": "Babban Jigon Kwatance",
619 "matrix_green_dark_theme": "Matrix Green Dark Jigo",
620 "monero_light_theme": "Jigon Hasken Monero",
621 + "auto_generate_subaddresses": "Saɓaƙa subaddresses ta kai tsaye",
622 "cake_2fa_preset": "Cake 2FA saiti",
623 "narrow": "kunkuntar",
624 "normal": "Na al'ada",
res/values/strings_hi.arb
+1
@@ -640,6 +640,7 @@
640 "high_contrast_theme": "उच्च कंट्रास्ट थीम",
641 "matrix_green_dark_theme": "मैट्रिक्स ग्रीन डार्क थीम",
642 "monero_light_theme": "मोनेरो लाइट थीम",
643 + "auto_generate_subaddresses": "स्वचालित रूप से उप-पते उत्पन्न करें",
644 "cake_2fa_preset": "केक 2एफए प्रीसेट",
645 "narrow": "सँकरा",
646 "normal": "सामान्य",
res/values/strings_hr.arb
+1
@@ -640,6 +640,7 @@
640 "high_contrast_theme": "Tema visokog kontrasta",
641 "matrix_green_dark_theme": "Matrix Green Dark Theme",
642 "monero_light_theme": "Monero lagana tema",
643 + "auto_generate_subaddresses": "Automatski generirajte podadrese",
644 "cake_2fa_preset": "Cake 2FA Preset",
645 "narrow": "Usko",
646 "normal": "Normalno",
res/values/strings_id.arb
+1
@@ -622,6 +622,7 @@
622 "setup_totp_recommended": "Siapkan TOTP (Disarankan)",
623 "disable_buy": "Nonaktifkan tindakan beli",
624 "disable_sell": "Nonaktifkan aksi jual",
625 + "auto_generate_subaddresses": "Menghasilkan subalamat secara otomatis",
626 "cake_2fa_preset": "Preset Kue 2FA",
627 "narrow": "Sempit",
628 "normal": "Normal",
res/values/strings_it.arb
+1
@@ -634,6 +634,7 @@
634 "setup_totp_recommended": "Imposta TOTP (consigliato)",
635 "disable_buy": "Disabilita l'azione di acquisto",
636 "disable_sell": "Disabilita l'azione di vendita",
637 + "auto_generate_subaddresses": "Genera automaticamente sottindirizzi",
638 "cake_2fa_preset": "Torta 2FA Preset",
639 "narrow": "Stretto",
640 "normal": "Normale",
res/values/strings_ja.arb
+2 -1
@@ -686,5 +686,6 @@
686 "support_title_other_links": "その他のサポートリンク",
687 "support_description_other_links": "私たちのコミュニティに参加するか、他の方法を通して私たちのパートナーに連絡してください",
688 "select_destination": "バックアップファイルの保存先を選択してください。",
689 - "save_to_downloads": "ダウンロードに保存"
689 + "save_to_downloads": "ダウンロードに保存",
690 + "auto_generate_subaddresses": "Autoはサブアドレスを生成します"
691 }
res/values/strings_ko.arb
+2 -1
@@ -686,5 +686,6 @@
686 "support_title_other_links": "다른 지원 링크",
687 "support_description_other_links": "다른 방법을 통해 커뮤니티에 가입하거나 파트너에게 연락하십시오.",
688 "select_destination": "백업 파일의 대상을 선택하십시오.",
689 - "save_to_downloads": "다운로드에 저장"
689 + "save_to_downloads": "다운로드에 저장",
690 + "auto_generate_subaddresses": "자동 생성 서브 아드 드레스"
691 }
res/values/strings_my.arb
+2 -1
@@ -684,5 +684,6 @@
684 "support_title_other_links": "အခြားအထောက်အပံ့လင့်များ",
685 "support_description_other_links": "ကျွန်ုပ်တို့၏လူမှုအသိုင်းအဝိုင်းများသို့ 0 င်ရောက်ပါ",
686 "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။",
687 - "save_to_downloads": "ဒေါင်းလုဒ်များထံ သိမ်းဆည်းပါ။"
687 + "save_to_downloads": "ဒေါင်းလုဒ်များထံ သိမ်းဆည်းပါ။",
688 + "auto_generate_subaddresses": "အော်တို Generate Subaddresses"
689 }
res/values/strings_nl.arb
+1
@@ -634,6 +634,7 @@
634 "setup_totp_recommended": "TOTP instellen (aanbevolen)",
635 "disable_buy": "Koopactie uitschakelen",
636 "disable_sell": "Verkoopactie uitschakelen",
637 + "auto_generate_subaddresses": "Automatisch subadressen genereren",
638 "cake_2fa_preset": "Taart 2FA Voorinstelling",
639 "narrow": "Smal",
640 "normal": "Normaal",
res/values/strings_pl.arb
+1
@@ -634,6 +634,7 @@
634 "setup_totp_recommended": "Skonfiguruj TOTP (zalecane)",
635 "disable_buy": "Wyłącz akcję kupna",
636 "disable_sell": "Wyłącz akcję sprzedaży",
637 + "auto_generate_subaddresses": "Automatycznie generuj podadresy",
638 "cake_2fa_preset": "Ciasto 2FA Preset",
639 "narrow": "Wąski",
640 "normal": "Normalna",
res/values/strings_pt.arb
+1
@@ -633,6 +633,7 @@
633 "setup_totp_recommended": "Configurar TOTP (recomendado)",
634 "disable_buy": "Desativar ação de compra",
635 "disable_sell": "Desativar ação de venda",
636 + "auto_generate_subaddresses": "Gerar subendereços automaticamente",
637 "cake_2fa_preset": "Predefinição de bolo 2FA",
638 "narrow": "Estreito",
639 "normal": "Normal",
res/values/strings_ru.arb
+2 -1
@@ -686,5 +686,6 @@
686 "support_title_other_links": "Другие ссылки на поддержку",
687 "support_description_other_links": "Присоединяйтесь к нашим сообществам или охватите нас наших партнеров с помощью других методов",
688 "select_destination": "Пожалуйста, выберите место для файла резервной копии.",
689 - "save_to_downloads": "Сохранить в загрузках"
689 + "save_to_downloads": "Сохранить в загрузках",
690 + "auto_generate_subaddresses": "Авто генерируйте Subaddresses"
691 }
res/values/strings_th.arb
+2 -1
@@ -684,5 +684,6 @@
684 "support_title_other_links": "ลิงค์สนับสนุนอื่น ๆ",
685 "support_description_other_links": "เข้าร่วมชุมชนของเราหรือเข้าถึงเราพันธมิตรของเราผ่านวิธีการอื่น ๆ",
686 "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง",
687 - "save_to_downloads": "บันทึกลงดาวน์โหลด"
687 + "save_to_downloads": "บันทึกลงดาวน์โหลด",
688 + "auto_generate_subaddresses": "Auto สร้าง subaddresses"
689 }
res/values/strings_tr.arb
+1
@@ -632,6 +632,7 @@
632 "setup_totp_recommended": "TOTP'yi kurun (Önerilir)",
633 "disable_buy": "Satın alma işlemini devre dışı bırak",
634 "disable_sell": "Satış işlemini devre dışı bırak",
635 + "auto_generate_subaddresses": "Alt adresleri otomatik olarak oluştur",
636 "cake_2fa_preset": "Kek 2FA Ön Ayarı",
637 "narrow": "Dar",
638 "normal": "Normal",
res/values/strings_uk.arb
+1
@@ -634,6 +634,7 @@
634 "setup_totp_recommended": "Налаштувати TOTP (рекомендовано)",
635 "disable_buy": "Вимкнути дію покупки",
636 "disable_sell": "Вимкнути дію продажу",
637 + "auto_generate_subaddresses": "Автоматично генерувати підадреси",
638 "cake_2fa_preset": "Торт 2FA Preset",
639 "narrow": "вузькі",
640 "normal": "нормальний",
res/values/strings_ur.arb
+2 -1
@@ -678,5 +678,6 @@
678 "support_title_other_links": "دوسرے سپورٹ لنکس",
679 "support_description_other_links": "ہماری برادریوں میں شامل ہوں یا دوسرے طریقوں سے ہمارے شراکت داروں تک پہنچیں",
680 "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ",
681 - "save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ"
681 + "save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ",
682 + "auto_generate_subaddresses": "آٹو سب ایڈریس تیار کرتا ہے"
683 }
res/values/strings_yo.arb
+2 -1
@@ -680,5 +680,6 @@
680 "matrix_green_dark_theme": "Matrix Green Dark Akori",
681 "monero_light_theme": "Monero Light Akori",
682 "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.",
683 - "save_to_downloads": "Fipamọ si Awọn igbasilẹ"
683 + "save_to_downloads": "Fipamọ si Awọn igbasilẹ",
684 + "auto_generate_subaddresses": "Aṣiṣe Ibi-Afọwọkọ"
685 }
res/values/strings_zh.arb
+2 -1
@@ -685,5 +685,6 @@
685 "matrix_green_dark_theme": "矩阵绿暗主题",
686 "monero_light_theme": "门罗币浅色主题",
687 "select_destination": "请选择备份文件的目的地。",
688 - "save_to_downloads": "保存到下载"
688 + "save_to_downloads": "保存到下载",
689 + "auto_generate_subaddresses": "自动生成子辅助"
690 }
tool/append_translation.dart
+5 -49
@@ -1,15 +1,6 @@
1 -import 'dart:convert';
2 -import 'dart:io';
3 -
4 -import 'package:translator/translator.dart';
5 -
6 -const defaultLang = "en";
7 -const langs = [
8 - "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "id", "it",
9 - "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tr", "uk", "ur", "yo",
10 - "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified)
11 -];
12 -final translator = GoogleTranslator();
1 +import 'utils/translation/arb_file_utils.dart';
2 +import 'utils/translation/translation_constants.dart';
3 +import 'utils/translation/translation_utils.dart';
4
5 void main(List<String> args) async {
6 if (args.length != 2) {
@@ -23,44 +14,9 @@ void main(List<String> args) async {
14 print('Appending "$name": "$text"');
15
16 for (var lang in langs) {
26 - final fileName = getFileName(lang);
17 + final fileName = getArbFileName(lang);
18 final translation = await getTranslation(text, lang);
19
29 - appendArbFile(fileName, name, translation);
20 + appendStringToArbFile(fileName, name, translation);
21 }
22 }
32 -
33 -void appendArbFile(String fileName, String name, String text) {
34 - final file = File(fileName);
35 - final inputContent = file.readAsStringSync();
36 - final arbObj = json.decode(inputContent) as Map<String, dynamic>;
37 -
38 - if (arbObj.containsKey(name)) {
39 - print("String $name already exists in $fileName!");
40 - return;
41 - }
42 -
43 - arbObj.addAll({name: text});
44 -
45 - final outputContent = json
46 - .encode(arbObj)
47 - .replaceAll('","', '",\n "')
48 - .replaceAll('{"', '{\n "')
49 - .replaceAll('"}', '"\n}')
50 - .replaceAll('":"', '": "');
51 -
52 - file.writeAsStringSync(outputContent);
53 -}
54 -
55 -
56 -Future<String> getTranslation(String text, String lang) async {
57 - if (lang == defaultLang) return text;
58 - return (await translator.translate(text, from: defaultLang, to: lang)).text;
59 -}
60 -
61 -String getFileName(String lang) {
62 - final shortLang = lang
63 - .split("-")
64 - .first;
65 - return "./res/values/strings_$shortLang.arb";
66 -}
tool/translation_consistence.dart new
+37
@@ -0,0 +1,37 @@
1 +import 'dart:io';
2 +
3 +import 'utils/translation/arb_file_utils.dart';
4 +import 'utils/translation/translation_constants.dart';
5 +import 'utils/translation/translation_utils.dart';
6 +
7 +void main(List<String> args) async {
8 + print('Checking Consistency of all arb-files. Default: $defaultLang');
9 +
10 + final doFix = args.contains("--fix");
11 +
12 + if (doFix)
13 + print('Auto fixing enabled!\n');
14 + else
15 + print('Auto fixing disabled!\nRun with arg "--fix" to enable autofix\n');
16 +
17 + final fileName = getArbFileName(defaultLang);
18 + final file = File(fileName);
19 + final arbObj = readArbFile(file);
20 +
21 + for (var lang in langs) {
22 + final fileName = getArbFileName(lang);
23 + final missingKeys = getMissingKeysInArbFile(fileName, arbObj.keys);
24 + if (missingKeys.isNotEmpty) {
25 + final missingDefaults = <String, String>{};
26 +
27 + missingKeys.forEach((key) {
28 + print('Missing in "$lang": "$key"');
29 + if (doFix)
30 + missingDefaults[key] = arbObj[key] as String;
31 + });
32 +
33 + if (missingDefaults.isNotEmpty)
34 + await appendTranslations(lang, missingDefaults);
35 + }
36 + }
37 +}
tool/utils/translation/arb_file_utils.dart new
+66
@@ -0,0 +1,66 @@
1 +import 'dart:convert';
2 +import 'dart:io';
3 +
4 +void appendStringToArbFile(String fileName, String name, String text) {
5 + final file = File(fileName);
6 + final arbObj = readArbFile(file);
7 +
8 + if (arbObj.containsKey(name)) {
9 + print("String $name already exists in $fileName!");
10 + return;
11 + }
12 +
13 + arbObj.addAll({name: text});
14 +
15 + final outputContent = json
16 + .encode(arbObj)
17 + .replaceAll('","', '",\n "')
18 + .replaceAll('{"', '{\n "')
19 + .replaceAll('"}', '"\n}')
20 + .replaceAll('":"', '": "');
21 +
22 + file.writeAsStringSync(outputContent);
23 +}
24 +
25 +void appendStringsToArbFile(String fileName, Map<String, String> strings) {
26 + final file = File(fileName);
27 + final arbObj = readArbFile(file);
28 +
29 + arbObj.addAll(strings);
30 +
31 + final outputContent = json
32 + .encode(arbObj)
33 + .replaceAll('","', '",\n "')
34 + .replaceAll('{"', '{\n "')
35 + .replaceAll('"}', '"\n}')
36 + .replaceAll('":"', '": "');
37 +
38 + file.writeAsStringSync(outputContent);
39 +}
40 +
41 +Map<String, dynamic> readArbFile(File file) {
42 + final inputContent = file.readAsStringSync();
43 +
44 + return json.decode(inputContent) as Map<String, dynamic>;
45 +}
46 +
47 +String getArbFileName(String lang) {
48 + final shortLang = lang
49 + .split("-")
50 + .first;
51 + return "./res/values/strings_$shortLang.arb";
52 +}
53 +
54 +List<String> getMissingKeysInArbFile(String fileName, Iterable<String> langKeys) {
55 + final file = File(fileName);
56 + final arbObj = readArbFile(file);
57 + final results = <String>[];
58 +
59 + for (var langKey in langKeys) {
60 + if (!arbObj.containsKey(langKey)) {
61 + results.add(langKey);
62 + }
63 + }
64 +
65 + return results;
66 +}
tool/utils/translation/translation_constants.dart new
+6
@@ -0,0 +1,6 @@
1 +const defaultLang = "en";
2 +const langs = [
3 + "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "id", "it",
4 + "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tr", "uk", "ur", "yo",
5 + "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified)
6 +];
tool/utils/translation/translation_utils.dart new
+37
@@ -0,0 +1,37 @@
1 +import 'package:translator/translator.dart';
2 +
3 +import 'arb_file_utils.dart';
4 +import 'translation_constants.dart';
5 +
6 +final translator = GoogleTranslator();
7 +
8 +Future<void> appendTranslation(String lang, String key, String text) async {
9 + final fileName = getArbFileName(lang);
10 + final translation = await getTranslation(text, lang);
11 +
12 + appendStringToArbFile(fileName, key, translation);
13 +}
14 +
15 +Future<void> appendTranslations(String lang, Map<String, String> defaults) async {
16 + final fileName = getArbFileName(lang);
17 + final translations = <String, String>{};
18 +
19 + for (var key in defaults.keys) {
20 + final value = defaults[key]!;
21 +
22 + if (value.contains("{")) continue;
23 + final translation = await getTranslation(value, lang);
24 +
25 + translations[key] = translation;
26 + }
27 +
28 + print(translations);
29 +
30 + appendStringsToArbFile(fileName, translations);
31 +}
32 +
33 +Future<String> getTranslation(String text, String lang) async {
34 + if (lang == defaultLang) return text;
35 + return (await translator.translate(text, from: defaultLang, to: lang)).text;
36 +}
37 +