Merge branch 'staging' into wownero/25-word

This commit is contained in:
sneurlax 2022-11-09 08:33:15 -06:00 committed by GitHub
commit bad88aeecb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 3730 additions and 1178 deletions

View file

@ -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) {

View file

@ -10,6 +10,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; import 'package:stackwallet/providers/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

View file

@ -10,6 +10,8 @@ import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/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),
);
}
},
),
],
),
),
],
), ),
), ),
); );

View file

@ -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));

View file

@ -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);

View file

@ -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);

View file

@ -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: () {},
),
],
),
], ],
), ),
), ),

View file

@ -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: () {},
),
],
),
], ],
), ),
)); ));

View file

@ -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,
);
},
),
],
), ),
), ),
), ),
], ),
), ],
), ),
); );
} }

View file

@ -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,
);
},
),
],
),
),
], ],
), ),
); );

View file

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

View file

@ -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,
), ),
]; ];

View file

@ -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,
),
),
],
),
],
), ),
), ),
), ),

View file

@ -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,

View file

@ -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(

View file

@ -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);
}, },
), ),
), ),

View file

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

View file

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

View file

@ -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(

View file

@ -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;

View file

@ -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;
} }

View file

@ -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;
} }
} }

View file

@ -9,8 +9,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/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);
} }

View file

@ -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;
} }
} }

View file

@ -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();

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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) {

View file

@ -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;
} }
} }

View file

@ -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;
} }

View file

@ -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;
} }
} }

View file

@ -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;
}

View file

@ -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:

View file

@ -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 = () {

View file

@ -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);

View file

@ -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);

View file

@ -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 {

View file

@ -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"),

View file

@ -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();
}
} }