Merge pull request #219 from cypherstack/staging

Staging
This commit is contained in:
julian-CStack 2022-11-14 13:07:52 -06:00 committed by GitHub
commit e641238657
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 6448 additions and 2214 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#E0E3E3"/>
<path d="M26 8H12.5C10.843 8 9.5 9.34297 9.5 11V29C9.5 30.657 10.843 32 12.5 32H26C27.657 32 29 30.657 29 29V11C29 9.34297 27.6547 8 26 8ZM19.25 14C20.907 14 22.25 15.343 22.25 17C22.25 18.657 20.907 20 19.25 20C17.5934 20 16.25 18.657 16.25 17C16.25 15.343 17.5953 14 19.25 14ZM23.75 26H14.75C14.3375 26 14 25.6625 14 25.25C14 23.1781 15.6781 21.5 17.75 21.5H20.75C22.8209 21.5 24.5 23.1791 24.5 25.25C24.5 25.6625 24.1625 26 23.75 26ZM31.25 11H30.5V15.5H31.25C31.6625 15.5 32 15.1625 32 14.75V11.75C32 11.3356 31.6625 11 31.25 11ZM31.25 17H30.5V21.5H31.25C31.6625 21.5 32 21.1625 32 20.75V17.75C32 17.3375 31.6625 17 31.25 17ZM31.25 23H30.5V27.5H31.25C31.6642 27.5 32 27.1642 32 26.75V23.75C32 23.3375 31.6625 23 31.25 23Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 899 B

View file

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#E0E3E3"/>
<path d="M30.6765 16.1586C30.8183 16.5281 30.698 16.9449 30.4058 17.2156L28.5452 18.9086C28.5925 19.2652 28.6183 19.6305 28.6183 19.9613C28.6183 20.3695 28.5925 20.7348 28.5452 21.0914L30.4058 22.7844C30.698 23.0551 30.8183 23.4676 30.6765 23.8414C30.4874 24.3527 30.2597 24.8469 30.0019 25.3152L29.7999 25.6633C29.5163 26.1359 29.1984 26.5828 28.8503 27.0082C28.5925 27.3133 28.1757 27.4207 27.7976 27.3004L25.4042 26.5355C24.8284 26.9781 24.1538 27.3477 23.5136 27.6313L22.9765 30.0848C22.8906 30.4715 22.5898 30.7465 22.1945 30.8496C21.6015 30.9484 20.9913 31 20.3296 31C19.7452 31 19.1351 30.9484 18.5421 30.8496C18.1468 30.7465 17.846 30.4715 17.7601 30.0848L17.223 27.6313C16.5441 27.3477 15.9081 26.9781 15.3323 26.5355L12.9407 27.3004C12.5609 27.4207 12.1419 27.3133 11.8875 27.0082C11.5391 26.5828 11.2211 26.1359 10.9375 25.6633L10.7364 25.3152C10.4756 24.8469 10.2487 24.3527 10.0584 23.8414C9.91914 23.4719 10.0364 23.0551 10.3312 22.7844L12.19 21.0914C12.1428 20.7348 12.1183 20.3695 12.1183 20C12.1183 19.6305 12.1428 19.2652 12.19 18.9086L10.3312 17.2156C10.0364 16.9449 9.91914 16.5324 10.0584 16.1586C10.2487 15.6473 10.476 15.1531 10.7364 14.6848L10.9371 14.3367C11.2207 13.8641 11.5391 13.4172 11.8875 12.9939C12.1419 12.6867 12.5609 12.5802 12.9407 12.7013L15.3323 13.4645C15.9081 13.0202 16.5441 12.6506 17.223 12.37L17.7601 9.91652C17.846 9.52637 18.1468 9.21656 18.5421 9.15082C19.1351 9.05161 19.7452 9 20.3296 9C20.9913 9 21.6015 9.05161 22.1945 9.15082C22.5898 9.21656 22.8906 9.52637 22.9765 9.91652L23.5136 12.37C24.1538 12.6506 24.8284 13.0202 25.4042 13.4645L27.7976 12.7013C28.1757 12.5802 28.5925 12.6867 28.8503 12.9939C29.1984 13.4172 29.5163 13.8641 29.7999 14.3367L30.0019 14.6848C30.2597 15.1531 30.4874 15.6473 30.6765 16.1586ZM20.3683 23.4375C22.2675 23.4375 23.8058 21.8992 23.8058 19.9613C23.8058 18.1008 22.2675 16.5238 20.3683 16.5238C18.4691 16.5238 16.9308 18.1008 16.9308 19.9613C16.9308 21.8992 18.4691 23.4375 20.3683 23.4375Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -1 +1 @@
Subproject commit 51f74f05d465a92e0118cf7c2bcfb049df21af42
Subproject commit 2da77438527732dfaa5398aa391eab5253dabe19

View file

@ -30,7 +30,8 @@ import 'package:stackwallet/pages/loading_view.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_login_view.dart';
import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart';
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/global/base_currencies_provider.dart';
// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart';
@ -207,6 +208,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
late final Completer<void> loadingCompleter;
bool didLoad = false;
bool _desktopHasPassword = false;
Future<void> load() async {
try {
@ -218,6 +220,11 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
await DB.instance.init();
await _prefs.init();
if (Util.isDesktop) {
_desktopHasPassword =
await ref.read(storageCryptoHandlerProvider).hasPassword();
}
_notificationsService = ref.read(notificationsProvider);
_nodeService = ref.read(nodeServiceChangeNotifierProvider);
_tradesService = ref.read(tradesServiceProvider);
@ -545,21 +552,23 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// FlutterNativeSplash.remove();
if (_wallets.hasWallets || _prefs.hasPin) {
// return HomeView();
if (Util.isDesktop &&
(_wallets.hasWallets || _desktopHasPassword)) {
String? startupWalletId;
if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) {
startupWalletId =
ref.read(prefsChangeNotifierProvider).startupWalletId;
}
// TODO proper desktop auth view
if (Util.isDesktop) {
Future<void>.delayed(Duration.zero).then((value) =>
Navigator.of(context).pushNamedAndRemoveUntil(
DesktopHomeView.routeName, (route) => false));
return Container();
return DesktopLoginView(startupWalletId: startupWalletId);
} else if (!Util.isDesktop &&
(_wallets.hasWallets || _prefs.hasPin)) {
// return HomeView();
String? startupWalletId;
if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) {
startupWalletId =
ref.read(prefsChangeNotifierProvider).startupWalletId;
}
return LockscreenView(

View file

@ -252,7 +252,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
SizedBox(
height: isDesktop ? 40 : 24,
),
if (coin == Coin.monero || coin == Coin.epicCash)
if (coin == Coin.monero ||
coin == Coin.epicCash ||
(coin == Coin.wownero &&
ref.watch(mnemonicWordCountStateProvider.state).state ==
25))
Text(
"Choose start date",
style: isDesktop
@ -264,11 +268,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin == Coin.monero || coin == Coin.epicCash)
if (coin == Coin.monero ||
coin == Coin.epicCash ||
(coin == Coin.wownero &&
ref.watch(mnemonicWordCountStateProvider.state).state ==
25))
SizedBox(
height: isDesktop ? 16 : 8,
),
if (coin == Coin.monero || coin == Coin.epicCash)
if (coin == Coin.monero ||
coin == Coin.epicCash ||
(coin == Coin.wownero &&
ref.watch(mnemonicWordCountStateProvider.state).state ==
25))
// if (!isDesktop)
RestoreFromDatePicker(
@ -278,11 +290,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
// if (isDesktop)
// // TODO desktop date picker
if (coin == Coin.monero || coin == Coin.epicCash)
if (coin == Coin.monero ||
coin == Coin.epicCash ||
(coin == Coin.wownero &&
ref.watch(mnemonicWordCountStateProvider.state).state ==
25))
const SizedBox(
height: 8,
),
if (coin == Coin.monero || coin == Coin.epicCash)
if (coin == Coin.monero ||
coin == Coin.epicCash ||
(coin == Coin.wownero &&
ref.watch(mnemonicWordCountStateProvider.state).state ==
25))
RoundedWhiteContainer(
child: Center(
child: Text(
@ -299,7 +319,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
),
),
),
if (coin == Coin.monero || coin == Coin.epicCash)
if (coin == Coin.monero ||
coin == Coin.epicCash ||
(coin == Coin.wownero &&
ref.watch(mnemonicWordCountStateProvider.state).state ==
25))
SizedBox(
height: isDesktop ? 24 : 16,
),

View file

@ -8,6 +8,7 @@ import 'package:bip39/src/wordlists/english.dart' as bip39wordlist;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_libmonero/monero/monero.dart';
import 'package:flutter_libmonero/wownero/wownero.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
@ -149,12 +150,18 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
super.dispose();
}
// TODO: check for wownero wordlist?
bool _isValidMnemonicWord(String word) {
// TODO: get the actual language
if (widget.coin == Coin.monero) {
var moneroWordList = monero.getMoneroWordList("English");
return moneroWordList.contains(word);
}
if (widget.coin == Coin.wownero) {
var wowneroWordList = wownero.getWowneroWordList("English",
seedWordsLength: widget.seedWordsLength);
return wowneroWordList.contains(word);
}
return _wordListHashSet.contains(word);
}
@ -180,7 +187,13 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
if (widget.coin == Coin.monero) {
height = monero.getHeigthByDate(date: widget.restoreFromDate);
} else if (widget.coin == Coin.wownero) {
height = wownero.getHeightByDate(date: widget.restoreFromDate);
}
// todo: wait until this implemented
// else if (widget.coin == Coin.wownero) {
// height = wownero.getHeightByDate(date: widget.restoreFromDate);
// }
// TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index
if (widget.coin == Coin.epicCash) {

View file

@ -10,6 +10,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -27,6 +28,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget {
required this.walletId,
this.routeOnSuccessName = WalletView.routeName,
required this.trade,
this.shouldSendPublicFiroFunds,
}) : super(key: key);
static const String routeName = "/confirmChangeNowSend";
@ -35,6 +37,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget {
final String walletId;
final String routeOnSuccessName;
final Trade trade;
final bool? shouldSendPublicFiroFunds;
@override
ConsumerState<ConfirmChangeNowSendView> createState() =>
@ -63,7 +66,15 @@ class _ConfirmChangeNowSendViewState
ref.read(walletsChangeNotifierProvider).getManager(walletId);
try {
final txid = await manager.confirmSend(txData: transactionInfo);
late final String txid;
if (widget.shouldSendPublicFiroFunds == true) {
txid = await (manager.wallet as FiroWallet)
.confirmSendPublic(txData: transactionInfo);
} else {
txid = await manager.confirmSend(txData: transactionInfo);
}
unawaited(manager.refresh());
// save note

View file

@ -10,6 +10,8 @@ import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -18,7 +20,9 @@ import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_text.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/expandable.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
@ -162,45 +166,14 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
late final String address;
late final Trade trade;
@override
void initState() {
walletId = widget.walletId;
amount = widget.amount;
address = widget.address;
trade = widget.trade;
super.initState();
}
@override
Widget build(BuildContext context) {
final manager = ref.watch(ref
.watch(walletsChangeNotifierProvider.notifier)
.getManagerProvider(walletId));
final locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
final coin = manager.coin;
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: MaterialButton(
splashColor: Theme.of(context).extension<StackColors>()!.highlight,
key: Key("walletsSheetItemButtonKey_$walletId"),
padding: const EdgeInsets.all(8),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () async {
Future<void> _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async {
final _amount = Format.decimalAmountToSatoshis(amount);
try {
bool wasCancelled = false;
unawaited(showDialog<dynamic>(
unawaited(
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: false,
@ -213,9 +186,14 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
},
);
},
));
),
);
final txData = await manager.prepareSend(
late Map<String, dynamic> txData;
// if not firo then do normal send
if (shouldSendPublicFiroFunds == null) {
txData = await manager.prepareSend(
address: address,
satoshiAmount: _amount,
args: {
@ -223,6 +201,29 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
// ref.read(feeRateTypeStateProvider)
},
);
} else {
final firoWallet = manager.wallet as FiroWallet;
// otherwise do firo send based on balance selected
if (shouldSendPublicFiroFunds) {
txData = await firoWallet.prepareSendPublic(
address: address,
satoshiAmount: _amount,
args: {
"feeRate": FeeRateType.average,
// ref.read(feeRateTypeStateProvider)
},
);
} else {
txData = await firoWallet.prepareSend(
address: address,
satoshiAmount: _amount,
args: {
"feeRate": FeeRateType.average,
// ref.read(feeRateTypeStateProvider)
},
);
}
}
if (!wasCancelled) {
// pop building dialog
@ -244,6 +245,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
walletId: walletId,
routeOnSuccessName: HomeView.routeName,
trade: trade,
shouldSendPublicFiroFunds: shouldSendPublicFiroFunds,
),
settings: const RouteSettings(
name: ConfirmChangeNowSendView.routeName,
@ -286,7 +288,225 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
);
// }
}
}
@override
void initState() {
walletId = widget.walletId;
amount = widget.amount;
address = widget.address;
trade = widget.trade;
super.initState();
}
@override
Widget build(BuildContext context) {
final manager = ref.watch(ref
.watch(walletsChangeNotifierProvider.notifier)
.getManagerProvider(walletId));
final locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
final coin = manager.coin;
final isFiro = coin == Coin.firoTestNet || coin == Coin.firo;
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: ConditionalParent(
condition: isFiro,
builder: (child) => Expandable(
header: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(12),
child: child,
),
),
body: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MaterialButton(
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"),
padding: const EdgeInsets.all(0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () => _send(
manager,
shouldSendPublicFiroFunds: false,
),
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(
top: 6,
left: 16,
right: 16,
bottom: 6,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Use private balance",
style: STextStyles.itemSubtitle(context),
),
FutureBuilder(
future: (manager.wallet as FiroWallet)
.availablePrivateBalance(),
builder: (builderContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
return Text(
"${Format.localizedStringAsFixed(
value: snapshot.data!,
locale: locale,
decimalPlaces: Constants.decimalPlaces,
)} ${coin.ticker}",
style: STextStyles.itemSubtitle(context),
);
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading balance",
"Loading balance.",
"Loading balance..",
"Loading balance..."
],
style: STextStyles.itemSubtitle(context),
);
}
},
),
],
),
SvgPicture.asset(
Assets.svg.chevronRight,
height: 14,
width: 7,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
),
],
),
),
),
),
MaterialButton(
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"),
padding: const EdgeInsets.all(0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () => _send(
manager,
shouldSendPublicFiroFunds: true,
),
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(
top: 6,
left: 16,
right: 16,
bottom: 6,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Use public balance",
style: STextStyles.itemSubtitle(context),
),
FutureBuilder(
future: (manager.wallet as FiroWallet)
.availablePublicBalance(),
builder: (builderContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
return Text(
"${Format.localizedStringAsFixed(
value: snapshot.data!,
locale: locale,
decimalPlaces: Constants.decimalPlaces,
)} ${coin.ticker}",
style: STextStyles.itemSubtitle(context),
);
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading balance",
"Loading balance.",
"Loading balance..",
"Loading balance..."
],
style: STextStyles.itemSubtitle(context),
);
}
},
),
],
),
SvgPicture.asset(
Assets.svg.chevronRight,
height: 14,
width: 7,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
),
],
),
),
),
),
const SizedBox(
height: 6,
),
],
),
),
child: ConditionalParent(
condition: !isFiro,
builder: (child) => MaterialButton(
splashColor: Theme.of(context).extension<StackColors>()!.highlight,
key: Key("walletsSheetItemButtonKey_$walletId"),
padding: const EdgeInsets.all(8),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () => _send(manager),
child: child,
),
child: Row(
children: [
Container(
@ -320,13 +540,17 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
manager.walletName,
style: STextStyles.titleBold12(context),
),
if (!isFiro)
const SizedBox(
height: 2,
),
if (!isFiro)
FutureBuilder(
future: manager.totalBalance,
builder: (builderContext, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
builder:
(builderContext, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
return Text(
"${Format.localizedStringAsFixed(
@ -359,6 +583,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
],
),
),
),
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -101,26 +102,29 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
return null;
}
String query = "";
Map<String, String> queryParams = {};
if (amountString.isNotEmpty) {
query += "amount=$amountString";
queryParams["amount"] = amountString;
}
if (noteString.isNotEmpty) {
if (query.isNotEmpty) {
query += "&";
}
query += "message=$noteString";
queryParams["message"] = noteString;
}
final uri = Uri(
scheme: widget.coin.uriScheme,
host: widget.receivingAddress,
query: query.isNotEmpty ? query : null,
String receivingAddress = widget.receivingAddress;
if ((widget.coin == Coin.bitcoincash ||
widget.coin == Coin.bitcoincashTestnet) &&
receivingAddress.contains(":")) {
// remove cash addr prefix
receivingAddress = receivingAddress.split(":").sublist(1).join();
}
final uriString = AddressUtils.buildUriString(
widget.coin,
receivingAddress,
queryParams,
);
final uriString = uri.toString().replaceFirst("://", ":");
Logging.instance.log("Generated receiving QR code for: $uriString",
level: LogLevel.Info);
@ -229,10 +233,21 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
@override
void initState() {
isDesktop = Util.isDesktop;
_uriString = Uri(
scheme: widget.coin.uriScheme,
host: widget.receivingAddress,
).toString().replaceFirst("://", ":");
String receivingAddress = widget.receivingAddress;
if ((widget.coin == Coin.bitcoincash ||
widget.coin == Coin.bitcoincashTestnet) &&
receivingAddress.contains(":")) {
// remove cash addr prefix
receivingAddress = receivingAddress.split(":").sublist(1).join();
}
_uriString = AddressUtils.buildUriString(
widget.coin,
receivingAddress,
{},
);
amountController = TextEditingController();
noteController = TextEditingController();
super.initState();

View file

@ -87,13 +87,13 @@ class _ConfirmTransactionViewState
txid = await manager.confirmSend(txData: transactionInfo);
}
unawaited(manager.refresh());
// save note
await ref
.read(notesServiceChangeNotifierProvider(walletId))
.editOrAddNote(txid: txid, note: note);
unawaited(manager.refresh());
// pop back to wallet
if (mounted) {
Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName));

View file

@ -110,7 +110,29 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
ref.read(nodeFormDataProvider).useSSL = false;
}
testPassed = await testMoneroNodeConnection(Uri.parse(uriString));
final response = await testMoneroNodeConnection(
Uri.parse(uriString),
false,
);
if (response.cert != null) {
if (mounted) {
final shouldAllowBadCert = await showBadX509CertificateDialog(
response.cert!,
response.url!,
response.port!,
context,
);
if (shouldAllowBadCert) {
final response = await testMoneroNodeConnection(
Uri.parse(uriString), true);
testPassed = response.success;
}
}
} else {
testPassed = response.success;
}
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);

