BIN
asset_sources/other/ios_launch_image/campfire/LaunchImage.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
asset_sources/other/ios_launch_image/campfire/LaunchImage@2x.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
asset_sources/other/ios_launch_image/campfire/LaunchImage@3x.png
Normal file
After Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 16 KiB |
69
asset_sources/svg/campfire/exchange_icons/nanswap.svg
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="75.209999mm"
|
||||
height="75.209999mm"
|
||||
viewBox="0 0 213.19692 213.2"
|
||||
version="1.1"
|
||||
id="svg30"
|
||||
sodipodi:docname="nanswap2.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata36">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>nanswap</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs34" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2635"
|
||||
inkscape:window-height="1461"
|
||||
id="namedview32"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.29390619"
|
||||
inkscape:cx="-79.957486"
|
||||
inkscape:cy="142.12913"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg30" />
|
||||
<title
|
||||
id="title2">nanswap</title>
|
||||
<circle
|
||||
cx="106.6"
|
||||
cy="106.6"
|
||||
r="106.6"
|
||||
id="circle4"
|
||||
style="fill:#4a90e2" />
|
||||
<path
|
||||
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
|
||||
id="path6"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000034" />
|
||||
<path
|
||||
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
|
||||
id="path8"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000034" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
69
asset_sources/svg/stack_duo/exchange_icons/nanswap.svg
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="75.209999mm"
|
||||
height="75.209999mm"
|
||||
viewBox="0 0 213.19692 213.2"
|
||||
version="1.1"
|
||||
id="svg30"
|
||||
sodipodi:docname="nanswap2.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata36">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>nanswap</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs34" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2635"
|
||||
inkscape:window-height="1461"
|
||||
id="namedview32"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.29390619"
|
||||
inkscape:cx="-79.957486"
|
||||
inkscape:cy="142.12913"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg30" />
|
||||
<title
|
||||
id="title2">nanswap</title>
|
||||
<circle
|
||||
cx="106.6"
|
||||
cy="106.6"
|
||||
r="106.6"
|
||||
id="circle4"
|
||||
style="fill:#4a90e2" />
|
||||
<path
|
||||
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
|
||||
id="path6"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000034" />
|
||||
<path
|
||||
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
|
||||
id="path8"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000034" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
69
asset_sources/svg/stack_wallet/exchange_icons/nanswap.svg
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="75.209999mm"
|
||||
height="75.209999mm"
|
||||
viewBox="0 0 213.19692 213.2"
|
||||
version="1.1"
|
||||
id="svg30"
|
||||
sodipodi:docname="nanswap2.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata36">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>nanswap</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs34" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2635"
|
||||
inkscape:window-height="1461"
|
||||
id="namedview32"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.29390619"
|
||||
inkscape:cx="-79.957486"
|
||||
inkscape:cy="142.12913"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg30" />
|
||||
<title
|
||||
id="title2">nanswap</title>
|
||||
<circle
|
||||
cx="106.6"
|
||||
cy="106.6"
|
||||
r="106.6"
|
||||
id="circle4"
|
||||
style="fill:#4a90e2" />
|
||||
<path
|
||||
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
|
||||
id="path6"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000034" />
|
||||
<path
|
||||
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
|
||||
id="path8"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000034" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -116,6 +116,22 @@ cd ..
|
|||
or manually by creating the files referenced in that script with the specified content.
|
||||
|
||||
### Build plugins
|
||||
#### Build script: `build_app.sh`
|
||||
The `build_app.sh` script is use to build applications Stack Wallet. View the script's help message with `./build_app.sh -h` for more information on its usage.
|
||||
|
||||
Options:
|
||||
|
||||
- `a <app>`: Specify the application ID (required). Valid options are `stack_wallet` or `stack_duo`.
|
||||
- `b <build_number>`: Specify the build number in 123 (required).
|
||||
- `p <platform>`: Specify the platform to build for (required). Valid options are `android`, `ios`, `macos`, `linux`, or `windows`.
|
||||
- `v <version>`: Specify the version of the application in 1.2.3 format (required).
|
||||
- `i`: Optional flag to skip building crypto plugins. Useful for updating `pubspec.yaml` and white-labelling different apps with the same plugins.
|
||||
|
||||
For example,
|
||||
```
|
||||
./build_app.sh -a stack_wallet -p linux -v 2.1.0 -b 210
|
||||
```
|
||||
|
||||
#### Building plugins for Android
|
||||
> Warning: This will take a long time, please be patient
|
||||
```
|
||||
|
|
3
ios/.gitignore
vendored
|
@ -30,3 +30,6 @@ Runner/GeneratedPluginRegistrant.*
|
|||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
|
||||
# app specific, handled by scripts
|
||||
Runner/Assets.xcassets/LaunchImage.imageset/*.png
|
||||
|
|
|
@ -15,6 +15,8 @@ abstract class AppConfig {
|
|||
static const prefix = _prefix;
|
||||
static const suffix = _suffix;
|
||||
|
||||
static const emptyWalletsMessage = _emptyWalletsMessage;
|
||||
|
||||
static String get appDefaultDataDirName => _appDataDirName;
|
||||
static String get shortDescriptionText => _shortDescriptionText;
|
||||
static String get commitHash => _commitHash;
|
||||
|
|
21
lib/models/keys/cw_key_data.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
import 'key_data_interface.dart';
|
||||
|
||||
class CWKeyData with KeyDataInterface {
|
||||
CWKeyData({
|
||||
required this.walletId,
|
||||
required String? privateSpendKey,
|
||||
required String? privateViewKey,
|
||||
required String? publicSpendKey,
|
||||
required String? publicViewKey,
|
||||
}) : keys = List.unmodifiable([
|
||||
(label: "Public View Key", key: publicViewKey),
|
||||
(label: "Private View Key", key: privateViewKey),
|
||||
(label: "Public Spend Key", key: publicSpendKey),
|
||||
(label: "Private Spend Key", key: privateSpendKey),
|
||||
]);
|
||||
|
||||
@override
|
||||
final String walletId;
|
||||
|
||||
final List<({String label, String key})> keys;
|
||||
}
|
3
lib/models/keys/key_data_interface.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
mixin KeyDataInterface {
|
||||
String get walletId;
|
||||
}
|
17
lib/models/keys/xpriv_data.dart
Normal file
|
@ -0,0 +1,17 @@
|
|||
import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||
import 'key_data_interface.dart';
|
||||
|
||||
class XPrivData with KeyDataInterface {
|
||||
XPrivData({
|
||||
required this.walletId,
|
||||
required this.fingerprint,
|
||||
required List<XPriv> xprivs,
|
||||
}) : xprivs = List.unmodifiable(xprivs);
|
||||
|
||||
@override
|
||||
final String walletId;
|
||||
|
||||
final String fingerprint;
|
||||
|
||||
final List<XPriv> xprivs;
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../../app_config.dart';
|
||||
import '../../../../../frost_route_generator.dart';
|
||||
import '../../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../../pages_desktop_specific/desktop_home_view.dart';
|
||||
|
@ -45,7 +46,7 @@ class _FrostCreateStep5State extends ConsumerState<FrostCreateStep5> {
|
|||
static const _warning = "These are your private keys. Please back them up, "
|
||||
"keep them safe and never share it with anyone. Your private keys are the"
|
||||
" only way you can access your funds if you forget PIN, lose your phone, "
|
||||
"etc. Stack Wallet does not keep nor is able to restore your private keys"
|
||||
"etc. ${AppConfig.prefix} does not keep nor is able to restore your private keys"
|
||||
".";
|
||||
|
||||
late final String seed, recoveryString, serializedKeys, multisigConfig;
|
||||
|
|
|
@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../../app_config.dart';
|
||||
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import '../../../providers/db/main_db_provider.dart';
|
||||
import '../../../providers/global/secure_store_provider.dart';
|
||||
|
@ -173,7 +174,7 @@ class _NewWalletRecoveryPhraseWarningViewState
|
|||
"write it down. Keep it safe and never share it with "
|
||||
"anyone. Your recovery phrase is the only way you can"
|
||||
" access your funds if you forget your PIN, lose your"
|
||||
" phone, etc.\n\nStack Wallet does not keep nor is "
|
||||
" phone, etc.\n\n${AppConfig.appName} does not keep nor is "
|
||||
"able to restore your recover phrase. Only you have "
|
||||
"access to your wallet.",
|
||||
style: isDesktop
|
||||
|
@ -427,7 +428,7 @@ class _NewWalletRecoveryPhraseWarningViewState
|
|||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
"I understand that Stack Wallet does not keep and cannot restore my recovery phrase, and If I lose my recovery phrase, I will not be able to access my funds.",
|
||||
"I understand that ${AppConfig.appName} does not keep and cannot restore my recovery phrase, and If I lose my recovery phrase, I will not be able to access my funds.",
|
||||
style: isDesktop
|
||||
? STextStyles.desktopTextMedium(
|
||||
context,
|
||||
|
|
|
@ -405,17 +405,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
|
|||
color: Colors.transparent,
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxTextButton(
|
||||
label: "Scan for Lelantus transactions",
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
enableLelantusScanning = newValue ?? true;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (coin is Firo)
|
||||
CheckboxTextButton(
|
||||
label: "Scan for Lelantus transactions",
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
enableLelantusScanning = newValue ?? true;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (coin is Firo)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
|
@ -500,6 +502,9 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1155,7 +1155,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
|
|||
),
|
||||
if (AppConfig.isStackCoin(selectedCrypto?.ticker))
|
||||
CustomTextButton(
|
||||
text: "Choose from Stack",
|
||||
text: "Choose from ${AppConfig.prefix}",
|
||||
onTap: () {
|
||||
try {
|
||||
final coin = AppConfig.getCryptoCurrencyForTicker(
|
||||
|
|
|
@ -8,11 +8,15 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../models/buy/response_objects/order.dart';
|
||||
import '../../notifications/show_flush_bar.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../themes/theme_providers.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
|
@ -44,6 +48,16 @@ class _BuyOrderDetailsViewState extends ConsumerState<BuyOrderDetailsView> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final orderDetails = '''
|
||||
Purchase ID: ${widget.order.paymentId}
|
||||
User ID: ${widget.order.userId}
|
||||
Quote ID: ${widget.order.quote.id}
|
||||
Quoted cost: ${widget.order.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.order.quote.fiat.ticker.toUpperCase()}
|
||||
Quoted amount: ${widget.order.quote.youReceiveCryptoAmount} ${widget.order.quote.crypto.ticker.toUpperCase()}
|
||||
Receiving ${widget.order.quote.crypto.ticker.toUpperCase()} address: ${widget.order.quote.receivingAddress}
|
||||
Provider: Simplex
|
||||
''';
|
||||
|
||||
return ConditionalParent(
|
||||
condition: !isDesktop,
|
||||
builder: (child) {
|
||||
|
@ -272,6 +286,43 @@ class _BuyOrderDetailsViewState extends ConsumerState<BuyOrderDetailsView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: orderDetails));
|
||||
if (context.mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.copy,
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.buttonTextSecondary,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
"Copy to clipboard",
|
||||
style: STextStyles.desktopButtonSecondaryEnabled(
|
||||
context,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
PrimaryButton(
|
||||
label: "Dismiss",
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
import '../../pages_desktop_specific/cashfusion/sub_widgets/fusion_progress.dart';
|
||||
import '../../providers/cash_fusion/fusion_progress_ui_state_provider.dart';
|
||||
|
@ -84,6 +85,8 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
|
|||
message: "Stopping fusion",
|
||||
);
|
||||
|
||||
await Wakelock.disable();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -96,6 +99,12 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Wakelock.disable();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool _succeeded =
|
||||
|
@ -108,6 +117,8 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
|
|||
.watch(fusionProgressUIStateProvider(widget.walletId))
|
||||
.fusionRoundsCompleted;
|
||||
|
||||
Wakelock.enable();
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return await _requestAndProcessCancel();
|
||||
|
|
|
@ -22,6 +22,7 @@ import '../../../services/exchange/change_now/change_now_exchange.dart';
|
|||
import '../../../services/exchange/exchange.dart';
|
||||
import '../../../services/exchange/exchange_data_loading_service.dart';
|
||||
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import '../../../services/exchange/nanswap/nanswap_exchange.dart';
|
||||
import '../../../services/exchange/trocador/trocador_exchange.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
import '../../../utilities/assets.dart';
|
||||
|
@ -117,6 +118,8 @@ class _ExchangeCurrencySelectionViewState
|
|||
.exchangeNameEqualTo(MajesticBankExchange.exchangeName)
|
||||
.or()
|
||||
.exchangeNameStartsWith(TrocadorExchange.exchangeName)
|
||||
.or()
|
||||
.exchangeNameStartsWith(NanswapExchange.exchangeName)
|
||||
.findAll();
|
||||
|
||||
final cn = await ChangeNowExchange.instance.getPairedCurrencies(
|
||||
|
|
|
@ -33,6 +33,7 @@ import '../../services/exchange/exchange.dart';
|
|||
import '../../services/exchange/exchange_data_loading_service.dart';
|
||||
import '../../services/exchange/exchange_response.dart';
|
||||
import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import '../../services/exchange/nanswap/nanswap_exchange.dart';
|
||||
import '../../services/exchange/trocador/trocador_exchange.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/amount/amount_unit.dart';
|
||||
|
@ -87,6 +88,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
|
|||
MajesticBankExchange.instance,
|
||||
ChangeNowExchange.instance,
|
||||
TrocadorExchange.instance,
|
||||
NanswapExchange.instance,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import '../../../app_config.dart';
|
||||
import '../../../models/exchange/incomplete_exchange.dart';
|
||||
import '../../../providers/providers.dart';
|
||||
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
import '../../../utilities/address_utils.dart';
|
||||
import '../../../utilities/barcode_scanner_interface.dart';
|
||||
|
@ -126,8 +125,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final supportsRefund =
|
||||
ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName;
|
||||
final supportsRefund = ref.watch(efExchangeProvider).supportsRefundAddress;
|
||||
|
||||
return Background(
|
||||
child: Scaffold(
|
||||
|
@ -196,7 +194,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
|
|||
),
|
||||
if (AppConfig.isStackCoin(model.receiveTicker))
|
||||
CustomTextButton(
|
||||
text: "Choose from Stack",
|
||||
text: "Choose from ${AppConfig.prefix}",
|
||||
onTap: () {
|
||||
try {
|
||||
final coin = AppConfig.coins.firstWhere(
|
||||
|
@ -482,7 +480,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
|
|||
),
|
||||
if (AppConfig.isStackCoin(model.sendTicker))
|
||||
CustomTextButton(
|
||||
text: "Choose from Stack",
|
||||
text: "Choose from ${AppConfig.prefix}",
|
||||
onTap: () {
|
||||
try {
|
||||
final coin = AppConfig.coins.firstWhere(
|
||||
|
|
|
@ -18,7 +18,6 @@ import '../../../models/exchange/response_objects/trade.dart';
|
|||
import '../../../providers/global/trades_service_provider.dart';
|
||||
import '../../../providers/providers.dart';
|
||||
import '../../../services/exchange/exchange_response.dart';
|
||||
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import '../../../services/notifications_api.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
import '../../../utilities/assets.dart';
|
||||
|
@ -63,8 +62,7 @@ class _Step3ViewState extends ConsumerState<Step3View> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final supportsRefund =
|
||||
ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName;
|
||||
final supportsRefund = ref.watch(efExchangeProvider).supportsRefundAddress;
|
||||
|
||||
return Background(
|
||||
child: Scaffold(
|
||||
|
|
|
@ -14,10 +14,9 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../app_config.dart';
|
||||
import '../../models/exchange/response_objects/trade.dart';
|
||||
import 'confirm_change_now_send.dart';
|
||||
import '../home_view/home_view.dart';
|
||||
import '../send_view/sub_widgets/building_transaction_dialog.dart';
|
||||
import '../../pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../../route_generator.dart';
|
||||
|
@ -43,6 +42,9 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart';
|
|||
import '../../widgets/expandable.dart';
|
||||
import '../../widgets/rounded_white_container.dart';
|
||||
import '../../widgets/stack_dialog.dart';
|
||||
import '../home_view/home_view.dart';
|
||||
import '../send_view/sub_widgets/building_transaction_dialog.dart';
|
||||
import 'confirm_change_now_send.dart';
|
||||
|
||||
class SendFromView extends ConsumerStatefulWidget {
|
||||
const SendFromView({
|
||||
|
@ -135,7 +137,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> {
|
|||
left: 32,
|
||||
),
|
||||
child: Text(
|
||||
"Send from Stack",
|
||||
"Send from ${AppConfig.prefix}",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -10,17 +10,19 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../models/exchange/aggregate_currency.dart';
|
||||
import 'exchange_provider_option.dart';
|
||||
import '../../../providers/providers.dart';
|
||||
import '../../../services/exchange/change_now/change_now_exchange.dart';
|
||||
import '../../../services/exchange/exchange.dart';
|
||||
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import '../../../services/exchange/nanswap/nanswap_exchange.dart';
|
||||
import '../../../services/exchange/trocador/trocador_exchange.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
import '../../../utilities/prefs.dart';
|
||||
import '../../../utilities/util.dart';
|
||||
import '../../../widgets/rounded_white_container.dart';
|
||||
import 'exchange_provider_option.dart';
|
||||
|
||||
class ExchangeProviderOptions extends ConsumerStatefulWidget {
|
||||
const ExchangeProviderOptions({
|
||||
|
@ -88,6 +90,11 @@ class _ExchangeProviderOptionsState
|
|||
sendCurrency: sendCurrency,
|
||||
receiveCurrency: receivingCurrency,
|
||||
);
|
||||
final showNanswap = exchangeSupported(
|
||||
exchangeName: NanswapExchange.exchangeName,
|
||||
sendCurrency: sendCurrency,
|
||||
receiveCurrency: receivingCurrency,
|
||||
);
|
||||
|
||||
return RoundedWhiteContainer(
|
||||
padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12),
|
||||
|
@ -134,6 +141,23 @@ class _ExchangeProviderOptionsState
|
|||
reversed: widget.reversed,
|
||||
exchange: TrocadorExchange.instance,
|
||||
),
|
||||
if ((showChangeNow || showMajesticBank || showTrocador) &&
|
||||
showNanswap)
|
||||
isDesktop
|
||||
? Container(
|
||||
height: 1,
|
||||
color:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
)
|
||||
: const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (showNanswap)
|
||||
ExchangeOption(
|
||||
fixedRate: widget.fixedRate,
|
||||
reversed: widget.reversed,
|
||||
exchange: NanswapExchange.instance,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -30,6 +30,7 @@ import '../../route_generator.dart';
|
|||
import '../../services/exchange/change_now/change_now_exchange.dart';
|
||||
import '../../services/exchange/exchange.dart';
|
||||
import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import '../../services/exchange/nanswap/nanswap_exchange.dart';
|
||||
import '../../services/exchange/simpleswap/simpleswap_exchange.dart';
|
||||
import '../../services/exchange/trocador/trocador_exchange.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
|
@ -263,7 +264,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
|
|||
trade.status == "waiting" ||
|
||||
trade.status == "Waiting"))
|
||||
SecondaryButton(
|
||||
label: "Send from Stack",
|
||||
label: "Send from ${AppConfig.prefix}",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
CryptoCurrency coin;
|
||||
|
@ -1330,6 +1331,10 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
|
|||
url =
|
||||
"https://majesticbank.sc/track?trx=${trade.tradeId}";
|
||||
break;
|
||||
case NanswapExchange.exchangeName:
|
||||
url =
|
||||
"https://nanswap.com/transaction/${trade.tradeId}";
|
||||
break;
|
||||
|
||||
default:
|
||||
if (trade.exchangeName
|
||||
|
@ -1374,7 +1379,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
|
|||
trade.status == "waiting" ||
|
||||
trade.status == "Waiting"))
|
||||
SecondaryButton(
|
||||
label: "Send from Stack",
|
||||
label: "Send from ${AppConfig.prefix}",
|
||||
onPressed: () {
|
||||
CryptoCurrency coin;
|
||||
try {
|
||||
|
|
|
@ -49,6 +49,8 @@ class _IntroViewState extends ConsumerState<IntroView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType ");
|
||||
final stack =
|
||||
ref.watch(themeProvider.select((value) => value.assets.stack));
|
||||
return Background(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
|
@ -68,16 +70,22 @@ class _IntroViewState extends ConsumerState<IntroView> {
|
|||
constraints: const BoxConstraints(
|
||||
maxWidth: 300,
|
||||
),
|
||||
child: SvgPicture.file(
|
||||
File(
|
||||
ref.watch(
|
||||
themeProvider.select(
|
||||
(value) => value.assets.stack,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: isDesktop ? 324 : 266,
|
||||
height: isDesktop ? 324 : 266,
|
||||
child: (stack.endsWith(".png"))
|
||||
? Image.file(
|
||||
File(
|
||||
stack,
|
||||
),
|
||||
)
|
||||
: SvgPicture.file(
|
||||
File(
|
||||
stack,
|
||||
),
|
||||
width: isDesktop ? 324 : 266,
|
||||
height: isDesktop ? 324 : 266,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -163,7 +171,7 @@ class _IntroViewState extends ConsumerState<IntroView> {
|
|||
),
|
||||
if (isDesktop)
|
||||
SecondaryButton(
|
||||
label: "Restore from Stack backup",
|
||||
label: "Restore from ${AppConfig.prefix} backup",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
CreatePasswordView.routeName,
|
||||
|
@ -306,7 +314,7 @@ class GetStartedButton extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
child: Text(
|
||||
"Create new Stack",
|
||||
"Create new ${AppConfig.prefix}",
|
||||
style: STextStyles.button(context).copyWith(fontSize: 20),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -188,12 +188,14 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
|
|||
_timeout = Duration.zero;
|
||||
|
||||
_checkUseBiometrics();
|
||||
_pinTextController.addListener(_onPinChanged);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
// _shakeController.dispose();
|
||||
_pinTextController.removeListener(_onPinChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -208,13 +210,27 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
|
|||
);
|
||||
}
|
||||
|
||||
final _pinTextController = TextEditingController();
|
||||
final FocusNode _pinFocusNode = FocusNode();
|
||||
|
||||
late SecureStorageInterface _secureStore;
|
||||
late Biometrics biometrics;
|
||||
int pinCount = 1;
|
||||
|
||||
final _pinTextController = TextEditingController();
|
||||
|
||||
void _onPinChanged() async {
|
||||
String enteredPin = _pinTextController.text;
|
||||
final storedPin = await _secureStore.read(key: 'stack_pin');
|
||||
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
|
||||
|
||||
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) {
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
);
|
||||
unawaited(_onUnlock());
|
||||
}
|
||||
}
|
||||
|
||||
Widget get _body => Background(
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
|
|
292
lib/pages/pinpad_views/pinpad_dialog.dart
Normal file
|
@ -0,0 +1,292 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../notifications/show_flush_bar.dart';
|
||||
import '../../providers/global/prefs_provider.dart';
|
||||
import '../../providers/global/secure_store_provider.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
import '../../utilities/biometrics.dart';
|
||||
import '../../utilities/flutter_secure_storage_interface.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
import '../../widgets/custom_pin_put/custom_pin_put.dart';
|
||||
import '../../widgets/shake/shake.dart';
|
||||
import '../../widgets/stack_dialog.dart';
|
||||
|
||||
class PinpadDialog extends ConsumerStatefulWidget {
|
||||
const PinpadDialog({
|
||||
super.key,
|
||||
required this.biometricsAuthenticationTitle,
|
||||
required this.biometricsLocalizedReason,
|
||||
required this.biometricsCancelButtonString,
|
||||
this.biometrics = const Biometrics(),
|
||||
this.customKeyLabel = "Button",
|
||||
});
|
||||
|
||||
final String biometricsAuthenticationTitle;
|
||||
final String biometricsLocalizedReason;
|
||||
final String biometricsCancelButtonString;
|
||||
final Biometrics biometrics;
|
||||
final String customKeyLabel;
|
||||
|
||||
@override
|
||||
ConsumerState<PinpadDialog> createState() => _PinpadDialogState();
|
||||
}
|
||||
|
||||
class _PinpadDialogState extends ConsumerState<PinpadDialog> {
|
||||
late final ShakeController _shakeController;
|
||||
|
||||
late int _attempts;
|
||||
bool _attemptLock = false;
|
||||
late Duration _timeout;
|
||||
static const maxAttemptsBeforeThrottling = 3;
|
||||
Timer? _timer;
|
||||
|
||||
final FocusNode _pinFocusNode = FocusNode();
|
||||
|
||||
late SecureStorageInterface _secureStore;
|
||||
late Biometrics biometrics;
|
||||
int pinCount = 1;
|
||||
|
||||
final _pinTextController = TextEditingController();
|
||||
|
||||
BoxDecoration get _pinPutDecoration {
|
||||
return BoxDecoration(
|
||||
color: Theme.of(context).extension<StackColors>()!.infoItemIcons,
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Theme.of(context).extension<StackColors>()!.infoItemIcons,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPinChanged() async {
|
||||
final enteredPin = _pinTextController.text;
|
||||
final storedPin = await _secureStore.read(key: 'stack_pin');
|
||||
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
|
||||
|
||||
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) {
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
);
|
||||
unawaited(_onUnlock());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUnlock() async {
|
||||
final now = DateTime.now().toUtc();
|
||||
ref.read(prefsChangeNotifierProvider).lastUnlocked =
|
||||
now.millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
Navigator.of(context).pop("verified success");
|
||||
}
|
||||
|
||||
Future<void> _checkUseBiometrics() async {
|
||||
if (!ref.read(prefsChangeNotifierProvider).isInitialized) {
|
||||
await ref.read(prefsChangeNotifierProvider).init();
|
||||
}
|
||||
|
||||
final bool useBiometrics =
|
||||
ref.read(prefsChangeNotifierProvider).useBiometrics;
|
||||
|
||||
final title = widget.biometricsAuthenticationTitle;
|
||||
final localizedReason = widget.biometricsLocalizedReason;
|
||||
final cancelButtonText = widget.biometricsCancelButtonString;
|
||||
|
||||
if (useBiometrics) {
|
||||
if (await biometrics.authenticate(
|
||||
title: title,
|
||||
localizedReason: localizedReason,
|
||||
cancelButtonText: cancelButtonText,
|
||||
)) {
|
||||
unawaited(_onUnlock());
|
||||
}
|
||||
// leave this commented to enable pin fall back should biometrics not work properly
|
||||
// else {
|
||||
// Navigator.pop(context);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSubmit(String pin) async {
|
||||
_attempts++;
|
||||
|
||||
if (_attempts > maxAttemptsBeforeThrottling) {
|
||||
_attemptLock = true;
|
||||
switch (_attempts) {
|
||||
case 4:
|
||||
_timeout = const Duration(seconds: 30);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
_timeout = const Duration(seconds: 60);
|
||||
break;
|
||||
|
||||
case 6:
|
||||
_timeout = const Duration(minutes: 5);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
_timeout = const Duration(minutes: 10);
|
||||
break;
|
||||
|
||||
case 8:
|
||||
_timeout = const Duration(minutes: 20);
|
||||
break;
|
||||
|
||||
case 9:
|
||||
_timeout = const Duration(minutes: 30);
|
||||
break;
|
||||
|
||||
default:
|
||||
_timeout = const Duration(minutes: 60);
|
||||
}
|
||||
|
||||
_timer?.cancel();
|
||||
_timer = Timer(_timeout, () {
|
||||
_attemptLock = false;
|
||||
_attempts = 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (_attemptLock) {
|
||||
String prettyTime = "";
|
||||
if (_timeout.inSeconds >= 60) {
|
||||
prettyTime += "${_timeout.inMinutes} minutes";
|
||||
} else {
|
||||
prettyTime += "${_timeout.inSeconds} seconds";
|
||||
}
|
||||
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message:
|
||||
"Incorrect PIN entered too many times. Please wait $prettyTime",
|
||||
context: context,
|
||||
iconAsset: Assets.svg.alertCircle,
|
||||
),
|
||||
);
|
||||
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
_pinTextController.text = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final storedPin = await _secureStore.read(key: 'stack_pin');
|
||||
|
||||
if (mounted) {
|
||||
if (storedPin == pin) {
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
);
|
||||
unawaited(_onUnlock());
|
||||
} else {
|
||||
unawaited(_shakeController.shake());
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message: "Incorrect PIN. Please try again",
|
||||
context: context,
|
||||
iconAsset: Assets.svg.alertCircle,
|
||||
),
|
||||
);
|
||||
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
_pinTextController.text = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_shakeController = ShakeController();
|
||||
|
||||
_secureStore = ref.read(secureStoreProvider);
|
||||
biometrics = widget.biometrics;
|
||||
_attempts = 0;
|
||||
_timeout = Duration.zero;
|
||||
|
||||
_checkUseBiometrics();
|
||||
_pinTextController.addListener(_onPinChanged);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
// _shakeController.dispose();
|
||||
_pinTextController.removeListener(_onPinChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StackDialogBase(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Shake(
|
||||
animationDuration: const Duration(milliseconds: 700),
|
||||
animationRange: 12,
|
||||
controller: _shakeController,
|
||||
child: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
"Enter PIN",
|
||||
style: STextStyles.pageTitleH1(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
CustomPinPut(
|
||||
fieldsCount: pinCount,
|
||||
eachFieldHeight: 12,
|
||||
eachFieldWidth: 12,
|
||||
textStyle: STextStyles.label(context).copyWith(
|
||||
fontSize: 1,
|
||||
),
|
||||
focusNode: _pinFocusNode,
|
||||
controller: _pinTextController,
|
||||
useNativeKeyboard: false,
|
||||
obscureText: "",
|
||||
inputDecoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
fillColor:
|
||||
Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
counterText: "",
|
||||
),
|
||||
submittedFieldDecoration: _pinPutDecoration,
|
||||
isRandom:
|
||||
ref.read(prefsChangeNotifierProvider).randomizePIN,
|
||||
onSubmit: _onSubmit,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import '../../../utilities/address_utils.dart';
|
|||
import '../../../utilities/text_styles.dart';
|
||||
import '../../../utilities/util.dart';
|
||||
import '../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../../widgets/address_private_key.dart';
|
||||
import '../../../widgets/background.dart';
|
||||
import '../../../widgets/conditional_parent.dart';
|
||||
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
|
@ -30,6 +31,7 @@ import '../../../widgets/custom_buttons/simple_copy_button.dart';
|
|||
import '../../../widgets/custom_buttons/simple_edit_button.dart';
|
||||
import '../../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import '../../../widgets/detail_item.dart';
|
||||
import '../../../widgets/qr.dart';
|
||||
import '../../../widgets/rounded_white_container.dart';
|
||||
import '../../../widgets/transaction_card.dart';
|
||||
|
@ -298,9 +300,9 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
_Item(
|
||||
DetailItem(
|
||||
title: "Address",
|
||||
data: address.value,
|
||||
detail: address.value,
|
||||
button: isDesktop
|
||||
? IconCopyButton(
|
||||
data: address.value,
|
||||
|
@ -312,9 +314,9 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
const _Div(
|
||||
height: 12,
|
||||
),
|
||||
_Item(
|
||||
DetailItem(
|
||||
title: "Label",
|
||||
data: label!.value,
|
||||
detail: label!.value,
|
||||
button: SimpleEditButton(
|
||||
editValue: label!.value,
|
||||
editLabel: 'label',
|
||||
|
@ -338,9 +340,9 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
height: 12,
|
||||
),
|
||||
if (address.derivationPath != null)
|
||||
_Item(
|
||||
DetailItem(
|
||||
title: "Derivation path",
|
||||
data: address.derivationPath!.value,
|
||||
detail: address.derivationPath!.value,
|
||||
button: Container(),
|
||||
),
|
||||
if (address.type == AddressType.spark)
|
||||
|
@ -348,27 +350,34 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
height: 12,
|
||||
),
|
||||
if (address.type == AddressType.spark)
|
||||
_Item(
|
||||
DetailItem(
|
||||
title: "Diversifier",
|
||||
data: address.derivationIndex.toString(),
|
||||
detail: address.derivationIndex.toString(),
|
||||
button: Container(),
|
||||
),
|
||||
const _Div(
|
||||
height: 12,
|
||||
),
|
||||
_Item(
|
||||
DetailItem(
|
||||
title: "Type",
|
||||
data: address.type.readableName,
|
||||
detail: address.type.readableName,
|
||||
button: Container(),
|
||||
),
|
||||
const _Div(
|
||||
height: 12,
|
||||
),
|
||||
_Item(
|
||||
DetailItem(
|
||||
title: "Sub type",
|
||||
data: address.subType.prettyName,
|
||||
detail: address.subType.prettyName,
|
||||
button: Container(),
|
||||
),
|
||||
const _Div(
|
||||
height: 12,
|
||||
),
|
||||
AddressPrivateKey(
|
||||
walletId: widget.walletId,
|
||||
address: address,
|
||||
),
|
||||
if (!isDesktop)
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
|
@ -631,64 +640,3 @@ class _Tags extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Item extends StatelessWidget {
|
||||
const _Item({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.data,
|
||||
required this.button,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String data;
|
||||
final Widget button;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: !Util.isDesktop,
|
||||
builder: (child) => RoundedWhiteContainer(
|
||||
child: child,
|
||||
),
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: child,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
button,
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
data.isNotEmpty
|
||||
? SelectableText(
|
||||
data,
|
||||
style: STextStyles.w500_14(context),
|
||||
)
|
||||
: Text(
|
||||
"$title will appear here",
|
||||
style: STextStyles.w500_14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,7 +175,7 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
|
|||
PrimaryButton(
|
||||
label: "Generate transaction",
|
||||
enabled: _userVerifyContinue &&
|
||||
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||
!fieldIsEmptyFlags.fold(false, (v, e) => v |= e),
|
||||
onPressed: () async {
|
||||
// collect Share strings
|
||||
final sharesCollected = controllers.map((e) => e.text).toList();
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../providers/global/prefs_provider.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
|
@ -218,7 +219,7 @@ class AdvancedSettingsView extends StatelessWidget {
|
|||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Stack Experience",
|
||||
text: "${AppConfig.prefix} Experience",
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
TextSpan(
|
||||
|
|
|
@ -24,6 +24,7 @@ import 'package:flutter_svg/svg.dart';
|
|||
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../models/isar/models/log.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../providers/global/debug_service_provider.dart';
|
||||
|
@ -421,7 +422,7 @@ class _DebugViewState extends ConsumerState<DebugView> {
|
|||
},
|
||||
child: CustomLoadingOverlay(
|
||||
message:
|
||||
"Generating Stack logs file",
|
||||
"Generating ${AppConfig.prefix} logs file",
|
||||
eventBus: eventBus,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/block_explorers.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
|
@ -91,7 +92,7 @@ class _ManageExplorerViewState extends ConsumerState<ManageExplorerView> {
|
|||
"every block explorer has a slightly different URL "
|
||||
"scheme.\n\nPaste in your block explorer of choice,"
|
||||
" then edit in [TXID] where the transaction ID "
|
||||
"should go, and Stack Wallet will auto fill the "
|
||||
"should go, and ${AppConfig.appName} will auto fill the "
|
||||
"transaction ID in that place of URL.",
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
|
|
|
@ -99,7 +99,7 @@ class GlobalSettingsView extends StatelessWidget {
|
|||
SettingsListButton(
|
||||
iconAssetName: Assets.svg.downloadFolder,
|
||||
iconSize: 14,
|
||||
title: "Stack backup & restore",
|
||||
title: "${AppConfig.prefix} backup & restore",
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
@ -113,9 +113,9 @@ class GlobalSettingsView extends StatelessWidget {
|
|||
biometricsCancelButtonString:
|
||||
"CANCEL",
|
||||
biometricsLocalizedReason:
|
||||
"Authenticate to access Stack backup & restore settings",
|
||||
"Authenticate to access ${AppConfig.prefix} backup & restore settings",
|
||||
biometricsAuthenticationTitle:
|
||||
"Stack backup",
|
||||
"${AppConfig.prefix} backup",
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name: "/swblockscreen",
|
||||
|
|
|
@ -61,9 +61,12 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> {
|
|||
|
||||
int pinCount = 1;
|
||||
|
||||
final TextEditingController _pinTextController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_secureStore = ref.read(secureStoreProvider);
|
||||
_pinTextController.addListener(_onPinChanged);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -74,9 +77,23 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> {
|
|||
_pinPutController2.dispose();
|
||||
_pinPutFocusNode1.dispose();
|
||||
_pinPutFocusNode2.dispose();
|
||||
_pinTextController.removeListener(_onPinChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onPinChanged() async {
|
||||
String enteredPin = _pinTextController.text;
|
||||
final storedPin = await _secureStore.read(key: 'stack_pin');
|
||||
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
|
||||
|
||||
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) {
|
||||
await _pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Background(
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../pinpad_views/lock_screen_view.dart';
|
||||
import 'change_pin_view/change_pin_view.dart';
|
||||
|
||||
import '../../../../providers/global/prefs_provider.dart';
|
||||
import '../../../../route_generator.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
|
@ -21,6 +20,8 @@ import '../../../../widgets/background.dart';
|
|||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
import '../../../pinpad_views/lock_screen_view.dart';
|
||||
import 'change_pin_view/change_pin_view.dart';
|
||||
|
||||
class SecurityView extends StatelessWidget {
|
||||
const SecurityView({
|
||||
|
@ -203,6 +204,54 @@ class SecurityView extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
),
|
||||
// The "autoPin" preference (whether to automatically accept a correct PIN).
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
return RawMaterialButton(
|
||||
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
onPressed: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Auto-accept correct PIN",
|
||||
style: STextStyles.titleBold12(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.autoPin),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.autoPin = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../providers/global/auto_swb_service_provider.dart';
|
||||
import '../../../../providers/providers.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
|
@ -316,7 +317,7 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
|
|||
children: [
|
||||
const TextSpan(
|
||||
text:
|
||||
"Auto Backup is a custom Stack Wallet feature that offers a convenient backup of your data.\n\nTo 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.\n\nFor more information, please see our website ",
|
||||
"Auto Backup is a custom ${AppConfig.appName} feature that offers a convenient backup of your data.\n\nTo 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.\n\nFor more information, please see our website ",
|
||||
),
|
||||
TextSpan(
|
||||
text: "stackwallet.com.",
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:flutter_svg/svg.dart';
|
|||
import 'package:stack_wallet_backup/stack_wallet_backup.dart';
|
||||
import 'package:zxcvbn/zxcvbn.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../providers/global/prefs_provider.dart';
|
||||
import '../../../../providers/global/secure_store_provider.dart';
|
||||
|
@ -651,12 +652,12 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
|
|||
builder: (_) => Platform.isAndroid
|
||||
? StackOkDialog(
|
||||
title:
|
||||
"Stack Auto Backup enabled and saved to:",
|
||||
"${AppConfig.prefix} Auto Backup enabled and saved to:",
|
||||
message: fileToSave,
|
||||
)
|
||||
: const StackOkDialog(
|
||||
title:
|
||||
"Stack Auto Backup enabled!",
|
||||
"${AppConfig.prefix} Auto Backup enabled!",
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:zxcvbn/zxcvbn.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../providers/global/secure_store_provider.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
|
@ -774,7 +775,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
|||
height: 26,
|
||||
),
|
||||
Text(
|
||||
"Stack backup saved to: \n",
|
||||
"${AppConfig.prefix} backup saved to: \n",
|
||||
style: STextStyles
|
||||
.desktopH3(context),
|
||||
),
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../app_config.dart';
|
||||
import '../../../../../themes/stack_colors.dart';
|
||||
import '../../../../../utilities/text_styles.dart';
|
||||
import '../../../../../utilities/util.dart';
|
||||
|
@ -89,7 +90,7 @@ class CancelStackRestoreDialog extends StatelessWidget {
|
|||
.snackBarBackError,
|
||||
child: Text(
|
||||
"If you cancel, the restore will not complete, and "
|
||||
"the wallets will not appear in your Stack.",
|
||||
"the wallets will not appear in your ${AppConfig.prefix}.",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'package:flutter_svg/svg.dart';
|
|||
import 'package:stack_wallet_backup/stack_wallet_backup.dart';
|
||||
import 'package:zxcvbn/zxcvbn.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../providers/global/prefs_provider.dart';
|
||||
import '../../../../providers/global/secure_store_provider.dart';
|
||||
|
@ -221,10 +222,11 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
|
|||
barrierDismissible: false,
|
||||
builder: (_) => Platform.isAndroid
|
||||
? StackOkDialog(
|
||||
title: "Stack Auto Backup saved to:",
|
||||
title: "${AppConfig.prefix} Auto Backup saved to:",
|
||||
message: fileToSave,
|
||||
)
|
||||
: const StackOkDialog(title: "Stack Auto Backup saved"),
|
||||
: const StackOkDialog(
|
||||
title: "${AppConfig.prefix} Auto Backup saved"),
|
||||
);
|
||||
if (mounted) {
|
||||
passwordController.text = "";
|
||||
|
|
|
@ -14,6 +14,8 @@ import 'package:file_picker/file_picker.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../../../../../app_config.dart';
|
||||
import '../../../../../utilities/util.dart';
|
||||
|
||||
class SWBFileSystem {
|
||||
|
@ -39,17 +41,18 @@ class SWBFileSystem {
|
|||
// debugPrint(rootPath!.absolute.toString());
|
||||
|
||||
late Directory sampleFolder;
|
||||
const dirName = "${AppConfig.prefix}_backup";
|
||||
|
||||
if (Platform.isIOS) {
|
||||
sampleFolder = Directory(rootPath!.path);
|
||||
} else if (Platform.isAndroid) {
|
||||
sampleFolder = Directory('${rootPath!.path}Documents/Stack_backups');
|
||||
sampleFolder = Directory('${rootPath!.path}Documents/$dirName');
|
||||
} else if (Platform.isLinux) {
|
||||
sampleFolder = Directory('${rootPath!.path}/Stack_backups');
|
||||
sampleFolder = Directory('${rootPath!.path}/$dirName');
|
||||
} else if (Platform.isWindows) {
|
||||
sampleFolder = Directory('${rootPath!.path}/Stack_backups');
|
||||
sampleFolder = Directory('${rootPath!.path}/$dirName');
|
||||
} else if (Platform.isMacOS) {
|
||||
sampleFolder = Directory('${rootPath!.path}/Stack_backups');
|
||||
sampleFolder = Directory('${rootPath!.path}/$dirName');
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../route_generator.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
|
@ -205,7 +206,7 @@ class _RestoreFromEncryptedStringViewState
|
|||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Decrypting Stack backup file",
|
||||
"Decrypting ${AppConfig.prefix} backup file",
|
||||
style:
|
||||
STextStyles.pageTitleH2(
|
||||
context,
|
||||
|
|
|
@ -338,7 +338,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
|||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Decrypting Stack backup file",
|
||||
"Decrypting ${AppConfig.prefix} backup file",
|
||||
style: STextStyles.pageTitleH2(
|
||||
context,
|
||||
).copyWith(
|
||||
|
@ -452,7 +452,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
|||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Decrypting Stack backup file",
|
||||
"Decrypting ${AppConfig.prefix} backup file",
|
||||
style: STextStyles.pageTitleH2(
|
||||
context,
|
||||
).copyWith(
|
||||
|
|
|
@ -10,9 +10,8 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'auto_backup_view.dart';
|
||||
import 'create_backup_view.dart';
|
||||
import 'restore_from_file_view.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/assets.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
|
@ -20,6 +19,9 @@ import '../../../../utilities/text_styles.dart';
|
|||
import '../../../../widgets/background.dart';
|
||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
import 'auto_backup_view.dart';
|
||||
import 'create_backup_view.dart';
|
||||
import 'restore_from_file_view.dart';
|
||||
|
||||
class StackBackupView extends StatelessWidget {
|
||||
const StackBackupView({
|
||||
|
@ -42,7 +44,7 @@ class StackBackupView extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
title: Text(
|
||||
"Stack backup",
|
||||
"${AppConfig.prefix} backup",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../../../../models/keys/cw_key_data.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/assets.dart';
|
||||
import '../../../../utilities/clipboard_interface.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../utilities/util.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/detail_item.dart';
|
||||
import '../../../../widgets/qr.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
|
||||
class CNWalletKeys extends StatefulWidget {
|
||||
const CNWalletKeys({
|
||||
super.key,
|
||||
required this.cwKeyData,
|
||||
required this.walletId,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
});
|
||||
|
||||
final CWKeyData cwKeyData;
|
||||
final String walletId;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
|
||||
@override
|
||||
State<CNWalletKeys> createState() => _CNWalletKeysState();
|
||||
}
|
||||
|
||||
class _CNWalletKeysState extends State<CNWalletKeys> {
|
||||
late String _currentDropDownValue;
|
||||
|
||||
String _current(String key) =>
|
||||
widget.cwKeyData.keys.firstWhere((e) => e.label == key).key;
|
||||
|
||||
Future<void> _copy() async {
|
||||
await widget.clipboardInterface.setData(
|
||||
ClipboardData(text: _current(_currentDropDownValue)),
|
||||
);
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_currentDropDownValue = widget.cwKeyData.keys.first.label;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: Util.isDesktop
|
||||
? const EdgeInsets.symmetric(horizontal: 20)
|
||||
: EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: Util.isDesktop ? MainAxisSize.min : MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
DetailItemBase(
|
||||
horizontal: true,
|
||||
borderColor: Util.isDesktop
|
||||
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
|
||||
: null,
|
||||
title: Text(
|
||||
"Selected key",
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
detail: SizedBox(
|
||||
width: Util.isDesktop ? 200 : 170,
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
value: _currentDropDownValue,
|
||||
items: [
|
||||
...widget.cwKeyData.keys.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e.label,
|
||||
child: Text(
|
||||
e.label,
|
||||
style: STextStyles.w500_14(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value is String) {
|
||||
setState(() {
|
||||
_currentDropDownValue = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
isExpanded: true,
|
||||
buttonStyleData: ButtonStyleData(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: SvgPicture.asset(
|
||||
Assets.svg.chevronDown,
|
||||
width: 12,
|
||||
height: 6,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
offset: const Offset(0, -10),
|
||||
elevation: 0,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
QR(
|
||||
data: _current(_currentDropDownValue),
|
||||
size:
|
||||
Util.isDesktop ? 256 : MediaQuery.of(context).size.width / 1.5,
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
borderColor: Util.isDesktop
|
||||
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
|
||||
: null,
|
||||
child: SelectableText(
|
||||
_current(_currentDropDownValue),
|
||||
style: STextStyles.w500_14(context),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
if (Util.isDesktop) const Spacer(),
|
||||
if (Util.isDesktop)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Copy",
|
||||
onPressed: _copy,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,9 +13,11 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../models/keys/cw_key_data.dart';
|
||||
import '../../../../models/keys/key_data_interface.dart';
|
||||
import '../../../../models/keys/xpriv_data.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/address_utils.dart';
|
||||
|
@ -27,13 +29,18 @@ import '../../../../utilities/util.dart';
|
|||
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../../../widgets/background.dart';
|
||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/custom_buttons/blue_text_button.dart';
|
||||
import '../../../../widgets/custom_buttons/simple_copy_button.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/desktop/secondary_button.dart';
|
||||
import '../../../../widgets/detail_item.dart';
|
||||
import '../../../../widgets/qr.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
import '../../../../widgets/stack_dialog.dart';
|
||||
import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
|
||||
import '../../../wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'cn_wallet_keys.dart';
|
||||
import 'wallet_xprivs.dart';
|
||||
|
||||
class WalletBackupView extends ConsumerWidget {
|
||||
const WalletBackupView({
|
||||
|
@ -41,7 +48,7 @@ class WalletBackupView extends ConsumerWidget {
|
|||
required this.walletId,
|
||||
required this.mnemonic,
|
||||
this.frostWalletData,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
this.keyData,
|
||||
});
|
||||
|
||||
static const String routeName = "/walletBackup";
|
||||
|
@ -54,14 +61,13 @@ class WalletBackupView extends ConsumerWidget {
|
|||
String keys,
|
||||
({String config, String keys})? prevGen,
|
||||
})? frostWalletData;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
final KeyDataInterface? keyData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
||||
final bool frost = frostWalletData != null;
|
||||
final prevGen = frostWalletData?.prevGen != null;
|
||||
|
||||
return Background(
|
||||
child: Scaffold(
|
||||
|
@ -77,296 +83,423 @@ class WalletBackupView extends ConsumerWidget {
|
|||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: AppBarIconButton(
|
||||
color: Theme.of(context).extension<StackColors>()!.background,
|
||||
shadows: const [],
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.copy,
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.topNavIconPrimary,
|
||||
),
|
||||
onPressed: () async {
|
||||
await clipboardInterface
|
||||
.setData(ClipboardData(text: mnemonic.join(" ")));
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
if (keyData != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: CustomTextButton(
|
||||
text: switch (keyData.runtimeType) {
|
||||
const (XPrivData) => "xpriv(s)",
|
||||
const (CWKeyData) => "keys",
|
||||
_ => throw UnimplementedError(
|
||||
"Don't forget to add your KeyDataInterface here! ${keyData.runtimeType}",
|
||||
),
|
||||
},
|
||||
onTap: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
MobileKeyDataView.routeName,
|
||||
arguments: (
|
||||
walletId: walletId,
|
||||
keyData: keyData!,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: frost
|
||||
? LayoutBuilder(
|
||||
builder: (builderContext, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight - 24,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Please write down your backup data. Keep it safe and "
|
||||
"never share it with anyone. "
|
||||
"Your backup data is the only way you can access your "
|
||||
"funds if you forget your PIN, lose your phone, etc."
|
||||
"\n\n"
|
||||
"${AppConfig.appName} does not keep nor is able to restore "
|
||||
"your backup data. "
|
||||
"Only you have access to your wallet.",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
// DetailItem(
|
||||
// title: "My name",
|
||||
// detail: frostWalletData!.myName,
|
||||
// button: Util.isDesktop
|
||||
// ? IconCopyButton(
|
||||
// data: frostWalletData!.myName,
|
||||
// )
|
||||
// : SimpleCopyButton(
|
||||
// data: frostWalletData!.myName,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 16,
|
||||
// ),
|
||||
DetailItem(
|
||||
title: "Multisig config",
|
||||
detail: frostWalletData!.config,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.config,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.config,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
DetailItem(
|
||||
title: "Keys",
|
||||
detail: frostWalletData!.keys,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.keys,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.keys,
|
||||
),
|
||||
),
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
if (prevGen)
|
||||
RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Previous generation info",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (prevGen)
|
||||
DetailItem(
|
||||
title: "Previous multisig config",
|
||||
detail: frostWalletData!.prevGen!.config,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data:
|
||||
frostWalletData!.prevGen!.config,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data:
|
||||
frostWalletData!.prevGen!.config,
|
||||
),
|
||||
),
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (prevGen)
|
||||
DetailItem(
|
||||
title: "Previous keys",
|
||||
detail: frostWalletData!.prevGen!.keys,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.prevGen!.keys,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.prevGen!.keys,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
? _FrostKeys(
|
||||
frostWalletData: frostWalletData,
|
||||
walletId: walletId,
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
ref.watch(pWalletName(walletId)),
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.label(context).copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
"Recovery Phrase",
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.pageTitleH1(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: MnemonicTable(
|
||||
words: mnemonic,
|
||||
isDesktop: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
TextButton(
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryEnabledButtonStyle(context),
|
||||
onPressed: () {
|
||||
final String data =
|
||||
AddressUtils.encodeQRSeedData(mnemonic);
|
||||
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
final width = MediaQuery.of(context).size.width / 2;
|
||||
return StackDialogBase(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
"Recovery phrase QR code",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
// key: _qrKey,
|
||||
child: SizedBox(
|
||||
width: width + 20,
|
||||
height: width + 20,
|
||||
child: QR(
|
||||
data: data,
|
||||
size: width,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
// await _capturePng(true);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getSecondaryEnabledButtonStyle(
|
||||
context,
|
||||
),
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: STextStyles.button(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Show QR Code",
|
||||
style: STextStyles.button(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
: _Mnemonic(
|
||||
walletId: walletId,
|
||||
mnemonic: mnemonic,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Mnemonic extends ConsumerWidget {
|
||||
const _Mnemonic({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
required this.mnemonic,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
});
|
||||
|
||||
final String walletId;
|
||||
final List<String> mnemonic;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
ref.watch(pWalletName(walletId)),
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.label(context).copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
"Recovery Phrase",
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.pageTitleH1(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
"Please write down your backup key. Keep it safe and never share "
|
||||
"it with anyone. Your backup key is the only way you can access"
|
||||
" your funds if you forget your PIN, lose your phone, etc.\n\n"
|
||||
"${AppConfig.appName} does not keep nor is able to restore your"
|
||||
" backup key. Only you have access to your wallet.",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: MnemonicTable(
|
||||
words: mnemonic,
|
||||
isDesktop: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: "Copy",
|
||||
onPressed: () async {
|
||||
await clipboardInterface
|
||||
.setData(ClipboardData(text: mnemonic.join(" ")));
|
||||
if (context.mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Show QR Code",
|
||||
onPressed: () {
|
||||
final String data = AddressUtils.encodeQRSeedData(mnemonic);
|
||||
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
final width = MediaQuery.of(context).size.width / 2;
|
||||
return StackDialogBase(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
"Recovery phrase QR code",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
// key: _qrKey,
|
||||
child: SizedBox(
|
||||
width: width + 20,
|
||||
height: width + 20,
|
||||
child: QR(
|
||||
data: data,
|
||||
size: width,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
// await _capturePng(true);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getSecondaryEnabledButtonStyle(
|
||||
context,
|
||||
),
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: STextStyles.button(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FrostKeys extends StatelessWidget {
|
||||
const _FrostKeys({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
this.frostWalletData,
|
||||
});
|
||||
|
||||
final String walletId;
|
||||
final ({
|
||||
String myName,
|
||||
String config,
|
||||
String keys,
|
||||
({String config, String keys})? prevGen,
|
||||
})? frostWalletData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final prevGen = frostWalletData?.prevGen != null;
|
||||
return LayoutBuilder(
|
||||
builder: (builderContext, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight - 24,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Please write down your backup data. Keep it safe and "
|
||||
"never share it with anyone. "
|
||||
"Your backup data is the only way you can access your "
|
||||
"funds if you forget your PIN, lose your phone, etc."
|
||||
"\n\n"
|
||||
"${AppConfig.appName} does not keep nor is able to restore "
|
||||
"your backup data. "
|
||||
"Only you have access to your wallet.",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
// DetailItem(
|
||||
// title: "My name",
|
||||
// detail: frostWalletData!.myName,
|
||||
// button: Util.isDesktop
|
||||
// ? IconCopyButton(
|
||||
// data: frostWalletData!.myName,
|
||||
// )
|
||||
// : SimpleCopyButton(
|
||||
// data: frostWalletData!.myName,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 16,
|
||||
// ),
|
||||
DetailItem(
|
||||
title: "Multisig config",
|
||||
detail: frostWalletData!.config,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.config,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.config,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
DetailItem(
|
||||
title: "Keys",
|
||||
detail: frostWalletData!.keys,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.keys,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.keys,
|
||||
),
|
||||
),
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
if (prevGen)
|
||||
RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Previous generation info",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (prevGen)
|
||||
DetailItem(
|
||||
title: "Previous multisig config",
|
||||
detail: frostWalletData!.prevGen!.config,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.prevGen!.config,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.prevGen!.config,
|
||||
),
|
||||
),
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (prevGen)
|
||||
DetailItem(
|
||||
title: "Previous keys",
|
||||
detail: frostWalletData!.prevGen!.keys,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.prevGen!.keys,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.prevGen!.keys,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileKeyDataView extends StatelessWidget {
|
||||
const MobileKeyDataView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
required this.keyData,
|
||||
});
|
||||
|
||||
static const String routeName = "/mobileXPrivView";
|
||||
|
||||
final String walletId;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
final KeyDataInterface keyData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Background(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: AppBar(
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Wallet ${switch (keyData.runtimeType) {
|
||||
const (XPrivData) => "xpriv(s)",
|
||||
const (CWKeyData) => "keys",
|
||||
_ => throw UnimplementedError(
|
||||
"Don't forget to add your KeyDataInterface here!",
|
||||
),
|
||||
}}",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: switch (keyData.runtimeType) {
|
||||
const (XPrivData) => WalletXPrivs(
|
||||
walletId: walletId,
|
||||
xprivData: keyData as XPrivData,
|
||||
),
|
||||
const (CWKeyData) => CNWalletKeys(
|
||||
walletId: walletId,
|
||||
cwKeyData: keyData as CWKeyData,
|
||||
),
|
||||
_ => throw UnimplementedError(
|
||||
"Don't forget to add your KeyDataInterface here!",
|
||||
),
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* This file is part of Stack Wallet.
|
||||
*
|
||||
* Copyright (c) 2023 Cypher Stack
|
||||
* All Rights Reserved.
|
||||
* The code is distributed under GPLv3 license, see LICENSE file for details.
|
||||
* Generated by Cypher Stack on 2023-05-26
|
||||
*
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../../../models/keys/xpriv_data.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/assets.dart';
|
||||
import '../../../../utilities/clipboard_interface.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../utilities/util.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/detail_item.dart';
|
||||
import '../../../../widgets/qr.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
|
||||
class WalletXPrivs extends ConsumerStatefulWidget {
|
||||
const WalletXPrivs({
|
||||
super.key,
|
||||
required this.xprivData,
|
||||
required this.walletId,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
});
|
||||
|
||||
final XPrivData xprivData;
|
||||
final String walletId;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
|
||||
@override
|
||||
ConsumerState<WalletXPrivs> createState() => WalletXPrivsState();
|
||||
}
|
||||
|
||||
class WalletXPrivsState extends ConsumerState<WalletXPrivs> {
|
||||
late String _currentDropDownValue;
|
||||
|
||||
String _current(String key) =>
|
||||
widget.xprivData.xprivs.firstWhere((e) => e.path == key).xpriv;
|
||||
|
||||
Future<void> _copy() async {
|
||||
await widget.clipboardInterface.setData(
|
||||
ClipboardData(text: _current(_currentDropDownValue)),
|
||||
);
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_currentDropDownValue = widget.xprivData.xprivs.first.path;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: Util.isDesktop
|
||||
? const EdgeInsets.symmetric(horizontal: 20)
|
||||
: EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: Util.isDesktop ? MainAxisSize.min : MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
DetailItem(
|
||||
title: "Master fingerprint",
|
||||
detail: widget.xprivData.fingerprint,
|
||||
horizontal: true,
|
||||
borderColor: Util.isDesktop
|
||||
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
|
||||
: null,
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
DetailItemBase(
|
||||
horizontal: true,
|
||||
borderColor: Util.isDesktop
|
||||
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
|
||||
: null,
|
||||
title: Text(
|
||||
"Derivation",
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
detail: SizedBox(
|
||||
width: Util.isDesktop ? 200 : 170,
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
value: _currentDropDownValue,
|
||||
items: [
|
||||
...widget.xprivData.xprivs.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e.path,
|
||||
child: Text(
|
||||
e.path,
|
||||
style: STextStyles.w500_14(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value is String) {
|
||||
setState(() {
|
||||
_currentDropDownValue = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
isExpanded: true,
|
||||
buttonStyleData: ButtonStyleData(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: SvgPicture.asset(
|
||||
Assets.svg.chevronDown,
|
||||
width: 12,
|
||||
height: 6,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
offset: const Offset(0, -10),
|
||||
elevation: 0,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
QR(
|
||||
data: _current(_currentDropDownValue),
|
||||
size:
|
||||
Util.isDesktop ? 256 : MediaQuery.of(context).size.width / 1.5,
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
borderColor: Util.isDesktop
|
||||
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
|
||||
: null,
|
||||
child: SelectableText(
|
||||
_current(_currentDropDownValue),
|
||||
style: STextStyles.w500_14(context),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
if (Util.isDesktop) const Spacer(),
|
||||
if (Util.isDesktop)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Copy",
|
||||
onPressed: _copy,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import 'package:tuple/tuple.dart';
|
|||
import '../../../db/hive/db.dart';
|
||||
import '../../../db/sqlite/firo_cache.dart';
|
||||
import '../../../models/epicbox_config_model.dart';
|
||||
import '../../../models/keys/key_data_interface.dart';
|
||||
import '../../../notifications/show_flush_bar.dart';
|
||||
import '../../../providers/global/wallets_provider.dart';
|
||||
import '../../../providers/ui/transaction_filter_provider.dart';
|
||||
|
@ -35,6 +36,8 @@ import '../../../wallets/crypto_currency/intermediate/frost_currency.dart';
|
|||
import '../../../wallets/crypto_currency/intermediate/nano_currency.dart';
|
||||
import '../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
|
||||
import '../../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
|
||||
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||
import '../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
|
||||
import '../../../widgets/background.dart';
|
||||
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
|
@ -95,9 +98,9 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
void initState() {
|
||||
walletId = widget.walletId;
|
||||
coin = widget.coin;
|
||||
// TODO: [prio=low] xpubs
|
||||
// xPubEnabled = ref.read(pWallets).getWallet(walletId).hasXPub;
|
||||
xPubEnabled = false;
|
||||
xPubEnabled =
|
||||
ref.read(pWallets).getWallet(walletId) is ExtendedKeysInterface;
|
||||
|
||||
xpub = "";
|
||||
|
||||
_currentSyncStatus = widget.initialSyncStatus;
|
||||
|
@ -301,6 +304,13 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
await wallet.getMnemonicAsWords();
|
||||
}
|
||||
|
||||
KeyDataInterface? keyData;
|
||||
if (wallet is ExtendedKeysInterface) {
|
||||
keyData = await wallet.getXPrivs();
|
||||
} else if (wallet is CwBasedInterface) {
|
||||
keyData = await wallet.getKeys();
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
|
@ -314,6 +324,7 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
mnemonic: mnemonic ?? [],
|
||||
frostWalletData:
|
||||
frostWalletData,
|
||||
keyData: keyData,
|
||||
),
|
||||
showBackButton: true,
|
||||
routeOnSuccess:
|
||||
|
@ -373,11 +384,30 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
return SettingsListButton(
|
||||
iconAssetName: Assets.svg.eye,
|
||||
title: "Wallet xPub",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
XPubView.routeName,
|
||||
arguments: widget.walletId,
|
||||
onPressed: () async {
|
||||
final xpubData = await showLoading(
|
||||
delay: const Duration(
|
||||
milliseconds: 800,
|
||||
),
|
||||
whileFuture: (ref
|
||||
.read(pWallets)
|
||||
.getWallet(walletId)
|
||||
as ExtendedKeysInterface)
|
||||
.getXPubs(),
|
||||
context: context,
|
||||
message: "Loading xpubs",
|
||||
rootNavigator: Util.isDesktop,
|
||||
);
|
||||
if (context.mounted) {
|
||||
await Navigator.of(context)
|
||||
.pushNamed(
|
||||
XPubView.routeName,
|
||||
arguments: (
|
||||
widget.walletId,
|
||||
xpubData
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../providers/providers.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
|
@ -69,7 +70,11 @@ class DeleteWalletWarningView extends ConsumerWidget {
|
|||
.extension<StackColors>()!
|
||||
.warningBackground,
|
||||
child: Text(
|
||||
"You are going to permanently delete your wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.",
|
||||
"You are going to permanently delete your wallet.\n\n"
|
||||
"If you delete your wallet, the only way you can have access"
|
||||
" to your funds is by using your backup key.\n\n"
|
||||
"${AppConfig.appName} does not keep nor is able to restore "
|
||||
"your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.",
|
||||
style: STextStyles.baseXS(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
|
|
|
@ -11,17 +11,22 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../providers/db/main_db_provider.dart';
|
||||
import '../../../../providers/providers.dart';
|
||||
import '../../../../route_generator.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../utilities/util.dart';
|
||||
import '../../../../wallets/isar/models/wallet_info.dart';
|
||||
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
||||
import '../../../../widgets/background.dart';
|
||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
import '../../../../widgets/stack_dialog.dart';
|
||||
import '../../../pinpad_views/lock_screen_view.dart';
|
||||
|
@ -31,7 +36,7 @@ import 'rbf_settings_view.dart';
|
|||
import 'rename_wallet_view.dart';
|
||||
import 'spark_info.dart';
|
||||
|
||||
class WalletSettingsWalletSettingsView extends ConsumerWidget {
|
||||
class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget {
|
||||
const WalletSettingsWalletSettingsView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
|
@ -42,7 +47,88 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
|
|||
final String walletId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<WalletSettingsWalletSettingsView> createState() =>
|
||||
_WalletSettingsWalletSettingsViewState();
|
||||
}
|
||||
|
||||
class _WalletSettingsWalletSettingsViewState
|
||||
extends ConsumerState<WalletSettingsWalletSettingsView> {
|
||||
bool _switchReuseAddressToggledLock = false; // Mutex.
|
||||
Future<void> _switchReuseAddressToggled(bool newValue) async {
|
||||
if (newValue) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final isDesktop = Util.isDesktop;
|
||||
return StackDialog(
|
||||
title: "Warning!",
|
||||
message:
|
||||
"Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?",
|
||||
leftButton: TextButton(
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getSecondaryEnabledButtonStyle(context),
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: STextStyles.itemSubtitle12(context),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
rightButton: TextButton(
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryEnabledButtonStyle(context),
|
||||
child: Text(
|
||||
"Continue",
|
||||
style: STextStyles.button(context),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
).then((confirmed) async {
|
||||
if (_switchReuseAddressToggledLock) {
|
||||
return;
|
||||
}
|
||||
_switchReuseAddressToggledLock = true; // Lock mutex.
|
||||
|
||||
try {
|
||||
if (confirmed == true) {
|
||||
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
|
||||
newEntries: {
|
||||
WalletInfoKeys.reuseAddress: true,
|
||||
},
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
} else {
|
||||
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
|
||||
newEntries: {
|
||||
WalletInfoKeys.reuseAddress: false,
|
||||
},
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
// ensure _switchReuseAddressToggledLock is set to false no matter what.
|
||||
_switchReuseAddressToggledLock = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
|
||||
newEntries: {
|
||||
WalletInfoKeys.reuseAddress: false,
|
||||
},
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Background(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
|
@ -80,7 +166,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
|
|||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RenameWalletView.routeName,
|
||||
arguments: walletId,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
|
@ -102,6 +188,177 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
|
|||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is RbfInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RbfSettingsView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"RBF settings",
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is RbfInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is MultiAddressInterface)
|
||||
RoundedWhiteContainer(
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
return RawMaterialButton(
|
||||
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
onPressed: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Reuse receiving address by default",
|
||||
style: STextStyles.titleBold12(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
pWalletInfo(widget.walletId).select(
|
||||
(value) => value.otherData),
|
||||
)[WalletInfoKeys.reuseAddress]
|
||||
as bool? ??
|
||||
false,
|
||||
onValueChanged: (newValue) {
|
||||
_switchReuseAddressToggled(newValue);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is MultiAddressInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is LelantusInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is LelantusInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
LelantusSettingsView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Lelantus settings",
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is SparkInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is SparkInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
SparkInfoView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Spark info",
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is RbfInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
|
@ -119,7 +376,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
|
|||
context: context,
|
||||
builder: (_) => StackDialog(
|
||||
title:
|
||||
"Do you want to delete ${ref.read(pWalletName(walletId))}?",
|
||||
"Do you want to delete ${ref.read(pWalletName(widget.walletId))}?",
|
||||
leftButton: TextButton(
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
|
@ -148,7 +405,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
|
|||
shouldUseMaterialRoute:
|
||||
RouteGenerator.useMaterialPageRoute,
|
||||
builder: (_) => LockscreenView(
|
||||
routeOnSuccessArguments: walletId,
|
||||
routeOnSuccessArguments: widget.walletId,
|
||||
showBackButton: true,
|
||||
routeOnSuccess:
|
||||
DeleteWalletWarningView.routeName,
|
||||
|
@ -188,116 +445,6 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(walletId)
|
||||
is LelantusInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(walletId)
|
||||
is LelantusInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
LelantusSettingsView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Lelantus settings",
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(walletId) is SparkInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(walletId) is SparkInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
SparkInfoView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Spark info",
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(walletId) is RbfInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(walletId) is RbfInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RbfSettingsView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"RBF settings",
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -10,27 +10,28 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../providers/global/wallets_provider.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/assets.dart';
|
||||
import '../../../../utilities/clipboard_interface.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../utilities/util.dart';
|
||||
import '../../../../wallets/wallet/wallet.dart';
|
||||
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||
import '../../../../widgets/background.dart';
|
||||
import '../../../../widgets/conditional_parent.dart';
|
||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/desktop/secondary_button.dart';
|
||||
import '../../../../widgets/loading_indicator.dart';
|
||||
import '../../../../widgets/detail_item.dart';
|
||||
import '../../../../widgets/qr.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
|
||||
|
@ -38,41 +39,30 @@ class XPubView extends ConsumerStatefulWidget {
|
|||
const XPubView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
required this.xpubData,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
});
|
||||
|
||||
final String walletId;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
final ({List<XPub> xpubs, String fingerprint}) xpubData;
|
||||
|
||||
static const String routeName = "/xpub";
|
||||
|
||||
@override
|
||||
ConsumerState<XPubView> createState() => _XPubViewState();
|
||||
ConsumerState<XPubView> createState() => XPubViewState();
|
||||
}
|
||||
|
||||
class _XPubViewState extends ConsumerState<XPubView> {
|
||||
final bool isDesktop = Util.isDesktop;
|
||||
class XPubViewState extends ConsumerState<XPubView> {
|
||||
late String _currentDropDownValue;
|
||||
|
||||
late ClipboardInterface _clipboardInterface;
|
||||
late final Wallet wallet;
|
||||
|
||||
String? xpub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_clipboardInterface = widget.clipboardInterface;
|
||||
wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
String _current(String key) =>
|
||||
widget.xpubData.xpubs.firstWhere((e) => e.path == key).xpub;
|
||||
|
||||
Future<void> _copy() async {
|
||||
await _clipboardInterface.setData(ClipboardData(text: xpub!));
|
||||
await widget.clipboardInterface.setData(
|
||||
ClipboardData(text: _current(_currentDropDownValue)),
|
||||
);
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
|
@ -85,8 +75,16 @@ class _XPubViewState extends ConsumerState<XPubView> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_currentDropDownValue = widget.xpubData.xpubs.first.path;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isDesktop = Util.isDesktop;
|
||||
|
||||
return ConditionalParent(
|
||||
condition: !isDesktop,
|
||||
builder: (child) => Background(
|
||||
|
@ -100,35 +98,9 @@ class _XPubViewState extends ConsumerState<XPubView> {
|
|||
},
|
||||
),
|
||||
title: Text(
|
||||
"Wallet xPub",
|
||||
"Wallet xpub(s)",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: AppBarIconButton(
|
||||
color:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
shadows: const [],
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.copy,
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.topNavIconPrimary,
|
||||
),
|
||||
onPressed: () {
|
||||
if (xpub != null) {
|
||||
_copy();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
|
@ -136,7 +108,25 @@ class _XPubViewState extends ConsumerState<XPubView> {
|
|||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
child: child,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: child,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -146,6 +136,7 @@ class _XPubViewState extends ConsumerState<XPubView> {
|
|||
maxWidth: 600,
|
||||
maxHeight: double.infinity,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
@ -155,7 +146,7 @@ class _XPubViewState extends ConsumerState<XPubView> {
|
|||
left: 32,
|
||||
),
|
||||
child: Text(
|
||||
"${wallet.info.name} xPub",
|
||||
"${ref.watch(pWalletName(widget.walletId))} xpub(s)",
|
||||
style: STextStyles.desktopH2(context),
|
||||
),
|
||||
),
|
||||
|
@ -167,153 +158,162 @@ class _XPubViewState extends ConsumerState<XPubView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(
|
||||
milliseconds: 150,
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(32, 0, 32, 32),
|
||||
child: child,
|
||||
child: SingleChildScrollView(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: Util.isDesktop ? MainAxisSize.min : MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (isDesktop) const SizedBox(height: 44),
|
||||
ConditionalParent(
|
||||
condition: !isDesktop,
|
||||
builder: (child) => Expanded(
|
||||
child: child,
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
DetailItem(
|
||||
title: "Master fingerprint",
|
||||
detail: widget.xpubData.fingerprint,
|
||||
horizontal: true,
|
||||
borderColor: Util.isDesktop
|
||||
? Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG
|
||||
: null,
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
DetailItemBase(
|
||||
horizontal: true,
|
||||
borderColor: Util.isDesktop
|
||||
? Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG
|
||||
: null,
|
||||
title: Text(
|
||||
"Derivation",
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
child: FutureBuilder(
|
||||
future: Future(() => "fixme"),
|
||||
// future: wallet.xpub,
|
||||
builder: (context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done &&
|
||||
snapshot.hasData) {
|
||||
xpub = snapshot.data!;
|
||||
}
|
||||
|
||||
const height = 600.0;
|
||||
Widget child;
|
||||
if (xpub == null) {
|
||||
child = const SizedBox(
|
||||
key: Key("loadingXPUB"),
|
||||
height: height,
|
||||
child: Center(
|
||||
child: LoadingIndicator(
|
||||
width: 100,
|
||||
detail: SizedBox(
|
||||
width: Util.isDesktop ? 200 : 170,
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
value: _currentDropDownValue,
|
||||
items: [
|
||||
...widget.xpubData.xpubs.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e.path,
|
||||
child: Text(
|
||||
e.path,
|
||||
style: STextStyles.w500_14(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value is String) {
|
||||
setState(() {
|
||||
_currentDropDownValue = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
isExpanded: true,
|
||||
buttonStyleData: ButtonStyleData(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
child = _XPub(
|
||||
xpub: xpub!,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: SvgPicture.asset(
|
||||
Assets.svg.chevronDown,
|
||||
width: 12,
|
||||
height: 6,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
offset: const Offset(0, -10),
|
||||
elevation: 0,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
QR(
|
||||
data: _current(_currentDropDownValue),
|
||||
size: Util.isDesktop
|
||||
? 256
|
||||
: MediaQuery.of(context).size.width / 1.5,
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
borderColor: Util.isDesktop
|
||||
? Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG
|
||||
: null,
|
||||
child: SelectableText(
|
||||
_current(_currentDropDownValue),
|
||||
style: STextStyles.w500_14(context),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
if (Util.isDesktop) const Spacer(),
|
||||
if (Util.isDesktop)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Copy",
|
||||
onPressed: _copy,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _XPub extends StatelessWidget {
|
||||
const _XPub({
|
||||
super.key,
|
||||
required this.xpub,
|
||||
required this.height,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
});
|
||||
|
||||
final String xpub;
|
||||
final double height;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isDesktop = Util.isDesktop;
|
||||
|
||||
return SizedBox(
|
||||
height: isDesktop ? height : double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
ConditionalParent(
|
||||
condition: !isDesktop,
|
||||
builder: (child) => RoundedWhiteContainer(
|
||||
child: child,
|
||||
),
|
||||
child: QR(
|
||||
data: xpub,
|
||||
size: isDesktop ? 280 : MediaQuery.of(context).size.width / 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(16),
|
||||
borderColor:
|
||||
Theme.of(context).extension<StackColors>()!.backgroundAppBar,
|
||||
child: SelectableText(
|
||||
xpub,
|
||||
style: STextStyles.largeMedium14(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
if (isDesktop)
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
buttonHeight: ButtonHeight.xl,
|
||||
label: "Cancel",
|
||||
onPressed: Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).pop,
|
||||
),
|
||||
),
|
||||
if (isDesktop) const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xl,
|
||||
label: "Copy",
|
||||
onPressed: () async {
|
||||
await clipboardInterface.setData(
|
||||
ClipboardData(
|
||||
text: xpub,
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!isDesktop) const Spacer(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../../models/keys/key_data_interface.dart';
|
||||
import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart';
|
||||
import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart';
|
||||
import '../../providers/global/wallets_provider.dart';
|
||||
|
@ -11,6 +12,8 @@ import '../../utilities/assets.dart';
|
|||
import '../../utilities/text_styles.dart';
|
||||
import '../../utilities/util.dart';
|
||||
import '../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
|
||||
import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||
import '../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
|
||||
import '../../widgets/background.dart';
|
||||
import '../../widgets/conditional_parent.dart';
|
||||
|
@ -264,7 +267,14 @@ class _FiroRescanRecoveryErrorViewState
|
|||
if (wallet is MnemonicInterface) {
|
||||
final mnemonic = await wallet.getMnemonicAsWords();
|
||||
|
||||
if (mounted) {
|
||||
KeyDataInterface? keyData;
|
||||
if (wallet is ExtendedKeysInterface) {
|
||||
keyData = await wallet.getXPrivs();
|
||||
} else if (wallet is CwBasedInterface) {
|
||||
keyData = await wallet.getKeys();
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
RouteGenerator.getRoute(
|
||||
|
@ -274,6 +284,7 @@ class _FiroRescanRecoveryErrorViewState
|
|||
routeOnSuccessArguments: (
|
||||
walletId: widget.walletId,
|
||||
mnemonic: mnemonic,
|
||||
keyData: keyData,
|
||||
),
|
||||
showBackButton: true,
|
||||
routeOnSuccess: WalletBackupView.routeName,
|
||||
|
|
|
@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../app_config.dart';
|
||||
import '../db/hive/db.dart';
|
||||
import '../pages_desktop_specific/password/create_password_view.dart';
|
||||
import '../providers/global/prefs_provider.dart';
|
||||
|
@ -108,7 +109,7 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Choose your Stack experience",
|
||||
"Choose your ${AppConfig.prefix} experience",
|
||||
style: isDesktop
|
||||
? STextStyles.desktopH2(context)
|
||||
: STextStyles.pageTitleH1(context),
|
||||
|
@ -253,10 +254,12 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> {
|
|||
)
|
||||
.then((_) {
|
||||
if (isEasy) {
|
||||
unawaited(
|
||||
ExchangeDataLoadingService.instance
|
||||
.loadAll(),
|
||||
);
|
||||
if (AppConfig.hasFeature(AppFeature.swap)) {
|
||||
unawaited(
|
||||
ExchangeDataLoadingService.instance
|
||||
.loadAll(),
|
||||
);
|
||||
}
|
||||
// unawaited(
|
||||
// BuyDataLoadingService().loadAll(ref));
|
||||
ref
|
||||
|
|
|
@ -960,8 +960,7 @@ class _DesktopTransactionCardRowState
|
|||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
context: context,
|
||||
message:
|
||||
"Restored Epic funds from your Seed have no Data.\nUse Stack Backup to keep your transaction history.",
|
||||
message: "Restored Epic funds from your Seed have no Data.",
|
||||
type: FlushBarType.warning,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
|
|
|
@ -33,6 +33,9 @@ class EmptyWallets extends ConsumerWidget {
|
|||
|
||||
final isDesktop = Util.isDesktop;
|
||||
|
||||
final stack =
|
||||
ref.watch(themeProvider.select((value) => value.assets.stack));
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
@ -47,21 +50,28 @@ class EmptyWallets extends ConsumerWidget {
|
|||
const Spacer(
|
||||
flex: 2,
|
||||
),
|
||||
SvgPicture.file(
|
||||
File(
|
||||
ref.watch(
|
||||
themeProvider.select(
|
||||
(value) => value.assets.stack,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: isDesktop ? 324 : MediaQuery.of(context).size.width / 3,
|
||||
child: (stack.endsWith(".png"))
|
||||
? Image.file(
|
||||
File(
|
||||
stack,
|
||||
),
|
||||
)
|
||||
: SvgPicture.file(
|
||||
File(
|
||||
stack,
|
||||
),
|
||||
width: isDesktop
|
||||
? 324
|
||||
: MediaQuery.of(context).size.width / 3,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktop ? 30 : 16,
|
||||
),
|
||||
Text(
|
||||
"You do not have any wallets yet. Start building your crypto Stack!",
|
||||
AppConfig.emptyWalletsMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: isDesktop
|
||||
? STextStyles.desktopSubtitleH2(context).copyWith(
|
||||
|
|
|
@ -20,6 +20,8 @@ import '../../models/isar/models/ethereum/eth_contract.dart';
|
|||
import '../../pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart';
|
||||
import '../../providers/db/main_db_provider.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../../services/event_bus/events/wallet_added_event.dart';
|
||||
import '../../services/event_bus/global_event_bus.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
import '../../utilities/constants.dart';
|
||||
|
@ -111,11 +113,7 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
|
|||
return element.toLowerCase().contains(term);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_searchController = TextEditingController();
|
||||
searchFieldFocusNode = FocusNode();
|
||||
|
||||
void updateWallets() {
|
||||
final walletsData =
|
||||
ref.read(mainDBProvider).isar.walletInfo.where().findAllSync();
|
||||
walletsData.removeWhere((e) => e.coin != widget.coin);
|
||||
|
@ -155,16 +153,40 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
|
|||
} else {
|
||||
// add non token wallet tuple to list
|
||||
for (final data in walletsData) {
|
||||
wallets.add(
|
||||
Tuple2(
|
||||
ref.read(pWallets).getWallet(
|
||||
data.walletId,
|
||||
),
|
||||
[],
|
||||
),
|
||||
);
|
||||
// desktop single coin apps may cause issues so lets just ignore the error and move on
|
||||
try {
|
||||
wallets.add(
|
||||
Tuple2(
|
||||
ref.read(pWallets).getWallet(
|
||||
data.walletId,
|
||||
),
|
||||
[],
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
// lol bandaid for single coin based apps
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_searchController = TextEditingController();
|
||||
searchFieldFocusNode = FocusNode();
|
||||
|
||||
updateWallets();
|
||||
|
||||
if (AppConfig.isSingleCoinApp) {
|
||||
GlobalEventBus.instance.on<WalletAddedEvent>().listen((_) {
|
||||
updateWallets();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
import '../../../providers/cash_fusion/fusion_progress_ui_state_provider.dart';
|
||||
import '../../../providers/global/prefs_provider.dart';
|
||||
|
@ -137,6 +139,8 @@ class _FusionDialogViewState extends ConsumerState<FusionDialogView> {
|
|||
message: "Stopping fusion",
|
||||
);
|
||||
|
||||
await Wakelock.disable();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -150,6 +154,12 @@ class _FusionDialogViewState extends ConsumerState<FusionDialogView> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
Wakelock.disable();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool _succeeded =
|
||||
|
@ -162,6 +172,10 @@ class _FusionDialogViewState extends ConsumerState<FusionDialogView> {
|
|||
.watch(fusionProgressUIStateProvider(widget.walletId))
|
||||
.fusionRoundsCompleted;
|
||||
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
}
|
||||
|
||||
return DesktopDialog(
|
||||
maxHeight: 600,
|
||||
child: SingleChildScrollView(
|
||||
|
|
|
@ -15,8 +15,7 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../models/contact_address_entry.dart';
|
||||
import '../../../../providers/exchange/exchange_send_from_wallet_id_provider.dart';
|
||||
import '../../../../providers/global/wallets_provider.dart';
|
||||
import '../../../../providers/providers.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/clipboard_interface.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
|
@ -88,7 +87,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
}
|
||||
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty,
|
||||
_next(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -120,7 +119,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
Logging.instance.log("$e\n$s", level: LogLevel.Info);
|
||||
}
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty,
|
||||
_next(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -167,7 +166,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
_toController.text = entry.address;
|
||||
ref.read(desktopExchangeModelProvider)!.recipientAddress = entry.address;
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty,
|
||||
_next(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -215,11 +214,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
_refundController.text = entry.address;
|
||||
ref.read(desktopExchangeModelProvider)!.refundAddress = entry.address;
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty && _refundController.text.isNotEmpty,
|
||||
_next(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _next() {
|
||||
if (doesRefundAddress) {
|
||||
return _toController.text.isNotEmpty && _refundController.text.isNotEmpty;
|
||||
} else {
|
||||
return _toController.text.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
late final bool doesRefundAddress;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
clipboard = widget.clipboard;
|
||||
|
@ -230,6 +239,13 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
_toFocusNode = FocusNode();
|
||||
_refundFocusNode = FocusNode();
|
||||
|
||||
doesRefundAddress = ref.read(efExchangeProvider).supportsRefundAddress;
|
||||
|
||||
if (!doesRefundAddress) {
|
||||
// hack: set to empty to not throw null unwrap error later
|
||||
ref.read(desktopExchangeModelProvider)!.refundAddress = "";
|
||||
}
|
||||
|
||||
final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state;
|
||||
if (tuple != null) {
|
||||
if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() ==
|
||||
|
@ -243,8 +259,9 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
ref.read(desktopExchangeModelProvider)!.recipientAddress =
|
||||
_toController.text;
|
||||
} else {
|
||||
if (ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() ==
|
||||
tuple.item2.ticker.toUpperCase()) {
|
||||
if (doesRefundAddress &&
|
||||
ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() ==
|
||||
tuple.item2.ticker.toUpperCase()) {
|
||||
_refundController.text = ref
|
||||
.read(pWallets)
|
||||
.getWallet(tuple.item1)
|
||||
|
@ -309,7 +326,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
),
|
||||
))
|
||||
CustomTextButton(
|
||||
text: "Choose from Stack",
|
||||
text: "Choose from ${AppConfig.prefix}",
|
||||
onTap: selectRecipientAddressFromStack,
|
||||
),
|
||||
],
|
||||
|
@ -341,8 +358,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
style: STextStyles.field(context),
|
||||
onChanged: (value) {
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty,
|
||||
_next(),
|
||||
);
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
|
@ -376,8 +392,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
.read(desktopExchangeModelProvider)!
|
||||
.recipientAddress = _toController.text;
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty,
|
||||
_next(),
|
||||
);
|
||||
},
|
||||
child: const XIcon(),
|
||||
|
@ -397,8 +412,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
.read(desktopExchangeModelProvider)!
|
||||
.recipientAddress = _toController.text;
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty,
|
||||
_next(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -435,155 +449,158 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
|
|||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Refund Wallet (required)",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconRight,
|
||||
),
|
||||
),
|
||||
if (AppConfig.isStackCoin(
|
||||
ref.watch(
|
||||
desktopExchangeModelProvider
|
||||
.select((value) => value!.sendTicker),
|
||||
),
|
||||
))
|
||||
CustomTextButton(
|
||||
text: "Choose from Stack",
|
||||
onTap: selectRefundAddressFromStack,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
if (doesRefundAddress)
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("refundExchangeStep2ViewAddressFieldKey"),
|
||||
controller: _refundController,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
// inputFormatters: <TextInputFormatter>[
|
||||
// FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")),
|
||||
// ],
|
||||
toolbarOptions: const ToolbarOptions(
|
||||
copy: false,
|
||||
cut: false,
|
||||
paste: true,
|
||||
selectAll: false,
|
||||
),
|
||||
focusNode: _refundFocusNode,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (value) {
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty,
|
||||
);
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address",
|
||||
_refundFocusNode,
|
||||
context,
|
||||
desktopMed: true,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
if (doesRefundAddress)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Refund Wallet (required)",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconRight,
|
||||
),
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _refundController.text.isEmpty
|
||||
? const EdgeInsets.only(right: 16)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_refundController.text.isNotEmpty
|
||||
? TextFieldIconButton(
|
||||
key: const Key(
|
||||
"sendViewClearAddressFieldButtonKey",
|
||||
),
|
||||
onTap: () {
|
||||
_refundController.text = "";
|
||||
ref
|
||||
.read(desktopExchangeModelProvider)!
|
||||
.refundAddress = _refundController.text;
|
||||
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty,
|
||||
);
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
key: const Key(
|
||||
"sendViewPasteAddressFieldButtonKey",
|
||||
),
|
||||
onTap: () async {
|
||||
final ClipboardData? data = await clipboard
|
||||
.getData(Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
final content = data.text!.trim();
|
||||
|
||||
_refundController.text = content;
|
||||
if (AppConfig.isStackCoin(
|
||||
ref.watch(
|
||||
desktopExchangeModelProvider
|
||||
.select((value) => value!.sendTicker),
|
||||
),
|
||||
))
|
||||
CustomTextButton(
|
||||
text: "Choose from ${AppConfig.prefix}",
|
||||
onTap: selectRefundAddressFromStack,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (doesRefundAddress)
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (doesRefundAddress)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("refundExchangeStep2ViewAddressFieldKey"),
|
||||
controller: _refundController,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
// inputFormatters: <TextInputFormatter>[
|
||||
// FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")),
|
||||
// ],
|
||||
toolbarOptions: const ToolbarOptions(
|
||||
copy: false,
|
||||
cut: false,
|
||||
paste: true,
|
||||
selectAll: false,
|
||||
),
|
||||
focusNode: _refundFocusNode,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (value) {
|
||||
widget.enableNextChanged.call(
|
||||
_next(),
|
||||
);
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address",
|
||||
_refundFocusNode,
|
||||
context,
|
||||
desktopMed: true,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _refundController.text.isEmpty
|
||||
? const EdgeInsets.only(right: 16)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_refundController.text.isNotEmpty
|
||||
? TextFieldIconButton(
|
||||
key: const Key(
|
||||
"sendViewClearAddressFieldButtonKey",
|
||||
),
|
||||
onTap: () {
|
||||
_refundController.text = "";
|
||||
ref
|
||||
.read(desktopExchangeModelProvider)!
|
||||
.refundAddress = _refundController.text;
|
||||
|
||||
widget.enableNextChanged.call(
|
||||
_toController.text.isNotEmpty &&
|
||||
_refundController.text.isNotEmpty,
|
||||
_next(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: _refundController.text.isEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (_refundController.text.isEmpty &&
|
||||
AppConfig.isStackCoin(
|
||||
ref.watch(
|
||||
desktopExchangeModelProvider
|
||||
.select((value) => value!.sendTicker),
|
||||
),
|
||||
))
|
||||
TextFieldIconButton(
|
||||
key: const Key("sendViewAddressBookButtonKey"),
|
||||
onTap: selectRefundFromAddressBook,
|
||||
child: const AddressBookIcon(),
|
||||
),
|
||||
],
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
key: const Key(
|
||||
"sendViewPasteAddressFieldButtonKey",
|
||||
),
|
||||
onTap: () async {
|
||||
final ClipboardData? data = await clipboard
|
||||
.getData(Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
final content = data.text!.trim();
|
||||
|
||||
_refundController.text = content;
|
||||
ref
|
||||
.read(desktopExchangeModelProvider)!
|
||||
.refundAddress = _refundController.text;
|
||||
|
||||
widget.enableNextChanged.call(
|
||||
_next(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: _refundController.text.isEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (_refundController.text.isEmpty &&
|
||||
AppConfig.isStackCoin(
|
||||
ref.watch(
|
||||
desktopExchangeModelProvider
|
||||
.select((value) => value!.sendTicker),
|
||||
),
|
||||
))
|
||||
TextFieldIconButton(
|
||||
key: const Key("sendViewAddressBookButtonKey"),
|
||||
onTap: selectRefundFromAddressBook,
|
||||
child: const AddressBookIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
borderColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
child: Text(
|
||||
"In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
if (doesRefundAddress)
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (doesRefundAddress)
|
||||
RoundedWhiteContainer(
|
||||
borderColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
child: Text(
|
||||
"In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,13 +10,14 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../step_scaffold.dart';
|
||||
import 'desktop_step_item.dart';
|
||||
|
||||
import '../../../../providers/providers.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/enums/exchange_rate_type_enum.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
import '../step_scaffold.dart';
|
||||
import 'desktop_step_item.dart';
|
||||
|
||||
class DesktopStep3 extends ConsumerStatefulWidget {
|
||||
const DesktopStep3({
|
||||
|
@ -97,20 +98,22 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> {
|
|||
) ??
|
||||
"Error",
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).extension<StackColors>()!.background,
|
||||
),
|
||||
DesktopStepItem(
|
||||
vertical: true,
|
||||
label:
|
||||
"Refund ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} address",
|
||||
value: ref.watch(
|
||||
desktopExchangeModelProvider
|
||||
.select((value) => value!.refundAddress),
|
||||
) ??
|
||||
"Error",
|
||||
),
|
||||
if (ref.watch(efExchangeProvider).supportsRefundAddress)
|
||||
Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).extension<StackColors>()!.background,
|
||||
),
|
||||
if (ref.watch(efExchangeProvider).supportsRefundAddress)
|
||||
DesktopStepItem(
|
||||
vertical: true,
|
||||
label:
|
||||
"Refund ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} address",
|
||||
value: ref.watch(
|
||||
desktopExchangeModelProvider
|
||||
.select((value) => value!.refundAddress),
|
||||
) ??
|
||||
"Error",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../../app_config.dart';
|
||||
import '../../../providers/providers.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
import '../../../utilities/amount/amount.dart';
|
||||
|
@ -87,7 +88,7 @@ class _DesktopChooseFromStackState
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Choose from Stack",
|
||||
"Choose from ${AppConfig.prefix}",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
const SizedBox(
|
||||
|
|
|
@ -29,11 +29,15 @@ import '../../../../utilities/address_utils.dart';
|
|||
import '../../../../utilities/assets.dart';
|
||||
import '../../../../utilities/clipboard_interface.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
import '../../../../utilities/enums/derive_path_type_enum.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../utilities/util.dart';
|
||||
import '../../../../wallets/crypto_currency/crypto_currency.dart';
|
||||
import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart';
|
||||
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../../../wallets/wallet/impl/bitcoin_wallet.dart';
|
||||
import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
||||
import '../../../../widgets/conditional_parent.dart';
|
||||
|
@ -65,10 +69,13 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
late final String walletId;
|
||||
late final ClipboardInterface clipboard;
|
||||
late final bool supportsSpark;
|
||||
late final bool showMultiType;
|
||||
|
||||
String? _sparkAddress;
|
||||
String? _qrcodeContent;
|
||||
bool _showSparkAddress = true;
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<AddressType> _walletAddressTypes = [];
|
||||
final Map<AddressType, String> _addressMap = {};
|
||||
final Map<AddressType, StreamSubscription<Address?>> _addressSubMap = {};
|
||||
|
||||
Future<void> generateNewAddress() async {
|
||||
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||
|
@ -95,12 +102,31 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
),
|
||||
);
|
||||
|
||||
await wallet.generateNewReceivingAddress();
|
||||
final Address? address;
|
||||
if (wallet is Bip39HDWallet && wallet is! BCashInterface) {
|
||||
final type = DerivePathType.values.firstWhere(
|
||||
(e) => e.getAddressType() == _walletAddressTypes[_currentIndex],
|
||||
);
|
||||
address = await wallet.generateNextReceivingAddress(
|
||||
derivePathType: type,
|
||||
);
|
||||
await ref.read(mainDBProvider).isar.writeTxn(() async {
|
||||
await ref.read(mainDBProvider).isar.addresses.put(address!);
|
||||
});
|
||||
} else {
|
||||
await wallet.generateNewReceivingAddress();
|
||||
address = null;
|
||||
}
|
||||
|
||||
shouldPop = true;
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
|
||||
setState(() {
|
||||
_addressMap[_walletAddressTypes[_currentIndex]] =
|
||||
address?.value ?? ref.read(pWalletReceivingAddress(walletId));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,11 +165,9 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
if (_sparkAddress != address.value) {
|
||||
setState(() {
|
||||
_sparkAddress = address.value;
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
_addressMap[AddressType.spark] = address.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,29 +179,56 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
walletId = widget.walletId;
|
||||
coin = ref.read(pWalletInfo(walletId)).coin;
|
||||
clipboard = widget.clipboard;
|
||||
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||
supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface;
|
||||
showMultiType = supportsSpark ||
|
||||
ref.read(pWallets).getWallet(walletId) is MultiAddressInterface;
|
||||
|
||||
if (supportsSpark) {
|
||||
_streamSub = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.addresses
|
||||
.where()
|
||||
.walletIdEqualTo(walletId)
|
||||
.filter()
|
||||
.typeEqualTo(AddressType.spark)
|
||||
.sortByDerivationIndexDesc()
|
||||
.findFirst()
|
||||
.asStream()
|
||||
.listen((event) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_sparkAddress = event?.value;
|
||||
});
|
||||
}
|
||||
_walletAddressTypes.add(wallet.info.mainAddressType);
|
||||
|
||||
if (showMultiType) {
|
||||
if (supportsSpark) {
|
||||
_walletAddressTypes.insert(0, AddressType.spark);
|
||||
} else {
|
||||
_walletAddressTypes.addAll(
|
||||
(wallet as Bip39HDWallet)
|
||||
.supportedAddressTypes
|
||||
.where((e) => e != wallet.info.mainAddressType),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) {
|
||||
_walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh);
|
||||
}
|
||||
|
||||
_addressMap[_walletAddressTypes[_currentIndex]] =
|
||||
ref.read(pWalletReceivingAddress(walletId));
|
||||
|
||||
if (showMultiType) {
|
||||
for (final type in _walletAddressTypes) {
|
||||
_addressSubMap[type] = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.addresses
|
||||
.where()
|
||||
.walletIdEqualTo(walletId)
|
||||
.filter()
|
||||
.typeEqualTo(type)
|
||||
.sortByDerivationIndexDesc()
|
||||
.findFirst()
|
||||
.asStream()
|
||||
.listen((event) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_addressMap[type] =
|
||||
event?.value ?? _addressMap[type] ?? "[No address yet]";
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
super.initState();
|
||||
|
@ -193,47 +244,41 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
||||
if (supportsSpark) {
|
||||
if (_showSparkAddress) {
|
||||
_qrcodeContent = _sparkAddress;
|
||||
} else {
|
||||
_qrcodeContent = ref.watch(pWalletReceivingAddress(walletId));
|
||||
}
|
||||
final String address;
|
||||
if (showMultiType) {
|
||||
address = _addressMap[_walletAddressTypes[_currentIndex]]!;
|
||||
} else {
|
||||
_qrcodeContent = ref.watch(pWalletReceivingAddress(walletId));
|
||||
address = ref.watch(pWalletReceivingAddress(walletId));
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ConditionalParent(
|
||||
condition: supportsSpark,
|
||||
condition: showMultiType,
|
||||
builder: (child) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<bool>(
|
||||
value: _showSparkAddress,
|
||||
child: DropdownButton2<int>(
|
||||
value: _currentIndex,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: true,
|
||||
child: Text(
|
||||
"Spark address",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
for (int i = 0; i < _walletAddressTypes.length; i++)
|
||||
DropdownMenuItem(
|
||||
value: i,
|
||||
child: Text(
|
||||
supportsSpark &&
|
||||
_walletAddressTypes[i] == AddressType.p2pkh
|
||||
? "Transparent address"
|
||||
: "${_walletAddressTypes[i].readableName} address",
|
||||
style: STextStyles.w500_14(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: false,
|
||||
child: Text(
|
||||
"Transparent address",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value is bool && value != _showSparkAddress) {
|
||||
if (value != null && value != _currentIndex) {
|
||||
setState(() {
|
||||
_showSparkAddress = value;
|
||||
_currentIndex = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -251,6 +296,16 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
),
|
||||
),
|
||||
),
|
||||
buttonStyleData: ButtonStyleData(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
offset: const Offset(0, -10),
|
||||
elevation: 0,
|
||||
|
@ -274,95 +329,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (_showSparkAddress)
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
clipboard.setData(
|
||||
ClipboardData(text: _sparkAddress ?? "Error"),
|
||||
);
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.backgroundAppBar,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
child: RoundedWhiteContainer(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"Your ${widget.contractAddress == null ? coin.ticker : ref.watch(
|
||||
pCurrentTokenWallet.select(
|
||||
(value) => value!.tokenContract.symbol,
|
||||
),
|
||||
)} SPARK address",
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.copy,
|
||||
width: 15,
|
||||
height: 15,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.infoItemIcons,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
"Copy",
|
||||
style: STextStyles.link2(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_sparkAddress ?? "Error",
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(
|
||||
context,
|
||||
).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_showSparkAddress) child,
|
||||
child,
|
||||
],
|
||||
),
|
||||
child: MouseRegion(
|
||||
|
@ -371,7 +338,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
onTap: () {
|
||||
clipboard.setData(
|
||||
ClipboardData(
|
||||
text: ref.watch(pWalletReceivingAddress(walletId)),
|
||||
text: address,
|
||||
),
|
||||
);
|
||||
showFloatingFlushBar(
|
||||
|
@ -435,7 +402,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
ref.watch(pWalletReceivingAddress(walletId)),
|
||||
address,
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context,
|
||||
).copyWith(
|
||||
|
@ -467,7 +434,8 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
supportsSpark)
|
||||
SecondaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: supportsSpark && _showSparkAddress
|
||||
onPressed: supportsSpark &&
|
||||
_walletAddressTypes[_currentIndex] == AddressType.spark
|
||||
? generateNewSparkAddress
|
||||
: generateNewAddress,
|
||||
label: "Generate new address",
|
||||
|
@ -479,7 +447,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
coin,
|
||||
_qrcodeContent ?? "",
|
||||
address,
|
||||
{},
|
||||
),
|
||||
size: 200,
|
||||
|
@ -518,7 +486,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
RouteGenerator.generateRoute(
|
||||
RouteSettings(
|
||||
name: GenerateUriQrCodeView.routeName,
|
||||
arguments: Tuple2(coin, _qrcodeContent ?? ""),
|
||||
arguments: Tuple2(coin, address),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -535,7 +503,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
|
||||
builder: (_) => GenerateUriQrCodeView(
|
||||
coin: coin,
|
||||
receivingAddress: _qrcodeContent ?? "",
|
||||
receivingAddress: address,
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name: GenerateUriQrCodeView.routeName,
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../../../../../app_config.dart';
|
||||
import '../../../../../db/sqlite/firo_cache.dart';
|
||||
import '../../../../../providers/db/main_db_provider.dart';
|
||||
import '../../../../../providers/global/prefs_provider.dart';
|
||||
|
@ -19,6 +20,7 @@ import '../../../../../providers/global/wallets_provider.dart';
|
|||
import '../../../../../themes/stack_colors.dart';
|
||||
import '../../../../../utilities/assets.dart';
|
||||
import '../../../../../utilities/text_styles.dart';
|
||||
import '../../../../../utilities/util.dart';
|
||||
import '../../../../../wallets/crypto_currency/crypto_currency.dart';
|
||||
import '../../../../../wallets/isar/models/wallet_info.dart';
|
||||
import '../../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
|
@ -32,6 +34,8 @@ import '../../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.da
|
|||
import '../../../../../widgets/custom_buttons/draggable_switch_button.dart';
|
||||
import '../../../../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../../../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import '../../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../../widgets/desktop/secondary_button.dart';
|
||||
import '../../../../../widgets/rounded_container.dart';
|
||||
|
||||
class MoreFeaturesDialog extends ConsumerStatefulWidget {
|
||||
|
@ -102,6 +106,117 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
|||
}
|
||||
}
|
||||
|
||||
bool _switchReuseAddressToggledLock = false; // Mutex.
|
||||
Future<void> _switchReuseAddressToggled(bool newValue) async {
|
||||
if (newValue) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final isDesktop = Util.isDesktop;
|
||||
return DesktopDialog(
|
||||
maxWidth: 576,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Text(
|
||||
"Warning!",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
),
|
||||
const DesktopDialogCloseButton(),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8,
|
||||
left: 32,
|
||||
right: 32,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?",
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 43,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
label: "Cancel",
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
label: "Continue",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
).then((confirmed) async {
|
||||
if (_switchReuseAddressToggledLock) {
|
||||
return;
|
||||
}
|
||||
_switchReuseAddressToggledLock = true; // Lock mutex.
|
||||
|
||||
try {
|
||||
if (confirmed == true) {
|
||||
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
|
||||
newEntries: {
|
||||
WalletInfoKeys.reuseAddress: true,
|
||||
},
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
} else {
|
||||
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
|
||||
newEntries: {
|
||||
WalletInfoKeys.reuseAddress: false,
|
||||
},
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
// ensure _switchReuseAddressToggledLock is set to false no matter what.
|
||||
_switchReuseAddressToggledLock = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
|
||||
newEntries: {
|
||||
WalletInfoKeys.reuseAddress: false,
|
||||
},
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final wallet = ref.watch(
|
||||
|
@ -167,7 +282,7 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
|||
if (wallet is OrdinalsInterface)
|
||||
_MoreFeaturesItem(
|
||||
label: "Ordinals",
|
||||
detail: "View and control your ordinals in Stack",
|
||||
detail: "View and control your ordinals in ${AppConfig.prefix}",
|
||||
iconAsset: Assets.svg.ordinal,
|
||||
onPressed: () async => widget.onOrdinalsPressed?.call(),
|
||||
),
|
||||
|
@ -253,6 +368,38 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
|||
],
|
||||
),
|
||||
),
|
||||
// reuseAddress preference.
|
||||
_MoreFeaturesItemBase(
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 3),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
pWalletInfo(widget.walletId)
|
||||
.select((value) => value.otherData),
|
||||
)[WalletInfoKeys.reuseAddress] as bool? ??
|
||||
false,
|
||||
onValueChanged: _switchReuseAddressToggled,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Reuse receiving address by default",
|
||||
style: STextStyles.w600_20(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 28,
|
||||
),
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../../../models/keys/key_data_interface.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../providers/desktop/storage_crypto_handler_provider.dart';
|
||||
import '../../../../providers/providers.dart';
|
||||
|
@ -22,6 +23,8 @@ import '../../../../utilities/assets.dart';
|
|||
import '../../../../utilities/constants.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
|
@ -100,12 +103,21 @@ class _UnlockWalletKeysDesktopState
|
|||
words = await wallet.getMnemonicAsWords();
|
||||
}
|
||||
|
||||
KeyDataInterface? keyData;
|
||||
if (wallet is ExtendedKeysInterface) {
|
||||
keyData = await wallet.getXPrivs();
|
||||
} else if (wallet is CwBasedInterface) {
|
||||
keyData = await wallet.getKeys();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
await Navigator.of(context).pushReplacementNamed(
|
||||
WalletKeysDesktopPopup.routeName,
|
||||
arguments: (
|
||||
mnemonic: words ?? [],
|
||||
walletId: widget.walletId,
|
||||
frostData: frostData,
|
||||
keyData: keyData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -338,13 +350,22 @@ class _UnlockWalletKeysDesktopState
|
|||
words = await wallet.getMnemonicAsWords();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
KeyDataInterface? keyData;
|
||||
if (wallet is ExtendedKeysInterface) {
|
||||
keyData = await wallet.getXPrivs();
|
||||
} else if (wallet is CwBasedInterface) {
|
||||
keyData = await wallet.getKeys();
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
await Navigator.of(context)
|
||||
.pushReplacementNamed(
|
||||
WalletKeysDesktopPopup.routeName,
|
||||
arguments: (
|
||||
mnemonic: words ?? [],
|
||||
walletId: widget.walletId,
|
||||
frostData: frostData,
|
||||
keyData: keyData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,15 +12,22 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../models/keys/cw_key_data.dart';
|
||||
import '../../../../models/keys/key_data_interface.dart';
|
||||
import '../../../../models/keys/xpriv_data.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
|
||||
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart';
|
||||
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart';
|
||||
import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/address_utils.dart';
|
||||
import '../../../../utilities/assets.dart';
|
||||
import '../../../../utilities/clipboard_interface.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../widgets/custom_tab_view.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
|
@ -28,22 +35,26 @@ import '../../../../widgets/desktop/secondary_button.dart';
|
|||
import '../../../../widgets/rounded_white_container.dart';
|
||||
import 'qr_code_desktop_popup_content.dart';
|
||||
|
||||
class WalletKeysDesktopPopup extends StatelessWidget {
|
||||
class WalletKeysDesktopPopup extends ConsumerWidget {
|
||||
const WalletKeysDesktopPopup({
|
||||
super.key,
|
||||
required this.words,
|
||||
required this.walletId,
|
||||
this.frostData,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
this.keyData,
|
||||
});
|
||||
|
||||
final List<String> words;
|
||||
final String walletId;
|
||||
final ({String keys, String config})? frostData;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
final KeyDataInterface? keyData;
|
||||
|
||||
static const String routeName = "walletKeysDesktopPopup";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 614,
|
||||
maxHeight: double.infinity,
|
||||
|
@ -69,7 +80,7 @@ class WalletKeysDesktopPopup extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 28,
|
||||
height: 6,
|
||||
),
|
||||
frostData != null
|
||||
? Column(
|
||||
|
@ -168,94 +179,35 @@ class WalletKeysDesktopPopup extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Recovery phrase",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
: keyData != null
|
||||
? CustomTabView(
|
||||
titles: [
|
||||
"Mnemonic",
|
||||
if (keyData is XPrivData) "XPriv(s)",
|
||||
if (keyData is CWKeyData) "Keys",
|
||||
],
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: _Mnemonic(
|
||||
words: words,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.",
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: MnemonicTable(
|
||||
words: words,
|
||||
isDesktop: true,
|
||||
itemBorderColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.buttonBackSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Show QR code",
|
||||
onPressed: () {
|
||||
// TODO: address utils
|
||||
final String value =
|
||||
AddressUtils.encodeQRSeedData(words);
|
||||
Navigator.of(context).pushNamed(
|
||||
QRCodeDesktopPopupContent.routeName,
|
||||
arguments: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (keyData is XPrivData)
|
||||
WalletXPrivs(
|
||||
xprivData: keyData as XPrivData,
|
||||
walletId: walletId,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
if (keyData is CWKeyData)
|
||||
CNWalletKeys(
|
||||
cwKeyData: keyData as CWKeyData,
|
||||
walletId: walletId,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Copy",
|
||||
onPressed: () async {
|
||||
await clipboardInterface.setData(
|
||||
ClipboardData(text: words.join(" ")),
|
||||
);
|
||||
if (context.mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: _Mnemonic(
|
||||
words: words,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
|
@ -264,3 +216,105 @@ class WalletKeysDesktopPopup extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Mnemonic extends StatelessWidget {
|
||||
const _Mnemonic({
|
||||
super.key,
|
||||
required this.words,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
});
|
||||
|
||||
final List<String> words;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
"Recovery phrase",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: Text(
|
||||
"Please write down your recovery phrase in the correct order and "
|
||||
"save it to keep your funds secure. You will also be asked to"
|
||||
" verify the words on the next screen.",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: MnemonicTable(
|
||||
words: words,
|
||||
isDesktop: true,
|
||||
itemBorderColor:
|
||||
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Copy",
|
||||
onPressed: () async {
|
||||
await clipboardInterface.setData(
|
||||
ClipboardData(text: words.join(" ")),
|
||||
);
|
||||
if (context.mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Show QR code",
|
||||
onPressed: () {
|
||||
// TODO: address utils
|
||||
final String value = AddressUtils.encodeQRSeedData(words);
|
||||
Navigator.of(context).pushNamed(
|
||||
QRCodeDesktopPopupContent.routeName,
|
||||
arguments: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,15 +18,19 @@ import 'package:flutter_svg/svg.dart';
|
|||
import '../../../../pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
|
||||
import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
|
||||
import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
|
||||
import '../../../../providers/global/wallets_provider.dart';
|
||||
import '../../../../route_generator.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/assets.dart';
|
||||
import '../../../../utilities/constants.dart';
|
||||
import '../../../../utilities/show_loading.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../utilities/util.dart';
|
||||
import '../../../../wallets/crypto_currency/coins/firo.dart';
|
||||
import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart';
|
||||
import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
|
||||
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||
import '../../../addresses/desktop_wallet_addresses_view.dart';
|
||||
import '../../../lelantus_coins/lelantus_coins_view.dart';
|
||||
import '../../../spark_coins/spark_coins_view.dart';
|
||||
|
@ -61,7 +65,7 @@ enum _WalletOptions {
|
|||
}
|
||||
}
|
||||
|
||||
class WalletOptionsButton extends StatelessWidget {
|
||||
class WalletOptionsButton extends ConsumerWidget {
|
||||
const WalletOptionsButton({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
|
@ -70,7 +74,7 @@ class WalletOptionsButton extends StatelessWidget {
|
|||
final String walletId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return RawMaterialButton(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 32,
|
||||
|
@ -148,28 +152,40 @@ class WalletOptionsButton extends StatelessWidget {
|
|||
}
|
||||
break;
|
||||
case _WalletOptions.showXpub:
|
||||
final result = await showDialog<bool?>(
|
||||
final xpubData = await showLoading(
|
||||
delay: const Duration(milliseconds: 800),
|
||||
whileFuture: (ref.read(pWallets).getWallet(walletId)
|
||||
as ExtendedKeysInterface)
|
||||
.getXPubs(),
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => Navigator(
|
||||
initialRoute: XPubView.routeName,
|
||||
onGenerateRoute: RouteGenerator.generateRoute,
|
||||
onGenerateInitialRoutes: (_, __) {
|
||||
return [
|
||||
RouteGenerator.generateRoute(
|
||||
RouteSettings(
|
||||
name: XPubView.routeName,
|
||||
arguments: walletId,
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
message: "Loading xpubs",
|
||||
rootNavigator: Util.isDesktop,
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
if (context.mounted) {
|
||||
final result = await showDialog<bool?>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => Navigator(
|
||||
initialRoute: XPubView.routeName,
|
||||
onGenerateRoute: RouteGenerator.generateRoute,
|
||||
onGenerateInitialRoutes: (_, __) {
|
||||
return [
|
||||
RouteGenerator.generateRoute(
|
||||
RouteSettings(
|
||||
name: XPubView.routeName,
|
||||
arguments: (walletId, xpubData),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -279,9 +295,8 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
|
|||
|
||||
final firoDebug = kDebugMode && (coin is Firo);
|
||||
|
||||
// TODO: [prio=low]
|
||||
// final bool xpubEnabled = manager.hasXPub;
|
||||
final bool xpubEnabled = false;
|
||||
final bool xpubEnabled =
|
||||
ref.watch(pWallets).getWallet(walletId) is ExtendedKeysInterface;
|
||||
|
||||
final bool canChangeRep = coin is NanoCurrency;
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../app_config.dart';
|
||||
import '../../db/hive/db.dart';
|
||||
import '../../notifications/show_flush_bar.dart';
|
||||
import '../../pages/intro_view.dart';
|
||||
|
@ -143,7 +144,7 @@ class _ForgotPasswordDesktopViewState
|
|||
),
|
||||
TextSpan(
|
||||
text: widget.shouldCreateNew
|
||||
? "create a new Stack"
|
||||
? "create a new ${AppConfig.prefix}"
|
||||
: "restore from backup",
|
||||
style: STextStyles.desktopTextSmallBold(context),
|
||||
),
|
||||
|
|
|
@ -74,7 +74,9 @@ class _ForgotPasswordDesktopViewState
|
|||
SizedBox(
|
||||
width: 400,
|
||||
child: Text(
|
||||
"${AppConfig.appName} does not store your password. Create new wallet or use a Stack backup file to restore your wallet.",
|
||||
"${AppConfig.appName} does not store your password. "
|
||||
"Create new wallet or use a ${AppConfig.prefix} "
|
||||
"backup file to restore your wallet.",
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.desktopTextSmall(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
|
@ -87,7 +89,7 @@ class _ForgotPasswordDesktopViewState
|
|||
height: 48,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Create new Stack",
|
||||
label: "Create new ${AppConfig.prefix}",
|
||||
onPressed: () {
|
||||
const shouldCreateNew = true;
|
||||
Navigator.of(context).pushNamed(
|
||||
|
@ -100,7 +102,7 @@ class _ForgotPasswordDesktopViewState
|
|||
height: 24,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: "Restore from Stack backup",
|
||||
label: "Restore from ${AppConfig.prefix} backup",
|
||||
onPressed: () {
|
||||
const shouldCreateNew = false;
|
||||
Navigator.of(context).pushNamed(
|
||||
|
|
|
@ -94,7 +94,7 @@ class _ForgottenPassphraseRestoreFromSWBState
|
|||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Decrypting Stack backup file",
|
||||
"Decrypting ${AppConfig.prefix} backup file",
|
||||
style: STextStyles.pageTitleH2(context).copyWith(
|
||||
color:
|
||||
Theme.of(context).extension<StackColors>()!.textWhite,
|
||||
|
@ -245,7 +245,7 @@ class _ForgottenPassphraseRestoreFromSWBState
|
|||
height: 32,
|
||||
),
|
||||
Text(
|
||||
"Use your Stack wallet backup file to restore your wallets, address book, and wallet preferences.",
|
||||
"Use your ${AppConfig.prefix} backup file to restore your wallets, address book, and wallet preferences.",
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.desktopTextSmall(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../notifications/show_flush_bar.dart';
|
||||
import '../../providers/desktop/storage_crypto_handler_provider.dart';
|
||||
import '../../themes/stack_colors.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
import '../../utilities/constants.dart';
|
||||
import '../../utilities/show_loading.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
import '../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import '../../widgets/desktop/primary_button.dart';
|
||||
import '../../widgets/desktop/secondary_button.dart';
|
||||
import '../../widgets/stack_text_field.dart';
|
||||
|
||||
class RequestDesktopAuthDialog extends ConsumerStatefulWidget {
|
||||
const RequestDesktopAuthDialog({
|
||||
super.key,
|
||||
this.title,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
ConsumerState<RequestDesktopAuthDialog> createState() =>
|
||||
_RequestDesktopAuthDialogState();
|
||||
}
|
||||
|
||||
class _RequestDesktopAuthDialogState
|
||||
extends ConsumerState<RequestDesktopAuthDialog> {
|
||||
late final TextEditingController passwordController;
|
||||
late final FocusNode passwordFocusNode;
|
||||
|
||||
bool continueEnabled = false;
|
||||
bool hidePassword = true;
|
||||
|
||||
bool _lock = false;
|
||||
Future<void> _auth() async {
|
||||
if (_lock) {
|
||||
return;
|
||||
}
|
||||
_lock = true;
|
||||
|
||||
try {
|
||||
final verified = await showLoading(
|
||||
whileFuture: ref
|
||||
.read(storageCryptoHandlerProvider)
|
||||
.verifyPassphrase(passwordController.text),
|
||||
context: context,
|
||||
message: "Checking...",
|
||||
rootNavigator: true,
|
||||
delay: const Duration(milliseconds: 1000),
|
||||
);
|
||||
|
||||
if (verified == true) {
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop("verified success");
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message: "Invalid passphrase!",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_lock = 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 DesktopDialog(
|
||||
maxWidth: 579,
|
||||
maxHeight: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
DesktopDialogCloseButton(
|
||||
onPressedOverride: Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).pop,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
SvgPicture.asset(
|
||||
Assets.svg.keys,
|
||||
width: 100,
|
||||
height: 58,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 55,
|
||||
),
|
||||
if (widget.title != null)
|
||||
Text(
|
||||
widget.title!,
|
||||
style: STextStyles.desktopH2(context),
|
||||
),
|
||||
if (widget.title != null)
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
"Enter your password",
|
||||
style: STextStyles.desktopTextMedium(context).copyWith(
|
||||
color: Theme.of(context).extension<StackColors>()!.textDark3,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("enterPasswordUnlockWalletKeysDesktopFieldKey"),
|
||||
focusNode: passwordFocusNode,
|
||||
controller: passwordController,
|
||||
style: STextStyles.desktopTextMedium(context).copyWith(
|
||||
height: 2,
|
||||
),
|
||||
obscureText: hidePassword,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
onSubmitted: (_) {
|
||||
if (continueEnabled) {
|
||||
_auth();
|
||||
}
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter password",
|
||||
passwordFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
suffixIcon: UnconstrainedBox(
|
||||
child: SizedBox(
|
||||
height: 70,
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
key: const Key(
|
||||
"enterUnlockWalletKeysDesktopFieldShowPasswordButtonKey",
|
||||
),
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
hidePassword = !hidePassword;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(1000),
|
||||
),
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
hidePassword
|
||||
? Assets.svg.eye
|
||||
: Assets.svg.eyeSlash,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark3,
|
||||
width: 24,
|
||||
height: 19,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
continueEnabled = newValue.isNotEmpty;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 55,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Cancel",
|
||||
onPressed: Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).pop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Continue",
|
||||
enabled: continueEnabled,
|
||||
onPressed: continueEnabled ? _auth : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../pages/settings_views/global_settings_view/advanced_views/manage_coin_units/manage_coin_units_view.dart';
|
||||
import '../../../../providers/global/prefs_provider.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
|
@ -184,7 +185,7 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Stack Experience",
|
||||
"${AppConfig.prefix} Experience",
|
||||
style: STextStyles.desktopTextExtraSmall(
|
||||
context,
|
||||
).copyWith(
|
||||
|
|
|
@ -226,9 +226,9 @@ class _DesktopEditBlockExplorerDialogState
|
|||
" every block explorer has a slightly different URL scheme."
|
||||
"\n\n"
|
||||
"Paste in your block explorer of choice, then edit in"
|
||||
" [TXID] where the transaction ID should go, and Stack"
|
||||
" Wallet will auto fill the transaction ID in that place"
|
||||
" of the URL.",
|
||||
" [TXID] where the transaction ID should go, and "
|
||||
"${AppConfig.appName} will auto fill the transaction"
|
||||
" ID in that place of the URL.",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../db/hive/db.dart';
|
||||
import '../../../../providers/global/prefs_provider.dart';
|
||||
import '../../../../providers/global/price_provider.dart';
|
||||
|
@ -69,7 +70,7 @@ class _StackPrivacyDialog extends ConsumerState<StackPrivacyDialog> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
"Choose Your Stack Experience",
|
||||
"Choose Your ${AppConfig.prefix} Experience",
|
||||
style: STextStyles.desktopH3(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
@ -192,9 +193,11 @@ class _StackPrivacyDialog extends ConsumerState<StackPrivacyDialog> {
|
|||
)
|
||||
.then((_) {
|
||||
if (isEasy) {
|
||||
unawaited(
|
||||
ExchangeDataLoadingService.instance.loadAll(),
|
||||
);
|
||||
if (AppConfig.hasFeature(AppFeature.swap)) {
|
||||
unawaited(
|
||||
ExchangeDataLoadingService.instance.loadAll(),
|
||||
);
|
||||
}
|
||||
ref
|
||||
.read(priceAnd24hChangeNotifierProvider)
|
||||
.start(true);
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'package:flutter_svg/svg.dart';
|
|||
import 'package:stack_wallet_backup/stack_wallet_backup.dart';
|
||||
import 'package:zxcvbn/zxcvbn.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
|
||||
import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart';
|
||||
|
@ -776,7 +777,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
|
|||
if (Platform.isAndroid) {
|
||||
return StackOkDialog(
|
||||
title:
|
||||
"Stack Auto Backup enabled and saved to:",
|
||||
"${AppConfig.prefix} Auto Backup enabled and saved to:",
|
||||
message: fileToSave,
|
||||
);
|
||||
} else if (Util.isDesktop) {
|
||||
|
@ -800,7 +801,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
|
|||
.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Stack Auto Backup enabled!",
|
||||
"${AppConfig.prefix} Auto Backup enabled!",
|
||||
style:
|
||||
STextStyles.desktopH3(
|
||||
context,
|
||||
|
@ -834,7 +835,8 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
|
|||
);
|
||||
} else {
|
||||
return const StackOkDialog(
|
||||
title: "Stack Auto Backup enabled!",
|
||||
title:
|
||||
"${AppConfig.prefix} Auto Backup enabled!",
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../../app_config.dart';
|
||||
import '../../../pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart';
|
||||
import '../../../providers/global/prefs_provider.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
|
@ -115,7 +116,7 @@ class _SyncingPreferencesSettings
|
|||
),
|
||||
TextSpan(
|
||||
text:
|
||||
"\n\nSet up your syncing preferences for all wallets in your Stack.",
|
||||
"\n\nSet up your syncing preferences for all wallets in your ${AppConfig.prefix}.",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context,
|
||||
),
|
||||
|
|
|
@ -22,6 +22,7 @@ import 'models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
|||
import 'models/isar/models/contact_entry.dart';
|
||||
import 'models/isar/models/isar_models.dart';
|
||||
import 'models/isar/ordinal.dart';
|
||||
import 'models/keys/key_data_interface.dart';
|
||||
import 'models/paynym/paynym_account_lite.dart';
|
||||
import 'models/send_view_auto_fill_data.dart';
|
||||
import 'pages/add_wallet_views/add_token_view/add_custom_token_view.dart';
|
||||
|
@ -198,6 +199,7 @@ import 'wallets/crypto_currency/crypto_currency.dart';
|
|||
import 'wallets/crypto_currency/intermediate/frost_currency.dart';
|
||||
import 'wallets/models/tx_data.dart';
|
||||
import 'wallets/wallet/wallet.dart';
|
||||
import 'wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||
import 'widgets/choose_coin_view.dart';
|
||||
import 'widgets/frost_scaffold.dart';
|
||||
|
||||
|
@ -908,11 +910,12 @@ class RouteGenerator {
|
|||
);
|
||||
|
||||
case XPubView.routeName:
|
||||
if (args is String) {
|
||||
if (args is (String, ({List<XPub> xpubs, String fingerprint}))) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => XPubView(
|
||||
walletId: args,
|
||||
walletId: args.$1,
|
||||
xpubData: args.$2,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
|
@ -1275,6 +1278,63 @@ class RouteGenerator {
|
|||
name: settings.name,
|
||||
),
|
||||
);
|
||||
} else if (args is ({
|
||||
String walletId,
|
||||
List<String> mnemonic,
|
||||
KeyDataInterface? keyData,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => WalletBackupView(
|
||||
walletId: args.walletId,
|
||||
mnemonic: args.mnemonic,
|
||||
keyData: args.keyData,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
} else if (args is ({
|
||||
String walletId,
|
||||
List<String> mnemonic,
|
||||
KeyDataInterface? keyData,
|
||||
({
|
||||
String myName,
|
||||
String config,
|
||||
String keys,
|
||||
({String config, String keys})? prevGen,
|
||||
})? frostWalletData,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => WalletBackupView(
|
||||
walletId: args.walletId,
|
||||
mnemonic: args.mnemonic,
|
||||
frostWalletData: args.frostWalletData,
|
||||
keyData: args.keyData,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case MobileKeyDataView.routeName:
|
||||
if (args is ({
|
||||
String walletId,
|
||||
KeyDataInterface keyData,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => MobileKeyDataView(
|
||||
walletId: args.walletId,
|
||||
keyData: args.keyData,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
|
@ -2293,26 +2353,51 @@ class RouteGenerator {
|
|||
case WalletKeysDesktopPopup.routeName:
|
||||
if (args is ({
|
||||
List<String> mnemonic,
|
||||
String walletId,
|
||||
({String keys, String config})? frostData
|
||||
})) {
|
||||
return FadePageRoute(
|
||||
WalletKeysDesktopPopup(
|
||||
words: args.mnemonic,
|
||||
walletId: args.walletId,
|
||||
frostData: args.frostData,
|
||||
),
|
||||
RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
// return getRoute(
|
||||
// shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
// builder: (_) => WalletKeysDesktopPopup(
|
||||
// words: args,
|
||||
// ),
|
||||
// settings: RouteSettings(
|
||||
// name: settings.name,
|
||||
// ),
|
||||
// );
|
||||
} else if (args is ({
|
||||
List<String> mnemonic,
|
||||
String walletId,
|
||||
({String keys, String config})? frostData,
|
||||
KeyDataInterface? keyData,
|
||||
})) {
|
||||
return FadePageRoute(
|
||||
WalletKeysDesktopPopup(
|
||||
words: args.mnemonic,
|
||||
walletId: args.walletId,
|
||||
frostData: args.frostData,
|
||||
keyData: args.keyData,
|
||||
),
|
||||
RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
} else if (args is ({
|
||||
List<String> mnemonic,
|
||||
String walletId,
|
||||
KeyDataInterface? keyData,
|
||||
})) {
|
||||
return FadePageRoute(
|
||||
WalletKeysDesktopPopup(
|
||||
words: args.mnemonic,
|
||||
walletId: args.walletId,
|
||||
keyData: args.keyData,
|
||||
),
|
||||
RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import 'package:event_bus/event_bus.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../app_config.dart';
|
||||
import '../models/isar/models/log.dart';
|
||||
import '../utilities/logger.dart';
|
||||
|
||||
|
@ -98,7 +99,7 @@ class DebugService extends ChangeNotifier {
|
|||
Future<String> exportToFile(String directory, EventBus eventBus) async {
|
||||
final now = DateTime.now();
|
||||
final filename =
|
||||
"Stack_Wallet_logs_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.txt";
|
||||
"${AppConfig.prefix}_Wallet_logs_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.txt";
|
||||
final filepath = "$directory/$filename";
|
||||
final File file = await File(filepath).create();
|
||||
|
||||
|
|
1
lib/services/event_bus/events/wallet_added_event.dart
Normal file
|
@ -0,0 +1 @@
|
|||
class WalletAddedEvent {}
|
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import 'package:decimal/decimal.dart';
|
||||
|
||||
import '../../models/exchange/response_objects/estimate.dart';
|
||||
import '../../models/exchange/response_objects/range.dart';
|
||||
import '../../models/exchange/response_objects/trade.dart';
|
||||
|
@ -17,6 +18,7 @@ import '../../models/isar/exchange_cache/pair.dart';
|
|||
import 'change_now/change_now_exchange.dart';
|
||||
import 'exchange_response.dart';
|
||||
import 'majestic_bank/majestic_bank_exchange.dart';
|
||||
import 'nanswap/nanswap_exchange.dart';
|
||||
import 'simpleswap/simpleswap_exchange.dart';
|
||||
import 'trocador/trocador_exchange.dart';
|
||||
|
||||
|
@ -33,6 +35,8 @@ abstract class Exchange {
|
|||
return MajesticBankExchange.instance;
|
||||
case TrocadorExchange.exchangeName:
|
||||
return TrocadorExchange.instance;
|
||||
case NanswapExchange.exchangeName:
|
||||
return NanswapExchange.instance;
|
||||
default:
|
||||
final split = name.split(" ");
|
||||
if (split.length >= 2) {
|
||||
|
@ -45,6 +49,8 @@ abstract class Exchange {
|
|||
|
||||
String get name;
|
||||
|
||||
bool get supportsRefundAddress => true;
|
||||
|
||||
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(bool fixedRate);
|
||||
|
||||
Future<ExchangeResponse<List<Currency>>> getPairedCurrencies(
|
||||
|
@ -97,6 +103,7 @@ abstract class Exchange {
|
|||
static List<Exchange> get exchangesWithTorSupport => [
|
||||
MajesticBankExchange.instance,
|
||||
TrocadorExchange.instance,
|
||||
NanswapExchange.instance, // Maybe??
|
||||
];
|
||||
|
||||
/// List of exchange names which support Tor.
|
||||
|
|
|
@ -23,6 +23,7 @@ import '../../utilities/prefs.dart';
|
|||
import '../../utilities/stack_file_system.dart';
|
||||
import 'change_now/change_now_exchange.dart';
|
||||
import 'majestic_bank/majestic_bank_exchange.dart';
|
||||
import 'nanswap/nanswap_exchange.dart';
|
||||
import 'trocador/trocador_exchange.dart';
|
||||
|
||||
class ExchangeDataLoadingService {
|
||||
|
@ -170,6 +171,7 @@ class ExchangeDataLoadingService {
|
|||
final futures = [
|
||||
loadMajesticBankCurrencies(),
|
||||
loadTrocadorCurrencies(),
|
||||
loadNanswapCurrencies(),
|
||||
];
|
||||
|
||||
// If using Tor, don't load data for exchanges which don't support Tor.
|
||||
|
@ -382,6 +384,31 @@ class ExchangeDataLoadingService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> loadNanswapCurrencies() async {
|
||||
if (_isar == null) {
|
||||
await initDB();
|
||||
}
|
||||
final responseCurrencies =
|
||||
await NanswapExchange.instance.getAllCurrencies(false);
|
||||
|
||||
if (responseCurrencies.value != null) {
|
||||
await isar.writeTxn(() async {
|
||||
final idsToDelete = await isar.currencies
|
||||
.where()
|
||||
.exchangeNameEqualTo(NanswapExchange.exchangeName)
|
||||
.idProperty()
|
||||
.findAll();
|
||||
await isar.currencies.deleteAll(idsToDelete);
|
||||
await isar.currencies.putAll(responseCurrencies.value!);
|
||||
});
|
||||
} else {
|
||||
Logging.instance.log(
|
||||
"loadNanswapCurrencies: $responseCurrencies",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Future<void> loadMajesticBankPairs() async {
|
||||
// final exchange = MajesticBankExchange.instance;
|
||||
//
|
||||
|
|
|
@ -46,6 +46,9 @@ class MajesticBankExchange extends Exchange {
|
|||
"XMR": "Monero",
|
||||
};
|
||||
|
||||
@override
|
||||
bool get supportsRefundAddress => false;
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<Trade>> createTrade({
|
||||
required String from,
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
class NCurrency {
|
||||
final String id;
|
||||
final String ticker;
|
||||
final String name;
|
||||
final String image;
|
||||
final String network;
|
||||
final bool hasExternalId;
|
||||
final bool feeLess;
|
||||
|
||||
NCurrency({
|
||||
required this.id,
|
||||
required this.ticker,
|
||||
required this.name,
|
||||
required this.image,
|
||||
required this.network,
|
||||
required this.hasExternalId,
|
||||
required this.feeLess,
|
||||
});
|
||||
|
||||
factory NCurrency.fromJson(Map<String, dynamic> json) {
|
||||
return NCurrency(
|
||||
id: json["id"] as String,
|
||||
ticker: json['ticker'] as String,
|
||||
name: json['name'] as String,
|
||||
image: json['image'] as String,
|
||||
network: json['network'] as String,
|
||||
hasExternalId: json['hasExternalId'] as bool,
|
||||
feeLess: json['feeless'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NCurrency {'
|
||||
'ticker: $ticker, '
|
||||
'name: $name, '
|
||||
'image: $image, '
|
||||
'network: $network, '
|
||||
'hasExternalId: $hasExternalId, '
|
||||
'feeless: $feeLess'
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
class NEstimate {
|
||||
final String from;
|
||||
final String to;
|
||||
final num amountFrom;
|
||||
final num amountTo;
|
||||
|
||||
NEstimate({
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.amountFrom,
|
||||
required this.amountTo,
|
||||
});
|
||||
|
||||
factory NEstimate.fromJson(Map<String, dynamic> json) {
|
||||
return NEstimate(
|
||||
from: json['from'] as String,
|
||||
to: json['to'] as String,
|
||||
amountFrom: json['amountFrom'] as num,
|
||||
amountTo: json['amountTo'] as num,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NEstimate {'
|
||||
'from: $from, '
|
||||
'to: $to, '
|
||||
'amountFrom: $amountFrom, '
|
||||
'amountTo: $amountTo '
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
class NTrade {
|
||||
final String id;
|
||||
final String from;
|
||||
final String to;
|
||||
final num expectedAmountFrom;
|
||||
final num expectedAmountTo;
|
||||
final String payinAddress;
|
||||
final String payoutAddress;
|
||||
|
||||
final String? payinExtraId;
|
||||
final String? fullLink;
|
||||
final String? status;
|
||||
final String? payinHash;
|
||||
final String? payoutHash;
|
||||
final num? fromAmount;
|
||||
final num? toAmount;
|
||||
final String? fromNetwork;
|
||||
final String? toNetwork;
|
||||
|
||||
NTrade({
|
||||
required this.id,
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.expectedAmountFrom,
|
||||
required this.expectedAmountTo,
|
||||
required this.payinAddress,
|
||||
required this.payoutAddress,
|
||||
this.payinExtraId,
|
||||
this.fullLink,
|
||||
this.status,
|
||||
this.payinHash,
|
||||
this.payoutHash,
|
||||
this.fromAmount,
|
||||
this.toAmount,
|
||||
this.fromNetwork,
|
||||
this.toNetwork,
|
||||
});
|
||||
|
||||
factory NTrade.fromJson(Map<String, dynamic> json) {
|
||||
return NTrade(
|
||||
id: json['id'] as String,
|
||||
from: json['from'] as String,
|
||||
to: json['to'] as String,
|
||||
expectedAmountFrom: num.parse(json['expectedAmountFrom'].toString()),
|
||||
expectedAmountTo: json['expectedAmountTo'] as num,
|
||||
payinAddress: json['payinAddress'] as String,
|
||||
payoutAddress: json['payoutAddress'] as String,
|
||||
fullLink: json['fullLink'] as String?,
|
||||
payinExtraId: json['payinExtraId'] as String?,
|
||||
status: json['status'] as String?,
|
||||
payinHash: json['payinHash'] as String?,
|
||||
payoutHash: json['payoutHash'] as String?,
|
||||
fromAmount: json['fromAmount'] as num?,
|
||||
toAmount: json['toAmount'] as num?,
|
||||
fromNetwork: json['fromNetwork'] as String?,
|
||||
toNetwork: json['toNetwork'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NTrade {'
|
||||
' id: $id, '
|
||||
' from: $from, '
|
||||
' to: $to, '
|
||||
' expectedAmountFrom: $expectedAmountFrom, '
|
||||
' expectedAmountTo: $expectedAmountTo, '
|
||||
' payinAddress: $payinAddress, '
|
||||
' payoutAddress: $payoutAddress, '
|
||||
' fullLink: $fullLink, '
|
||||
' payinExtraId: $payinExtraId, '
|
||||
' status: $status, '
|
||||
' payinHash: $payinHash, '
|
||||
' payoutHash: $payoutHash '
|
||||
' fromAmount: $fromAmount, '
|
||||
' toAmount: $toAmount, '
|
||||
' fromNetwork: $fromNetwork, '
|
||||
' toNetwork: $toNetwork, '
|
||||
'}';
|
||||
}
|
||||
}
|
517
lib/services/exchange/nanswap/nanswap_api.dart
Normal file
|
@ -0,0 +1,517 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../exceptions/exchange/exchange_exception.dart';
|
||||
import '../../../external_api_keys.dart';
|
||||
import '../../../networking/http.dart';
|
||||
import '../../../utilities/logger.dart';
|
||||
import '../../../utilities/prefs.dart';
|
||||
import '../../tor_service.dart';
|
||||
import '../exchange_response.dart';
|
||||
import 'api_response_models/n_currency.dart';
|
||||
import 'api_response_models/n_estimate.dart';
|
||||
import 'api_response_models/n_trade.dart';
|
||||
|
||||
class NanswapAPI {
|
||||
NanswapAPI._();
|
||||
|
||||
static const authority = "api.nanswap.com";
|
||||
static const version = "v1";
|
||||
|
||||
static NanswapAPI? _instance;
|
||||
static NanswapAPI get instance => _instance ??= NanswapAPI._();
|
||||
|
||||
final _client = HTTP();
|
||||
|
||||
Uri _buildUri({required String endpoint, Map<String, String>? params}) {
|
||||
return Uri.https(authority, "/$version/$endpoint", params);
|
||||
}
|
||||
|
||||
Future<dynamic> _makeGetRequest(Uri uri) async {
|
||||
int code = -1;
|
||||
try {
|
||||
final response = await _client.get(
|
||||
url: uri,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
: null,
|
||||
);
|
||||
|
||||
code = response.code;
|
||||
|
||||
final parsed = jsonDecode(response.body);
|
||||
|
||||
return parsed;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"NanswapAPI._makeRequest($uri) HTTP:$code threw: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _makePostRequest(
|
||||
Uri uri,
|
||||
Map<String, dynamic> body,
|
||||
) async {
|
||||
int code = -1;
|
||||
try {
|
||||
final response = await _client.post(
|
||||
url: uri,
|
||||
headers: {
|
||||
'nanswap-api-key': kNanswapApiKey,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: jsonEncode(body),
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
: null,
|
||||
);
|
||||
|
||||
code = response.code;
|
||||
|
||||
final data = response.body;
|
||||
final parsed = jsonDecode(data);
|
||||
|
||||
return parsed;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"NanswapAPI._makePostRequest($uri) HTTP:$code threw: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ============= API ===================================================
|
||||
|
||||
// GET List of supported currencies
|
||||
// https://api.nanswap.com/v1/all-currencies
|
||||
//
|
||||
// Returns a Key => Value map of available currencies.
|
||||
//
|
||||
// The Key is the ticker, that can be used in the from and to params of the /get-estimate, /get-limit, /create-order.
|
||||
//
|
||||
// The Value is the currency info:
|
||||
//
|
||||
// name
|
||||
//
|
||||
// logo
|
||||
//
|
||||
// network Network of the crypto.
|
||||
//
|
||||
// hasExternalId Boolean. If the crypto require a memo/id.
|
||||
//
|
||||
// feeless Boolean. If crypto has 0 network fees.
|
||||
//
|
||||
// HEADERS
|
||||
// Accept
|
||||
//
|
||||
// application/json
|
||||
Future<ExchangeResponse<List<NCurrency>>> getSupportedCurrencies() async {
|
||||
final uri = _buildUri(
|
||||
endpoint: "all-currencies",
|
||||
);
|
||||
|
||||
try {
|
||||
final json = await _makeGetRequest(uri);
|
||||
|
||||
final List<NCurrency> result = [];
|
||||
for (final key in (json as Map).keys) {
|
||||
final _map = json[key] as Map;
|
||||
_map["id"] = key;
|
||||
result.add(
|
||||
NCurrency.fromJson(
|
||||
Map<String, dynamic>.from(_map),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ExchangeResponse(value: result);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Nanswap.getSupportedCurrencies() exception: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET Get estimate
|
||||
// https://api.nanswap.com/v1/get-estimate?from=XNO&to=BAN&amount=10
|
||||
//
|
||||
// Get estimated exchange amount.
|
||||
// HEADERS
|
||||
// Accept
|
||||
//
|
||||
// application/json
|
||||
// PARAMS
|
||||
//
|
||||
// from
|
||||
// XNO
|
||||
// Ticker from
|
||||
//
|
||||
// to
|
||||
// BAN
|
||||
// Ticker to
|
||||
//
|
||||
// amount
|
||||
// 10
|
||||
// Amount from
|
||||
Future<ExchangeResponse<NEstimate>> getEstimate({
|
||||
required String amountFrom,
|
||||
required String from,
|
||||
required String to,
|
||||
}) async {
|
||||
final uri = _buildUri(
|
||||
endpoint: "get-estimate",
|
||||
params: {
|
||||
"to": to.toUpperCase(),
|
||||
"from": from.toUpperCase(),
|
||||
"amount": amountFrom,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final json = await _makeGetRequest(uri);
|
||||
|
||||
try {
|
||||
final map = Map<String, dynamic>.from(json as Map);
|
||||
|
||||
// not sure why the api responds without these sometimes...
|
||||
map["to"] ??= to.toUpperCase();
|
||||
map["from"] ??= from.toUpperCase();
|
||||
|
||||
return ExchangeResponse(
|
||||
value: NEstimate.fromJson(
|
||||
map,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
Logging.instance.log(
|
||||
"Nanswap.getEstimate() response was: $json",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Nanswap.getEstimate() exception: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET Get estimate reverse
|
||||
// https://api.nanswap.com/v1/get-estimate-reverse?from=XNO&to=BAN&amount=1650
|
||||
//
|
||||
// (Only available for feeless crypto)
|
||||
//
|
||||
// Get estimate but reversed, it takes toAmount and returns the fromAmount
|
||||
// estimation. Allows to let user input directly their toAmount wanted.
|
||||
// HEADERS
|
||||
// Accept
|
||||
//
|
||||
// application/json
|
||||
// PARAMS
|
||||
// from
|
||||
// XNO
|
||||
// Ticker from
|
||||
//
|
||||
// to
|
||||
// BAN
|
||||
// Ticker to
|
||||
//
|
||||
// amount
|
||||
// 1650
|
||||
// Amount to
|
||||
Future<ExchangeResponse<NEstimate>> getEstimateReversed({
|
||||
required String amountTo,
|
||||
required String from,
|
||||
required String to,
|
||||
}) async {
|
||||
final uri = _buildUri(
|
||||
endpoint: "get-estimate-reverse",
|
||||
params: {
|
||||
"to": to.toUpperCase(),
|
||||
"from": from.toUpperCase(),
|
||||
"amount": amountTo,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final json = await _makeGetRequest(uri);
|
||||
|
||||
final map = Map<String, dynamic>.from(json as Map);
|
||||
|
||||
// not sure why the api responds without these sometimes...
|
||||
map["to"] ??= to.toUpperCase();
|
||||
map["from"] ??= from.toUpperCase();
|
||||
|
||||
return ExchangeResponse(
|
||||
value: NEstimate.fromJson(
|
||||
map,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Nanswap.getEstimateReverse() exception: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET Get order limit amount
|
||||
// https://api.nanswap.com/v1/get-limits?from=XNO&to=BAN
|
||||
//
|
||||
// Returns minimum and maximum from amount for a given pair. Maximum amount depends of current liquidity.
|
||||
// HEADERS
|
||||
// Accept
|
||||
//
|
||||
// application/json
|
||||
// PARAMS
|
||||
// from
|
||||
// XNO
|
||||
// Ticker from
|
||||
//
|
||||
// to
|
||||
// BAN
|
||||
// Ticker to
|
||||
Future<ExchangeResponse<({num minFrom, num maxFrom})>> getOrderLimits({
|
||||
required String from,
|
||||
required String to,
|
||||
}) async {
|
||||
final uri = _buildUri(
|
||||
endpoint: "get-limits",
|
||||
params: {
|
||||
"to": to.toUpperCase(),
|
||||
"from": from.toUpperCase(),
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final json = await _makeGetRequest(uri);
|
||||
|
||||
return ExchangeResponse(
|
||||
value: (
|
||||
minFrom: json["min"] as num,
|
||||
maxFrom: json["max"] as num,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Nanswap.getOrderLimits() exception: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST Create a new order
|
||||
// https://api.nanswap.com/v1/create-order
|
||||
//
|
||||
// Create a new order and returns order data. You need to send the request body as JSON.
|
||||
// A valid API key is required in nanswap-api-key header for this request.
|
||||
// You can get one at https://nanswap.com/API
|
||||
// Request:
|
||||
//
|
||||
// * from ticker of currency you want to exchange
|
||||
// * to ticker of currency you want to receive
|
||||
// * amount The amount you want to send
|
||||
// * toAddress The address that will recieve the exchanged funds
|
||||
// * extraId (optional) Memo/Id of the toAddress
|
||||
//
|
||||
// * itemName (optional) An item name that will be displayed on transaction
|
||||
// page. Can be used by merchant to provide a better UX to users. Max 128 char.
|
||||
// * maxDurationSeconds (optional) Maximum seconds after what transaction
|
||||
// expires. Min: 30s Max: 259200s. Default to 72h or 5min if itemName is set
|
||||
// Reponse:
|
||||
//
|
||||
// * id Order id.
|
||||
// * from ticker of currency you want to exchange
|
||||
// * to ticker of currency you want to receive
|
||||
// * expectedAmountFrom The amount you want to send
|
||||
// * expectedAmountTo Estimated value that you will get based on the field expectedAmountFrom
|
||||
// * payinAddress Nanswap's address you need to send the funds to
|
||||
// * payinExtraId If present, the extra/memo id required for the payinAddress
|
||||
// * payoutAddress The address that will recieve the exchanged funds
|
||||
// * fullLink URL of the transaction
|
||||
// AUTHORIZATIONAPI Key
|
||||
// Key
|
||||
//
|
||||
// nanswap-api-key
|
||||
// Value
|
||||
//
|
||||
// <value>
|
||||
// HEADERS
|
||||
// nanswap-api-key
|
||||
//
|
||||
// API_KEY
|
||||
//
|
||||
// (Required)
|
||||
// Content-Type
|
||||
//
|
||||
// application/json
|
||||
// Accept
|
||||
//
|
||||
// application/json
|
||||
Future<ExchangeResponse<NTrade>> createOrder({
|
||||
required String from,
|
||||
required String to,
|
||||
required num fromAmount,
|
||||
required String toAddress,
|
||||
String? extraIdOrMemo,
|
||||
}) async {
|
||||
final uri = _buildUri(
|
||||
endpoint: "create-order",
|
||||
);
|
||||
|
||||
final body = {
|
||||
"from": from.toUpperCase(),
|
||||
"to": to.toUpperCase(),
|
||||
"amount": fromAmount,
|
||||
"toAddress": toAddress,
|
||||
};
|
||||
|
||||
if (extraIdOrMemo != null) {
|
||||
body["extraId"] = extraIdOrMemo;
|
||||
}
|
||||
|
||||
try {
|
||||
final json = await _makePostRequest(uri, body);
|
||||
|
||||
try {
|
||||
return ExchangeResponse(
|
||||
value: NTrade.fromJson(
|
||||
Map<String, dynamic>.from(json as Map),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
debugPrint(json.toString());
|
||||
rethrow;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Nanswap.createOrder() exception: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET Get order id data
|
||||
// https://api.nanswap.com/v1/get-order?id=zYkxDxfmYRM
|
||||
//
|
||||
// Returns data of an order id.
|
||||
// Response:
|
||||
//
|
||||
// id Order id.
|
||||
//
|
||||
// status Order status, can be one of the following : [waiting, exchanging, sending, completed, error]
|
||||
//
|
||||
// from ticker of currency you want to exchange
|
||||
//
|
||||
// fromNetwork network of the currency you want to exchange.
|
||||
//
|
||||
// to ticker of currency you want to receive
|
||||
//
|
||||
// toNetwork network of the currency you want to receive.
|
||||
//
|
||||
// expectedAmountFrom The amount you want to send
|
||||
//
|
||||
// expectedAmountTo Estimated value that you will get based on the field expectedAmountFrom
|
||||
//
|
||||
// amountFrom From Amount Exchanged
|
||||
//
|
||||
// amountTo To Amount Exchanged
|
||||
//
|
||||
// payinAddress Nanswap's address you need to send the funds to
|
||||
//
|
||||
// payinExtraId If present, the extra/memo id required for the payinAddress
|
||||
//
|
||||
// payoutAddress The address that will recieve the exchanged funds
|
||||
//
|
||||
// payinHash Hash of the transaction you sent us
|
||||
//
|
||||
// senderAddress Address which sent us the funds
|
||||
//
|
||||
// payoutHash Hash of the transaction we sent to you
|
||||
//
|
||||
// HEADERS
|
||||
// Accept
|
||||
//
|
||||
// application/json
|
||||
// PARAMS
|
||||
// id
|
||||
//
|
||||
// zYkxDxfmYRM
|
||||
//
|
||||
// The order id
|
||||
Future<ExchangeResponse<NTrade>> getOrder({required String id}) async {
|
||||
final uri = _buildUri(
|
||||
endpoint: "get-order",
|
||||
params: {
|
||||
"id": id,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final json = await _makeGetRequest(uri);
|
||||
|
||||
try {
|
||||
return ExchangeResponse(
|
||||
value: NTrade.fromJson(
|
||||
Map<String, dynamic>.from(json as Map),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
debugPrint(json.toString());
|
||||
rethrow;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Nanswap.getOrder($id) exception: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
461
lib/services/exchange/nanswap/nanswap_exchange.dart
Normal file
|
@ -0,0 +1,461 @@
|
|||
import 'package:decimal/decimal.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../app_config.dart';
|
||||
import '../../../exceptions/exchange/exchange_exception.dart';
|
||||
import '../../../models/exchange/response_objects/estimate.dart';
|
||||
import '../../../models/exchange/response_objects/range.dart';
|
||||
import '../../../models/exchange/response_objects/trade.dart';
|
||||
import '../../../models/isar/exchange_cache/currency.dart';
|
||||
import '../../../models/isar/exchange_cache/pair.dart';
|
||||
import '../exchange.dart';
|
||||
import '../exchange_response.dart';
|
||||
import 'api_response_models/n_estimate.dart';
|
||||
import 'nanswap_api.dart';
|
||||
|
||||
class NanswapExchange extends Exchange {
|
||||
NanswapExchange._();
|
||||
|
||||
static NanswapExchange? _instance;
|
||||
static NanswapExchange get instance => _instance ??= NanswapExchange._();
|
||||
|
||||
static const exchangeName = "Nanswap";
|
||||
|
||||
static const filter = ["BTC", "BAN", "XNO"];
|
||||
|
||||
@override
|
||||
bool get supportsRefundAddress => false;
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<Trade>> createTrade({
|
||||
required String from,
|
||||
required String to,
|
||||
required bool fixedRate,
|
||||
required Decimal amount,
|
||||
required String addressTo,
|
||||
String? extraId,
|
||||
required String addressRefund,
|
||||
required String refundExtraId,
|
||||
Estimate? estimate,
|
||||
required bool reversed,
|
||||
}) async {
|
||||
try {
|
||||
if (fixedRate) {
|
||||
throw ExchangeException(
|
||||
"Nanswap fixedRate not available",
|
||||
ExchangeExceptionType.generic,
|
||||
);
|
||||
}
|
||||
if (refundExtraId.isNotEmpty) {
|
||||
throw ExchangeException(
|
||||
"Nanswap refundExtraId not available",
|
||||
ExchangeExceptionType.generic,
|
||||
);
|
||||
}
|
||||
if (addressRefund.isNotEmpty) {
|
||||
throw ExchangeException(
|
||||
"Nanswap addressRefund not available",
|
||||
ExchangeExceptionType.generic,
|
||||
);
|
||||
}
|
||||
if (reversed) {
|
||||
throw ExchangeException(
|
||||
"Nanswap reversed not available",
|
||||
ExchangeExceptionType.generic,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await NanswapAPI.instance.createOrder(
|
||||
from: from,
|
||||
to: to,
|
||||
fromAmount: amount.toDouble(),
|
||||
toAddress: addressTo,
|
||||
extraIdOrMemo: extraId,
|
||||
);
|
||||
|
||||
if (response.exception != null) {
|
||||
return ExchangeResponse(
|
||||
exception: response.exception,
|
||||
);
|
||||
}
|
||||
|
||||
final t = response.value!;
|
||||
print(t);
|
||||
|
||||
return ExchangeResponse(
|
||||
value: Trade(
|
||||
uuid: const Uuid().v1(),
|
||||
tradeId: t.id,
|
||||
rateType: "estimated",
|
||||
direction: "normal",
|
||||
timestamp: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
payInCurrency: from,
|
||||
payInAmount: t.expectedAmountFrom.toString(),
|
||||
payInAddress: t.payinAddress,
|
||||
payInNetwork: t.toNetwork ?? t.to,
|
||||
payInExtraId: t.payinExtraId ?? "",
|
||||
payInTxid: t.payinHash ?? "",
|
||||
payOutCurrency: to,
|
||||
payOutAmount: t.expectedAmountTo.toString(),
|
||||
payOutAddress: t.payoutAddress,
|
||||
payOutNetwork: t.fromNetwork ?? t.from,
|
||||
payOutExtraId: "",
|
||||
payOutTxid: t.payoutHash ?? "",
|
||||
refundAddress: "",
|
||||
refundExtraId: "",
|
||||
status: "waiting",
|
||||
exchangeName: exchangeName,
|
||||
),
|
||||
);
|
||||
} on ExchangeException catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: e,
|
||||
);
|
||||
} catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(
|
||||
bool fixedRate,
|
||||
) async {
|
||||
try {
|
||||
if (fixedRate) {
|
||||
throw ExchangeException(
|
||||
"Nanswap fixedRate not available",
|
||||
ExchangeExceptionType.generic,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await NanswapAPI.instance.getSupportedCurrencies();
|
||||
|
||||
if (response.exception != null) {
|
||||
return ExchangeResponse(
|
||||
exception: response.exception,
|
||||
);
|
||||
}
|
||||
|
||||
return ExchangeResponse(
|
||||
value: response.value!
|
||||
.where((e) => filter.contains(e.id))
|
||||
.map(
|
||||
(e) => Currency(
|
||||
exchangeName: exchangeName,
|
||||
ticker: e.id,
|
||||
name: e.name,
|
||||
network: e.network,
|
||||
image: e.image,
|
||||
isFiat: false,
|
||||
rateType: SupportedRateType.estimated,
|
||||
isStackCoin: AppConfig.isStackCoin(e.id),
|
||||
tokenContract: null,
|
||||
isAvailable: true,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
} on ExchangeException catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: e,
|
||||
);
|
||||
} catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<List<Pair>>> getAllPairs(bool fixedRate) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<List<Estimate>>> getEstimates(
|
||||
String from,
|
||||
String to,
|
||||
Decimal amount,
|
||||
bool fixedRate,
|
||||
bool reversed,
|
||||
) async {
|
||||
try {
|
||||
if (fixedRate) {
|
||||
throw ExchangeException(
|
||||
"Nanswap fixedRate not available",
|
||||
ExchangeExceptionType.generic,
|
||||
);
|
||||
}
|
||||
|
||||
final ExchangeResponse<NEstimate> response;
|
||||
if (reversed) {
|
||||
response = await NanswapAPI.instance.getEstimateReversed(
|
||||
from: from,
|
||||
to: to,
|
||||
amountTo: amount.toString(),
|
||||
);
|
||||
} else {
|
||||
response = await NanswapAPI.instance.getEstimate(
|
||||
from: from,
|
||||
to: to,
|
||||
amountFrom: amount.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
if (response.exception != null) {
|
||||
return ExchangeResponse(
|
||||
exception: response.exception,
|
||||
);
|
||||
}
|
||||
|
||||
final t = response.value!;
|
||||
|
||||
return ExchangeResponse(
|
||||
value: [
|
||||
Estimate(
|
||||
estimatedAmount: Decimal.parse(
|
||||
(reversed ? t.amountFrom : t.amountTo).toString(),
|
||||
),
|
||||
fixedRate: fixedRate,
|
||||
reversed: reversed,
|
||||
exchangeProvider: exchangeName,
|
||||
),
|
||||
],
|
||||
);
|
||||
} on ExchangeException catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: e,
|
||||
);
|
||||
} catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<List<Currency>>> getPairedCurrencies(
|
||||
String forCurrency,
|
||||
bool fixedRate,
|
||||
) async {
|
||||
try {
|
||||
if (fixedRate) {
|
||||
throw ExchangeException(
|
||||
"Nanswap fixedRate not available",
|
||||
ExchangeExceptionType.generic,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await getAllCurrencies(
|
||||
fixedRate,
|
||||
);
|
||||
|
||||
if (response.exception != null) {
|
||||
return ExchangeResponse(
|
||||
exception: response.exception,
|
||||
);
|
||||
}
|
||||
|
||||
return ExchangeResponse(
|
||||
value: response.value!..removeWhere((e) => e.ticker == forCurrency),
|
||||
);
|
||||
} on ExchangeException catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: e,
|
||||
);
|
||||
} catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<List<Pair>>> getPairsFor(
|
||||
String currency,
|
||||
bool fixedRate,
|
||||
) async {
|
||||
throw UnsupportedError("Not used");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<Range>> getRange(
|
||||
String from,
|
||||
String to,
|
||||
bool fixedRate,
|
||||
) async {
|
||||
try {
|
||||
if (fixedRate) {
|
||||
throw ExchangeException(
|
||||
"Nanswap fixedRate not available",
|
||||
ExchangeExceptionType.generic,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await NanswapAPI.instance.getOrderLimits(
|
||||
from: from,
|
||||
to: to,
|
||||
);
|
||||
|
||||
if (response.exception != null) {
|
||||
return ExchangeResponse(
|
||||
exception: response.exception,
|
||||
);
|
||||
}
|
||||
|
||||
final t = response.value!;
|
||||
|
||||
return ExchangeResponse(
|
||||
value: Range(
|
||||
min: Decimal.parse(t.minFrom.toString()),
|
||||
max: Decimal.parse(t.maxFrom.toString()),
|
||||
),
|
||||
);
|
||||
} on ExchangeException catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: e,
|
||||
);
|
||||
} catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<Trade>> getTrade(String tradeId) async {
|
||||
try {
|
||||
final response = await NanswapAPI.instance.getOrder(
|
||||
id: tradeId,
|
||||
);
|
||||
|
||||
if (response.exception != null) {
|
||||
return ExchangeResponse(
|
||||
exception: response.exception,
|
||||
);
|
||||
}
|
||||
|
||||
final t = response.value!;
|
||||
|
||||
return ExchangeResponse(
|
||||
value: Trade(
|
||||
uuid: const Uuid().v1(),
|
||||
tradeId: t.id,
|
||||
rateType: "estimated",
|
||||
direction: "normal",
|
||||
timestamp: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
payInCurrency: t.from,
|
||||
payInAmount: t.expectedAmountFrom.toString(),
|
||||
payInAddress: t.payinAddress,
|
||||
payInNetwork: t.toNetwork ?? t.to,
|
||||
payInExtraId: t.payinExtraId ?? "",
|
||||
payInTxid: t.payinHash ?? "",
|
||||
payOutCurrency: t.to,
|
||||
payOutAmount: t.expectedAmountTo.toString(),
|
||||
payOutAddress: t.payoutAddress,
|
||||
payOutNetwork: t.fromNetwork ?? t.from,
|
||||
payOutExtraId: "",
|
||||
payOutTxid: t.payoutHash ?? "",
|
||||
refundAddress: "",
|
||||
refundExtraId: "",
|
||||
status: t.status ?? "unknown",
|
||||
exchangeName: exchangeName,
|
||||
),
|
||||
);
|
||||
} on ExchangeException catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: e,
|
||||
);
|
||||
} catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<List<Trade>>> getTrades() async {
|
||||
// TODO: implement getTrades
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => exchangeName;
|
||||
|
||||
@override
|
||||
Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
|
||||
try {
|
||||
final response = await NanswapAPI.instance.getOrder(
|
||||
id: trade.tradeId,
|
||||
);
|
||||
|
||||
if (response.exception != null) {
|
||||
return ExchangeResponse(
|
||||
exception: response.exception,
|
||||
);
|
||||
}
|
||||
|
||||
final t = response.value!;
|
||||
|
||||
return ExchangeResponse(
|
||||
value: Trade(
|
||||
uuid: trade.uuid,
|
||||
tradeId: t.id,
|
||||
rateType: trade.rateType,
|
||||
direction: trade.rateType,
|
||||
timestamp: trade.timestamp,
|
||||
updatedAt: DateTime.now(),
|
||||
payInCurrency: t.from,
|
||||
payInAmount: t.expectedAmountFrom.toString(),
|
||||
payInAddress: t.payinAddress,
|
||||
payInNetwork: t.toNetwork ?? trade.payInNetwork,
|
||||
payInExtraId: t.payinExtraId ?? trade.payInExtraId,
|
||||
payInTxid: t.payinHash ?? trade.payInTxid,
|
||||
payOutCurrency: t.to,
|
||||
payOutAmount: t.expectedAmountTo.toString(),
|
||||
payOutAddress: t.payoutAddress,
|
||||
payOutNetwork: t.fromNetwork ?? trade.payOutNetwork,
|
||||
payOutExtraId: trade.payOutExtraId,
|
||||
payOutTxid: t.payoutHash ?? trade.payOutTxid,
|
||||
refundAddress: trade.refundAddress,
|
||||
refundExtraId: trade.refundExtraId,
|
||||
status: t.status ?? "unknown",
|
||||
exchangeName: exchangeName,
|
||||
),
|
||||
);
|
||||
} on ExchangeException catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: e,
|
||||
);
|
||||
} catch (e) {
|
||||
return ExchangeResponse(
|
||||
exception: ExchangeException(
|
||||
e.toString(),
|
||||
ExchangeExceptionType.generic,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,12 +13,10 @@ import 'dart:async';
|
|||
import 'package:flutter_libmonero/monero/monero.dart' as monero;
|
||||
import 'package:flutter_libmonero/wownero/wownero.dart' as wownero;
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../app_config.dart';
|
||||
import '../db/hive/db.dart';
|
||||
import '../db/isar/main_db.dart';
|
||||
import 'node_service.dart';
|
||||
import 'notifications_service.dart';
|
||||
import 'trade_sent_from_stack_service.dart';
|
||||
import '../app_config.dart';
|
||||
import '../utilities/enums/sync_type_enum.dart';
|
||||
import '../utilities/flutter_secure_storage_interface.dart';
|
||||
import '../utilities/logger.dart';
|
||||
|
@ -28,6 +26,11 @@ import '../wallets/isar/models/wallet_info.dart';
|
|||
import '../wallets/wallet/impl/epiccash_wallet.dart';
|
||||
import '../wallets/wallet/wallet.dart';
|
||||
import '../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
|
||||
import 'event_bus/events/wallet_added_event.dart';
|
||||
import 'event_bus/global_event_bus.dart';
|
||||
import 'node_service.dart';
|
||||
import 'notifications_service.dart';
|
||||
import 'trade_sent_from_stack_service.dart';
|
||||
|
||||
class Wallets {
|
||||
Wallets._private();
|
||||
|
@ -59,6 +62,7 @@ class Wallets {
|
|||
);
|
||||
}
|
||||
_wallets[wallet.walletId] = wallet;
|
||||
GlobalEventBus.instance.fire(WalletAddedEvent());
|
||||
}
|
||||
|
||||
Future<void> deleteWallet(
|
||||
|
|
|
@ -31,7 +31,7 @@ final pThemeService = Provider<ThemeService>((ref) {
|
|||
});
|
||||
|
||||
class ThemeService {
|
||||
static const _currentDefaultThemeVersion = 10;
|
||||
static const _currentDefaultThemeVersion = 15;
|
||||
ThemeService._();
|
||||
static ThemeService? _instance;
|
||||
static ThemeService get instance => _instance ??= ThemeService._();
|
||||
|
|
|
@ -9,8 +9,10 @@
|
|||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../services/exchange/change_now/change_now_exchange.dart';
|
||||
import '../services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import '../services/exchange/nanswap/nanswap_exchange.dart';
|
||||
import '../services/exchange/simpleswap/simpleswap_exchange.dart';
|
||||
import '../services/exchange/trocador/trocador_exchange.dart';
|
||||
|
||||
|
@ -45,6 +47,7 @@ class _EXCHANGE {
|
|||
String get majesticBankBlue => "${_path}mb_blue.svg";
|
||||
String get majesticBankGreen => "${_path}mb_green.svg";
|
||||
String get trocador => "${_path}trocador.svg";
|
||||
String get nanswap => "${_path}nanswap.svg";
|
||||
|
||||
String getIconFor({required String exchangeName}) {
|
||||
switch (exchangeName) {
|
||||
|
@ -56,6 +59,8 @@ class _EXCHANGE {
|
|||
return majesticBankBlue;
|
||||
case TrocadorExchange.exchangeName:
|
||||
return trocador;
|
||||
case NanswapExchange.exchangeName:
|
||||
return nanswap;
|
||||
default:
|
||||
throw ArgumentError("Invalid exchange name passed to "
|
||||
"Assets.exchange.getIconFor()");
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import '../app_config.dart';
|
||||
import '../wallets/crypto_currency/crypto_currency.dart';
|
||||
|
||||
abstract class DefaultNodes {
|
||||
static const String defaultNodeIdPrefix = "default_";
|
||||
static String buildId(CryptoCurrency cryptoCurrency) =>
|
||||
"$defaultNodeIdPrefix${cryptoCurrency.identifier}";
|
||||
static const String defaultName = "Stack Default";
|
||||
static const String defaultName = "${AppConfig.prefix} Default";
|
||||
}
|
||||
|
|
|
@ -11,18 +11,19 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../app_config.dart';
|
||||
import '../db/hive/db.dart';
|
||||
import '../services/event_bus/events/global/tor_status_changed_event.dart';
|
||||
import '../services/event_bus/global_event_bus.dart';
|
||||
import '../app_config.dart';
|
||||
import '../wallets/crypto_currency/crypto_currency.dart';
|
||||
import '../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
|
||||
import 'amount/amount_unit.dart';
|
||||
import 'constants.dart';
|
||||
import 'enums/backup_frequency_type.dart';
|
||||
import 'enums/languages_enum.dart';
|
||||
import 'enums/sync_type_enum.dart';
|
||||
import '../wallets/crypto_currency/crypto_currency.dart';
|
||||
import '../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Prefs extends ChangeNotifier {
|
||||
Prefs._();
|
||||
|
@ -69,6 +70,7 @@ class Prefs extends ChangeNotifier {
|
|||
await _setMaxDecimals();
|
||||
_useTor = await _getUseTor();
|
||||
_fusionServerInfo = await _getFusionServerInfo();
|
||||
_autoPin = await _getAutoPin();
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
@ -1103,4 +1105,30 @@ class Prefs extends ChangeNotifier {
|
|||
|
||||
return actualMap;
|
||||
}
|
||||
|
||||
// Automatic PIN entry.
|
||||
|
||||
bool _autoPin = false;
|
||||
|
||||
bool get autoPin => _autoPin;
|
||||
|
||||
set autoPin(bool autoPin) {
|
||||
if (_autoPin != autoPin) {
|
||||
DB.instance.put<dynamic>(
|
||||
boxName: DB.boxNamePrefs,
|
||||
key: "autoPin",
|
||||
value: autoPin,
|
||||
);
|
||||
_autoPin = autoPin;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _getAutoPin() async {
|
||||
return await DB.instance.get<dynamic>(
|
||||
boxName: DB.boxNamePrefs,
|
||||
key: "autoPin",
|
||||
) as bool? ??
|
||||
false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,24 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../themes/stack_colors.dart';
|
||||
import 'logger.dart';
|
||||
import '../widgets/custom_loading_overlay.dart';
|
||||
import 'logger.dart';
|
||||
|
||||
Future<T> minWaitFuture<T>(
|
||||
Future<T> future, {
|
||||
required Duration delay,
|
||||
}) async {
|
||||
final results = await Future.wait(
|
||||
[
|
||||
future,
|
||||
Future<dynamic>.delayed(delay),
|
||||
],
|
||||
);
|
||||
|
||||
return results.first as T;
|
||||
}
|
||||
|
||||
Future<T?> showLoading<T>({
|
||||
required Future<T> whileFuture,
|
||||
|
@ -23,6 +38,7 @@ Future<T?> showLoading<T>({
|
|||
bool rootNavigator = false,
|
||||
bool opaqueBG = false,
|
||||
void Function(Exception)? onException,
|
||||
Duration? delay,
|
||||
}) async {
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
|
@ -49,7 +65,11 @@ Future<T?> showLoading<T>({
|
|||
T? result;
|
||||
|
||||
try {
|
||||
result = await whileFuture;
|
||||
if (delay != null) {
|
||||
result = await minWaitFuture(whileFuture, delay: delay);
|
||||
} else {
|
||||
result = await whileFuture;
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"showLoading caught: $e\n$s",
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'dart:io';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:solana/solana.dart';
|
||||
|
||||
import '../networking/http.dart';
|
||||
import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
|
||||
|
@ -15,6 +14,7 @@ import '../wallets/crypto_currency/crypto_currency.dart';
|
|||
import '../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart';
|
||||
import '../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
|
||||
import '../wallets/crypto_currency/intermediate/nano_currency.dart';
|
||||
import '../wallets/wallet/impl/solana_wallet.dart';
|
||||
import 'connection_check/electrum_connection_check.dart';
|
||||
import 'logger.dart';
|
||||
import 'test_epic_box_connection.dart';
|
||||
|
@ -210,14 +210,20 @@ Future<bool> testNodeConnection({
|
|||
|
||||
case Solana():
|
||||
try {
|
||||
RpcClient rpcClient;
|
||||
if (formData.host!.startsWith("http") ||
|
||||
formData.host!.startsWith("https")) {
|
||||
rpcClient = RpcClient("${formData.host}:${formData.port}");
|
||||
} else {
|
||||
rpcClient = RpcClient("http://${formData.host}:${formData.port}");
|
||||
}
|
||||
await rpcClient.getEpochInfo().then((value) => testPassed = true);
|
||||
final rpcClient = SolanaWallet.createRpcClient(
|
||||
formData.host!,
|
||||
formData.port!,
|
||||
formData.useSSL ?? false,
|
||||
ref.read(prefsChangeNotifierProvider),
|
||||
ref.read(pTorService),
|
||||
);
|
||||
|
||||
final health = await rpcClient.getHealth();
|
||||
Logging.instance.log(
|
||||
"Solana testNodeConnection \"health=$health\"",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
return true;
|
||||
} catch (_) {
|
||||
testPassed = false;
|
||||
}
|
||||
|
|