diff --git a/lib/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_cubit.dart b/lib/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_cubit.dart new file mode 100644 index 00000000..bad35060 --- /dev/null +++ b/lib/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_cubit.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:miro/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_state.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart'; +import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart'; + +class SignInKeyfileDrawerPageCubit extends Cubit { + final KeyfileDropzoneCubit keyfileDropzoneCubit; + final TextEditingController passwordTextEditingController; + late final StreamSubscription _keyfileDropzoneStateSubscription; + + SignInKeyfileDrawerPageCubit({ + required this.keyfileDropzoneCubit, + required this.passwordTextEditingController, + }) : super(const SignInKeyfileDrawerPageState()) { + _keyfileDropzoneStateSubscription = keyfileDropzoneCubit.stream.listen(_listenKeyfileChange); + } + + @override + Future close() { + keyfileDropzoneCubit.close(); + _keyfileDropzoneStateSubscription.cancel(); + return super.close(); + } + + void notifyPasswordChanged() { + bool keyfileUploadedBool = keyfileDropzoneCubit.state.hasKeyfile; + if (keyfileUploadedBool) { + _decryptKeyfile(); + } + } + + void _listenKeyfileChange(KeyfileDropzoneState keyfileDropzoneState) { + bool keyfileValidBool = keyfileDropzoneState.keyfileExceptionType == null; + if (keyfileValidBool) { + _decryptKeyfile(); + } else { + emit(SignInKeyfileDrawerPageState(keyfileExceptionType: keyfileDropzoneState.keyfileExceptionType)); + } + } + + void _decryptKeyfile() { + try { + String password = passwordTextEditingController.text; + EncryptedKeyfileModel encryptedKeyfileModel = keyfileDropzoneCubit.state.encryptedKeyfileModel!; + DecryptedKeyfileModel decryptedKeyfileModel = encryptedKeyfileModel.decrypt(password); + + emit(SignInKeyfileDrawerPageState(decryptedKeyfileModel: decryptedKeyfileModel)); + } on KeyfileException catch (e) { + emit(SignInKeyfileDrawerPageState(keyfileExceptionType: e.keyfileExceptionType)); + } catch (e) { + emit(const SignInKeyfileDrawerPageState(keyfileExceptionType: KeyfileExceptionType.invalidKeyfile)); + } + } +} diff --git a/lib/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_state.dart b/lib/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_state.dart new file mode 100644 index 00000000..0d1cc12f --- /dev/null +++ b/lib/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_state.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart'; + +class SignInKeyfileDrawerPageState extends Equatable { + final KeyfileExceptionType? keyfileExceptionType; + final DecryptedKeyfileModel? decryptedKeyfileModel; + + const SignInKeyfileDrawerPageState({ + this.keyfileExceptionType, + this.decryptedKeyfileModel, + }); + + @override + List get props => [keyfileExceptionType, decryptedKeyfileModel]; +} diff --git a/lib/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart b/lib/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart new file mode 100644 index 00000000..da8692fa --- /dev/null +++ b/lib/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; +import 'dart:html' as html; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart'; +import 'package:miro/shared/entity/keyfile/keyfile_entity.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/generic/file_model.dart'; +import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart'; +import 'package:miro/shared/utils/logger/app_logger.dart'; + +class KeyfileDropzoneCubit extends Cubit { + KeyfileDropzoneCubit() : super(KeyfileDropzoneState.empty()); + + Future uploadFileViaHtml(dynamic htmlFile) async { + if (htmlFile is html.File) { + updateSelectedFile(await FileModel.fromHtmlFile(htmlFile)); + } else { + AppLogger().log(message: 'Unsupported file type ${htmlFile.runtimeType}'); + } + } + + Future uploadFileManually() async { + FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles(allowMultiple: false); + if (filePickerResult == null) { + return; + } + PlatformFile platformFile = filePickerResult.files.single; + if (platformFile.bytes == null) { + return; + } + + updateSelectedFile(FileModel.fromPlatformFile(platformFile)); + } + + void updateSelectedFile(FileModel fileModel) { + try { + Map keyfileJson = jsonDecode(fileModel.content) as Map; + KeyfileEntity keyfileEntity = KeyfileEntity.fromJson(keyfileJson); + EncryptedKeyfileModel encryptedKeyfileModel = EncryptedKeyfileModel.fromEntity(keyfileEntity); + emit(KeyfileDropzoneState(encryptedKeyfileModel: encryptedKeyfileModel, fileModel: fileModel)); + } on KeyfileException catch (keyfileException) { + AppLogger().log(message: keyfileException.keyfileExceptionType.toString()); + emit(KeyfileDropzoneState(keyfileExceptionType: keyfileException.keyfileExceptionType, fileModel: fileModel)); + } catch (e) { + AppLogger().log(message: 'Invalid keyfile: $e'); + emit(KeyfileDropzoneState(keyfileExceptionType: KeyfileExceptionType.invalidKeyfile, fileModel: fileModel)); + } + } +} diff --git a/lib/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart b/lib/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart new file mode 100644 index 00000000..b8fca11c --- /dev/null +++ b/lib/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/generic/file_model.dart'; +import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart'; + +class KeyfileDropzoneState extends Equatable { + final EncryptedKeyfileModel? encryptedKeyfileModel; + final FileModel? fileModel; + final KeyfileExceptionType? keyfileExceptionType; + + const KeyfileDropzoneState({ + this.encryptedKeyfileModel, + this.fileModel, + this.keyfileExceptionType, + }); + + factory KeyfileDropzoneState.empty() { + return const KeyfileDropzoneState(); + } + + bool get hasFile => fileModel != null; + + bool get hasKeyfile => encryptedKeyfileModel != null; + + @override + List get props => [encryptedKeyfileModel, fileModel, keyfileExceptionType]; +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 058a0ea8..0a2f2a5e 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -29,29 +29,31 @@ class MessageLookup extends MessageLookupByLibrary { static String m3(amount) => "Tip must be greater or equal ${amount}"; - static String m4(separator, networkName, parsedRemainingTime) => + static String m4(version) => "Keyfile version ${version}"; + + static String m5(separator, networkName, parsedRemainingTime) => "Connecting to <${networkName}>${separator} Please wait... ${parsedRemainingTime}"; - static String m5(errorsCount) => "Found ${errorsCount} problems with server"; + static String m6(errorsCount) => "Found ${errorsCount} problems with server"; - static String m6(latestBlockTime) => + static String m7(latestBlockTime) => "The last available block on this interx was created long time ago ${latestBlockTime}. The displayed contents may be out of date."; - static String m7(seconds) => "Refresh in ${seconds} sec."; + static String m8(seconds) => "Refresh in ${seconds} sec."; - static String m8(availableAmountText, tokenDenominationModelName) => + static String m9(availableAmountText, tokenDenominationModelName) => "Available: ${availableAmountText} ${tokenDenominationModelName}"; - static String m9(hash) => "Transaction hash: 0x${hash}"; + static String m10(hash) => "Transaction hash: 0x${hash}"; - static String m10(amount) => "+ ${amount} more"; + static String m11(amount) => "+ ${amount} more"; - static String m11(widgetFeeTokenAmountModel) => + static String m12(widgetFeeTokenAmountModel) => "Transaction fee ${widgetFeeTokenAmountModel}"; - static String m12(txMsgType) => "Preview for ${txMsgType} unavailable"; + static String m13(txMsgType) => "Preview for ${txMsgType} unavailable"; - static String m13(selected) => "${selected} selected"; + static String m14(selected) => "${selected} selected"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -233,13 +235,17 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Enter password"), "keyfileErrorCannotBeEmpty": MessageLookupByLibrary.simpleMessage("Keyfile cannot be empty"), + "keyfileErrorInvalid": + MessageLookupByLibrary.simpleMessage("Invalid Keyfile"), "keyfileErrorPasswordsMatch": MessageLookupByLibrary.simpleMessage("Passwords don\'t match"), + "keyfileErrorUnsupportedVersion": + MessageLookupByLibrary.simpleMessage("Unsupported version"), + "keyfileErrorWrongPassword": + MessageLookupByLibrary.simpleMessage("Wrong password"), "keyfileHintPassword": MessageLookupByLibrary.simpleMessage("Password"), "keyfileHintRepeatPassword": MessageLookupByLibrary.simpleMessage("Repeat password"), - "keyfileInvalid": - MessageLookupByLibrary.simpleMessage("Invalid Keyfile"), "keyfileSignIn": MessageLookupByLibrary.simpleMessage("Sign in with Keyfile"), "keyfileTip": MessageLookupByLibrary.simpleMessage( @@ -252,10 +258,9 @@ class MessageLookup extends MessageLookupByLibrary { "Drop Keyfile to the dropzone"), "keyfileToastDownloaded": MessageLookupByLibrary.simpleMessage("Keyfile downloaded"), + "keyfileVersion": m4, "keyfileWarning": MessageLookupByLibrary.simpleMessage( "You won’t be able to download it again"), - "keyfileWrongPassword": - MessageLookupByLibrary.simpleMessage("Wrong password"), "kiraNetwork": MessageLookupByLibrary.simpleMessage("Kira Network"), "mnemonic": MessageLookupByLibrary.simpleMessage("Mnemonic"), "mnemonicEnter": @@ -312,7 +317,7 @@ class MessageLookup extends MessageLookupByLibrary { "networkCheckedConnection": MessageLookupByLibrary.simpleMessage("Checked connection"), "networkChoose": MessageLookupByLibrary.simpleMessage("Choose network"), - "networkConnectingTo": m4, + "networkConnectingTo": m5, "networkConnectionCancelled": MessageLookupByLibrary.simpleMessage("Connection cancelled"), "networkConnectionEstablished": @@ -327,7 +332,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("undefined"), "networkHintCustomAddress": MessageLookupByLibrary.simpleMessage("Custom address"), - "networkHowManyProblems": m5, + "networkHowManyProblems": m6, "networkList": MessageLookupByLibrary.simpleMessage("List of networks"), "networkNoAvailable": MessageLookupByLibrary.simpleMessage("No available networks"), @@ -349,7 +354,7 @@ class MessageLookup extends MessageLookupByLibrary { "The application is incompatible with this server. Some views may not work correctly."), "networkWarningMissingInfo": MessageLookupByLibrary.simpleMessage( "Connecting a wallet unavailable due to missing essential data from network."), - "networkWarningWhenLastBlock": m6, + "networkWarningWhenLastBlock": m7, "or": MessageLookupByLibrary.simpleMessage("or "), "paginatedListPageSize": MessageLookupByLibrary.simpleMessage("Page size"), @@ -363,7 +368,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Successful"), "proposalsVoters": MessageLookupByLibrary.simpleMessage("Voters"), "refresh": MessageLookupByLibrary.simpleMessage("Refresh"), - "refreshInSeconds": m7, + "refreshInSeconds": m8, "sec": MessageLookupByLibrary.simpleMessage("sec."), "seeAll": MessageLookupByLibrary.simpleMessage("See all"), "seeMore": MessageLookupByLibrary.simpleMessage("See more"), @@ -424,7 +429,7 @@ class MessageLookup extends MessageLookupByLibrary { "toastSuccessfullyCopied": MessageLookupByLibrary.simpleMessage("Successfully copied"), "tx": MessageLookupByLibrary.simpleMessage("Transactions"), - "txAvailableBalances": m8, + "txAvailableBalances": m9, "txButtonBackToAccount": MessageLookupByLibrary.simpleMessage("Back to account"), "txButtonClaimAllRewards": @@ -477,7 +482,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("See more on Explorer"), "txFetchingRemoteData": MessageLookupByLibrary.simpleMessage( "Fetching remote data. Please wait..."), - "txHash": m9, + "txHash": m10, "txHintAmountToClaim": MessageLookupByLibrary.simpleMessage("Amount to claim"), "txHintClaim": MessageLookupByLibrary.simpleMessage("Claim"), @@ -498,7 +503,7 @@ class MessageLookup extends MessageLookupByLibrary { "txListAmountFeesOnly": MessageLookupByLibrary.simpleMessage("Fees only"), "txListAmountPlusFees": MessageLookupByLibrary.simpleMessage("+ fees"), - "txListAmountPlusMore": m10, + "txListAmountPlusMore": m11, "txListDate": MessageLookupByLibrary.simpleMessage("Date"), "txListDetails": MessageLookupByLibrary.simpleMessage("Details"), "txListDirection": MessageLookupByLibrary.simpleMessage("Direction"), @@ -536,10 +541,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Unknown transaction type"), "txMsgUndelegate": MessageLookupByLibrary.simpleMessage("Unstake Tokens"), - "txNoticeFee": m11, + "txNoticeFee": m12, "txPleaseSelectToken": MessageLookupByLibrary.simpleMessage("Please select a token"), - "txPreviewUnavailable": m12, + "txPreviewUnavailable": m13, "txRecipientWillGet": MessageLookupByLibrary.simpleMessage("Recipient will get"), "txSearchTokens": MessageLookupByLibrary.simpleMessage("Search tokens"), @@ -569,7 +574,7 @@ class MessageLookup extends MessageLookupByLibrary { "validatorsAbout": MessageLookupByLibrary.simpleMessage("About Validator"), "validatorsActive": MessageLookupByLibrary.simpleMessage("Active"), - "validatorsButtonFilter": m13, + "validatorsButtonFilter": m14, "validatorsDropdownAll": MessageLookupByLibrary.simpleMessage("All"), "validatorsHintSearch": MessageLookupByLibrary.simpleMessage("Search validators"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 5d4bae30..012fad5b 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -977,6 +977,16 @@ class S { ); } + /// `Invalid Keyfile` + String get keyfileErrorInvalid { + return Intl.message( + 'Invalid Keyfile', + name: 'keyfileErrorInvalid', + desc: '', + args: [], + ); + } + /// `Passwords don't match` String get keyfileErrorPasswordsMatch { return Intl.message( @@ -987,6 +997,26 @@ class S { ); } + /// `Unsupported version` + String get keyfileErrorUnsupportedVersion { + return Intl.message( + 'Unsupported version', + name: 'keyfileErrorUnsupportedVersion', + desc: '', + args: [], + ); + } + + /// `Wrong password` + String get keyfileErrorWrongPassword { + return Intl.message( + 'Wrong password', + name: 'keyfileErrorWrongPassword', + desc: '', + args: [], + ); + } + /// `Password` String get keyfileHintPassword { return Intl.message( @@ -1087,31 +1117,21 @@ class S { ); } - /// `Enter password` - String get keyfileEnterPassword { - return Intl.message( - 'Enter password', - name: 'keyfileEnterPassword', - desc: '', - args: [], - ); - } - - /// `Invalid Keyfile` - String get keyfileInvalid { + /// `Drop file` + String get keyfileDropFile { return Intl.message( - 'Invalid Keyfile', - name: 'keyfileInvalid', + 'Drop file', + name: 'keyfileDropFile', desc: '', args: [], ); } - /// `Wrong password` - String get keyfileWrongPassword { + /// `Enter password` + String get keyfileEnterPassword { return Intl.message( - 'Wrong password', - name: 'keyfileWrongPassword', + 'Enter password', + name: 'keyfileEnterPassword', desc: '', args: [], ); @@ -1127,13 +1147,13 @@ class S { ); } - /// `Drop file` - String get keyfileDropFile { + /// `Keyfile version {version}` + String keyfileVersion(String version) { return Intl.message( - 'Drop file', - name: 'keyfileDropFile', + 'Keyfile version $version', + name: 'keyfileVersion', desc: '', - args: [], + args: [version], ); } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 281ab19e..e58ee6a6 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -134,7 +134,10 @@ "keyfile": "Keyfile", "keyfileButtonDownload": "Download", "keyfileErrorCannotBeEmpty": "Keyfile cannot be empty", + "keyfileErrorInvalid": "Invalid Keyfile", "keyfileErrorPasswordsMatch": "Passwords don't match", + "keyfileErrorUnsupportedVersion": "Unsupported version", + "keyfileErrorWrongPassword": "Wrong password", "keyfileHintPassword": "Password", "keyfileHintRepeatPassword": "Repeat password", "keyfileTip": "Keyfile is a file which contains encrypted data.", @@ -145,11 +148,17 @@ "keyfileSignIn": "Sign in with Keyfile", "keyfileToDropzone": "Drop Keyfile to the dropzone", "keyfileDropHere": "Please drop Keyfile here", + "keyfileDropFile": "Drop file", "keyfileEnterPassword": "Enter password", - "keyfileInvalid": "Invalid Keyfile", - "keyfileWrongPassword": "Wrong password", "keyfileCreatePassword": "Create password for keyfile", - "keyfileDropFile": "Drop file", + "keyfileVersion": "Keyfile version {version}", + "@keyfileVersion": { + "placeholders": { + "version": { + "type": "String" + } + } + }, "mnemonic": "Mnemonic", "mnemonicErrorUnexpected": "Something unexpected happened", diff --git a/lib/shared/entity/keyfile/keyfile_entity.dart b/lib/shared/entity/keyfile/keyfile_entity.dart new file mode 100644 index 00000000..289e1344 --- /dev/null +++ b/lib/shared/entity/keyfile/keyfile_entity.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; + +class KeyfileEntity extends Equatable { + final String version; + final String publicKey; + final String secretData; + + const KeyfileEntity({ + required this.version, + required this.publicKey, + required this.secretData, + }); + + factory KeyfileEntity.fromJson(Map json) { + try { + return KeyfileEntity( + version: json['version'] as String, + publicKey: json['public_key'] as String, + secretData: json['secret_data'] as String, + ); + } catch (_) { + throw const KeyfileException(KeyfileExceptionType.invalidKeyfile); + } + } + + Map toJson() { + return { + 'version': version, + 'public_key': publicKey, + 'secret_data': secretData, + }; + } + + @override + List get props => [version, publicKey, secretData]; +} diff --git a/lib/shared/entity/keyfile/keyfile_secret_data_entity.dart b/lib/shared/entity/keyfile/keyfile_secret_data_entity.dart new file mode 100644 index 00000000..5a822beb --- /dev/null +++ b/lib/shared/entity/keyfile/keyfile_secret_data_entity.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; + +class KeyfileSecretDataEntity extends Equatable { + final String privateKey; + + const KeyfileSecretDataEntity({ + required this.privateKey, + }); + + factory KeyfileSecretDataEntity.fromJson(Map json) { + return KeyfileSecretDataEntity( + privateKey: json['private_key'] as String, + ); + } + + Map toJson() { + return { + 'private_key': privateKey, + }; + } + + @override + List get props => [privateKey]; +} diff --git a/lib/shared/exceptions/invalid_keyfile_exception.dart b/lib/shared/exceptions/invalid_keyfile_exception.dart deleted file mode 100644 index 975d1eca..00000000 --- a/lib/shared/exceptions/invalid_keyfile_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class InvalidKeyFileException implements Exception { - final dynamic message; - - InvalidKeyFileException([this.message]); -} diff --git a/lib/shared/exceptions/invalid_password_exception.dart b/lib/shared/exceptions/invalid_password_exception.dart deleted file mode 100644 index 566b8e62..00000000 --- a/lib/shared/exceptions/invalid_password_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class InvalidPasswordException implements Exception { - final dynamic message; - - InvalidPasswordException([this.message]); -} diff --git a/lib/shared/exceptions/keyfile_exception/keyfile_exception.dart b/lib/shared/exceptions/keyfile_exception/keyfile_exception.dart new file mode 100644 index 00000000..fe105407 --- /dev/null +++ b/lib/shared/exceptions/keyfile_exception/keyfile_exception.dart @@ -0,0 +1,11 @@ +import 'package:equatable/equatable.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; + +class KeyfileException extends Equatable implements Exception { + final KeyfileExceptionType keyfileExceptionType; + + const KeyfileException(this.keyfileExceptionType); + + @override + List get props => [keyfileExceptionType]; +} diff --git a/lib/shared/exceptions/keyfile_exception/keyfile_exception_type.dart b/lib/shared/exceptions/keyfile_exception/keyfile_exception_type.dart new file mode 100644 index 00000000..b9f8db5b --- /dev/null +++ b/lib/shared/exceptions/keyfile_exception/keyfile_exception_type.dart @@ -0,0 +1,4 @@ +enum KeyfileExceptionType { + invalidKeyfile, + wrongPassword, +} diff --git a/lib/shared/models/generic/file_model.dart b/lib/shared/models/generic/file_model.dart new file mode 100644 index 00000000..ddfe6de6 --- /dev/null +++ b/lib/shared/models/generic/file_model.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'dart:html' as html; +import 'dart:math'; + +import 'package:equatable/equatable.dart'; +import 'package:file_picker/file_picker.dart'; + +class FileModel extends Equatable { + final int size; + final String name; + final String content; + final String? extension; + + const FileModel({ + required this.size, + required this.name, + required this.content, + required this.extension, + }); + + factory FileModel.fromPlatformFile(PlatformFile platformFile) { + final List fileBytes = platformFile.bytes ?? List.empty(); + return FileModel( + name: platformFile.name, + size: platformFile.size, + extension: platformFile.extension, + content: String.fromCharCodes(fileBytes), + ); + } + + static Future fromHtmlFile(html.File htmlFile) async { + final Completer kiraDropzoneFileModelCompleter = Completer(); + final html.FileReader htmlFileReader = html.FileReader()..readAsText(htmlFile); + final StreamSubscription fileUploadStream = htmlFileReader.onLoadEnd.listen((_) { + String result = htmlFileReader.result.toString(); + FileModel kiraDropzoneFileModel = FileModel( + name: htmlFile.name, + size: htmlFile.size, + extension: htmlFile.name.split('.').last, + content: result, + ); + kiraDropzoneFileModelCompleter.complete(kiraDropzoneFileModel); + }); + FileModel kiraDropzoneFileModel = await kiraDropzoneFileModelCompleter.future; + await fileUploadStream.cancel(); + return kiraDropzoneFileModel; + } + + String get sizeString { + assert(size >= 0, 'File size must be greater than or equal to 0'); + List siSuffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + int unitIndex = (log(size) / log(1024)).floor(); + num unitValue = size / pow(1024, unitIndex); + String unitString = unitValue.toStringAsFixed(1); + if (unitValue.toStringAsFixed(1).endsWith('.0')) { + unitString = unitValue.toInt().toString(); + } + return '$unitString ${siSuffixes[unitIndex]}'; + } + + @override + List get props => [name, extension, content, size]; +} diff --git a/lib/shared/models/keyfile/decrypted_keyfile_model.dart b/lib/shared/models/keyfile/decrypted_keyfile_model.dart new file mode 100644 index 00000000..023108d5 --- /dev/null +++ b/lib/shared/models/keyfile/decrypted_keyfile_model.dart @@ -0,0 +1,50 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:equatable/equatable.dart'; +import 'package:hex/hex.dart'; +import 'package:miro/shared/entity/keyfile/keyfile_entity.dart'; +import 'package:miro/shared/entity/keyfile/keyfile_secret_data_entity.dart'; +import 'package:miro/shared/models/keyfile/keyfile_secret_data_model.dart'; +import 'package:miro/shared/models/wallet/wallet.dart'; +import 'package:miro/shared/utils/cryptography/aes256.dart'; +import 'package:miro/shared/utils/cryptography/secp256k1.dart'; + +class DecryptedKeyfileModel extends Equatable { + static String latestKeyfileVersion = '2.0.0'; + final KeyfileSecretDataModel keyfileSecretDataModel; + final String? version; + + const DecryptedKeyfileModel({ + required this.keyfileSecretDataModel, + this.version, + }); + + String buildFileContent(String password) { + KeyfileSecretDataEntity keyfileSecretDataEntity = KeyfileSecretDataEntity( + privateKey: HEX.encode(keyfileSecretDataModel.wallet.privateKey), + ); + + String secretData = Aes256.encrypt(password, jsonEncode(keyfileSecretDataEntity.toJson())); + Uint8List publicKeyBytes = Secp256k1.privateKeyBytesToPublic(keyfileSecretDataModel.wallet.privateKey); + + KeyfileEntity keyfileEntity = KeyfileEntity( + version: latestKeyfileVersion, + publicKey: base64Encode(publicKeyBytes), + secretData: secretData, + ); + + Map fileContentJson = keyfileEntity.toJson(); + JsonEncoder encoder = const JsonEncoder.withIndent(' '); + String jsonContent = encoder.convert(fileContentJson); + return jsonContent; + } + + String get fileName { + Wallet wallet = keyfileSecretDataModel.wallet; + return 'keyfile_${wallet.address.buildBech32AddressShort(delimiter: '_')}.json'; + } + + @override + List get props => [keyfileSecretDataModel, version]; +} diff --git a/lib/shared/models/keyfile/encrypted_keyfile_model.dart b/lib/shared/models/keyfile/encrypted_keyfile_model.dart new file mode 100644 index 00000000..13df6329 --- /dev/null +++ b/lib/shared/models/keyfile/encrypted_keyfile_model.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:equatable/equatable.dart'; +import 'package:hex/hex.dart'; +import 'package:miro/shared/entity/keyfile/keyfile_entity.dart'; +import 'package:miro/shared/entity/keyfile/keyfile_secret_data_entity.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart'; +import 'package:miro/shared/models/keyfile/keyfile_secret_data_model.dart'; +import 'package:miro/shared/models/wallet/wallet.dart'; +import 'package:miro/shared/models/wallet/wallet_address.dart'; +import 'package:miro/shared/utils/cryptography/aes256.dart'; + +class EncryptedKeyfileModel extends Equatable { + final String version; + final String encryptedSecretData; + final Uint8List publicKey; + + const EncryptedKeyfileModel({ + required this.version, + required this.encryptedSecretData, + required this.publicKey, + }); + + factory EncryptedKeyfileModel.fromEntity(KeyfileEntity keyfileEntity) { + return EncryptedKeyfileModel( + version: keyfileEntity.version, + encryptedSecretData: keyfileEntity.secretData, + publicKey: base64Decode(keyfileEntity.publicKey), + ); + } + + DecryptedKeyfileModel decrypt(String password) { + bool passwordValidBool = Aes256.verifyPassword(password, encryptedSecretData); + if (passwordValidBool == false) { + throw const KeyfileException(KeyfileExceptionType.wrongPassword); + } + + late KeyfileSecretDataEntity keyfileSecretDataEntity; + try { + Map secretDataJson = jsonDecode(Aes256.decrypt(password, encryptedSecretData)) as Map; + keyfileSecretDataEntity = KeyfileSecretDataEntity.fromJson(secretDataJson); + } catch (_) { + throw const KeyfileException(KeyfileExceptionType.invalidKeyfile); + } + + return DecryptedKeyfileModel( + version: version, + keyfileSecretDataModel: KeyfileSecretDataModel( + wallet: Wallet( + address: WalletAddress.fromPublicKey(publicKey), + privateKey: Uint8List.fromList(HEX.decode(keyfileSecretDataEntity.privateKey)), + ), + ), + ); + } + + @override + List get props => [version, encryptedSecretData, publicKey]; +} diff --git a/lib/shared/models/keyfile/keyfile_secret_data_model.dart b/lib/shared/models/keyfile/keyfile_secret_data_model.dart new file mode 100644 index 00000000..ecf07653 --- /dev/null +++ b/lib/shared/models/keyfile/keyfile_secret_data_model.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:miro/shared/models/wallet/wallet.dart'; + +class KeyfileSecretDataModel extends Equatable { + final Wallet wallet; + + const KeyfileSecretDataModel({ + required this.wallet, + }); + + @override + List get props => [wallet]; +} diff --git a/lib/shared/models/wallet/keyfile.dart b/lib/shared/models/wallet/keyfile.dart deleted file mode 100644 index c662c484..00000000 --- a/lib/shared/models/wallet/keyfile.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; -import 'package:hex/hex.dart'; -import 'package:miro/shared/exceptions/invalid_keyfile_exception.dart'; -import 'package:miro/shared/exceptions/invalid_password_exception.dart'; -import 'package:miro/shared/models/wallet/wallet.dart'; -import 'package:miro/shared/utils/cryptography/aes256.dart'; - -/// Stores the content of the keyfile.json. -/// -/// The keyfile is used to login into the wallet and to store user data -/// The content of the keyfile is encrypted with the AES265 algorithm, using the user's password -class KeyFile extends Equatable { - /// Latest version of keyfile - static const String latestVersion = '1.0.1'; - - /// Keyfile wallet - final Wallet wallet; - - /// Version of keyfile - final String version; - - const KeyFile({ - required this.wallet, - this.version = latestVersion, - }); - - /// Creates a Keyfile instance from encrypted file content - /// - /// Throws [InvalidPasswordException] if cannot decrypt secret data - /// Throws [InvalidKeyFileException] if cannot parse file content to json - factory KeyFile.decode(String keyFileAsString, String password) { - late Map keyFileAsJson; - try { - keyFileAsJson = jsonDecode(keyFileAsString) as Map; - } catch (_) { - throw InvalidKeyFileException(); - } - try { - Map secretData = jsonDecode( - Aes256.decrypt( - password, - keyFileAsJson['secretData'] as String, - ), - ) as Map; - - return KeyFile( - version: keyFileAsJson['version'] as String, - wallet: Wallet.fromKeyFileData(keyFileAsJson, secretData), - ); - } catch (_) { - throw InvalidPasswordException(); - } - } - - String encode(String password) { - JsonEncoder encoder = const JsonEncoder.withIndent(' '); - return encoder.convert(_encryptJson(password)); - } - - String get fileName { - return 'keyfile_${wallet.address.buildBech32AddressShort(delimiter: '_')}.json'; - } - - Map _encryptJson(String password) { - return { - ..._getPublicJsonData(), - 'secretData': Aes256.encrypt(password, jsonEncode(_getPrivateJsonData())) - }; - } - - Map _getPublicJsonData() { - return { - 'bech32Address': wallet.address.bech32Address, - 'version': latestVersion, - }; - } - - Map _getPrivateJsonData() { - return { - 'privateKey': HEX.encode(wallet.privateKey), - }; - } - - @override - List get props => [wallet, version]; -} diff --git a/lib/shared/models/wallet/wallet.dart b/lib/shared/models/wallet/wallet.dart index fe388a53..27d13531 100644 --- a/lib/shared/models/wallet/wallet.dart +++ b/lib/shared/models/wallet/wallet.dart @@ -57,7 +57,7 @@ class Wallet extends Equatable { ); } - factory Wallet.fromKeyFileData(Map publicData, Map secretData) { + factory Wallet.fromKeyfileData(Map publicData, Map secretData) { final WalletAddress walletAddress = WalletAddress.fromBech32(publicData['bech32Address'] as String); final Uint8List privateKey = Uint8List.fromList(HEX.decode(secretData['privateKey'] as String)); diff --git a/lib/test/utils/test_utils.dart b/lib/test/utils/test_utils.dart index 36034521..efd8c915 100644 --- a/lib/test/utils/test_utils.dart +++ b/lib/test/utils/test_utils.dart @@ -168,6 +168,15 @@ class TestUtils { lastRefreshDateTime: defaultLastRefreshDateTime, ); + static Object? catchException(Function() function) { + try { + function(); + return null; + } catch (e) { + return e; + } + } + static Future initIntegrationTest() async { await initLocator(); await globalLocator().init(); diff --git a/lib/views/pages/drawer/create_wallet_drawer_page/download_keyfile_section/download_keyfile_section_controller.dart b/lib/views/pages/drawer/create_wallet_drawer_page/download_keyfile_section/download_keyfile_section_controller.dart index c65a1340..9bdeb61f 100644 --- a/lib/views/pages/drawer/create_wallet_drawer_page/download_keyfile_section/download_keyfile_section_controller.dart +++ b/lib/views/pages/drawer/create_wallet_drawer_page/download_keyfile_section/download_keyfile_section_controller.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:miro/shared/controllers/browser/browser_controller.dart'; -import 'package:miro/shared/models/wallet/keyfile.dart'; +import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart'; +import 'package:miro/shared/models/keyfile/keyfile_secret_data_model.dart'; import 'package:miro/shared/models/wallet/wallet.dart'; import 'package:miro/views/widgets/kira/kira_text_field/kira_text_field_controller.dart'; @@ -24,11 +25,13 @@ class DownloadKeyfileSectionController { } void downloadKeyfile(Wallet wallet) { - KeyFile keyfile = KeyFile(wallet: wallet); - String password = passwordTextController.textEditingController.text; - String keyfileString = keyfile.encode(password); + DecryptedKeyfileModel decryptedKeyfileModel = DecryptedKeyfileModel( + keyfileSecretDataModel: KeyfileSecretDataModel(wallet: wallet), + ); - BrowserController.downloadFile([keyfileString], keyfile.fileName); + String password = passwordTextController.textEditingController.text; + String fileContent = decryptedKeyfileModel.buildFileContent(password); + BrowserController.downloadFile([fileContent], decryptedKeyfileModel.fileName); } void validatePassword() { diff --git a/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone.dart b/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone.dart deleted file mode 100644 index c1e16d9d..00000000 --- a/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:miro/config/theme/design_colors.dart'; -import 'package:miro/generated/l10n.dart'; -import 'package:miro/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_controller.dart'; -import 'package:miro/views/widgets/generic/text_link.dart'; -import 'package:miro/views/widgets/kira/kira_dropzone/kira_dropzone.dart'; -import 'package:miro/views/widgets/kira/kira_dropzone/models/dropzone_controller.dart'; -import 'package:miro/views/widgets/kira/kira_dropzone/models/dropzone_file.dart'; - -class KeyfileDropzone extends StatefulWidget { - final KeyfileDropzoneController controller; - final ValidateKeyfile validate; - final double width; - final double height; - - const KeyfileDropzone({ - required this.controller, - required this.validate, - this.width = double.infinity, - this.height = 128, - Key? key, - }) : super(key: key); - - @override - State createState() => _KeyfileDropzone(); -} - -class _KeyfileDropzone extends State { - final KiraDropzoneController dropZoneController = KiraDropzoneController(); - bool isHover = false; - DropzoneFile? actualFile; - String? errorMessage; - - @override - void initState() { - super.initState(); - _initController(); - } - - @override - Widget build(BuildContext context) { - return Container( - height: widget.height, - width: widget.width, - decoration: BoxDecoration( - border: Border.all( - width: 1, - color: errorMessage != null ? DesignColors.redStatus1 : DesignColors.white1, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Stack( - children: [ - Positioned.fill( - child: KiraDropzone( - controller: dropZoneController, - onHover: () => _setHoverState(status: true), - onLeave: () => _setHoverState(status: false), - onPickFile: _onFilePicked, - ), - ), - Positioned.fill( - child: InkWell( - onTap: () => dropZoneController.pickFile(), - child: Container( - padding: const EdgeInsets.all(10), - child: _buildPreview(), - ), - ), - ), - ], - ), - ); - } - - void _initController() { - widget.controller.initController( - dropzoneController: dropZoneController, - validate: _validate, - setErrorMessage: _setErrorMessage, - ); - } - - String? _validate() { - errorMessage = widget.validate(actualFile); - setState(() {}); - return errorMessage; - } - - void _setErrorMessage(String? message) { - setState(() { - errorMessage = message; - }); - } - - void _onFilePicked(DropzoneFile file) { - _setHoverState(status: false); - setState(() { - errorMessage = widget.validate(file); - actualFile = file; - }); - } - - void _setHoverState({required bool status}) { - setState(() { - isHover = status; - }); - } - - Widget _buildPreview() { - if (isHover) { - return _buildDropPreview(); - } - if (actualFile == null) { - return _buildEmptyPreview(); - } - return _buildFilePreview(); - } - - Widget _buildDropPreview() { - TextTheme textTheme = Theme.of(context).textTheme; - - return Center( - child: Text( - S.of(context).keyfileDropFile.toUpperCase(), - style: textTheme.bodyMedium!.copyWith(color: DesignColors.white1), - ), - ); - } - - Widget _buildEmptyPreview() { - TextTheme textTheme = Theme.of(context).textTheme; - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 28), - Text( - S.of(context).keyfileDropHere, - style: textTheme.bodyMedium!.copyWith( - color: DesignColors.white1, - ), - ), - Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - S.of(context).or, - style: textTheme.bodyMedium!.copyWith( - color: DesignColors.white1, - ), - ), - TextLink( - text: S.of(context).browse, - textStyle: textTheme.bodyMedium!, - onTap: () => dropZoneController.pickFile(), - ), - ], - ), - const SizedBox(height: 10), - Text( - errorMessage ?? '', - style: textTheme.bodyMedium!.copyWith( - color: DesignColors.redStatus1, - ), - ), - ], - ); - } - - Widget _buildFilePreview() { - TextTheme textTheme = Theme.of(context).textTheme; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.insert_drive_file, - color: DesignColors.white1, - size: 50, - ), - const SizedBox(width: 8), - SizedBox( - width: 200, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - actualFile!.name, - overflow: TextOverflow.ellipsis, - style: textTheme.bodyMedium!.copyWith( - color: DesignColors.white2, - ), - ), - if (errorMessage == null) - Text( - actualFile!.sizeString, - overflow: TextOverflow.ellipsis, - style: textTheme.bodyMedium!.copyWith( - color: DesignColors.white1, - ), - ) - else - Text( - errorMessage!, - overflow: TextOverflow.ellipsis, - style: textTheme.bodyMedium!.copyWith( - color: DesignColors.redStatus1, - ), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_controller.dart b/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_controller.dart deleted file mode 100644 index 44d7a7cb..00000000 --- a/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_controller.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:miro/views/widgets/kira/kira_dropzone/models/dropzone_controller.dart'; -import 'package:miro/views/widgets/kira/kira_dropzone/models/dropzone_file.dart'; - -typedef ValidateKeyfile = String? Function(DropzoneFile?); - -class KeyfileDropzoneController { - late KiraDropzoneController dropzoneController; - late void Function(String?) setErrorMessage; - late String? Function() validate; - - void initController({ - required KiraDropzoneController dropzoneController, - required String? Function() validate, - required void Function(String?) setErrorMessage, - }) { - this.dropzoneController = dropzoneController; - this.validate = validate; - this.setErrorMessage = setErrorMessage; - } -} diff --git a/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_preview.dart b/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_preview.dart new file mode 100644 index 00000000..5f6bc8ef --- /dev/null +++ b/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_preview.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart'; +import 'package:miro/config/theme/design_colors.dart'; +import 'package:miro/generated/l10n.dart'; +import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart'; + +class KeyfileDropzonePreview extends StatelessWidget { + final KeyfileDropzoneState keyfileDropzoneState; + final String? errorMessage; + + const KeyfileDropzonePreview({ + required this.keyfileDropzoneState, + required this.errorMessage, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + TextTheme textTheme = Theme.of(context).textTheme; + EncryptedKeyfileModel? encryptedKeyfileModel; + + encryptedKeyfileModel = keyfileDropzoneState.encryptedKeyfileModel; + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.insert_drive_file, + color: DesignColors.white1, + size: 50, + ), + const SizedBox(width: 8), + SizedBox( + width: 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + keyfileDropzoneState.fileModel?.name ?? '---', + overflow: TextOverflow.ellipsis, + style: textTheme.bodyMedium!.copyWith( + color: DesignColors.white1, + ), + ), + if (encryptedKeyfileModel != null) ...[ + const SizedBox(height: 5), + Text( + S.of(context).keyfileVersion(encryptedKeyfileModel.version), + style: textTheme.bodySmall!.copyWith( + color: DesignColors.accent, + ), + ), + ], + const SizedBox(height: 3), + if (errorMessage != null) + Text( + errorMessage!, + overflow: TextOverflow.ellipsis, + style: textTheme.bodySmall!.copyWith( + color: DesignColors.redStatus1, + ), + ) + else + Text( + keyfileDropzoneState.fileModel?.sizeString ?? '---', + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: textTheme.bodySmall!.copyWith( + color: DesignColors.white2, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page.dart b/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page.dart index 4e880581..4f1641e4 100644 --- a/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page.dart +++ b/lib/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page.dart @@ -1,22 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:miro/blocs/generic/auth/auth_cubit.dart'; +import 'package:miro/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_cubit.dart'; +import 'package:miro/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_state.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart'; import 'package:miro/config/locator.dart'; import 'package:miro/generated/l10n.dart'; -import 'package:miro/shared/exceptions/invalid_keyfile_exception.dart'; -import 'package:miro/shared/exceptions/invalid_password_exception.dart'; -import 'package:miro/shared/models/wallet/keyfile.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart'; import 'package:miro/shared/models/wallet/wallet.dart'; import 'package:miro/shared/utils/logger/app_logger.dart'; -import 'package:miro/shared/utils/logger/log_level.dart'; import 'package:miro/shared/utils/string_utils.dart'; import 'package:miro/views/layout/drawer/drawer_subtitle.dart'; import 'package:miro/views/layout/scaffold/kira_scaffold.dart'; import 'package:miro/views/pages/drawer/sign_in_drawer_page/create_wallet_link_button.dart'; -import 'package:miro/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone.dart'; -import 'package:miro/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_controller.dart'; +import 'package:miro/views/pages/drawer/sign_in_drawer_page/sign_in_keyfile_drawer_page/keyfile_dropzone_preview.dart'; import 'package:miro/views/widgets/buttons/kira_elevated_button.dart'; -import 'package:miro/views/widgets/kira/kira_dropzone/models/dropzone_file.dart'; +import 'package:miro/views/widgets/kira/kira_dropzone/kira_dropzone.dart'; import 'package:miro/views/widgets/kira/kira_text_field/kira_text_field.dart'; import 'package:miro/views/widgets/kira/kira_text_field/kira_text_field_controller.dart'; @@ -29,109 +31,122 @@ class SignInKeyfileDrawerPage extends StatefulWidget { class _SignInKeyfileDrawerPage extends State { final AuthCubit authCubit = globalLocator(); - final KiraTextFieldController keyfilePasswordController = KiraTextFieldController(); - final KeyfileDropzoneController dropZoneController = KeyfileDropzoneController(); + final KiraTextFieldController keyfileKiraTextFieldController = KiraTextFieldController(); + late final KeyfileDropzoneCubit keyfileDropzoneCubit = KeyfileDropzoneCubit(); + + late final SignInKeyfileDrawerPageCubit signInKeyfileDrawerPageCubit = SignInKeyfileDrawerPageCubit( + passwordTextEditingController: keyfileKiraTextFieldController.textEditingController, + keyfileDropzoneCubit: keyfileDropzoneCubit, + ); @override void dispose() { - keyfilePasswordController.close(); + keyfileKiraTextFieldController.close(); + keyfileDropzoneCubit.close(); + signInKeyfileDrawerPageCubit.close(); super.dispose(); } @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DrawerTitle( - title: S.of(context).keyfileSignIn, - subtitle: S.of(context).keyfileToDropzone, - tooltipMessage: S.of(context).keyfileTipSecretData, - ), - const SizedBox(height: 37), - KeyfileDropzone( - controller: dropZoneController, - validate: _validateKeyFile, - ), - const SizedBox(height: 16), - KiraTextField( - controller: keyfilePasswordController, - hint: S.of(context).keyfileEnterPassword, - inputFormatters: [ - FilteringTextInputFormatter.deny(StringUtils.whitespacesRegExp), + return BlocBuilder( + bloc: signInKeyfileDrawerPageCubit, + builder: (BuildContext context, SignInKeyfileDrawerPageState signInKeyfileDrawerPageState) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DrawerTitle( + title: S.of(context).keyfileSignIn, + subtitle: S.of(context).keyfileToDropzone, + tooltipMessage: S.of(context).keyfileTipSecretData, + ), + const SizedBox(height: 37), + BlocProvider( + create: (_) => keyfileDropzoneCubit, + child: BlocBuilder( + bloc: keyfileDropzoneCubit, + builder: (BuildContext context, KeyfileDropzoneState keyfileDropzoneState) { + KeyfileExceptionType? keyfileExceptionType = signInKeyfileDrawerPageState.keyfileExceptionType; + + return KiraDropzone( + hasFileBool: keyfileDropzoneState.hasFile, + width: double.infinity, + height: 128, + emptyLabel: S.of(context).keyfileDropHere, + uploadViaHtmlFile: (dynamic htmlFile) { + keyfileDropzoneCubit.uploadFileViaHtml(htmlFile); + }, + uploadFileManually: keyfileDropzoneCubit.uploadFileManually, + errorMessage: _selectDropzoneErrorMessage(keyfileExceptionType), + filePreviewErrorBuilder: (String? errorMessage) { + return KeyfileDropzonePreview( + keyfileDropzoneState: keyfileDropzoneState, + errorMessage: errorMessage, + ); + }, + ); + }, + ), + ), + const SizedBox(height: 16), + KiraTextField( + controller: keyfileKiraTextFieldController, + hint: S.of(context).keyfileEnterPassword, + inputFormatters: [ + FilteringTextInputFormatter.deny(StringUtils.whitespacesRegExp), + ], + obscureText: true, + onChanged: (_) => signInKeyfileDrawerPageCubit.notifyPasswordChanged(), + ), + const SizedBox(height: 24), + KiraElevatedButton( + onPressed: () => _handleSignInButtonPressed(signInKeyfileDrawerPageState), + title: S.of(context).connectWalletButtonSignIn, + ), + const CreateWalletLinkButton(), ], - obscureText: true, - validator: (_) => _validateKeyFilePassword(), - ), - const SizedBox(height: 24), - KiraElevatedButton( - onPressed: _pressSignInButton, - title: S.of(context).connectWalletButtonSignIn, - ), - const SizedBox(height: 32), - const CreateWalletLinkButton(), - const SizedBox(height: 32), - ], + ); + }, ); } - String? _validateKeyFile(DropzoneFile? file) { - if (file == null) { - String errorMessage = S.of(context).keyfileErrorCannotBeEmpty; - AppLogger().log(message: errorMessage, logLevel: LogLevel.warning); - return errorMessage; - } - try { - _getWalletFromKeyFileString(file.content); - return null; - } on InvalidKeyFileException catch (_) { - String errorMessage = S.of(context).keyfileInvalid; - AppLogger().log(message: errorMessage, logLevel: LogLevel.warning); - return errorMessage; - } catch (e) { - AppLogger().log(message: 'Unknown error: ${e.toString()}', logLevel: LogLevel.fatal); - return null; + void _handleSignInButtonPressed(SignInKeyfileDrawerPageState signInKeyfileDrawerPageState) { + if (signInKeyfileDrawerPageState.decryptedKeyfileModel != null) { + _pressSignInButton(signInKeyfileDrawerPageState); } + + String? errorMessage = _selectTextFieldErrorMessage(signInKeyfileDrawerPageState.keyfileExceptionType); + keyfileKiraTextFieldController.setErrorMessage(errorMessage); + _selectDropzoneErrorMessage(signInKeyfileDrawerPageState.keyfileExceptionType); } - String? _validateKeyFilePassword() { - DropzoneFile? file = dropZoneController.dropzoneController.currentFile; - if (file == null) { - String errorMessage = S.of(context).keyfileErrorCannotBeEmpty; - AppLogger().log(message: errorMessage, logLevel: LogLevel.warning); - dropZoneController.setErrorMessage(errorMessage); - } - try { - _getWalletFromKeyFileString(file!.content); - } on InvalidPasswordException { - String errorMessage = S.of(context).keyfileWrongPassword; - AppLogger().log(message: errorMessage, logLevel: LogLevel.warning); - return errorMessage; + String? _selectDropzoneErrorMessage(KeyfileExceptionType? keyfileExceptionType) { + if (keyfileExceptionType == KeyfileExceptionType.invalidKeyfile) { + return S.of(context).keyfileErrorInvalid; + } else { + return null; } - return null; } - void _pressSignInButton() { - bool keyfileValid = dropZoneController.validate() == null; - keyfilePasswordController.reloadErrorMessage(); - bool passwordValid = keyfilePasswordController.errorNotifier.value == null; - - if (keyfileValid && passwordValid) { - Wallet wallet = _getWalletFromKeyFileString(dropZoneController.dropzoneController.currentFile!.content); - authCubit.signIn(wallet); - KiraScaffold.of(context).closeEndDrawer(); + String? _selectTextFieldErrorMessage(KeyfileExceptionType? keyfileExceptionType) { + if (keyfileKiraTextFieldController.textEditingController.text.isEmpty) { + return null; } + return switch (keyfileExceptionType) { + KeyfileExceptionType.wrongPassword => S.of(context).keyfileErrorWrongPassword, + (_) => null, + }; } - Wallet _getWalletFromKeyFileString(String keyFileEncryptedContent) { + void _pressSignInButton(SignInKeyfileDrawerPageState signInKeyfileDrawerPageState) { try { - String password = keyfilePasswordController.textEditingController.text; - KeyFile keyFile = KeyFile.decode(keyFileEncryptedContent, password); - return keyFile.wallet; - } catch (e) { - AppLogger().log(message: 'Unknown error: ${e.toString()}', logLevel: LogLevel.fatal); - rethrow; + DecryptedKeyfileModel decryptedKeyfileModel = signInKeyfileDrawerPageState.decryptedKeyfileModel!; + Wallet wallet = decryptedKeyfileModel.keyfileSecretDataModel.wallet; + authCubit.signIn(wallet); + KiraScaffold.of(context).closeEndDrawer(); + } catch (_) { + AppLogger().log(message: 'No keyfile uploaded'); } } } diff --git a/lib/views/widgets/kira/kira_dropzone/kira_dropzone.dart b/lib/views/widgets/kira/kira_dropzone/kira_dropzone.dart index f7ef4742..dfa2302e 100644 --- a/lib/views/widgets/kira/kira_dropzone/kira_dropzone.dart +++ b/lib/views/widgets/kira/kira_dropzone/kira_dropzone.dart @@ -1,24 +1,30 @@ -import 'dart:html' as html; - -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; -import 'package:miro/views/widgets/kira/kira_dropzone/models/dropzone_controller.dart'; -import 'package:miro/views/widgets/kira/kira_dropzone/models/dropzone_file.dart'; +import 'package:miro/config/theme/design_colors.dart'; +import 'package:miro/views/widgets/kira/kira_dropzone/kira_dropzone_drop_view.dart'; +import 'package:miro/views/widgets/kira/kira_dropzone/kira_dropzone_empty_view.dart'; + +typedef FilePreviewErrorBuilder = Widget Function(String? errorMessage); class KiraDropzone extends StatefulWidget { - final KiraDropzoneController controller; - final ValueChanged? onPickFile; - final ValueChanged? onError; - final VoidCallback? onHover; - final VoidCallback? onLeave; + final bool hasFileBool; + final double width; + final double height; + final String emptyLabel; + final ValueChanged uploadViaHtmlFile; + final VoidCallback uploadFileManually; + final FilePreviewErrorBuilder filePreviewErrorBuilder; + final String? errorMessage; const KiraDropzone({ - required this.controller, - this.onPickFile, - this.onError, - this.onHover, - this.onLeave, + required this.hasFileBool, + required this.width, + required this.height, + required this.emptyLabel, + required this.uploadViaHtmlFile, + required this.uploadFileManually, + required this.filePreviewErrorBuilder, + this.errorMessage, Key? key, }) : super(key: key); @@ -27,72 +33,65 @@ class KiraDropzone extends StatefulWidget { } class _KiraDropzone extends State { - late DropzoneViewController dropzoneViewController; - - @override - void initState() { - super.initState(); - _initController(); - } + bool hoveredBool = false; @override Widget build(BuildContext context) { - return DropzoneView( - operation: DragOperation.all, - cursor: CursorType.pointer, - onCreated: (DropzoneViewController controller) => dropzoneViewController = controller, - onError: widget.onError, - onHover: widget.onHover, - onDrop: _onDropzoneDrop, - onLeave: widget.onLeave, - ); - } + late Widget dropzonePreview; - void _initController() { - widget.controller.initController( - pickFile: _pickFileManual, - ); - } - - Future _pickFileManual() async { - FilePickerResult? uploadResult = await FilePicker.platform.pickFiles(allowMultiple: false); - if (uploadResult != null) { - PlatformFile platformFile = uploadResult.files.single; - if (platformFile.bytes == null) { - return null; - } - DropzoneFile file = DropzoneFile( - name: platformFile.name, - size: platformFile.size, - extension: platformFile.extension, - content: String.fromCharCodes(platformFile.bytes!), + if (hoveredBool) { + dropzonePreview = const KiraDropzoneDropView(); + } else if (widget.hasFileBool) { + dropzonePreview = widget.filePreviewErrorBuilder(widget.errorMessage); + } else { + dropzonePreview = KiraDropzoneEmptyView( + emptyLabel: widget.emptyLabel, + onTap: widget.uploadFileManually, ); - _setFile(file); - return file; } - return null; + + return Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: widget.errorMessage != null ? DesignColors.redStatus1 : DesignColors.white1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Stack( + children: [ + Positioned.fill( + child: DropzoneView( + operation: DragOperation.all, + cursor: CursorType.grab, + onHover: () => _setHoverState(status: true), + onDrop: _listenFileDrop, + onLeave: () => _setHoverState(status: false), + ), + ), + Positioned.fill( + child: InkWell( + onTap: widget.uploadFileManually, + child: Padding( + padding: const EdgeInsets.all(10), + child: dropzonePreview, + ), + ), + ), + ], + ), + ); } - void _onDropzoneDrop(dynamic uploadedFile) { - if (uploadedFile is html.File) { - final html.FileReader reader = html.FileReader()..readAsText(uploadedFile); - reader.onLoadEnd.listen((html.ProgressEvent event) { - String result = reader.result.toString(); - DropzoneFile file = DropzoneFile( - name: uploadedFile.name, - size: uploadedFile.size, - extension: uploadedFile.name.split('.').last, - content: result, - ); - _setFile(file); - }); - } + void _listenFileDrop(dynamic htmlFile) { + widget.uploadViaHtmlFile(htmlFile); + _setHoverState(status: false); } - void _setFile(DropzoneFile file) { - widget.controller.currentFile = file; - if (widget.onPickFile != null) { - widget.onPickFile!(file); - } + void _setHoverState({required bool status}) { + hoveredBool = status; + setState(() {}); } } diff --git a/lib/views/widgets/kira/kira_dropzone/kira_dropzone_drop_view.dart b/lib/views/widgets/kira/kira_dropzone/kira_dropzone_drop_view.dart new file mode 100644 index 00000000..fcf4caec --- /dev/null +++ b/lib/views/widgets/kira/kira_dropzone/kira_dropzone_drop_view.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:miro/config/theme/design_colors.dart'; +import 'package:miro/generated/l10n.dart'; + +class KiraDropzoneDropView extends StatelessWidget { + const KiraDropzoneDropView({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + TextTheme textTheme = Theme.of(context).textTheme; + + return Center( + child: Text( + S.of(context).keyfileDropFile.toUpperCase(), + style: textTheme.bodyMedium!.copyWith(color: DesignColors.white1), + ), + ); + } +} diff --git a/lib/views/widgets/kira/kira_dropzone/kira_dropzone_empty_view.dart b/lib/views/widgets/kira/kira_dropzone/kira_dropzone_empty_view.dart new file mode 100644 index 00000000..1d82c976 --- /dev/null +++ b/lib/views/widgets/kira/kira_dropzone/kira_dropzone_empty_view.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:miro/config/theme/design_colors.dart'; +import 'package:miro/generated/l10n.dart'; +import 'package:miro/views/widgets/generic/text_link.dart'; + +class KiraDropzoneEmptyView extends StatelessWidget { + final String emptyLabel; + final VoidCallback? onTap; + + const KiraDropzoneEmptyView({ + required this.emptyLabel, + required this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + TextTheme textTheme = Theme.of(context).textTheme; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + emptyLabel, + style: textTheme.bodyMedium!.copyWith( + color: DesignColors.white1, + ), + ), + Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + S.of(context).or, + style: textTheme.bodyMedium!.copyWith( + color: DesignColors.white1, + ), + ), + TextLink( + text: S.of(context).browse, + textStyle: textTheme.bodyMedium!, + onTap: onTap, + ), + ], + ), + ], + ); + } +} diff --git a/lib/views/widgets/kira/kira_dropzone/models/dropzone_controller.dart b/lib/views/widgets/kira/kira_dropzone/models/dropzone_controller.dart deleted file mode 100644 index f4566e5a..00000000 --- a/lib/views/widgets/kira/kira_dropzone/models/dropzone_controller.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:miro/views/widgets/kira/kira_dropzone/models/dropzone_file.dart'; - -typedef FilePickedCallback = Future Function(); - -class KiraDropzoneController { - late FilePickedCallback pickFile; - DropzoneFile? currentFile; - - void initController({ - required FilePickedCallback pickFile, - }) { - this.pickFile = pickFile; - } -} diff --git a/lib/views/widgets/kira/kira_dropzone/models/dropzone_file.dart b/lib/views/widgets/kira/kira_dropzone/models/dropzone_file.dart deleted file mode 100644 index 747140e5..00000000 --- a/lib/views/widgets/kira/kira_dropzone/models/dropzone_file.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:math'; - -class DropzoneFile { - final String name; - final String? extension; - final String content; - final int size; - - DropzoneFile({ - required this.name, - required this.extension, - required this.content, - required this.size, - }); - - String get sizeString { - if (size <= 0) { - return '0 B'; - } - final List siSuffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - final int unitIndex = (log(size) / log(1024)).floor(); - final String unitValue = (size / pow(1024, unitIndex)).toStringAsFixed(2); - return '$unitValue ${siSuffixes[unitIndex]}'; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 9c285859..1848c6d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # 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: 1.26.0 +version: 1.27.0 environment: sdk: ">=3.1.3" diff --git a/test/unit/blocs/pages/drawer/sign_in_keyfile_drawer_page_cubit_test.dart b/test/unit/blocs/pages/drawer/sign_in_keyfile_drawer_page_cubit_test.dart new file mode 100644 index 00000000..d726098a --- /dev/null +++ b/test/unit/blocs/pages/drawer/sign_in_keyfile_drawer_page_cubit_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:miro/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_cubit.dart'; +import 'package:miro/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_state.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/generic/file_model.dart'; +import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart'; +import 'package:miro/shared/models/keyfile/keyfile_secret_data_model.dart'; +import 'package:miro/shared/models/wallet/mnemonic.dart'; +import 'package:miro/shared/models/wallet/wallet.dart'; +import 'package:miro/test/mock_locator.dart'; +import 'package:miro/test/utils/test_utils.dart'; + +// To run this test type in console: +// fvm flutter test test/unit/blocs/pages/drawer/sign_in_keyfile_drawer_page_cubit_test.dart --platform chrome --null-assertions +Future main() async { + await initMockLocator(); + await TestUtils.setupNetworkModel(networkUri: Uri.parse('https://healthy.kira.network/')); + + FileModel actualValidFileModel = const FileModel( + size: 537, + name: 'keyfile_kira143q_k9wx', + extension: 'json', + content: + '{"version": "2.0.0", "public_key": "AlLas8CJ6lm5yZJ8h0U5Qu9nzVvgvskgHuURPB3jvUx8", "secret_data": "RDjZC9U7JTFrsk3u9D7jDf17Ih1IerFTga6ayTR3Ig6Ay5vaRtHAhm/rnmuIQBUDeyXvffcpElfsZwh4LvOwwzzLd9pzRq5CLk3LBqAT6zC/aPsNimo5uXEESeIfua5oUBbob6eyO4bMMLh2NMUhoo/2CIg="}', + ); + + FileModel actualInvalidFileModel = const FileModel( + size: 537, + name: 'invalid_keyfile_kira143q_k9wx', + extension: 'exe', + content: 'invalid_content', + ); + + group('Tests of [SignInKeyfileDrawerPageCubit] process', () { + // Arrange + KeyfileDropzoneCubit actualKeyfileDropzoneCubit = KeyfileDropzoneCubit(); + TextEditingController actualPasswordTextEditingController = TextEditingController(); + SignInKeyfileDrawerPageCubit actualSignInKeyfileDrawerPageCubit = SignInKeyfileDrawerPageCubit( + keyfileDropzoneCubit: actualKeyfileDropzoneCubit, + passwordTextEditingController: actualPasswordTextEditingController, + ); + + test('Should return empty [SignInKeyfileDrawerPageState] as a default [SignInKeyfileDrawerPageState]', () async { + // Assert + SignInKeyfileDrawerPageState expectedSignInKeyfileDrawerPageState = const SignInKeyfileDrawerPageState(); + + expect(actualSignInKeyfileDrawerPageCubit.state, expectedSignInKeyfileDrawerPageState); + }); + + test('Should return [SignInKeyfileDrawerPageState] with [KeyfileExceptionType.invalidKeyfile] if keyfile is invalid', () async { + // Act + actualKeyfileDropzoneCubit.updateSelectedFile(actualInvalidFileModel); + await Future.delayed(const Duration(milliseconds: 100)); + + // Assert + SignInKeyfileDrawerPageState expectedSignInKeyfileDrawerPageState = const SignInKeyfileDrawerPageState( + keyfileExceptionType: KeyfileExceptionType.invalidKeyfile, + decryptedKeyfileModel: null, + ); + + expect(actualSignInKeyfileDrawerPageCubit.state, expectedSignInKeyfileDrawerPageState); + }); + + test('Should return [SignInKeyfileDrawerPageState] with [KeyfileExceptionType.wrongPassword] if keyfile is valid but password is not provided', () async { + // Act + actualKeyfileDropzoneCubit.updateSelectedFile(actualValidFileModel); + await Future.delayed(const Duration(milliseconds: 100)); + + // Assert + SignInKeyfileDrawerPageState expectedSignInKeyfileDrawerPageState = const SignInKeyfileDrawerPageState( + keyfileExceptionType: KeyfileExceptionType.wrongPassword, + decryptedKeyfileModel: null, + ); + + expect(actualSignInKeyfileDrawerPageCubit.state, expectedSignInKeyfileDrawerPageState); + }); + + test('Should return [SignInKeyfileDrawerPageState] with [KeyfileExceptionType.wrongPassword] if provided password is invalid', () async { + // Act + actualPasswordTextEditingController.text = '123456'; + actualSignInKeyfileDrawerPageCubit.notifyPasswordChanged(); + + // Assert + SignInKeyfileDrawerPageState expectedSignInKeyfileDrawerPageState = const SignInKeyfileDrawerPageState( + keyfileExceptionType: KeyfileExceptionType.wrongPassword, + decryptedKeyfileModel: null, + ); + + expect(actualSignInKeyfileDrawerPageCubit.state, expectedSignInKeyfileDrawerPageState); + }); + + test('Should return [SignInKeyfileDrawerPageState] with decrypted keyfile after enter valid password', () async { + // Act + actualPasswordTextEditingController.text = '123'; + actualSignInKeyfileDrawerPageCubit.notifyPasswordChanged(); + + // Assert + SignInKeyfileDrawerPageState expectedSignInKeyfileDrawerPageState = SignInKeyfileDrawerPageState( + decryptedKeyfileModel: DecryptedKeyfileModel( + version: '2.0.0', + keyfileSecretDataModel: KeyfileSecretDataModel( + wallet: Wallet.derive( + mnemonic: Mnemonic( + value: + 'require point property company tongue busy bench burden caution gadget knee glance thought bulk assist month cereal report quarter tool section often require shield'), + ), + ), + ), + ); + + expect(actualSignInKeyfileDrawerPageCubit.state, expectedSignInKeyfileDrawerPageState); + }); + }); +} diff --git a/test/unit/blocs/widgets/kira/kira_dropzone/keyfile_dropzone_cubit_test.dart b/test/unit/blocs/widgets/kira/kira_dropzone/keyfile_dropzone_cubit_test.dart new file mode 100644 index 00000000..bd49be99 --- /dev/null +++ b/test/unit/blocs/widgets/kira/kira_dropzone/keyfile_dropzone_cubit_test.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart'; +import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/generic/file_model.dart'; +import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart'; +import 'package:miro/test/mock_locator.dart'; +import 'package:miro/test/utils/test_utils.dart'; + +// To run this test type in console: +// fvm flutter test test/unit/blocs/widgets/kira/kira_dropzone/keyfile_dropzone_cubit_test.dart --platform chrome --null-assertions +Future main() async { + await initMockLocator(); + + group('Tests of KeyfileDropzoneCubit.updateSelectedFile()', () { + test('Should return [KeyfileDropzoneState]', () { + // Arrange + KeyfileDropzoneCubit actualKeyfileDropzoneCubit = KeyfileDropzoneCubit(); + + // Assert + KeyfileDropzoneState expectedKeyfileDropzoneState = KeyfileDropzoneState.empty(); + + TestUtils.printInfo('Should return [KeyfileDropzoneState.empty] as a default [KeyfileDropzoneState]'); + expect(actualKeyfileDropzoneCubit.state, expectedKeyfileDropzoneState); + + // **************************************************************************************** + + // Arrange + FileModel fileModel = const FileModel( + size: 537, + name: 'keyfile_kira143q_k9wx', + extension: 'json', + content: + '{"version": "2.0.0", "public_key": "AlLas8CJ6lm5yZJ8h0U5Qu9nzVvgvskgHuURPB3jvUx8", "secret_data": "RDjZC9U7JTFrsk3u9D7jDf17Ih1IerFTga6ayTR3Ig6Ay5vaRtHAhm/rnmuIQBUDeyXvffcpElfsZwh4LvOwwzzLd9pzRq5CLk3LBqAT6zC/aPsNimo5uXEESeIfua5oUBbob6eyO4bMMLh2NMUhoo/2CIg="}', + ); + + // Act + actualKeyfileDropzoneCubit.updateSelectedFile(fileModel); + + // Assert + expectedKeyfileDropzoneState = KeyfileDropzoneState( + encryptedKeyfileModel: EncryptedKeyfileModel( + version: '2.0.0', + publicKey: base64Decode('AlLas8CJ6lm5yZJ8h0U5Qu9nzVvgvskgHuURPB3jvUx8'), + encryptedSecretData: + 'RDjZC9U7JTFrsk3u9D7jDf17Ih1IerFTga6ayTR3Ig6Ay5vaRtHAhm/rnmuIQBUDeyXvffcpElfsZwh4LvOwwzzLd9pzRq5CLk3LBqAT6zC/aPsNimo5uXEESeIfua5oUBbob6eyO4bMMLh2NMUhoo/2CIg=', + ), + fileModel: fileModel, + ); + + TestUtils.printInfo('Should return [KeyfileDropzoneState] with uploaded keyfile'); + expect(actualKeyfileDropzoneCubit.state, expectedKeyfileDropzoneState); + + // **************************************************************************************** + + // Arrange + fileModel = const FileModel(size: 537, name: 'holiday_photo', extension: 'jpg', content: 'sea'); + + // Act + actualKeyfileDropzoneCubit.updateSelectedFile(fileModel); + + // Assert + expectedKeyfileDropzoneState = KeyfileDropzoneState( + fileModel: fileModel, + keyfileExceptionType: KeyfileExceptionType.invalidKeyfile, + ); + + TestUtils.printInfo('Should return [KeyfileDropzoneState] with uploaded file and Keyfile error if uploaded file is not a keyfile'); + expect(actualKeyfileDropzoneCubit.state, expectedKeyfileDropzoneState); + }); + }); +} diff --git a/test/unit/shared/models/generic/file_model_test.dart b/test/unit/shared/models/generic/file_model_test.dart new file mode 100644 index 00000000..32cf87d0 --- /dev/null +++ b/test/unit/shared/models/generic/file_model_test.dart @@ -0,0 +1,115 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:miro/shared/models/generic/file_model.dart'; + +// To run this test type in console: +// fvm flutter test test/unit/shared/models/generic/file_model_test.dart --platform chrome --null-assertions +void main() { + group('Tests of [FileModel.sizeString] getter', () { + test('Should [return 1 kB] from [1 024 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1024, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1 kB'); + }); + + test('Should [return 1.5 kB] from [1536 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1536, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1.5 kB'); + }); + + test('Should [return 1 MB] from [1 048 576 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1048576, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1 MB'); + }); + + test('Should [return 1.5 MB] from [1 572 864 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1572864, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1.5 MB'); + }); + + test('Should [return 1 GB] from [1 073 741 824 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1073741824, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1 GB'); + }); + + test('Should [return 1.5 GB] from [1 610 612 736 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1610612736, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1.5 GB'); + }); + + test('Should [return 1 TB] from [1 099 511 627 776 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1099511627776, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1 TB'); + }); + + test('Should [return 1.5 TB] from [1 649 267 441 664 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1649267441664, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1.5 TB'); + }); + + test('Should [return 1 PB] from [1 125 899 906 842 624 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1125899906842624, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1 PB'); + }); + + test('Should [return 1.5 PB] from [1 688 849 860 263 936 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1688849860263936, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1.5 PB'); + }); + + test('Should [return 1 EB] from [1 152 921 504 606 846 976 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1152921504606846976, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1 EB'); + }); + + test('Should [return 1.5 EB] from [1 729 382 256 910 270 464 bytes]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: 1729382256910270464, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect(actualFileModel.sizeString, '1.5 EB'); + }); + + test('Should [throw assertion error] if size is [negative number]', () { + // Arrange + FileModel actualFileModel = const FileModel(size: -1024, name: 'name', content: 'content', extension: 'extension'); + + // Assert + expect( + () => actualFileModel.sizeString, + throwsA(isA()), + ); + }); + }); +} diff --git a/test/unit/shared/models/keyfile/decrypted_keyfile_model_test.dart b/test/unit/shared/models/keyfile/decrypted_keyfile_model_test.dart new file mode 100644 index 00000000..3124625a --- /dev/null +++ b/test/unit/shared/models/keyfile/decrypted_keyfile_model_test.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:miro/shared/entity/keyfile/keyfile_entity.dart'; +import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart'; +import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart'; +import 'package:miro/shared/models/keyfile/keyfile_secret_data_model.dart'; +import 'package:miro/shared/models/wallet/mnemonic.dart'; +import 'package:miro/shared/models/wallet/wallet.dart'; +import 'package:miro/test/mock_locator.dart'; +import 'package:miro/test/utils/test_utils.dart'; + +// To run this test type in console: +// fvm flutter test test/unit/shared/models/keyfile/decrypted_keyfile_model_test.dart --platform chrome --null-assertions +Future main() async { + await initMockLocator(); + await TestUtils.setupNetworkModel(networkUri: Uri.parse('https://healthy.kira.network/')); + String actualPassword = '123'; + // @formatter:off + Mnemonic actualMnemonic = Mnemonic(value: 'require point property company tongue busy bench burden caution gadget knee glance thought bulk assist month cereal report quarter tool section often require shield'); + Wallet actualWallet = Wallet.derive(mnemonic: actualMnemonic); + // @formatter:on + + group('Tests of DecryptedKeyfileModel.buildFileContent() method', () { + test('Should [return DecryptedKeyfileModel] representing keyfile in latest version [v2.0.0]', () { + // Arrange + DecryptedKeyfileModel actualDecryptedKeyfileModel = DecryptedKeyfileModel( + version: '2.0.0', + keyfileSecretDataModel: KeyfileSecretDataModel(wallet: actualWallet), + ); + + // Act + // Because of the random salt, we can't compare the whole keyfile content. + // So basing on generated keyfile, we can parse it back to [DecryptedKeyfileModel] and compare result + String actualKeyfileContent = actualDecryptedKeyfileModel.buildFileContent(actualPassword); + KeyfileEntity actualKeyfileEntity = KeyfileEntity.fromJson(jsonDecode(actualKeyfileContent) as Map); + EncryptedKeyfileModel actualEncryptedKeyfileModel = EncryptedKeyfileModel.fromEntity(actualKeyfileEntity); + actualDecryptedKeyfileModel = actualEncryptedKeyfileModel.decrypt(actualPassword); + + // Assert + DecryptedKeyfileModel expectedDecryptedKeyfileModel = DecryptedKeyfileModel( + version: '2.0.0', + keyfileSecretDataModel: KeyfileSecretDataModel(wallet: actualWallet), + ); + + expect(actualDecryptedKeyfileModel, expectedDecryptedKeyfileModel); + }); + }); + + group('Tests of DecryptedKeyfileModel.fileName getter', () { + test('Should return proper file name', () { + // Arrange + DecryptedKeyfileModel decryptedKeyfileModel = DecryptedKeyfileModel( + version: '2.0.0', + keyfileSecretDataModel: KeyfileSecretDataModel(wallet: actualWallet), + ); + + // Act + String actualFileName = decryptedKeyfileModel.fileName; + + // Assert + String expectedFileName = 'keyfile_kira143q_k9wx.json'; + + expect(actualFileName, expectedFileName); + }); + }); +} \ No newline at end of file diff --git a/test/unit/shared/models/keyfile/encrypted_keyfile_model_test.dart b/test/unit/shared/models/keyfile/encrypted_keyfile_model_test.dart new file mode 100644 index 00000000..069188f3 --- /dev/null +++ b/test/unit/shared/models/keyfile/encrypted_keyfile_model_test.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:miro/shared/entity/keyfile/keyfile_entity.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception.dart'; +import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart'; +import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart'; +import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart'; +import 'package:miro/shared/models/keyfile/keyfile_secret_data_model.dart'; +import 'package:miro/shared/models/wallet/mnemonic.dart'; +import 'package:miro/shared/models/wallet/wallet.dart'; +import 'package:miro/test/mock_locator.dart'; +import 'package:miro/test/utils/test_utils.dart'; + +// To run this test type in console: +// fvm flutter test test/unit/shared/models/keyfile/encrypted_keyfile_model_test.dart --platform chrome --null-assertions +Future main() async { + await initMockLocator(); + await TestUtils.setupNetworkModel(networkUri: Uri.parse('https://healthy.kira.network/')); + String actualPassword = '123'; + // @formatter:off + Mnemonic actualMnemonic = Mnemonic(value: 'require point property company tongue busy bench burden caution gadget knee glance thought bulk assist month cereal report quarter tool section often require shield'); + Wallet actualWallet = Wallet.derive(mnemonic: actualMnemonic); + // @formatter:on + + group('Tests of EncryptedKeyfileModel.fromEntity() factory constructor', () { + test('Should [return EncryptedKeyfileModel] with version 2.0.0', () { + // Arrange + // @formatter:off + Map actualKeyfileContent = { + 'public_key': 'AlLas8CJ6lm5yZJ8h0U5Qu9nzVvgvskgHuURPB3jvUx8', + 'version': '2.0.0', + 'secret_data': 'RDjZC9U7JTFrsk3u9D7jDf17Ih1IerFTga6ayTR3Ig6Ay5vaRtHAhm/rnmuIQBUDeyXvffcpElfsZwh4LvOwwzzLd9pzRq5CLk3LBqAT6zC/aPsNimo5uXEESeIfua5oUBbob6eyO4bMMLh2NMUhoo/2CIg=' + }; + // @formatter:on + + // Act + KeyfileEntity actualKeyfileEntity = KeyfileEntity.fromJson(actualKeyfileContent); + EncryptedKeyfileModel actualEncryptedKeyfileModel = EncryptedKeyfileModel.fromEntity(actualKeyfileEntity); + + // Assert + // @formatter:off + EncryptedKeyfileModel expectedEncryptedKeyfileModel = EncryptedKeyfileModel( + version: '2.0.0', + publicKey: base64Decode('AlLas8CJ6lm5yZJ8h0U5Qu9nzVvgvskgHuURPB3jvUx8'), + encryptedSecretData: 'RDjZC9U7JTFrsk3u9D7jDf17Ih1IerFTga6ayTR3Ig6Ay5vaRtHAhm/rnmuIQBUDeyXvffcpElfsZwh4LvOwwzzLd9pzRq5CLk3LBqAT6zC/aPsNimo5uXEESeIfua5oUBbob6eyO4bMMLh2NMUhoo/2CIg=', + ); + // @formatter:on + + expect(actualEncryptedKeyfileModel, expectedEncryptedKeyfileModel); + }); + + test('Should [throw KeyfileException] with [KeyfileExceptionType.invalidKeyfile] if keyfile is invalid', () { + // Act + Object? actualException = TestUtils.catchException(() => KeyfileEntity.fromJson(const {'invalid_key': 'invalid_value'})); + + // Assert + KeyfileException expectedException = const KeyfileException(KeyfileExceptionType.invalidKeyfile); + + expect(actualException, expectedException); + }); + }); + + group('Tests of EncryptedKeyfileModel.decrypt() method', () { + test('Should [return DecryptedKeyfileModel] with version 2.0.0', () { + // Arrange + // @formatter:off + EncryptedKeyfileModel actualEncryptedKeyfileModel = EncryptedKeyfileModel( + version: '2.0.0', + publicKey: base64Decode('AlLas8CJ6lm5yZJ8h0U5Qu9nzVvgvskgHuURPB3jvUx8'), + encryptedSecretData: 'RDjZC9U7JTFrsk3u9D7jDf17Ih1IerFTga6ayTR3Ig6Ay5vaRtHAhm/rnmuIQBUDeyXvffcpElfsZwh4LvOwwzzLd9pzRq5CLk3LBqAT6zC/aPsNimo5uXEESeIfua5oUBbob6eyO4bMMLh2NMUhoo/2CIg=', + ); + // @formatter:on + + // Act + DecryptedKeyfileModel actualDecryptedKeyfileModel = actualEncryptedKeyfileModel.decrypt(actualPassword); + + // Assert + DecryptedKeyfileModel expectedDecryptedKeyfileModel = DecryptedKeyfileModel( + version: '2.0.0', + keyfileSecretDataModel: KeyfileSecretDataModel(wallet: actualWallet), + ); + + expect(actualDecryptedKeyfileModel, expectedDecryptedKeyfileModel); + }); + + test('Should [throw KeyfileException] with [KeyfileExceptionType.wrongPassword] if password is invalid', () { + // Arrange + // @formatter:off + EncryptedKeyfileModel actualEncryptedKeyfileModel = EncryptedKeyfileModel( + version: '2.0.0', + publicKey: base64Decode('AlLas8CJ6lm5yZJ8h0U5Qu9nzVvgvskgHuURPB3jvUx8'), + encryptedSecretData: 'RDjZC9U7JTFrsk3u9D7jDf17Ih1IerFTga6ayTR3Ig6Ay5vaRtHAhm/rnmuIQBUDeyXvffcpElfsZwh4LvOwwzzLd9pzRq5CLk3LBqAT6zC/aPsNimo5uXEESeIfua5oUBbob6eyO4bMMLh2NMUhoo/2CIg=', + ); + // @formatter:on + + // Act + Object? actualException = TestUtils.catchException(() => actualEncryptedKeyfileModel.decrypt('invalid_password')); + + // Assert + KeyfileException expectedException = const KeyfileException(KeyfileExceptionType.wrongPassword); + + expect(actualException, expectedException); + }); + }); +} \ No newline at end of file diff --git a/test/unit/shared/models/wallet/keyfile_test.dart b/test/unit/shared/models/wallet/keyfile_test.dart deleted file mode 100644 index 38b57343..00000000 --- a/test/unit/shared/models/wallet/keyfile_test.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:miro/shared/exceptions/invalid_keyfile_exception.dart'; -import 'package:miro/shared/exceptions/invalid_password_exception.dart'; -import 'package:miro/shared/models/wallet/keyfile.dart'; -import 'package:miro/shared/models/wallet/mnemonic.dart'; -import 'package:miro/shared/models/wallet/wallet.dart'; -import 'package:miro/shared/models/wallet/wallet_address.dart'; -import 'package:miro/test/mock_locator.dart'; -import 'package:miro/test/utils/test_utils.dart'; - -// To run this test type in console: -// fvm flutter test test/unit/shared/models/wallet/keyfile_test.dart --platform chrome --null-assertions -Future main() async { - await initMockLocator(); - await TestUtils.setupNetworkModel(networkUri: Uri.parse('https://healthy.kira.network/')); - - // @formatter:off - const String keyFileJSONString = '{\n' - ' "bech32Address": "kira1gdury9ednrjj8fluwj9ea5e6cu5jr9jvekl7u3",\n' - ' "version": "1.0.1",\n' - ' "secretData": "aUDAzfuGC1fB8ACYylYy5Fdqj2upwotYNwuBrb7koYUxmhJaRA8jn8qLXWbFbjbQ/q2mIBtJW5O3clFog+GM2DxiR/1baD00eVOIoZ1Q3IkVcxOSJlYTprV5NsGEQ5mOwjFX3fIHHpqUY0CGCMG7+NBmDPQ="\n' - '}'; - - // Actual Values for tests - const String actualMnemonicString = 'equal success expand debris crash despair awake bachelor athlete discover drop tilt reveal give oven polar party exact sign chalk hurdle move tilt chronic'; - const String actualPassword = 'kiraPassword'; - final Mnemonic actualMnemonic = Mnemonic(value: actualMnemonicString); - final Wallet actualWallet = Wallet.derive(mnemonic: actualMnemonic); - final KeyFile actualKeyfile = KeyFile(wallet: actualWallet, version: KeyFile.latestVersion); - - // Expected Values of tests - // const WalletDetails expectedWalletDetails = Wallet.defaultWalletDetails; - final Uint8List expectedAddressBytes = Uint8List.fromList([67, 120, 50, 23, 45, 152, 229, 35, 167, 252, 116, 139, 158, 211, 58, 199, 41, 33, 150, 76]); - final WalletAddress expectedAddress = WalletAddress(addressBytes: expectedAddressBytes); - - final Uint8List expectedPrivateKey = Uint8List.fromList([158, 115, 126, 2, 208, 98, 193, 1, 114, 159, 189, 20, 131, 168, 118, 66, 223, 196, 48, 193, 71, 233, 115, 59, 192, 240, 216, 104, 85, 120, 94, 60]); - final Wallet expectedWallet = Wallet(address: expectedAddress, privateKey: expectedPrivateKey); - const String expectedVersion = KeyFile.latestVersion; - final KeyFile expectedKeyFile = KeyFile(wallet: expectedWallet, version: expectedVersion); - const String expectedKeyFileName = 'keyfile_kira1gdu_l7u3.json'; - // @formatter:on - - group('Tests of method encode()', () { - // Because AES256.encode() always gives random String we cannot match the hardcoded expected result. - // That`s why we check whether it is possible to encode and decode KeyFile - test('Should encode() secret data, build valid JSON file and check it via decode() JSON back to KeyFile', () async { - String encodedKeyFile = actualKeyfile.encode(actualPassword); - expect( - KeyFile.decode(encodedKeyFile, actualPassword), - actualKeyfile, - ); - }); - }); - - group('Tests of factory constructor Keyfile.decode()', () { - test('Should parse JSON data and decode secret data', () async { - expect( - KeyFile.decode(keyFileJSONString, actualPassword), - expectedKeyFile, - ); - }); - - test('Should build filename in format ex. keyfile_kiraXXXX_XXXX', () async { - expect( - KeyFile.decode(keyFileJSONString, actualPassword).fileName, - expectedKeyFileName, - ); - }); - - test('Should throw FormatException for invalid String data in keyFileJSON', () async { - String actualInvalidKeyFileJSONString = 'invalid keyfile content'; - - expect( - () => KeyFile.decode(actualInvalidKeyFileJSONString, actualPassword), - throwsA(isA()), - ); - }); - - test('Should throw InvalidPasswordException for invalid password', () async { - String actualInvalidPassword = 'invalid PASSWORD'; - - expect( - () => KeyFile.decode(keyFileJSONString, actualInvalidPassword), - throwsA(isA()), - ); - }); - }); -} diff --git a/test/unit/shared/models/wallet/wallet_test.dart b/test/unit/shared/models/wallet/wallet_test.dart index b08e8247..e406c1d4 100644 --- a/test/unit/shared/models/wallet/wallet_test.dart +++ b/test/unit/shared/models/wallet/wallet_test.dart @@ -20,12 +20,12 @@ Future main() async { final Mnemonic actualMnemonic = Mnemonic(value: actualMnemonicString); final Wallet actualWallet = Wallet.derive(mnemonic: actualMnemonic); - const Map actualKeyFilePublicJSON = { - 'version': '1.0.1', + const Map actualKeyfilePublicJSON = { + 'version': '2.0.0', 'bech32Address': 'kira1gdury9ednrjj8fluwj9ea5e6cu5jr9jvekl7u3', }; - const Map actualKeyFilePrivateJSON = { + const Map actualKeyfilePrivateJSON = { 'privateKey': '9e737e02d062c101729fbd1483a87642dfc430c147e9733bc0f0d86855785e3c', }; @@ -62,10 +62,10 @@ Future main() async { }); }); - group('Tests of factory constructor Wallet.fromKeyFileData()', () { + group('Tests of factory constructor Wallet.fromKeyfileData()', () { test('Should create wallet keys from derived private and public json', () async { expect( - Wallet.fromKeyFileData(actualKeyFilePublicJSON, actualKeyFilePrivateJSON), + Wallet.fromKeyfileData(actualKeyfilePublicJSON, actualKeyfilePrivateJSON), expectedWallet, ); });