View file

@ -97,7 +97,29 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
testPassed = await testMoneroNodeConnection(Uri.parse(uriString));
final response = await testMoneroNodeConnection(
Uri.parse(uriString),
false,
);
if (response.cert != null) {
if (mounted) {
final shouldAllowBadCert = await showBadX509CertificateDialog(
response.cert!,
response.url!,
response.port!,
context,
);
if (shouldAllowBadCert) {
final response = await testMoneroNodeConnection(
Uri.parse(uriString), true);
testPassed = response.success;
}
}
} else {
testPassed = response.success;
}
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);

View file

@ -15,7 +15,10 @@ import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/progress_bar.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
@ -93,8 +96,14 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
@ -122,6 +131,36 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
);
},
),
),
);
},
child: ConditionalParent(
condition: isDesktop,
builder: (child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
"Choose file location",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
),
),
child,
],
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -139,8 +178,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
await stackFileSystem.prepareStorage();
if (mounted) {
await stackFileSystem
.pickDir(context);
await stackFileSystem.pickDir(context);
}
if (mounted) {
@ -150,8 +188,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
});
}
} catch (e, s) {
Logging.instance.log("$e\n$s",
level: LogLevel.Error);
Logging.instance
.log("$e\n$s", level: LogLevel.Error);
}
},
controller: fileLocationController,
@ -180,8 +218,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
),
),
),
key: const Key(
"createBackupSaveToFileLocationTextFieldKey"),
key:
const Key("createBackupSaveToFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
@ -196,8 +234,21 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
);
}),
if (!Platform.isAndroid)
const SizedBox(
height: 8,
SizedBox(
height: !isDesktop ? 8 : 24,
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Create a passphrase",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
),
ClipRRect(
borderRadius: BorderRadius.circular(
@ -216,6 +267,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
passwordFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
@ -231,9 +284,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
@ -257,8 +308,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
}
final result = zxcvbn.evaluate(newValue);
String suggestionsAndTips = "";
for (var sug
in result.feedback.suggestions!.toSet()) {
for (var sug in result.feedback.suggestions!.toSet()) {
suggestionsAndTips += "$sug\n";
}
suggestionsAndTips += result.feedback.warning!;
@ -275,8 +325,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
}
if (feedback.endsWith("\n")) {
feedback =
feedback.substring(0, feedback.length - 2);
feedback = feedback.substring(0, feedback.length - 2);
}
setState(() {
@ -328,9 +377,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
percent: passwordStrength < 0.25
? 0.03
: passwordStrength,
percent: passwordStrength < 0.25 ? 0.03 : passwordStrength,
),
),
const SizedBox(
@ -353,6 +400,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
passwordRepeatFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
@ -368,9 +417,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
@ -394,8 +441,9 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
const SizedBox(
height: 16,
),
const Spacer(),
TextButton(
if (!isDesktop) const Spacer(),
!isDesktop
? TextButton(
style: shouldEnableCreate
? Theme.of(context)
.extension<StackColors>()!
@ -403,6 +451,114 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonColor(context),
onPressed: !shouldEnableCreate
? null
: () async {
final String pathToSave =
fileLocationController.text;
final String passphrase = passwordController.text;
final String repeatPassphrase =
passwordRepeatController.text;
if (pathToSave.isEmpty) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Directory not chosen",
context: context,
));
return;
}
if (!(await Directory(pathToSave).exists())) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Directory does not exist",
context: context,
));
return;
}
if (passphrase.isEmpty) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "A passphrase is required",
context: context,
));
return;
}
if (passphrase != repeatPassphrase) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Passphrase does not match",
context: context,
));
return;
}
unawaited(showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => const StackDialog(
title: "Encrypting backup",
message: "This shouldn't take long",
),
));
// make sure the dialog is able to be displayed for at least 1 second
await Future<void>.delayed(
const Duration(seconds: 1));
final DateTime now = DateTime.now();
final String fileToSave =
"$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb";
final backup = await SWB.createStackWalletJSON();
bool result =
await SWB.encryptStackWalletWithPassphrase(
fileToSave,
passphrase,
jsonEncode(backup),
);
if (mounted) {
// pop encryption progress dialog
Navigator.of(context).pop();
if (result) {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => Platform.isAndroid
? StackOkDialog(
title: "Backup saved to:",
message: fileToSave,
)
: const StackOkDialog(
title: "Backup creation succeeded"),
);
passwordController.text = "";
passwordRepeatController.text = "";
setState(() {});
} else {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => const StackOkDialog(
title: "Backup creation failed"),
);
}
}
},
child: Text(
"Create backup",
style: STextStyles.button(context),
),
)
: Row(
children: [
PrimaryButton(
width: 183,
desktopMed: true,
label: "Create backup",
enabled: shouldEnableCreate,
onPressed: !shouldEnableCreate
? null
: () async {
@ -502,19 +658,21 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
}
}
},
child: Text(
"Create backup",
style: STextStyles.button(context),
),
const SizedBox(
width: 16,
),
SecondaryButton(
width: 183,
desktopMed: true,
label: "Cancel",
onPressed: () {},
),
],
),
],
),
),
),
);
},
),
),
);
}
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
@ -8,6 +9,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart';
// import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -15,13 +17,17 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:tuple/tuple.dart';
import 'package:stackwallet/utilities/util.dart';
class RestoreFromFileView extends ConsumerStatefulWidget {
const RestoreFromFileView({Key? key}) : super(key: key);
@ -42,6 +48,17 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
bool hidePassword = true;
Future<void> restoreBackupPopup(BuildContext context) async {
// await showDialog<dynamic>(
// context: context,
// useSafeArea: false,
// barrierDismissible: true,
// builder: (context) {
// return const RestoreBackupDialog();
// },
// );
}
@override
void initState() {
stackFileSystem = StackFileSystem();
@ -65,14 +82,21 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
@ -94,6 +118,37 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
);
},
),
),
);
},
child: ConditionalParent(
condition: isDesktop,
builder: (child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Choose file location",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
),
child,
],
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -114,8 +169,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
});
}
} catch (e, s) {
Logging.instance
.log("$e\n$s", level: LogLevel.Error);
Logging.instance.log("$e\n$s", level: LogLevel.Error);
}
},
controller: fileLocationController,
@ -154,8 +208,21 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
),
onChanged: (newValue) {},
),
const SizedBox(
height: 8,
SizedBox(
height: !isDesktop ? 8 : 24,
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Enter passphrase",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
),
ClipRRect(
borderRadius: BorderRadius.circular(
@ -174,6 +241,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
passwordFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
@ -214,8 +283,9 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
const SizedBox(
height: 16,
),
const Spacer(),
TextButton(
if (!isDesktop) const Spacer(),
!isDesktop
? TextButton(
style: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? Theme.of(context)
@ -230,8 +300,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
: () async {
final String fileToRestore =
fileLocationController.text;
final String passphrase =
passwordController.text;
final String passphrase = passwordController.text;
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
@ -240,7 +309,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
}
if (!(await File(fileToRestore).exists())) {
showFloatingFlushBar(
await showFloatingFlushBar(
type: FlushBarType.warning,
message: "Backup file does not exist",
context: context,
@ -249,6 +318,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
}
bool shouldPop = false;
unawaited(
showDialog<dynamic>(
barrierDismissible: false,
context: context,
@ -288,6 +358,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
],
),
),
),
);
final String? jsonString = await compute(
@ -304,7 +375,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
passwordController.text = "";
if (jsonString == null) {
showFloatingFlushBar(
await showFloatingFlushBar(
type: FlushBarType.warning,
message: "Failed to decrypt backup file",
context: context,
@ -312,7 +383,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
return;
}
Navigator.of(context).push(
await Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (_) => StackRestoreProgressView(
jsonString: jsonString,
@ -325,6 +396,169 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
"Restore",
style: STextStyles.button(context),
),
)
: Row(
children: [
PrimaryButton(
width: 183,
desktopMed: true,
label: "Restore",
enabled: !(passwordController.text.isEmpty ||
fileLocationController.text.isEmpty),
onPressed: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? null
: () async {
final String fileToRestore =
fileLocationController.text;
final String passphrase =
passwordController.text;
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (!(await File(fileToRestore).exists())) {
await showFloatingFlushBar(
type: FlushBarType.warning,
message: "Backup file does not exist",
context: context,
);
return;
}
bool shouldPop = false;
unawaited(
showDialog<dynamic>(
barrierDismissible: false,
context: context,
builder: (_) => WillPopScope(
onWillPop: () async {
return shouldPop;
},
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Material(
color: Colors.transparent,
child: Center(
child: Text(
"Decrypting Stack backup file",
style:
STextStyles.pageTitleH2(
context)
.copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.textWhite,
),
),
),
),
const SizedBox(
height: 64,
),
const Center(
child: LoadingIndicator(
width: 100,
),
),
],
),
),
),
);
final String? jsonString = await compute(
SWB.decryptStackWalletWithPassphrase,
Tuple2(fileToRestore, passphrase),
debugLabel:
"stack wallet decryption compute",
);
if (mounted) {
// pop LoadingIndicator
shouldPop = true;
Navigator.of(
context,
rootNavigator: true,
).pop();
passwordController.text = "";
if (jsonString == null) {
await showFloatingFlushBar(
type: FlushBarType.warning,
message:
"Failed to decrypt backup file",
context: context,
);
return;
}
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return DesktopDialog(
maxHeight: 750,
maxWidth: 600,
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight:
constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
mainAxisAlignment:
MainAxisAlignment
.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Padding(
padding:
const EdgeInsets
.all(32),
child: Text(
"Restoring Stack Wallet",
style: STextStyles
.desktopH3(
context),
textAlign:
TextAlign
.center,
),
),
const DesktopDialogCloseButton(),
],
),
const SizedBox(
height: 30,
),
Padding(
padding: EdgeInsets
.symmetric(
horizontal:
32),
child:
StackRestoreProgressView(
jsonString:
jsonString,
),
),
],
),
@ -333,7 +567,24 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
);
},
),
),
);
});
}
},
),
const SizedBox(
width: 16,
),
SecondaryButton(
width: 183,
desktopMed: true,
label: "Cancel",
onPressed: () {},
),
],
),
],
),
));
}
}

View file

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
@ -17,6 +16,8 @@ import 'package:stackwallet/utilities/enums/stack_restoring_status.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
@ -39,6 +40,8 @@ class StackRestoreProgressView extends ConsumerStatefulWidget {
class _StackRestoreProgressViewState
extends ConsumerState<StackRestoreProgressView> {
bool isDesktop = Util.isDesktop;
Future<void> _cancel() async {
bool shouldPop = false;
unawaited(showDialog<void>(
@ -79,10 +82,15 @@ class _StackRestoreProgressViewState
await SWB.cancelRestore();
shouldPop = true;
int count = 0;
if (mounted) {
Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile
!isDesktop
? Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile
? RestoreFromEncryptedStringView.routeName
: StackBackupView.routeName));
: StackBackupView.routeName))
: Navigator.of(context).popUntil((_) => count++ >= 2);
}
}
@ -179,16 +187,23 @@ class _StackRestoreProgressViewState
@override
Widget build(BuildContext context) {
bool isDesktop = Util.isDesktop;
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (_success) {
_addWalletsToHomeView();
@ -213,6 +228,11 @@ class _StackRestoreProgressViewState
top: 12,
right: 12,
),
child: child,
),
),
);
},
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(
@ -422,21 +442,14 @@ class _StackRestoreProgressViewState
),
),
const SizedBox(
height: 80,
height: 30,
),
],
),
),
),
),
floatingActionButton: SizedBox(
SizedBox(
width: MediaQuery.of(context).size.width - 32,
child: TextButton(
onPressed: () async {
if (_success) {
_addWalletsToHomeView();
Navigator.of(context)
.popUntil(ModalRoute.withName(HomeView.routeName));
Navigator.of(context).pop();
} else {
if (await _requestCancel()) {
await _cancel();
@ -456,6 +469,9 @@ class _StackRestoreProgressViewState
),
),
),
],
),
),
),
);
}

View file

