mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-16 17:27:39 +00:00
Merge branch 'staging' into wownero/25-word
This commit is contained in:
commit
bad88aeecb
39 changed files with 3730 additions and 1178 deletions
|
@ -150,6 +150,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
101
lib/pages_desktop_specific/forgot_password_desktop_view.dart
Normal file
101
lib/pages_desktop_specific/forgot_password_desktop_view.dart
Normal file
|
@ -0,0 +1,101 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
|
||||
class ForgotPasswordDesktopView extends StatefulWidget {
|
||||
const ForgotPasswordDesktopView({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
static const String routeName = "/forgotPasswordDesktop";
|
||||
|
||||
@override
|
||||
State<ForgotPasswordDesktopView> createState() =>
|
||||
_ForgotPasswordDesktopViewState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordDesktopViewState extends State<ForgotPasswordDesktopView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopScaffold(
|
||||
appBar: DesktopAppBar(
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
isCompactHeight: false,
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 480,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.stackIcon(context),
|
||||
width: 100,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 42,
|
||||
),
|
||||
Text(
|
||||
"Stack Wallet",
|
||||
style: STextStyles.desktopH1(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: Text(
|
||||
"Stack Wallet does not store your password. Create new wallet or use a Stack backup file to restore your wallet.",
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.desktopTextSmall(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Create new wallet",
|
||||
onPressed: () {
|
||||
// // todo delete everything and start fresh?
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: "Restore from backup",
|
||||
onPressed: () {
|
||||
// todo SWB restore
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: kDesktopAppBarHeight,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/home/desktop_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,
|
||||
),
|
||||
];
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
@ -0,0 +1,716 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS;
|
||||
import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
const kGithubAPI = "https://api.github.com";
|
||||
const kGithubSearch = "/search/commits";
|
||||
const kGithubHead = "/repos";
|
||||
|
||||
enum CommitStatus { isHead, isOldCommit, notACommit, notLoaded }
|
||||
|
||||
Future<bool> doesCommitExist(
|
||||
String organization,
|
||||
String project,
|
||||
String commit,
|
||||
) async {
|
||||
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
|
||||
final Client client = Client();
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
"$kGithubAPI$kGithubHead/$organization/$project/commits/$commit");
|
||||
|
||||
final commitQuery = await client.get(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
final response = jsonDecode(commitQuery.body.toString());
|
||||
Logging.instance.log("doesCommitExist $project $commit $response",
|
||||
level: LogLevel.Info);
|
||||
bool isThereCommit;
|
||||
try {
|
||||
isThereCommit = response['sha'] == commit;
|
||||
Logging.instance
|
||||
.log("isThereCommit $isThereCommit", level: LogLevel.Info);
|
||||
return isThereCommit;
|
||||
} catch (e, s) {
|
||||
return false;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("$e $s", level: LogLevel.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isHeadCommit(
|
||||
String organization,
|
||||
String project,
|
||||
String branch,
|
||||
String commit,
|
||||
) async {
|
||||
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
|
||||
final Client client = Client();
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
"$kGithubAPI$kGithubHead/$organization/$project/commits/$branch");
|
||||
|
||||
final commitQuery = await client.get(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
final response = jsonDecode(commitQuery.body.toString());
|
||||
Logging.instance.log("isHeadCommit $project $commit $branch $response",
|
||||
level: LogLevel.Info);
|
||||
bool isHead;
|
||||
try {
|
||||
isHead = response['sha'] == commit;
|
||||
Logging.instance.log("isHead $isHead", level: LogLevel.Info);
|
||||
return isHead;
|
||||
} catch (e, s) {
|
||||
return false;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("$e $s", level: LogLevel.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class DesktopAboutView extends ConsumerWidget {
|
||||
const DesktopAboutView({Key? key}) : super(key: key);
|
||||
|
||||
static const String routeName = "/desktopAboutView";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
String firoCommit = FIRO_VERSIONS.getPluginVersion();
|
||||
String epicCashCommit = EPIC_VERSIONS.getPluginVersion();
|
||||
String moneroCommit = MONERO_VERSIONS.getPluginVersion();
|
||||
List<Future> futureFiroList = [
|
||||
doesCommitExist("cypherstack", "flutter_liblelantus", firoCommit),
|
||||
isHeadCommit("cypherstack", "flutter_liblelantus", "main", firoCommit),
|
||||
];
|
||||
Future commitFiroFuture = Future.wait(futureFiroList);
|
||||
List<Future> futureEpicList = [
|
||||
doesCommitExist("cypherstack", "flutter_libepiccash", epicCashCommit),
|
||||
isHeadCommit(
|
||||
"cypherstack", "flutter_libepiccash", "main", epicCashCommit),
|
||||
];
|
||||
Future commitEpicFuture = Future.wait(futureEpicList);
|
||||
List<Future> futureMoneroList = [
|
||||
doesCommitExist("cypherstack", "flutter_libmonero", moneroCommit),
|
||||
isHeadCommit("cypherstack", "flutter_libmonero", "main", moneroCommit),
|
||||
];
|
||||
Future commitMoneroFuture = Future.wait(futureMoneroList);
|
||||
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
return DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: true,
|
||||
leading: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
Text(
|
||||
"About",
|
||||
style: STextStyles.desktopH3(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 24, 35),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RoundedWhiteContainer(
|
||||
width: 929,
|
||||
height: 411,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, top: 10),
|
||||
child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Stack Wallet",
|
||||
style: STextStyles.desktopH3(context),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
RichText(
|
||||
textAlign: TextAlign.start,
|
||||
text: TextSpan(
|
||||
style: STextStyles.label(context),
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
"By using Stack Wallet, you agree to the ",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark3),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Terms of service",
|
||||
style: STextStyles.richLink(context)
|
||||
.copyWith(fontSize: 14),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
"https://stackwallet.com/terms-of-service.html"),
|
||||
mode:
|
||||
LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
TextSpan(
|
||||
text: " and ",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark3),
|
||||
),
|
||||
TextSpan(
|
||||
text: "Privacy policy",
|
||||
style: STextStyles.richLink(context)
|
||||
.copyWith(fontSize: 14),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
"https://stackwallet.com/privacy-policy.html"),
|
||||
mode:
|
||||
LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(right: 10, bottom: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context,
|
||||
AsyncSnapshot<PackageInfo> snapshot) {
|
||||
String version = "";
|
||||
String signature = "";
|
||||
String build = "";
|
||||
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done &&
|
||||
snapshot.hasData) {
|
||||
version = snapshot.data!.version;
|
||||
build = snapshot.data!.buildNumber;
|
||||
signature = snapshot.data!.buildSignature;
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Version",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
SelectableText(
|
||||
version,
|
||||
style:
|
||||
STextStyles.itemSubtitle(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
width: 400,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Build number",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
SelectableText(
|
||||
build,
|
||||
style:
|
||||
STextStyles.itemSubtitle(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Build signature",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
SelectableText(
|
||||
signature,
|
||||
style:
|
||||
STextStyles.itemSubtitle(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
width: 350,
|
||||
),
|
||||
FutureBuilder(
|
||||
future: commitFiroFuture,
|
||||
builder: (context,
|
||||
AsyncSnapshot<dynamic>
|
||||
snapshot) {
|
||||
bool commitExists = false;
|
||||
bool isHead = false;
|
||||
CommitStatus stateOfCommit =
|
||||
CommitStatus.notLoaded;
|
||||
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState
|
||||
.done &&
|
||||
snapshot.hasData) {
|
||||
commitExists = snapshot
|
||||
.data![0] as bool;
|
||||
isHead = snapshot.data![1]
|
||||
as bool;
|
||||
if (commitExists &&
|
||||
isHead) {
|
||||
stateOfCommit =
|
||||
CommitStatus.isHead;
|
||||
} else if (commitExists) {
|
||||
stateOfCommit =
|
||||
CommitStatus
|
||||
.isOldCommit;
|
||||
} else {
|
||||
stateOfCommit =
|
||||
CommitStatus
|
||||
.notACommit;
|
||||
}
|
||||
}
|
||||
TextStyle indicationStyle =
|
||||
STextStyles.itemSubtitle(
|
||||
context);
|
||||
switch (stateOfCommit) {
|
||||
case CommitStatus.isHead:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorGreen);
|
||||
break;
|
||||
case CommitStatus
|
||||
.isOldCommit:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorYellow);
|
||||
break;
|
||||
case CommitStatus
|
||||
.notACommit:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorRed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
children: [
|
||||
Text(
|
||||
"Firo Build Commit",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
SelectableText(
|
||||
firoCommit,
|
||||
style: indicationStyle,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 35),
|
||||
Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: commitEpicFuture,
|
||||
builder: (context,
|
||||
AsyncSnapshot<dynamic>
|
||||
snapshot) {
|
||||
bool commitExists = false;
|
||||
bool isHead = false;
|
||||
CommitStatus stateOfCommit =
|
||||
CommitStatus.notLoaded;
|
||||
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState
|
||||
.done &&
|
||||
snapshot.hasData) {
|
||||
commitExists = snapshot
|
||||
.data![0] as bool;
|
||||
isHead = snapshot.data![1]
|
||||
as bool;
|
||||
if (commitExists &&
|
||||
isHead) {
|
||||
stateOfCommit =
|
||||
CommitStatus.isHead;
|
||||
} else if (commitExists) {
|
||||
stateOfCommit =
|
||||
CommitStatus
|
||||
.isOldCommit;
|
||||
} else {
|
||||
stateOfCommit =
|
||||
CommitStatus
|
||||
.notACommit;
|
||||
}
|
||||
}
|
||||
TextStyle indicationStyle =
|
||||
STextStyles.itemSubtitle(
|
||||
context);
|
||||
switch (stateOfCommit) {
|
||||
case CommitStatus.isHead:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorGreen);
|
||||
break;
|
||||
case CommitStatus
|
||||
.isOldCommit:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorYellow);
|
||||
break;
|
||||
case CommitStatus
|
||||
.notACommit:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorRed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
children: [
|
||||
Text(
|
||||
"Epic Cash Build Commit",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
SelectableText(
|
||||
epicCashCommit,
|
||||
style: indicationStyle,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
const SizedBox(
|
||||
width: 105,
|
||||
),
|
||||
FutureBuilder(
|
||||
future: commitMoneroFuture,
|
||||
builder: (context,
|
||||
AsyncSnapshot<dynamic>
|
||||
snapshot) {
|
||||
bool commitExists = false;
|
||||
bool isHead = false;
|
||||
CommitStatus stateOfCommit =
|
||||
CommitStatus.notLoaded;
|
||||
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState
|
||||
.done &&
|
||||
snapshot.hasData) {
|
||||
commitExists = snapshot
|
||||
.data![0] as bool;
|
||||
isHead = snapshot.data![1]
|
||||
as bool;
|
||||
if (commitExists &&
|
||||
isHead) {
|
||||
stateOfCommit =
|
||||
CommitStatus.isHead;
|
||||
} else if (commitExists) {
|
||||
stateOfCommit =
|
||||
CommitStatus
|
||||
.isOldCommit;
|
||||
} else {
|
||||
stateOfCommit =
|
||||
CommitStatus
|
||||
.notACommit;
|
||||
}
|
||||
}
|
||||
TextStyle indicationStyle =
|
||||
STextStyles.itemSubtitle(
|
||||
context);
|
||||
switch (stateOfCommit) {
|
||||
case CommitStatus.isHead:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorGreen);
|
||||
break;
|
||||
case CommitStatus
|
||||
.isOldCommit:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorYellow);
|
||||
break;
|
||||
case CommitStatus
|
||||
.notACommit:
|
||||
indicationStyle = STextStyles
|
||||
.itemSubtitle(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.accentColorRed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
children: [
|
||||
Text(
|
||||
"Monero Build Commit",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
SelectableText(
|
||||
moneroCommit,
|
||||
style: indicationStyle,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 35),
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Website",
|
||||
style: STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
BlueTextButton(
|
||||
text:
|
||||
"https://stackwallet.com",
|
||||
onTap: () {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
"https://stackwallet.com"),
|
||||
mode: LaunchMode
|
||||
.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/support_view.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||
|
||||
class DesktopSupportView extends ConsumerStatefulWidget {
|
||||
const DesktopSupportView({Key? key}) : super(key: key);
|
||||
|
||||
static const String routeName = "/desktopSupportView";
|
||||
|
||||
@override
|
||||
ConsumerState<DesktopSupportView> createState() => _DesktopSupportView();
|
||||
}
|
||||
|
||||
class _DesktopSupportView extends ConsumerState<DesktopSupportView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
return DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: true,
|
||||
leading: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
Text(
|
||||
"Support",
|
||||
style: STextStyles.desktopH3(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 0, 0),
|
||||
child: Row(
|
||||
children: const [
|
||||
SizedBox(
|
||||
width: 576,
|
||||
child: SupportView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -85,6 +85,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear
|
|||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||
import 'package:stackwallet/pages/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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = () {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue