diff --git a/README.md b/README.md index 463fadfbc..1e7ebb458 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Cake Wallet -The project description, motivation, build scripts, instructions, tests will be added soon (Spring 2020); +The project description, motivation, build scripts, instructions, tests will be added soon (Spring 202X); Copyright (c) 2020 Cake Technologies LLC. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 136e5ce14..4513c32f6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ pathForWalletDir({@required String name, @required WalletType ty Future pathForWallet({@required String name, @required WalletType type}) async => await pathForWalletDir(name: name, type: type) .then((path) => path + '/$name'); + +Future outdatedAndroidPathForWalletDir({String name}) async { + final directory = await getApplicationDocumentsDirectory(); + final pathDir = directory.path + '/$name'; + + return pathDir; +} \ No newline at end of file diff --git a/lib/monero/monero_account_list.dart b/lib/monero/monero_account_list.dart index c8522b59b..9792d3c1d 100644 --- a/lib/monero/monero_account_list.dart +++ b/lib/monero/monero_account_list.dart @@ -20,7 +20,7 @@ abstract class MoneroAccountListBase with Store { bool _isRefreshing; bool _isUpdating; - Future update() async { + void update() async { if (_isUpdating) { return; } diff --git a/lib/monero/monero_wallet.dart b/lib/monero/monero_wallet.dart index 1eaab9b18..a68dbffff 100644 --- a/lib/monero/monero_wallet.dart +++ b/lib/monero/monero_wallet.dart @@ -125,8 +125,8 @@ abstract class MoneroWalletBase extends WalletBase with Store { _onAccountChangeReaction?.reaction?.dispose(); } - Future validate() async { - await accountList.update(); + bool validate() { + accountList.update(); final accountListLength = accountList.accounts?.length ?? 0; if (accountListLength <= 0) { diff --git a/lib/monero/monero_wallet_service.dart b/lib/monero/monero_wallet_service.dart index 91b9c184d..191b340e4 100644 --- a/lib/monero/monero_wallet_service.dart +++ b/lib/monero/monero_wallet_service.dart @@ -27,7 +27,7 @@ class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { class MoneroWalletLoadingException implements Exception { @override - String toString() => 'The wallet is damaged.'; + String toString() => 'Failure to load the wallet.'; } class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { @@ -93,6 +93,11 @@ class MoneroWalletService extends WalletService< Future openWallet(String name, String password) async { try { final path = await pathForWallet(name: name, type: WalletType.monero); + + if (!File(path).existsSync()) { + await repairOldAndroidWallet(name); + } + await monero_wallet_manager .openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values.firstWhere( @@ -100,17 +105,17 @@ class MoneroWalletService extends WalletService< orElse: () => null); final wallet = MoneroWallet( filename: monero_wallet.getFilename(), walletInfo: walletInfo); - final isValid = await wallet.validate(); + final isValid = wallet.validate(); if (!isValid) { // if (wallet.seed?.isNotEmpty ?? false) { - // let restore from seed in this case; - // final seed = wallet.seed; - // final credentials = MoneroRestoreWalletFromSeedCredentials( - // name: name, password: password, mnemonic: seed, height: 2000000) - // ..walletInfo = walletInfo; - // await remove(name); - // return restoreFromSeed(credentials); + // let restore from seed in this case; + // final seed = wallet.seed; + // final credentials = MoneroRestoreWalletFromSeedCredentials( + // name: name, password: password, mnemonic: seed, height: 2000000) + // ..walletInfo = walletInfo; + // await remove(name); + // return restoreFromSeed(credentials); // } throw MoneroWalletLoadingException(); @@ -187,4 +192,38 @@ class MoneroWalletService extends WalletService< rethrow; } } + + Future repairOldAndroidWallet(String name) async { + try { + if (!Platform.isAndroid) { + return; + } + + final oldAndroidWalletDirPath = + await outdatedAndroidPathForWalletDir(name: name); + final dir = Directory(oldAndroidWalletDirPath); + + if (!dir.existsSync()) { + throw MoneroWalletLoadingException(); + } + + final newWalletDirPath = + await pathForWalletDir(name: name, type: WalletType.monero); + + dir.listSync().forEach((f) { + final file = File(f.path); + final name = f.path.split('/').last; + final newPath = newWalletDirPath + '/$name'; + final newFile = File(newPath); + print(file.path); + if (!newFile.existsSync()) { + newFile.createSync(); + } + newFile.writeAsBytesSync(file.readAsBytesSync()); + }); + } catch (e) { + print(e.toString()); + throw MoneroWalletLoadingException(); + } + } } diff --git a/lib/monero/pending_monero_transaction.dart b/lib/monero/pending_monero_transaction.dart index ef18a8f12..c939110a0 100644 --- a/lib/monero/pending_monero_transaction.dart +++ b/lib/monero/pending_monero_transaction.dart @@ -5,6 +5,14 @@ import 'package:cake_wallet/entities/crypto_currency.dart'; import 'package:cake_wallet/core/amount_converter.dart'; import 'package:cake_wallet/core/pending_transaction.dart'; +class DoubleSpendException implements Exception { + DoubleSpendException(); + + @override + String toString() => + 'This transaction cannot be committed. This can be due to many reasons including the wallet not being synced, there is not enough XMR in your available balance, or previous transactions are not yet fully processed.'; +} + class PendingMoneroTransaction with PendingTransaction { PendingMoneroTransaction(this.pendingTransactionDescription); @@ -22,7 +30,18 @@ class PendingMoneroTransaction with PendingTransaction { CryptoCurrency.xmr, pendingTransactionDescription.fee); @override - Future commit() async => + Future commit() async { + try { monero_transaction_history.commitTransactionFromPointerAddress( address: pendingTransactionDescription.pointerAddress); + } catch (e) { + final message = e.toString(); + + if (message.contains('Reason: double spend')) { + throw DoubleSpendException(); + } + + rethrow; + } + } } diff --git a/lib/src/screens/auth/auth_page.dart b/lib/src/screens/auth/auth_page.dart index 82d765fd6..36dc2204b 100644 --- a/lib/src/screens/auth/auth_page.dart +++ b/lib/src/screens/auth/auth_page.dart @@ -40,13 +40,14 @@ class AuthPageState extends State { reaction((_) => widget.authViewModel.state, (ExecutionState state) { if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) { + _authBar?.dismiss(); if (widget.onAuthenticationFinished != null) { widget.onAuthenticationFinished(true, this); } else { - _authBar?.dismiss(); showBar(context, S.of(context).authenticated); } }); + setState(() {}); } if (state is IsExecutingState) { diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 202784ee8..11d063cfd 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -15,9 +15,10 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_h import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; class ReceivePage extends BasePage { - ReceivePage({this.addressListViewModel}); + ReceivePage({this.addressListViewModel}) : _cryptoAmountFocus = FocusNode(); final WalletAddressListViewModel addressListViewModel; @@ -33,6 +34,8 @@ class ReceivePage extends BasePage { @override Color get titleColor => Colors.white; + final FocusNode _cryptoAmountFocus; + @override Widget Function(BuildContext, Widget) get rootWrapper => (BuildContext context, Widget scaffold) => Container( @@ -67,53 +70,68 @@ class ReceivePage extends BasePage { @override Widget body(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(24, 80, 24, 40), - child: QRWidget( - addressListViewModel: addressListViewModel, - isAmountFieldShow: true, - ), - ), - Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => Container( - height: 1, color: Theme.of(context).dividerColor), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); + return KeyboardActions( + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: isDarkTheme + ? Color.fromRGBO(48, 51, 60, 1.0) + : Color.fromRGBO(98, 98, 98, 1.0), + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _cryptoAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ]), + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(24, 80, 24, 40), + child: QRWidget( + addressListViewModel: addressListViewModel, + isAmountFieldShow: true, + amountTextFieldFocusNode: _cryptoAmountFocus), + ), + Observer( + builder: (_) => ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => Container( + height: 1, color: Theme.of(context).dividerColor), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: addressListViewModel.items.length, + itemBuilder: (context, index) { + final item = addressListViewModel.items[index]; + Widget cell = Container(); - if (item is WalletAccountListHeader) { - cell = HeaderTile( - onTap: () async => await showPopUp( - context: context, - builder: (_) => - getIt.get()), - title: S.of(context).accounts, - icon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).textTheme.display1.color, - )); - } + if (item is WalletAccountListHeader) { + cell = HeaderTile( + onTap: () async => await showPopUp( + context: context, + builder: (_) => + getIt.get()), + title: S.of(context).accounts, + icon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: + Theme.of(context).textTheme.display1.color, + )); + } - if (item is WalletAddressListHeader) { - cell = HeaderTile( - onTap: () => Navigator.of(context) - .pushNamed(Routes.newSubaddress), - title: S.of(context).addresses, - icon: Icon( - Icons.add, - size: 20, - color: Theme.of(context).textTheme.display1.color, - )); - } + if (item is WalletAddressListHeader) { + cell = HeaderTile( + onTap: () => Navigator.of(context) + .pushNamed(Routes.newSubaddress), + title: S.of(context).addresses, + icon: Icon( + Icons.add, + size: 20, + color: + Theme.of(context).textTheme.display1.color, + )); + } if (item is WalletAddressListItem) { final isFirst = addressListViewModel.isFirstAddress; @@ -146,17 +164,17 @@ class ReceivePage extends BasePage { }); } - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30)), - child: cell, - ); - })), - ], - ), - ); + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30)), + child: cell, + ); + })), + ], + ), + )); } } diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 46daa7f9e..ba2101d93 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -11,7 +11,9 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_v class QRWidget extends StatelessWidget { QRWidget( - {@required this.addressListViewModel, this.isAmountFieldShow = false}) + {@required this.addressListViewModel, + this.isAmountFieldShow = false, + this.amountTextFieldFocusNode}) : amountController = TextEditingController(), _formKey = GlobalKey() { amountController.addListener(() => addressListViewModel.amount = @@ -21,6 +23,7 @@ class QRWidget extends StatelessWidget { final WalletAddressListViewModel addressListViewModel; final bool isAmountFieldShow; final TextEditingController amountController; + final FocusNode amountTextFieldFocusNode; final GlobalKey _formKey; @override @@ -45,7 +48,7 @@ class QRWidget extends StatelessWidget { data: addressListViewModel.uri.toString(), backgroundColor: Colors.transparent, foregroundColor: Colors.white, - //Theme.of(context).textTheme.headline.color, + //Theme.of(context).textTheme.headline.color, ))))), Spacer(flex: 3) ]), @@ -68,6 +71,7 @@ class QRWidget extends StatelessWidget { child: Form( key: _formKey, child: BaseTextFormField( + focusNode: amountTextFieldFocusNode, controller: amountController, keyboardType: TextInputType.numberWithOptions( decimal: true), diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index e444bc687..ddebbffa0 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -77,7 +77,7 @@ class SeedWidgetState extends State { fontSize: 16.0, color: Theme.of(context).hintColor))), Padding( padding: EdgeInsets.only(right: 40, top: 10), - child: ValidableAnnotatedEditableText( + child: ValidatableAnnotatedEditableText( cursorColor: Colors.blue, backgroundCursorColor: Colors.blue, validStyle: TextStyle( diff --git a/lib/src/widgets/validable_annotated_editable_text.dart b/lib/src/widgets/validable_annotated_editable_text.dart index e603c0327..37bde13a2 100644 --- a/lib/src/widgets/validable_annotated_editable_text.dart +++ b/lib/src/widgets/validable_annotated_editable_text.dart @@ -22,8 +22,8 @@ class TextAnnotation extends Comparable { int compareTo(TextAnnotation other) => text.compareTo(other.text); } -class ValidableAnnotatedEditableText extends EditableText { - ValidableAnnotatedEditableText({ +class ValidatableAnnotatedEditableText extends EditableText { + ValidatableAnnotatedEditableText({ Key key, FocusNode focusNode, TextEditingController controller, @@ -49,7 +49,7 @@ class ValidableAnnotatedEditableText extends EditableText { controller: controller, cursorColor: cursorColor, style: validStyle, - keyboardType: TextInputType.text, + keyboardType: TextInputType.visiblePassword, autocorrect: false, autofocus: false, selectionColor: selectionColor, @@ -73,14 +73,14 @@ class ValidableAnnotatedEditableText extends EditableText { final TextStyle invalidStyle; @override - ValidableAnnotatedEditableTextState createState() => - ValidableAnnotatedEditableTextState(); + ValidatableAnnotatedEditableTextState createState() => + ValidatableAnnotatedEditableTextState(); } -class ValidableAnnotatedEditableTextState extends EditableTextState { +class ValidatableAnnotatedEditableTextState extends EditableTextState { @override - ValidableAnnotatedEditableText get widget => - super.widget as ValidableAnnotatedEditableText; + ValidatableAnnotatedEditableText get widget => + super.widget as ValidatableAnnotatedEditableText; List getRanges() { final result = List(); diff --git a/lib/view_model/auth_view_model.dart b/lib/view_model/auth_view_model.dart index 76df95283..5bf5c25a1 100644 --- a/lib/view_model/auth_view_model.dart +++ b/lib/view_model/auth_view_model.dart @@ -110,6 +110,8 @@ abstract class AuthViewModelBase with Store { if (isAuthenticated) { state = ExecutedSuccessfullyState(); + } else { + state = FailureState('Failure biometric authentication'); } } } catch(e) { diff --git a/pubspec.lock b/pubspec.lock index 87132a57e..f254541af 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -401,10 +401,10 @@ packages: description: path: "." ref: cake - resolved-ref: a734c2ea3239f9153dba6f5bec740e1df54ee754 + resolved-ref: d4d68a9c1e4c45eb236cd7a5a2fac84c394a7605 url: "https://github.com/cake-tech/flutter_secure_storage.git" source: git - version: "3.3.55" + version: "3.3.57" flutter_slidable: dependency: "direct main" description: @@ -498,7 +498,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.18" + version: "2.1.19" intl: dependency: "direct main" description: @@ -1000,7 +1000,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.3" + version: "1.7.4" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 81461d475..0634e3f2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Cake Wallet. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 4.0.4+14 +version: 4.0.8+20 environment: sdk: ">=2.7.0 <3.0.0" @@ -31,6 +31,7 @@ dependencies: git: url: https://github.com/cake-tech/flutter_secure_storage.git ref: cake + version: 3.3.57 provider: ^3.1.0 rxdart: ^0.22.2 yaml: ^2.1.16