@ -4,7 +4,10 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:url_launcher/url_launcher.dart';
@ -18,10 +21,16 @@ class SupportView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
debugPrint("BUILD: $runtimeType");
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
@ -35,6 +44,10 @@ class SupportView extends StatelessWidget {
),
body: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -44,7 +57,11 @@ class SupportView extends StatelessWidget {
style: STextStyles.smallMed12(context),
),
),
const SizedBox(
isDesktop
? const SizedBox(
height: 24,
)
: const SizedBox(
height: 12,
),
RoundedWhiteContainer(
@ -58,10 +75,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("https://t.me/stackwallet"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -69,6 +88,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.socials.telegram,
@ -88,6 +110,17 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "@stackwallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://t.me/stackwallet"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
@ -105,10 +138,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("https://discord.gg/RZMG3yUm"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -116,6 +151,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.socials.discord,
@ -135,6 +173,18 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "Stack Wallet" : "",
onTap: () {
launchUrl(
Uri.parse(
"https://discord.gg/RZMG3yUm"), //expired link?
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
@ -152,10 +202,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("https://www.reddit.com/r/stackwallet/"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -163,6 +215,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.socials.reddit,
@ -182,6 +237,17 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "r/stackwallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://www.reddit.com/r/stackwallet/"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
@ -199,10 +265,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("https://twitter.com/stack_wallet"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -210,6 +278,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.socials.twitter,
@ -229,6 +300,17 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "@stack_wallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://twitter.com/stack_wallet"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
@ -246,10 +328,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("mailto://support@stackwallet.com"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -257,6 +341,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.svg.envelope,
@ -276,12 +363,22 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "support@stackwallet.com" : "",
onTap: () {
launchUrl(
Uri.parse("mailto://support@stackwallet.com"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
],
),
),
);
}
}

View file

@ -471,7 +471,8 @@ class _TransactionDetailsViewState
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
@ -484,7 +485,8 @@ class _TransactionDetailsViewState
? STextStyles
.desktopTextExtraExtraSmall(
context)
: STextStyles.itemSubtitle(context),
: STextStyles.itemSubtitle(
context),
),
const SizedBox(
height: 8,
@ -500,7 +502,8 @@ class _TransactionDetailsViewState
String addressOrContactName =
_transaction.address;
if (snapshot.connectionState ==
ConnectionState.done &&
ConnectionState
.done &&
snapshot.hasData) {
addressOrContactName =
snapshot.data!;
@ -536,11 +539,13 @@ class _TransactionDetailsViewState
StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(
: STextStyles
.itemSubtitle12(
context),
),
],
),
),
if (isDesktop)
IconCopyButton(
data: _transaction.address,

View file

@ -1,10 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
@ -18,7 +20,7 @@ import 'package:stackwallet/widgets/progress_bar.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:zxcvbn/zxcvbn.dart';
class CreatePasswordView extends StatefulWidget {
class CreatePasswordView extends ConsumerStatefulWidget {
const CreatePasswordView({
Key? key,
this.secureStore = const SecureStorageWrapper(
@ -31,10 +33,10 @@ class CreatePasswordView extends StatefulWidget {
final FlutterSecureStorageInterface secureStore;
@override
State<CreatePasswordView> createState() => _CreatePasswordViewState();
ConsumerState<CreatePasswordView> createState() => _CreatePasswordViewState();
}
class _CreatePasswordViewState extends State<CreatePasswordView> {
class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
late final TextEditingController passwordController;
late final TextEditingController passwordRepeatController;
@ -76,8 +78,16 @@ class _CreatePasswordViewState extends State<CreatePasswordView> {
return;
}
await widget.secureStore
.write(key: "stackDesktopPassword", value: passphrase);
try {
await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase);
} catch (e) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Error: $e",
context: context,
));
return;
}
if (mounted) {
unawaited(Navigator.of(context)

View file

@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
class DesktopLoginView extends StatefulWidget {
const DesktopLoginView({
Key? key,
this.startupWalletId,
}) : super(key: key);
static const String routeName = "/desktopLogin";
final String? startupWalletId;
@override
State<DesktopLoginView> createState() => _DesktopLoginViewState();
}
class _DesktopLoginViewState extends State<DesktopLoginView> {
late final TextEditingController passwordController;
late final FocusNode passwordFocusNode;
bool hidePassword = true;
bool _continueEnabled = false;
@override
void initState() {
passwordController = TextEditingController();
passwordFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
passwordController.dispose();
passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DesktopScaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 480,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
Assets.svg.stackIcon(context),
width: 100,
),
const SizedBox(
height: 42,
),
Text(
"Stack Wallet",
style: STextStyles.desktopH1(context),
),
const SizedBox(
height: 24,
),
SizedBox(
width: 350,
child: Text(
"Open source multicoin wallet for everyone",
textAlign: TextAlign.center,
style: STextStyles.desktopSubtitleH1(context),
),
),
const SizedBox(
height: 24,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("desktopLoginPasswordFieldKey"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter password",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: SizedBox(
height: 70,
child: Row(
children: [
const SizedBox(
width: 24,
),
GestureDetector(
key: const Key(
"restoreFromFilePasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 24,
height: 24,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
),
onChanged: (newValue) {
setState(() {
_continueEnabled = passwordController.text.isNotEmpty;
});
},
),
),
const SizedBox(
height: 24,
),
PrimaryButton(
label: "Continue",
enabled: _continueEnabled,
onPressed: () {
// todo auth
Navigator.of(context).pushNamedAndRemoveUntil(
DesktopHomeView.routeName,
(route) => false,
);
},
),
const SizedBox(
height: 60,
),
BlueTextButton(
text: "Forgot password?",
textSize: 20,
onTap: () {
Navigator.of(context).pushNamed(
ForgotPasswordDesktopView.routeName,
);
},
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
class ForgotPasswordDesktopView extends StatefulWidget {
const ForgotPasswordDesktopView({
Key? key,
}) : super(key: key);
static const String routeName = "/forgotPasswordDesktop";
@override
State<ForgotPasswordDesktopView> createState() =>
_ForgotPasswordDesktopViewState();
}
class _ForgotPasswordDesktopViewState extends State<ForgotPasswordDesktopView> {
@override
Widget build(BuildContext context) {
return DesktopScaffold(
appBar: DesktopAppBar(
leading: AppBarBackButton(
onPressed: () async {
if (mounted) {
Navigator.of(context).pop();
}
},
),
isCompactHeight: false,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 480,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
Assets.svg.stackIcon(context),
width: 100,
),
const SizedBox(
height: 42,
),
Text(
"Stack Wallet",
style: STextStyles.desktopH1(context),
),
const SizedBox(
height: 24,
),
SizedBox(
width: 400,
child: Text(
"Stack Wallet does not store your password. Create new wallet or use a Stack backup file to restore your wallet.",
textAlign: TextAlign.center,
style: STextStyles.desktopTextSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
),
const SizedBox(
height: 48,
),
PrimaryButton(
label: "Create new wallet",
onPressed: () {
// // todo delete everything and start fresh?
},
),
const SizedBox(
height: 24,
),
SecondaryButton(
label: "Restore from backup",
onPressed: () {
// todo SWB restore
},
),
const SizedBox(
height: kDesktopAppBarHeight,
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class DesktopAddressBook extends ConsumerStatefulWidget {
const DesktopAddressBook({Key? key}) : super(key: key);
static const String routeName = "/desktopAddressBook";
@override
ConsumerState<DesktopAddressBook> createState() => _DesktopAddressBook();
}
class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> {
late final TextEditingController _searchController;
late final FocusNode _searchFocusNode;
String filter = "";
@override
void initState() {
_searchController = TextEditingController();
_searchFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
DesktopAppBar(
isCompactHeight: true,
leading: Row(
children: [
const SizedBox(
width: 24,
),
Text(
"Address Book",
style: STextStyles.desktopH3(context),
)
],
),
),
const SizedBox(height: 53),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
SizedBox(
height: 60,
width: 489,
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: false,
enableSuggestions: false,
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: (newString) {
setState(() => filter = newString);
},
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Search...",
_searchFocusNode,
context,
).copyWith(
labelStyle: STextStyles.fieldLabel(context)
.copyWith(fontSize: 16),
prefixIcon: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 16,
),
child: SvgPicture.asset(
Assets.svg.search,
width: 16,
height: 16,
),
),
suffixIcon: _searchController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_searchController.text = "";
filter = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
],
),
),
// Expanded(
// child: hasWallets ? const MyWallets() : const EmptyWallets(),
// ),
],
);
}
}

View file

@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
@ -29,19 +32,25 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
Container(
color: Colors.red,
),
Container(
color: Colors.orange,
const Navigator(
key: Key("desktopAddressBookHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopAddressBook.routeName,
),
const Navigator(
key: Key("desktopSettingHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopSettingsView.routeName,
),
Container(
color: Colors.blue,
const Navigator(
key: Key("desktopSupportHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopSupportView.routeName,
),
Container(
color: Colors.pink,
const Navigator(
key: Key("desktopAboutHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopAboutView.routeName,
),
];

View file

@ -1,10 +1,14 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/isar/models/log.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/debug_service_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/enums/log_level_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
@ -105,7 +109,7 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
],
),
Expanded(
flex: 24,
// flex: 24,
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) {
@ -314,7 +318,7 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
),
),
),
const Spacer(),
// const Spacer(),
Padding(
padding: const EdgeInsets.all(32),
child: Row(
@ -322,7 +326,18 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
Expanded(
child: SecondaryButton(
label: "Clear logs",
onPressed: () {},
onPressed: () async {
await ref.read(debugServiceProvider).deleteAllMessages();
await ref.read(debugServiceProvider).updateRecentLogs();
if (mounted) {
Navigator.pop(context);
unawaited(showFloatingFlushBar(
type: FlushBarType.info,
context: context,
message: 'Logs cleared!'));
}
},
),
),
const SizedBox(

View file

@ -1,16 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/ui/color_theme_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/color_theme.dart';
import 'package:stackwallet/utilities/theme/dark_colors.dart';
import 'package:stackwallet/utilities/theme/light_colors.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import '../../../providers/global/prefs_provider.dart';
import '../../../utilities/constants.dart';
import '../../../widgets/custom_buttons/draggable_switch_button.dart';
class AppearanceOptionSettings extends ConsumerStatefulWidget {
const AppearanceOptionSettings({Key? key}) : super(key: key);
@ -23,6 +26,19 @@ class AppearanceOptionSettings extends ConsumerStatefulWidget {
class _AppearanceOptionSettings
extends ConsumerState<AppearanceOptionSettings> {
// late bool isLight;
// @override
// void initState() {
//
// super.initState();
// }
//
// @override
// void dispose() {
// super.dispose();
// }
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
@ -128,10 +144,7 @@ class _AppearanceOptionSettings
),
),
const Padding(
padding: EdgeInsets.only(
left: 10,
right: 10,
),
padding: EdgeInsets.all(10),
child: ThemeToggle(),
),
],
@ -143,7 +156,7 @@ class _AppearanceOptionSettings
}
}
class ThemeToggle extends StatefulWidget {
class ThemeToggle extends ConsumerStatefulWidget {
const ThemeToggle({
Key? key,
}) : super(key: key);
@ -152,47 +165,113 @@ class ThemeToggle extends StatefulWidget {
// final void Function(bool)? onChanged;
@override
State<StatefulWidget> createState() => _ThemeToggle();
ConsumerState<ThemeToggle> createState() => _ThemeToggle();
}
class _ThemeToggle extends State<ThemeToggle> {
class _ThemeToggle extends ConsumerState<ThemeToggle> {
// late bool externalCallsEnabled;
late String _selectedTheme;
@override
void initState() {
_selectedTheme =
DB.instance.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme")
as String? ??
"light";
super.initState();
}
@override
Widget build(BuildContext context) {
return Row(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: RawMaterialButton(
elevation: 0,
MaterialButton(
splashColor: Colors.transparent,
hoverColor: Colors.transparent,
padding: const EdgeInsets.all(0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius * 2,
Constants.size.circularBorderRadius,
),
),
onPressed: () {}, //onPressed
child: Stack(
onPressed: () {
DB.instance.put<dynamic>(
boxName: DB.boxNameTheme,
key: "colorScheme",
value: ThemeType.light.name,
);
ref.read(colorThemeProvider.state).state =
StackColors.fromStackColorTheme(
LightColors(),
);
setState(() {
_selectedTheme = "light";
});
},
child: SizedBox(
width: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
left: 24,
Container(
decoration: BoxDecoration(
border: Border.all(
width: 2.5,
color: _selectedTheme == "light"
? Theme.of(context)
.extension<StackColors>()!
.infoItemIcons
: Theme.of(context).extension<StackColors>()!.popupBG,
),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: SvgPicture.asset(
Assets.svg.themeLight,
),
),
Padding(
padding: const EdgeInsets.only(
left: 50,
top: 12,
const SizedBox(
height: 12,
),
child: Text(
Row(
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: "light",
groupValue: _selectedTheme,
onChanged: (newValue) {
if (newValue is String && newValue == "light") {
DB.instance.put<dynamic>(
boxName: DB.boxNameTheme,
key: "colorScheme",
value: ThemeType.light.name,
);
ref.read(colorThemeProvider.state).state =
StackColors.fromStackColorTheme(
LightColors(),
);
setState(() {
_selectedTheme = "light";
});
}
},
),
),
const SizedBox(
width: 14,
),
Text(
"Light",
style:
STextStyles.desktopTextExtraSmall(context).copyWith(
@ -201,114 +280,114 @@ class _ThemeToggle extends State<ThemeToggle> {
.textDark,
),
),
)
],
),
// if (externalCallsEnabled)
Positioned(
bottom: 0,
left: 6,
child: SvgPicture.asset(
Assets.svg.checkCircle,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
),
// if (!externalCallsEnabled)
// Positioned(
// top: 4,
// right: 4,
// child: Container(
// width: 20,
// height: 20,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(1000),
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFieldDefaultBG,
// ),
// ),
// ),
],
),
),
),
const SizedBox(
width: 1,
width: 20,
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: RawMaterialButton(
elevation: 0,
MaterialButton(
splashColor: Colors.transparent,
hoverColor: Colors.transparent,
padding: const EdgeInsets.all(0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius * 2,
Constants.size.circularBorderRadius,
),
),
onPressed: () {}, //onPressed
child: Stack(
onPressed: () {
DB.instance.put<dynamic>(
boxName: DB.boxNameTheme,
key: "colorScheme",
value: ThemeType.dark.name,
);
ref.read(colorThemeProvider.state).state =
StackColors.fromStackColorTheme(
DarkColors(),
);
setState(() {
_selectedTheme = "dark";
});
},
child: SizedBox(
width: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(
Container(
decoration: BoxDecoration(
border: Border.all(
width: 2.5,
color: _selectedTheme == "dark"
? Theme.of(context)
.extension<StackColors>()!
.infoItemIcons
: Theme.of(context).extension<StackColors>()!.popupBG,
),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: SvgPicture.asset(
Assets.svg.themeDark,
),
Padding(
padding: const EdgeInsets.only(
left: 50,
top: 12,
),
child: Text(
const SizedBox(
height: 12,
),
Row(
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: "dark",
groupValue: _selectedTheme,
onChanged: (newValue) {
if (newValue is String && newValue == "dark") {
DB.instance.put<dynamic>(
boxName: DB.boxNameTheme,
key: "colorScheme",
value: ThemeType.dark.name,
);
ref.read(colorThemeProvider.state).state =
StackColors.fromStackColorTheme(
DarkColors(),
);
setState(() {
_selectedTheme = "dark";
});
}
},
),
),
const SizedBox(
width: 14,
),
Text(
"Dark",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
style:
STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
],
),
// if (externalCallsEnabled)
Positioned(
bottom: 0,
left: 0,
child: SvgPicture.asset(
Assets.svg.checkCircle,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
),
// if (!externalCallsEnabled)
// Positioned(
// top: 4,
// right: 4,
// child: Container(
// width: 20,
// height: 20,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(1000),
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFieldDefaultBG,
// ),
// ),
// ),
],
),
),
),
),
],
);
}

View file

@ -2,14 +2,28 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart';
import 'package:stackwallet/providers/global/locale_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../providers/global/auto_swb_service_provider.dart';
import '../../../../widgets/custom_buttons/blue_text_button.dart';
class BackupRestoreSettings extends ConsumerStatefulWidget {
const BackupRestoreSettings({Key? key}) : super(key: key);
@ -21,206 +35,52 @@ class BackupRestoreSettings extends ConsumerStatefulWidget {
}
class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return ListView(
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: [
Padding(
padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(
Assets.svg.backupAuto,
width: 48,
height: 48,
),
Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Auto Backup",
style: STextStyles.desktopTextSmall(context),
),
TextSpan(
text:
"\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data."
"To ensure maximum security, we recommend using a unique password that you haven't used anywhere "
"else on the internet before. Your password is not stored.",
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
TextSpan(
text:
"\n\nFor more information, please see our website ",
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
TextSpan(
text: "stackwallet.com",
style: STextStyles.richLink(context)
.copyWith(fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(
Uri.parse("https://stackwallet.com/"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Padding(
padding: EdgeInsets.all(
10,
),
child: AutoBackupButton(),
),
],
),
],
),
),
),
const SizedBox(
height: 25,
),
Padding(
padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(
Assets.svg.backupAdd,
width: 48,
height: 48,
alignment: Alignment.topLeft,
),
Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Manual Backup",
style: STextStyles.desktopTextSmall(context),
),
TextSpan(
text:
"\n\nCreate manual backup to easily transfer your data between devices. "
"You will create a backup file that can be later used in the Restore option. "
"Use a strong password to encrypt your data.",
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
],
),
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Padding(
padding: EdgeInsets.all(
10,
),
child: ManualBackupButton(),
),
],
),
],
),
),
),
const SizedBox(
height: 25,
),
Padding(
padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(
Assets.svg.backupRestore,
width: 48,
height: 48,
alignment: Alignment.topLeft,
),
Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Restore Backup",
style: STextStyles.desktopTextSmall(context),
),
TextSpan(
text:
"\n\nUse your Stack Wallet backup file to restore your wallets, address book "
"and wallet preferences.",
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
],
),
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Padding(
padding: EdgeInsets.all(
10,
),
child: RestoreBackupButton(),
),
],
),
],
),
),
),
],
);
late bool createBackup = false;
late bool restoreBackup = false;
final toggleController = DSBController();
late final TextEditingController fileLocationController;
late final TextEditingController passwordController;
late final TextEditingController frequencyController;
late final FocusNode fileLocationFocusNode;
late final FocusNode passwordFocusNode;
String prettySinceLastBackupString(DateTime? time) {
if (time == null) {
return "-";
}
final difference = DateTime.now().difference(time);
int value;
String postfix;
if (difference < const Duration(seconds: 60)) {
value = difference.inSeconds;
postfix = "seconds";
} else if (difference < const Duration(minutes: 60)) {
value = difference.inMinutes;
postfix = "minutes";
} else if (difference < const Duration(hours: 24)) {
value = difference.inHours;
postfix = "hours";
} else if (difference.inDays < 8) {
value = difference.inDays;
postfix = "days";
} else {
// if greater than a week return the actual date
return DateFormat.yMMMMd(
ref.read(localeServiceChangeNotifierProvider).locale)
.format(time);
}
class AutoBackupButton extends ConsumerWidget {
const AutoBackupButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> enableAutoBackup() async {
if (value == 1) {
postfix = postfix.substring(0, postfix.length - 1);
}
return "$value $postfix ago";
}
Future<void> enableAutoBackup(BuildContext context) async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
@ -231,80 +91,460 @@ class AutoBackupButton extends ConsumerWidget {
);
}
return SizedBox(
width: 200,
height: 48,
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () {
enableAutoBackup();
},
child: Text(
"Enable auto backup",
style: STextStyles.button(context),
),
),
);
}
}
class ManualBackupButton extends ConsumerWidget {
const ManualBackupButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return SizedBox(
width: 200,
height: 48,
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () {},
child: Text(
"Create manual backup",
style: STextStyles.button(context),
),
),
);
}
}
class RestoreBackupButton extends ConsumerWidget {
const RestoreBackupButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> restoreBackup() async {
Future<void> createAutoBackup() async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const RestoreBackupDialog();
return CreateAutoBackup();
},
);
}
return SizedBox(
width: 200,
height: 48,
child: TextButton(
Future<void> attemptDisable() async {
final result = await showDialog<bool?>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Disable Auto Backup",
message:
"You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Text(
"Back",
style: STextStyles.button(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.accentColorDark,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () {
restoreBackup();
},
child: Text(
"Restore",
"Disable",
style: STextStyles.button(context),
),
onPressed: () {
Navigator.of(context).pop();
setState(() {
ref.watch(prefsChangeNotifierProvider).isAutoBackupEnabled =
false;
});
},
),
);
},
);
if (mounted) {
if (result is bool && result) {
ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = false;
Navigator.of(context).pop();
} else {
toggleController.activate?.call();
}
}
}
@override
void initState() {
fileLocationController = TextEditingController();
passwordController = TextEditingController();
frequencyController = TextEditingController();
passwordController.text = "---------------";
fileLocationController.text =
ref.read(prefsChangeNotifierProvider).autoBackupLocation ?? " ";
frequencyController.text = Format.prettyFrequencyType(
ref.read(prefsChangeNotifierProvider).backupFrequencyType);
fileLocationFocusNode = FocusNode();
passwordFocusNode = FocusNode();
// _toggle = ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled;
super.initState();
}
@override
void dispose() {
fileLocationController.dispose();
passwordController.dispose();
frequencyController.dispose();
fileLocationFocusNode.dispose();
passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider
.select((value) => value.isAutoBackupEnabled));
ref.listen(
prefsChangeNotifierProvider
.select((value) => value.backupFrequencyType),
(previous, BackupFrequencyType next) {
frequencyController.text = Format.prettyFrequencyType(next);
});
return LayoutBuilder(builder: (context, constraints) {
return SingleChildScrollView(
scrollDirection: Axis.vertical,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
Assets.svg.backupAuto,
width: 48,
height: 48,
),
isEnabledAutoBackup
? SvgPicture.asset(
Assets.svg.enableButton,
)
: SvgPicture.asset(
Assets.svg.disableButton,
),
],
),
),
Center(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Auto Backup",
style: STextStyles.desktopTextSmall(
context),
),
TextSpan(
text:
"\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data."
"To ensure maximum security, we recommend using a unique password that you haven't used anywhere "
"else on the internet before. Your password is not stored.",
style: STextStyles
.desktopTextExtraExtraSmall(
context),
),
TextSpan(
text:
"\n\nFor more information, please see our website ",
style: STextStyles
.desktopTextExtraExtraSmall(
context),
),
TextSpan(
text: "stackwallet.com",
style: STextStyles.richLink(context)
.copyWith(fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(
Uri.parse(
"https://stackwallet.com/"),
mode: LaunchMode
.externalApplication,
);
},
),
],
),
),
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: !isEnabledAutoBackup
? PrimaryButton(
desktopMed: true,
width: 200,
label: "Enable auto backup",
onPressed: () {
enableAutoBackup(context);
},
)
: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Container(
width: 403,
color: Theme.of(context)
.extension<StackColors>()!
.background,
child: Padding(
padding:
const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Text(
"Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}",
style: STextStyles
.itemSubtitle(
context),
),
BlueTextButton(
text: "Back up now",
onTap: () {
ref
.read(
autoSWBServiceProvider)
.doBackup();
},
),
],
),
],
),
),
),
const SizedBox(
height: 20,
),
Row(
children: [
PrimaryButton(
desktopMed: true,
width: 190,
label: "Disable auto backup",
onPressed: () {
attemptDisable();
},
),
const SizedBox(width: 16),
SecondaryButton(
desktopMed: true,
width: 190,
label: "Edit auto backup",
onPressed: () {
createAutoBackup();
},
),
],
)
],
),
),
],
),
],
),
),
),
const SizedBox(
height: 25,
),
Padding(
padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: SvgPicture.asset(
Assets.svg.backupAdd,
width: 48,
height: 48,
alignment: Alignment.topLeft,
),
),
Center(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Manual Backup",
style: STextStyles.desktopTextSmall(
context),
),
TextSpan(
text:
"\n\nCreate manual backup to easily transfer your data between devices. "
"You will create a backup file that can be later used in the Restore option. "
"Use a strong password to encrypt your data.",
style: STextStyles
.desktopTextExtraExtraSmall(
context),
),
],
),
),
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(
10,
),
child: createBackup
? const SizedBox(
width: 512,
child: CreateBackupView(),
)
: PrimaryButton(
desktopMed: true,
width: 200,
label: "Create manual backup",
onPressed: () {
setState(() {
createBackup = true;
});
},
),
),
],
),
],
),
),
),
const SizedBox(
height: 25,
),
Padding(
padding: const EdgeInsets.only(
right: 30,
bottom: 40,
),
child: RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: SvgPicture.asset(
Assets.svg.backupRestore,
width: 48,
height: 48,
alignment: Alignment.topLeft,
),
),
Center(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Restore Backup",
style: STextStyles.desktopTextSmall(
context),
),
TextSpan(
text:
"\n\nUse your Stack Wallet backup file to restore your wallets, address book "
"and wallet preferences.",
style: STextStyles
.desktopTextExtraExtraSmall(
context),
),
],
),
),
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(
10,
),
child: restoreBackup
? const SizedBox(
width: 512,
child: RestoreFromFileView(),
)
: PrimaryButton(
desktopMed: true,
width: 200,
label: "Restore backup",
onPressed: () {
setState(() {
restoreBackup = true;
});
},
),
),
],
),
],
),
),
),
],
),
),
));
});
}
}

