mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-17 01:37:54 +00:00
Merge branch 'staging' into wownero/25-word
This commit is contained in:
commit
bad88aeecb
39 changed files with 3730 additions and 1178 deletions
|
@ -150,6 +150,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: check for wownero wordlist?
|
||||||
bool _isValidMnemonicWord(String word) {
|
bool _isValidMnemonicWord(String word) {
|
||||||
// TODO: get the actual language
|
// TODO: get the actual language
|
||||||
if (widget.coin == Coin.monero) {
|
if (widget.coin == Coin.monero) {
|
||||||
|
@ -189,6 +190,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
||||||
} else if (widget.coin == Coin.wownero) {
|
} else if (widget.coin == Coin.wownero) {
|
||||||
height = wownero.getHeightByDate(date: widget.restoreFromDate);
|
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
|
// TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index
|
||||||
if (widget.coin == Coin.epicCash) {
|
if (widget.coin == Coin.epicCash) {
|
||||||
|
|
|
@ -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/exchange/trade_sent_from_stack_lookup_provider.dart';
|
||||||
import 'package:stackwallet/providers/providers.dart';
|
import 'package:stackwallet/providers/providers.dart';
|
||||||
import 'package:stackwallet/route_generator.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/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/format.dart';
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
@ -27,6 +28,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget {
|
||||||
required this.walletId,
|
required this.walletId,
|
||||||
this.routeOnSuccessName = WalletView.routeName,
|
this.routeOnSuccessName = WalletView.routeName,
|
||||||
required this.trade,
|
required this.trade,
|
||||||
|
this.shouldSendPublicFiroFunds,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
static const String routeName = "/confirmChangeNowSend";
|
static const String routeName = "/confirmChangeNowSend";
|
||||||
|
@ -35,6 +37,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget {
|
||||||
final String walletId;
|
final String walletId;
|
||||||
final String routeOnSuccessName;
|
final String routeOnSuccessName;
|
||||||
final Trade trade;
|
final Trade trade;
|
||||||
|
final bool? shouldSendPublicFiroFunds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ConfirmChangeNowSendView> createState() =>
|
ConsumerState<ConfirmChangeNowSendView> createState() =>
|
||||||
|
@ -63,7 +66,15 @@ class _ConfirmChangeNowSendViewState
|
||||||
ref.read(walletsChangeNotifierProvider).getManager(walletId);
|
ref.read(walletsChangeNotifierProvider).getManager(walletId);
|
||||||
|
|
||||||
try {
|
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());
|
unawaited(manager.refresh());
|
||||||
|
|
||||||
// save note
|
// save note
|
||||||
|
|
|
@ -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/pages/send_view/sub_widgets/building_transaction_dialog.dart';
|
||||||
import 'package:stackwallet/providers/providers.dart';
|
import 'package:stackwallet/providers/providers.dart';
|
||||||
import 'package:stackwallet/route_generator.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/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.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/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
import 'package:stackwallet/widgets/animated_text.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/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/rounded_white_container.dart';
|
||||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
|
||||||
|
@ -162,6 +166,130 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
late final String address;
|
late final String address;
|
||||||
late final Trade trade;
|
late final Trade trade;
|
||||||
|
|
||||||
|
Future<void> _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async {
|
||||||
|
final _amount = Format.decimalAmountToSatoshis(amount);
|
||||||
|
|
||||||
|
try {
|
||||||
|
bool wasCancelled = false;
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
useSafeArea: false,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) {
|
||||||
|
return BuildingTransactionDialog(
|
||||||
|
onCancel: () {
|
||||||
|
wasCancelled = true;
|
||||||
|
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
late Map<String, dynamic> txData;
|
||||||
|
|
||||||
|
// if not firo then do normal send
|
||||||
|
if (shouldSendPublicFiroFunds == null) {
|
||||||
|
txData = await manager.prepareSend(
|
||||||
|
address: address,
|
||||||
|
satoshiAmount: _amount,
|
||||||
|
args: {
|
||||||
|
"feeRate": FeeRateType.average,
|
||||||
|
// 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
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
txData["note"] =
|
||||||
|
"${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange";
|
||||||
|
txData["address"] = address;
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
RouteGenerator.getRoute(
|
||||||
|
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
|
||||||
|
builder: (_) => ConfirmChangeNowSendView(
|
||||||
|
transactionInfo: txData,
|
||||||
|
walletId: walletId,
|
||||||
|
routeOnSuccessName: HomeView.routeName,
|
||||||
|
trade: trade,
|
||||||
|
shouldSendPublicFiroFunds: shouldSendPublicFiroFunds,
|
||||||
|
),
|
||||||
|
settings: const RouteSettings(
|
||||||
|
name: ConfirmChangeNowSendView.routeName,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// if (mounted) {
|
||||||
|
// pop building dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
await showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
useSafeArea: false,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (context) {
|
||||||
|
return StackDialog(
|
||||||
|
title: "Transaction failed",
|
||||||
|
message: e.toString(),
|
||||||
|
rightButton: TextButton(
|
||||||
|
style: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.getSecondaryEnabledButtonColor(context),
|
||||||
|
child: Text(
|
||||||
|
"Ok",
|
||||||
|
style: STextStyles.button(context).copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.buttonTextSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
walletId = widget.walletId;
|
walletId = widget.walletId;
|
||||||
|
@ -182,181 +310,278 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
|
|
||||||
final coin = manager.coin;
|
final coin = manager.coin;
|
||||||
|
|
||||||
|
final isFiro = coin == Coin.firoTestNet || coin == Coin.firo;
|
||||||
|
|
||||||
return RoundedWhiteContainer(
|
return RoundedWhiteContainer(
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
child: MaterialButton(
|
child: ConditionalParent(
|
||||||
splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
condition: isFiro,
|
||||||
key: Key("walletsSheetItemButtonKey_$walletId"),
|
builder: (child) => Expandable(
|
||||||
padding: const EdgeInsets.all(8),
|
header: Container(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
color: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
child: Padding(
|
||||||
borderRadius: BorderRadius.circular(
|
padding: const EdgeInsets.all(12),
|
||||||
Constants.size.circularBorderRadius,
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
child: ConditionalParent(
|
||||||
final _amount = Format.decimalAmountToSatoshis(amount);
|
condition: !isFiro,
|
||||||
|
builder: (child) => MaterialButton(
|
||||||
try {
|
splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
bool wasCancelled = false;
|
key: Key("walletsSheetItemButtonKey_$walletId"),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
unawaited(showDialog<dynamic>(
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
context: context,
|
shape: RoundedRectangleBorder(
|
||||||
useSafeArea: false,
|
borderRadius: BorderRadius.circular(
|
||||||
barrierDismissible: false,
|
Constants.size.circularBorderRadius,
|
||||||
builder: (context) {
|
),
|
||||||
return BuildingTransactionDialog(
|
),
|
||||||
onCancel: () {
|
onPressed: () => _send(manager),
|
||||||
wasCancelled = true;
|
child: child,
|
||||||
|
),
|
||||||
Navigator.of(context).pop();
|
child: Row(
|
||||||
},
|
children: [
|
||||||
);
|
Container(
|
||||||
},
|
decoration: BoxDecoration(
|
||||||
));
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
final txData = await manager.prepareSend(
|
.colorForCoin(manager.coin)
|
||||||
address: address,
|
.withOpacity(0.5),
|
||||||
satoshiAmount: _amount,
|
borderRadius: BorderRadius.circular(
|
||||||
args: {
|
Constants.size.circularBorderRadius,
|
||||||
"feeRate": FeeRateType.average,
|
|
||||||
// ref.read(feeRateTypeStateProvider)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!wasCancelled) {
|
|
||||||
// pop building dialog
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
txData["note"] =
|
|
||||||
"${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange";
|
|
||||||
txData["address"] = address;
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
RouteGenerator.getRoute(
|
|
||||||
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
|
|
||||||
builder: (_) => ConfirmChangeNowSendView(
|
|
||||||
transactionInfo: txData,
|
|
||||||
walletId: walletId,
|
|
||||||
routeOnSuccessName: HomeView.routeName,
|
|
||||||
trade: trade,
|
|
||||||
),
|
|
||||||
settings: const RouteSettings(
|
|
||||||
name: ConfirmChangeNowSendView.routeName,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
child: Padding(
|
||||||
}
|
padding: const EdgeInsets.all(6),
|
||||||
} catch (e) {
|
child: SvgPicture.asset(
|
||||||
// if (mounted) {
|
Assets.svg.iconFor(coin: coin),
|
||||||
// pop building dialog
|
width: 24,
|
||||||
Navigator.of(context).pop();
|
height: 24,
|
||||||
|
),
|
||||||
await showDialog<dynamic>(
|
),
|
||||||
context: context,
|
),
|
||||||
useSafeArea: false,
|
const SizedBox(
|
||||||
barrierDismissible: true,
|
width: 12,
|
||||||
builder: (context) {
|
),
|
||||||
return StackDialog(
|
Expanded(
|
||||||
title: "Transaction failed",
|
child: Column(
|
||||||
message: e.toString(),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
rightButton: TextButton(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: Theme.of(context)
|
children: [
|
||||||
.extension<StackColors>()!
|
Text(
|
||||||
.getSecondaryEnabledButtonColor(context),
|
manager.walletName,
|
||||||
child: Text(
|
style: STextStyles.titleBold12(context),
|
||||||
"Ok",
|
),
|
||||||
style: STextStyles.button(context).copyWith(
|
if (!isFiro)
|
||||||
color: Theme.of(context)
|
const SizedBox(
|
||||||
.extension<StackColors>()!
|
height: 2,
|
||||||
.buttonTextSecondary,
|
|
||||||
),
|
),
|
||||||
),
|
if (!isFiro)
|
||||||
onPressed: () {
|
FutureBuilder(
|
||||||
Navigator.of(context).pop();
|
future: manager.totalBalance,
|
||||||
},
|
builder:
|
||||||
),
|
(builderContext, AsyncSnapshot<Decimal> snapshot) {
|
||||||
);
|
if (snapshot.connectionState ==
|
||||||
},
|
ConnectionState.done &&
|
||||||
);
|
snapshot.hasData) {
|
||||||
// }
|
return Text(
|
||||||
}
|
"${Format.localizedStringAsFixed(
|
||||||
},
|
value: snapshot.data!,
|
||||||
child: Row(
|
locale: locale,
|
||||||
children: [
|
decimalPlaces: coin == Coin.monero
|
||||||
Container(
|
? Constants.decimalPlacesMonero
|
||||||
decoration: BoxDecoration(
|
: coin == Coin.wownero
|
||||||
color: Theme.of(context)
|
? Constants.decimalPlacesWownero
|
||||||
.extension<StackColors>()!
|
: Constants.decimalPlaces,
|
||||||
.colorForCoin(manager.coin)
|
)} ${coin.ticker}",
|
||||||
.withOpacity(0.5),
|
style: STextStyles.itemSubtitle(context),
|
||||||
borderRadius: BorderRadius.circular(
|
);
|
||||||
Constants.size.circularBorderRadius,
|
} else {
|
||||||
|
return AnimatedText(
|
||||||
|
stringsToLoopThrough: const [
|
||||||
|
"Loading balance",
|
||||||
|
"Loading balance.",
|
||||||
|
"Loading balance..",
|
||||||
|
"Loading balance..."
|
||||||
|
],
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
],
|
||||||
padding: const EdgeInsets.all(6),
|
),
|
||||||
child: SvgPicture.asset(
|
|
||||||
Assets.svg.iconFor(coin: coin),
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
manager.walletName,
|
|
||||||
style: STextStyles.titleBold12(context),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 2,
|
|
||||||
),
|
|
||||||
FutureBuilder(
|
|
||||||
future: manager.totalBalance,
|
|
||||||
builder: (builderContext, AsyncSnapshot<Decimal> snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.done &&
|
|
||||||
snapshot.hasData) {
|
|
||||||
return Text(
|
|
||||||
"${Format.localizedStringAsFixed(
|
|
||||||
value: snapshot.data!,
|
|
||||||
locale: locale,
|
|
||||||
decimalPlaces: coin == Coin.monero
|
|
||||||
? Constants.decimalPlacesMonero
|
|
||||||
: coin == Coin.wownero
|
|
||||||
? Constants.decimalPlacesWownero
|
|
||||||
: 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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -87,13 +87,13 @@ class _ConfirmTransactionViewState
|
||||||
txid = await manager.confirmSend(txData: transactionInfo);
|
txid = await manager.confirmSend(txData: transactionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
unawaited(manager.refresh());
|
|
||||||
|
|
||||||
// save note
|
// save note
|
||||||
await ref
|
await ref
|
||||||
.read(notesServiceChangeNotifierProvider(walletId))
|
.read(notesServiceChangeNotifierProvider(walletId))
|
||||||
.editOrAddNote(txid: txid, note: note);
|
.editOrAddNote(txid: txid, note: note);
|
||||||
|
|
||||||
|
unawaited(manager.refresh());
|
||||||
|
|
||||||
// pop back to wallet
|
// pop back to wallet
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName));
|
Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName));
|
||||||
|
|
|
@ -110,7 +110,29 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
||||||
ref.read(nodeFormDataProvider).useSSL = false;
|
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) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
|
|
@ -97,7 +97,29 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
|
||||||
|
|
||||||
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
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) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
|
|
@ -147,7 +147,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Choose file location",
|
"Choose file location",
|
||||||
style: STextStyles.desktopTextExtraExtraSmall(context)
|
style: STextStyles.desktopTextExtraExtraSmall(context)
|
||||||
|
@ -157,25 +157,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
.textDark3),
|
.textDark3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// child,
|
child,
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
PrimaryButton(
|
|
||||||
desktopMed: true,
|
|
||||||
width: 200,
|
|
||||||
label: "Create backup",
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
SecondaryButton(
|
|
||||||
desktopMed: true,
|
|
||||||
width: 200,
|
|
||||||
label: "Cancel",
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -252,8 +234,21 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
if (!Platform.isAndroid)
|
if (!Platform.isAndroid)
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
height: 8,
|
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(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
|
@ -272,6 +267,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
passwordFocusNode,
|
passwordFocusNode,
|
||||||
context,
|
context,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
|
labelStyle:
|
||||||
|
isDesktop ? STextStyles.fieldLabel(context) : null,
|
||||||
suffixIcon: UnconstrainedBox(
|
suffixIcon: UnconstrainedBox(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -403,6 +400,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
passwordRepeatFocusNode,
|
passwordRepeatFocusNode,
|
||||||
context,
|
context,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
|
labelStyle:
|
||||||
|
isDesktop ? STextStyles.fieldLabel(context) : null,
|
||||||
suffixIcon: UnconstrainedBox(
|
suffixIcon: UnconstrainedBox(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -442,113 +441,235 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
if (!isDesktop) const Spacer(),
|
||||||
TextButton(
|
!isDesktop
|
||||||
style: shouldEnableCreate
|
? TextButton(
|
||||||
? Theme.of(context)
|
style: shouldEnableCreate
|
||||||
.extension<StackColors>()!
|
? Theme.of(context)
|
||||||
.getPrimaryEnabledButtonColor(context)
|
.extension<StackColors>()!
|
||||||
: Theme.of(context)
|
.getPrimaryEnabledButtonColor(context)
|
||||||
.extension<StackColors>()!
|
: Theme.of(context)
|
||||||
.getPrimaryDisabledButtonColor(context),
|
.extension<StackColors>()!
|
||||||
onPressed: !shouldEnableCreate
|
.getPrimaryDisabledButtonColor(context),
|
||||||
? null
|
onPressed: !shouldEnableCreate
|
||||||
: () async {
|
? null
|
||||||
final String pathToSave = fileLocationController.text;
|
: () async {
|
||||||
final String passphrase = passwordController.text;
|
final String pathToSave =
|
||||||
final String repeatPassphrase =
|
fileLocationController.text;
|
||||||
passwordRepeatController.text;
|
final String passphrase = passwordController.text;
|
||||||
|
final String repeatPassphrase =
|
||||||
|
passwordRepeatController.text;
|
||||||
|
|
||||||
if (pathToSave.isEmpty) {
|
if (pathToSave.isEmpty) {
|
||||||
unawaited(showFloatingFlushBar(
|
unawaited(showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "Directory not chosen",
|
message: "Directory not chosen",
|
||||||
context: context,
|
context: context,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(await Directory(pathToSave).exists())) {
|
if (!(await Directory(pathToSave).exists())) {
|
||||||
unawaited(showFloatingFlushBar(
|
unawaited(showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "Directory does not exist",
|
message: "Directory does not exist",
|
||||||
context: context,
|
context: context,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (passphrase.isEmpty) {
|
if (passphrase.isEmpty) {
|
||||||
unawaited(showFloatingFlushBar(
|
unawaited(showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "A passphrase is required",
|
message: "A passphrase is required",
|
||||||
context: context,
|
context: context,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (passphrase != repeatPassphrase) {
|
if (passphrase != repeatPassphrase) {
|
||||||
unawaited(showFloatingFlushBar(
|
unawaited(showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "Passphrase does not match",
|
message: "Passphrase does not match",
|
||||||
context: context,
|
context: context,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
unawaited(showDialog<dynamic>(
|
unawaited(showDialog<dynamic>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (_) => const StackDialog(
|
builder: (_) => const StackDialog(
|
||||||
title: "Encrypting backup",
|
title: "Encrypting backup",
|
||||||
message: "This shouldn't take long",
|
message: "This shouldn't take long",
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
// make sure the dialog is able to be displayed for at least 1 second
|
// make sure the dialog is able to be displayed for at least 1 second
|
||||||
await Future<void>.delayed(const Duration(seconds: 1));
|
await Future<void>.delayed(
|
||||||
|
const Duration(seconds: 1));
|
||||||
|
|
||||||
final DateTime now = DateTime.now();
|
final DateTime now = DateTime.now();
|
||||||
final String fileToSave =
|
final String fileToSave =
|
||||||
"$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb";
|
"$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb";
|
||||||
|
|
||||||
final backup = await SWB.createStackWalletJSON();
|
final backup = await SWB.createStackWalletJSON();
|
||||||
|
|
||||||
bool result = await SWB.encryptStackWalletWithPassphrase(
|
bool result =
|
||||||
fileToSave,
|
await SWB.encryptStackWalletWithPassphrase(
|
||||||
passphrase,
|
fileToSave,
|
||||||
jsonEncode(backup),
|
passphrase,
|
||||||
);
|
jsonEncode(backup),
|
||||||
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
// pop encryption progress dialog
|
// pop encryption progress dialog
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
await showDialog<dynamic>(
|
await showDialog<dynamic>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (_) => Platform.isAndroid
|
builder: (_) => Platform.isAndroid
|
||||||
? StackOkDialog(
|
? StackOkDialog(
|
||||||
title: "Backup saved to:",
|
title: "Backup saved to:",
|
||||||
message: fileToSave,
|
message: fileToSave,
|
||||||
)
|
)
|
||||||
: const StackOkDialog(
|
: const StackOkDialog(
|
||||||
title: "Backup creation succeeded"),
|
title: "Backup creation succeeded"),
|
||||||
);
|
);
|
||||||
passwordController.text = "";
|
passwordController.text = "";
|
||||||
passwordRepeatController.text = "";
|
passwordRepeatController.text = "";
|
||||||
setState(() {});
|
setState(() {});
|
||||||
} else {
|
} else {
|
||||||
await showDialog<dynamic>(
|
await showDialog<dynamic>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (_) => const StackOkDialog(
|
builder: (_) => const StackOkDialog(
|
||||||
title: "Backup creation failed"),
|
title: "Backup creation failed"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"Create backup",
|
"Create backup",
|
||||||
style: STextStyles.button(context),
|
style: STextStyles.button(context),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
PrimaryButton(
|
||||||
|
width: 183,
|
||||||
|
desktopMed: true,
|
||||||
|
label: "Create backup",
|
||||||
|
enabled: shouldEnableCreate,
|
||||||
|
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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
SecondaryButton(
|
||||||
|
width: 183,
|
||||||
|
desktopMed: true,
|
||||||
|
label: "Cancel",
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -131,7 +131,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Choose file location",
|
"Choose file location",
|
||||||
style: STextStyles.desktopTextExtraExtraSmall(context)
|
style: STextStyles.desktopTextExtraExtraSmall(context)
|
||||||
|
@ -142,27 +142,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// child,
|
child,
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
PrimaryButton(
|
|
||||||
desktopMed: true,
|
|
||||||
width: 200,
|
|
||||||
label: "Restore",
|
|
||||||
onPressed: () {
|
|
||||||
restoreBackupPopup(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
SecondaryButton(
|
|
||||||
desktopMed: true,
|
|
||||||
width: 200,
|
|
||||||
label: "Cancel",
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -225,9 +205,22 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
),
|
),
|
||||||
onChanged: (newValue) {},
|
onChanged: (newValue) {},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
height: 8,
|
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(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
Constants.size.circularBorderRadius,
|
Constants.size.circularBorderRadius,
|
||||||
|
@ -245,6 +238,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
passwordFocusNode,
|
passwordFocusNode,
|
||||||
context,
|
context,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
|
labelStyle:
|
||||||
|
isDesktop ? STextStyles.fieldLabel(context) : null,
|
||||||
suffixIcon: UnconstrainedBox(
|
suffixIcon: UnconstrainedBox(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -285,114 +280,237 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
if (!isDesktop) const Spacer(),
|
||||||
TextButton(
|
!isDesktop
|
||||||
style: passwordController.text.isEmpty ||
|
? TextButton(
|
||||||
fileLocationController.text.isEmpty
|
style: passwordController.text.isEmpty ||
|
||||||
? Theme.of(context)
|
fileLocationController.text.isEmpty
|
||||||
.extension<StackColors>()!
|
? Theme.of(context)
|
||||||
.getPrimaryDisabledButtonColor(context)
|
.extension<StackColors>()!
|
||||||
: Theme.of(context)
|
.getPrimaryDisabledButtonColor(context)
|
||||||
.extension<StackColors>()!
|
: Theme.of(context)
|
||||||
.getPrimaryEnabledButtonColor(context),
|
.extension<StackColors>()!
|
||||||
onPressed: passwordController.text.isEmpty ||
|
.getPrimaryEnabledButtonColor(context),
|
||||||
fileLocationController.text.isEmpty
|
onPressed: passwordController.text.isEmpty ||
|
||||||
? null
|
fileLocationController.text.isEmpty
|
||||||
: () async {
|
? null
|
||||||
final String fileToRestore =
|
: () async {
|
||||||
fileLocationController.text;
|
final String fileToRestore =
|
||||||
final String passphrase = passwordController.text;
|
fileLocationController.text;
|
||||||
|
final String passphrase = passwordController.text;
|
||||||
|
|
||||||
if (FocusScope.of(context).hasFocus) {
|
if (FocusScope.of(context).hasFocus) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
await Future<void>.delayed(
|
await Future<void>.delayed(
|
||||||
const Duration(milliseconds: 75));
|
const Duration(milliseconds: 75));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await File(fileToRestore).exists())) {
|
if (!(await File(fileToRestore).exists())) {
|
||||||
showFloatingFlushBar(
|
await showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "Backup file does not exist",
|
message: "Backup file does not exist",
|
||||||
context: context,
|
context: context,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldPop = false;
|
bool shouldPop = false;
|
||||||
showDialog<dynamic>(
|
await showDialog<dynamic>(
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => WillPopScope(
|
builder: (_) => WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
return shouldPop;
|
return shouldPop;
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment:
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
CrossAxisAlignment.stretch,
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Material(
|
children: [
|
||||||
color: Colors.transparent,
|
Material(
|
||||||
child: Center(
|
color: Colors.transparent,
|
||||||
child: Text(
|
child: Center(
|
||||||
"Decrypting Stack backup file",
|
child: Text(
|
||||||
style: STextStyles.pageTitleH2(context)
|
"Decrypting Stack backup file",
|
||||||
.copyWith(
|
style:
|
||||||
color: Theme.of(context)
|
STextStyles.pageTitleH2(context)
|
||||||
.extension<StackColors>()!
|
.copyWith(
|
||||||
.textWhite,
|
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).pop();
|
||||||
|
|
||||||
|
passwordController.text = "";
|
||||||
|
|
||||||
|
if (jsonString == null) {
|
||||||
|
await showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Failed to decrypt backup file",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
RouteGenerator.getRoute(
|
||||||
|
builder: (_) => StackRestoreProgressView(
|
||||||
|
jsonString: jsonString,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
const SizedBox(
|
}
|
||||||
height: 64,
|
},
|
||||||
),
|
child: Text(
|
||||||
const Center(
|
"Restore",
|
||||||
child: LoadingIndicator(
|
style: STextStyles.button(context),
|
||||||
width: 100,
|
),
|
||||||
),
|
)
|
||||||
),
|
: 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;
|
||||||
|
|
||||||
final String? jsonString = await compute(
|
if (FocusScope.of(context).hasFocus) {
|
||||||
SWB.decryptStackWalletWithPassphrase,
|
FocusScope.of(context).unfocus();
|
||||||
Tuple2(fileToRestore, passphrase),
|
await Future<void>.delayed(
|
||||||
debugLabel: "stack wallet decryption compute",
|
const Duration(milliseconds: 75));
|
||||||
);
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (!(await File(fileToRestore).exists())) {
|
||||||
// pop LoadingIndicator
|
await showFloatingFlushBar(
|
||||||
shouldPop = true;
|
type: FlushBarType.warning,
|
||||||
Navigator.of(context).pop();
|
message: "Backup file does not exist",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
passwordController.text = "";
|
bool shouldPop = false;
|
||||||
|
await 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (jsonString == null) {
|
final String? jsonString = await compute(
|
||||||
showFloatingFlushBar(
|
SWB.decryptStackWalletWithPassphrase,
|
||||||
type: FlushBarType.warning,
|
Tuple2(fileToRestore, passphrase),
|
||||||
message: "Failed to decrypt backup file",
|
debugLabel:
|
||||||
context: context,
|
"stack wallet decryption compute",
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.of(context).push(
|
if (mounted) {
|
||||||
RouteGenerator.getRoute(
|
// pop LoadingIndicator
|
||||||
builder: (_) => StackRestoreProgressView(
|
shouldPop = true;
|
||||||
jsonString: jsonString,
|
Navigator.of(context).pop();
|
||||||
),
|
|
||||||
),
|
passwordController.text = "";
|
||||||
);
|
|
||||||
}
|
if (jsonString == null) {
|
||||||
},
|
await showFloatingFlushBar(
|
||||||
child: Text(
|
type: FlushBarType.warning,
|
||||||
"Restore",
|
message:
|
||||||
style: STextStyles.button(context),
|
"Failed to decrypt backup file",
|
||||||
),
|
context: context,
|
||||||
),
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
RouteGenerator.getRoute(
|
||||||
|
builder: (_) =>
|
||||||
|
StackRestoreProgressView(
|
||||||
|
jsonString: jsonString,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
SecondaryButton(
|
||||||
|
width: 183,
|
||||||
|
desktopMed: true,
|
||||||
|
label: "Cancel",
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
|
@ -4,7 +4,10 @@ import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.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/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:stackwallet/widgets/rounded_white_container.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
@ -18,269 +21,363 @@ class SupportView extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDesktop = Util.isDesktop;
|
||||||
|
|
||||||
debugPrint("BUILD: $runtimeType");
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
|
||||||
return Scaffold(
|
return ConditionalParent(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
condition: !isDesktop,
|
||||||
appBar: AppBar(
|
builder: (child) {
|
||||||
leading: AppBarBackButton(
|
return Scaffold(
|
||||||
onPressed: () {
|
backgroundColor:
|
||||||
Navigator.of(context).pop();
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
},
|
appBar: AppBar(
|
||||||
),
|
leading: AppBarBackButton(
|
||||||
title: Text(
|
onPressed: () {
|
||||||
"Support",
|
Navigator.of(context).pop();
|
||||||
style: STextStyles.navBarTitle(context),
|
},
|
||||||
),
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
RoundedWhiteContainer(
|
|
||||||
child: Text(
|
|
||||||
"If you need support or want to report a bug, reach out to us on any of our socials!",
|
|
||||||
style: STextStyles.smallMed12(context),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
title: Text(
|
||||||
height: 12,
|
"Support",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
),
|
),
|
||||||
RoundedWhiteContainer(
|
),
|
||||||
padding: const EdgeInsets.all(0),
|
body: Padding(
|
||||||
child: RawMaterialButton(
|
padding: const EdgeInsets.all(16),
|
||||||
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
child: child,
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
),
|
||||||
shape: RoundedRectangleBorder(
|
);
|
||||||
borderRadius: BorderRadius.circular(
|
},
|
||||||
Constants.size.circularBorderRadius,
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
RoundedWhiteContainer(
|
||||||
|
child: Text(
|
||||||
|
"If you need support or want to report a bug, reach out to us on any of our socials!",
|
||||||
|
style: STextStyles.smallMed12(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isDesktop
|
||||||
|
? const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
)
|
||||||
|
: const SizedBox(
|
||||||
|
height: 12,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
RoundedWhiteContainer(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
child: RawMaterialButton(
|
||||||
|
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("https://t.me/stackwallet"),
|
Uri.parse("https://t.me/stackwallet"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
child: Padding(
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
child: Padding(
|
||||||
horizontal: 12,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 20,
|
horizontal: 12,
|
||||||
),
|
vertical: 20,
|
||||||
child: Row(
|
),
|
||||||
children: [
|
child: Row(
|
||||||
SvgPicture.asset(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Assets.socials.telegram,
|
children: [
|
||||||
width: iconSize,
|
Row(
|
||||||
height: iconSize,
|
children: [
|
||||||
color: Theme.of(context)
|
SvgPicture.asset(
|
||||||
.extension<StackColors>()!
|
Assets.socials.telegram,
|
||||||
.accentColorDark,
|
width: iconSize,
|
||||||
),
|
height: iconSize,
|
||||||
const SizedBox(
|
color: Theme.of(context)
|
||||||
width: 12,
|
.extension<StackColors>()!
|
||||||
),
|
.accentColorDark,
|
||||||
Text(
|
),
|
||||||
"Telegram",
|
const SizedBox(
|
||||||
style: STextStyles.titleBold12(context),
|
width: 12,
|
||||||
textAlign: TextAlign.left,
|
),
|
||||||
),
|
Text(
|
||||||
],
|
"Telegram",
|
||||||
),
|
style: STextStyles.titleBold12(context),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "@stackwallet" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("https://t.me/stackwallet"),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
),
|
||||||
height: 8,
|
const SizedBox(
|
||||||
),
|
height: 8,
|
||||||
RoundedWhiteContainer(
|
),
|
||||||
padding: const EdgeInsets.all(0),
|
RoundedWhiteContainer(
|
||||||
child: RawMaterialButton(
|
padding: const EdgeInsets.all(0),
|
||||||
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
child: RawMaterialButton(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
shape: RoundedRectangleBorder(
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
borderRadius: BorderRadius.circular(
|
shape: RoundedRectangleBorder(
|
||||||
Constants.size.circularBorderRadius,
|
borderRadius: BorderRadius.circular(
|
||||||
),
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("https://discord.gg/RZMG3yUm"),
|
Uri.parse("https://discord.gg/RZMG3yUm"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
child: Padding(
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
child: Padding(
|
||||||
horizontal: 12,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 20,
|
horizontal: 12,
|
||||||
),
|
vertical: 20,
|
||||||
child: Row(
|
),
|
||||||
children: [
|
child: Row(
|
||||||
SvgPicture.asset(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Assets.socials.discord,
|
children: [
|
||||||
width: iconSize,
|
Row(
|
||||||
height: iconSize,
|
children: [
|
||||||
color: Theme.of(context)
|
SvgPicture.asset(
|
||||||
.extension<StackColors>()!
|
Assets.socials.discord,
|
||||||
.accentColorDark,
|
width: iconSize,
|
||||||
),
|
height: iconSize,
|
||||||
const SizedBox(
|
color: Theme.of(context)
|
||||||
width: 12,
|
.extension<StackColors>()!
|
||||||
),
|
.accentColorDark,
|
||||||
Text(
|
),
|
||||||
"Discord",
|
const SizedBox(
|
||||||
style: STextStyles.titleBold12(context),
|
width: 12,
|
||||||
textAlign: TextAlign.left,
|
),
|
||||||
),
|
Text(
|
||||||
],
|
"Discord",
|
||||||
),
|
style: STextStyles.titleBold12(context),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "Stack Wallet" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
"https://discord.gg/RZMG3yUm"), //expired link?
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
),
|
||||||
height: 8,
|
const SizedBox(
|
||||||
),
|
height: 8,
|
||||||
RoundedWhiteContainer(
|
),
|
||||||
padding: const EdgeInsets.all(0),
|
RoundedWhiteContainer(
|
||||||
child: RawMaterialButton(
|
padding: const EdgeInsets.all(0),
|
||||||
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
child: RawMaterialButton(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
shape: RoundedRectangleBorder(
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
borderRadius: BorderRadius.circular(
|
shape: RoundedRectangleBorder(
|
||||||
Constants.size.circularBorderRadius,
|
borderRadius: BorderRadius.circular(
|
||||||
),
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("https://www.reddit.com/r/stackwallet/"),
|
Uri.parse("https://www.reddit.com/r/stackwallet/"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
child: Padding(
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
child: Padding(
|
||||||
horizontal: 12,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 20,
|
horizontal: 12,
|
||||||
),
|
vertical: 20,
|
||||||
child: Row(
|
),
|
||||||
children: [
|
child: Row(
|
||||||
SvgPicture.asset(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Assets.socials.reddit,
|
children: [
|
||||||
width: iconSize,
|
Row(
|
||||||
height: iconSize,
|
children: [
|
||||||
color: Theme.of(context)
|
SvgPicture.asset(
|
||||||
.extension<StackColors>()!
|
Assets.socials.reddit,
|
||||||
.accentColorDark,
|
width: iconSize,
|
||||||
),
|
height: iconSize,
|
||||||
const SizedBox(
|
color: Theme.of(context)
|
||||||
width: 12,
|
.extension<StackColors>()!
|
||||||
),
|
.accentColorDark,
|
||||||
Text(
|
),
|
||||||
"Reddit",
|
const SizedBox(
|
||||||
style: STextStyles.titleBold12(context),
|
width: 12,
|
||||||
textAlign: TextAlign.left,
|
),
|
||||||
),
|
Text(
|
||||||
],
|
"Reddit",
|
||||||
),
|
style: STextStyles.titleBold12(context),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "r/stackwallet" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("https://www.reddit.com/r/stackwallet/"),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
),
|
||||||
height: 8,
|
const SizedBox(
|
||||||
),
|
height: 8,
|
||||||
RoundedWhiteContainer(
|
),
|
||||||
padding: const EdgeInsets.all(0),
|
RoundedWhiteContainer(
|
||||||
child: RawMaterialButton(
|
padding: const EdgeInsets.all(0),
|
||||||
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
child: RawMaterialButton(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
shape: RoundedRectangleBorder(
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
borderRadius: BorderRadius.circular(
|
shape: RoundedRectangleBorder(
|
||||||
Constants.size.circularBorderRadius,
|
borderRadius: BorderRadius.circular(
|
||||||
),
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("https://twitter.com/stack_wallet"),
|
Uri.parse("https://twitter.com/stack_wallet"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
child: Padding(
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
child: Padding(
|
||||||
horizontal: 12,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 20,
|
horizontal: 12,
|
||||||
),
|
vertical: 20,
|
||||||
child: Row(
|
),
|
||||||
children: [
|
child: Row(
|
||||||
SvgPicture.asset(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Assets.socials.twitter,
|
children: [
|
||||||
width: iconSize,
|
Row(
|
||||||
height: iconSize,
|
children: [
|
||||||
color: Theme.of(context)
|
SvgPicture.asset(
|
||||||
.extension<StackColors>()!
|
Assets.socials.twitter,
|
||||||
.accentColorDark,
|
width: iconSize,
|
||||||
),
|
height: iconSize,
|
||||||
const SizedBox(
|
color: Theme.of(context)
|
||||||
width: 12,
|
.extension<StackColors>()!
|
||||||
),
|
.accentColorDark,
|
||||||
Text(
|
),
|
||||||
"Twitter",
|
const SizedBox(
|
||||||
style: STextStyles.titleBold12(context),
|
width: 12,
|
||||||
textAlign: TextAlign.left,
|
),
|
||||||
),
|
Text(
|
||||||
],
|
"Twitter",
|
||||||
),
|
style: STextStyles.titleBold12(context),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "@stack_wallet" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("https://twitter.com/stack_wallet"),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
),
|
||||||
height: 8,
|
const SizedBox(
|
||||||
),
|
height: 8,
|
||||||
RoundedWhiteContainer(
|
),
|
||||||
padding: const EdgeInsets.all(0),
|
RoundedWhiteContainer(
|
||||||
child: RawMaterialButton(
|
padding: const EdgeInsets.all(0),
|
||||||
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
child: RawMaterialButton(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
shape: RoundedRectangleBorder(
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
borderRadius: BorderRadius.circular(
|
shape: RoundedRectangleBorder(
|
||||||
Constants.size.circularBorderRadius,
|
borderRadius: BorderRadius.circular(
|
||||||
),
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("mailto://support@stackwallet.com"),
|
Uri.parse("mailto://support@stackwallet.com"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
child: Padding(
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
child: Padding(
|
||||||
horizontal: 12,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 20,
|
horizontal: 12,
|
||||||
),
|
vertical: 20,
|
||||||
child: Row(
|
),
|
||||||
children: [
|
child: Row(
|
||||||
SvgPicture.asset(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Assets.svg.envelope,
|
children: [
|
||||||
width: iconSize,
|
Row(
|
||||||
height: iconSize,
|
children: [
|
||||||
color: Theme.of(context)
|
SvgPicture.asset(
|
||||||
.extension<StackColors>()!
|
Assets.svg.envelope,
|
||||||
.accentColorDark,
|
width: iconSize,
|
||||||
),
|
height: iconSize,
|
||||||
const SizedBox(
|
color: Theme.of(context)
|
||||||
width: 12,
|
.extension<StackColors>()!
|
||||||
),
|
.accentColorDark,
|
||||||
Text(
|
),
|
||||||
"Email",
|
const SizedBox(
|
||||||
style: STextStyles.titleBold12(context),
|
width: 12,
|
||||||
textAlign: TextAlign.left,
|
),
|
||||||
),
|
Text(
|
||||||
],
|
"Email",
|
||||||
),
|
style: STextStyles.titleBold12(context),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "support@stackwallet.com" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("mailto://support@stackwallet.com"),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import 'package:flutter/material.dart';
|
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/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/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/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
|
||||||
class DesktopLoginView extends StatefulWidget {
|
class DesktopLoginView extends StatefulWidget {
|
||||||
const DesktopLoginView({
|
const DesktopLoginView({
|
||||||
|
@ -18,28 +26,157 @@ class DesktopLoginView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopLoginViewState extends State<DesktopLoginView> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return DesktopScaffold(
|
||||||
child: Column(
|
body: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
SizedBox(
|
||||||
"Login",
|
width: 480,
|
||||||
style: STextStyles.desktopH3(context),
|
child: Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
PrimaryButton(
|
children: [
|
||||||
label: "Login",
|
SvgPicture.asset(
|
||||||
onPressed: () {
|
Assets.svg.stackIcon(context),
|
||||||
// todo auth
|
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(
|
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||||
DesktopHomeView.routeName,
|
DesktopHomeView.routeName,
|
||||||
(route) => false,
|
(route) => false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: "Forgot password?",
|
||||||
|
textSize: 20,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
ForgotPasswordDesktopView.routeName,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
101
lib/pages_desktop_specific/forgot_password_desktop_view.dart
Normal file
101
lib/pages_desktop_specific/forgot_password_desktop_view.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.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/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/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/route_generator.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
|
||||||
|
@ -37,11 +39,15 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
|
||||||
onGenerateRoute: RouteGenerator.generateRoute,
|
onGenerateRoute: RouteGenerator.generateRoute,
|
||||||
initialRoute: DesktopSettingsView.routeName,
|
initialRoute: DesktopSettingsView.routeName,
|
||||||
),
|
),
|
||||||
Container(
|
const Navigator(
|
||||||
color: Colors.blue,
|
key: Key("desktopSupportHomeKey"),
|
||||||
|
onGenerateRoute: RouteGenerator.generateRoute,
|
||||||
|
initialRoute: DesktopSupportView.routeName,
|
||||||
),
|
),
|
||||||
Container(
|
const Navigator(
|
||||||
color: Colors.pink,
|
key: Key("desktopAboutHomeKey"),
|
||||||
|
onGenerateRoute: RouteGenerator.generateRoute,
|
||||||
|
initialRoute: DesktopAboutView.routeName,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/src/widgets/framework.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/svg.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/assets.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.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/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
|
||||||
import 'package:stackwallet/widgets/rounded_white_container.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 {
|
class AppearanceOptionSettings extends ConsumerStatefulWidget {
|
||||||
const AppearanceOptionSettings({Key? key}) : super(key: key);
|
const AppearanceOptionSettings({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@ -140,7 +143,10 @@ class _AppearanceOptionSettings
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ThemeToggle(),
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: ThemeToggle(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -150,7 +156,7 @@ class _AppearanceOptionSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThemeToggle extends StatefulWidget {
|
class ThemeToggle extends ConsumerStatefulWidget {
|
||||||
const ThemeToggle({
|
const ThemeToggle({
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
@ -159,187 +165,226 @@ class ThemeToggle extends StatefulWidget {
|
||||||
// final void Function(bool)? onChanged;
|
// final void Function(bool)? onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _ThemeToggle();
|
ConsumerState<ThemeToggle> createState() => _ThemeToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ThemeToggle extends State<ThemeToggle> {
|
class _ThemeToggle extends ConsumerState<ThemeToggle> {
|
||||||
// late bool externalCallsEnabled;
|
// 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
MaterialButton(
|
||||||
padding: const EdgeInsets.all(8.0),
|
splashColor: Colors.transparent,
|
||||||
child: RawMaterialButton(
|
hoverColor: Colors.transparent,
|
||||||
elevation: 0,
|
padding: const EdgeInsets.all(0),
|
||||||
hoverColor: Colors.transparent,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: BorderSide(
|
borderRadius: BorderRadius.circular(
|
||||||
color:
|
Constants.size.circularBorderRadius,
|
||||||
Theme.of(context).extension<StackColors>()!.infoItemIcons,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
// side: !externalCallsEnabled
|
|
||||||
// ? BorderSide.none
|
|
||||||
// : BorderSide(
|
|
||||||
// color: Theme.of(context)
|
|
||||||
// .extension<StackColors>()!
|
|
||||||
// .infoItemIcons,
|
|
||||||
// width: 2,
|
|
||||||
// ),
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
Constants.size.circularBorderRadius * 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () {}, //onPressed
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 24,
|
|
||||||
),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
Assets.svg.themeLight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 50,
|
|
||||||
top: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"Light",
|
|
||||||
style: STextStyles.desktopTextExtraSmall(context)
|
|
||||||
.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.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(
|
|
||||||
// bottom: 0,
|
|
||||||
// left: 6,
|
|
||||||
// child: Container(
|
|
||||||
// width: 20,
|
|
||||||
// height: 20,
|
|
||||||
// decoration: BoxDecoration(
|
|
||||||
// borderRadius: BorderRadius.circular(1000),
|
|
||||||
// color: Theme.of(context)
|
|
||||||
// .extension<StackColors>()!
|
|
||||||
// .textFieldDefaultBG,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
onPressed: () {
|
||||||
const SizedBox(
|
DB.instance.put<dynamic>(
|
||||||
width: 1,
|
boxName: DB.boxNameTheme,
|
||||||
),
|
key: "colorScheme",
|
||||||
Expanded(
|
value: ThemeType.light.name,
|
||||||
child: Padding(
|
);
|
||||||
padding: const EdgeInsets.all(8.0),
|
ref.read(colorThemeProvider.state).state =
|
||||||
child: RawMaterialButton(
|
StackColors.fromStackColorTheme(
|
||||||
elevation: 0,
|
LightColors(),
|
||||||
hoverColor: Colors.transparent,
|
);
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
// side: !externalCallsEnabled
|
setState(() {
|
||||||
// ? BorderSide.none
|
_selectedTheme = "light";
|
||||||
// : BorderSide(
|
});
|
||||||
// color: Theme.of(context)
|
},
|
||||||
// .extension<StackColors>()!
|
child: SizedBox(
|
||||||
// .infoItemIcons,
|
width: 200,
|
||||||
// width: 2,
|
child: Column(
|
||||||
// ),
|
mainAxisSize: MainAxisSize.min,
|
||||||
borderRadius: BorderRadius.circular(
|
children: [
|
||||||
Constants.size.circularBorderRadius * 2,
|
Container(
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
),
|
border: Border.all(
|
||||||
onPressed: () {}, //onPressed
|
width: 2.5,
|
||||||
child: Padding(
|
color: _selectedTheme == "light"
|
||||||
padding: const EdgeInsets.all(8.0),
|
? Theme.of(context)
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
Assets.svg.themeDark,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 45,
|
|
||||||
top: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"Dark",
|
|
||||||
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(
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
child: Container(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(1000),
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textFieldDefaultBG,
|
.infoItemIcons
|
||||||
),
|
: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.svg.themeLight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
),
|
||||||
|
MaterialButton(
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
hoverColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: [
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -64,48 +64,56 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
|
||||||
height: 48,
|
height: 48,
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.all(10),
|
children: [
|
||||||
child: RichText(
|
Expanded(
|
||||||
textAlign: TextAlign.start,
|
child: Padding(
|
||||||
text: TextSpan(
|
padding: const EdgeInsets.all(10),
|
||||||
children: [
|
child: RichText(
|
||||||
TextSpan(
|
textAlign: TextAlign.start,
|
||||||
text: "Auto Backup",
|
text: TextSpan(
|
||||||
style:
|
children: [
|
||||||
STextStyles.desktopTextSmall(context),
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
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(
|
Column(
|
||||||
|
@ -148,39 +156,49 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.all(10),
|
children: [
|
||||||
child: RichText(
|
Expanded(
|
||||||
textAlign: TextAlign.start,
|
child: Padding(
|
||||||
text: TextSpan(
|
padding: const EdgeInsets.all(10),
|
||||||
children: [
|
child: RichText(
|
||||||
TextSpan(
|
textAlign: TextAlign.start,
|
||||||
text: "Manual Backup",
|
text: TextSpan(
|
||||||
style:
|
children: [
|
||||||
STextStyles.desktopTextSmall(context),
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(
|
padding: const EdgeInsets.all(
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
child: createBackup
|
child: createBackup
|
||||||
? const CreateBackupView()
|
? const SizedBox(
|
||||||
|
width: 512,
|
||||||
|
child: CreateBackupView(),
|
||||||
|
)
|
||||||
: PrimaryButton(
|
: PrimaryButton(
|
||||||
desktopMed: true,
|
desktopMed: true,
|
||||||
width: 200,
|
width: 200,
|
||||||
|
@ -217,38 +235,48 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.all(10),
|
children: [
|
||||||
child: RichText(
|
Expanded(
|
||||||
textAlign: TextAlign.start,
|
child: Padding(
|
||||||
text: TextSpan(
|
padding: const EdgeInsets.all(10),
|
||||||
children: [
|
child: RichText(
|
||||||
TextSpan(
|
textAlign: TextAlign.start,
|
||||||
text: "Restore Backup",
|
text: TextSpan(
|
||||||
style:
|
children: [
|
||||||
STextStyles.desktopTextSmall(context),
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
TextSpan(
|
),
|
||||||
text:
|
|
||||||
"\n\nUse your Stack Wallet backup file to restore your wallets, address book "
|
|
||||||
"and wallet preferences.",
|
|
||||||
style: STextStyles
|
|
||||||
.desktopTextExtraExtraSmall(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(
|
padding: const EdgeInsets.all(
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
child: restoreBackup
|
child: restoreBackup
|
||||||
? RestoreFromFileView()
|
? const SizedBox(
|
||||||
|
width: 512,
|
||||||
|
child: RestoreFromFileView(),
|
||||||
|
)
|
||||||
: PrimaryButton(
|
: PrimaryButton(
|
||||||
desktopMed: true,
|
desktopMed: true,
|
||||||
width: 200,
|
width: 200,
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/log_level_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.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/custom_buttons/app_bar_icon_button.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/progress_bar.dart';
|
||||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:zxcvbn/zxcvbn.dart';
|
||||||
|
|
||||||
class CreateAutoBackup extends StatefulWidget {
|
class CreateAutoBackup extends StatefulWidget {
|
||||||
const CreateAutoBackup({Key? key}) : super(key: key);
|
const CreateAutoBackup({Key? key}) : super(key: key);
|
||||||
|
@ -22,13 +31,24 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
late final TextEditingController passphraseController;
|
late final TextEditingController passphraseController;
|
||||||
late final TextEditingController passphraseRepeatController;
|
late final TextEditingController passphraseRepeatController;
|
||||||
|
|
||||||
late final FocusNode chooseFileLocation;
|
late final StackFileSystem stackFileSystem;
|
||||||
late final FocusNode passphraseFocusNode;
|
late final FocusNode passphraseFocusNode;
|
||||||
late final FocusNode passphraseRepeatFocusNode;
|
late final FocusNode passphraseRepeatFocusNode;
|
||||||
|
final zxcvbn = Zxcvbn();
|
||||||
|
|
||||||
bool shouldShowPasswordHint = true;
|
bool shouldShowPasswordHint = true;
|
||||||
bool hidePassword = 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 =>
|
bool get fieldsMatch =>
|
||||||
passphraseController.text == passphraseRepeatController.text;
|
passphraseController.text == passphraseRepeatController.text;
|
||||||
|
|
||||||
|
@ -42,14 +62,26 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
stackFileSystem = StackFileSystem();
|
||||||
|
|
||||||
fileLocationController = TextEditingController();
|
fileLocationController = TextEditingController();
|
||||||
passphraseController = TextEditingController();
|
passphraseController = TextEditingController();
|
||||||
passphraseRepeatController = TextEditingController();
|
passphraseRepeatController = TextEditingController();
|
||||||
|
|
||||||
chooseFileLocation = FocusNode();
|
|
||||||
passphraseFocusNode = FocusNode();
|
passphraseFocusNode = FocusNode();
|
||||||
passphraseRepeatFocusNode = 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();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +91,6 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
passphraseController.dispose();
|
passphraseController.dispose();
|
||||||
passphraseRepeatController.dispose();
|
passphraseRepeatController.dispose();
|
||||||
|
|
||||||
chooseFileLocation.dispose();
|
|
||||||
passphraseFocusNode.dispose();
|
passphraseFocusNode.dispose();
|
||||||
passphraseRepeatFocusNode.dispose();
|
passphraseRepeatFocusNode.dispose();
|
||||||
|
|
||||||
|
@ -71,9 +102,9 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
debugPrint("BUILD: $runtimeType ");
|
debugPrint("BUILD: $runtimeType ");
|
||||||
|
|
||||||
String? selectedItem = "Every 10 minutes";
|
String? selectedItem = "Every 10 minutes";
|
||||||
|
final isDesktop = Util.isDesktop;
|
||||||
return DesktopDialog(
|
return DesktopDialog(
|
||||||
maxHeight: 650,
|
maxHeight: 680,
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -127,198 +158,289 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
left: 32,
|
child: Column(
|
||||||
right: 32,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
),
|
children: [
|
||||||
child: ClipRRect(
|
if (!Platform.isAndroid)
|
||||||
borderRadius: BorderRadius.circular(
|
Consumer(builder: (context, ref, __) {
|
||||||
Constants.size.circularBorderRadius,
|
return Container(
|
||||||
),
|
|
||||||
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(
|
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(1000),
|
child: TextField(
|
||||||
),
|
autocorrect: false,
|
||||||
height: 32,
|
enableSuggestions: false,
|
||||||
width: 32,
|
onTap: Platform.isAndroid
|
||||||
child: Center(
|
? null
|
||||||
child: SvgPicture.asset(
|
: () async {
|
||||||
Assets.svg.folder,
|
try {
|
||||||
color: Theme.of(context)
|
await stackFileSystem.prepareStorage();
|
||||||
.extension<StackColors>()!
|
|
||||||
.textDark3,
|
if (mounted) {
|
||||||
width: 20,
|
await stackFileSystem.pickDir(context);
|
||||||
height: 17.5,
|
}
|
||||||
),
|
|
||||||
),
|
if (mounted) {
|
||||||
),
|
setState(() {
|
||||||
),
|
fileLocationController.text =
|
||||||
),
|
stackFileSystem.dirPath ?? "";
|
||||||
),
|
});
|
||||||
),
|
}
|
||||||
const SizedBox(
|
} catch (e, s) {
|
||||||
height: 24,
|
Logging.instance
|
||||||
),
|
.log("$e\n$s", level: LogLevel.Error);
|
||||||
Container(
|
}
|
||||||
alignment: Alignment.centerLeft,
|
},
|
||||||
padding: const EdgeInsets.only(left: 32),
|
controller: fileLocationController,
|
||||||
child: Text(
|
style: STextStyles.field(context),
|
||||||
"Create a passphrase",
|
decoration: InputDecoration(
|
||||||
style: STextStyles.desktopTextExtraSmall(context).copyWith(
|
hintText: "Save to...",
|
||||||
color: Theme.of(context).extension<StackColors>()!.textDark3,
|
hintStyle: STextStyles.fieldLabel(context),
|
||||||
),
|
suffixIcon: UnconstrainedBox(
|
||||||
textAlign: TextAlign.left,
|
child: Row(
|
||||||
),
|
children: [
|
||||||
),
|
const SizedBox(
|
||||||
const SizedBox(
|
width: 16,
|
||||||
height: 10,
|
),
|
||||||
),
|
SvgPicture.asset(
|
||||||
Padding(
|
Assets.svg.folder,
|
||||||
padding: const EdgeInsets.only(
|
color: Theme.of(context)
|
||||||
left: 32,
|
.extension<StackColors>()!
|
||||||
right: 32,
|
.textDark3,
|
||||||
),
|
width: 16,
|
||||||
child: ClipRRect(
|
height: 16,
|
||||||
borderRadius: BorderRadius.circular(
|
),
|
||||||
Constants.size.circularBorderRadius,
|
const SizedBox(
|
||||||
),
|
width: 12,
|
||||||
child: TextField(
|
),
|
||||||
key: const Key("createBackupPassphrase"),
|
],
|
||||||
focusNode: passphraseFocusNode,
|
),
|
||||||
controller: passphraseController,
|
|
||||||
style: STextStyles.desktopTextMedium(context).copyWith(
|
|
||||||
height: 2,
|
|
||||||
),
|
|
||||||
obscureText: hidePassword,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autocorrect: false,
|
|
||||||
decoration: standardInputDecoration(
|
|
||||||
"Create passphrase",
|
|
||||||
passphraseFocusNode,
|
|
||||||
context,
|
|
||||||
).copyWith(
|
|
||||||
labelStyle:
|
|
||||||
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).extension<StackColors>()!.textDark3,
|
|
||||||
),
|
|
||||||
suffixIcon: UnconstrainedBox(
|
|
||||||
child: GestureDetector(
|
|
||||||
key: const Key(
|
|
||||||
"createDesktopAutoBackupShowPassphraseButton1"),
|
|
||||||
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,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.textDark3,
|
|
||||||
width: 20,
|
|
||||||
height: 17.5,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
key: const Key(
|
||||||
|
"createBackupSaveToFileLocationTextFieldKey"),
|
||||||
|
readOnly: true,
|
||||||
|
toolbarOptions: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: false,
|
||||||
|
paste: false,
|
||||||
|
selectAll: false,
|
||||||
|
),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
// ref.read(addressEntryDataProvider(widget.id)).address = newValue;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
if (!Platform.isAndroid)
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
ClipRRect(
|
||||||
),
|
borderRadius: BorderRadius.circular(
|
||||||
),
|
Constants.size.circularBorderRadius,
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 32,
|
|
||||||
right: 32,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
Constants.size.circularBorderRadius,
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
key: const Key("createBackupPassphrase"),
|
|
||||||
focusNode: passphraseRepeatFocusNode,
|
|
||||||
controller: passphraseRepeatController,
|
|
||||||
style: STextStyles.desktopTextMedium(context).copyWith(
|
|
||||||
height: 2,
|
|
||||||
),
|
|
||||||
obscureText: hidePassword,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autocorrect: false,
|
|
||||||
decoration: standardInputDecoration(
|
|
||||||
"Confirm passphrase",
|
|
||||||
passphraseRepeatFocusNode,
|
|
||||||
context,
|
|
||||||
).copyWith(
|
|
||||||
labelStyle:
|
|
||||||
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).extension<StackColors>()!.textDark3,
|
|
||||||
),
|
),
|
||||||
suffixIcon: UnconstrainedBox(
|
child: TextField(
|
||||||
child: GestureDetector(
|
key: const Key("createBackupPasswordFieldKey1"),
|
||||||
key: const Key(
|
focusNode: passphraseFocusNode,
|
||||||
"createDesktopAutoBackupShowPassphraseButton2"),
|
controller: passphraseController,
|
||||||
onTap: () async {
|
style: STextStyles.field(context),
|
||||||
|
obscureText: hidePassword,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Create passphrase",
|
||||||
|
passphraseFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
labelStyle:
|
||||||
|
isDesktop ? STextStyles.fieldLabel(context) : null,
|
||||||
|
suffixIcon: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
key: const Key(
|
||||||
|
"createBackupPasswordFieldShowPasswordButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
hidePassword = !hidePassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
hidePassword
|
||||||
|
? Assets.svg.eye
|
||||||
|
: Assets.svg.eyeSlash,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
if (newValue.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
hidePassword = !hidePassword;
|
passwordFeedback = "";
|
||||||
});
|
});
|
||||||
},
|
return;
|
||||||
child: Container(
|
}
|
||||||
decoration: BoxDecoration(
|
final result = zxcvbn.evaluate(newValue);
|
||||||
color: Colors.transparent,
|
String suggestionsAndTips = "";
|
||||||
borderRadius: BorderRadius.circular(1000),
|
for (var sug in result.feedback.suggestions!.toSet()) {
|
||||||
),
|
suggestionsAndTips += "$sug\n";
|
||||||
height: 32,
|
}
|
||||||
width: 32,
|
suggestionsAndTips += result.feedback.warning!;
|
||||||
child: Center(
|
String feedback =
|
||||||
child: SvgPicture.asset(
|
// "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
|
||||||
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
|
suggestionsAndTips;
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
passwordStrength = result.score! / 4;
|
||||||
.textDark3,
|
|
||||||
width: 20,
|
// hack fix to format back string returned from zxcvbn
|
||||||
height: 17.5,
|
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: 12,
|
||||||
|
right: 12,
|
||||||
|
top: 10,
|
||||||
|
),
|
||||||
|
child: ProgressBar(
|
||||||
|
key: const Key("createStackBackUpProgressBar"),
|
||||||
|
width: 510,
|
||||||
|
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("createBackupPasswordFieldKey2"),
|
||||||
|
focusNode: passphraseRepeatFocusNode,
|
||||||
|
controller: passphraseRepeatController,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
obscureText: hidePassword,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Confirm passphrase",
|
||||||
|
passphraseRepeatFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
labelStyle: STextStyles.fieldLabel(context),
|
||||||
|
suffixIcon: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
key: const Key(
|
||||||
|
"createBackupPasswordFieldShowPasswordButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
hidePassword = !hidePassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
hidePassword
|
||||||
|
? Assets.svg.eye
|
||||||
|
: Assets.svg.eyeSlash,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {});
|
||||||
|
// TODO: ? check if passwords match?
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
@ -376,6 +498,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Spacer(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
|
@ -61,8 +61,7 @@ class EnableBackupDialog extends StatelessWidget {
|
||||||
child: SecondaryButton(
|
child: SecondaryButton(
|
||||||
label: "Cancel",
|
label: "Cancel",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
int count = 0;
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).popUntil((_) => count++ >= 2);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -85,6 +85,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear
|
||||||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||||
import 'package:stackwallet/pages/wallets_view/wallets_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/create_password/create_password_view.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/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/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/my_stack_view/my_stack_view.dart';
|
||||||
|
@ -99,6 +100,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/security_settings.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.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/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/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/node_connection_status_changed_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||||
|
@ -996,6 +999,12 @@ class RouteGenerator {
|
||||||
builder: (_) => const CreatePasswordView(),
|
builder: (_) => const CreatePasswordView(),
|
||||||
settings: RouteSettings(name: settings.name));
|
settings: RouteSettings(name: settings.name));
|
||||||
|
|
||||||
|
case ForgotPasswordDesktopView.routeName:
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => const ForgotPasswordDesktopView(),
|
||||||
|
settings: RouteSettings(name: settings.name));
|
||||||
|
|
||||||
case DesktopHomeView.routeName:
|
case DesktopHomeView.routeName:
|
||||||
return getRoute(
|
return getRoute(
|
||||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
@ -1084,6 +1093,18 @@ class RouteGenerator {
|
||||||
builder: (_) => const AdvancedSettings(),
|
builder: (_) => const AdvancedSettings(),
|
||||||
settings: RouteSettings(name: settings.name));
|
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 WalletKeysDesktopPopup.routeName:
|
case WalletKeysDesktopPopup.routeName:
|
||||||
if (args is List<String>) {
|
if (args is List<String>) {
|
||||||
return FadePageRoute(
|
return FadePageRoute(
|
||||||
|
|
|
@ -20,10 +20,13 @@ class AddressBookService extends ChangeNotifier {
|
||||||
List<Contact> get contacts {
|
List<Contact> get contacts {
|
||||||
final keys = List<String>.from(
|
final keys = List<String>.from(
|
||||||
DB.instance.keys<dynamic>(boxName: DB.boxNameAddressBook));
|
DB.instance.keys<dynamic>(boxName: DB.boxNameAddressBook));
|
||||||
return keys
|
final _contacts = keys
|
||||||
.map((id) => Contact.fromJson(Map<String, dynamic>.from(DB.instance
|
.map((id) => Contact.fromJson(Map<String, dynamic>.from(DB.instance
|
||||||
.get<dynamic>(boxName: DB.boxNameAddressBook, key: id) as Map)))
|
.get<dynamic>(boxName: DB.boxNameAddressBook, key: id) as Map)))
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
_contacts
|
||||||
|
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
return _contacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Contact>>? _addressBookEntries;
|
Future<List<Contact>>? _addressBookEntries;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -174,9 +175,10 @@ class BitcoinWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1282,6 +1284,54 @@ class BitcoinWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
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
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
return Address.validateAddress(address, _network);
|
return Address.validateAddress(address, _network);
|
||||||
|
@ -2660,6 +2710,7 @@ class BitcoinWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -207,9 +208,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
_getCurrentAddressForChain(0, DerivePathType.bip44);
|
_getCurrentAddressForChain(0, DerivePathType.bip44);
|
||||||
Future<String>? _currentReceivingAddressP2PKH;
|
Future<String>? _currentReceivingAddressP2PKH;
|
||||||
|
|
||||||
Future<String> get currentReceivingAddressP2SH =>
|
// Future<String> get currentReceivingAddressP2SH =>
|
||||||
_currentReceivingAddressP2SH ??=
|
// _currentReceivingAddressP2SH ??=
|
||||||
_getCurrentAddressForChain(0, DerivePathType.bip49);
|
// _getCurrentAddressForChain(0, DerivePathType.bip49);
|
||||||
Future<String>? _currentReceivingAddressP2SH;
|
Future<String>? _currentReceivingAddressP2SH;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -268,7 +269,11 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
try {
|
try {
|
||||||
if (bitbox.Address.detectFormat(address) ==
|
if (bitbox.Address.detectFormat(address) ==
|
||||||
bitbox.Address.formatCashAddr) {
|
bitbox.Address.formatCashAddr) {
|
||||||
address = bitbox.Address.toLegacyAddress(address);
|
if (validateCashAddr(address)) {
|
||||||
|
address = bitbox.Address.toLegacyAddress(address);
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('$address is not currently supported');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e, s) {}
|
} catch (e, s) {}
|
||||||
try {
|
try {
|
||||||
|
@ -293,11 +298,14 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Bech32 decode fail
|
// Bech32 decode fail
|
||||||
}
|
}
|
||||||
if (_network.bech32 != decodeBech32!.hrp) {
|
|
||||||
throw ArgumentError('Invalid prefix or Network mismatch');
|
if (decodeBech32 != null) {
|
||||||
}
|
if (_network.bech32 != decodeBech32.hrp) {
|
||||||
if (decodeBech32.version != 0) {
|
throw ArgumentError('Invalid prefix or Network mismatch');
|
||||||
throw ArgumentError('Invalid address version');
|
}
|
||||||
|
if (decodeBech32.version != 0) {
|
||||||
|
throw ArgumentError('Invalid address version');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw ArgumentError('$address has no matching Script');
|
throw ArgumentError('$address has no matching Script');
|
||||||
|
@ -1154,6 +1162,63 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
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
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
try {
|
try {
|
||||||
|
@ -1168,12 +1233,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format == bitbox.Address.formatCashAddr) {
|
if (format == bitbox.Address.formatCashAddr) {
|
||||||
String addr = address;
|
return validateCashAddr(address);
|
||||||
if (address.contains(":")) {
|
|
||||||
addr = address.split(":").last;
|
|
||||||
}
|
|
||||||
|
|
||||||
return addr.startsWith("q");
|
|
||||||
} else {
|
} else {
|
||||||
return address.startsWith("1");
|
return address.startsWith("1");
|
||||||
}
|
}
|
||||||
|
@ -2036,7 +2096,8 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
String _convertToScriptHash(String bchAddress, NetworkType network) {
|
String _convertToScriptHash(String bchAddress, NetworkType network) {
|
||||||
try {
|
try {
|
||||||
if (bitbox.Address.detectFormat(bchAddress) ==
|
if (bitbox.Address.detectFormat(bchAddress) ==
|
||||||
bitbox.Address.formatCashAddr) {
|
bitbox.Address.formatCashAddr &&
|
||||||
|
validateCashAddr(bchAddress)) {
|
||||||
bchAddress = bitbox.Address.toLegacyAddress(bchAddress);
|
bchAddress = bitbox.Address.toLegacyAddress(bchAddress);
|
||||||
}
|
}
|
||||||
final output = Address.addressToOutputScript(bchAddress, network);
|
final output = Address.addressToOutputScript(bchAddress, network);
|
||||||
|
@ -2114,7 +2175,8 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
List<String> allAddressesOld = await _fetchAllOwnAddresses();
|
List<String> allAddressesOld = await _fetchAllOwnAddresses();
|
||||||
List<String> allAddresses = [];
|
List<String> allAddresses = [];
|
||||||
for (String address in allAddressesOld) {
|
for (String address in allAddressesOld) {
|
||||||
if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy) {
|
if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy &&
|
||||||
|
addressType(address: address) == DerivePathType.bip44) {
|
||||||
allAddresses.add(bitbox.Address.toCashAddress(address));
|
allAddresses.add(bitbox.Address.toCashAddress(address));
|
||||||
} else {
|
} else {
|
||||||
allAddresses.add(address);
|
allAddresses.add(address);
|
||||||
|
@ -2449,6 +2511,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2832,7 +2895,12 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
String address = output["scriptPubKey"]["addresses"][0] as String;
|
String address = output["scriptPubKey"]["addresses"][0] as String;
|
||||||
if (bitbox.Address.detectFormat(address) ==
|
if (bitbox.Address.detectFormat(address) ==
|
||||||
bitbox.Address.formatCashAddr) {
|
bitbox.Address.formatCashAddr) {
|
||||||
address = bitbox.Address.toLegacyAddress(address);
|
if (validateCashAddr(address)) {
|
||||||
|
address = bitbox.Address.toLegacyAddress(address);
|
||||||
|
} else {
|
||||||
|
throw Exception(
|
||||||
|
"Unsupported address found during fetchBuildTxData(): $address");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!addressTxid.containsKey(address)) {
|
if (!addressTxid.containsKey(address)) {
|
||||||
addressTxid[address] = <String>[];
|
addressTxid[address] = <String>[];
|
||||||
|
@ -2863,10 +2931,6 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
);
|
);
|
||||||
for (int i = 0; i < p2pkhLength; i++) {
|
for (int i = 0; i < p2pkhLength; i++) {
|
||||||
String address = addressesP2PKH[i];
|
String address = addressesP2PKH[i];
|
||||||
if (bitbox.Address.detectFormat(address) ==
|
|
||||||
bitbox.Address.formatCashAddr) {
|
|
||||||
address = bitbox.Address.toLegacyAddress(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
// receives
|
// receives
|
||||||
final receiveDerivation = receiveDerivations[address];
|
final receiveDerivation = receiveDerivations[address];
|
||||||
|
@ -3376,9 +3440,10 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/epiccash/epiccash_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/firo/firo_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/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/namecoin/namecoin_wallet.dart';
|
||||||
|
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
|
||||||
import 'package:stackwallet/services/transaction_notification_tracker.dart';
|
import 'package:stackwallet/services/transaction_notification_tracker.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/prefs.dart';
|
import 'package:stackwallet/utilities/prefs.dart';
|
||||||
|
@ -277,4 +277,7 @@ abstract class CoinServiceAPI {
|
||||||
Future<int> estimateFeeFor(int satoshiAmount, int feeRate);
|
Future<int> estimateFeeFor(int satoshiAmount, int feeRate);
|
||||||
|
|
||||||
Future<bool> generateNewAddress();
|
Future<bool> generateNewAddress();
|
||||||
|
|
||||||
|
// used for electrumx coins
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -1051,6 +1052,54 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
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
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
return Address.validateAddress(address, _network);
|
return Address.validateAddress(address, _network);
|
||||||
|
@ -2273,6 +2322,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2983,9 +3033,10 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -558,9 +558,10 @@ class EpicCashWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -832,10 +833,16 @@ class EpicCashWallet extends CoinServiceAPI {
|
||||||
final txLogEntryFirst = txLogEntry[0];
|
final txLogEntryFirst = txLogEntry[0];
|
||||||
Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst");
|
Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst");
|
||||||
final wallet = await Hive.openBox<dynamic>(_walletId);
|
final wallet = await Hive.openBox<dynamic>(_walletId);
|
||||||
final slateToAddresses = (await wallet.get("slate_to_address")) as Map?;
|
final slateToAddresses =
|
||||||
slateToAddresses?[txLogEntryFirst['tx_slate_id']] = txData['addresss'];
|
(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);
|
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) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Error sending $e - $s", level: LogLevel.Error);
|
Logging.instance.log("Error sending $e - $s", level: LogLevel.Error);
|
||||||
|
@ -2154,8 +2161,9 @@ class EpicCashWallet extends CoinServiceAPI {
|
||||||
as String? ??
|
as String? ??
|
||||||
"";
|
"";
|
||||||
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
|
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("commitId: $commitId $slateId", level: LogLevel.Info);
|
"commitId: $commitId, slateId: $slateId, id: ${tx["id"]}",
|
||||||
|
level: LogLevel.Info);
|
||||||
|
|
||||||
bool isCancelled = tx["tx_type"] == "TxSentCancelled" ||
|
bool isCancelled = tx["tx_type"] == "TxSentCancelled" ||
|
||||||
tx["tx_type"] == "TxReceivedCancelled";
|
tx["tx_type"] == "TxReceivedCancelled";
|
||||||
|
@ -2258,6 +2266,14 @@ class EpicCashWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
// not used in epic
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
// not used in epic
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
||||||
|
|
||||||
|
|
|
@ -821,9 +821,10 @@ class FiroWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -907,6 +908,52 @@ class FiroWallet extends CoinServiceAPI {
|
||||||
Future<models.TransactionData> get _txnData =>
|
Future<models.TransactionData> get _txnData =>
|
||||||
_transactionData ??= _fetchTransactionData();
|
_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
|
/// Holds wallet lelantus transaction data
|
||||||
Future<models.TransactionData>? _lelantusTransactionData;
|
Future<models.TransactionData>? _lelantusTransactionData;
|
||||||
Future<models.TransactionData> get lelantusTransactionData =>
|
Future<models.TransactionData> get lelantusTransactionData =>
|
||||||
|
@ -1109,6 +1156,9 @@ class FiroWallet extends CoinServiceAPI {
|
||||||
final txHash = await _electrumXClient.broadcastTransaction(
|
final txHash = await _electrumXClient.broadcastTransaction(
|
||||||
rawTx: txData["hex"] as String);
|
rawTx: txData["hex"] as String);
|
||||||
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
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;
|
return txHash;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
||||||
|
@ -3464,6 +3514,7 @@ class FiroWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -174,9 +175,10 @@ class LitecoinWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1284,6 +1286,54 @@ class LitecoinWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
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
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
return Address.validateAddress(address, _network, _network.bech32!);
|
return Address.validateAddress(address, _network, _network.bech32!);
|
||||||
|
@ -2672,6 +2722,7 @@ class LitecoinWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,9 @@ class Manager with ChangeNotifier {
|
||||||
try {
|
try {
|
||||||
final txid = await _currentWallet.confirmSend(txData: txData);
|
final txid = await _currentWallet.confirmSend(txData: txData);
|
||||||
|
|
||||||
|
txData["txid"] = txid;
|
||||||
|
await _currentWallet.updateSentCachedTxData(txData);
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return txid;
|
return txid;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1190,6 +1190,14 @@ class MoneroWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
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 {
|
Future<TransactionData> _fetchTransactionData() async {
|
||||||
final transactions = walletBase?.transactionHistory!.transactions;
|
final transactions = walletBase?.transactionHistory!.transactions;
|
||||||
|
|
||||||
|
@ -1376,9 +1384,10 @@ class MoneroWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -170,9 +171,10 @@ class NamecoinWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1275,6 +1277,54 @@ class NamecoinWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
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
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
return Address.validateAddress(address, _network, namecoin.bech32!);
|
return Address.validateAddress(address, _network, namecoin.bech32!);
|
||||||
|
@ -2672,6 +2722,7 @@ class NamecoinWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1214,6 +1214,14 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
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 {
|
Future<TransactionData> _fetchTransactionData() async {
|
||||||
final transactions = walletBase?.transactionHistory!.transactions;
|
final transactions = walletBase?.transactionHistory!.transactions;
|
||||||
|
|
||||||
|
@ -1401,9 +1409,10 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,121 @@
|
||||||
import 'dart:convert';
|
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/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 {
|
try {
|
||||||
final client = http.Client();
|
client.badCertificateCallback = (cert, url, port) {
|
||||||
final response = await client
|
if (allowBadX509Certificate) {
|
||||||
.post(
|
return true;
|
||||||
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);
|
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?
|
// TODO: json decoded without error so assume connection exists?
|
||||||
// or we can check for certain values in the response to decide
|
// or we can check for certain values in the response to decide
|
||||||
return true;
|
return MoneroNodeConnectionResponse(null, null, null, true);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
if (badCertResponse != null) {
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -508,6 +508,25 @@ class STextStyles {
|
||||||
|
|
||||||
// Desktop
|
// 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) {
|
static TextStyle desktopH2(BuildContext context) {
|
||||||
switch (_theme(context).themeType) {
|
switch (_theme(context).themeType) {
|
||||||
case ThemeType.light:
|
case ThemeType.light:
|
||||||
|
|
|
@ -10,11 +10,13 @@ class BlueTextButton extends ConsumerStatefulWidget {
|
||||||
required this.text,
|
required this.text,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
|
this.textSize,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
final double? textSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<BlueTextButton> createState() => _BlueTextButtonState();
|
ConsumerState<BlueTextButton> createState() => _BlueTextButtonState();
|
||||||
|
@ -67,7 +69,14 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: widget.text,
|
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
|
recognizer: widget.enabled
|
||||||
? (TapGestureRecognizer()
|
? (TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
|
|
|
@ -110,7 +110,29 @@ class _NodeCardState extends ConsumerState<NodeCard> {
|
||||||
|
|
||||||
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
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) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
|
|
@ -93,7 +93,29 @@ class NodeOptionsSheet extends ConsumerWidget {
|
||||||
|
|
||||||
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
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) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
|
|
@ -94,19 +94,19 @@ void main() {
|
||||||
test("get contacts", () {
|
test("get contacts", () {
|
||||||
final service = AddressBookService();
|
final service = AddressBookService();
|
||||||
expect(service.contacts.toString(),
|
expect(service.contacts.toString(),
|
||||||
[contactA, contactB, contactC].toString());
|
[contactC, contactB, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("get addressBookEntries", () async {
|
test("get addressBookEntries", () async {
|
||||||
final service = AddressBookService();
|
final service = AddressBookService();
|
||||||
expect((await service.addressBookEntries).toString(),
|
expect((await service.addressBookEntries).toString(),
|
||||||
[contactA, contactB, contactC].toString());
|
[contactC, contactB, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("search contacts", () async {
|
test("search contacts", () async {
|
||||||
final service = AddressBookService();
|
final service = AddressBookService();
|
||||||
final results = await service.search("j");
|
final results = await service.search("j");
|
||||||
expect(results.toString(), [contactA, contactB].toString());
|
expect(results.toString(), [contactB, contactA].toString());
|
||||||
|
|
||||||
final results2 = await service.search("ja");
|
final results2 = await service.search("ja");
|
||||||
expect(results2.toString(), [contactB].toString());
|
expect(results2.toString(), [contactB].toString());
|
||||||
|
@ -118,7 +118,7 @@ void main() {
|
||||||
expect(results4.toString(), <Contact>[].toString());
|
expect(results4.toString(), <Contact>[].toString());
|
||||||
|
|
||||||
final results5 = await service.search("");
|
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");
|
final results6 = await service.search("epic address");
|
||||||
expect(results6.toString(), [contactC].toString());
|
expect(results6.toString(), [contactC].toString());
|
||||||
|
@ -140,7 +140,7 @@ void main() {
|
||||||
expect(result, false);
|
expect(result, false);
|
||||||
expect(service.contacts.length, 3);
|
expect(service.contacts.length, 3);
|
||||||
expect(service.contacts.toString(),
|
expect(service.contacts.toString(),
|
||||||
[contactA, contactB, contactC].toString());
|
[contactC, contactB, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("edit contact", () async {
|
test("edit contact", () async {
|
||||||
|
@ -149,14 +149,14 @@ void main() {
|
||||||
expect(await service.editContact(editedContact), true);
|
expect(await service.editContact(editedContact), true);
|
||||||
expect(service.contacts.length, 3);
|
expect(service.contacts.length, 3);
|
||||||
expect(service.contacts.toString(),
|
expect(service.contacts.toString(),
|
||||||
[contactA, editedContact, contactC].toString());
|
[contactC, contactA, editedContact].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("remove existing contact", () async {
|
test("remove existing contact", () async {
|
||||||
final service = AddressBookService();
|
final service = AddressBookService();
|
||||||
await service.removeContact(contactB.id);
|
await service.removeContact(contactB.id);
|
||||||
expect(service.contacts.length, 2);
|
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 {
|
test("remove non existing contact", () async {
|
||||||
|
@ -164,7 +164,7 @@ void main() {
|
||||||
await service.removeContact("some id");
|
await service.removeContact("some id");
|
||||||
expect(service.contacts.length, 3);
|
expect(service.contacts.length, 3);
|
||||||
expect(service.contacts.toString(),
|
expect(service.contacts.toString(),
|
||||||
[contactA, contactB, contactC].toString());
|
[contactC, contactB, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
|
|
@ -60,7 +60,7 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group("validate mainnet bitcoincash addresses", () {
|
group("mainnet bitcoincash addressType", () {
|
||||||
MockElectrumX? client;
|
MockElectrumX? client;
|
||||||
MockCachedElectrumX? cachedClient;
|
MockCachedElectrumX? cachedClient;
|
||||||
MockPriceAPI? priceAPI;
|
MockPriceAPI? priceAPI;
|
||||||
|
@ -136,6 +136,168 @@ void main() {
|
||||||
verifyNoMoreInteractions(priceAPI);
|
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", () {
|
test("invalid mainnet bitcoincash legacy/p2pkh address", () {
|
||||||
expect(
|
expect(
|
||||||
mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"),
|
mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"),
|
||||||
|
|
|
@ -182,4 +182,10 @@ class FakeCoinServiceAPI extends CoinServiceAPI {
|
||||||
// TODO: implement generateNewAddress
|
// TODO: implement generateNewAddress
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) {
|
||||||
|
// TODO: implement updateSentCachedTxData
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue