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();
}
// TODO: check for wownero wordlist?
bool _isValidMnemonicWord(String word) {
// TODO: get the actual language
if (widget.coin == Coin.monero) {
@ -189,6 +190,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
} else if (widget.coin == Coin.wownero) {
height = wownero.getHeightByDate(date: widget.restoreFromDate);
}
// todo: wait until this implemented
// else if (widget.coin == Coin.wownero) {
// height = wownero.getHeightByDate(date: widget.restoreFromDate);
// }
// TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index
if (widget.coin == Coin.epicCash) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -147,7 +147,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(10.0),
padding: const EdgeInsets.only(bottom: 10),
child: Text(
"Choose file location",
style: STextStyles.desktopTextExtraExtraSmall(context)
@ -157,25 +157,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
.textDark3),
),
),
// 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: () {},
),
],
),
child,
],
);
},
@ -252,8 +234,21 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
);
}),
if (!Platform.isAndroid)
const SizedBox(
height: 8,
SizedBox(
height: !isDesktop ? 8 : 24,
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Create a passphrase",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
),
ClipRRect(
borderRadius: BorderRadius.circular(
@ -272,6 +267,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
passwordFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
@ -403,6 +400,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
passwordRepeatFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
@ -442,8 +441,9 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
const SizedBox(
height: 16,
),
const Spacer(),
TextButton(
if (!isDesktop) const Spacer(),
!isDesktop
? TextButton(
style: shouldEnableCreate
? Theme.of(context)
.extension<StackColors>()!
@ -454,7 +454,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
onPressed: !shouldEnableCreate
? null
: () async {
final String pathToSave = fileLocationController.text;
final String pathToSave =
fileLocationController.text;
final String passphrase = passwordController.text;
final String repeatPassphrase =
passwordRepeatController.text;
@ -501,7 +502,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
),
));
// 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 String fileToSave =
@ -509,7 +511,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
final backup = await SWB.createStackWalletJSON();
bool result = await SWB.encryptStackWalletWithPassphrase(
bool result =
await SWB.encryptStackWalletWithPassphrase(
fileToSave,
passphrase,
jsonEncode(backup),
@ -548,6 +551,124 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
"Create backup",
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,
children: [
Padding(
padding: const EdgeInsets.all(10.0),
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Choose file location",
style: STextStyles.desktopTextExtraExtraSmall(context)
@ -142,27 +142,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
textAlign: TextAlign.left,
),
),
// 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: () {},
),
],
),
child,
],
);
},
@ -225,8 +205,21 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
),
onChanged: (newValue) {},
),
const SizedBox(
height: 8,
SizedBox(
height: !isDesktop ? 8 : 24,
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Enter passphrase",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
),
ClipRRect(
borderRadius: BorderRadius.circular(
@ -245,6 +238,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
passwordFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
@ -285,8 +280,9 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
const SizedBox(
height: 16,
),
const Spacer(),
TextButton(
if (!isDesktop) const Spacer(),
!isDesktop
? TextButton(
style: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? Theme.of(context)
@ -310,7 +306,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
}
if (!(await File(fileToRestore).exists())) {
showFloatingFlushBar(
await showFloatingFlushBar(
type: FlushBarType.warning,
message: "Backup file does not exist",
context: context,
@ -319,7 +315,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
}
bool shouldPop = false;
showDialog<dynamic>(
await showDialog<dynamic>(
barrierDismissible: false,
context: context,
builder: (_) => WillPopScope(
@ -327,7 +323,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
return shouldPop;
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment:
CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Material(
@ -335,7 +332,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
child: Center(
child: Text(
"Decrypting Stack backup file",
style: STextStyles.pageTitleH2(context)
style:
STextStyles.pageTitleH2(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
@ -371,7 +369,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
passwordController.text = "";
if (jsonString == null) {
showFloatingFlushBar(
await showFloatingFlushBar(
type: FlushBarType.warning,
message: "Failed to decrypt backup file",
context: context,
@ -379,7 +377,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
return;
}
Navigator.of(context).push(
await Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (_) => StackRestoreProgressView(
jsonString: jsonString,
@ -392,6 +390,126 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
"Restore",
style: STextStyles.button(context),
),
)
: Row(
children: [
PrimaryButton(
width: 183,
desktopMed: true,
label: "Restore",
enabled: !(passwordController.text.isEmpty ||
fileLocationController.text.isEmpty),
onPressed: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? null
: () async {
final String fileToRestore =
fileLocationController.text;
final String passphrase =
passwordController.text;
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (!(await File(fileToRestore).exists())) {
await showFloatingFlushBar(
type: FlushBarType.warning,
message: "Backup file does not exist",
context: context,
);
return;
}
bool shouldPop = false;
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,
),
),
],
),
),
);
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(
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/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:url_launcher/url_launcher.dart';
@ -18,10 +21,16 @@ class SupportView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
debugPrint("BUILD: $runtimeType");
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
@ -35,6 +44,10 @@ class SupportView extends StatelessWidget {
),
body: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -44,7 +57,11 @@ class SupportView extends StatelessWidget {
style: STextStyles.smallMed12(context),
),
),
const SizedBox(
isDesktop
? const SizedBox(
height: 24,
)
: const SizedBox(
height: 12,
),
RoundedWhiteContainer(
@ -58,10 +75,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("https://t.me/stackwallet"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -69,6 +88,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.socials.telegram,
@ -88,6 +110,17 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "@stackwallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://t.me/stackwallet"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
@ -105,10 +138,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("https://discord.gg/RZMG3yUm"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -116,6 +151,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.socials.discord,
@ -135,6 +173,18 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "Stack Wallet" : "",
onTap: () {
launchUrl(
Uri.parse(
"https://discord.gg/RZMG3yUm"), //expired link?
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
@ -152,10 +202,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("https://www.reddit.com/r/stackwallet/"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -163,6 +215,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.socials.reddit,
@ -182,6 +237,17 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "r/stackwallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://www.reddit.com/r/stackwallet/"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
@ -199,10 +265,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("https://twitter.com/stack_wallet"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -210,6 +278,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.socials.twitter,
@ -229,6 +300,17 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "@stack_wallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://twitter.com/stack_wallet"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
@ -246,10 +328,12 @@ class SupportView extends StatelessWidget {
),
),
onPressed: () {
if (!isDesktop) {
launchUrl(
Uri.parse("mailto://support@stackwallet.com"),
mode: LaunchMode.externalApplication,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
@ -257,6 +341,9 @@ class SupportView extends StatelessWidget {
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgPicture.asset(
Assets.svg.envelope,
@ -276,12 +363,22 @@ class SupportView extends StatelessWidget {
),
],
),
BlueTextButton(
text: isDesktop ? "support@stackwallet.com" : "",
onTap: () {
launchUrl(
Uri.parse("mailto://support@stackwallet.com"),
mode: LaunchMode.externalApplication,
);
},
),
],
),
),
),
),
],
),
),
);
}
}

View file

@ -1,7 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
class DesktopLoginView extends StatefulWidget {
const DesktopLoginView({
@ -18,19 +26,133 @@ class DesktopLoginView extends StatefulWidget {
}
class _DesktopLoginViewState extends State<DesktopLoginView> {
late final TextEditingController passwordController;
late final FocusNode passwordFocusNode;
bool hidePassword = true;
bool _continueEnabled = false;
@override
void initState() {
passwordController = TextEditingController();
passwordFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
passwordController.dispose();
passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
child: Column(
return DesktopScaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 480,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
Assets.svg.stackIcon(context),
width: 100,
),
const SizedBox(
height: 42,
),
Text(
"Login",
style: STextStyles.desktopH3(context),
"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: "Login",
label: "Continue",
enabled: _continueEnabled,
onPressed: () {
// todo auth
@ -39,7 +161,22 @@ class _DesktopLoginViewState extends State<DesktopLoginView> {
(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_settings_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
@ -37,11 +39,15 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopSettingsView.routeName,
),
Container(
color: Colors.blue,
const Navigator(
key: Key("desktopSupportHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopSupportView.routeName,
),
Container(
color: Colors.pink,
const Navigator(
key: Key("desktopAboutHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: DesktopAboutView.routeName,
),
];

View file

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

View file

@ -64,6 +64,9 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
height: 48,
),
Center(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
@ -72,8 +75,8 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
children: [
TextSpan(
text: "Auto Backup",
style:
STextStyles.desktopTextSmall(context),
style: STextStyles.desktopTextSmall(
context),
),
TextSpan(
text:
@ -81,13 +84,15 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
"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),
.desktopTextExtraExtraSmall(
context),
),
TextSpan(
text:
"\n\nFor more information, please see our website ",
style: STextStyles
.desktopTextExtraExtraSmall(context),
.desktopTextExtraExtraSmall(
context),
),
TextSpan(
text: "stackwallet.com",
@ -98,8 +103,8 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
launchUrl(
Uri.parse(
"https://stackwallet.com/"),
mode:
LaunchMode.externalApplication,
mode: LaunchMode
.externalApplication,
);
},
),
@ -108,6 +113,9 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
),
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -148,6 +156,9 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
alignment: Alignment.topLeft,
),
Center(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
@ -156,8 +167,8 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
children: [
TextSpan(
text: "Manual Backup",
style:
STextStyles.desktopTextSmall(context),
style: STextStyles.desktopTextSmall(
context),
),
TextSpan(
text:
@ -165,22 +176,29 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
"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),
.desktopTextExtraExtraSmall(
context),
),
],
),
),
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(
padding: const EdgeInsets.all(
10,
),
child: createBackup
? const CreateBackupView()
? const SizedBox(
width: 512,
child: CreateBackupView(),
)
: PrimaryButton(
desktopMed: true,
width: 200,
@ -217,6 +235,9 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
alignment: Alignment.topLeft,
),
Center(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
@ -225,30 +246,37 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
children: [
TextSpan(
text: "Restore Backup",
style:
STextStyles.desktopTextSmall(context),
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),
.desktopTextExtraExtraSmall(
context),
),
],
),
),
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(
padding: const EdgeInsets.all(
10,
),
child: restoreBackup
? RestoreFromFileView()
? const SizedBox(
width: 512,
child: RestoreFromFileView(),
)
: PrimaryButton(
desktopMed: true,
width: 200,

View file

@ -1,14 +1,23 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/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/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/progress_bar.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:zxcvbn/zxcvbn.dart';
class CreateAutoBackup extends StatefulWidget {
const CreateAutoBackup({Key? key}) : super(key: key);
@ -22,13 +31,24 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
late final TextEditingController passphraseController;
late final TextEditingController passphraseRepeatController;
late final FocusNode chooseFileLocation;
late final StackFileSystem stackFileSystem;
late final FocusNode passphraseFocusNode;
late final FocusNode passphraseRepeatFocusNode;
final zxcvbn = Zxcvbn();
bool shouldShowPasswordHint = true;
bool hidePassword = true;
String passwordFeedback =
"Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters.";
double passwordStrength = 0.0;
bool get shouldEnableCreate {
return fileLocationController.text.isNotEmpty &&
passphraseController.text.isNotEmpty &&
passphraseRepeatController.text.isNotEmpty;
}
bool get fieldsMatch =>
passphraseController.text == passphraseRepeatController.text;
@ -42,14 +62,26 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
@override
void initState() {
stackFileSystem = StackFileSystem();
fileLocationController = TextEditingController();
passphraseController = TextEditingController();
passphraseRepeatController = TextEditingController();
chooseFileLocation = FocusNode();
passphraseFocusNode = FocusNode();
passphraseRepeatFocusNode = FocusNode();
if (Platform.isAndroid) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final dir = await stackFileSystem.prepareStorage();
if (mounted) {
setState(() {
fileLocationController.text = dir.path;
});
}
});
}
super.initState();
}
@ -59,7 +91,6 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
passphraseController.dispose();
passphraseRepeatController.dispose();
chooseFileLocation.dispose();
passphraseFocusNode.dispose();
passphraseRepeatFocusNode.dispose();
@ -71,9 +102,9 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
debugPrint("BUILD: $runtimeType ");
String? selectedItem = "Every 10 minutes";
final isDesktop = Util.isDesktop;
return DesktopDialog(
maxHeight: 650,
maxHeight: 680,
maxWidth: 600,
child: Column(
children: [
@ -127,89 +158,105 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
height: 10,
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("backupChooseFileLocation"),
focusNode: chooseFileLocation,
controller: fileLocationController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
textAlign: TextAlign.left,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Save to...",
chooseFileLocation,
context,
).copyWith(
labelStyle:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
suffixIcon: Container(
decoration: BoxDecoration(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!Platform.isAndroid)
Consumer(builder: (context, ref, __) {
return Container(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
child: TextField(
autocorrect: false,
enableSuggestions: false,
onTap: Platform.isAndroid
? null
: () async {
try {
await stackFileSystem.prepareStorage();
if (mounted) {
await stackFileSystem.pickDir(context);
}
if (mounted) {
setState(() {
fileLocationController.text =
stackFileSystem.dirPath ?? "";
});
}
} catch (e, s) {
Logging.instance
.log("$e\n$s", level: LogLevel.Error);
}
},
controller: fileLocationController,
style: STextStyles.field(context),
decoration: InputDecoration(
hintText: "Save to...",
hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 20,
height: 17.5,
),
),
width: 16,
height: 16,
),
const SizedBox(
width: 12,
),
],
),
),
),
key: const Key(
"createBackupSaveToFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {
// ref.read(addressEntryDataProvider(widget.id)).address = newValue;
},
),
);
}),
if (!Platform.isAndroid)
const SizedBox(
height: 24,
),
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 32),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Create a passphrase",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
),
const SizedBox(
height: 10,
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
),
child: ClipRRect(
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPassphrase"),
key: const Key("createBackupPasswordFieldKey1"),
focusNode: passphraseFocusNode,
controller: passphraseController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
@ -219,62 +266,133 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
context,
).copyWith(
labelStyle:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: GestureDetector(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createDesktopAutoBackupShowPassphraseButton1"),
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 20,
height: 17.5,
),
),
),
),
),
),
),
width: 16,
height: 16,
),
),
const SizedBox(
height: 16,
width: 12,
),
],
),
),
),
onChanged: (newValue) {
if (newValue.isEmpty) {
setState(() {
passwordFeedback = "";
});
return;
}
final result = zxcvbn.evaluate(newValue);
String suggestionsAndTips = "";
for (var sug in result.feedback.suggestions!.toSet()) {
suggestionsAndTips += "$sug\n";
}
suggestionsAndTips += result.feedback.warning!;
String feedback =
// "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
suggestionsAndTips;
passwordStrength = result.score! / 4;
// hack fix to format back string returned from zxcvbn
if (feedback.contains("phrasesNo need")) {
feedback = feedback.replaceFirst(
"phrasesNo need", "phrases\nNo need");
}
if (feedback.endsWith("\n")) {
feedback = feedback.substring(0, feedback.length - 2);
}
setState(() {
passwordFeedback = feedback;
});
},
),
),
if (passphraseFocusNode.hasFocus ||
passphraseRepeatFocusNode.hasFocus ||
passphraseController.text.isNotEmpty)
Padding(
padding: EdgeInsets.only(
left: 12,
right: 12,
top: passwordFeedback.isNotEmpty ? 4 : 0,
),
child: passwordFeedback.isNotEmpty
? Text(
passwordFeedback,
style: STextStyles.infoSmall(context),
)
: null,
),
if (passphraseFocusNode.hasFocus ||
passphraseRepeatFocusNode.hasFocus ||
passphraseController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
left: 12,
right: 12,
top: 10,
),
child: ClipRRect(
child: ProgressBar(
key: const Key("createStackBackUpProgressBar"),
width: 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("createBackupPassphrase"),
key: const Key("createBackupPasswordFieldKey2"),
focusNode: passphraseRepeatFocusNode,
controller: passphraseRepeatController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
@ -283,42 +401,46 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
passphraseRepeatFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
labelStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: GestureDetector(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createDesktopAutoBackupShowPassphraseButton2"),
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 20,
height: 17.5,
),
),
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
setState(() {});
// TODO: ? check if passwords match?
},
),
),
],
),
),
const SizedBox(
@ -376,6 +498,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
},
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(32),
child: Row(

View file

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

View file

@ -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/wallets_view/wallets_view.dart';
import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart';
import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
@ -99,6 +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/settings_menu.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
@ -996,6 +999,12 @@ class RouteGenerator {
builder: (_) => const CreatePasswordView(),
settings: RouteSettings(name: settings.name));
case ForgotPasswordDesktopView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const ForgotPasswordDesktopView(),
settings: RouteSettings(name: settings.name));
case DesktopHomeView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
@ -1084,6 +1093,18 @@ class RouteGenerator {
builder: (_) => const AdvancedSettings(),
settings: RouteSettings(name: settings.name));
case DesktopSupportView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const DesktopSupportView(),
settings: RouteSettings(name: settings.name));
case DesktopAboutView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const DesktopAboutView(),
settings: RouteSettings(name: settings.name));
case WalletKeysDesktopPopup.routeName:
if (args is List<String>) {
return FadePageRoute(

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import 'package:bitcoindart/bitcoindart.dart';
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:crypto/crypto.dart';
import 'package:decimal/decimal.dart';
import 'package:devicelocale/devicelocale.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';
@ -207,9 +208,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
_getCurrentAddressForChain(0, DerivePathType.bip44);
Future<String>? _currentReceivingAddressP2PKH;
Future<String> get currentReceivingAddressP2SH =>
_currentReceivingAddressP2SH ??=
_getCurrentAddressForChain(0, DerivePathType.bip49);
// Future<String> get currentReceivingAddressP2SH =>
// _currentReceivingAddressP2SH ??=
// _getCurrentAddressForChain(0, DerivePathType.bip49);
Future<String>? _currentReceivingAddressP2SH;
@override
@ -268,7 +269,11 @@ class BitcoinCashWallet extends CoinServiceAPI {
try {
if (bitbox.Address.detectFormat(address) ==
bitbox.Address.formatCashAddr) {
if (validateCashAddr(address)) {
address = bitbox.Address.toLegacyAddress(address);
} else {
throw ArgumentError('$address is not currently supported');
}
}
} catch (e, s) {}
try {
@ -293,13 +298,16 @@ class BitcoinCashWallet extends CoinServiceAPI {
} catch (err) {
// Bech32 decode fail
}
if (_network.bech32 != decodeBech32!.hrp) {
if (decodeBech32 != null) {
if (_network.bech32 != decodeBech32.hrp) {
throw ArgumentError('Invalid prefix or Network mismatch');
}
if (decodeBech32.version != 0) {
throw ArgumentError('Invalid address version');
}
}
}
throw ArgumentError('$address has no matching Script');
}
@ -1154,6 +1162,63 @@ class BitcoinCashWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
TransactionData? cachedTxData;
// hack to add tx to txData before refresh completes
// required based on current app architecture where we don't properly store
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
}
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
bool validateCashAddr(String cashAddr) {
String addr = cashAddr;
if (cashAddr.contains(":")) {
addr = cashAddr.split(":").last;
}
return addr.startsWith("q");
}
@override
bool validateAddress(String address) {
try {
@ -1168,12 +1233,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
}
if (format == bitbox.Address.formatCashAddr) {
String addr = address;
if (address.contains(":")) {
addr = address.split(":").last;
}
return addr.startsWith("q");
return validateCashAddr(address);
} else {
return address.startsWith("1");
}
@ -2036,7 +2096,8 @@ class BitcoinCashWallet extends CoinServiceAPI {
String _convertToScriptHash(String bchAddress, NetworkType network) {
try {
if (bitbox.Address.detectFormat(bchAddress) ==
bitbox.Address.formatCashAddr) {
bitbox.Address.formatCashAddr &&
validateCashAddr(bchAddress)) {
bchAddress = bitbox.Address.toLegacyAddress(bchAddress);
}
final output = Address.addressToOutputScript(bchAddress, network);
@ -2114,7 +2175,8 @@ class BitcoinCashWallet extends CoinServiceAPI {
List<String> allAddressesOld = await _fetchAllOwnAddresses();
List<String> allAddresses = [];
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));
} else {
allAddresses.add(address);
@ -2449,6 +2511,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_tx_model', value: txModel);
cachedTxData = txModel;
return txModel;
}
@ -2832,7 +2895,12 @@ class BitcoinCashWallet extends CoinServiceAPI {
String address = output["scriptPubKey"]["addresses"][0] as String;
if (bitbox.Address.detectFormat(address) ==
bitbox.Address.formatCashAddr) {
if (validateCashAddr(address)) {
address = bitbox.Address.toLegacyAddress(address);
} else {
throw Exception(
"Unsupported address found during fetchBuildTxData(): $address");
}
}
if (!addressTxid.containsKey(address)) {
addressTxid[address] = <String>[];
@ -2863,10 +2931,6 @@ class BitcoinCashWallet extends CoinServiceAPI {
);
for (int i = 0; i < p2pkhLength; i++) {
String address = addressesP2PKH[i];
if (bitbox.Address.detectFormat(address) ==
bitbox.Address.formatCashAddr) {
address = bitbox.Address.toLegacyAddress(address);
}
// receives
final receiveDerivation = receiveDerivations[address];
@ -3376,9 +3440,10 @@ class BitcoinCashWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1190,6 +1190,14 @@ class MoneroWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
// not used in monero
TransactionData? cachedTxData;
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// not used in monero
}
Future<TransactionData> _fetchTransactionData() async {
final transactions = walletBase?.transactionHistory!.transactions;
@ -1376,9 +1384,10 @@ class MoneroWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}

View file

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

View file

@ -1214,6 +1214,14 @@ class WowneroWallet extends CoinServiceAPI {
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
// not used in wownero
TransactionData? cachedTxData;
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// not used in wownero
}
Future<TransactionData> _fetchTransactionData() async {
final transactions = walletBase?.transactionHistory!.transactions;
@ -1401,9 +1409,10 @@ class WowneroWallet extends CoinServiceAPI {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
rethrow;
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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