View file

@ -1,55 +1,111 @@
import 'dart:convert';
import 'dart:io';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stack_wallet_backup/stack_wallet_backup.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/enums/log_level_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/progress_bar.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:zxcvbn/zxcvbn.dart';
class CreateAutoBackup extends StatefulWidget {
const CreateAutoBackup({Key? key}) : super(key: key);
class CreateAutoBackup extends ConsumerStatefulWidget {
const CreateAutoBackup({
Key? key,
this.secureStore = const SecureStorageWrapper(
FlutterSecureStorage(),
),
}) : super(key: key);
final FlutterSecureStorageInterface secureStore;
@override
State<StatefulWidget> createState() => _CreateAutoBackup();
ConsumerState<CreateAutoBackup> createState() => _CreateAutoBackup();
}
class _CreateAutoBackup extends State<CreateAutoBackup> {
class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
late final TextEditingController fileLocationController;
late final TextEditingController passphraseController;
late final TextEditingController passphraseRepeatController;
late final FocusNode chooseFileLocation;
late final FlutterSecureStorageInterface secureStore;
late final StackFileSystem stackFileSystem;
late final FocusNode passphraseFocusNode;
late final FocusNode passphraseRepeatFocusNode;
final zxcvbn = Zxcvbn();
bool shouldShowPasswordHint = true;
bool hidePassword = true;
String passwordFeedback =
"Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters.";
double passwordStrength = 0.0;
bool get shouldEnableCreate {
return fileLocationController.text.isNotEmpty &&
passphraseController.text.isNotEmpty &&
passphraseRepeatController.text.isNotEmpty;
}
bool get fieldsMatch =>
passphraseController.text == passphraseRepeatController.text;
String _currentDropDownValue = "Every 10 minutes";
BackupFrequencyType _currentDropDownValue =
BackupFrequencyType.everyTenMinutes;
final List<String> _dropDownItems = [
"Every 10 minutes",
"Every 20 minutes",
"Every 30 minutes",
final List<BackupFrequencyType> _dropDownItems = [
BackupFrequencyType.everyTenMinutes,
BackupFrequencyType.everyAppStart,
BackupFrequencyType.afterClosingAWallet,
];
@override
void initState() {
secureStore = widget.secureStore;
stackFileSystem = StackFileSystem();
fileLocationController = TextEditingController();
passphraseController = TextEditingController();
passphraseRepeatController = TextEditingController();
chooseFileLocation = FocusNode();
passphraseFocusNode = FocusNode();
passphraseRepeatFocusNode = FocusNode();
if (Platform.isAndroid) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final dir = await stackFileSystem.prepareStorage();
if (mounted) {
setState(() {
fileLocationController.text = dir.path;
});
}
});
}
super.initState();
}
@ -59,7 +115,6 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
passphraseController.dispose();
passphraseRepeatController.dispose();
chooseFileLocation.dispose();
passphraseFocusNode.dispose();
passphraseRepeatFocusNode.dispose();
@ -70,10 +125,13 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType ");
String? selectedItem = "Every 10 minutes";
bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider
.select((value) => value.isAutoBackupEnabled));
String? selectedItem = "Every 10 minutes";
final isDesktop = Util.isDesktop;
return DesktopDialog(
maxHeight: 650,
maxHeight: 680,
maxWidth: 600,
child: Column(
children: [
@ -127,89 +185,103 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
height: 10,
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("backupChooseFileLocation"),
focusNode: chooseFileLocation,
controller: fileLocationController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
textAlign: TextAlign.left,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Save to...",
chooseFileLocation,
context,
).copyWith(
labelStyle:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
suffixIcon: Container(
decoration: BoxDecoration(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!Platform.isAndroid)
Consumer(builder: (context, ref, __) {
return Container(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
child: TextField(
autocorrect: false,
enableSuggestions: false,
onTap: Platform.isAndroid
? null
: () async {
try {
await stackFileSystem.prepareStorage();
if (mounted) {
await stackFileSystem.pickDir(context);
}
if (mounted) {
setState(() {
fileLocationController.text =
stackFileSystem.dirPath ?? "";
});
}
} catch (e, s) {
Logging.instance
.log("$e\n$s", level: LogLevel.Error);
}
},
controller: fileLocationController,
style: STextStyles.field(context),
decoration: InputDecoration(
hintText: "Save to...",
hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 20,
height: 17.5,
),
),
width: 16,
height: 16,
),
const SizedBox(
width: 12,
),
],
),
),
),
key: const Key(
"createBackupSaveToFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {},
),
);
}),
if (!Platform.isAndroid)
const SizedBox(
height: 24,
),
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 32),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Create a passphrase",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
),
const SizedBox(
height: 10,
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
),
child: ClipRRect(
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPassphrase"),
key: const Key("createBackupPasswordFieldKey1"),
focusNode: passphraseFocusNode,
controller: passphraseController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
@ -219,62 +291,133 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
context,
).copyWith(
labelStyle:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: GestureDetector(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createDesktopAutoBackupShowPassphraseButton1"),
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 20,
height: 17.5,
),
),
),
),
),
),
),
width: 16,
height: 16,
),
),
const SizedBox(
height: 16,
width: 12,
),
],
),
),
),
onChanged: (newValue) {
if (newValue.isEmpty) {
setState(() {
passwordFeedback = "";
});
return;
}
final result = zxcvbn.evaluate(newValue);
String suggestionsAndTips = "";
for (var sug in result.feedback.suggestions!.toSet()) {
suggestionsAndTips += "$sug\n";
}
suggestionsAndTips += result.feedback.warning!;
String feedback =
// "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
suggestionsAndTips;
passwordStrength = result.score! / 4;
// hack fix to format back string returned from zxcvbn
if (feedback.contains("phrasesNo need")) {
feedback = feedback.replaceFirst(
"phrasesNo need", "phrases\nNo need");
}
if (feedback.endsWith("\n")) {
feedback = feedback.substring(0, feedback.length - 2);
}
setState(() {
passwordFeedback = feedback;
});
},
),
),
if (passphraseFocusNode.hasFocus ||
passphraseRepeatFocusNode.hasFocus ||
passphraseController.text.isNotEmpty)
Padding(
padding: EdgeInsets.only(
left: 12,
right: 12,
top: passwordFeedback.isNotEmpty ? 4 : 0,
),
child: passwordFeedback.isNotEmpty
? Text(
passwordFeedback,
style: STextStyles.infoSmall(context),
)
: null,
),
if (passphraseFocusNode.hasFocus ||
passphraseRepeatFocusNode.hasFocus ||
passphraseController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
left: 12,
right: 12,
top: 10,
),
child: ClipRRect(
child: ProgressBar(
key: const Key("createStackBackUpProgressBar"),
width: 512,
height: 5,
fillColor: passwordStrength < 0.51
? Theme.of(context)
.extension<StackColors>()!
.accentColorRed
: passwordStrength < 1
? Theme.of(context)
.extension<StackColors>()!
.accentColorYellow
: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
percent:
passwordStrength < 0.25 ? 0.03 : passwordStrength,
),
),
const SizedBox(
height: 10,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPassphrase"),
key: const Key("createBackupPasswordFieldKey2"),
focusNode: passphraseRepeatFocusNode,
controller: passphraseRepeatController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
@ -283,42 +426,46 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
passphraseRepeatFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
labelStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: GestureDetector(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createDesktopAutoBackupShowPassphraseButton2"),
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 20,
height: 17.5,
),
),
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
setState(() {});
// TODO: ? check if passwords match?
},
),
),
],
),
),
const SizedBox(
@ -343,39 +490,85 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
left: 32,
right: 32,
),
child: DropdownButtonFormField(
child: isDesktop
? DropdownButtonHideUnderline(
child: DropdownButton2(
offset: Offset(0, -10),
isExpanded: true,
elevation: 0,
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
icon: SvgPicture.asset(
Assets.svg.chevronDown,
width: 10,
height: 5,
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
dropdownColor:
Theme.of(context).extension<StackColors>()!.textFieldActiveBG,
// focusColor: ,
dropdownElevation: 0,
value: _currentDropDownValue,
items: _dropDownItems
.map(
(e) => DropdownMenuItem(
items: [
..._dropDownItems.map(
(e) {
String message = "";
switch (e) {
case BackupFrequencyType.everyTenMinutes:
message = "Every 10 minutes";
break;
case BackupFrequencyType.everyAppStart:
message = "Every app startup";
break;
case BackupFrequencyType.afterClosingAWallet:
message =
"After closing a cryptocurrency wallet";
break;
}
return DropdownMenuItem(
value: e,
child: Text(e),
child: Text(message),
);
},
),
)
.toList(),
],
onChanged: (value) {
if (value is String) {
if (value is BackupFrequencyType) {
if (ref
.read(prefsChangeNotifierProvider)
.backupFrequencyType !=
value) {
ref
.read(prefsChangeNotifierProvider)
.backupFrequencyType = value;
}
setState(() {
_currentDropDownValue = value;
});
}
},
icon: SvgPicture.asset(
Assets.svg.chevronDown,
width: 10,
height: 5,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
buttonPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
buttonDecoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
dropdownDecoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
)
: null,
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(32),
child: Row(
@ -385,7 +578,9 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
label: "Cancel",
onPressed: () {
int count = 0;
Navigator.of(context).popUntil((_) => count++ >= 2);
!isEnabledAutoBackup
? Navigator.of(context).popUntil((_) => count++ >= 2)
: Navigator.of(context).pop();
},
),
),
@ -395,8 +590,164 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
Expanded(
child: PrimaryButton(
label: "Enable Auto Backup",
enabled: false,
onPressed: () {},
enabled: shouldEnableCreate,
onPressed: !shouldEnableCreate
? null
: () async {
final String pathToSave =
fileLocationController.text;
final String passphrase = passphraseController.text;
final String repeatPassphrase =
passphraseRepeatController.text;
if (pathToSave.isEmpty) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Directory not chosen",
context: context,
);
return;
}
if (!(await Directory(pathToSave).exists())) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Directory does not exist",
context: context,
);
return;
}
if (passphrase.isEmpty) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "A passphrase is required",
context: context,
);
return;
}
if (passphrase != repeatPassphrase) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Passphrase does not match",
context: context,
);
return;
}
showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => const StackDialog(
title: "Encrypting initial backup",
message: "This shouldn't take long",
),
);
// make sure the dialog is able to be displayed for at least some time
final fut = Future<void>.delayed(
const Duration(milliseconds: 300));
String adkString;
int adkVersion;
try {
final adk =
await compute(generateAdk, passphrase);
adkString = Format.uint8listToString(adk.item2);
adkVersion = adk.item1;
} on Exception catch (e, s) {
String err = getErrorMessageFromSWBException(e);
Logging.instance
.log("$err\n$s", level: LogLevel.Error);
// pop encryption progress dialog
Navigator.of(context).pop();
showFloatingFlushBar(
type: FlushBarType.warning,
message: err,
context: context,
);
return;
} catch (e, s) {
Logging.instance
.log("$e\n$s", level: LogLevel.Error);
// pop encryption progress dialog
Navigator.of(context).pop();
showFloatingFlushBar(
type: FlushBarType.warning,
message: "$e",
context: context,
);
return;
}
await secureStore.write(
key: "auto_adk_string", value: adkString);
await secureStore.write(
key: "auto_adk_version_string",
value: adkVersion.toString());
final DateTime now = DateTime.now();
final String fileToSave =
createAutoBackupFilename(pathToSave, now);
final backup = await SWB.createStackWalletJSON();
bool result = await SWB.encryptStackWalletWithADK(
fileToSave,
adkString,
jsonEncode(backup),
adkVersion: adkVersion,
);
// this future should already be complete unless there was an error encrypting
await Future.wait([fut]);
if (mounted) {
// pop encryption progress dialog
int count = 0;
Navigator.of(context)
.popUntil((_) => count++ >= 2);
if (result) {
ref
.read(prefsChangeNotifierProvider)
.autoBackupLocation = pathToSave;
ref
.read(prefsChangeNotifierProvider)
.lastAutoBackup = now;
ref
.read(prefsChangeNotifierProvider)
.isAutoBackupEnabled = true;
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => Platform.isAndroid
? StackOkDialog(
title:
"Stack Auto Backup enabled and saved to:",
message: fileToSave,
)
: const StackOkDialog(
title: "Stack Auto Backup enabled!"),
);
if (mounted) {
passphraseController.text = "";
passphraseRepeatController.text = "";
int count = 0;
Navigator.of(context)
.popUntil((_) => count++ >= 2);
}
} else {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => const StackOkDialog(
title: "Failed to enable Auto Backup"),
);
}
}
},
),
)
],

View file

@ -61,8 +61,7 @@ class EnableBackupDialog extends StatelessWidget {
child: SecondaryButton(
label: "Cancel",
onPressed: () {
int count = 0;
Navigator.of(context).popUntil((_) => count++ >= 2);
Navigator.of(context).pop();
},
),
),

View file

@ -1,93 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
class RestoreBackupDialog extends StatelessWidget {
const RestoreBackupDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DesktopDialog(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(32),
child: Text(
"Restoring Stack Wallet",
style: STextStyles.desktopH3(context),
textAlign: TextAlign.center,
),
),
const DesktopDialogCloseButton(),
],
),
const SizedBox(
height: 30,
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
),
child: Row(
children: [
Text(
"Settings",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
textAlign: TextAlign.left,
),
],
),
),
// RoundedWhiteContainer(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(),
// ],
// ),
// ),
const Spacer(),
Padding(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
onPressed: () {
Navigator.of(context).pop();
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Continue",
onPressed: () {
// Navigator.of(context).pop();
// onConfirm.call();
},
),
)
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,716 @@
import 'dart:convert';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS;
import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart';
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:url_launcher/url_launcher.dart';
const kGithubAPI = "https://api.github.com";
const kGithubSearch = "/search/commits";
const kGithubHead = "/repos";
enum CommitStatus { isHead, isOldCommit, notACommit, notLoaded }
Future<bool> doesCommitExist(
String organization,
String project,
String commit,
) async {
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
final Client client = Client();
try {
final uri = Uri.parse(
"$kGithubAPI$kGithubHead/$organization/$project/commits/$commit");
final commitQuery = await client.get(
uri,
headers: {'Content-Type': 'application/json'},
);
final response = jsonDecode(commitQuery.body.toString());
Logging.instance.log("doesCommitExist $project $commit $response",
level: LogLevel.Info);
bool isThereCommit;
try {
isThereCommit = response['sha'] == commit;
Logging.instance
.log("isThereCommit $isThereCommit", level: LogLevel.Info);
return isThereCommit;
} catch (e, s) {
return false;
}
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error);
return false;
}
}
Future<bool> isHeadCommit(
String organization,
String project,
String branch,
String commit,
) async {
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
final Client client = Client();
try {
final uri = Uri.parse(
"$kGithubAPI$kGithubHead/$organization/$project/commits/$branch");
final commitQuery = await client.get(
uri,
headers: {'Content-Type': 'application/json'},
);
final response = jsonDecode(commitQuery.body.toString());
Logging.instance.log("isHeadCommit $project $commit $branch $response",
level: LogLevel.Info);
bool isHead;
try {
isHead = response['sha'] == commit;
Logging.instance.log("isHead $isHead", level: LogLevel.Info);
return isHead;
} catch (e, s) {
return false;
}
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error);
return false;
}
}
class DesktopAboutView extends ConsumerWidget {
const DesktopAboutView({Key? key}) : super(key: key);
static const String routeName = "/desktopAboutView";
@override
Widget build(BuildContext context, WidgetRef ref) {
String firoCommit = FIRO_VERSIONS.getPluginVersion();
String epicCashCommit = EPIC_VERSIONS.getPluginVersion();
String moneroCommit = MONERO_VERSIONS.getPluginVersion();
List<Future> futureFiroList = [
doesCommitExist("cypherstack", "flutter_liblelantus", firoCommit),
isHeadCommit("cypherstack", "flutter_liblelantus", "main", firoCommit),
];
Future commitFiroFuture = Future.wait(futureFiroList);
List<Future> futureEpicList = [
doesCommitExist("cypherstack", "flutter_libepiccash", epicCashCommit),
isHeadCommit(
"cypherstack", "flutter_libepiccash", "main", epicCashCommit),
];
Future commitEpicFuture = Future.wait(futureEpicList);
List<Future> futureMoneroList = [
doesCommitExist("cypherstack", "flutter_libmonero", moneroCommit),
isHeadCommit("cypherstack", "flutter_libmonero", "main", moneroCommit),
];
Future commitMoneroFuture = Future.wait(futureMoneroList);
debugPrint("BUILD: $runtimeType");
return DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: true,
leading: Row(
children: [
const SizedBox(
width: 24,
height: 24,
),
Text(
"About",
style: STextStyles.desktopH3(context),
)
],
),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 10, 24, 35),
child: Row(
children: [
Expanded(
child: RoundedWhiteContainer(
width: 929,
height: 411,
child: Padding(
padding: const EdgeInsets.only(left: 10, top: 10),
child: Column(
// mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Stack Wallet",
style: STextStyles.desktopH3(context),
textAlign: TextAlign.start,
),
],
),
const SizedBox(height: 16),
Row(
children: [
RichText(
textAlign: TextAlign.start,
text: TextSpan(
style: STextStyles.label(context),
children: [
TextSpan(
text:
"By using Stack Wallet, you agree to the ",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
),
TextSpan(
text: "Terms of service",
style: STextStyles.richLink(context)
.copyWith(fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(
Uri.parse(
"https://stackwallet.com/terms-of-service.html"),
mode:
LaunchMode.externalApplication,
);
},
),
TextSpan(
text: " and ",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
),
TextSpan(
text: "Privacy policy",
style: STextStyles.richLink(context)
.copyWith(fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(
Uri.parse(
"https://stackwallet.com/privacy-policy.html"),
mode:
LaunchMode.externalApplication,
);
},
),
],
),
),
],
),
const SizedBox(height: 32),
Padding(
padding:
const EdgeInsets.only(right: 10, bottom: 10),
child: Column(
children: [
FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context,
AsyncSnapshot<PackageInfo> snapshot) {
String version = "";
String signature = "";
String build = "";
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
version = snapshot.data!.version;
build = snapshot.data!.buildNumber;
signature = snapshot.data!.buildSignature;
}
return Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Version",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark),
),
const SizedBox(
height: 2,
),
SelectableText(
version,
style:
STextStyles.itemSubtitle(
context),
),
],
),
const SizedBox(
width: 400,
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Build number",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark),
),
const SizedBox(
height: 2,
),
SelectableText(
build,
style:
STextStyles.itemSubtitle(
context),
),
],
),
],
),
const SizedBox(height: 32),
Row(
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Build signature",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark),
),
const SizedBox(
height: 2,
),
SelectableText(
signature,
style:
STextStyles.itemSubtitle(
context),
),
],
),
const SizedBox(
width: 350,
),
FutureBuilder(
future: commitFiroFuture,
builder: (context,
AsyncSnapshot<dynamic>
snapshot) {
bool commitExists = false;
bool isHead = false;
CommitStatus stateOfCommit =
CommitStatus.notLoaded;
if (snapshot.connectionState ==
ConnectionState
.done &&
snapshot.hasData) {
commitExists = snapshot
.data![0] as bool;
isHead = snapshot.data![1]
as bool;
if (commitExists &&
isHead) {
stateOfCommit =
CommitStatus.isHead;
} else if (commitExists) {
stateOfCommit =
CommitStatus
.isOldCommit;
} else {
stateOfCommit =
CommitStatus
.notACommit;
}
}
TextStyle indicationStyle =
STextStyles.itemSubtitle(
context);
switch (stateOfCommit) {
case CommitStatus.isHead:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorGreen);
break;
case CommitStatus
.isOldCommit:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorYellow);
break;
case CommitStatus
.notACommit:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorRed);
break;
default:
break;
}
return Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
"Firo Build Commit",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark),
),
const SizedBox(
height: 2,
),
SelectableText(
firoCommit,
style: indicationStyle,
),
],
);
}),
],
),
const SizedBox(height: 35),
Row(
children: [
FutureBuilder(
future: commitEpicFuture,
builder: (context,
AsyncSnapshot<dynamic>
snapshot) {
bool commitExists = false;
bool isHead = false;
CommitStatus stateOfCommit =
CommitStatus.notLoaded;
if (snapshot.connectionState ==
ConnectionState
.done &&
snapshot.hasData) {
commitExists = snapshot
.data![0] as bool;
isHead = snapshot.data![1]
as bool;
if (commitExists &&
isHead) {
stateOfCommit =
CommitStatus.isHead;
} else if (commitExists) {
stateOfCommit =
CommitStatus
.isOldCommit;
} else {
stateOfCommit =
CommitStatus
.notACommit;
}
}
TextStyle indicationStyle =
STextStyles.itemSubtitle(
context);
switch (stateOfCommit) {
case CommitStatus.isHead:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorGreen);
break;
case CommitStatus
.isOldCommit:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorYellow);
break;
case CommitStatus
.notACommit:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorRed);
break;
default:
break;
}
return Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
"Epic Cash Build Commit",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark),
),
const SizedBox(
height: 2,
),
SelectableText(
epicCashCommit,
style: indicationStyle,
),
],
);
}),
const SizedBox(
width: 105,
),
FutureBuilder(
future: commitMoneroFuture,
builder: (context,
AsyncSnapshot<dynamic>
snapshot) {
bool commitExists = false;
bool isHead = false;
CommitStatus stateOfCommit =
CommitStatus.notLoaded;
if (snapshot.connectionState ==
ConnectionState
.done &&
snapshot.hasData) {
commitExists = snapshot
.data![0] as bool;
isHead = snapshot.data![1]
as bool;
if (commitExists &&
isHead) {
stateOfCommit =
CommitStatus.isHead;
} else if (commitExists) {
stateOfCommit =
CommitStatus
.isOldCommit;
} else {
stateOfCommit =
CommitStatus
.notACommit;
}
}
TextStyle indicationStyle =
STextStyles.itemSubtitle(
context);
switch (stateOfCommit) {
case CommitStatus.isHead:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorGreen);
break;
case CommitStatus
.isOldCommit:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorYellow);
break;
case CommitStatus
.notACommit:
indicationStyle = STextStyles
.itemSubtitle(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.accentColorRed);
break;
default:
break;
}
return Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
"Monero Build Commit",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark),
),
const SizedBox(
height: 2,
),
SelectableText(
moneroCommit,
style: indicationStyle,
),
],
);
}),
],
),
const SizedBox(height: 35),
Row(
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Website",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark),
),
const SizedBox(
height: 2,
),
BlueTextButton(
text:
"https://stackwallet.com",
onTap: () {
launchUrl(
Uri.parse(
"https://stackwallet.com"),
mode: LaunchMode
.externalApplication,
);
},
),
],
)
],
)
],
);
},
)
],
),
)
],
),
),
),
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/support_view.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
class DesktopSupportView extends ConsumerStatefulWidget {
const DesktopSupportView({Key? key}) : super(key: key);
static const String routeName = "/desktopSupportView";
@override
ConsumerState<DesktopSupportView> createState() => _DesktopSupportView();
}
class _DesktopSupportView extends ConsumerState<DesktopSupportView> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: true,
leading: Row(
children: [
const SizedBox(
width: 24,
height: 24,
),
Text(
"Support",
style: STextStyles.desktopH3(context),
)
],
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 10, 0, 0),
child: Row(
children: const [
SizedBox(
width: 576,
child: SupportView(),
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/utilities/desktop_password_service.dart';
final storageCryptoHandlerProvider = Provider<DPS>((ref) => DPS());

View file

@ -85,6 +85,8 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart';
import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
@ -99,6 +101,8 @@ import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_sett
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
@ -996,6 +1000,12 @@ class RouteGenerator {
builder: (_) => const CreatePasswordView(),
settings: RouteSettings(name: settings.name));
case ForgotPasswordDesktopView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const ForgotPasswordDesktopView(),
settings: RouteSettings(name: settings.name));
case DesktopHomeView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
@ -1084,6 +1094,24 @@ class RouteGenerator {
builder: (_) => const AdvancedSettings(),
settings: RouteSettings(name: settings.name));
case DesktopSupportView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const DesktopSupportView(),
settings: RouteSettings(name: settings.name));
case DesktopAboutView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const DesktopAboutView(),
settings: RouteSettings(name: settings.name));
case DesktopAddressBook.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const DesktopAddressBook(),
settings: RouteSettings(name: settings.name));
case WalletKeysDesktopPopup.routeName:
if (args is List<String>) {
return FadePageRoute(

View file

@ -20,10 +20,13 @@ class AddressBookService extends ChangeNotifier {
List<Contact> get contacts {
final keys = List<String>.from(
DB.instance.keys<dynamic>(boxName: DB.boxNameAddressBook));
return keys
final _contacts = keys
.map((id) => Contact.fromJson(Map<String, dynamic>.from(DB.instance
.get<dynamic>(boxName: DB.boxNameAddressBook, key: id) as Map)))
.toList(growable: false);
_contacts
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return _contacts;
}
Future<List<Contact>>? _addressBookEntries;

View file

@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:crypto/crypto.dart';
import 'package:decimal/decimal.dart';
import 'package:devicelocale/devicelocale.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';
@ -174,9 +175,10 @@ class BitcoinWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}
@ -1282,6 +1284,54 @@ class BitcoinWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
TransactionData? cachedTxData;
// hack to add tx to txData before refresh completes
// required based on current app architecture where we don't properly store
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
}
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
@override
bool validateAddress(String address) {
return Address.validateAddress(address, _network);
@ -2660,6 +2710,7 @@ class BitcoinWallet extends CoinServiceAPI {
await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_tx_model', value: txModel);
cachedTxData = txModel;
return txModel;
}

View file

@ -6,11 +6,12 @@ import 'dart:typed_data';
import 'package:bech32/bech32.dart';
import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitbox/bitbox.dart' as Bitbox;
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoindart/bitcoindart.dart';
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:crypto/crypto.dart';
import 'package:decimal/decimal.dart';
import 'package:devicelocale/devicelocale.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';
@ -207,9 +208,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
_getCurrentAddressForChain(0, DerivePathType.bip44);
Future<String>? _currentReceivingAddressP2PKH;
Future<String> get currentReceivingAddressP2SH =>
_currentReceivingAddressP2SH ??=
_getCurrentAddressForChain(0, DerivePathType.bip49);
// Future<String> get currentReceivingAddressP2SH =>
// _currentReceivingAddressP2SH ??=
// _getCurrentAddressForChain(0, DerivePathType.bip49);
Future<String>? _currentReceivingAddressP2SH;
@override
@ -258,7 +259,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
}
Future<void> updateStoredChainHeight({required int newHeight}) async {
DB.instance.put<dynamic>(
await DB.instance.put<dynamic>(
boxName: walletId, key: "storedChainHeight", value: newHeight);
}
@ -266,8 +267,13 @@ class BitcoinCashWallet extends CoinServiceAPI {
Uint8List? decodeBase58;
Segwit? decodeBech32;
try {
if (Bitbox.Address.detectFormat(address) == 0) {
address = Bitbox.Address.toLegacyAddress(address);
if (bitbox.Address.detectFormat(address) ==
bitbox.Address.formatCashAddr) {
if (validateCashAddr(address)) {
address = bitbox.Address.toLegacyAddress(address);
} else {
throw ArgumentError('$address is not currently supported');
}
}
} catch (e, s) {}
try {
@ -292,13 +298,16 @@ class BitcoinCashWallet extends CoinServiceAPI {
} catch (err) {
// Bech32 decode fail
}
if (_network.bech32 != decodeBech32!.hrp) {
if (decodeBech32 != null) {
if (_network.bech32 != decodeBech32.hrp) {
throw ArgumentError('Invalid prefix or Network mismatch');
}
if (decodeBech32.version != 0) {
throw ArgumentError('Invalid address version');
}
}
}
throw ArgumentError('$address has no matching Script');
}
@ -609,7 +618,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
// get address tx counts
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
if (kDebugMode) {
print("Counts $counts");
}
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int count = counts["${_id}_$k"]!;
@ -745,6 +756,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
// notify on new incoming transaction
for (final tx in unconfirmedTxnsToNotifyPending) {
if (tx.txType == "Received") {
unawaited(
NotificationApi.showNotification(
title: "Incoming transaction",
body: walletName,
@ -756,9 +768,11 @@ class BitcoinCashWallet extends CoinServiceAPI {
txid: tx.txid,
confirmations: tx.confirmations,
requiredConfirmations: MINIMUM_CONFIRMATIONS,
),
);
await txTracker.addNotifiedPending(tx.txid);
} else if (tx.txType == "Sent") {
unawaited(
NotificationApi.showNotification(
title: "Sending transaction",
body: walletName,
@ -770,6 +784,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
txid: tx.txid,
confirmations: tx.confirmations,
requiredConfirmations: MINIMUM_CONFIRMATIONS,
),
);
await txTracker.addNotifiedPending(tx.txid);
}
@ -778,6 +793,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
// notify on confirmed
for (final tx in unconfirmedTxnsToNotifyConfirmed) {
if (tx.txType == "Received") {
unawaited(
NotificationApi.showNotification(
title: "Incoming transaction confirmed",
body: walletName,
@ -786,10 +802,12 @@ class BitcoinCashWallet extends CoinServiceAPI {
date: DateTime.now(),
shouldWatchForUpdates: false,
coinName: coin.name,
),
);
await txTracker.addNotifiedConfirmed(tx.txid);
} else if (tx.txType == "Sent") {
unawaited(
NotificationApi.showNotification(
title: "Outgoing transaction confirmed",
body: walletName,
@ -798,6 +816,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
date: DateTime.now(),
shouldWatchForUpdates: false,
coinName: coin.name,
),
);
await txTracker.addNotifiedConfirmed(tx.txid);
}
@ -862,7 +881,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
if (currentHeight != storedHeight) {
if (currentHeight != -1) {
// -1 failed to fetch current height
updateStoredChainHeight(newHeight: currentHeight);
await updateStoredChainHeight(newHeight: currentHeight);
}
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
@ -1143,14 +1162,82 @@ class BitcoinCashWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
TransactionData? cachedTxData;
// hack to add tx to txData before refresh completes
// required based on current app architecture where we don't properly store
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
}
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
bool validateCashAddr(String cashAddr) {
String addr = cashAddr;
if (cashAddr.contains(":")) {
addr = cashAddr.split(":").last;
}
return addr.startsWith("q");
}
@override
bool validateAddress(String address) {
try {
// 0 for bitcoincash: address scheme, 1 for legacy address
final format = Bitbox.Address.detectFormat(address);
final format = bitbox.Address.detectFormat(address);
if (kDebugMode) {
print("format $format");
}
if (_coin == Coin.bitcoincashTestnet) {
return true;
} catch (e, s) {
}
if (format == bitbox.Address.formatCashAddr) {
return validateCashAddr(address);
} else {
return address.startsWith("1");
}
} catch (e) {
return false;
}
}
@ -1226,7 +1313,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
);
if (shouldRefresh) {
refresh();
unawaited(refresh());
}
}
@ -1522,12 +1609,15 @@ class BitcoinCashWallet extends CoinServiceAPI {
break;
}
if (kDebugMode) {
print("Array key is ${jsonEncode(arrayKey)}");
}
final internalChainArray =
DB.instance.get<dynamic>(boxName: walletId, key: arrayKey);
if (derivePathType == DerivePathType.bip44) {
if (Bitbox.Address.detectFormat(internalChainArray.last as String) == 1) {
return Bitbox.Address.toCashAddress(internalChainArray.last as String);
if (bitbox.Address.detectFormat(internalChainArray.last as String) ==
bitbox.Address.formatLegacy) {
return bitbox.Address.toCashAddress(internalChainArray.last as String);
}
}
return internalChainArray.last as String;
@ -1642,7 +1732,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
batches[batchNumber] = {};
}
final scripthash = _convertToScriptHash(allAddresses[i], _network);
if (kDebugMode) {
print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash");
}
batches[batchNumber]!.addAll({
scripthash: [scripthash]
});
@ -1818,20 +1910,28 @@ class BitcoinCashWallet extends CoinServiceAPI {
}) async {
try {
final Map<String, List<dynamic>> args = {};
if (kDebugMode) {
print("Address $addresses");
}
for (final entry in addresses.entries) {
args[entry.key] = [_convertToScriptHash(entry.value, _network)];
}
if (kDebugMode) {
print("Args ${jsonEncode(args)}");
}
final response = await electrumXClient.getBatchHistory(args: args);
if (kDebugMode) {
print("Response ${jsonEncode(response)}");
}
final Map<String, int> result = {};
for (final entry in response.entries) {
result[entry.key] = entry.value.length;
}
if (kDebugMode) {
print("result ${jsonEncode(result)}");
}
return result;
} catch (e, s) {
Logging.instance.log(
@ -1995,8 +2095,10 @@ class BitcoinCashWallet extends CoinServiceAPI {
/// Returns the scripthash or throws an exception on invalid bch address
String _convertToScriptHash(String bchAddress, NetworkType network) {
try {
if (Bitbox.Address.detectFormat(bchAddress) == 0) {
bchAddress = Bitbox.Address.toLegacyAddress(bchAddress);
if (bitbox.Address.detectFormat(bchAddress) ==
bitbox.Address.formatCashAddr &&
validateCashAddr(bchAddress)) {
bchAddress = bitbox.Address.toLegacyAddress(bchAddress);
}
final output = Address.addressToOutputScript(bchAddress, network);
final hash = sha256.convert(output.toList(growable: false)).toString();
@ -2073,8 +2175,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
List<String> allAddressesOld = await _fetchAllOwnAddresses();
List<String> allAddresses = [];
for (String address in allAddressesOld) {
if (Bitbox.Address.detectFormat(address) == 1) {
allAddresses.add(Bitbox.Address.toCashAddress(address));
if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy &&
addressType(address: address) == DerivePathType.bip44) {
allAddresses.add(bitbox.Address.toCashAddress(address));
} else {
allAddresses.add(address);
}
@ -2085,8 +2188,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
as List<dynamic>;
List<dynamic> changeAddressesP2PKH = [];
for (var address in changeAddressesP2PKHOld) {
if (Bitbox.Address.detectFormat(address as String) == 1) {
changeAddressesP2PKH.add(Bitbox.Address.toCashAddress(address));
if (bitbox.Address.detectFormat(address as String) ==
bitbox.Address.formatLegacy) {
changeAddressesP2PKH.add(bitbox.Address.toCashAddress(address));
} else {
changeAddressesP2PKH.add(address);
}
@ -2108,21 +2212,27 @@ class BitcoinCashWallet extends CoinServiceAPI {
unconfirmedCachedTransactions
.removeWhere((key, value) => value.confirmedStatus);
if (kDebugMode) {
print("CACHED_TRANSACTIONS_IS $cachedTransactions");
}
if (cachedTransactions != null) {
for (final tx in allTxHashes.toList(growable: false)) {
final txHeight = tx["height"] as int;
if (txHeight > 0 &&
txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) {
if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) {
print(cachedTransactions.findTransaction(tx["tx_hash"] as String));
if (kDebugMode) {
print(
cachedTransactions.findTransaction(tx["tx_hash"] as String));
print(unconfirmedCachedTransactions[tx["tx_hash"] as String]);
}
final cachedTx =
cachedTransactions.findTransaction(tx["tx_hash"] as String);
if (!(cachedTx != null &&
addressType(address: cachedTx.address) ==
DerivePathType.bip44 &&
Bitbox.Address.detectFormat(cachedTx.address) == 1)) {
bitbox.Address.detectFormat(cachedTx.address) ==
bitbox.Address.formatLegacy)) {
allTxHashes.remove(tx);
}
}
@ -2401,6 +2511,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_tx_model', value: txModel);
cachedTxData = txModel;
return txModel;
}
@ -2782,8 +2893,14 @@ class BitcoinCashWallet extends CoinServiceAPI {
final n = output["n"];
if (n != null && n == utxosToUse[i].vout) {
String address = output["scriptPubKey"]["addresses"][0] as String;
if (Bitbox.Address.detectFormat(address) == 0) {
address = Bitbox.Address.toLegacyAddress(address);
if (bitbox.Address.detectFormat(address) ==
bitbox.Address.formatCashAddr) {
if (validateCashAddr(address)) {
address = bitbox.Address.toLegacyAddress(address);
} else {
throw Exception(
"Unsupported address found during fetchBuildTxData(): $address");
}
}
if (!addressTxid.containsKey(address)) {
addressTxid[address] = <String>[];
@ -2814,9 +2931,6 @@ class BitcoinCashWallet extends CoinServiceAPI {
);
for (int i = 0; i < p2pkhLength; i++) {
String address = addressesP2PKH[i];
if (Bitbox.Address.detectFormat(address) == 0) {
address = Bitbox.Address.toLegacyAddress(address);
}
// receives
final receiveDerivation = receiveDerivations[address];
@ -2950,36 +3064,36 @@ class BitcoinCashWallet extends CoinServiceAPI {
required List<String> recipients,
required List<int> satoshiAmounts,
}) async {
final builder = Bitbox.Bitbox.transactionBuilder();
final builder = bitbox.Bitbox.transactionBuilder();
// retrieve address' utxos from the rest api
List<Bitbox.Utxo> _utxos =
List<bitbox.Utxo> _utxos =
[]; // await Bitbox.Address.utxo(address) as List<Bitbox.Utxo>;
utxosToUse.forEach((element) {
_utxos.add(Bitbox.Utxo(
for (var element in utxosToUse) {
_utxos.add(bitbox.Utxo(
element.txid,
element.vout,
Bitbox.BitcoinCash.fromSatoshi(element.value),
bitbox.BitcoinCash.fromSatoshi(element.value),
element.value,
0,
MINIMUM_CONFIRMATIONS + 1));
});
Logger.print("bch utxos: ${_utxos}");
}
Logger.print("bch utxos: $_utxos");
// placeholder for input signatures
final signatures = <Map>[];
final List<Map<dynamic, dynamic>> signatures = [];
// placeholder for total input balance
int totalBalance = 0;
// int totalBalance = 0;
// iterate through the list of address _utxos and use them as inputs for the
// withdrawal transaction
_utxos.forEach((Bitbox.Utxo utxo) {
for (var utxo in _utxos) {
// add the utxo as an input for the transaction
builder.addInput(utxo.txid, utxo.vout);
final ec = utxoSigningData[utxo.txid]["keyPair"] as ECPair;
final bitboxEC = Bitbox.ECPair.fromWIF(ec.toWIF());
final bitboxEC = bitbox.ECPair.fromWIF(ec.toWIF());
// add a signature to the list to be used later
signatures.add({
@ -2988,15 +3102,15 @@ class BitcoinCashWallet extends CoinServiceAPI {
"original_amount": utxo.satoshis
});
totalBalance += utxo.satoshis;
});
// totalBalance += utxo.satoshis;
}
// calculate the fee based on number of inputs and one expected output
final fee =
Bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length);
// final fee =
// bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length);
// calculate how much balance will be left over to spend after the fee
final sendAmount = totalBalance - fee;
// final sendAmount = totalBalance - fee;
// add the output based on the address provided in the testing data
for (int i = 0; i < recipients.length; i++) {
@ -3006,12 +3120,12 @@ class BitcoinCashWallet extends CoinServiceAPI {
}
// sign all inputs
signatures.forEach((signature) {
for (var signature in signatures) {
builder.sign(
signature["vin"] as int,
signature["key_pair"] as Bitbox.ECPair,
signature["key_pair"] as bitbox.ECPair,
signature["original_amount"] as int);
});
}
// build the transaction
final tx = builder.build();
@ -3038,7 +3152,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
);
// clear cache
_cachedElectrumXClient.clearSharedTransactionCache(coin: coin);
await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin);
// back up data
await _rescanBackup();
@ -3326,9 +3440,10 @@ class BitcoinCashWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}

View file

@ -9,8 +9,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/prefs.dart';
@ -277,4 +277,7 @@ abstract class CoinServiceAPI {
Future<int> estimateFeeFor(int satoshiAmount, int feeRate);
Future<bool> generateNewAddress();
// used for electrumx coins
Future<void> updateSentCachedTxData(Map<String, dynamic> txData);
}

View file

@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:crypto/crypto.dart';
import 'package:decimal/decimal.dart';
import 'package:devicelocale/devicelocale.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';
@ -1051,6 +1052,54 @@ class DogecoinWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
TransactionData? cachedTxData;
// hack to add tx to txData before refresh completes
// required based on current app architecture where we don't properly store
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
}
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
@override
bool validateAddress(String address) {
return Address.validateAddress(address, _network);
@ -2273,6 +2322,7 @@ class DogecoinWallet extends CoinServiceAPI {
await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_tx_model', value: txModel);
cachedTxData = txModel;
return txModel;
}
@ -2983,9 +3033,10 @@ class DogecoinWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}

View file

@ -558,9 +558,10 @@ class EpicCashWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}
@ -832,10 +833,16 @@ class EpicCashWallet extends CoinServiceAPI {
final txLogEntryFirst = txLogEntry[0];
Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst");
final wallet = await Hive.openBox<dynamic>(_walletId);
final slateToAddresses = (await wallet.get("slate_to_address")) as Map?;
slateToAddresses?[txLogEntryFirst['tx_slate_id']] = txData['addresss'];
final slateToAddresses =
(await wallet.get("slate_to_address")) as Map? ?? {};
final slateId = txLogEntryFirst['tx_slate_id'] as String;
slateToAddresses[slateId] = txData['addresss'];
await wallet.put('slate_to_address', slateToAddresses);
return txLogEntryFirst['tx_slate_id'] as String;
final slatesToCommits = await getSlatesToCommits();
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
Logging.instance.log("sent commitId: $commitId", level: LogLevel.Info);
return commitId!;
// return txLogEntryFirst['tx_slate_id'] as String;
}
} catch (e, s) {
Logging.instance.log("Error sending $e - $s", level: LogLevel.Error);
@ -2154,8 +2161,9 @@ class EpicCashWallet extends CoinServiceAPI {
as String? ??
"";
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
Logging.instance
.log("commitId: $commitId $slateId", level: LogLevel.Info);
Logging.instance.log(
"commitId: $commitId, slateId: $slateId, id: ${tx["id"]}",
level: LogLevel.Info);
bool isCancelled = tx["tx_type"] == "TxSentCancelled" ||
tx["tx_type"] == "TxReceivedCancelled";
@ -2258,6 +2266,14 @@ class EpicCashWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
// not used in epic
TransactionData? cachedTxData;
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// not used in epic
}
@override
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();

View file

@ -821,9 +821,10 @@ class FiroWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}
@ -907,6 +908,52 @@ class FiroWallet extends CoinServiceAPI {
Future<models.TransactionData> get _txnData =>
_transactionData ??= _fetchTransactionData();
models.TransactionData? cachedTxData;
// hack to add tx to txData before refresh completes
// required based on current app architecture where we don't properly store
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final currentPrice = await firoPrice;
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
}
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
/// Holds wallet lelantus transaction data
Future<models.TransactionData>? _lelantusTransactionData;
Future<models.TransactionData> get lelantusTransactionData =>
@ -1109,6 +1156,9 @@ class FiroWallet extends CoinServiceAPI {
final txHash = await _electrumXClient.broadcastTransaction(
rawTx: txData["hex"] as String);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
txData["txid"] = txHash;
// dirty ui update hack
await updateSentCachedTxData(txData as Map<String, dynamic>);
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
@ -3464,6 +3514,7 @@ class FiroWallet extends CoinServiceAPI {
await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_tx_model', value: txModel);
cachedTxData = txModel;
return txModel;
}

View file

@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:crypto/crypto.dart';
import 'package:decimal/decimal.dart';
import 'package:devicelocale/devicelocale.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';
@ -174,9 +175,10 @@ class LitecoinWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}
@ -1284,6 +1286,54 @@ class LitecoinWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
TransactionData? cachedTxData;
// hack to add tx to txData before refresh completes
// required based on current app architecture where we don't properly store
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
}
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
@override
bool validateAddress(String address) {
return Address.validateAddress(address, _network, _network.bech32!);
@ -2672,6 +2722,7 @@ class LitecoinWallet extends CoinServiceAPI {
await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_tx_model', value: txModel);
cachedTxData = txModel;
return txModel;
}

View file

@ -108,6 +108,9 @@ class Manager with ChangeNotifier {
try {
final txid = await _currentWallet.confirmSend(txData: txData);
txData["txid"] = txid;
await _currentWallet.updateSentCachedTxData(txData);
notifyListeners();
return txid;
} catch (e) {

View file

@ -699,7 +699,7 @@ class MoneroWallet extends CoinServiceAPI {
name: name,
type: WalletType.monero,
isRecovery: false,
restoreHeight: credentials.height ?? 0,
restoreHeight: bufferedCreateHeight,
date: DateTime.now(),
path: path,
dirPath: dirPath,
@ -1190,6 +1190,14 @@ class MoneroWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
// not used in monero
TransactionData? cachedTxData;
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// not used in monero
}
Future<TransactionData> _fetchTransactionData() async {
final transactions = walletBase?.transactionHistory!.transactions;
@ -1345,10 +1353,8 @@ class MoneroWallet extends CoinServiceAPI {
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
@override
// TODO: implement validateAddress
bool validateAddress(String address) {
bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) ||
RegExp("[a-zA-Z0-9]{106}").hasMatch(address);
bool valid = walletBase!.validateAddress(address);
return valid;
}
@ -1376,9 +1382,10 @@ class MoneroWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}

View file

@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:crypto/crypto.dart';
import 'package:decimal/decimal.dart';
import 'package:devicelocale/devicelocale.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';
@ -170,9 +171,10 @@ class NamecoinWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}
@ -1275,6 +1277,54 @@ class NamecoinWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
TransactionData? cachedTxData;
// hack to add tx to txData before refresh completes
// required based on current app architecture where we don't properly store
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
}
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
@override
bool validateAddress(String address) {
return Address.validateAddress(address, _network, namecoin.bech32!);
@ -2672,6 +2722,7 @@ class NamecoinWallet extends CoinServiceAPI {
await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_tx_model', value: txModel);
cachedTxData = txModel;
return txModel;
}

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pending_transaction.dart';
@ -647,7 +648,7 @@ class WowneroWallet extends CoinServiceAPI {
}
//TODO: take in the default language when creating wallet.
Future<void> _generateNewWallet() async {
Future<void> _generateNewWallet({int seedWordsLength = 14}) async {
Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
// TODO: ping wownero server and make sure the genesis hash matches
@ -685,9 +686,7 @@ class WowneroWallet extends CoinServiceAPI {
await pathForWalletDir(name: name, type: WalletType.wownero);
final path = await pathForWallet(name: name, type: WalletType.wownero);
credentials = wownero.createWowneroNewWalletCredentials(
name: name,
language: "English",
);
name: name, language: "English", seedWordsLength: seedWordsLength);
walletInfo = WalletInfo.external(
id: WalletBase.idFor(name, WalletType.wownero),
@ -712,9 +711,12 @@ class WowneroWallet extends CoinServiceAPI {
// To restore from a seed
final wallet = await _walletCreationService?.create(credentials);
// subtract a couple days to ensure we have a buffer for SWB
final bufferedCreateHeight =
getSeedHeightSync(wallet?.seed.trim() as String);
final bufferedCreateHeight = (seedWordsLength == 14)
? getSeedHeightSync(wallet?.seed.trim() as String)
: wownero.getHeightByDate(
date: DateTime.now().subtract(const Duration(
days:
2))); // subtract a couple days to ensure we have a buffer for SWB
await DB.instance.put<dynamic>(
boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight);
@ -722,6 +724,7 @@ class WowneroWallet extends CoinServiceAPI {
await _secureStore.write(
key: '${_walletId}_mnemonic', value: wallet?.seed.trim());
walletInfo.address = wallet?.walletAddresses.address;
await DB.instance
.add<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo);
@ -778,7 +781,7 @@ class WowneroWallet extends CoinServiceAPI {
@override
// TODO: implement initializeWallet
Future<bool> initializeNew() async {
Future<bool> initializeNew({int seedWordsLength = 14}) async {
await _prefs.init();
// TODO: ping actual wownero network
// try {
@ -796,7 +799,7 @@ class WowneroWallet extends CoinServiceAPI {
prefs = await SharedPreferences.getInstance();
keysStorage = KeyService(storage!);
await _generateNewWallet();
await _generateNewWallet(seedWordsLength: seedWordsLength);
// var password;
// try {
// password =
@ -942,6 +945,11 @@ class WowneroWallet extends CoinServiceAPI {
required int maxNumberOfIndexesToCheck,
required int height,
}) async {
final int seedLength = mnemonic.trim().split(" ").length;
if (!(seedLength == 14 || seedLength == 25)) {
throw Exception("Invalid wownero mnemonic length found: $seedLength");
}
await _prefs.init();
longMutex = true;
final start = DateTime.now();
@ -969,7 +977,18 @@ class WowneroWallet extends CoinServiceAPI {
await _secureStore.write(
key: '${_walletId}_mnemonic', value: mnemonic.trim());
// extract seed height from 14 word seed
if (seedLength == 14) {
height = getSeedHeightSync(mnemonic.trim());
} else {
// 25 word seed. TODO validate
if (height == 0) {
height = wownero.getHeightByDate(
date: DateTime.now().subtract(const Duration(
days:
2))); // subtract a couple days to ensure we have a buffer for SWB\
}
}
await DB.instance
.put<dynamic>(boxName: walletId, key: "restoreHeight", value: height);
@ -1195,6 +1214,14 @@ class WowneroWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
// not used in wownero
TransactionData? cachedTxData;
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// not used in wownero
}
Future<TransactionData> _fetchTransactionData() async {
final transactions = walletBase?.transactionHistory!.transactions;
@ -1351,10 +1378,8 @@ class WowneroWallet extends CoinServiceAPI {
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
@override
// TODO: implement validateAddress
bool validateAddress(String address) {
bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) ||
RegExp("[a-zA-Z0-9]{106}").hasMatch(address);
bool valid = walletBase!.validateAddress(address);
return valid;
}
@ -1382,9 +1407,10 @@ class WowneroWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}

View file

@ -59,6 +59,8 @@ class _SVG {
String txExchangeFailed(BuildContext context) =>
"assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg";
String get framedGear => "assets/svg/framed-gear.svg";
String get framedAddressBook => "assets/svg/framed-address-book.svg";
String get themeLight => "assets/svg/light/light-mode.svg";
String get themeDark => "assets/svg/dark/dark-theme.svg";
String get circleNode => "assets/svg/node-circle.svg";
@ -67,6 +69,7 @@ class _SVG {
String get circleLanguage => "assets/svg/language-circle.svg";
String get circleDollarSign => "assets/svg/dollar-sign-circle.svg";
String get circleLock => "assets/svg/lock-circle.svg";
String get enableButton => "assets/svg/enabled-button.svg";
String get disableButton => "assets/svg/Button.svg";
String get polygon => "assets/svg/Polygon.svg";
String get personaIncognito => "assets/svg/persona-incognito-1.svg";

View file

@ -35,10 +35,6 @@ abstract class Constants {
static const int pinLength = 4;
// enable testnet
// TODO: currently unused
static const bool allowTestnets = true;
// Enable Logger.print statements
static const bool disableLogger = false;
@ -66,7 +62,7 @@ abstract class Constants {
values.addAll([25]);
break;
case Coin.wownero:
values.addAll([14]);
values.addAll([14, 25]);
break;
}
return values;

View file

@ -0,0 +1,89 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stack_wallet_backup/secure_storage.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
const String _kKeyBlobKey = "swbKeyBlobKeyStringID";
String _getMessageFromException(Object exception) {
if (exception is IncorrectPassphrase) {
return exception.errMsg();
}
if (exception is BadDecryption) {
return exception.errMsg();
}
if (exception is InvalidLength) {
return exception.errMsg();
}
if (exception is EncodingError) {
return exception.errMsg();
}
return exception.toString();
}
class DPS {
StorageCryptoHandler? _handler;
final SecureStorageWrapper secureStorageWrapper;
StorageCryptoHandler get handler {
if (_handler == null) {
throw Exception(
"DPS: attempted to access handler without proper authentication");
}
return _handler!;
}
DPS({
this.secureStorageWrapper = const SecureStorageWrapper(
FlutterSecureStorage(),
),
});
Future<void> initFromNew(String passphrase) async {
if (_handler != null) {
throw Exception("DPS: attempted to re initialize with new passphrase");
}
try {
_handler = await StorageCryptoHandler.fromNewPassphrase(passphrase);
await secureStorageWrapper.write(
key: _kKeyBlobKey,
value: await _handler!.getKeyBlob(),
);
} catch (e, s) {
Logging.instance.log(
"${_getMessageFromException(e)}\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
Future<void> initFromExisting(String passphrase) async {
if (_handler != null) {
throw Exception(
"DPS: attempted to re initialize with existing passphrase");
}
final keyBlob = await secureStorageWrapper.read(key: _kKeyBlobKey);
if (keyBlob == null) {
throw Exception(
"DPS: failed to find keyBlob while attempting to initialize with existing passphrase");
}
try {
_handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob);
} catch (e, s) {
Logging.instance.log(
"${_getMessageFromException(e)}\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
Future<bool> hasPassword() async {
return (await secureStorageWrapper.read(key: _kKeyBlobKey)) != null;
}
}

View file

@ -132,7 +132,7 @@ extension CoinExt on Coin {
case Coin.litecoinTestNet:
return "litecoin";
case Coin.bitcoincashTestnet:
return "bitcoincash";
return "bchtest";
case Coin.firoTestNet:
return "firo";
case Coin.dogecoinTestNet:

View file

@ -1,26 +1,121 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
Future<bool> testMoneroNodeConnection(Uri uri) async {
class MoneroNodeConnectionResponse {
final X509Certificate? cert;
final String? url;
final int? port;
final bool success;
MoneroNodeConnectionResponse(this.cert, this.url, this.port, this.success);
}
Future<MoneroNodeConnectionResponse> testMoneroNodeConnection(
Uri uri,
bool allowBadX509Certificate,
) async {
final client = HttpClient();
MoneroNodeConnectionResponse? badCertResponse;
try {
final client = http.Client();
final response = await client
.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({"jsonrpc": "2.0", "id": "0", "method": "get_info"}),
)
.timeout(const Duration(milliseconds: 1200),
onTimeout: () async => http.Response('Error', 408));
final result = jsonDecode(response.body);
// TODO: json decoded without error so assume connection exists?
// or we can check for certain values in the response to decide
client.badCertificateCallback = (cert, url, port) {
if (allowBadX509Certificate) {
return true;
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
}
if (badCertResponse == null) {
badCertResponse = MoneroNodeConnectionResponse(cert, url, port, false);
} else {
return false;
}
return false;
};
final request = await client.postUrl(uri);
final body = utf8.encode(
jsonEncode({
"jsonrpc": "2.0",
"id": "0",
"method": "get_info",
}),
);
request.headers.add(
'Content-Length',
body.length.toString(),
preserveHeaderCase: true,
);
request.headers.set(
'Content-Type',
'application/json',
preserveHeaderCase: true,
);
request.add(body);
final response = await request.close();
final result = await response.transform(utf8.decoder).join();
// TODO: json decoded without error so assume connection exists?
// or we can check for certain values in the response to decide
return MoneroNodeConnectionResponse(null, null, null, true);
} catch (e, s) {
if (badCertResponse != null) {
return badCertResponse!;
} else {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
return MoneroNodeConnectionResponse(null, null, null, false);
}
} finally {
client.close(force: true);
}
}
Future<bool> showBadX509CertificateDialog(
X509Certificate cert,
String url,
int port,
BuildContext context,
) async {
final chars = Format.uint8listToString(cert.sha1)
.toUpperCase()
.characters
.toList(growable: false);
String sha1 = chars.sublist(0, 2).join();
for (int i = 2; i < chars.length; i += 2) {
sha1 += ":${chars.sublist(i, i + 2).join()}";
}
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) {
return StackDialog(
title: "Untrusted X509Certificate",
message: "SHA1:\n$sha1",
leftButton: SecondaryButton(
label: "Cancel",
onPressed: () {
Navigator.of(context).pop(false);
},
),
rightButton: PrimaryButton(
label: "Trust",
onPressed: () {
Navigator.of(context).pop(true);
},
),
);
},
);
return result ?? false;
}

View file

@ -508,6 +508,25 @@ class STextStyles {
// Desktop
static TextStyle desktopH1(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 40,
height: 40 / 40,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 40,
height: 40 / 40,
);
}
}
static TextStyle desktopH2(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:

View file

@ -10,11 +10,13 @@ class BlueTextButton extends ConsumerStatefulWidget {
required this.text,
this.onTap,
this.enabled = true,
this.textSize,
}) : super(key: key);
final String text;
final VoidCallback? onTap;
final bool enabled;
final double? textSize;
@override
ConsumerState<BlueTextButton> createState() => _BlueTextButtonState();
@ -67,7 +69,14 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
textAlign: TextAlign.center,
text: TextSpan(
text: widget.text,
style: STextStyles.link2(context).copyWith(color: color),
style: widget.textSize == null
? STextStyles.link2(context).copyWith(
color: color,
)
: STextStyles.link2(context).copyWith(
color: color,
fontSize: widget.textSize,
),
recognizer: widget.enabled
? (TapGestureRecognizer()
..onTap = () {

View file

@ -110,7 +110,29 @@ class _NodeCardState extends ConsumerState<NodeCard> {
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
testPassed = await testMoneroNodeConnection(Uri.parse(uriString));
final response = await testMoneroNodeConnection(
Uri.parse(uriString),
false,
);
if (response.cert != null) {
if (mounted) {
final shouldAllowBadCert = await showBadX509CertificateDialog(
response.cert!,
response.url!,
response.port!,
context,
);
if (shouldAllowBadCert) {
final response = await testMoneroNodeConnection(
Uri.parse(uriString), true);
testPassed = response.success;
}
}
} else {
testPassed = response.success;
}
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);

View file

@ -93,7 +93,29 @@ class NodeOptionsSheet extends ConsumerWidget {
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
testPassed = await testMoneroNodeConnection(Uri.parse(uriString));
final response = await testMoneroNodeConnection(
Uri.parse(uriString),
false,
);
if (response.cert != null) {
// if (mounted) {
final shouldAllowBadCert = await showBadX509CertificateDialog(
response.cert!,
response.url!,
response.port!,
context,
);
if (shouldAllowBadCert) {
final response =
await testMoneroNodeConnection(Uri.parse(uriString), true);
testPassed = response.success;
}
// }
} else {
testPassed = response.success;
}
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
class StackDialogBase extends StatelessWidget {
const StackDialogBase({
@ -17,7 +18,8 @@ class StackDialogBase extends StatelessWidget {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment:
!Util.isDesktop ? MainAxisAlignment.end : MainAxisAlignment.center,
children: [
Material(
borderRadius: BorderRadius.circular(
@ -179,9 +181,15 @@ class StackOkDialog extends StatelessWidget {
),
Expanded(
child: TextButton(
onPressed: () {
onPressed: !Util.isDesktop
? () {
Navigator.of(context).pop();
onOkPressed?.call("OK");
}
: () {
int count = 0;
Navigator.of(context).popUntil((_) => count++ >= 2);
// onOkPressed?.call("OK");
},
style: Theme.of(context)
.extension<StackColors>()!

View file

@ -1378,8 +1378,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: b7b184ec36466f2a24104a7056de88881cb0c1e9
resolved-ref: b7b184ec36466f2a24104a7056de88881cb0c1e9
ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d"
resolved-ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d"
url: "https://github.com/cypherstack/stack_wallet_backup.git"
source: git
version: "0.0.1"

View file

@ -11,7 +11,7 @@ description: Stack 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: 1.5.14+86
version: 1.5.17+89
environment:
sdk: ">=2.17.0 <3.0.0"
@ -54,7 +54,7 @@ dependencies:
stack_wallet_backup:
git:
url: https://github.com/cypherstack/stack_wallet_backup.git
ref: b7b184ec36466f2a24104a7056de88881cb0c1e9
ref: 011dc9ce3d29f5fdeeaf711d58b5122f055c146d
# Utility plugins
# provider: ^6.0.1
@ -298,6 +298,7 @@ flutter:
- assets/svg/persona-easy-1.svg
- assets/svg/persona-incognito-1.svg
- assets/svg/Button.svg
- assets/svg/enabled-button.svg
- assets/svg/lock-circle.svg
- assets/svg/dollar-sign-circle.svg
- assets/svg/language-circle.svg
@ -338,6 +339,8 @@ flutter:
- assets/svg/message-question-1.svg
- assets/svg/drd-icon.svg
- assets/svg/box-auto.svg
- assets/svg/framed-address-book.svg
- assets/svg/framed-gear.svg
# exchange icons
- assets/svg/exchange_icons/change_now_logo_1.svg
- assets/svg/exchange_icons/simpleswap-icon.svg

View file

@ -1,25 +1,37 @@
#!/bin/bash
LINUX_DIRECTORY=$(pwd)
mkdir build
mkdir -p build
# Build JsonCPP
cd build
git clone https://github.com/open-source-parsers/jsoncpp.git
cd jsoncpp
cd build || exit
if ! [ -x "$(command -v git)" ]; then
echo 'Error: git is not installed.' >&2
exit 1
fi
git -C jsoncpp pull || git clone https://github.com/open-source-parsers/jsoncpp.git jsoncpp
cd jsoncpp || exit
git checkout 1.7.4
mkdir build
cd build
mkdir -p build
cd build || exit
cmake -DCMAKE_BUILD_TYPE=release -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=ON -DARCHIVE_INSTALL_DIR=. -G "Unix Makefiles" ..
make -j$(nproc)
make -j"$(nproc)"
cd $LINUX_DIRECTORY
cd "$LINUX_DIRECTORY" || exit
# Build libSecret
# sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl
# sudo apt install python3-pip
#pip3 install --user meson --upgrade
# pip3 install --user gi-docgen
cd build
git clone https://gitlab.gnome.org/GNOME/libsecret.git
cd libsecret
cd build || exit
git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret
cd libsecret || exit
if ! [ -x "$(command -v meson)" ]; then
echo 'Error: meson is not installed.' >&2
exit 1
fi
meson _build
if ! [ -x "$(command -v ninja)" ]; then
echo 'Error: ninja is not installed.' >&2
exit 1
fi
ninja -C _build

View file

@ -94,19 +94,19 @@ void main() {
test("get contacts", () {
final service = AddressBookService();
expect(service.contacts.toString(),
[contactA, contactB, contactC].toString());
[contactC, contactB, contactA].toString());
});
test("get addressBookEntries", () async {
final service = AddressBookService();
expect((await service.addressBookEntries).toString(),
[contactA, contactB, contactC].toString());
[contactC, contactB, contactA].toString());
});
test("search contacts", () async {
final service = AddressBookService();
final results = await service.search("j");
expect(results.toString(), [contactA, contactB].toString());
expect(results.toString(), [contactB, contactA].toString());
final results2 = await service.search("ja");
expect(results2.toString(), [contactB].toString());
@ -118,7 +118,7 @@ void main() {
expect(results4.toString(), <Contact>[].toString());
final results5 = await service.search("");
expect(results5.toString(), [contactA, contactB, contactC].toString());
expect(results5.toString(), [contactC, contactB, contactA].toString());
final results6 = await service.search("epic address");
expect(results6.toString(), [contactC].toString());
@ -140,7 +140,7 @@ void main() {
expect(result, false);
expect(service.contacts.length, 3);
expect(service.contacts.toString(),
[contactA, contactB, contactC].toString());
[contactC, contactB, contactA].toString());
});
test("edit contact", () async {
@ -149,14 +149,14 @@ void main() {
expect(await service.editContact(editedContact), true);
expect(service.contacts.length, 3);
expect(service.contacts.toString(),
[contactA, editedContact, contactC].toString());
[contactC, contactA, editedContact].toString());
});
test("remove existing contact", () async {
final service = AddressBookService();
await service.removeContact(contactB.id);
expect(service.contacts.length, 2);
expect(service.contacts.toString(), [contactA, contactC].toString());
expect(service.contacts.toString(), [contactC, contactA].toString());
});
test("remove non existing contact", () async {
@ -164,7 +164,7 @@ void main() {
await service.removeContact("some id");
expect(service.contacts.length, 3);
expect(service.contacts.toString(),
[contactA, contactB, contactC].toString());
[contactC, contactB, contactA].toString());
});
tearDown(() async {

View file

@ -1,4 +1,3 @@
import 'package:bitcoindart/bitcoindart.dart';
import 'package:decimal/decimal.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
@ -61,7 +60,7 @@ void main() {
});
});
group("validate mainnet bitcoincash addresses", () {
group("mainnet bitcoincash addressType", () {
MockElectrumX? client;
MockCachedElectrumX? cachedClient;
MockPriceAPI? priceAPI;
@ -137,10 +136,172 @@ void main() {
verifyNoMoreInteractions(priceAPI);
});
test("P2PKH cashaddr with prefix", () {
expect(
mainnetWallet?.addressType(
address:
"bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"),
DerivePathType.bip44);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("P2PKH cashaddr without prefix", () {
expect(
mainnetWallet?.addressType(
address: "qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"),
DerivePathType.bip44);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("Multisig cashaddr with prefix", () {
expect(
() => mainnetWallet?.addressType(
address:
"bitcoincash:pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"),
throwsArgumentError);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("Multisig cashaddr without prefix", () {
expect(
() => mainnetWallet?.addressType(
address: "pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"),
throwsArgumentError);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("Multisig/P2SH address", () {
expect(
mainnetWallet?.addressType(
address: "3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"),
DerivePathType.bip49);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
});
group("validate mainnet bitcoincash addresses", () {
MockElectrumX? client;
MockCachedElectrumX? cachedClient;
MockPriceAPI? priceAPI;
FakeSecureStorage? secureStore;
MockTransactionNotificationTracker? tracker;
BitcoinCashWallet? mainnetWallet;
setUp(() {
client = MockElectrumX();
cachedClient = MockCachedElectrumX();
priceAPI = MockPriceAPI();
secureStore = FakeSecureStorage();
tracker = MockTransactionNotificationTracker();
mainnetWallet = BitcoinCashWallet(
walletId: "validateAddressMainNet",
walletName: "validateAddressMainNet",
coin: Coin.bitcoincash,
client: client!,
cachedClient: cachedClient!,
tracker: tracker!,
priceAPI: priceAPI,
secureStore: secureStore,
);
});
test("valid mainnet legacy/p2pkh address type", () {
expect(
mainnetWallet?.validateAddress("1DP3PUePwMa5CoZwzjznVKhzdLsZftjcAT"),
true);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("valid mainnet legacy/p2pkh cashaddr with prefix address type", () {
expect(
mainnetWallet?.validateAddress(
"bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"),
true);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("valid mainnet legacy/p2pkh cashaddr without prefix address type", () {
expect(
mainnetWallet
?.validateAddress("qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"),
true);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("invalid legacy/p2pkh address type", () {
expect(
mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"),
false);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test(
"invalid cashaddr (is valid multisig but bitbox is broken for multisig)",
() {
expect(
mainnetWallet
?.validateAddress("pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"),
false);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("multisig address should fail for bitbox", () {
expect(
mainnetWallet?.validateAddress("3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"),
false);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(priceAPI);
});
test("invalid mainnet bitcoincash legacy/p2pkh address", () {
expect(
mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"),
true);
false);
expect(secureStore?.interactions, 0);
verifyNoMoreInteractions(client);
verifyNoMoreInteractions(cachedClient);

View file

@ -182,4 +182,10 @@ class FakeCoinServiceAPI extends CoinServiceAPI {
// TODO: implement generateNewAddress
throw UnimplementedError();
}
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) {
// TODO: implement updateSentCachedTxData
throw UnimplementedError();
}
}

View file

@ -0,0 +1,234 @@
import 'dart:core';
import 'dart:core' as core;
import 'dart:io';
import 'dart:math';
import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:flutter_libmonero/core/key_service.dart';
import 'package:flutter_libmonero/core/wallet_creation_service.dart';
import 'package:flutter_libmonero/monero/monero.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
import 'package:hive_test/hive_test.dart';
import 'package:mockito/annotations.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stackwallet/services/wallets.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
// TODO trim down to the minimum imports above
import 'monero_wallet_test_data.dart';
//FlutterSecureStorage? storage;
FakeSecureStorage? storage;
WalletService? walletService;
SharedPreferences? prefs;
KeyService? keysStorage;
MoneroWalletBase? walletBase;
late WalletCreationService _walletCreationService;
dynamic _walletInfoSource;
Wallets? walletsService;
String path = '';
String name = 'namee${Random().nextInt(10000000)}';
int nettype = 0;
WalletType type = WalletType.monero;
@GenerateMocks([])
void main() async {
storage = FakeSecureStorage();
prefs = await SharedPreferences.getInstance();
keysStorage = KeyService(storage!);
WalletInfo walletInfo = WalletInfo.external(
id: '',
name: '',
type: type,
isRecovery: false,
restoreHeight: 0,
date: DateTime.now(),
path: '',
address: '',
dirPath: '');
late WalletCredentials credentials;
monero.onStartup();
bool hiveAdaptersRegistered = false;
group("Mainnet tests", () {
setUp(() async {
await setUpTestHive();
if (!hiveAdaptersRegistered) {
hiveAdaptersRegistered = true;
Hive.registerAdapter(NodeAdapter());
Hive.registerAdapter(WalletInfoAdapter());
Hive.registerAdapter(WalletTypeAdapter());
Hive.registerAdapter(UnspentCoinsInfoAdapter());
final wallets = await Hive.openBox('wallets');
await wallets.put('currentWalletName', name);
_walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName);
walletService = monero
.createMoneroWalletService(_walletInfoSource as Box<WalletInfo>);
}
try {
// if (name?.isEmpty ?? true) {
// name = await generateName();
// }
final dirPath = await pathForWalletDir(name: name, type: type);
path = await pathForWallet(name: name, type: type);
credentials =
// // creating a new wallet
// monero.createMoneroNewWalletCredentials(
// name: name, language: "English");
// restoring a previous wallet
monero.createMoneroRestoreWalletFromSeedCredentials(
name: name, height: 2580000, mnemonic: testMnemonic);
walletInfo = WalletInfo.external(
id: WalletBase.idFor(name, type),
name: name,
type: type,
isRecovery: false,
restoreHeight: credentials.height ?? 0,
date: DateTime.now(),
path: path,
address: "",
dirPath: dirPath);
credentials.walletInfo = walletInfo;
_walletCreationService = WalletCreationService(
secureStorage: storage,
sharedPreferences: prefs,
walletService: walletService,
keyService: keysStorage,
);
_walletCreationService.changeWalletType();
} catch (e, s) {
print(e);
print(s);
}
});
test("Test mainnet address generation from seed", () async {
final wallet = await
// _walletCreationService.create(credentials);
_walletCreationService.restoreFromSeed(credentials);
walletInfo.address = wallet.walletAddresses.address;
//print(walletInfo.address);
await _walletInfoSource.add(walletInfo);
walletBase?.close();
walletBase = wallet as MoneroWalletBase;
//print("${walletBase?.seed}");
expect(
await walletBase!.validateAddress(walletInfo.address ?? ''), true);
// print(walletBase);
// loggerPrint(walletBase.toString());
// loggerPrint("name: ${walletBase!.name} seed: ${walletBase!.seed} id: "
// "${walletBase!.id} walletinfo: ${toStringForinfo(walletBase!.walletInfo)} type: ${walletBase!.type} balance: "
// "${walletBase!.balance.entries.first.value.available} currency: ${walletBase!.currency}");
expect(walletInfo.address, mainnetTestData[0][0]);
expect(
await walletBase!.getTransactionAddress(0, 0), mainnetTestData[0][0]);
expect(
await walletBase!.getTransactionAddress(0, 1), mainnetTestData[0][1]);
expect(
await walletBase!.getTransactionAddress(0, 2), mainnetTestData[0][2]);
expect(
await walletBase!.getTransactionAddress(1, 0), mainnetTestData[1][0]);
expect(
await walletBase!.getTransactionAddress(1, 1), mainnetTestData[1][1]);
expect(
await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]);
expect(
await walletBase!.validateAddress(''), false);
expect(
await walletBase!.validateAddress('4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), true);
expect(
await walletBase!.validateAddress('4asdfkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gpjkl'), false);
expect(
await walletBase!.validateAddress('8AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), false);
expect(
await walletBase!.validateAddress('84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), true);
expect(
await walletBase!.validateAddress('8asdfuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenjkl'), false);
expect(
await walletBase!.validateAddress('44kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), false);
});
});
/*
// Not needed; only folder created, wallet files not saved yet. TODO test saving and deleting wallet files and make sure to clean up leftover folder afterwards
group("Mainnet wallet deletion test", () {
test("Test mainnet wallet existence", () {
expect(monero_wallet_manager.isWalletExistSync(path: path), true);
});
test("Test mainnet wallet deletion", () {
// Remove wallet from wallet service
walletService?.remove(name);
walletsService?.removeWallet(walletId: name);
expect(monero_wallet_manager.isWalletExistSync(path: path), false);
});
});
group("Mainnet node tests", () {
test("Test mainnet node connection", () async {
await walletBase?.connectToNode(
node: Node(
uri: "monero-stagenet.stackwallet.com:38081",
type: WalletType.moneroStageNet));
await walletBase!.rescan(
height:
credentials.height); // Probably shouldn't be rescanning from 0...
await walletBase!.getNodeHeight();
int height = await walletBase!.getNodeHeight();
print('height: $height');
bool connected = await walletBase!.isConnected();
print('connected: $connected');
//expect...
});
});
*/
// TODO test deletion of wallets ... and delete them
}
Future<String> pathForWalletDir(
{required String name, required WalletType type}) async {
Directory root = (await getApplicationDocumentsDirectory());
if (Platform.isIOS) {
root = (await getLibraryDirectory());
}
final prefix = walletTypeToString(type).toLowerCase();
final walletsDir = Directory('${root.path}/wallets');
final walletDire = Directory('${walletsDir.path}/$prefix/$name');
if (!walletDire.existsSync()) {
walletDire.createSync(recursive: true);
}
return walletDire.path;
}
Future<String> pathForWallet(
{required String name, required WalletType type}) async =>
await pathForWalletDir(name: name, type: type)
.then((path) => path + '/$name');

View file

@ -0,0 +1,14 @@
String testMnemonic =
'agreed aquarium wallets uptight karate wonders afoot guys itself nucleus reduce lamb fully fewest bimonthly dazed skulls magically mocked fugitive imbalance saga calamity dialect itself';
var mainnetTestData = [
[
'4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn',
'82WsoLmbZt3BPwJMF5PfT8GitThJzUq3FFoSQyr4fKfJdxZebgY3mHPcnAqTBA3FFwZRGxC4ZDwkfE1VVULPa55x3xXgCbj',
'84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'
],
[
'86SF44CsTBYU3vk1X7nGBbQnrUSknGbd6Uw8a9hUUgy3KBeXTDvk3pm8upMzZKw17m3mLPEzbcPp5WLpYVoHR5PKNVtFrHH',
'8Aa9LNGdBHwYUMsy6M9ZVXMEkTBZyEDT7aQmY32trCxbU6dwkZJSCSbcpyL7UiTB9QXXosomZtJYvUJ296vTNX5yQ81KaA2',
'85C5zZRcaD89PKmXEwjcYMVAUqoH5rrAXe3GokvSupXnDmccYvZagz5Qem7bQLteEw4iFEJ9oRk9BNfjTi4K2cyTJbTMMPT'
]
];

View file

@ -0,0 +1,372 @@
import 'dart:core';
import 'dart:core' as core;
import 'dart:io';
import 'dart:math';
import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_wownero/wownero_wallet.dart';
import 'package:flutter_libmonero/core/key_service.dart';
import 'package:flutter_libmonero/core/wallet_creation_service.dart';
import 'package:flutter_libmonero/wownero/wownero.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
import 'package:hive_test/hive_test.dart';
import 'package:mockito/annotations.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'wownero_wallet_test_data.dart';
FakeSecureStorage? storage;
WalletService? walletService;
SharedPreferences? prefs;
KeyService? keysStorage;
WowneroWalletBase? walletBase;
late WalletCreationService _walletCreationService;
dynamic _walletInfoSource;
String path = '';
String name = '';
int nettype = 0;
WalletType type = WalletType.wownero;
@GenerateMocks([])
void main() async {
storage = FakeSecureStorage();
prefs = await SharedPreferences.getInstance();
keysStorage = KeyService(storage!);
WalletInfo walletInfo = WalletInfo.external(
id: '',
name: '',
type: type,
isRecovery: false,
restoreHeight: 0,
date: DateTime.now(),
path: '',
address: '',
dirPath: '');
late WalletCredentials credentials;
wownero.onStartup();
bool hiveAdaptersRegistered = false;
group("Wownero 14 word seed generation", () {
setUp(() async {
await setUpTestHive();
if (!hiveAdaptersRegistered) {
hiveAdaptersRegistered = true;
Hive.registerAdapter(NodeAdapter());
Hive.registerAdapter(WalletInfoAdapter());
Hive.registerAdapter(WalletTypeAdapter());
Hive.registerAdapter(UnspentCoinsInfoAdapter());
final wallets = await Hive.openBox('wallets');
await wallets.put('currentWalletName', name);
_walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName);
walletService = wownero
.createWowneroWalletService(_walletInfoSource as Box<WalletInfo>);
}
bool hasThrown = false;
try {
name = 'namee${Random().nextInt(10000000)}';
final dirPath = await pathForWalletDir(name: name, type: type);
path = await pathForWallet(name: name, type: type);
credentials = wownero.createWowneroNewWalletCredentials(
name: name,
language: "English",
seedWordsLength: 14); // TODO catch failure
walletInfo = WalletInfo.external(
id: WalletBase.idFor(name, type),
name: name,
type: type,
isRecovery: false,
restoreHeight: credentials.height ?? 0,
date: DateTime.now(),
path: path,
address: "",
dirPath: dirPath);
credentials.walletInfo = walletInfo;
_walletCreationService = WalletCreationService(
secureStorage: storage,
sharedPreferences: prefs,
walletService: walletService,
keyService: keysStorage,
);
_walletCreationService.changeWalletType();
} catch (e, s) {
print(e);
print(s);
hasThrown = true;
}
expect(hasThrown, false);
});
test("Wownero 14 word seed address generation", () async {
final wallet = await _walletCreationService.create(credentials);
// TODO validate mnemonic
walletInfo.address = wallet.walletAddresses.address;
bool hasThrown = false;
try {
await _walletInfoSource.add(walletInfo);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
expect(
await walletBase!.validateAddress(wallet.walletAddresses.address ?? ''), true);
} catch (_) {
hasThrown = true;
}
expect(hasThrown, false);
// Address validation
expect(
await walletBase!.validateAddress(''), false);
expect(
await walletBase!.validateAddress('Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi'), true);
expect(
await walletBase!.validateAddress('WasdfHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmjkl'), false);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
});
// TODO delete left over wallet file with name: name
});
group("Wownero 14 word seed restoration", () {
setUp(() async {
bool hasThrown = false;
try {
name = 'namee${Random().nextInt(10000000)}';
final dirPath = await pathForWalletDir(name: name, type: type);
path = await pathForWallet(name: name, type: type);
credentials = wownero.createWowneroRestoreWalletFromSeedCredentials(
name: name,
height: 465760,
mnemonic: testMnemonic14); // TODO catch failure
walletInfo = WalletInfo.external(
id: WalletBase.idFor(name, type),
name: name,
type: type,
isRecovery: false,
restoreHeight: credentials.height ?? 0,
date: DateTime.now(),
path: path,
address: "",
dirPath: dirPath);
credentials.walletInfo = walletInfo;
_walletCreationService = WalletCreationService(
secureStorage: storage,
sharedPreferences: prefs,
walletService: walletService,
keyService: keysStorage,
);
_walletCreationService.changeWalletType();
} catch (e, s) {
print(e);
print(s);
hasThrown = true;
}
expect(hasThrown, false);
});
test("Wownero 14 word seed address generation", () async {
final wallet = await _walletCreationService.restoreFromSeed(credentials);
walletInfo.address = wallet.walletAddresses.address;
bool hasThrown = false;
try {
await _walletInfoSource.add(walletInfo);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
expect(walletInfo.address, mainnetTestData14[0][0]);
expect(await walletBase!.getTransactionAddress(0, 0),
mainnetTestData14[0][0]);
expect(await walletBase!.getTransactionAddress(0, 1),
mainnetTestData14[0][1]);
expect(await walletBase!.getTransactionAddress(0, 2),
mainnetTestData14[0][2]);
expect(await walletBase!.getTransactionAddress(1, 0),
mainnetTestData14[1][0]);
expect(await walletBase!.getTransactionAddress(1, 1),
mainnetTestData14[1][1]);
expect(await walletBase!.getTransactionAddress(1, 2),
mainnetTestData14[1][2]);
} catch (_) {
hasThrown = true;
}
expect(hasThrown, false);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
});
// TODO delete left over wallet file with name: name
});
group("Wownero 25 word seed generation", () {
setUp(() async {
bool hasThrown = false;
try {
name = 'namee${Random().nextInt(10000000)}';
final dirPath = await pathForWalletDir(name: name, type: type);
path = await pathForWallet(name: name, type: type);
credentials = wownero.createWowneroNewWalletCredentials(
name: name,
language: "English",
seedWordsLength: 25); // TODO catch failure
walletInfo = WalletInfo.external(
id: WalletBase.idFor(name, type),
name: name,
type: type,
isRecovery: false,
restoreHeight: credentials.height ?? 0,
date: DateTime.now(),
path: path,
address: "",
dirPath: dirPath);
credentials.walletInfo = walletInfo;
_walletCreationService = WalletCreationService(
secureStorage: storage,
sharedPreferences: prefs,
walletService: walletService,
keyService: keysStorage,
);
_walletCreationService.changeWalletType();
} catch (e, s) {
print(e);
print(s);
hasThrown = true;
}
expect(hasThrown, false);
});
test("Wownero 25 word seed address generation", () async {
final wallet = await _walletCreationService.create(credentials);
// TODO validate mnemonic
walletInfo.address = wallet.walletAddresses.address;
bool hasThrown = false;
try {
await _walletInfoSource.add(walletInfo);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
// TODO validate
//expect(walletInfo.address, mainnetTestData14[0][0]);
} catch (_) {
hasThrown = true;
}
expect(hasThrown, false);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
});
// TODO delete left over wallet file with name: name
});
group("Wownero 25 word seed restoration", () {
setUp(() async {
bool hasThrown = false;
try {
name = 'namee${Random().nextInt(10000000)}';
final dirPath = await pathForWalletDir(name: name, type: type);
path = await pathForWallet(name: name, type: type);
credentials = wownero.createWowneroRestoreWalletFromSeedCredentials(
name: name,
height: 465760,
mnemonic: testMnemonic25); // TODO catch failure
walletInfo = WalletInfo.external(
id: WalletBase.idFor(name, type),
name: name,
type: type,
isRecovery: false,
restoreHeight: credentials.height ?? 0,
date: DateTime.now(),
path: path,
address: "",
dirPath: dirPath);
credentials.walletInfo = walletInfo;
_walletCreationService = WalletCreationService(
secureStorage: storage,
sharedPreferences: prefs,
walletService: walletService,
keyService: keysStorage,
);
_walletCreationService.changeWalletType();
} catch (e, s) {
print(e);
print(s);
hasThrown = true;
}
expect(hasThrown, false);
});
test("Wownero 25 word seed address generation", () async {
final wallet = await _walletCreationService.restoreFromSeed(credentials);
walletInfo.address = wallet.walletAddresses.address;
bool hasThrown = false;
try {
await _walletInfoSource.add(walletInfo);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
expect(walletInfo.address, mainnetTestData25[0][0]);
} catch (_) {
hasThrown = true;
}
expect(hasThrown, false);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
});
// TODO delete left over wallet file with name: name
});
}
Future<String> pathForWalletDir(
{required String name, required WalletType type}) async {
Directory root = (await getApplicationDocumentsDirectory());
if (Platform.isIOS) {
root = (await getLibraryDirectory());
}
final prefix = walletTypeToString(type).toLowerCase();
final walletsDir = Directory('${root.path}/wallets');
final walletDire = Directory('${walletsDir.path}/$prefix/$name');
if (!walletDire.existsSync()) {
walletDire.createSync(recursive: true);
}
return walletDire.path;
}
Future<String> pathForWallet(
{required String name, required WalletType type}) async =>
await pathForWalletDir(name: name, type: type)
.then((path) => path + '/$name');

View file

@ -0,0 +1,22 @@
String testMnemonic14 =
'weather cruise school such silly profit clerk wage reduce obtain ill sand episode shadow';
var mainnetTestData14 = [
[
'Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi',
'WW3K54QzmMFB1uTZh3LVvgQYqANLmX1FkJHLJ4sU1E7BQmp8nGizyBnjNXSgsjCa4BQ3Rw3GG5jw1ByUkaUjSywm2KmHAbFvK',
'WW3e3F51KAojcSW2G5WimmE1WVFsbBHc6HppZFBa6dNiEn21cThXzdGGDbpv89aTKXSRSPSFaetK6HgCozYawaYz2knUi9Hmn'
],
[
'WW2nx7MFruyN2CcXnGnMbDdvqsyZUGQthLWKYPkQ4iM9XCE54RyWVjNjgopryUbyi9WKzYhHDai2wENbh1Jh1UHa28CL72TYt',
'WW34p57QBMoD6MEZVTu5u9R7G3KeYqvN4eYbvHLYsgbWXpLe992fBvVB7ANJNvaGmPg2uwY5oKjwKbpo4fDU6cGS231PmvXrZ',
'WW2KQLLt6gjC9gRsC4NGehbAZX6UPU7sK89UQFwSg3NKj3MXPwnjh5BiJVqYYNQb6JNsfa7oP7eDjLagtLa2H6YP11RhUNQqw'
]
];
String testMnemonic25 =
'myth byline benches sadness nylon tamper guide giving match angled lurk rally makeup alarms river soapy dolphin woven ticket maul examine public luggage mammal alarms';
var mainnetTestData25 = [
[
'Wo3piMnt1ztjLktFJNsfs9ce6N1tyHk7DB93cNqTGPJ7To3RS7W2q5DdxgQAG5E6RQXQhchQD7ip8WWL3fD8Ww5K2XmAXYxta'
]
];