mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-30 06:48:49 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LongPressPopupBuilder extends StatelessWidget {
|
||||
const LongPressPopupBuilder(
|
||||
{super.key, required this.child, required this.popup, this.spacing = 8});
|
||||
|
||||
final Widget child;
|
||||
final Widget popup;
|
||||
final double spacing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onLongPress: () {
|
||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
final size = renderBox.size;
|
||||
|
||||
showPopUp(
|
||||
context: context,
|
||||
builder: (context) => _buildPopup(context, offset, size),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopup(BuildContext context, Offset offset, Size size) {
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: offset.dx * 2,
|
||||
top: offset.dy + size.height + spacing,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: popup,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class ChartsAsset {
|
||||
const ChartsAsset({required this.asset, required this.isFavorite});
|
||||
|
||||
factory ChartsAsset.fromJson(Map<String, dynamic> json) => ChartsAsset(
|
||||
asset: currencyFromApiString(json["asset"] as String) as CryptoCurrency,
|
||||
isFavorite: json["isFavorite"] != 0,
|
||||
);
|
||||
|
||||
final CryptoCurrency asset;
|
||||
final bool isFavorite;
|
||||
|
||||
static const tableName = "ChartsAssets";
|
||||
|
||||
Map<String, dynamic> toJson() => {"asset": asset.apiString, "isFavorite": isFavorite ? 1 : 0};
|
||||
|
||||
static Future<List<ChartsAsset>> get() async {
|
||||
final json = await db!.query(tableName);
|
||||
return List.generate(json.length, (index) => ChartsAsset.fromJson(json[index]));
|
||||
}
|
||||
|
||||
Future<void> insert() async {
|
||||
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> remove() async {
|
||||
await db!.delete(tableName, where: "asset = ?", whereArgs: [asset.apiString]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
extension DateTimeX on DateTime {
|
||||
int get secondsSinceEpoch => millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
static DateTime fromSecondsSinceEpoch(int seconds) =>
|
||||
DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "package:hash_wallet/.secrets.g.dart" as secrets;
|
||||
import "package:hash_wallet/utils/feature_flag.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/datetime_extension.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/utils/proxy_wrapper.dart";
|
||||
|
||||
const priceApiHost = "prices.cakewallet.com";
|
||||
const priceApiHostOnion = "46wisoe2uwipcj2j4og6smiq7hbmj34fkkrquwlzbbsqtgat7bf3erid.onion";
|
||||
|
||||
class LatestPriceRequest {
|
||||
LatestPriceRequest({required this.from, required this.to});
|
||||
|
||||
final Currency from;
|
||||
final Currency to;
|
||||
|
||||
Uri get uri => Uri.https(priceApiHost, "/v3/rates", {
|
||||
"base": from.apiString,
|
||||
"quote": to.apiString,
|
||||
});
|
||||
|
||||
Uri get onionUri => Uri.http(priceApiHostOnion, uri.path, uri.queryParameters);
|
||||
}
|
||||
|
||||
class PriceRequest {
|
||||
const PriceRequest({
|
||||
required this.interval,
|
||||
required this.from,
|
||||
required this.to,
|
||||
this.beginTime,
|
||||
this.count,
|
||||
});
|
||||
|
||||
final DateTime? beginTime;
|
||||
final Duration interval;
|
||||
final int? count;
|
||||
final Currency from;
|
||||
final Currency to;
|
||||
|
||||
Uri get uri => Uri.https(priceApiHost, "/v3/rates", {
|
||||
"start": (beginTime?.secondsSinceEpoch ?? 0).toString(),
|
||||
"end": DateTime.now().secondsSinceEpoch.toString(),
|
||||
"interval": "${interval.inSeconds}",
|
||||
if (count != null) "count": count.toString(),
|
||||
"quote": from.apiString,
|
||||
"base": to.apiString,
|
||||
});
|
||||
|
||||
Uri get onionUri => Uri.http(priceApiHostOnion, uri.path, uri.queryParameters);
|
||||
}
|
||||
|
||||
class PriceApiClient {
|
||||
static Future<Map<String, dynamic>?> _getJson(Uri uri) async {
|
||||
if (!FeatureFlag.isChartsEnabled) return null;
|
||||
final resp =
|
||||
await ProxyWrapper().get(headers: {"x-api-key": secrets.fiatApiKey}, clearnetUri: uri);
|
||||
if (!(resp.statusCode >= 200 && resp.statusCode < 300)) {
|
||||
printV("server returned code: ${resp.statusCode}\nuri: ${uri}\nresp body: ${resp.body}");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return jsonDecode(resp.body) as Map<String, dynamic>?;
|
||||
} catch (e) {
|
||||
printV("failed to decode response for ${uri.host}/${uri.path}: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<PriceData>> getPrices(PriceRequest request, {bool torOnly = false}) async {
|
||||
final List<PriceData> ret = [];
|
||||
printV(request.uri);
|
||||
final data = await _getJson(torOnly ? request.onionUri : request.uri);
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
final results = data["results"] as Map<String, dynamic>?;
|
||||
if (results == null) {
|
||||
printV(data.toString());
|
||||
return [];
|
||||
}
|
||||
printV(data.toString());
|
||||
for (final time in results.keys) {
|
||||
ret.add(
|
||||
PriceData(
|
||||
time: DateTimeX.fromSecondsSinceEpoch(int.parse(time)),
|
||||
base: request.from,
|
||||
quote: Money.parse((results[time] as num).toStringAsFixed(20), request.to, strictParsing: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static Future<PriceData?> getLatestPrice(
|
||||
LatestPriceRequest request, {
|
||||
bool torOnly = false,
|
||||
}) async {
|
||||
final data = await _getJson(torOnly ? request.onionUri : request.uri);
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
final results = data["results"] as Map<String, dynamic>?;
|
||||
if (results == null) {
|
||||
return null;
|
||||
}
|
||||
final result = results[results.keys.first];
|
||||
return PriceData(
|
||||
time: DateTime.now(),
|
||||
base: request.from,
|
||||
quote: Money.parse((result as num).toString(), request.to, strictParsing: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import "package:hash_wallet/entities/fiat_currency.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/datetime_extension.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
Currency currencyFromApiString(String key) {
|
||||
final parts = key.split(".");
|
||||
final type = parts[0];
|
||||
final id = parts[1];
|
||||
|
||||
switch (type) {
|
||||
case "fiat":
|
||||
return FiatCurrency.deserialize(raw: id);
|
||||
case "crypto":
|
||||
return CryptoCurrency.fromString(id);
|
||||
case "evm":
|
||||
throw UnimplementedError();
|
||||
case "sol":
|
||||
throw UnimplementedError();
|
||||
}
|
||||
throw Exception("unknown api string");
|
||||
}
|
||||
|
||||
class PriceData implements Comparable<PriceData> {
|
||||
const PriceData({required this.time, required this.base, required this.quote,});
|
||||
|
||||
factory PriceData.fromJson(Map<String, dynamic> json) => PriceData(
|
||||
time: DateTimeX.fromSecondsSinceEpoch(json["timestamp"] as int),
|
||||
base: currencyFromApiString(json["fromCurrency"] as String),
|
||||
quote: Money.parse(
|
||||
json["price"] as String, currencyFromApiString(json["toCurrency"] as String),
|
||||
strictParsing: false),
|
||||
);
|
||||
final DateTime time;
|
||||
final Currency base;
|
||||
final Money quote;
|
||||
|
||||
static const tableName = "PriceData";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"timestamp": time.secondsSinceEpoch.toString(),
|
||||
"price": quote.toString(),
|
||||
"fromCurrency": base.apiString,
|
||||
"toCurrency": quote.currency.apiString,
|
||||
};
|
||||
|
||||
static Future<List<PriceData>> get(
|
||||
Currency from,
|
||||
Currency to,
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
) async {
|
||||
final json = await db!.query(
|
||||
tableName,
|
||||
where: "fromCurrency = ? AND toCurrency = ? AND timestamp >= ? AND timestamp <= ?",
|
||||
whereArgs: [
|
||||
from.apiString,
|
||||
to.apiString,
|
||||
start?.secondsSinceEpoch ?? 0,
|
||||
// dart doesn't have INT_MAX, apparently.
|
||||
end?.secondsSinceEpoch ?? 0x7FFFFFFFFFFFFFFF,
|
||||
],
|
||||
);
|
||||
return List.generate(json.length, (index) => PriceData.fromJson(json[index]));
|
||||
}
|
||||
|
||||
Future<void> insert() async {
|
||||
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
static Future<void> insertMany(Iterable<PriceData> data) async {
|
||||
if (data.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final batch = db!.batch();
|
||||
|
||||
for (final datum in data) {
|
||||
batch.insert(
|
||||
tableName,
|
||||
datum.toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is PriceData &&
|
||||
time == other.time &&
|
||||
base == other.base &&
|
||||
quote.currency == other.quote.currency;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(time, base, quote.currency);
|
||||
|
||||
@override
|
||||
int compareTo(PriceData other) => time.compareTo(other.time);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import "package:hash_wallet/entities/fiat_api_mode.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/price_api_client.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/chart_range.dart";
|
||||
import "package:hash_wallet/store/settings_store.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
|
||||
abstract class PriceSource {
|
||||
const PriceSource();
|
||||
|
||||
Future<List<PriceData>> get(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
Currency from,
|
||||
Currency to,
|
||||
Duration interval,
|
||||
{required bool torOnly,}
|
||||
);
|
||||
}
|
||||
|
||||
mixin UpdatablePriceSource {
|
||||
Future<void> update(Iterable<PriceData> newData);
|
||||
}
|
||||
|
||||
class DatabasePriceSource extends PriceSource with UpdatablePriceSource {
|
||||
const DatabasePriceSource();
|
||||
|
||||
@override
|
||||
Future<List<PriceData>> get(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
Currency from,
|
||||
Currency to,
|
||||
Duration interval,
|
||||
{required bool torOnly,}
|
||||
) =>
|
||||
PriceData.get(from, to, start, end);
|
||||
|
||||
@override
|
||||
Future<void> update(Iterable<PriceData> newData) async {
|
||||
await PriceData.insertMany(newData);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiPriceSource extends PriceSource {
|
||||
const ApiPriceSource();
|
||||
|
||||
@override
|
||||
Future<List<PriceData>> get(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
Currency from,
|
||||
Currency to,
|
||||
Duration interval,
|
||||
{required bool torOnly,}
|
||||
) =>
|
||||
PriceApiClient.getPrices(
|
||||
torOnly: torOnly,
|
||||
PriceRequest(beginTime: start, interval: interval, from: from, to: to),
|
||||
);
|
||||
}
|
||||
|
||||
class PriceStore {
|
||||
const PriceStore({required this.settingsStore});
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
static const priceSources = [
|
||||
// TODO InMemoryPriceSource() - easily implementable w this pattern but idk if we need to optimize this that much
|
||||
DatabasePriceSource(),
|
||||
ApiPriceSource(),
|
||||
];
|
||||
|
||||
Future<List<PriceData>> getPrices(Currency from, Currency to, ChartRange range) async {
|
||||
final Set<PriceData> data = {};
|
||||
|
||||
final end = DateTime.now();
|
||||
DateTime? start;
|
||||
if (range.duration == null) {
|
||||
start = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
} else {
|
||||
start = end.subtract(range.duration!);
|
||||
}
|
||||
start = _alignedStart(start, range.dataPrecision);
|
||||
final alignedStart = start;
|
||||
|
||||
for (final source in priceSources) {
|
||||
final sourceData = await source.get(start!, end, from, to, range.dataPrecision,
|
||||
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly,);
|
||||
data.addAll(sourceData);
|
||||
start = _firstUnavailablePrice(data.toList(), range.dataPrecision, start, end);
|
||||
if (start == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (final source in priceSources) {
|
||||
if (source case final UpdatablePriceSource s) {
|
||||
await s.update(data);
|
||||
}
|
||||
}
|
||||
|
||||
return _alignedData(alignedStart, end, data, range.dataPrecision);
|
||||
}
|
||||
|
||||
static List<PriceData> _alignedData(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
Iterable<PriceData> data,
|
||||
Duration precision,
|
||||
) {
|
||||
final ret = data
|
||||
.where((datum) => datum.time.millisecondsSinceEpoch % precision.inMilliseconds == 0)
|
||||
.toList();
|
||||
ret.sort((a, b) => a.time.compareTo(b.time));
|
||||
return ret;
|
||||
}
|
||||
|
||||
static DateTime _alignedStart(DateTime start, Duration precision) {
|
||||
final alignedStartMs =
|
||||
(start.millisecondsSinceEpoch ~/ precision.inMilliseconds) * precision.inMilliseconds;
|
||||
return DateTime.fromMillisecondsSinceEpoch(alignedStartMs);
|
||||
}
|
||||
|
||||
static DateTime? _firstUnavailablePrice(
|
||||
List<PriceData> prices,
|
||||
Duration precision,
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
) {
|
||||
if (prices.isEmpty) {
|
||||
return start;
|
||||
}
|
||||
prices.sort();
|
||||
|
||||
final precisionMs = precision.inMilliseconds;
|
||||
DateTime expected = start;
|
||||
for (final price in prices) {
|
||||
if (price.time.millisecondsSinceEpoch % precisionMs != 0 || price.time.isBefore(start)) {
|
||||
continue;
|
||||
}
|
||||
if (price.time.difference(expected) > precision) {
|
||||
return expected;
|
||||
}
|
||||
expected = price.time.add(precision);
|
||||
}
|
||||
if (end.difference(expected) > precision) {
|
||||
return expected;
|
||||
}
|
||||
|
||||
final last = prices.last;
|
||||
printV("last.time: ${last.time.toIso8601String()} end: ${end.toIso8601String()}");
|
||||
if (last.time.isAfter(end.subtract(precision)) ||
|
||||
last.time.isAtSameMomentAs(end.subtract(precision))) {
|
||||
return null;
|
||||
}
|
||||
return last.time.add(precision);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
class ChartRange {
|
||||
const ChartRange._(this.duration, this.displayText, this.dataPrecision);
|
||||
|
||||
final Duration? duration;
|
||||
final String displayText;
|
||||
|
||||
// api can handle any precision up to 5min, regardless of data range.
|
||||
// we set a "preferred" precision for 2 reasons:
|
||||
// - less precision means less data sent, less bandwidth used, and thus less load on the backend
|
||||
// - less data points means the chart is easier to look through on a small screen
|
||||
final Duration dataPrecision;
|
||||
|
||||
static const oneHour = ChartRange._(Duration(hours: 1), "1H", Duration(minutes: 5));
|
||||
static const oneDay = ChartRange._(Duration(days: 1), "1D", Duration(minutes: 15));
|
||||
static const sevenDays = ChartRange._(Duration(days: 7), "7D", Duration(hours: 1));
|
||||
static const thirtyDays = ChartRange._(Duration(days: 30), "30D", Duration(hours: 4));
|
||||
static const oneYear = ChartRange._(Duration(days: 365), "1Y", Duration(days: 1));
|
||||
static const all = ChartRange._(null, "ALL", Duration(days: 1));
|
||||
|
||||
static const ranges = [oneHour, oneDay, sevenDays, thirtyDays, oneYear, all];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
|
||||
class PriceChangeData implements Comparable<PriceChangeData> {
|
||||
const PriceChangeData({required this.direction, required this.amount, required this.percentage});
|
||||
|
||||
final PriceChangeDirection direction;
|
||||
final Money amount;
|
||||
final String percentage;
|
||||
|
||||
@override
|
||||
int compareTo(PriceChangeData other) {
|
||||
final double thisValue =
|
||||
double.parse(percentage) * (direction == PriceChangeDirection.up ? 1 : -1);
|
||||
final double otherValue =
|
||||
double.parse(other.percentage) * (other.direction == PriceChangeDirection.up ? 1 : -1);
|
||||
return thisValue.compareTo(otherValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import "dart:ui";
|
||||
|
||||
class PriceChangeDirection {
|
||||
|
||||
const PriceChangeDirection._(this.color, this.symbol);
|
||||
final Color color;
|
||||
final String symbol;
|
||||
|
||||
static const up = PriceChangeDirection._(Color(0xFF6FC84E), "+");
|
||||
static const down = PriceChangeDirection._(Color(0xFFEA696F), "-");
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_data.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
|
||||
const List<CryptoCurrency> cryptoCurrenciesByMarketcap = [
|
||||
CryptoCurrency.btc,
|
||||
CryptoCurrency.eth,
|
||||
CryptoCurrency.usdt,
|
||||
CryptoCurrency.xrp,
|
||||
CryptoCurrency.bnb,
|
||||
CryptoCurrency.sol,
|
||||
CryptoCurrency.usdc,
|
||||
CryptoCurrency.doge,
|
||||
CryptoCurrency.trx,
|
||||
CryptoCurrency.ton,
|
||||
CryptoCurrency.bch,
|
||||
CryptoCurrency.ltc,
|
||||
CryptoCurrency.xmr,
|
||||
CryptoCurrency.matic,
|
||||
CryptoCurrency.arb,
|
||||
CryptoCurrency.dai,
|
||||
CryptoCurrency.paxg,
|
||||
CryptoCurrency.zec,
|
||||
CryptoCurrency.dcr,
|
||||
CryptoCurrency.nano,
|
||||
CryptoCurrency.zano,
|
||||
CryptoCurrency.deuro,
|
||||
CryptoCurrency.wow,
|
||||
];
|
||||
|
||||
abstract class PriceDataSortCriterium {
|
||||
const PriceDataSortCriterium();
|
||||
|
||||
String get name;
|
||||
|
||||
String get iconPath;
|
||||
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
);
|
||||
|
||||
static const all = [
|
||||
MarketcapSortCriterium(),
|
||||
GainsSortCriterium(),
|
||||
LossesSortCriterium(),
|
||||
AlphabeticalSortCriterium(),
|
||||
];
|
||||
}
|
||||
|
||||
class AlphabeticalSortCriterium extends PriceDataSortCriterium {
|
||||
const AlphabeticalSortCriterium();
|
||||
|
||||
@override
|
||||
String get name => S.current.alphabetical;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/charts_sort_criteria/alpha.svg";
|
||||
|
||||
@override
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
) =>
|
||||
(a.fullName ?? a.title).compareTo(b.fullName ?? b.title);
|
||||
}
|
||||
|
||||
class MarketcapSortCriterium extends PriceDataSortCriterium {
|
||||
const MarketcapSortCriterium();
|
||||
|
||||
@override
|
||||
String get name => S.current.marketcap;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/charts_sort_criteria/marketcap.svg";
|
||||
|
||||
@override
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
) {
|
||||
final aIndex = cryptoCurrenciesByMarketcap.indexOf(a);
|
||||
final bIndex = cryptoCurrenciesByMarketcap.indexOf(b);
|
||||
|
||||
if (aIndex != -1 && bIndex != -1) {
|
||||
return aIndex.compareTo(bIndex);
|
||||
}
|
||||
if (aIndex != -1) {
|
||||
return -1;
|
||||
}
|
||||
if (bIndex != -1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (a.fullName ?? a.title).compareTo(b.fullName ?? b.title);
|
||||
}
|
||||
}
|
||||
|
||||
class GainsSortCriterium extends PriceDataSortCriterium {
|
||||
const GainsSortCriterium();
|
||||
|
||||
@override
|
||||
String get name => S.current.gains;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/charts_sort_criteria/gains.svg";
|
||||
|
||||
@override
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
) =>
|
||||
changeDataB.compareTo(changeDataA);
|
||||
}
|
||||
|
||||
class LossesSortCriterium extends PriceDataSortCriterium {
|
||||
const LossesSortCriterium();
|
||||
|
||||
@override
|
||||
String get name => S.current.losses;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/charts_sort_criteria/losses.svg";
|
||||
|
||||
@override
|
||||
int comparator(
|
||||
PriceChangeData changeDataA,
|
||||
PriceChangeData changeDataB,
|
||||
CryptoCurrency a,
|
||||
CryptoCurrency b,
|
||||
) =>
|
||||
changeDataA.compareTo(changeDataB);
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/preferences_key.dart';
|
||||
import "package:hash_wallet/new-ui/page_open_listener.dart";
|
||||
import "package:hash_wallet/new-ui/page_open_listener.dart";
|
||||
import 'package:hash_wallet/new-ui/pages/charts_page.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/home_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/changelog_modal.dart';
|
||||
import "package:hash_wallet/new-ui/widgets/wallet_removal_popup.dart";
|
||||
import 'package:hash_wallet/src/screens/contact/contact_list_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/cake_features_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart';
|
||||
@@ -17,6 +22,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../view_model/dashboard/dashboard_view_model.dart';
|
||||
|
||||
class NewDashboard extends StatefulWidget {
|
||||
@@ -34,7 +40,7 @@ class NewDashboard extends StatefulWidget {
|
||||
getIt.get<WalletListPage>(),
|
||||
getIt.get<ContactListPage>(),
|
||||
getIt.get<CakeFeaturesPage>(),
|
||||
Placeholder(),
|
||||
if (FeatureFlag.isChartsEnabled) getIt.get<ChartsPage>(),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -51,6 +57,7 @@ class _NewDashboardState extends State<NewDashboard> {
|
||||
setState(() {
|
||||
_selectedPage = 0;
|
||||
});
|
||||
_showWalletRemovalPopup(context);
|
||||
});
|
||||
|
||||
// Hash Bags: changelog modal disabled. The Cake-supplied changelog
|
||||
@@ -60,6 +67,7 @@ class _NewDashboardState extends State<NewDashboard> {
|
||||
// own changelog content + versioning under our control.
|
||||
// Future.delayed(Duration(milliseconds: 300)).then((_) => _showChangelog(context));
|
||||
_showVulnerableSeedsPopup(context);
|
||||
_showWalletRemovalPopup(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -68,6 +76,7 @@ class _NewDashboardState extends State<NewDashboard> {
|
||||
bottomSheetService: widget.bottomSheetService,
|
||||
child: CupertinoScaffold(
|
||||
body: Material(
|
||||
key: ValueKey("new_dashboard_page_key"),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
@@ -95,14 +104,18 @@ class _NewDashboardState extends State<NewDashboard> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
bottom: !(Platform.isIOS),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
|
||||
child: AbsorbPointer(
|
||||
absorbing: true,
|
||||
child: Container(color: Colors.transparent),
|
||||
// Invisible pointer-absorbing strip behind the nav bar: it must not
|
||||
// be reachable by screen-reader traversal either.
|
||||
ExcludeSemantics(
|
||||
child: SafeArea(
|
||||
bottom: !(Platform.isIOS),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
|
||||
child: AbsorbPointer(
|
||||
absorbing: true,
|
||||
child: Container(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -113,6 +126,9 @@ class _NewDashboardState extends State<NewDashboard> {
|
||||
setState(() {
|
||||
_selectedPage = index;
|
||||
});
|
||||
if (widget.dashboardPageWidgets[_selectedPage] case PageOpenListener page) {
|
||||
page.onPageOpen();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -171,4 +187,14 @@ class _NewDashboardState extends State<NewDashboard> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showWalletRemovalPopup(BuildContext context) async {
|
||||
if(await widget.dashboardViewModel.shouldShowRemovalPopup()) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
final toBeRemoved = await widget.dashboardViewModel.walletsToBeRemoved;
|
||||
if(context.mounted) {
|
||||
await showPopUp<void>(context: context, builder: (context)=>WalletRemovalPopup(affectedWallets: toBeRemoved,));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
abstract interface class PageOpenListener {
|
||||
void onPageOpen();
|
||||
}
|
||||
@@ -56,6 +56,7 @@ class _AboutPageState extends State<AboutPage> {
|
||||
topBar: ModalTopBar(
|
||||
title: S.of(context).about,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
content: Container(
|
||||
|
||||
@@ -1,493 +0,0 @@
|
||||
import 'dart:ui';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/monero/monero.dart';
|
||||
import 'package:hash_wallet/wownero/wownero.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/card_customizer.dart';
|
||||
import 'package:hash_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/modal_grab_handle.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/account_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
|
||||
import 'package:cw_core/balance_card_style_settings.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/generate_name.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class AccountCustomizerListItem {
|
||||
final BalanceCard card;
|
||||
final int order;
|
||||
final AccountListItem accountListItem;
|
||||
|
||||
AccountCustomizerListItem(
|
||||
{required this.card, required this.order, required this.accountListItem});
|
||||
}
|
||||
|
||||
class AccountCustomizer extends StatefulWidget {
|
||||
const AccountCustomizer(
|
||||
{super.key,
|
||||
required this.accountListViewModel,
|
||||
required this.accountEditOrCreateViewModel,
|
||||
required this.dashboardViewModel});
|
||||
|
||||
final MoneroAccountListViewModel accountListViewModel;
|
||||
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
State<AccountCustomizer> createState() => _AccountCustomizerState();
|
||||
}
|
||||
|
||||
class _AccountCustomizerState extends State<AccountCustomizer> {
|
||||
static const double _kStackVisibleFactor = 0.2;
|
||||
late final double cardWidth = MediaQuery.of(context).size.width * 0.9;
|
||||
|
||||
final List<AccountCustomizerListItem> _items = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadCards();
|
||||
final wallet = widget.dashboardViewModel.wallet;
|
||||
final int? activeId = wallet.type == WalletType.monero
|
||||
? monero?.getCurrentAccount(wallet).id
|
||||
: wallet.type == WalletType.wownero
|
||||
? wownero?.getCurrentAccount(wallet).id
|
||||
: null;
|
||||
if (activeId == null) return;
|
||||
for (int i = 0; i < _items.length - 1; i++) {
|
||||
if (_items[i].accountListItem.id == activeId) {
|
||||
final lastIndex = _items.length - 1;
|
||||
final temp = _items[i];
|
||||
_items[i] = _items[lastIndex];
|
||||
_items[lastIndex] = temp;
|
||||
saveCardOrder();
|
||||
widget.dashboardViewModel.loadCardDesigns();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadCards() {
|
||||
_items.clear();
|
||||
|
||||
final accounts = widget.accountListViewModel.accounts;
|
||||
for (int i = 0; i < accounts.length; i++) {
|
||||
final index = widget.dashboardViewModel.cardOrder[i];
|
||||
|
||||
if (index == null || index >= accounts.length) {
|
||||
// db order broken.
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
|
||||
_items.add(AccountCustomizerListItem(
|
||||
card: BalanceCard(
|
||||
accountName: accounts[index].label,
|
||||
accountIndex: accounts[index].id,
|
||||
balance: accounts[index].balance ?? "0.00",
|
||||
accountBalance: accounts[index].balance ?? "0.00",
|
||||
designSwitchDuration: Duration.zero,
|
||||
assetName: widget.accountListViewModel.currency.title,
|
||||
onCustomizeTapped: (i == accounts.length - 1) ? _openCardCustomizer : null,
|
||||
selected: i == accounts.length - 1,
|
||||
width: cardWidth,
|
||||
design: widget.dashboardViewModel.cardDesigns[index],
|
||||
),
|
||||
order: index,
|
||||
accountListItem: accounts[index]));
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_items.isEmpty) return SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).wallet_accounts,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingIcon: Icon(Icons.refresh),
|
||||
onTrailingPressed: showResetDialog,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Text(
|
||||
S.of(context).account_customizer_desc,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
ReorderableListView.builder(
|
||||
padding: EdgeInsets.only(bottom: 196),
|
||||
scrollController: ModalScrollController.of(context),
|
||||
onReorder: reorder,
|
||||
proxyDecorator: (child, index, animation) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, _) {
|
||||
final animValue = Curves.easeOutCubic.transform(animation.value);
|
||||
final scale = lerpDouble(1, 1.05, animValue)!;
|
||||
|
||||
return Opacity(
|
||||
opacity: 1 - animValue.clamp(0.0, 0.1),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: cardWidth,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _items[index].card,
|
||||
);
|
||||
},
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final card = _items[index].card;
|
||||
|
||||
return Container(
|
||||
key: ValueKey(index),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
reorder(index, _items.length);
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: _kStackVisibleFactor,
|
||||
child: card,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 50),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
onTap: _showAddAccountModal,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add,
|
||||
size: 28,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
S.of(context).add_account,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)))),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _checkReadyToManage() {
|
||||
if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).wallet_is_syncing,
|
||||
alertContent: S.of(context).cannot_manage_accounts_during_sync,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: Navigator.of(context).pop));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _showAddAccountModal() async {
|
||||
if (!_checkReadyToManage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final res = await showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
final modal = getIt.get<AccountCreationModal>();
|
||||
return Material(child: modal);
|
||||
});
|
||||
if (res != null && res is bool && res == true) {
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
loadCards();
|
||||
await saveCardOrder();
|
||||
}
|
||||
}
|
||||
|
||||
void _openCardCustomizer() {
|
||||
if (!_checkReadyToManage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bloc = getIt.get<CardCustomizerBloc>(param1: false);
|
||||
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) {
|
||||
return BlocProvider(
|
||||
create: (context) => bloc,
|
||||
child: Material(
|
||||
child: CardCustomizer(
|
||||
cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
|
||||
widget.dashboardViewModel.wallet.currency.name,
|
||||
cryptoName: widget.dashboardViewModel.wallet.currency.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)).then((_) async {
|
||||
bloc.add(DesignSaved());
|
||||
await bloc.stream.firstWhere((item) => item is CardCustomizerSaved);
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
loadCards();
|
||||
});
|
||||
}
|
||||
|
||||
void reorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final AccountCustomizerListItem item = _items.removeAt(oldIndex);
|
||||
_items.insert(newIndex, item);
|
||||
});
|
||||
|
||||
// necessary to copy all this to keep constant constructor for BalanceCard
|
||||
for (int i = 0; i < _items.length; i++) {
|
||||
_items[i] = AccountCustomizerListItem(
|
||||
card: BalanceCard(
|
||||
accountName: _items[i].card.accountName,
|
||||
balance: _items[i].card.balance,
|
||||
accountIndex: _items[i].card.accountIndex,
|
||||
accountBalance: _items[i].card.accountBalance,
|
||||
assetName: _items[i].card.assetName,
|
||||
designSwitchDuration: _items[i].card.designSwitchDuration,
|
||||
onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null,
|
||||
selected: i == _items.length - 1,
|
||||
width: _items[i].card.width,
|
||||
design: _items[i].card.design,
|
||||
),
|
||||
order: i,
|
||||
accountListItem: _items[i].accountListItem);
|
||||
}
|
||||
|
||||
if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
|
||||
widget.accountListViewModel.select(_items[_items.length - 1].accountListItem);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveCardOrder() async {
|
||||
for (int i = 0; i < _items.length; i++) {
|
||||
final idx = _items.indexWhere((element) => element.accountListItem.id == i);
|
||||
printV("$i: $idx");
|
||||
|
||||
await BalanceCardStyleSettings.fromCardDesign(
|
||||
widget.dashboardViewModel.wallet.walletInfo.internalId,
|
||||
i,
|
||||
idx,
|
||||
_items[idx].card.design)
|
||||
.insert();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showResetDialog() async {
|
||||
final res = await showPopUp(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).reset,
|
||||
alertContent: S.of(context).card_order_reset_desc,
|
||||
leftButtonText: S.of(context).yes,
|
||||
rightButtonText: S.of(context).no,
|
||||
actionLeftButton: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
actionRightButton: Navigator.of(context).pop);
|
||||
});
|
||||
if (res != null && res is bool && res) {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reset() async {
|
||||
_items.clear();
|
||||
|
||||
final accounts = widget.accountListViewModel.accounts;
|
||||
for (int i = 0; i < widget.accountListViewModel.accounts.length; i++) {
|
||||
_items.add(AccountCustomizerListItem(
|
||||
card: BalanceCard(
|
||||
accountName: accounts[i].label,
|
||||
accountIndex: accounts[i].id,
|
||||
balance: accounts[i].balance ?? "0.00",
|
||||
accountBalance: accounts[i].balance ?? "0.00",
|
||||
assetName: widget.accountListViewModel.currency.title,
|
||||
selected: true,
|
||||
designSwitchDuration: Duration(milliseconds: 200),
|
||||
width: cardWidth,
|
||||
design: i >= widget.dashboardViewModel.cardDesigns.length
|
||||
? CardDesign.genericDefault
|
||||
: widget.dashboardViewModel.cardDesigns[i],
|
||||
),
|
||||
order: i,
|
||||
accountListItem: accounts[i]));
|
||||
}
|
||||
|
||||
saveCardOrder();
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
class AccountCreationModal extends StatefulWidget {
|
||||
const AccountCreationModal({super.key, required this.accountEditOrCreateViewModel});
|
||||
|
||||
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
|
||||
|
||||
@override
|
||||
State<AccountCreationModal> createState() => _AccountCreationModalState();
|
||||
}
|
||||
|
||||
class _AccountCreationModalState extends State<AccountCreationModal> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(30))),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ModalGrabHandle(),
|
||||
ModalTopBar(title: S.of(context).create_account),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
spacing: 50,
|
||||
children: [
|
||||
SizedBox(),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(hintText: S.of(context).account_name),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
_controller.text = await generateName();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/randomize.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
NewPrimaryButton(
|
||||
onPressed: () async {
|
||||
if (_loading) return;
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
widget.accountEditOrCreateViewModel.label = _controller.text;
|
||||
await widget.accountEditOrCreateViewModel.save();
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
text: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: _loading,
|
||||
),
|
||||
SizedBox(),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/education_page.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class AccountEducationPage extends EducationPage {
|
||||
const AccountEducationPage({required super.settingsStore, super.key});
|
||||
|
||||
@override
|
||||
String get educationId => "accounts";
|
||||
|
||||
@override
|
||||
String get title => S.current.accounts;
|
||||
|
||||
@override
|
||||
String get iconPath => "assets/new-ui/settings_row_icons/accounts.svg";
|
||||
|
||||
@override
|
||||
String get completionLabel => S.current.accounts_education_understand_continue;
|
||||
|
||||
@override
|
||||
String progressLabel(int current, int total) =>
|
||||
S.current.accounts_education_progress("$current", "$total");
|
||||
|
||||
@override
|
||||
List<EducationSlide> get slides => <EducationSlide>[
|
||||
EducationSlide(
|
||||
distributeChildren: true,
|
||||
children: [
|
||||
EducationText(text: S.current.accounts_education_organize_title),
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/accounts_overview.svg",
|
||||
key: ValueKey("accounts-education-overview-image"),
|
||||
width: 133,
|
||||
height: 324,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_organize_description,
|
||||
secondary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
EducationSlide(
|
||||
children: [
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/recovery_shield.svg",
|
||||
key: ValueKey("accounts-education-recovery-image"),
|
||||
width: 213,
|
||||
height: 213,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 25.5),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_recovery_title,
|
||||
highlightedText: S.current.accounts_education_recovery_highlight,
|
||||
),
|
||||
const SizedBox(height: 25.5),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_recovery_description,
|
||||
secondary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
EducationSlide(
|
||||
children: [
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/account_order.svg",
|
||||
key: ValueKey("accounts-education-order-image"),
|
||||
width: 309,
|
||||
height: 48,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 51),
|
||||
EducationText(text: S.current.accounts_education_order_title),
|
||||
const SizedBox(height: 51),
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/ordered_account.svg",
|
||||
key: ValueKey("accounts-education-ordered-account-image"),
|
||||
width: 160,
|
||||
height: 99,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 51),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EducationText(
|
||||
text: S.current.accounts_education_order_description,
|
||||
highlightedText: S.current.accounts_education_order_highlight,
|
||||
),
|
||||
const SizedBox(height: 25.5),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_order_restore_description,
|
||||
secondary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
EducationSlide(
|
||||
children: [
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/account_education/archive_warning.svg",
|
||||
key: ValueKey("accounts-education-archive-image"),
|
||||
width: 232,
|
||||
height: 101,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 51),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EducationText(
|
||||
text: S.current.accounts_education_archive_title,
|
||||
warning: true,
|
||||
),
|
||||
const SizedBox(height: 13),
|
||||
EducationText(
|
||||
text: S.current.accounts_education_archive_description,
|
||||
secondary: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -2,12 +2,11 @@ import 'dart:ui';
|
||||
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/monero/monero.dart';
|
||||
import 'package:hash_wallet/new-ui/long_press_popup.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/addresses_page/address_info.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/long_press_menu.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/long_press_menu/long_press_menu.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
@@ -16,26 +15,36 @@ import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:hash_wallet/wownero/wownero.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
import 'package:hash_wallet/utils/list_item.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
|
||||
class AddressesPageArgs {
|
||||
AddressesPageArgs({required this.showHidden, this.popOnSelection = false});
|
||||
|
||||
final bool showHidden;
|
||||
final bool popOnSelection;
|
||||
}
|
||||
|
||||
|
||||
class NewAddressesPage extends StatefulWidget {
|
||||
const NewAddressesPage(
|
||||
{super.key,
|
||||
required this.addressListViewModel,
|
||||
required this.dashboardViewModel,
|
||||
required this.showHidden});
|
||||
required this.showHidden, required this.popOnSelection});
|
||||
|
||||
final WalletAddressListViewModel addressListViewModel;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool showHidden;
|
||||
final bool popOnSelection;
|
||||
|
||||
@override
|
||||
State<NewAddressesPage> createState() => _NewAddressesPageState();
|
||||
@@ -58,6 +67,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
late final TextEditingController _searchController;
|
||||
|
||||
CardDesign? design;
|
||||
ReactionDisposer? _cardDesignsDisposer;
|
||||
|
||||
void updateItems() {
|
||||
setState(() {
|
||||
@@ -75,15 +85,32 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
loadCardDesign();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
reaction((context) => widget.dashboardViewModel.cardDesigns.first, (value) {
|
||||
if (!mounted) return;
|
||||
_cardDesignsDisposer = reaction((_) => widget.dashboardViewModel.currentCardDesign, (value) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
design = value;
|
||||
design = widget.dashboardViewModel.currentCardDesign;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> loadCardDesign() async {
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
design = widget.dashboardViewModel.currentCardDesign;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cardDesignsDisposer?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final filteredItems = items.where((item) {
|
||||
@@ -105,6 +132,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
ModalTopBar(
|
||||
title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses,
|
||||
leadingIcon: Icon(Icons.arrow_back),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
onTrailingPressed: () {}),
|
||||
Expanded(
|
||||
@@ -118,8 +146,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
children: [
|
||||
if (widget.dashboardViewModel.type == WalletType.monero ||
|
||||
widget.dashboardViewModel.type == WalletType.wownero)
|
||||
if (widget.dashboardViewModel.isMultiAccountsEnabled)
|
||||
Observer(
|
||||
builder: (_) => AccountPreviewHeader(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
@@ -131,6 +158,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
if(!widget.popOnSelection)
|
||||
ShowHiddenButton()
|
||||
],
|
||||
),
|
||||
@@ -146,11 +174,15 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
|
||||
builder: (_) => AddressRow(
|
||||
selected:
|
||||
item.address == widget.addressListViewModel.address.address,
|
||||
first: widget.showHidden && index == 0,
|
||||
first: (widget.showHidden || widget.popOnSelection) && index == 0,
|
||||
last: index == filteredItems.length - 1,
|
||||
item: item,
|
||||
onSelect: () {
|
||||
widget.addressListViewModel.setAddress(item);
|
||||
if(widget.popOnSelection) {
|
||||
Navigator.of(context).pop(item.address);
|
||||
} else {
|
||||
widget.addressListViewModel.setAddress(item);
|
||||
}
|
||||
},
|
||||
onLabelChanged: updateItems,
|
||||
onAddressHidden: () async {
|
||||
@@ -212,24 +244,31 @@ class AddressSearchBox extends StatelessWidget {
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1),
|
||||
borderRadius: BorderRadius.circular(99999)),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).search,
|
||||
hintStyle: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
// The hint names the field only while it is empty, so the label is
|
||||
// supplied once there is text to keep exactly one announcement.
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
label: controller.text.isEmpty ? null : S.of(context).search,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).search,
|
||||
hintStyle: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
prefixIcon: ExcludeSemantics(child: Icon(Icons.search)),
|
||||
filled: false,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
),
|
||||
prefixIcon: Icon(Icons.search),
|
||||
filled: false,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(99999),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -247,8 +286,7 @@ class AccountPreviewHeader extends StatelessWidget {
|
||||
final CardDesign? design;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
Widget build(BuildContext context) => Container(
|
||||
height: 64,
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
decoration: BoxDecoration(
|
||||
@@ -271,14 +309,7 @@ class AccountPreviewHeader extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
// Dispatch by wallet type: the null-aware operator only
|
||||
// guards plugin presence, so monero?. on a Wownero
|
||||
// wallet still executes CWMonero's cast and throws.
|
||||
dashboardViewModel.wallet.type == WalletType.monero
|
||||
? monero?.getCurrentAccount(dashboardViewModel.wallet).label ?? ""
|
||||
: dashboardViewModel.wallet.type == WalletType.wownero
|
||||
? wownero?.getCurrentAccount(dashboardViewModel.wallet).label ?? ""
|
||||
: "",
|
||||
dashboardViewModel.accountListViewModel?.selectedAccount?.label ?? '',
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
Text(
|
||||
@@ -306,7 +337,6 @@ class AccountPreviewHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddressRow extends StatelessWidget {
|
||||
@@ -338,116 +368,139 @@ class AddressRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasLabel = item.name != null && item.name!.isNotEmpty;
|
||||
final hideLabel = item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onSelect,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: LongPressPopupBuilder(
|
||||
popup: LongPressMenu(
|
||||
items: [
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).set_label,
|
||||
iconPath: "assets/new-ui/address_set_label.svg",
|
||||
onSelected: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
final res = await showPopUp(
|
||||
context: context,
|
||||
builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
|
||||
Future<void> editLabel() async {
|
||||
final res = await showPopUp(
|
||||
context: context, builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
|
||||
|
||||
if (res != null) {
|
||||
onLabelChanged();
|
||||
}
|
||||
}),
|
||||
LongPressMenuItem(
|
||||
label: item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address,
|
||||
iconPath: "assets/new-ui/address_hide.svg",
|
||||
onSelected: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
onAddressHidden();
|
||||
},
|
||||
color: Theme.of(context).colorScheme.error),
|
||||
LongPressMenuItem(
|
||||
label: 'Info',
|
||||
iconPath: "assets/images/info_icon.svg",
|
||||
onSelected: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
await showPopUp(
|
||||
context: context,
|
||||
builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
|
||||
}),
|
||||
],
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(first ? 16 : 0),
|
||||
bottom: Radius.circular(last ? 16 : 0),
|
||||
if (res != null) {
|
||||
onLabelChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showInfo() async {
|
||||
await showPopUp(
|
||||
context: context, builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
|
||||
}
|
||||
|
||||
// The row is a single control: selection on tap, and the actions that are
|
||||
// otherwise reachable only by long press exposed as custom actions.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
|
||||
CustomSemanticsAction(label: S.of(context).set_label): editLabel,
|
||||
CustomSemanticsAction(label: hideLabel): onAddressHidden,
|
||||
CustomSemanticsAction(label: S.of(context).info): showInfo,
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: onSelect,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: LongPressPopupBuilder(
|
||||
popup: LongPressMenu(
|
||||
items: [
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).set_label,
|
||||
iconPath: "assets/new-ui/address_set_label.svg",
|
||||
onSelected: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
editLabel();
|
||||
}),
|
||||
LongPressMenuItem(
|
||||
label: hideLabel,
|
||||
iconPath: "assets/new-ui/address_hide.svg",
|
||||
onSelected: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
onAddressHidden();
|
||||
},
|
||||
color: Theme.of(context).colorScheme.error),
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).info,
|
||||
iconPath: "assets/images/info_icon.svg",
|
||||
onSelected: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
showInfo();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
spacing: 4,
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(first ? 16 : 0),
|
||||
bottom: Radius.circular(last ? 16 : 0),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (hasLabel)
|
||||
Text(
|
||||
item.name!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Expanded(
|
||||
child: AddressFormatter.buildSegmentedAddress(
|
||||
address: item.address,
|
||||
walletType: walletType,
|
||||
textAlign: TextAlign.left,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (hasLabel)
|
||||
Text(
|
||||
item.name!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: hasLabel
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
shouldTruncate: (hasLabel)),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Expanded(
|
||||
child: AddressFormatter.buildSegmentedAddress(
|
||||
address: item.address,
|
||||
walletType: walletType,
|
||||
textAlign: TextAlign.left,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: hasLabel
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
shouldTruncate: (hasLabel)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${S.of(context).transactions}: ${item.txCount}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
"${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${S.of(context).transactions}: ${item.txCount}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
"${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
// The row's selected state carries this information.
|
||||
if (selected)
|
||||
ExcludeSemantics(
|
||||
child: HashImageWidget(imageUrl: "assets/new-ui/checkmark.svg"))
|
||||
],
|
||||
),
|
||||
if (selected) HashImageWidget(imageUrl: "assets/new-ui/checkmark.svg")
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -467,26 +520,34 @@ class ShowHiddenButton extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).show_hidden_addresses),
|
||||
RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: HashImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg"))
|
||||
],
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).show_hidden_addresses),
|
||||
ExcludeSemantics(
|
||||
child: RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child:
|
||||
HashImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg")),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -116,8 +116,10 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
|
||||
ModalTopBar(
|
||||
title: S.of(context).enter_amount,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
|
||||
trailingIcon: Icon(Icons.history),
|
||||
trailingSemanticLabel: S.of(context).history,
|
||||
onTrailingPressed: () {
|
||||
Navigator.pushNamed(context, Routes.bridgeHistoryPage,
|
||||
arguments: widget.bridgeHistoryViewModel);
|
||||
@@ -274,6 +276,7 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
iconColor: theme.colorScheme.onPrimary,
|
||||
icon: Icon(Icons.arrow_forward, size: 20),
|
||||
semanticLabel: S.of(context).continue_text,
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
|
||||
@@ -87,6 +87,7 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
|
||||
],
|
||||
),
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
);
|
||||
},
|
||||
@@ -217,6 +218,7 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
|
||||
bridgeViewModel.executeBridge();
|
||||
},
|
||||
swiperText: S.of(context).swipe_to_bridge,
|
||||
accessibleNavigationModeButtonText: S.of(context).confirm,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -46,6 +46,7 @@ class BridgeDetailPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: "Bridge Transfer Details",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/view_model/bridge/bridge_history_view_model.dart';
|
||||
@@ -25,6 +26,7 @@ class BridgeHistoryPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: "Bridge history",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
@@ -30,6 +31,7 @@ class BridgeNetworkPage extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: 'Destination Network',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
SizedBox(height: 48),
|
||||
@@ -57,7 +59,8 @@ class BridgeNetworkPage extends StatelessWidget {
|
||||
(chain) {
|
||||
final chainName = chain.name;
|
||||
return ListItemRegularRow(
|
||||
iconPath: 'assets/images/crypto/${chainName.toLowerCase()}.webp',
|
||||
iconPath:
|
||||
'assets/new-ui/crypto_full_icons/${chainName.toLowerCase()}.svg',
|
||||
keyValue: chain.chainId.toString(),
|
||||
label: chainName,
|
||||
onTap: () {
|
||||
|
||||
@@ -55,6 +55,7 @@ class _BridgeReceiveAddressInputPageState extends State<BridgeReceiveAddressInpu
|
||||
ModalTopBar(
|
||||
title: 'Input Receive Address',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
SizedBox(height: 48),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hash_wallet/view_model/bridge/bridge_receiving_wallet_option.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/bridge/bridge_receive_address_input_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
@@ -42,6 +43,7 @@ class _BridgeReceivingWalletPageState extends State<BridgeReceivingWalletPage> {
|
||||
ModalTopBar(
|
||||
title: 'Receiving Wallet',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
@@ -63,7 +65,7 @@ class _BridgeReceivingWalletPageState extends State<BridgeReceivingWalletPage> {
|
||||
child: HashImageWidget(
|
||||
borderRadius: 8,
|
||||
imageUrl: chainName.isNotEmpty
|
||||
? 'assets/images/crypto/${chainName.toLowerCase()}.webp'
|
||||
? 'assets/new-ui/crypto_full_icons/${chainName.toLowerCase()}.svg'
|
||||
: null,
|
||||
width: 72,
|
||||
height: 72,
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:hash_wallet/buy/payment_method.dart";
|
||||
import "package:hash_wallet/buy/sell_buy_states.dart";
|
||||
import "package:hash_wallet/entities/fiat_currency.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_provider_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/floating_amount_input.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/currency_symbol_text.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/token_chain_display.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
|
||||
class NewBuySellParams {
|
||||
NewBuySellParams({required this.mode, this.initialCurrency});
|
||||
|
||||
final BuySellPageMode mode;
|
||||
final CryptoCurrency? initialCurrency;
|
||||
}
|
||||
|
||||
class NewBuySellAmountPage extends StatefulWidget {
|
||||
const NewBuySellAmountPage({required this.buySellViewModel, super.key});
|
||||
|
||||
final BuySellViewModel buySellViewModel;
|
||||
|
||||
@override
|
||||
State<NewBuySellAmountPage> createState() => _NewBuySellAmountPageState();
|
||||
}
|
||||
|
||||
class _NewBuySellAmountPageState extends State<NewBuySellAmountPage> {
|
||||
bool _customAmountMode = false;
|
||||
bool _isLoadingPaymentMethods = false;
|
||||
final customInputController = TextEditingController();
|
||||
final customInputFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
customInputController.dispose();
|
||||
customInputFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// this is a hack for the "up to" display to show the actual upper limit.
|
||||
// limits are loaded alongside quotes, and quotes for some providers only load properly with an amount and payment method selected.
|
||||
// it'll be refactored soon anyway, i guess.
|
||||
widget.buySellViewModel.changeFiatAmount(amount: widget.buySellViewModel.defaultAmounts[3]);
|
||||
widget.buySellViewModel.calculateBestRate();
|
||||
when(
|
||||
(_) => widget.buySellViewModel.paymentMethodState is PaymentMethodLoaded,
|
||||
() => widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods
|
||||
.firstWhere((item) => item.paymentMethodType == PaymentType.all),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => ModalTopBar(
|
||||
title: _pageTitle,
|
||||
bottomText: !_customAmountMode || widget.buySellViewModel.maxFiatAmount == null
|
||||
? null
|
||||
: "${S.of(context).up_to} ~${widget.buySellViewModel.maxFiatAmount} ${widget.buySellViewModel.fiatCurrency.title}",
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _customAmountMode
|
||||
? BuySellCustomAmountInput(
|
||||
fiatCurrency: widget.buySellViewModel.fiatCurrency,
|
||||
cryptoCurrency: widget.buySellViewModel.cryptoCurrency,
|
||||
cryptoAmount: widget.buySellViewModel.cryptoAmount,
|
||||
isLoading: _isLoadingPaymentMethods,
|
||||
hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies,
|
||||
onCurrencySelectorPressed: () => selectCryptoCurrency(context),
|
||||
controller: customInputController,
|
||||
focusNode: customInputFocusNode,
|
||||
onContinuePressed: () {
|
||||
navigateToProviders(context);
|
||||
},
|
||||
onChanged: (amount) {
|
||||
if (widget.buySellViewModel.mode == BuySellPageMode.sell) {
|
||||
widget.buySellViewModel.setCryptoAmountFromFiat(amount);
|
||||
} else {
|
||||
widget.buySellViewModel.changeFiatAmount(amount: amount);
|
||||
}
|
||||
},
|
||||
)
|
||||
: BuySellDefaultAmountSelector(
|
||||
key: const ValueKey(0),
|
||||
defaultAmounts: widget.buySellViewModel.defaultAmounts,
|
||||
fiatCurrency: widget.buySellViewModel.fiatCurrency,
|
||||
currentAmount: widget.buySellViewModel.fiatAmount,
|
||||
hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies,
|
||||
onCurrencySelectorPressed: () => selectCryptoCurrency(context),
|
||||
onFiatSelectorPressed: () => selectFiatCurrency(context),
|
||||
cryptoCurrency: widget.buySellViewModel.cryptoCurrency,
|
||||
isLoading: _isLoadingPaymentMethods,
|
||||
mode: widget.buySellViewModel.mode,
|
||||
onSelected: (amount) async {
|
||||
if (amount == null) {
|
||||
// this resets the rate and prevents showing 0 usd = 0.something btc
|
||||
await widget.buySellViewModel.changeFiatAmount(amount: "");
|
||||
setState(() {
|
||||
_customAmountMode = true;
|
||||
});
|
||||
customInputFocusNode.requestFocus();
|
||||
} else {
|
||||
if (widget.buySellViewModel.mode == BuySellPageMode.sell) {
|
||||
await widget.buySellViewModel.setCryptoAmountFromFiat(amount);
|
||||
} else {
|
||||
await widget.buySellViewModel.changeFiatAmount(amount: amount);
|
||||
}
|
||||
if (context.mounted) {
|
||||
await navigateToProviders(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void selectCryptoCurrency(BuildContext context) => CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
args: CurrencyPickerArgs(
|
||||
items: widget.buySellViewModel.activeWalletCurrencies.toList(),
|
||||
onSelected: (item) => widget.buySellViewModel.changeCryptoCurrency(currency: item),
|
||||
symbolResolver: widget.buySellViewModel.amountParsingProxy.getCryptoSymbol,
|
||||
),
|
||||
);
|
||||
|
||||
void selectFiatCurrency(BuildContext context) => FiatCurrencyPickerSheet.show(
|
||||
context: context,
|
||||
selected: widget.buySellViewModel.fiatCurrency,
|
||||
onSelected: (item) => widget.buySellViewModel.changeFiatCurrency(currency: item),
|
||||
);
|
||||
|
||||
String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy
|
||||
? S.current.buy
|
||||
: S.current.sell +
|
||||
((widget.buySellViewModel.cryptoCurrencies.length == 1)
|
||||
? " ${widget.buySellViewModel.cryptoCurrencies.first.fullName}"
|
||||
: "");
|
||||
|
||||
Future<void> navigateToProviders(BuildContext context) async {
|
||||
if (_isLoadingPaymentMethods) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
_isLoadingPaymentMethods = true;
|
||||
});
|
||||
|
||||
await asyncWhen(
|
||||
(_) => [PaymentMethodLoaded, PaymentMethodFailed]
|
||||
.contains(widget.buySellViewModel.paymentMethodState.runtimeType),
|
||||
);
|
||||
|
||||
if (widget.buySellViewModel.paymentMethodState is PaymentMethodFailed) {
|
||||
if (context.mounted) {
|
||||
await showPopUp(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).failed_to_load_payment_methods,
|
||||
alertContent: S.of(context).please_try_again_later,
|
||||
buttonText: "OK",
|
||||
buttonAction: Navigator.of(context).pop,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods
|
||||
.firstWhere((item) => item.paymentMethodType == PaymentType.all);
|
||||
|
||||
// unawaited because BuySellProviderPage has a nice little "loading rates..." ui thingie
|
||||
unawaited(widget.buySellViewModel.calculateBestRate());
|
||||
|
||||
final page = BuySellProviderPage(buySellViewModel: widget.buySellViewModel);
|
||||
if (context.mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context).push(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(color: Colors.transparent, child: page),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoadingPaymentMethods = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BuySellCustomAmountInput extends StatelessWidget {
|
||||
const BuySellCustomAmountInput({
|
||||
required this.fiatCurrency,
|
||||
required this.cryptoCurrency,
|
||||
required this.cryptoAmount,
|
||||
required this.controller,
|
||||
required this.onContinuePressed,
|
||||
required this.isLoading,
|
||||
required this.onChanged,
|
||||
required this.focusNode,
|
||||
required this.hasCurrencySelector,
|
||||
required this.onCurrencySelectorPressed,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final FiatCurrency fiatCurrency;
|
||||
final CryptoCurrency cryptoCurrency;
|
||||
final String cryptoAmount;
|
||||
final TextEditingController controller;
|
||||
final FocusNode focusNode;
|
||||
final bool isLoading;
|
||||
final bool hasCurrencySelector;
|
||||
final VoidCallback onCurrencySelectorPressed;
|
||||
final VoidCallback onContinuePressed;
|
||||
final Function(String) onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox.shrink(),
|
||||
Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
FloatingAmountInput(
|
||||
currency: fiatCurrency,
|
||||
focusNode: focusNode,
|
||||
controller: controller,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Opacity(
|
||||
opacity: cryptoAmount.isEmpty ? 0 : 1,
|
||||
child: Text(
|
||||
"≈ ${cryptoAmount} ${cryptoCurrency.symbol}",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: NewPrimaryButton(
|
||||
disabled: controller.text.isEmpty,
|
||||
onPressed: onContinuePressed,
|
||||
isLoading: isLoading,
|
||||
text: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class BuySellDefaultAmountSelector extends StatelessWidget {
|
||||
const BuySellDefaultAmountSelector({
|
||||
required this.onFiatSelectorPressed,
|
||||
required this.defaultAmounts,
|
||||
required this.fiatCurrency,
|
||||
required this.mode,
|
||||
required this.onSelected,
|
||||
required this.isLoading,
|
||||
required this.hasCurrencySelector,
|
||||
required this.onCurrencySelectorPressed,
|
||||
required this.cryptoCurrency,
|
||||
super.key,
|
||||
this.currentAmount,
|
||||
});
|
||||
|
||||
final List<String> defaultAmounts;
|
||||
final String? currentAmount;
|
||||
final bool isLoading;
|
||||
final bool hasCurrencySelector;
|
||||
final VoidCallback onCurrencySelectorPressed;
|
||||
final VoidCallback onFiatSelectorPressed;
|
||||
final CryptoCurrency cryptoCurrency;
|
||||
final FiatCurrency fiatCurrency;
|
||||
final BuySellPageMode mode;
|
||||
final Function(String?) onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hasCurrencySelector)
|
||||
BuySellCurrencyPickerPill(
|
||||
title: mode == BuySellPageMode.sell ? S.of(context).sell : S.of(context).buy,
|
||||
curr: cryptoCurrency,
|
||||
onTap: onCurrencySelectorPressed,
|
||||
),
|
||||
BuySellCurrencyPickerPill(
|
||||
title: mode == BuySellPageMode.sell
|
||||
? S.of(context).receive
|
||||
: S.of(context).purchase_with,
|
||||
curr: fiatCurrency,
|
||||
onTap: onFiatSelectorPressed,
|
||||
),
|
||||
Text(
|
||||
mode == BuySellPageMode.sell
|
||||
? S.of(context).choose_amount_to_sell
|
||||
: S.of(context).choose_amount_to_buy,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// +1 for "custom" option
|
||||
itemCount: defaultAmounts.length + 1,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 16,
|
||||
mainAxisExtent: 105,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final String? item = index == defaultAmounts.length ? null : defaultAmounts[index];
|
||||
|
||||
return BuySellAmountPill(
|
||||
isLoading: isLoading && item == currentAmount,
|
||||
amount: item == null ? null : Money.parse(item, fiatCurrency),
|
||||
onTap: () => onSelected(item),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class BuySellAmountPill extends StatelessWidget {
|
||||
const BuySellAmountPill({required this.onTap, required this.isLoading, super.key, this.amount});
|
||||
|
||||
final Money? amount;
|
||||
final VoidCallback onTap;
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(9999999999),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(9999999999),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(9999999999),
|
||||
onTap: onTap,
|
||||
child: isLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (amount != null)
|
||||
MoneyText(
|
||||
amount!,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
showSymbol: false,
|
||||
),
|
||||
Text(
|
||||
amount?.currency.symbol ?? S.of(context).custom,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: amount == null
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (amount == null)
|
||||
Text(
|
||||
S.of(context).enter_amount,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class BuySellCurrencyPickerPill extends StatelessWidget {
|
||||
const BuySellCurrencyPickerPill(
|
||||
{required this.title, required this.curr, required this.onTap, super.key});
|
||||
|
||||
final String title;
|
||||
final Currency curr;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
height: 62,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (curr is CryptoCurrency)
|
||||
TokenChainDisplay(size: 30, asset: curr as CryptoCurrency)
|
||||
else if (curr is FiatCurrency)
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Text(
|
||||
(curr as FiatCurrency).emoji,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
)),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(
|
||||
curr.fullName ?? curr.symbol,
|
||||
),
|
||||
CurrencySymbolText(
|
||||
curr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg",
|
||||
width: 8,
|
||||
height: 8,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import "package:hash_wallet/buy/payment_method.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class BuySellConfirmationPage extends StatelessWidget {
|
||||
const BuySellConfirmationPage({required this.buySellViewModel, super.key});
|
||||
|
||||
final BuySellViewModel buySellViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: _pageTitle,
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) => Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
Column(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
"${buySellViewModel.fiatAmount} ${buySellViewModel.fiatCurrency}",
|
||||
style: const TextStyle(fontSize: 32),
|
||||
),
|
||||
if (buySellViewModel.amountForQuote(buySellViewModel.selectedQuote!) !=
|
||||
null)
|
||||
Text(
|
||||
"≈ ${buySellViewModel.amountForQuote(buySellViewModel.selectedQuote!)?.toStringWithSymbol(fractionalDigits: 8)}",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: NewListSections(
|
||||
sections: {
|
||||
"": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "provider",
|
||||
label: S.of(context).provider,
|
||||
trailingWidget: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: Theme.of(context).brightness == Brightness.light
|
||||
? buySellViewModel.selectedQuote!.lightIconPath
|
||||
: buySellViewModel.selectedQuote!.darkIconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
Text(
|
||||
buySellViewModel.selectedQuote!.rampName ??
|
||||
buySellViewModel.selectedQuote!.provider.title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (buySellViewModel.selectedQuote!.paymentType.title?.isNotEmpty
|
||||
?? buySellViewModel.selectedQuote!.customPaymentMethodType?.isNotEmpty
|
||||
?? false)
|
||||
ListItemRegularRow(
|
||||
showArrow: false,
|
||||
keyValue: "payment method",
|
||||
label: S.of(context).payment_method,
|
||||
trailingText: buySellViewModel.selectedQuote!.paymentType.title
|
||||
?? buySellViewModel.selectedQuote!.customPaymentMethodType,
|
||||
),
|
||||
ListItemRegularRow(
|
||||
showArrow: false,
|
||||
keyValue: "rate",
|
||||
label: S.of(context).rate,
|
||||
trailingText: buySellViewModel.selectedQuote!.topLeftSubTitle,
|
||||
),
|
||||
if (_limitsText != null)
|
||||
ListItemRegularRow(
|
||||
showArrow: false,
|
||||
keyValue: "limits",
|
||||
label: "Limits",
|
||||
trailingText: _limitsText,
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: NewPrimaryButton(
|
||||
onPressed: () => confirm(context),
|
||||
text: S.of(context).proceed,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
String? get _limitsText {
|
||||
final limits = buySellViewModel.selectedQuote?.limits;
|
||||
final max = limits?.max;
|
||||
if (limits == null || max == null || max == double.infinity) return null;
|
||||
|
||||
return "${limits.min ?? 0} - $max";
|
||||
}
|
||||
|
||||
String get _pageTitle =>
|
||||
"${buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell} ${buySellViewModel.cryptoCurrency.fullName ?? ""}";
|
||||
|
||||
void confirm(BuildContext context) {
|
||||
final page = BuySellRedirectingPage(buySellViewModel: buySellViewModel);
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
Navigator.of(context, rootNavigator: true).push(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
child: page,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:hash_wallet/buy/payment_method.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class BuySellPaymentMethodPage extends StatelessWidget {
|
||||
const BuySellPaymentMethodPage({required this.buySellViewModel, super.key});
|
||||
|
||||
final BuySellViewModel buySellViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).payment_method,
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Observer(
|
||||
builder: (_) => NewListSections(
|
||||
sections: {
|
||||
"": buySellViewModel.paymentMethods
|
||||
.map(
|
||||
(item) => ListItemRegularRow(
|
||||
keyValue: item.title,
|
||||
label: item.title,
|
||||
showArrow: false,
|
||||
iconPath: Theme.of(context).brightness == Brightness.light
|
||||
? item.lightIconPath
|
||||
: item.darkIconPath,
|
||||
iconColor: item.paymentMethodType.isMonochromeIcon
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: null,
|
||||
trailingWidget: buySellViewModel.selectedPaymentMethod == item
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
buySellViewModel.changeOption(item);
|
||||
unawaited(buySellViewModel.calculateBestRate());
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import "package:hash_wallet/buy/buy_quote.dart";
|
||||
import "package:hash_wallet/buy/sell_buy_states.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_dropdown.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart";
|
||||
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_payment_method_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class BuySellProviderPage extends StatefulWidget {
|
||||
const BuySellProviderPage({required this.buySellViewModel, super.key});
|
||||
|
||||
final BuySellViewModel buySellViewModel;
|
||||
|
||||
@override
|
||||
State<BuySellProviderPage> createState() => _BuySellProviderPageState();
|
||||
}
|
||||
|
||||
class _BuySellProviderPageState extends State<BuySellProviderPage> {
|
||||
bool _allProvidersExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: _pageTitle,
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
if (widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 24,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_outlined, size: 48),
|
||||
Column(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).could_not_load_quotes,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
(widget.buySellViewModel.buySellQuotState as BuySellQuotFailed)
|
||||
.errorMessage ??
|
||||
S.of(context).please_try_again_later,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.buySellViewModel.buySellQuotState is BuySellQuotLoading) {
|
||||
return Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
const CupertinoActivityIndicator(),
|
||||
Text(
|
||||
S.of(context).loading_rates,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: NewListSections(
|
||||
showHeader: true,
|
||||
sections: {
|
||||
"": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "payment method",
|
||||
label: S.of(context).payment_method,
|
||||
showArrow: true,
|
||||
onTap: () {
|
||||
final page = BuySellPaymentMethodPage(
|
||||
buySellViewModel: widget.buySellViewModel,
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
color: Colors.transparent,
|
||||
child: page,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
trailingText: widget.buySellViewModel.selectedPaymentMethod?.title,
|
||||
),
|
||||
],
|
||||
S.of(context).available_providers: [
|
||||
...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem),
|
||||
if (widget.buySellViewModel.sortedQuotes.isNotEmpty)
|
||||
ListItemDropdown(
|
||||
keyValue: "more options",
|
||||
label: S.of(context).more_options,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_allProvidersExpanded = !_allProvidersExpanded;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_allProvidersExpanded)
|
||||
...widget.buySellViewModel.sortedQuotes.map(quoteListItem),
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
String get _pageTitle =>
|
||||
"${widget.buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell} ${widget.buySellViewModel.cryptoCurrency.fullName ?? ""}";
|
||||
|
||||
ListItem quoteListItem(Quote quote) {
|
||||
final fiatAmount = widget.buySellViewModel.fiatAmountForQuote(quote);
|
||||
return ListItemRegularRow(
|
||||
keyValue: quote.provider.title,
|
||||
label: quote.rampName ?? quote.provider.title,
|
||||
secondaryLabel: quote.rampName != null ? quote.provider.title : null,
|
||||
subtitle: quote.badges.isEmpty ? null : quote.badges.join(" - "),
|
||||
subtitleColor: Theme.of(context).colorScheme.primary,
|
||||
iconPath: Theme.of(context).brightness == Brightness.light
|
||||
? quote.lightIconPath
|
||||
: quote.darkIconPath,
|
||||
onTap: () {
|
||||
widget.buySellViewModel.changeOption(quote);
|
||||
navigateToConfirmation(context);
|
||||
},
|
||||
leadingIconSize: 32,
|
||||
trailingWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
MoneyText.optional(widget.buySellViewModel.amountForQuote(quote)),
|
||||
if (fiatAmount != null)
|
||||
Text(
|
||||
"= ${fiatAmount.toStringWithSymbol(fractionalDigits: 2, trimZeros: false)}",
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void navigateToConfirmation(BuildContext context) {
|
||||
final page = BuySellConfirmationPage(buySellViewModel: widget.buySellViewModel);
|
||||
Navigator.of(context).push(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
color: Colors.transparent,
|
||||
child: page,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import "package:hash_wallet/buy/sell_buy_states.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class BuySellRedirectingPage extends StatefulWidget {
|
||||
const BuySellRedirectingPage({required this.buySellViewModel, super.key});
|
||||
|
||||
final BuySellViewModel buySellViewModel;
|
||||
|
||||
@override
|
||||
State<BuySellRedirectingPage> createState() => _BuySellRedirectingPageState();
|
||||
}
|
||||
|
||||
class _BuySellRedirectingPageState extends State<BuySellRedirectingPage> {
|
||||
bool _hasRedirected = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(const Duration(seconds: 2)).then((_) {
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => widget.buySellViewModel.launchTrade(context));
|
||||
setState(() {
|
||||
_hasRedirected = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
canPop: false,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final showExitButton =
|
||||
_hasRedirected || widget.buySellViewModel.buySellQuotState is BuySellQuotFailed;
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox.shrink(),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 24,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_outlined, size: 48),
|
||||
Column(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).could_not_proceed_with_trade,
|
||||
style:
|
||||
const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
(widget.buySellViewModel.buySellQuotState as BuySellQuotFailed)
|
||||
.errorMessage ??
|
||||
S.of(context).please_try_again_later,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 24,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: Theme.of(context).brightness == Brightness.light
|
||||
? widget.buySellViewModel.selectedQuote!.lightIconPath
|
||||
: widget.buySellViewModel.selectedQuote!.darkIconPath,
|
||||
width: 64,
|
||||
height: 64,
|
||||
),
|
||||
Column(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text(
|
||||
"${S.of(context).connecting_you_to} ${widget.buySellViewModel.selectedQuote!.provider.title}...",
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
"${widget.buySellViewModel.fiatAmount} ${widget.buySellViewModel.fiatCurrency} → ${widget.buySellViewModel.amountForQuote(widget.buySellViewModel.selectedQuote!)?.toStringWithSymbol(fractionalDigits: 8)}",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
showExitButton
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: NewPrimaryButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
text: S.of(context).close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,97 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import "dart:math";
|
||||
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/account_confirmation_content.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:hash_wallet/src/widgets/base_alert_dialog.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:hash_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
class CardCustomizer extends StatefulWidget {
|
||||
const CardCustomizer({super.key, required this.cryptoTitle, required this.cryptoName});
|
||||
const CardCustomizer({
|
||||
required this.cryptoTitle,
|
||||
required this.cryptoName,
|
||||
this.account,
|
||||
this.fiatBalance,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String cryptoTitle;
|
||||
final String cryptoName;
|
||||
final AccountListItem? account;
|
||||
final String? fiatBalance;
|
||||
|
||||
static Future<bool> show({
|
||||
required BuildContext context,
|
||||
required DashboardViewModel dashboardViewModel,
|
||||
required AccountListItem? account,
|
||||
bool lightningMode = false,
|
||||
bool canHide = false,
|
||||
bool asPage = false,
|
||||
}) async {
|
||||
final bloc = getIt.get<CardCustomizerBloc>(
|
||||
param1: CardCustomizerBlocParams(
|
||||
lightningMode: lightningMode,
|
||||
amountDisplayMode: dashboardViewModel.settingsStore.displayAmountsInSatoshi,
|
||||
canHide: canHide,
|
||||
),
|
||||
);
|
||||
if (bloc.state is CardCustomizerNotLoaded) {
|
||||
await bloc.stream.firstWhere((state) => state is! CardCustomizerNotLoaded);
|
||||
}
|
||||
if (!context.mounted) {
|
||||
await bloc.close();
|
||||
return false;
|
||||
}
|
||||
final balanceViewModel = dashboardViewModel.balanceViewModel;
|
||||
final customizer = BlocProvider.value(
|
||||
value: bloc,
|
||||
child: Material(
|
||||
child: CardCustomizer(
|
||||
cryptoTitle: dashboardViewModel.wallet.currency.fullName ??
|
||||
dashboardViewModel.wallet.currency.name,
|
||||
cryptoName: dashboardViewModel.wallet.currency.name,
|
||||
account: account,
|
||||
fiatBalance: account == null ? null : balanceViewModel.accountFiatBalance(account),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final archived = await (asPage
|
||||
? Navigator.of(context).push<bool>(CupertinoPageRoute(builder: (_) => customizer))
|
||||
: CupertinoScaffold.showCupertinoModalBottomSheet<bool>(
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
context: context,
|
||||
builder: (context) => ModalNavigator(
|
||||
parentContext: context,
|
||||
heightMode: ModalHeightModes.fullScreen,
|
||||
rootPage: customizer,
|
||||
),
|
||||
));
|
||||
|
||||
// Save edits before AccountHidden writes the hidden state.
|
||||
bloc.add(DesignSaved());
|
||||
await bloc.stream.firstWhere((state) => state is CardCustomizerSaved);
|
||||
if (archived == true) {
|
||||
bloc.add(AccountHidden());
|
||||
await bloc.stream.firstWhere((state) => state is CardCustomizerSaved);
|
||||
}
|
||||
await bloc.close();
|
||||
return archived == true;
|
||||
}
|
||||
|
||||
@override
|
||||
State<CardCustomizer> createState() => _CardCustomizerState();
|
||||
@@ -20,9 +99,10 @@ class CardCustomizer extends StatefulWidget {
|
||||
|
||||
class _CardCustomizerState extends State<CardCustomizer> {
|
||||
final accountNameController = TextEditingController();
|
||||
bool editEnabled = false;
|
||||
late final CardCustomizerBloc bloc;
|
||||
|
||||
bool get _isAccount => widget.account != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -30,78 +110,132 @@ class _CardCustomizerState extends State<CardCustomizer> {
|
||||
bloc = context.read<CardCustomizerBloc>();
|
||||
|
||||
if (bloc.state is! CardCustomizerNotLoaded) {
|
||||
editEnabled = bloc.state.accountName.isNotEmpty;
|
||||
accountNameController.text = bloc.state.accountName;
|
||||
} else {
|
||||
late final StreamSubscription sub;
|
||||
sub = bloc.stream.listen((state) {
|
||||
if (state is! CardCustomizerNotLoaded) {
|
||||
setState(() {
|
||||
editEnabled = state.accountName.isNotEmpty;
|
||||
});
|
||||
sub.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool _showIconStylePanel(CardCustomizerState state) {
|
||||
final design = state.availableDesigns[state.selectedDesignIndex];
|
||||
return design.backgroundType == CardDesignBackgroundTypes.svgIcon &&
|
||||
state.availableIconPaths.isNotEmpty;
|
||||
}
|
||||
|
||||
CardDesign _cardStylePreviewDesign(CardCustomizerState state, int index) {
|
||||
var design = state.availableDesigns[index].withGradient(state.selectedColor);
|
||||
if (design.backgroundType == CardDesignBackgroundTypes.svgIcon &&
|
||||
state.availableIconPaths.isNotEmpty) {
|
||||
design = design.withIcon(state.availableIconPaths[state.selectedIconIndex]);
|
||||
}
|
||||
return design;
|
||||
}
|
||||
|
||||
Future<void> _requestArchive() async {
|
||||
final account = AccountListItem(
|
||||
id: widget.account!.id,
|
||||
label: bloc.state.accountName,
|
||||
balance: widget.account!.balance,
|
||||
);
|
||||
final isFunded = account.isFunded;
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => AlertWithTwoActions(
|
||||
alertTitle: isFunded
|
||||
? S.of(dialogContext).archive_account_confirmation_title
|
||||
: S.of(dialogContext).archive_account_title,
|
||||
alertContent: "",
|
||||
alertContentTextWidget: ArchiveConfirmationContent(
|
||||
account: account,
|
||||
fiatBalance: widget.fiatBalance,
|
||||
isFunded: isFunded,
|
||||
),
|
||||
leftButtonText: S.of(dialogContext).cancel,
|
||||
rightButtonText: S.of(dialogContext).continue_text,
|
||||
leftAlertButtonStyle: AlertButtonStyle.primary(dialogContext),
|
||||
rightAlertButtonStyle: AlertButtonStyle.secondary(dialogContext),
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
|
||||
actionRightButton: () => Navigator.of(dialogContext).pop(true),
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<CardCustomizerBloc, CardCustomizerState>(
|
||||
listenWhen: (previous, current) => previous.accountName != current.accountName,
|
||||
listener: (context, state) {
|
||||
if (accountNameController.text != state.accountName) {
|
||||
accountNameController.text = state.accountName;
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
|
||||
builder: (context, state) {
|
||||
if (state is CardCustomizerNotLoaded) return SizedBox.shrink();
|
||||
return PopScope(
|
||||
child: SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
spacing: 25.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
// trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
// onTrailingPressed: () {},
|
||||
),
|
||||
if (editEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).account_name),
|
||||
TextField(
|
||||
maxLength: 32,
|
||||
decoration: InputDecoration(counterText: ""),
|
||||
onChanged: (value) {
|
||||
context.read<CardCustomizerBloc>().add(AccountNameChanged(value));
|
||||
},
|
||||
controller: accountNameController,
|
||||
)
|
||||
],
|
||||
),
|
||||
void dispose() {
|
||||
accountNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocListener<CardCustomizerBloc, CardCustomizerState>(
|
||||
listenWhen: (previous, current) => previous.accountName != current.accountName,
|
||||
listener: (context, state) {
|
||||
if (accountNameController.text != state.accountName) {
|
||||
accountNameController.text = state.accountName;
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
|
||||
builder: (context, state) {
|
||||
if (state is CardCustomizerNotLoaded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return PopScope(
|
||||
child: SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
spacing: 25,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: _isAccount ? S.of(context).edit_account : S.of(context).edit_card,
|
||||
subtitle: _isAccount ? "#${widget.account!.id + 1}" : null,
|
||||
leadingIcon:
|
||||
Icon(_isAccount ? Icons.arrow_back_ios_new : Icons.close, size: 18),
|
||||
leadingSemanticLabel:
|
||||
_isAccount ? S.of(context).seed_alert_back : S.of(context).close,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
BalanceCard(
|
||||
width: min(MediaQuery.of(context).size.width * 0.87, 768),
|
||||
selected: true,
|
||||
designSwitchDuration: Duration(milliseconds: 300),
|
||||
accountName: editEnabled ? state.accountName : "",
|
||||
balance: "0.00",
|
||||
assetName: state.displaySats ? "sats" : widget.cryptoName,
|
||||
capitalizeAssetName: !state.displaySats,
|
||||
design: state.selectedDesign,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
BalanceCard(
|
||||
width: min(MediaQuery.of(context).size.width * 0.87, 768),
|
||||
selected: true,
|
||||
designSwitchDuration: const Duration(milliseconds: 300),
|
||||
accountIndex: _isAccount ? widget.account!.id + 1 : null,
|
||||
accountName: !_isAccount
|
||||
? ""
|
||||
: state.accountName.trim().isEmpty
|
||||
? S.of(context).unnamed_account
|
||||
: state.accountName,
|
||||
accountBalance: widget.account?.balance,
|
||||
balance: _isAccount ? "" : "0.00",
|
||||
fiatBalance: widget.fiatBalance ?? "",
|
||||
assetName: state.displaySats ? "sats" : widget.cryptoName,
|
||||
capitalizeAssetName: !state.displaySats,
|
||||
design: state.selectedDesign,
|
||||
),
|
||||
if (_isAccount)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).account_name),
|
||||
TextField(
|
||||
maxLength: 32,
|
||||
decoration: const InputDecoration(counterText: ""),
|
||||
onChanged: (value) {
|
||||
context.read<CardCustomizerBloc>().add(AccountNameChanged(value));
|
||||
},
|
||||
controller: accountNameController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
@@ -111,146 +245,292 @@ class _CardCustomizerState extends State<CardCustomizer> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).card_style),
|
||||
Container(
|
||||
height: 63,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.availableDesigns.length,
|
||||
separatorBuilder: (context, index) {
|
||||
return SizedBox(width: 8.0);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(CardDesignSelected(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
color:
|
||||
((index == state.selectedDesignIndex)
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).card_style),
|
||||
SizedBox(
|
||||
height: 63,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.availableDesigns.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(CardDesignSelected(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
side: BorderSide(
|
||||
color: ((index == state.selectedDesignIndex)
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Colors.transparent),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadiusGeometry.circular(12),
|
||||
),
|
||||
),
|
||||
child: AnimatedScale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
scale: index == state.selectedDesignIndex ? 0.94 : 1,
|
||||
child: BalanceCard(
|
||||
width: 96,
|
||||
borderRadius: 10,
|
||||
selected: false,
|
||||
designSwitchDuration:
|
||||
const Duration(milliseconds: 300),
|
||||
design: _cardStylePreviewDesign(state, index),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: _showIconStylePanel(state)
|
||||
? Column(
|
||||
key: const ValueKey("icon_style_panel"),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).icon_style),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.availableIconPaths.length,
|
||||
separatorBuilder: (context, _) =>
|
||||
const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final icon = state.availableIconPaths[index];
|
||||
final isSelected =
|
||||
index == state.selectedIconIndex;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(IconStyleSelected(index));
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration:
|
||||
const Duration(milliseconds: 200),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.withAlpha(64)
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
: Colors.transparent),
|
||||
width: 1),
|
||||
borderRadius:
|
||||
BorderRadiusGeometry.circular(12),
|
||||
),
|
||||
),
|
||||
child: AnimatedScale(
|
||||
duration: Duration(milliseconds: 200),
|
||||
scale: index == state.selectedDesignIndex
|
||||
? 0.94
|
||||
: 1,
|
||||
child: BalanceCard(
|
||||
width: 96,
|
||||
borderRadius: 10,
|
||||
selected: false,
|
||||
designSwitchDuration:
|
||||
Duration(milliseconds: 300),
|
||||
design: state.availableDesigns[index],
|
||||
gradient: state.selectedDesign.gradient,
|
||||
),
|
||||
: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: HashImageWidget(
|
||||
imageUrl: icon.path,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
)),
|
||||
SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey("icon_style_hidden")),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).color),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
spacing: 4, // space between items in a row
|
||||
runSpacing: 8,
|
||||
children: List.generate(state.availableColors.length,
|
||||
(index) {
|
||||
return Material(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
spacing: 4,
|
||||
runSpacing: 8,
|
||||
children: List.generate(
|
||||
state.availableColors.length,
|
||||
(index) => Material(
|
||||
borderRadius: BorderRadius.circular(999999999),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999999999),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999999999),
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(ColorSelected(index));
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 200),
|
||||
opacity: index == state.selectedColorIndex
|
||||
? 1
|
||||
: 0,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(99999999),
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface))),
|
||||
),
|
||||
AnimatedScale(
|
||||
duration: Duration(milliseconds: 200),
|
||||
scale: index == state.selectedColorIndex
|
||||
? 0.8
|
||||
: 1,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(99999999),
|
||||
gradient:
|
||||
state.availableColors[index]),
|
||||
onTap: () {
|
||||
context
|
||||
.read<CardCustomizerBloc>()
|
||||
.add(ColorSelected(index));
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity:
|
||||
index == state.selectedColorIndex ? 1 : 0,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(99999999),
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedScale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
scale: index == state.selectedColorIndex
|
||||
? 0.8
|
||||
: 1,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(99999999),
|
||||
gradient: state.availableColors[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (bloc.canHide)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 0, 18, 24),
|
||||
child: ListItemStyleWrapper(
|
||||
isFirstInSection: true,
|
||||
isLastInSection: true,
|
||||
onTap: _requestArchive,
|
||||
builder: (context, textStyle, labelStyle) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 40),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2.5),
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/archived.svg",
|
||||
width: 19,
|
||||
height: 19,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).archive_account,
|
||||
style: textStyle.copyWith(
|
||||
height: 18 / 14,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
S.of(context).archive_account_reversible,
|
||||
style: labelStyle.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 15 / 12,
|
||||
letterSpacing: -0.06,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox.square(
|
||||
dimension: 16,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_right.svg",
|
||||
width: 7,
|
||||
height: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/page_open_listener.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/asset_grid.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/asset_grid_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/chart_header.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
class ChartsPage extends StatelessWidget implements PageOpenListener {
|
||||
const ChartsPage({required this.chartsBloc, super.key});
|
||||
|
||||
final ChartsBloc chartsBloc;
|
||||
|
||||
@override
|
||||
void onPageOpen() {
|
||||
chartsBloc.add(Init());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
|
||||
bloc: chartsBloc,
|
||||
builder: (context, state) {
|
||||
if (state is ChartsInitial) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 24,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
width: 36,
|
||||
height: 36,
|
||||
imageUrl: "assets/new-ui/navbar/charts.svg",
|
||||
),
|
||||
CupertinoActivityIndicator(),
|
||||
if (kDebugMode)
|
||||
Text(
|
||||
"devs: if this doesn't go away in a few seconds, either your device is painfully slow or the db is messed up",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is ChartsDisabled) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
width: 36,
|
||||
height: 36,
|
||||
imageUrl: "assets/new-ui/navbar/charts.svg",
|
||||
),
|
||||
Text(
|
||||
S.of(context).charts_unavailable,
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(S.of(context).charts_unavailable_desc),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
sliver: CupertinoSliverRefreshControl(
|
||||
refreshTriggerPullDistance: 160,
|
||||
refreshIndicatorExtent: 90,
|
||||
onRefresh: () async => chartsBloc.add(PageRefreshed()),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
if (state is ChartsStateWithData)
|
||||
ChartHeader(
|
||||
bloc: chartsBloc,
|
||||
currency: state.pinnedCurrency,
|
||||
chartHeight: 100,
|
||||
chartPadding: 22,
|
||||
centered: false,
|
||||
favorite: true,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [ChartsAssetGridHeader(bloc: chartsBloc), ChartsAssetGrid(bloc: chartsBloc)],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 96,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -4,15 +4,25 @@ import 'package:hash_wallet/new-ui/widgets/modal_header.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import "package:hash_wallet/view_model/unspent_coins/unspent_coins_item.dart";
|
||||
|
||||
class CoinControlPageArgs {
|
||||
final bool canEdit;
|
||||
final UnspentCoinType? coinTypeToSpendFrom;
|
||||
|
||||
const CoinControlPageArgs({required this.canEdit, this.coinTypeToSpendFrom});
|
||||
}
|
||||
|
||||
class NewCoinControlPage extends StatefulWidget {
|
||||
const NewCoinControlPage({super.key, required this.unspentCoinsListViewModel});
|
||||
const NewCoinControlPage(
|
||||
{super.key, required this.unspentCoinsListViewModel, required this.canEdit});
|
||||
|
||||
final UnspentCoinsListViewModel unspentCoinsListViewModel;
|
||||
final bool canEdit;
|
||||
|
||||
@override
|
||||
State<NewCoinControlPage> createState() => _NewCoinControlPageState();
|
||||
@@ -94,9 +104,12 @@ class _NewCoinControlPageState extends State<NewCoinControlPage> {
|
||||
child: ModalHeader(
|
||||
iconPath: "assets/new-ui/settings_row_icons/coin-control.svg",
|
||||
title: "Coin Control",
|
||||
message: S.of(context).coin_control_desc),
|
||||
message: widget.canEdit
|
||||
? S.of(context).coin_control_desc
|
||||
: S.of(context).coin_control_desc_no_edit),
|
||||
),
|
||||
if (widget.unspentCoinsListViewModel.items.isNotEmpty)
|
||||
if (widget.unspentCoinsListViewModel.items.isNotEmpty &&
|
||||
widget.canEdit)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
@@ -124,6 +137,10 @@ class _NewCoinControlPageState extends State<NewCoinControlPage> {
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
if (widget.unspentCoinsListViewModel.nonFrozenItems.isEmpty &&
|
||||
widget.unspentCoinsListViewModel.frozenItems.isEmpty) ...[
|
||||
@@ -144,6 +161,7 @@ class _NewCoinControlPageState extends State<NewCoinControlPage> {
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: CoinControlListSection(
|
||||
canEdit: widget.canEdit,
|
||||
items: widget.unspentCoinsListViewModel.nonFrozenItems,
|
||||
unspentCoinsListViewModel:
|
||||
widget.unspentCoinsListViewModel),
|
||||
@@ -170,6 +188,7 @@ class _NewCoinControlPageState extends State<NewCoinControlPage> {
|
||||
.onSurfaceVariant),
|
||||
),
|
||||
CoinControlListSection(
|
||||
canEdit: widget.canEdit,
|
||||
items: widget
|
||||
.unspentCoinsListViewModel.frozenItems,
|
||||
unspentCoinsListViewModel:
|
||||
@@ -194,9 +213,13 @@ class _NewCoinControlPageState extends State<NewCoinControlPage> {
|
||||
|
||||
class CoinControlListSection extends StatelessWidget {
|
||||
const CoinControlListSection(
|
||||
{super.key, required this.items, required this.unspentCoinsListViewModel});
|
||||
{super.key,
|
||||
required this.items,
|
||||
required this.unspentCoinsListViewModel,
|
||||
required this.canEdit});
|
||||
|
||||
final List<UnspentCoinsItem> items;
|
||||
final bool canEdit;
|
||||
final UnspentCoinsListViewModel unspentCoinsListViewModel;
|
||||
|
||||
@override
|
||||
@@ -231,6 +254,7 @@ class CoinControlListSection extends StatelessWidget {
|
||||
isLoading: item.isBeingSaved,
|
||||
isFirst: index == 0,
|
||||
isLast: index == items.length - 1,
|
||||
hasCheckbox: canEdit,
|
||||
onCheckBoxTap: item.isFrozen
|
||||
? null
|
||||
: () async {
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/store/settings_store.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
abstract class EducationPage extends StatefulWidget {
|
||||
const EducationPage({required this.settingsStore, super.key});
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
String get educationId;
|
||||
String get title;
|
||||
String get iconPath;
|
||||
String get completionLabel;
|
||||
List<EducationSlide> get slides;
|
||||
String progressLabel(int current, int total);
|
||||
|
||||
bool get isDismissed => settingsStore.isEducationDismissed(educationId);
|
||||
|
||||
Future<void> show(BuildContext context) async {
|
||||
await showCupertinoModalBottomSheet<void>(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => this,
|
||||
);
|
||||
await settingsStore.dismissEducation(educationId);
|
||||
}
|
||||
|
||||
@override
|
||||
State<EducationPage> createState() => _EducationPageState();
|
||||
}
|
||||
|
||||
class _EducationPageState extends State<EducationPage> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
bool _isCompleting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _complete() {
|
||||
if (_isCompleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isCompleting = true;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Future<void> _continue(int pageCount) async {
|
||||
if (_currentPage == pageCount - 1) {
|
||||
_complete();
|
||||
return;
|
||||
}
|
||||
|
||||
await _pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pages = widget.slides;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: widget.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.title, style: Theme.of(context).textTheme.headlineMedium),
|
||||
const Spacer(),
|
||||
ModernButton(
|
||||
size: 36,
|
||||
icon: const Icon(Icons.close),
|
||||
semanticLabel: S.of(context).close,
|
||||
onPressed: _complete,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: (page) => setState(() => _currentPage = page),
|
||||
children: pages,
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
label: widget.progressLabel(_currentPage + 1, pages.length),
|
||||
child: ExcludeSemantics(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
pages.length,
|
||||
(index) => AnimatedContainer(
|
||||
key: ValueKey("education-dot-$index"),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index == _currentPage
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: NewPrimaryButton(
|
||||
onPressed: () => _continue(pages.length),
|
||||
text: _currentPage == pages.length - 1
|
||||
? widget.completionLabel
|
||||
: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EducationSlide extends StatelessWidget {
|
||||
const EducationSlide({
|
||||
required this.children,
|
||||
super.key,
|
||||
this.distributeChildren = false,
|
||||
});
|
||||
|
||||
final List<Widget> children;
|
||||
final bool distributeChildren;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 8),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight - 16),
|
||||
child: distributeChildren
|
||||
? IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0; index < children.length; index++) ...[
|
||||
children[index],
|
||||
if (index < children.length - 1) ...[
|
||||
const SizedBox(height: 24),
|
||||
const Spacer(),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class EducationText extends StatelessWidget {
|
||||
const EducationText({
|
||||
required this.text,
|
||||
super.key,
|
||||
this.highlightedText,
|
||||
this.secondary = false,
|
||||
this.warning = false,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final String? highlightedText;
|
||||
final bool secondary;
|
||||
final bool warning;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: warning
|
||||
? context.customColors.warningOutlineColor
|
||||
: secondary
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
final highlight = highlightedText;
|
||||
final highlightStart = highlight == null ? -1 : text.indexOf(highlight);
|
||||
|
||||
if (highlight == null || highlightStart < 0) {
|
||||
return Text(text, textAlign: TextAlign.center, style: style);
|
||||
}
|
||||
|
||||
final highlightEnd = highlightStart + highlight.length;
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
style: style,
|
||||
children: [
|
||||
TextSpan(text: text.substring(0, highlightStart)),
|
||||
TextSpan(
|
||||
text: text.substring(highlightStart, highlightEnd),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
TextSpan(text: text.substring(highlightEnd)),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:hash_wallet/core/utilities.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/account_confirmation_content.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/modal_page_wrapper.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:hash_wallet/src/widgets/base_alert_dialog.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:hash_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
|
||||
import "package:cw_core/balance_card_layout.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class HiddenAccountsPage extends StatefulWidget {
|
||||
const HiddenAccountsPage({
|
||||
required this.accountListViewModel,
|
||||
required this.dashboardViewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WalletAccountListViewModel accountListViewModel;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
State<HiddenAccountsPage> createState() => _HiddenAccountsPageState();
|
||||
}
|
||||
|
||||
class _HiddenAccountsPageState extends State<HiddenAccountsPage> {
|
||||
final List<AccountListItem> _items = [];
|
||||
final Set<int> _accountsBeingRestored = <int>{};
|
||||
bool _isLoading = true;
|
||||
|
||||
int get _walletInfoId => widget.dashboardViewModel.wallet.walletInfo.internalId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(_loadAccounts());
|
||||
}
|
||||
|
||||
Future<BalanceCardLayout> _layout() async => BalanceCardLayout.resolve(
|
||||
accountIndices: widget.accountListViewModel.accounts.map((account) => account.id).toList(),
|
||||
settings: await BalanceCardStyleSettings.getAll(_walletInfoId),
|
||||
);
|
||||
|
||||
Future<void> _loadAccounts() async {
|
||||
final accounts = widget.accountListViewModel.accounts;
|
||||
final layout = await _layout();
|
||||
final hiddenAccounts = <AccountListItem>[];
|
||||
|
||||
for (final accountIndex in layout.hidden) {
|
||||
final account = accounts.firstWhereOrNull((account) => account.id == accountIndex);
|
||||
if (account != null) {
|
||||
hiddenAccounts.add(account);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_items
|
||||
..clear()
|
||||
..addAll(hiddenAccounts);
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _confirmUnarchival(AccountListItem account) async {
|
||||
final isFunded = account.isFunded;
|
||||
|
||||
final result = await showPopUp<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => AlertWithTwoActions(
|
||||
alertTitle: S.of(dialogContext).unarchive_account_title,
|
||||
alertContent: "",
|
||||
alertContentTextWidget: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AccountSummary(account: account),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
isFunded
|
||||
? S.of(dialogContext).unarchive_account_funds_description
|
||||
: S.of(dialogContext).unarchive_account_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(dialogContext).textTheme.bodyMedium,
|
||||
),
|
||||
if (isFunded) ...[
|
||||
const SizedBox(height: 20),
|
||||
AccountFundsSummary(
|
||||
account: account,
|
||||
fiatBalance: widget.dashboardViewModel.balanceViewModel.accountFiatBalance(account),
|
||||
borderColor: Theme.of(dialogContext).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
leftButtonText: S.of(dialogContext).cancel,
|
||||
rightButtonText: S.of(dialogContext).continue_text,
|
||||
leftAlertButtonStyle: AlertButtonStyle.secondary(dialogContext),
|
||||
rightAlertButtonStyle: AlertButtonStyle.primary(dialogContext),
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
|
||||
actionRightButton: () => Navigator.of(dialogContext).pop(true),
|
||||
),
|
||||
);
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Future<void> _unarchive(AccountListItem account) async {
|
||||
final accountForConfirmation =
|
||||
widget.accountListViewModel.accounts.firstWhereOrNull((item) => item.id == account.id) ??
|
||||
account;
|
||||
final confirmed = await _confirmUnarchival(accountForConfirmation);
|
||||
if (!confirmed || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _accountsBeingRestored.add(account.id));
|
||||
try {
|
||||
final restored = (await _layout()).unhiding(account.id);
|
||||
await BalanceCardStyleSettings.setVisibleOrder(_walletInfoId, restored.orders);
|
||||
final latestAccount =
|
||||
widget.accountListViewModel.accounts.firstWhereOrNull((item) => item.id == account.id) ??
|
||||
account;
|
||||
await widget.accountListViewModel.select(latestAccount);
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
await _loadAccounts();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _accountsBeingRestored.remove(account.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ModalPageWrapper(
|
||||
topBar: ModalTopBar(
|
||||
title: S.of(context).archived_accounts,
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
content: _isLoading
|
||||
? const SizedBox(
|
||||
height: 320,
|
||||
child: Center(child: CupertinoActivityIndicator()),
|
||||
)
|
||||
: _items.isEmpty
|
||||
? const _EmptyArchiveView()
|
||||
: _PopulatedArchiveView(
|
||||
accounts: _items,
|
||||
accountsBeingRestored: _accountsBeingRestored,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
onUnarchive: _unarchive,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _EmptyArchiveView extends StatelessWidget {
|
||||
const _EmptyArchiveView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height - 180,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ArchiveIcon(size: 50, color: theme.colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
S.of(context).no_archived_accounts,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
S.of(context).no_archived_accounts_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PopulatedArchiveView extends StatelessWidget {
|
||||
const _PopulatedArchiveView({
|
||||
required this.accounts,
|
||||
required this.accountsBeingRestored,
|
||||
required this.dashboardViewModel,
|
||||
required this.onUnarchive,
|
||||
});
|
||||
|
||||
final List<AccountListItem> accounts;
|
||||
final Set<int> accountsBeingRestored;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final Future<void> Function(AccountListItem account) onUnarchive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final fundedAccounts = accounts.where((account) => account.isFunded).toList(growable: false);
|
||||
final emptyAccounts =
|
||||
accounts.where((account) => !account.isFunded).toList(growable: false);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ArchiveIcon(size: 50, color: theme.colorScheme.primary),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 36),
|
||||
child: Text(
|
||||
S.of(context).select_account_to_unarchive,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (fundedAccounts.isNotEmpty) ...[
|
||||
_ArchiveSectionHeader(
|
||||
title: S.of(context).funded_accounts,
|
||||
description: S.of(context).funded_accounts_description,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_accountRows(context, fundedAccounts),
|
||||
],
|
||||
if (fundedAccounts.isNotEmpty && emptyAccounts.isNotEmpty) const SizedBox(height: 28),
|
||||
if (emptyAccounts.isNotEmpty) ...[
|
||||
_ArchiveSectionHeader(title: S.of(context).empty_accounts),
|
||||
const SizedBox(height: 12),
|
||||
_accountRows(context, emptyAccounts),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _accountRows(BuildContext context, List<AccountListItem> sectionAccounts) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0; index < sectionAccounts.length; index++) ...[
|
||||
_ArchivedAccountRow(
|
||||
account: sectionAccounts[index],
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
isBusy: accountsBeingRestored.contains(sectionAccounts[index].id),
|
||||
onTap: accountsBeingRestored.contains(sectionAccounts[index].id)
|
||||
? null
|
||||
: () => onUnarchive(sectionAccounts[index]),
|
||||
),
|
||||
if (index < sectionAccounts.length - 1)
|
||||
Divider(
|
||||
height: 1,
|
||||
indent: 48,
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ArchivedAccountRow extends StatelessWidget {
|
||||
const _ArchivedAccountRow({
|
||||
required this.account,
|
||||
required this.dashboardViewModel,
|
||||
required this.isBusy,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool isBusy;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final label = account.label.trim();
|
||||
|
||||
return Material(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/account.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${account.id + 1}. ",
|
||||
style: TextStyle(color: theme.colorScheme.primary),
|
||||
),
|
||||
TextSpan(
|
||||
text: label.isEmpty ? S.of(context).unnamed_account : label,
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_AccountBalanceTrailing(
|
||||
account: account,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
isBusy: isBusy,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArchiveSectionHeader extends StatelessWidget {
|
||||
const _ArchiveSectionHeader({required this.title, this.description});
|
||||
|
||||
final String title;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
if (description != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountBalanceTrailing extends StatelessWidget {
|
||||
const _AccountBalanceTrailing({
|
||||
required this.account,
|
||||
required this.dashboardViewModel,
|
||||
required this.isBusy,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool isBusy;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
if (isBusy) {
|
||||
return const CupertinoActivityIndicator(radius: 10);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (account.isFunded) ...[
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 142),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final fiatBalance = dashboardViewModel.balanceViewModel.accountFiatBalance(account);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
MoneyText(
|
||||
account.balance,
|
||||
fractionalDigits: account.balance.decimals,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
if (fiatBalance != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
fiatBalance,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArchiveIcon extends StatelessWidget {
|
||||
const _ArchiveIcon({required this.size, required this.color});
|
||||
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox.square(
|
||||
dimension: size,
|
||||
child: Center(
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/archived.svg",
|
||||
width: size * 19 / 24,
|
||||
height: size * 19 / 24,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
+306
-216
@@ -1,31 +1,44 @@
|
||||
import 'package:hash_wallet/core/auth_service.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/account_customizer.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/card_customizer.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/settings_page.dart';
|
||||
import 'package:hash_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/cards/cards_view.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/wallet_info.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/nft_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import "dart:async";
|
||||
|
||||
import "package:hash_wallet/bitcoin/bitcoin.dart";
|
||||
import "package:hash_wallet/core/auth_service.dart";
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/main.dart";
|
||||
import "package:hash_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:hash_wallet/new-ui/pages/card_customizer.dart";
|
||||
import "package:hash_wallet/new-ui/pages/account_education_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/accounts_promo.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:cw_core/sync_status.dart";
|
||||
import "package:hash_wallet/new-ui/pages/seed/seed_backup_reminder_page.dart";
|
||||
import "package:hash_wallet/new-ui/pages/send_page.dart";
|
||||
import "package:hash_wallet/new-ui/pages/settings_page.dart";
|
||||
import "package:hash_wallet/new-ui/pages/wallet_accounts_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/cards/cards_view.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/mweb_ad.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/seed_backup_reminder_card.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/utils/payment_request.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/nft_view_model.dart";
|
||||
import "package:cw_core/unspent_coin_type.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class NewHomePage extends StatefulWidget {
|
||||
NewHomePage({super.key, required this.dashboardViewModel, required this.nftViewModel});
|
||||
const NewHomePage({required this.dashboardViewModel, required this.nftViewModel, super.key});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final NFTViewModel nftViewModel;
|
||||
@@ -34,225 +47,302 @@ class NewHomePage extends StatefulWidget {
|
||||
State<NewHomePage> createState() => _NewHomePageState();
|
||||
}
|
||||
|
||||
class _NewHomePageState extends State<NewHomePage> {
|
||||
MoneroAccountListViewModel? accountListViewModel;
|
||||
bool _lightningMode = false;
|
||||
class _NewHomePageState extends State<NewHomePage> with RouteAware {
|
||||
late final ReactionDisposer _walletReaction;
|
||||
late final ReactionDisposer _migrationReaction;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setAccountViewModel();
|
||||
reaction((_) => widget.dashboardViewModel.wallet, (_) {
|
||||
_setAccountViewModel();
|
||||
setState(() {
|
||||
_lightningMode = false;
|
||||
});
|
||||
|
||||
_walletReaction = reaction((_) => widget.dashboardViewModel.wallet, (_) {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_migrationReaction = reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
|
||||
if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true;
|
||||
showMaterialModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ZcashMigrationModal(
|
||||
hasContinue: true,
|
||||
balance: widget.dashboardViewModel.wallet.balance.values.first.unavailable
|
||||
.toStringWithSymbol(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _setAccountViewModel() {
|
||||
accountListViewModel = widget.dashboardViewModel.balanceViewModel.hasAccounts
|
||||
? getIt.get<MoneroAccountListViewModel>()
|
||||
: null;
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
void dispose() {
|
||||
_walletReaction();
|
||||
_migrationReaction();
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() => widget.dashboardViewModel.loadSeedBackupReminder();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
sliver: CupertinoSliverRefreshControl(
|
||||
refreshTriggerPullDistance: 160,
|
||||
refreshIndicatorExtent: 90,
|
||||
onRefresh: () => widget.dashboardViewModel.refreshDashboard(),
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
sliver: CupertinoSliverRefreshControl(
|
||||
refreshTriggerPullDistance: 160,
|
||||
refreshIndicatorExtent: 90,
|
||||
onRefresh: () {
|
||||
unawaited(widget.nftViewModel.getNFTAssetByWallet());
|
||||
|
||||
return widget.dashboardViewModel.refreshDashboard();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: 24.0,
|
||||
children: [
|
||||
TopBar(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
lightningMode: _lightningMode,
|
||||
onLightningSwitchPress: () {
|
||||
setState(() {
|
||||
_lightningMode = !_lightningMode;
|
||||
});
|
||||
},
|
||||
onSettingsButtonPress: () {
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(85),
|
||||
builder: (context) => FractionallySizedBox(
|
||||
child: Material(
|
||||
child: NewSettingsPage(
|
||||
SliverToBoxAdapter(
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final _lightningMode = widget.dashboardViewModel.lightningMode;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: 24.0,
|
||||
children: [
|
||||
TopBar(
|
||||
key: ValueKey(widget.dashboardViewModel.wallet.id),
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
authService: getIt.get<AuthService>(),
|
||||
))),
|
||||
);
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => WalletInfoBar(
|
||||
lightningMode: _lightningMode,
|
||||
hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType,
|
||||
name: widget.dashboardViewModel.wallet.name,
|
||||
hasCustomize: accountListViewModel != null,
|
||||
onCustomizeButtonTap: openAccountCustomizer),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => CardsView(
|
||||
key: ValueKey(widget.dashboardViewModel.wallet.name),
|
||||
onCustomizeTapped: openCardCustomizer,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
accountListViewModel: accountListViewModel,
|
||||
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
|
||||
lightningMode: _lightningMode,
|
||||
onLightningSwitchPress: () {
|
||||
widget.dashboardViewModel.toggleLightningMode();
|
||||
},
|
||||
onSettingsButtonPress: () {
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(85),
|
||||
builder: (context) => FractionallySizedBox(
|
||||
child: Material(
|
||||
child: NewSettingsPage(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
authService: getIt.get<AuthService>(),
|
||||
),
|
||||
),
|
||||
),
|
||||
).then((_) async {
|
||||
widget.dashboardViewModel.accountListViewModel?.reload();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Observer(builder: (_) {
|
||||
return AnimatedSize(
|
||||
duration: Duration(milliseconds: 150),
|
||||
curve: Curves.easeInOutCubic,
|
||||
child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
|
||||
? Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 12,
|
||||
width: double.infinity,
|
||||
),
|
||||
Text(
|
||||
S.of(context).long_press_show_balance,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
],
|
||||
)
|
||||
: SizedBox(width: double.infinity),
|
||||
);
|
||||
}),
|
||||
UnconfirmedBalanceWidget(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
],
|
||||
),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
return Column(
|
||||
children: [
|
||||
CoinActionRow(
|
||||
lightningMode: _lightningMode,
|
||||
showSwap: widget.dashboardViewModel.isEnabledSwapAction,
|
||||
walletType: widget.dashboardViewModel.wallet.type,
|
||||
Column(
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeOut,
|
||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||
// the front (selected) card sits at the bottom of the stack box
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
),
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(_lightningMode),
|
||||
child: CardsView(
|
||||
key: ValueKey(
|
||||
"${widget.dashboardViewModel.wallet.name}_${_lightningMode}_${widget.dashboardViewModel.accountListViewModel?.accounts.length ?? 0}_${widget.dashboardViewModel.cardDesigns.length}"),
|
||||
onCustomizeTapped: openCardCustomizer,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
accountListViewModel:
|
||||
widget.dashboardViewModel.accountListViewModel,
|
||||
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
|
||||
lightningMode: _lightningMode),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => AnimatedSize(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeInOutCubic,
|
||||
child: (widget.dashboardViewModel
|
||||
.shouldShowBalanceHiddenMessage)
|
||||
? Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
width: double.infinity,
|
||||
),
|
||||
Text(
|
||||
S.of(context).long_press_show_balance,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant),
|
||||
)
|
||||
],
|
||||
)
|
||||
: const SizedBox(width: double.infinity),
|
||||
)),
|
||||
UnconfirmedBalanceWidget(
|
||||
dashboardViewModel: widget.dashboardViewModel),
|
||||
SeedBackupReminderCard(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
onTap: openSeedBackupReminder,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (AccountsPromo.supportsWallet(widget.dashboardViewModel.wallet.type) &&
|
||||
widget.dashboardViewModel.accountListViewModel != null &&
|
||||
!_lightningMode)
|
||||
AccountsPromo(
|
||||
settingsStore: widget.dashboardViewModel.settingsStore,
|
||||
walletName: walletTypeToString(widget.dashboardViewModel.wallet.type),
|
||||
onTap: _openAccountsFromPromo,
|
||||
),
|
||||
MwebAd(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
Column(
|
||||
children: [
|
||||
CoinActionRow(
|
||||
lightningMode: _lightningMode,
|
||||
showSwap: widget.dashboardViewModel.isEnabledSwapAction,
|
||||
walletType: widget.dashboardViewModel.wallet.type,
|
||||
),
|
||||
MwebAd(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => AssetsHistorySection(
|
||||
nftViewModel: widget.nftViewModel,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
Observer(
|
||||
builder: (_) => AssetsHistorySection(
|
||||
nftViewModel: widget.nftViewModel,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 80.0),
|
||||
)
|
||||
]),
|
||||
Container(
|
||||
height: (MediaQuery.of(context).padding.top),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: <Color>[
|
||||
Theme.of(context).colorScheme.surface.withAlpha(5),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(25),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(50),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(100),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(150),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(175),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(200),
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: 80.0),
|
||||
)
|
||||
]),
|
||||
Container(
|
||||
height: (MediaQuery.of(context).padding.top),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: <Color>[
|
||||
Theme.of(context).colorScheme.surface.withAlpha(5),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(25),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(50),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(100),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(150),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(175),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(200),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> openCardCustomizer() async {
|
||||
if (!_checkReadyToManage()) return;
|
||||
await CardCustomizer.show(
|
||||
context: context,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
account: !widget.dashboardViewModel.lightningMode &&
|
||||
widget.dashboardViewModel.isMultiAccountsEnabled
|
||||
? widget.dashboardViewModel.accountListViewModel?.selectedAccount
|
||||
: null,
|
||||
lightningMode: widget.dashboardViewModel.lightningMode,
|
||||
);
|
||||
await widget.dashboardViewModel.accountListViewModel?.reload();
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
}
|
||||
|
||||
void openSeedBackupReminder() {
|
||||
Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (_) => SeedBackupReminderPage(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
authService: getIt.get<AuthService>(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void openAccountCustomizer() async {
|
||||
Future<void> openAccountCustomizer() async {
|
||||
if (widget.dashboardViewModel.accountListViewModel == null || !_checkReadyToManage()) return;
|
||||
|
||||
final page = getIt.get<WalletAccountsPage>(param1: widget.dashboardViewModel);
|
||||
|
||||
await CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ModalNavigator(
|
||||
parentContext: context,
|
||||
heightMode: ModalHeightModes.fullScreen,
|
||||
rootPage: Material(
|
||||
child: AccountCustomizer(
|
||||
accountListViewModel: accountListViewModel!,
|
||||
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>(),
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
)),
|
||||
);
|
||||
},
|
||||
builder: (context) => ModalNavigator(
|
||||
parentContext: context,
|
||||
heightMode: ModalHeightModes.fullScreen,
|
||||
rootPage: Material(
|
||||
child: page,
|
||||
),
|
||||
),
|
||||
);
|
||||
widget.dashboardViewModel.loadCardDesigns();
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
}
|
||||
|
||||
void openCardCustomizer() async {
|
||||
final bloc = getIt.get<CardCustomizerBloc>(
|
||||
param1: _lightningMode,
|
||||
param2: widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi);
|
||||
await CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ModalNavigator(
|
||||
parentContext: context,
|
||||
heightMode: ModalHeightModes.fullScreen,
|
||||
rootPage: BlocProvider(
|
||||
create: (context) => bloc,
|
||||
child: Material(
|
||||
child: CardCustomizer(
|
||||
cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
|
||||
widget.dashboardViewModel.wallet.currency.name,
|
||||
cryptoName: widget.dashboardViewModel.wallet.currency.name,
|
||||
)),
|
||||
));
|
||||
},
|
||||
);
|
||||
bloc.add(DesignSaved());
|
||||
await bloc.stream.firstWhere((s) => s is CardCustomizerSaved);
|
||||
widget.dashboardViewModel.loadCardDesigns();
|
||||
Future<void> _openAccountsFromPromo() async {
|
||||
if (!AccountsPromo.supportsWallet(widget.dashboardViewModel.wallet.type) ||
|
||||
widget.dashboardViewModel.accountListViewModel == null || !_checkReadyToManage()) return;
|
||||
|
||||
await AccountEducationPage(
|
||||
settingsStore: widget.dashboardViewModel.settingsStore,
|
||||
).show(context);
|
||||
if (mounted) await openAccountCustomizer();
|
||||
}
|
||||
|
||||
bool _checkReadyToManage() {
|
||||
if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).wallet_is_syncing,
|
||||
alertContent: S.of(context).cannot_manage_accounts_during_sync,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: Navigator.of(context).pop,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class _LightningUsernamePageState extends State<LightningUsernamePage> {
|
||||
ModalTopBar(
|
||||
title: "Lightning ${S.of(context).username}",
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
@@ -111,6 +112,7 @@ class _LightningUsernamePageState extends State<LightningUsernamePage> {
|
||||
),
|
||||
SizedBox(),
|
||||
NewPrimaryButton(
|
||||
key: ValueKey("lightning_username_page_skip_button_key"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
},
|
||||
|
||||
+182
-174
@@ -1,50 +1,52 @@
|
||||
import 'package:hash_wallet/core/utilities.dart';
|
||||
import 'package:hash_wallet/entities/auto_generate_subaddress_status.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_address_type.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_address_widget.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_amount_display.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_info_box.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_label_widget.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_qr_code.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_token_display.dart';
|
||||
import 'package:hash_wallet/utils/share_util.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/receive_option_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:cw_core/receive_page_option.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/anonpay/anonpay_donation_link_info.dart';
|
||||
import 'package:hash_wallet/entities/preferences_key.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/anonpay_receive_page.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import "package:hash_wallet/anonpay/anonpay_donation_link_info.dart";
|
||||
import "package:hash_wallet/bitcoin/bitcoin.dart";
|
||||
import "package:hash_wallet/core/utilities.dart";
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/entities/auto_generate_subaddress_status.dart";
|
||||
import "package:hash_wallet/entities/preferences_key.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_address_type.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_address_widget.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_amount_display.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_info_box.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_label_modal.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_label_widget.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_qr_code.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_token_display.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/utils/share_util.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/receive_option_view_model.dart";
|
||||
import "package:hash_wallet/view_model/wallet_address_list/wallet_address_list_item.dart";
|
||||
import "package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/payment_uris.dart";
|
||||
import "package:cw_core/receive_page_option.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
||||
class NewReceivePage extends StatefulWidget {
|
||||
NewReceivePage(
|
||||
{super.key,
|
||||
required this.addressListViewModel,
|
||||
required this.receiveOptionViewModel,
|
||||
required this.dashboardViewModel,
|
||||
required this.lightningMode,
|
||||
CryptoCurrency? initialCurrency}) {
|
||||
NewReceivePage({
|
||||
required this.addressListViewModel,
|
||||
required this.receiveOptionViewModel,
|
||||
required this.dashboardViewModel,
|
||||
required this.lightningMode,
|
||||
super.key,
|
||||
CryptoCurrency? initialCurrency,
|
||||
}) {
|
||||
if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) {
|
||||
addressListViewModel.setTokenCurrency(initialCurrency);
|
||||
}
|
||||
@@ -69,82 +71,49 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (widget.lightningMode) {
|
||||
widget.addressListViewModel.setTokenCurrency(CryptoCurrency.btcln);
|
||||
widget.receiveOptionViewModel.selectReceiveOption(
|
||||
widget.receiveOptionViewModel.options
|
||||
.firstWhereOrNull((item) => item.value.contains("Lightning")) ??
|
||||
ReceivePageOption.mainnet,
|
||||
);
|
||||
widget.addressListViewModel.setTokenCurrency(CryptoCurrency.btcln);
|
||||
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
|
||||
} else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) {
|
||||
widget.receiveOptionViewModel.selectReceiveOption(
|
||||
widget.receiveOptionViewModel.options
|
||||
.firstWhereOrNull((item) => item.value.contains("Standard")) ??
|
||||
ReceivePageOption.mainnet,
|
||||
);
|
||||
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => widget.addressListViewModel.uri, (newAddress) {
|
||||
_reloadAddressWithLabel(newAddress);
|
||||
});
|
||||
reaction((_) => widget.addressListViewModel.uri, _reloadAddressWithLabel);
|
||||
|
||||
_addressItemWithLabel =
|
||||
widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull((item) {
|
||||
return (item is WalletAddressListItem &&
|
||||
item.address == widget.addressListViewModel.uri.address);
|
||||
}) as WalletAddressListItem?;
|
||||
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
|
||||
(item) =>
|
||||
item is WalletAddressListItem && item.address == widget.addressListViewModel.uri.address,
|
||||
) as WalletAddressListItem?;
|
||||
|
||||
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, (option) {
|
||||
// The infobox depends on the selected address type, so the page has to be
|
||||
// rebuilt when it changes.
|
||||
if (mounted) setState(() {});
|
||||
|
||||
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption,
|
||||
(ReceivePageOption option) {
|
||||
if (widget.dashboardViewModel.type == WalletType.bitcoin &&
|
||||
bitcoin!.isBitcoinReceivePageOption(option)) {
|
||||
widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
|
||||
if (option.value.contains("Lightning")) {
|
||||
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
|
||||
} else {
|
||||
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case ReceivePageOption.anonPayInvoice:
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.anonPayInvoicePage,
|
||||
arguments: [widget.addressListViewModel.address.address, option],
|
||||
);
|
||||
break;
|
||||
case ReceivePageOption.anonPayDonationLink:
|
||||
final sharedPreferences = getIt.get<SharedPreferences>();
|
||||
final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
|
||||
final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink);
|
||||
final donationWalletName =
|
||||
sharedPreferences.getString(PreferencesKey.donationLinkWalletName);
|
||||
|
||||
if (clearnetUrl != null &&
|
||||
onionUrl != null &&
|
||||
widget.addressListViewModel.wallet.name == donationWalletName) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.anonPayReceivePage,
|
||||
arguments: AnonPayReceivePageArgs(
|
||||
invoiceInfo: AnonpayDonationLinkInfo(
|
||||
clearnetUrl: clearnetUrl,
|
||||
onionUrl: onionUrl,
|
||||
address: widget.addressListViewModel.address.address,
|
||||
),
|
||||
qrImage: widget.addressListViewModel.qrImage,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.anonPayInvoicePage,
|
||||
arguments: [widget.addressListViewModel.address.address, option],
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if ([WalletType.bitcoin, WalletType.litecoin]
|
||||
.contains(widget.addressListViewModel.type)) {
|
||||
widget.addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option));
|
||||
}
|
||||
if ([WalletType.bitcoin, WalletType.litecoin]
|
||||
.contains(widget.addressListViewModel.type)) {
|
||||
widget.addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -154,16 +123,19 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1;
|
||||
final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty;
|
||||
final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed;
|
||||
final infobox = ReceiveInfoBox.forWalletType(widget.addressListViewModel.type,
|
||||
supportedCurrencies: widget.addressListViewModel.tokenCurrencies
|
||||
.whereType<CryptoCurrency>()
|
||||
.toList(), onDismissed: () {
|
||||
widget.addressListViewModel.dismissInfobox();
|
||||
setState(() {});
|
||||
},
|
||||
autoGenerateSubaddressStatus: widget.lightningMode
|
||||
? AutoGenerateSubaddressStatus.disabled
|
||||
: widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus);
|
||||
final infobox = ReceiveInfoBox.forWalletType(
|
||||
widget.addressListViewModel.type,
|
||||
supportedCurrencies:
|
||||
widget.addressListViewModel.tokenCurrencies.whereType<CryptoCurrency>().toList(),
|
||||
onDismissed: () {
|
||||
widget.addressListViewModel.dismissInfobox();
|
||||
setState(() {});
|
||||
},
|
||||
autoGenerateSubaddressStatus: widget.lightningMode
|
||||
? AutoGenerateSubaddressStatus.disabled
|
||||
: widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus,
|
||||
addressRotates: _selectedAddressRotates,
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -175,9 +147,7 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(24),
|
||||
),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
@@ -186,30 +156,49 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: _largeQrMode ? "" : S.of(context).receive,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
trailingIcon: _largeQrMode
|
||||
? Icon(Icons.share)
|
||||
: widget.addressListViewModel.hasAddressRotation
|
||||
/* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
|
||||
&&
|
||||
!(widget.receiveOptionViewModel.selectedReceiveOption.description ?? "")
|
||||
.toLowerCase()
|
||||
.contains("mweb")
|
||||
? Icon(Icons.refresh)
|
||||
: null,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
onTrailingPressed: () {
|
||||
if (_largeQrMode) {
|
||||
ShareUtil.share(
|
||||
text: widget.addressListViewModel.uri.toString(),
|
||||
context: context,
|
||||
);
|
||||
} else if (widget.addressListViewModel.hasAddressRotation) {
|
||||
widget.addressListViewModel.rotateAddress();
|
||||
}
|
||||
},
|
||||
trailingWidget: Observer(
|
||||
builder: (_) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _largeQrMode ||
|
||||
widget.addressListViewModel.hasAddressRotation
|
||||
/* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
|
||||
&&
|
||||
!(widget.receiveOptionViewModel.selectedReceiveOption.description ??
|
||||
"")
|
||||
.toLowerCase()
|
||||
.contains("mweb")
|
||||
? ModernButton(
|
||||
key: ValueKey(_largeQrMode),
|
||||
size: 36,
|
||||
icon: _largeQrMode
|
||||
? const Icon(Icons.share)
|
||||
: widget.addressListViewModel.isRotatingAddress
|
||||
? const CupertinoActivityIndicator()
|
||||
: const Icon(Icons.refresh),
|
||||
semanticLabel: _largeQrMode
|
||||
? S.of(context).share_address
|
||||
: S.of(context).rotate_address,
|
||||
onPressed: () {
|
||||
if (_largeQrMode) {
|
||||
ShareUtil.share(
|
||||
text: widget.addressListViewModel.uri.toString(),
|
||||
context: context,
|
||||
);
|
||||
} else {
|
||||
if (widget.addressListViewModel.hasAddressRotation) {
|
||||
widget.addressListViewModel.rotateAddress();
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -242,31 +231,45 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
ReceiveAddressWidget(
|
||||
addressListViewModel: widget.addressListViewModel,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _showLabelModal,
|
||||
child: ReceiveLabelWidget(
|
||||
name: _addressItemWithLabel?.name ?? "",
|
||||
largeQrMode: _largeQrMode,
|
||||
)),
|
||||
// The label chip animates to zero height when there is no
|
||||
// label (or in large QR mode); keep it out of the semantics
|
||||
// tree entirely while it is collapsed.
|
||||
ExcludeSemantics(
|
||||
excluding: _largeQrMode || !hasLabel,
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
hint: S.of(context).set_label,
|
||||
child: GestureDetector(
|
||||
onTap: _showLabelModal,
|
||||
child: ReceiveLabelWidget(
|
||||
name: _addressItemWithLabel?.name ?? "",
|
||||
largeQrMode: _largeQrMode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => ReceiveBottomButtons(
|
||||
key: const ValueKey(0),
|
||||
largeQrMode: _largeQrMode,
|
||||
showAccountsButton: widget.addressListViewModel.hasAddressList,
|
||||
showLabelButton: widget.addressListViewModel.hasAddressList && !hasLabel,
|
||||
copyData: widget.addressListViewModel.hasPayjoin
|
||||
? null
|
||||
: ClipboardData(
|
||||
text: widget.addressListViewModel.displayAmount.isEmpty
|
||||
? widget.addressListViewModel.uri.address
|
||||
: widget.addressListViewModel.uri.toString(),
|
||||
),
|
||||
onCopyButtonPressed: () {
|
||||
if (widget.addressListViewModel.hasPayjoin) {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
PayjoinCopyModal(uri: widget.addressListViewModel.uri));
|
||||
} else {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: widget.addressListViewModel.displayAmount.isEmpty
|
||||
? widget.addressListViewModel.uri.address
|
||||
: widget.addressListViewModel.uri.toString()),
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
PayjoinCopyModal(uri: widget.addressListViewModel.uri),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -275,12 +278,10 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black.withAlpha(80),
|
||||
builder: (context) {
|
||||
return ReceiveAmountModal(
|
||||
walletAddressListViewModel: widget.addressListViewModel,
|
||||
onSubmitted: (amount) {},
|
||||
);
|
||||
},
|
||||
builder: (context) => ReceiveAmountModal(
|
||||
walletAddressListViewModel: widget.addressListViewModel,
|
||||
onSubmitted: (amount) {},
|
||||
),
|
||||
);
|
||||
},
|
||||
onLabelButtonPressed: _showLabelModal,
|
||||
@@ -293,22 +294,25 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
),
|
||||
),
|
||||
ReceiveLargeAmountPreview(
|
||||
amount: widget.addressListViewModel.displayAmount,
|
||||
currency: widget.addressListViewModel.cryptoCurrencySymbol,
|
||||
largeQrMode: _largeQrMode),
|
||||
amount: widget.addressListViewModel.displayAmount,
|
||||
currency: widget.addressListViewModel.cryptoCurrencySymbol,
|
||||
largeQrMode: _largeQrMode,
|
||||
),
|
||||
if (infobox != null && !widget.addressListViewModel.isLightning)
|
||||
ClipRect(
|
||||
child: AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
heightFactor: infoboxDismissed ? 0 : 1,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedOpacity(
|
||||
child: AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
heightFactor: infoboxDismissed ? 0 : 1,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: infoboxDismissed ? 0 : 1,
|
||||
curve: Curves.easeOutCubic,
|
||||
child: infobox),
|
||||
))
|
||||
child: infobox,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -318,14 +322,18 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Zcash is the only wallet type that also offers static address types, so
|
||||
/// the rotation notice must not be shown for it unless the disposable
|
||||
/// transparent type is the one currently selected.
|
||||
bool get _selectedAddressRotates => true;
|
||||
|
||||
void _showLabelModal() {
|
||||
showMaterialModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black.withAlpha(80),
|
||||
builder: (context) {
|
||||
return getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel);
|
||||
}).then((value) {
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black.withAlpha(80),
|
||||
builder: (_) => getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel),
|
||||
).then((value) {
|
||||
_reloadAddressWithLabel(widget.addressListViewModel.uri);
|
||||
});
|
||||
}
|
||||
@@ -334,8 +342,8 @@ class _NewReceivePageState extends State<NewReceivePage> {
|
||||
// FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later
|
||||
setState(() {
|
||||
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
|
||||
(item) => (item is WalletAddressListItem && item.address == newAddress.address))
|
||||
as WalletAddressListItem;
|
||||
(item) => item is WalletAddressListItem && item.address == newAddress.address,
|
||||
) as WalletAddressListItem?;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,643 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
class ScanPage extends StatelessWidget {
|
||||
const ScanPage({super.key});
|
||||
import 'package:hash_wallet/entities/qr_scanner.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/scan_page/network_list.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/send_page/floating_icon_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:fast_scanner/fast_scanner.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:ur/ur_decoder.dart';
|
||||
|
||||
class ScanPage extends StatefulWidget {
|
||||
const ScanPage({super.key, this.showHelp = false, this.showManualInput = true});
|
||||
|
||||
final bool showHelp;
|
||||
final bool showManualInput;
|
||||
|
||||
@override
|
||||
State<ScanPage> createState() => _ScanPageState();
|
||||
}
|
||||
|
||||
class _ScanPageState extends State<ScanPage> {
|
||||
final MobileScannerController controller = MobileScannerController();
|
||||
int? _numCameras;
|
||||
bool _frontFlashMode = false;
|
||||
bool _textInputMode = false;
|
||||
final TextEditingController textController = TextEditingController();
|
||||
final FocusNode textFocusNode = FocusNode();
|
||||
List<String> urCodes = [];
|
||||
late var ur = URQRToURQRData(urCodes);
|
||||
final decoder = URDecoder();
|
||||
bool popped = false;
|
||||
Barcode? _barcode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_numCameras = controller.value.availableCameras;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
textController.dispose();
|
||||
textFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
final double cutoutSize = MediaQuery.of(context).size.width * 0.8;
|
||||
const double cutoutRadius = 24.0;
|
||||
const Duration textModeSwitchDuration = Duration(milliseconds: 300);
|
||||
final buttonColor = _frontFlashMode ? Colors.black.withAlpha(40) : Colors.white.withAlpha(40);
|
||||
final buttonIconColor = _frontFlashMode ? Colors.black : Colors.white;
|
||||
final isScanningURQR = decoder.processedPartsCount() > 0;
|
||||
final double targetRadius = isScanningURQR ? (cutoutSize / 2) : cutoutRadius;
|
||||
|
||||
return Material(
|
||||
child: Stack(
|
||||
children: [
|
||||
MobileScanner(
|
||||
controller: controller,
|
||||
onDetect: _handleBarcode,
|
||||
),
|
||||
// A full screen tap target would otherwise show up as one giant unlabeled
|
||||
// node; leaving text mode is also possible with the back button.
|
||||
Positioned.fill(
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_textInputMode = false;
|
||||
}),
|
||||
child: RepaintBoundary(
|
||||
child: AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInQuad,
|
||||
duration: textModeSwitchDuration,
|
||||
child: _textInputMode
|
||||
? BackdropFilter(
|
||||
key: ValueKey(1),
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||
child: Container(
|
||||
color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153),
|
||||
),
|
||||
)
|
||||
: TweenAnimationBuilder<double>(
|
||||
key: const ValueKey(0),
|
||||
tween: Tween<double>(begin: 0.0, end: targetRadius),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||
child: Container(
|
||||
color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153),
|
||||
),
|
||||
),
|
||||
builder: (context, radius, child) {
|
||||
return ClipPath(
|
||||
clipper: HoleClipper(
|
||||
width: cutoutSize,
|
||||
height: cutoutSize,
|
||||
radius: radius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _textInputMode || isScanningURQR ? 0 : 1,
|
||||
duration: textModeSwitchDuration,
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: targetRadius),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, radius, child) => Center(
|
||||
child: Container(
|
||||
width: cutoutSize,
|
||||
height: cutoutSize,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white, width: 4.0),
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingWidget: AnimatedOpacity(
|
||||
duration: textModeSwitchDuration,
|
||||
opacity: _textInputMode ? 0 : 1,
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if ((_numCameras ?? 0) > 1)
|
||||
ModernButton.svg(
|
||||
size: 36,
|
||||
iconSize: 24,
|
||||
svgPath: "assets/new-ui/camera_flip.svg",
|
||||
onPressed: () {
|
||||
controller.switchCamera();
|
||||
setState(() {
|
||||
_frontFlashMode = false;
|
||||
});
|
||||
},
|
||||
iconColor: buttonIconColor,
|
||||
backgroundColor: buttonColor,
|
||||
semanticLabel: S.of(context).switch_camera,
|
||||
),
|
||||
ModernButton(
|
||||
size: 36,
|
||||
iconSize: 24,
|
||||
icon: Icon(
|
||||
(controller.value.torchState == TorchState.on || _frontFlashMode)
|
||||
? Icons.flash_off_outlined
|
||||
: Icons.flash_on),
|
||||
semanticLabel:
|
||||
(controller.value.torchState == TorchState.on || _frontFlashMode)
|
||||
? S.of(context).turn_flash_off
|
||||
: S.of(context).turn_flash_on,
|
||||
onPressed: () {
|
||||
if (controller.value.cameraDirection == CameraFacing.front) {
|
||||
setState(() {
|
||||
_frontFlashMode = !_frontFlashMode;
|
||||
});
|
||||
} else {
|
||||
controller.toggleTorch();
|
||||
}
|
||||
},
|
||||
iconColor: buttonIconColor,
|
||||
backgroundColor: buttonColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
leadingWidget: Row(
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
spacing: 24,
|
||||
children: [
|
||||
ModernButton(
|
||||
size: 36,
|
||||
iconSize: 18,
|
||||
icon: Icon(Icons.arrow_back_ios_new),
|
||||
semanticLabel: S.of(context).seed_alert_back,
|
||||
onPressed: () {
|
||||
if (_textInputMode) {
|
||||
setState(() {
|
||||
_textInputMode = false;
|
||||
});
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
iconColor: buttonIconColor,
|
||||
backgroundColor: buttonColor,
|
||||
),
|
||||
Text(
|
||||
S.of(context).scan,
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600, color: buttonIconColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 18 +
|
||||
max(MediaQuery.of(context).viewInsets.bottom,
|
||||
MediaQuery.of(context).viewPadding.bottom),
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: RepaintBoundary(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _textInputMode ? 1 : 0,
|
||||
duration: textModeSwitchDuration,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
label: S.of(context).enter_code,
|
||||
child: TextField(
|
||||
enabled: _textInputMode,
|
||||
controller: textController,
|
||||
focusNode: textFocusNode,
|
||||
onSubmitted: (val) {
|
||||
if (val.isNotEmpty) {
|
||||
Navigator.of(context).pop(val);
|
||||
} else {
|
||||
setState(() {
|
||||
_textInputMode = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(hintText: S.of(context).enter_code),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
FloatingIconButton(
|
||||
iconPath: "assets/new-ui/paste.svg",
|
||||
semanticLabel: S.of(context).paste,
|
||||
onPressed: () async {
|
||||
final data = await Clipboard.getData("text/plain");
|
||||
if (data?.text != null) {
|
||||
textController.text = data!.text!;
|
||||
}
|
||||
}),
|
||||
SizedBox(
|
||||
width: 2,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 120,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: RepaintBoundary(
|
||||
child: AnimatedOpacity(
|
||||
duration: textModeSwitchDuration,
|
||||
opacity: _textInputMode ? 0 : 1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
// "not mvp"
|
||||
// ScanPageButton(
|
||||
// onTap: () async {
|
||||
// FilePickerResult? res = await FilePicker.platform.pickFiles(
|
||||
// type: FileType.image,
|
||||
// allowMultiple: false,
|
||||
// withData: false,
|
||||
// );
|
||||
//
|
||||
// if (res != null && res.paths.isNotEmpty && res.paths.first != null) {
|
||||
// final capture = await controller.analyzeImage(res.paths.first!);
|
||||
// if (capture != null) {
|
||||
// _handleBarcode(capture);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// icon: Icons.photo_outlined,
|
||||
// label: S.of(context).gallery,
|
||||
// buttonColor: buttonColor,
|
||||
// buttonIconColor: buttonIconColor),
|
||||
if (widget.showManualInput)
|
||||
ScanPageButton(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_textInputMode = true;
|
||||
});
|
||||
Future.delayed(textModeSwitchDuration)
|
||||
.then((val) => textFocusNode.requestFocus());
|
||||
},
|
||||
icon: Icons.edit_outlined,
|
||||
label: S.of(context).manual_input,
|
||||
buttonColor: buttonColor,
|
||||
buttonIconColor: buttonIconColor),
|
||||
if (widget.showHelp)
|
||||
ScanPageButton(
|
||||
onTap: () async {
|
||||
if (_textInputMode) return;
|
||||
try {
|
||||
controller.stop();
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (context) => ScanPageNetworkList());
|
||||
} finally {
|
||||
controller.start();
|
||||
}
|
||||
},
|
||||
icon: Icons.question_mark,
|
||||
semanticsLabel: S.of(context).help,
|
||||
buttonColor: buttonColor,
|
||||
buttonIconColor: buttonIconColor)
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 500),
|
||||
opacity: isScanningURQR ? 1 : 0,
|
||||
child: Center(
|
||||
child: SegmentedCircularProgress(
|
||||
progress: URQrProgress(
|
||||
expectedPartCount: decoder.expectedPartCount() ?? 0,
|
||||
processedPartsCount: decoder.processedPartsCount(),
|
||||
receivedPartIndexes: decoder.receivedPartIndexes().toList(),
|
||||
percentage: decoder.estimatedPercentComplete(),
|
||||
),
|
||||
size: cutoutSize + 20,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
inactiveColor: Colors.white.withAlpha(102)),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 120,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 500),
|
||||
opacity: isScanningURQR ? 1 : 0,
|
||||
// Three separate digits mean nothing on their own: announce the progress
|
||||
// as one node, and only while a multi part code is actually being scanned.
|
||||
child: ExcludeSemantics(
|
||||
excluding: !isScanningURQR,
|
||||
child: Semantics(
|
||||
container: true,
|
||||
liveRegion: true,
|
||||
label: S.of(context).qr_parts_scanned(
|
||||
"${decoder.processedPartsCount()}", "${decoder.expectedPartCount() ?? 0}"),
|
||||
excludeSemantics: true,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${decoder.processedPartsCount()}",
|
||||
style: TextStyle(
|
||||
fontSize: 45,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
Text(
|
||||
"/",
|
||||
style: TextStyle(
|
||||
fontSize: 45, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
"${decoder.expectedPartCount()}",
|
||||
style: TextStyle(
|
||||
fontSize: 45, color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleBarcode(BarcodeCapture barcodes) {
|
||||
try {
|
||||
_handleBarcodeInternal(barcodes);
|
||||
} catch (e, st) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: S.of(context).error_dialog_content,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
printV("$e\n$st");
|
||||
}
|
||||
}
|
||||
|
||||
void _handleBarcodeInternal(BarcodeCapture barcodes) {
|
||||
for (final barcode in barcodes.barcodes) {
|
||||
if (barcode.rawValue?.trim().isEmpty ?? false == false) continue;
|
||||
if (barcode.rawValue!.startsWith("ur:")) {
|
||||
if (urCodes.contains(barcode.rawValue)) continue;
|
||||
decoder.receivePart(barcode.rawValue!);
|
||||
setState(() {
|
||||
urCodes.add(barcode.rawValue!);
|
||||
ur = URQRToURQRData(urCodes);
|
||||
});
|
||||
if (decoder.estimatedPercentComplete() == 1) {
|
||||
setState(() {
|
||||
popped = true;
|
||||
});
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.of(context).pop(ur.inputs.join("\n"));
|
||||
});
|
||||
}
|
||||
;
|
||||
}
|
||||
}
|
||||
if (urCodes.isNotEmpty) return;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_barcode = barcodes.barcodes.firstOrNull;
|
||||
});
|
||||
if (_barcode != null && popped != true) {
|
||||
setState(() {
|
||||
popped = true;
|
||||
});
|
||||
Navigator.of(context).pop(_barcode!.rawValue ?? _barcode!.rawBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScanPageButton extends StatelessWidget {
|
||||
const ScanPageButton(
|
||||
{super.key,
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
this.label,
|
||||
this.semanticsLabel,
|
||||
required this.buttonColor,
|
||||
required this.buttonIconColor});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final IconData icon;
|
||||
final String? label;
|
||||
|
||||
/// Name for the icon-only variant, which has no visible [label].
|
||||
final String? semanticsLabel;
|
||||
final Color buttonColor;
|
||||
final Color buttonIconColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: true,
|
||||
label: semanticsLabel ?? label,
|
||||
onTap: onTap,
|
||||
excludeSemantics: true,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
decoration:
|
||||
BoxDecoration(color: buttonColor, borderRadius: BorderRadius.circular(99999)),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: label == null ? 10 : 16,
|
||||
right: label == null ? 10 : 20),
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Icon(icon, size: 28, color: buttonIconColor),
|
||||
if (label != null)
|
||||
Text(label!,
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500, color: buttonIconColor))
|
||||
],
|
||||
),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
class HoleClipper extends CustomClipper<Path> {
|
||||
final double width;
|
||||
final double height;
|
||||
final double radius;
|
||||
|
||||
HoleClipper({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.radius,
|
||||
});
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final Path fullScreenPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||
|
||||
final Path cutoutPath = Path()
|
||||
..addRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: Offset(size.width / 2, size.height / 2),
|
||||
width: width,
|
||||
height: height,
|
||||
),
|
||||
Radius.circular(radius),
|
||||
),
|
||||
);
|
||||
|
||||
return Path.combine(
|
||||
PathOperation.difference,
|
||||
fullScreenPath,
|
||||
cutoutPath,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant HoleClipper oldClipper) {
|
||||
return oldClipper.width != width || oldClipper.height != height || oldClipper.radius != radius;
|
||||
}
|
||||
}
|
||||
|
||||
class SegmentedCircularProgress extends StatelessWidget {
|
||||
final URQrProgress progress;
|
||||
final Color activeColor;
|
||||
final Color inactiveColor;
|
||||
final double size;
|
||||
|
||||
const SegmentedCircularProgress({
|
||||
Key? key,
|
||||
required this.progress,
|
||||
required this.activeColor,
|
||||
required this.inactiveColor,
|
||||
required this.size,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size(size, size),
|
||||
painter: _SegmentedCirclePainter(
|
||||
progress: progress,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SegmentedCirclePainter extends CustomPainter {
|
||||
final URQrProgress progress;
|
||||
static const double strokeWidth = 10;
|
||||
static const double gapAngle = 0.08;
|
||||
final Color activeColor;
|
||||
final Color inactiveColor;
|
||||
|
||||
_SegmentedCirclePainter({
|
||||
required this.progress,
|
||||
required this.activeColor,
|
||||
required this.inactiveColor,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
final totalSegments = progress.expectedPartCount;
|
||||
|
||||
final sweepAngle = (2 * pi - (gapAngle * totalSegments)) / totalSegments;
|
||||
|
||||
final paint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = StrokeCap.butt;
|
||||
|
||||
double startAngle = -pi / 2 + (gapAngle / 2);
|
||||
|
||||
for (int i = 0; i < totalSegments; i++) {
|
||||
bool isHighlighted = progress.receivedPartIndexes.contains(i);
|
||||
|
||||
paint.color = isHighlighted ? activeColor : inactiveColor;
|
||||
|
||||
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
|
||||
|
||||
startAngle += sweepAngle + gapAngle;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _SegmentedCirclePainter oldDelegate) {
|
||||
return !oldDelegate.progress.equals(progress) ||
|
||||
oldDelegate.progress.expectedPartCount != progress.expectedPartCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_scaffold.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/stacked_buttons.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class DismissSeedVerificationPage extends StatelessWidget {
|
||||
const DismissSeedVerificationPage({required this.onConfirm, super.key});
|
||||
|
||||
final VoidCallback onConfirm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SeedPageScaffold(
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 295),
|
||||
child: SeedPageHeader(
|
||||
image: const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/warning_triangle.svg",
|
||||
width: 125,
|
||||
height: 111.72,
|
||||
),
|
||||
imageSpacing: 48,
|
||||
titleSpacing: 24,
|
||||
title: S.of(context).dismiss_verification_title,
|
||||
description: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: "${S.of(context).dismiss_verification_description}\n"),
|
||||
TextSpan(
|
||||
text: S.of(context).seed_display_path,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
footer: StackedButtons(
|
||||
primaryKey: const ValueKey("dismiss_seed_verification_page_confirm_button_key"),
|
||||
primaryText: S.of(context).dismiss_verification_confirm,
|
||||
onPrimary: onConfirm,
|
||||
secondaryKey: const ValueKey("dismiss_seed_verification_page_back_button_key"),
|
||||
secondaryText: S.of(context).seed_alert_back,
|
||||
onSecondary: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/utils/feature_flag.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
void openWalletAfterSeedFlow(BuildContext context, WalletType walletType) {
|
||||
if (FeatureFlag.isLightningEnabled && walletType == WalletType.bitcoin) {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
Routes.lightningUsernamePage,
|
||||
(route) => route.isFirst,
|
||||
arguments: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/seed/open_wallet_after_seed_flow.dart";
|
||||
import "package:hash_wallet/new-ui/pages/seed/skip_seed_verification_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_checkbox_row.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_hero_image.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_scaffold.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/stacked_buttons.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class PreSeedPage extends StatefulWidget {
|
||||
const PreSeedPage(this.wallet, {super.key});
|
||||
|
||||
final WalletBase wallet;
|
||||
|
||||
@override
|
||||
State<PreSeedPage> createState() => _PreSeedPageState();
|
||||
}
|
||||
|
||||
class _PreSeedPageState extends State<PreSeedPage> {
|
||||
bool _understandsOnlyWayToRecover = false;
|
||||
bool _willWriteItDown = false;
|
||||
bool _willNeverShareIt = false;
|
||||
|
||||
bool get _hasCheckedAll => _understandsOnlyWayToRecover && _willWriteItDown && _willNeverShareIt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
canPop: false,
|
||||
child: SeedPageScaffold(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 24,
|
||||
children: [
|
||||
SeedPageHeader(
|
||||
image: const SeedHeroImage(),
|
||||
title: S.of(context).recovery_phrase_intro_title,
|
||||
description: Text(
|
||||
S.of(context).recovery_phrase_intro_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SeedCheckboxRow(
|
||||
key: const ValueKey("pre_seed_page_only_way_checkbox_key"),
|
||||
iconPath: "assets/new-ui/key.svg",
|
||||
text: S.of(context).recovery_phrase_check_only_way,
|
||||
isChecked: _understandsOnlyWayToRecover,
|
||||
onChanged: (value) => setState(() => _understandsOnlyWayToRecover = value),
|
||||
),
|
||||
SeedCheckboxRow(
|
||||
key: const ValueKey("pre_seed_page_write_down_checkbox_key"),
|
||||
iconPath: "assets/new-ui/set-amount.svg",
|
||||
text: S.of(context).recovery_phrase_check_write_down,
|
||||
isChecked: _willWriteItDown,
|
||||
onChanged: (value) => setState(() => _willWriteItDown = value),
|
||||
),
|
||||
SeedCheckboxRow(
|
||||
key: const ValueKey("pre_seed_page_never_share_checkbox_key"),
|
||||
iconPath: "assets/new-ui/visibility.svg",
|
||||
text: S.of(context).recovery_phrase_check_never_share,
|
||||
isChecked: _willNeverShareIt,
|
||||
onChanged: (value) => setState(() => _willNeverShareIt = value),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!_hasCheckedAll)
|
||||
Text(
|
||||
S.of(context).recovery_phrase_check_all_to_proceed,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.customColors.warningOutlineColor,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
footer: _hasCheckedAll
|
||||
? StackedButtons(
|
||||
primaryKey: const ValueKey("pre_seed_page_button_key"),
|
||||
primaryText: S.of(context).show_recovery_phrase,
|
||||
onPrimary: () => Navigator.of(context).pushNamed(Routes.seed, arguments: true),
|
||||
secondaryKey: const ValueKey("pre_seed_page_skip_button_key"),
|
||||
secondaryText: S.of(context).skip_this_step,
|
||||
onSecondary: _confirmSkip,
|
||||
secondaryAsLink: true,
|
||||
)
|
||||
: null,
|
||||
alignTop: true,
|
||||
),
|
||||
);
|
||||
|
||||
void _confirmSkip() {
|
||||
Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (_) => SkipSeedVerificationPage(
|
||||
onConfirm: () => openWalletAfterSeedFlow(context, widget.wallet.type),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import "package:hash_wallet/core/auth_service.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/seed/dismiss_seed_verification_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_hero_image.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_scaffold.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/stacked_buttons.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SeedBackupReminderPage extends StatelessWidget {
|
||||
const SeedBackupReminderPage({
|
||||
required this.dashboardViewModel,
|
||||
required this.authService,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final AuthService authService;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SeedPageScaffold(
|
||||
content: SeedPageHeader(
|
||||
image: const SeedHeroImage(),
|
||||
title: S.of(context).recovery_phrase_intro_title,
|
||||
description: Text(
|
||||
S.of(context).recovery_phrase_intro_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
),
|
||||
footer: StackedButtons(
|
||||
primaryKey: const ValueKey("seed_backup_reminder_page_button_key"),
|
||||
primaryText: S.of(context).show_recovery_phrase,
|
||||
onPrimary: () => authService.authenticateAction(
|
||||
context,
|
||||
route: Routes.seed,
|
||||
arguments: false,
|
||||
conditionToDetermineIfToUse2FA: dashboardViewModel
|
||||
.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||
),
|
||||
secondaryKey: const ValueKey("seed_backup_reminder_page_dismiss_button_key"),
|
||||
secondaryText: S.of(context).dismiss_this_step,
|
||||
onSecondary: () => Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (_) => DismissSeedVerificationPage(onConfirm: () => _dismiss(context)),
|
||||
),
|
||||
),
|
||||
secondaryAsLink: true,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _dismiss(BuildContext context) async {
|
||||
await dashboardViewModel.dismissSeedBackupReminder();
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import "package:hash_wallet/core/auth_service.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_hero_image.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_scaffold.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/stacked_buttons.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/store/settings_store.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ShowKeysDisclaimerPage extends StatelessWidget {
|
||||
const ShowKeysDisclaimerPage({
|
||||
required this.authService,
|
||||
required this.settingsStore,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AuthService authService;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SeedPageScaffold(
|
||||
content: SeedPageHeader(
|
||||
image: const SeedHeroImage(showWarningBadge: true),
|
||||
title: S.of(context).secret_information_ahead,
|
||||
description: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: "${S.of(context).secret_information_ahead_description}\n\n"),
|
||||
TextSpan(
|
||||
text: S.of(context).secret_information_ahead_warning,
|
||||
style: TextStyle(color: context.customColors.warningOutlineColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
),
|
||||
footer: StackedButtons(
|
||||
primaryKey: const ValueKey("show_keys_disclaimer_page_button_key"),
|
||||
primaryText: S.of(context).show_recovery_phrase_and_keys,
|
||||
onPrimary: () => authService.authenticateAction(
|
||||
context,
|
||||
route: Routes.showKeys,
|
||||
conditionToDetermineIfToUse2FA:
|
||||
settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||
),
|
||||
secondaryKey: const ValueKey("show_keys_disclaimer_page_back_button_key"),
|
||||
secondaryText: S.of(context).seed_alert_back,
|
||||
onSecondary: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/seed/seed_page_scaffold.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/stacked_buttons.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SkipSeedVerificationPage extends StatelessWidget {
|
||||
const SkipSeedVerificationPage({required this.onConfirm, super.key});
|
||||
|
||||
final VoidCallback onConfirm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SeedPageScaffold(
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 295),
|
||||
child: SeedPageHeader(
|
||||
image: const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/warning_triangle.svg",
|
||||
width: 125,
|
||||
height: 111.72,
|
||||
),
|
||||
imageSpacing: 48,
|
||||
titleSpacing: 24,
|
||||
title: S.of(context).skip_verification_title,
|
||||
description: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).skip_verification_warning,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.customColors.warningOutlineColor,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
S.of(context).skip_verification_question,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
footer: StackedButtons(
|
||||
primaryKey: const ValueKey("skip_seed_verification_page_confirm_button_key"),
|
||||
primaryText: S.of(context).skip_verification_confirm,
|
||||
onPrimary: onConfirm,
|
||||
secondaryKey: const ValueKey("skip_seed_verification_page_back_button_key"),
|
||||
secondaryText: S.of(context).seed_alert_back,
|
||||
onSecondary: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
+1062
-1011
File diff suppressed because it is too large
Load Diff
+414
-138
@@ -1,109 +1,221 @@
|
||||
import 'package:hash_wallet/core/auth_service.dart';
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart';
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import "package:hash_wallet/core/auth_service.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:hash_wallet/new-ui/pages/coin_control_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
bool _trueFunc(DashboardViewModel _) => true;
|
||||
|
||||
bool _falseFunc(DashboardViewModel _) => false;
|
||||
|
||||
bool _isBtc(DashboardViewModel vm) => vm.wallet.type == WalletType.bitcoin;
|
||||
bool _alwaysVisible(DashboardViewModel _) => true;
|
||||
|
||||
bool _hasLightning(DashboardViewModel vm) => vm.hasLightning;
|
||||
|
||||
bool _hasSilentPayments(DashboardViewModel vm) => vm.hasSilentPayments;
|
||||
|
||||
bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
|
||||
|
||||
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
|
||||
|
||||
bool _isCupcake(DashboardViewModel vm) =>
|
||||
vm.wallet.hardwareWalletType == HardwareWalletType.cupcake;
|
||||
bool _hasAccounts(DashboardViewModel vm) => vm.wallet.hasAccountsSupport;
|
||||
|
||||
bool _hasCoinControl(DashboardViewModel vm) => vm.wallet.hasCoinControl;
|
||||
|
||||
bool _requiresKeyImageSync(DashboardViewModel vm) =>
|
||||
vm.wallet.type == WalletType.monero &&
|
||||
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
|
||||
|
||||
class SettingsListItem {
|
||||
const SettingsListItem(
|
||||
this.iconPath,
|
||||
this.title,
|
||||
this.route, {
|
||||
this.isCore = false,
|
||||
this.requireAuth = false,
|
||||
this.use2fa = _neverUse2fa,
|
||||
this.condition = _alwaysVisible,
|
||||
this.routeArgs,
|
||||
this.routeArgsBuilder,
|
||||
});
|
||||
|
||||
static bool _neverUse2fa(DashboardViewModel _) => false;
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String route;
|
||||
final bool isCore;
|
||||
final Object? routeArgs;
|
||||
final Object? Function(DashboardViewModel)? routeArgsBuilder;
|
||||
final bool requireAuth;
|
||||
final bool Function(DashboardViewModel) use2fa;
|
||||
final bool Function(DashboardViewModel) condition;
|
||||
|
||||
const SettingsListItem(this.iconPath, this.title, this.route,
|
||||
{this.routeArgs = null,
|
||||
this.requireAuth = false,
|
||||
this.use2fa = _falseFunc,
|
||||
this.condition = _trueFunc});
|
||||
}
|
||||
|
||||
class SettingsSectionData {
|
||||
const SettingsSectionData(
|
||||
this.title,
|
||||
this.titleIconPath,
|
||||
this.groups, {
|
||||
this.titleColor,
|
||||
this.headerSpacing = 16,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String titleIconPath;
|
||||
final List<SettingsListItem> items;
|
||||
final Map<String, List<ListItem>> groups;
|
||||
final Color? titleColor;
|
||||
final double headerSpacing;
|
||||
}
|
||||
|
||||
const SettingsSectionData(this.title, this.titleIconPath, this.items);
|
||||
class WalletSettingsResolver {
|
||||
const WalletSettingsResolver();
|
||||
|
||||
static SettingsSectionData walletSettings =
|
||||
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/nodes.svg", S.current.nodes, Routes.manageNodes),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/privacy.svg", S.current.privacy, Routes.privacyPage),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/seed.svg", S.current.seed_and_keys, Routes.showKeys,
|
||||
routeArgs: true,
|
||||
requireAuth: true,
|
||||
use2fa: (vm) => vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/lightning_username.svg",
|
||||
"Lightning ${S.current.username}", Routes.lightningUsernamePage,
|
||||
condition: _hasLightning),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/wc.svg", S.current.walletConnect,
|
||||
String titleFor(WalletType walletType, S strings) =>
|
||||
'${walletTypeToString(walletType)} ${strings.settings_title}'.trim();
|
||||
|
||||
String iconPathFor(WalletType walletType) =>
|
||||
"assets/new-ui/network_icons/${walletType.name.toLowerCase()}.svg";
|
||||
|
||||
List<List<SettingsListItem>> resolveSections(DashboardViewModel viewModel) {
|
||||
final visibleItems = <SettingsListItem>[
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/accounts.svg",
|
||||
viewModel.wallet.type == WalletType.bitcoin ? S.current.accounts_onchain : S.current.accounts,
|
||||
Routes.walletAccountsPage,
|
||||
isCore: true,
|
||||
condition: _hasAccounts,
|
||||
routeArgsBuilder: (vm) => vm,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/nodes.svg",
|
||||
S.current.nodes,
|
||||
Routes.manageNodes,
|
||||
isCore: true,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/coin-control.svg",
|
||||
S.current.coin_control_settings,
|
||||
Routes.unspentCoinsList,
|
||||
isCore: true,
|
||||
condition: _hasCoinControl,
|
||||
routeArgs: const CoinControlPageArgs(canEdit: false),
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/lightning_username.svg",
|
||||
"Lightning ${S.current.username}",
|
||||
Routes.lightningUsernamePage,
|
||||
condition: _hasLightning,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/silent-payments.svg",
|
||||
S.current.silent_payments,
|
||||
Routes.silentPaymentsSettings,
|
||||
condition: _hasSilentPayments,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/mweb.svg",
|
||||
S.current.litecoin_mweb,
|
||||
Routes.mwebSettings,
|
||||
condition: _hasMweb,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/wc.svg",
|
||||
S.current.walletConnect,
|
||||
Routes.walletConnectConnectionsListing,
|
||||
condition: _hasWalletConnect),
|
||||
//SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc),
|
||||
//SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs,
|
||||
Routes.urqrAnimatedPage,
|
||||
routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage),
|
||||
]);
|
||||
condition: _hasWalletConnect,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/sync-balance.svg",
|
||||
S.current.resync_device,
|
||||
Routes.syncKeyImagesDevices,
|
||||
routeArgs: const {"export-outputs": "export-outputs"},
|
||||
condition: _requiresKeyImageSync,
|
||||
),
|
||||
].where((item) => item.condition(viewModel)).toList();
|
||||
|
||||
static SettingsSectionData appSettings =
|
||||
SettingsSectionData(S.current.app_settings, "assets/new-ui/hash-setting.svg", [
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/connections.svg", S.current.connections,
|
||||
Routes.connectionSync),
|
||||
// SettingsListItem("assets/new-ui/settings_row_icons/defaults.svg", "Defaults", ""),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/display.svg", S.current.display,
|
||||
Routes.displaySettingsPage),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/security.svg", S.current.security,
|
||||
Routes.securityBackupPage),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/backup.svg", S.current.backup, Routes.backup,
|
||||
requireAuth: true,
|
||||
use2fa: (vm) => vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
|
||||
]);
|
||||
final coreItems = visibleItems.where((item) => item.isCore).toList();
|
||||
final featureItems = visibleItems.where((item) => !item.isCore).toList();
|
||||
|
||||
static SettingsSectionData otherSettings = SettingsSectionData("", "", [
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/support.svg", S.current.settings_support, Routes.support),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/info.svg", S.current.about, Routes.aboutPage),
|
||||
]);
|
||||
return [
|
||||
if (coreItems.isNotEmpty) coreItems,
|
||||
if (featureItems.isNotEmpty) featureItems,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
static List<SettingsSectionData> all = [walletSettings, appSettings, otherSettings];
|
||||
class SettingsPageSectionsResolver {
|
||||
const SettingsPageSectionsResolver();
|
||||
|
||||
List<SettingsListItem> walletGeneral(S strings) => [
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/privacy.svg',
|
||||
strings.privacy,
|
||||
Routes.privacyPage,
|
||||
),
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/seed.svg',
|
||||
strings.recovery_and_keys,
|
||||
Routes.showKeysDisclaimer,
|
||||
),
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/other.svg',
|
||||
strings.other,
|
||||
Routes.otherSettingsPage,
|
||||
),
|
||||
];
|
||||
|
||||
List<SettingsListItem> appSettings() => [
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/connections.svg",
|
||||
S.current.connections,
|
||||
Routes.connectionSync,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/display.svg",
|
||||
S.current.display,
|
||||
Routes.displaySettingsPage,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/security.svg",
|
||||
S.current.security,
|
||||
Routes.securityBackupPage,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/backup.svg",
|
||||
S.current.backup,
|
||||
Routes.backup,
|
||||
requireAuth: true,
|
||||
use2fa: (vm) => vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||
),
|
||||
];
|
||||
|
||||
List<SettingsListItem> supportAndAbout(S strings) => [
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/support.svg',
|
||||
strings.settings_support,
|
||||
Routes.support,
|
||||
),
|
||||
SettingsListItem(
|
||||
'assets/new-ui/settings_row_icons/info.svg',
|
||||
strings.about,
|
||||
Routes.aboutPage,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
class NewSettingsPage extends StatelessWidget {
|
||||
const NewSettingsPage({super.key, required this.dashboardViewModel, required this.authService});
|
||||
const NewSettingsPage({
|
||||
super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final AuthService authService;
|
||||
@@ -111,84 +223,248 @@ class NewSettingsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ModalNavigator(
|
||||
parentContext: context,
|
||||
rootPage: SettingsMainPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
authService: authService,
|
||||
));
|
||||
parentContext: context,
|
||||
rootPage: SettingsMainPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
authService: authService,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsMainPage extends StatelessWidget {
|
||||
const SettingsMainPage({super.key, required this.dashboardViewModel, required this.authService});
|
||||
const SettingsMainPage({
|
||||
super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
static const _walletSettingsResolver = WalletSettingsResolver();
|
||||
static const _sectionsResolver = SettingsPageSectionsResolver();
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final AuthService authService;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).settings_title,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: ModalScrollController.of(context),
|
||||
padding: const EdgeInsets.fromLTRB(18, 0, 18, 32),
|
||||
children: [
|
||||
for (final (index, section) in _sections(context).indexed) ...[
|
||||
if (index > 0) ...[
|
||||
const SizedBox(height: 24),
|
||||
Divider(color: Theme.of(context).colorScheme.surfaceContainerHighest),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
_SettingsSectionHeader(
|
||||
iconPath: section.titleIconPath,
|
||||
title: section.title,
|
||||
foregroundColor: section.titleColor,
|
||||
),
|
||||
SizedBox(height: section.headerSpacing),
|
||||
for (final (groupIndex, group) in section.groups.entries.indexed) ...[
|
||||
if (groupIndex > 0) const SizedBox(height: 24),
|
||||
NewListSections(sections: {group.key: group.value}),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
List<SettingsSectionData> _sections(BuildContext context) => [
|
||||
SettingsSectionData(
|
||||
dashboardViewModel.wallet.name,
|
||||
"assets/new-ui/wallet-setting.svg",
|
||||
{
|
||||
"wallet_type_settings": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "wallet_type_settings",
|
||||
label: _walletSettingsResolver.titleFor(
|
||||
dashboardViewModel.wallet.type,
|
||||
S.of(context),
|
||||
),
|
||||
iconPath: _walletSettingsResolver.iconPathFor(dashboardViewModel.wallet.type),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => WalletSettingsPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
authService: authService,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
"wallet_general": _buildRows(context, _sectionsResolver.walletGeneral(S.of(context))),
|
||||
},
|
||||
titleColor: Theme.of(context).colorScheme.primary,
|
||||
headerSpacing: 20,
|
||||
),
|
||||
SettingsSectionData(
|
||||
S.of(context).app_settings,
|
||||
"assets/new-ui/hash-setting.svg",
|
||||
{
|
||||
"app_settings": _buildRows(context, _sectionsResolver.appSettings()),
|
||||
"support_and_about":
|
||||
_buildRows(context, _sectionsResolver.supportAndAbout(S.of(context))),
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
List<ListItem> _buildRows(BuildContext context, List<SettingsListItem> items) => items
|
||||
.where((item) => item.condition(dashboardViewModel))
|
||||
.map((item) => _buildRow(context, item))
|
||||
.toList();
|
||||
|
||||
ListItemRegularRow _buildRow(BuildContext context, SettingsListItem item) => ListItemRegularRow(
|
||||
keyValue: item.route,
|
||||
label: item.title,
|
||||
iconPath: item.iconPath,
|
||||
onTap: () => _openItem(context, item),
|
||||
);
|
||||
|
||||
Future<void> _openItem(BuildContext context, SettingsListItem item) async {
|
||||
final arguments = item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs;
|
||||
if (item.requireAuth) {
|
||||
await authService.authenticateAction(
|
||||
context,
|
||||
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
|
||||
route: item.route,
|
||||
arguments: arguments,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.of(context).pushNamed(item.route, arguments: arguments);
|
||||
}
|
||||
}
|
||||
|
||||
class WalletSettingsPage extends StatelessWidget {
|
||||
const WalletSettingsPage({
|
||||
super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
static const _resolver = WalletSettingsResolver();
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final AuthService authService;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<ListItem> buildItems(SettingsSectionData section) => section.items
|
||||
.map((item) => item.condition(dashboardViewModel)
|
||||
? ListItemRegularRow(
|
||||
keyValue: item.title,
|
||||
label: item.title,
|
||||
iconPath: item.iconPath,
|
||||
onTap: () {
|
||||
if (item.route.isNotEmpty) {
|
||||
if (item.requireAuth) {
|
||||
authService.authenticateAction(context,
|
||||
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
|
||||
route: item.route);
|
||||
} else {
|
||||
Navigator.of(context).pushNamed(item.route, arguments: item.routeArgs);
|
||||
}
|
||||
}
|
||||
})
|
||||
: null)
|
||||
.whereType<ListItem>()
|
||||
.toList();
|
||||
final strings = S.of(context);
|
||||
final sections = _resolver.resolveSections(dashboardViewModel);
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(children: [
|
||||
ModalTopBar(
|
||||
title: "Settings",
|
||||
leadingIcon: Icon(Icons.close),
|
||||
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
|
||||
onTrailingPressed: () {},
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(controller: ModalScrollController.of(context), children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
children: SettingsSectionData.all.expand((section) {
|
||||
return [
|
||||
if (section.titleIconPath.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: section.titleIconPath,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
)),
|
||||
Text(section.title, style: Theme.of(context).textTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
NewListSections(sections: {section.title: buildItems(section)}),
|
||||
];
|
||||
}).toList(),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: _resolver.titleFor(dashboardViewModel.wallet.type, strings),
|
||||
titleLeadingWidget: HashImageWidget(
|
||||
imageUrl: _resolver.iconPathFor(dashboardViewModel.wallet.type),
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: strings.seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: ModalScrollController.of(context),
|
||||
padding: const EdgeInsets.fromLTRB(18, 0, 18, 32),
|
||||
children: [
|
||||
for (var index = 0; index < sections.length; index++) ...[
|
||||
if (index > 0) const SizedBox(height: 24),
|
||||
NewListSections(
|
||||
sections: {
|
||||
'wallet_settings_$index': sections[index]
|
||||
.map(
|
||||
(item) => ListItemRegularRow(
|
||||
keyValue: item.route,
|
||||
label: item.title,
|
||||
iconPath: item.iconPath,
|
||||
onTap: () => _openItem(context, item),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openItem(BuildContext context, SettingsListItem item) async {
|
||||
final arguments = item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs;
|
||||
if (item.requireAuth) {
|
||||
await authService.authenticateAction(
|
||||
context,
|
||||
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
|
||||
route: item.route,
|
||||
arguments: arguments,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.of(context).pushNamed(item.route, arguments: arguments);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsSectionHeader extends StatelessWidget {
|
||||
const _SettingsSectionHeader({
|
||||
required this.iconPath,
|
||||
required this.title,
|
||||
this.foregroundColor,
|
||||
});
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final Color? foregroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
height: 20,
|
||||
width: 20,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: color),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+558
-827
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,544 @@
|
||||
import "dart:async";
|
||||
import "dart:math";
|
||||
import "dart:ui";
|
||||
|
||||
import "package:hash_wallet/core/utilities.dart";
|
||||
import "package:hash_wallet/core/amount_parsing_proxy.dart";
|
||||
import "package:hash_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:hash_wallet/src/screens/settings/widgets/account_creation_modal.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/account_education_page.dart";
|
||||
import "package:hash_wallet/new-ui/pages/card_customizer.dart";
|
||||
import "package:hash_wallet/new-ui/pages/hidden_accounts.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:hash_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:hash_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
|
||||
import "package:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
|
||||
import "package:cw_core/balance_card_layout.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:cw_core/sync_status.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class AccountCustomizerListItem {
|
||||
const AccountCustomizerListItem({
|
||||
required this.card,
|
||||
required this.accountListItem,
|
||||
required this.settings,
|
||||
});
|
||||
|
||||
final BalanceCard card;
|
||||
final AccountListItem accountListItem;
|
||||
final BalanceCardStyleSettings? settings;
|
||||
}
|
||||
|
||||
class WalletAccountsPage extends StatefulWidget {
|
||||
const WalletAccountsPage({
|
||||
required this.accountListViewModel,
|
||||
required this.accountEditOrCreateViewModel,
|
||||
required this.dashboardViewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WalletAccountListViewModel accountListViewModel;
|
||||
final WalletAccountEditOrCreateViewModel accountEditOrCreateViewModel;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
State<WalletAccountsPage> createState() => _WalletAccountsPageState();
|
||||
}
|
||||
|
||||
class _WalletAccountsPageState extends State<WalletAccountsPage> {
|
||||
final List<AccountCustomizerListItem> _items = [];
|
||||
bool _hasArchivedAccounts = false;
|
||||
bool _loading = true;
|
||||
ReactionDisposer? _accountsReaction;
|
||||
|
||||
bool get _isMultiAccountsEnabled => widget.dashboardViewModel.isMultiAccountsEnabled;
|
||||
|
||||
String get _assetName => AmountParsingProxy(
|
||||
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi,
|
||||
).getCryptoSymbol(widget.accountListViewModel.currency);
|
||||
|
||||
bool get _capitalizeAssetName =>
|
||||
widget.dashboardViewModel.wallet.type != WalletType.bitcoin ||
|
||||
widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi !=
|
||||
BitcoinAmountDisplayMode.satoshi;
|
||||
|
||||
double get cardWidth => min(MediaQuery.sizeOf(context).width * 0.9, 768);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_accountsReaction = reaction(
|
||||
(_) => widget.accountListViewModel.accounts
|
||||
.map((account) => "${account.id}:${account.label}:${account.balance}")
|
||||
.join(","),
|
||||
(_) {
|
||||
if (mounted) unawaited(loadCards());
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => unawaited(_initialize()));
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (!mounted) return;
|
||||
await _bringActiveAccountToFront();
|
||||
if (!mounted || !_isMultiAccountsEnabled || _items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final educationPage = AccountEducationPage(
|
||||
settingsStore: widget.dashboardViewModel.settingsStore,
|
||||
);
|
||||
if (!educationPage.isDismissed) {
|
||||
await educationPage.show(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_accountsReaction?.call();
|
||||
saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int get _walletInfoId => widget.dashboardViewModel.wallet.walletInfo.internalId;
|
||||
|
||||
Future<void> _bringActiveAccountToFront() async {
|
||||
await loadCards();
|
||||
|
||||
final activeAccount =
|
||||
widget.accountListViewModel.accounts.firstWhereOrNull((account) => account.isSelected);
|
||||
if (activeAccount == null || _items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final index = _items.indexWhere((item) => item.accountListItem.id == activeAccount.id);
|
||||
if (index == -1 || index == _items.length - 1 || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
reorder(index, _items.length);
|
||||
await saveCardOrder();
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
}
|
||||
|
||||
Future<void> loadCards() async {
|
||||
if (!mounted) return;
|
||||
final accounts = widget.accountListViewModel.accounts;
|
||||
final unnamedAccount = S.of(context).unnamed_account;
|
||||
final resolvedCardWidth = cardWidth;
|
||||
final walletCurrency = widget.dashboardViewModel.wallet.currency;
|
||||
final styleSettings = await BalanceCardStyleSettings.getAll(_walletInfoId);
|
||||
final layout = BalanceCardLayout.resolve(
|
||||
accountIndices: accounts.map((account) => account.id).toList(),
|
||||
settings: styleSettings,
|
||||
);
|
||||
|
||||
final List<AccountCustomizerListItem> newItems = [];
|
||||
for (int position = 0; position < layout.visible.length; position++) {
|
||||
final accountIndex = layout.visible[position];
|
||||
final account = accounts.firstWhereOrNull((item) => item.id == accountIndex);
|
||||
|
||||
if (account == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final setting = layout.settingFor(accountIndex);
|
||||
final isFrontCard = position == layout.visible.length - 1;
|
||||
final accountLabel = account.label.trim().isEmpty ? unnamedAccount : account.label;
|
||||
|
||||
newItems.add(
|
||||
AccountCustomizerListItem(
|
||||
card: BalanceCard(
|
||||
accountName: "${account.id + 1}. $accountLabel",
|
||||
accountBalance: account.balance,
|
||||
fiatBalance:
|
||||
widget.dashboardViewModel.balanceViewModel.accountFiatBalance(account) ?? "",
|
||||
designSwitchDuration: Duration.zero,
|
||||
assetName: _assetName,
|
||||
capitalizeAssetName: _capitalizeAssetName,
|
||||
onCustomizeTapped: isFrontCard ? _openCardCustomizer : null,
|
||||
selected: isFrontCard,
|
||||
width: resolvedCardWidth,
|
||||
design: CardDesign.fromStyleSettings(setting, walletCurrency),
|
||||
),
|
||||
accountListItem: account,
|
||||
settings: setting,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_items
|
||||
..clear()
|
||||
..addAll(newItems);
|
||||
_hasArchivedAccounts = layout.hidden.isNotEmpty;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (layout.needsRepair) {
|
||||
await BalanceCardStyleSettings.setVisibleOrder(_walletInfoId, layout.orders);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).accounts,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingWidget: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
ModernButton(
|
||||
semanticLabel: S.of(context).accounts_help,
|
||||
icon: const Icon(Icons.question_mark),
|
||||
size: 36,
|
||||
iconSize: 19,
|
||||
onPressed: () => AccountEducationPage(
|
||||
settingsStore: widget.dashboardViewModel.settingsStore,
|
||||
).show(context),
|
||||
),
|
||||
if (_isMultiAccountsEnabled)
|
||||
ModernButton.svg(
|
||||
semanticLabel: S.of(context).archived_accounts,
|
||||
svgPath: "assets/new-ui/archived.svg",
|
||||
size: 36,
|
||||
iconSize: 19,
|
||||
backgroundColor:
|
||||
_hasArchivedAccounts ? Theme.of(context).colorScheme.primary : null,
|
||||
iconColor:
|
||||
_hasArchivedAccounts ? Theme.of(context).colorScheme.onPrimary : null,
|
||||
onPressed: _openArchivedAccounts,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.dashboardViewModel.canToggleMultiAccounts)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: NewListSections(sections: {
|
||||
"": [
|
||||
ListItemToggle(
|
||||
keyValue: S.of(context).multiple_accounts,
|
||||
label: S.of(context).multiple_accounts,
|
||||
value: _isMultiAccountsEnabled,
|
||||
onChanged: (value) async {
|
||||
await widget.dashboardViewModel.setMultiAccountsEnabled(value);
|
||||
if (mounted) await _initialize();
|
||||
},
|
||||
),
|
||||
],
|
||||
}),
|
||||
),
|
||||
if (_isMultiAccountsEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 18),
|
||||
child: Text(
|
||||
S.of(context).account_customizer_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: !_isMultiAccountsEnabled
|
||||
? const SizedBox.shrink()
|
||||
: _loading
|
||||
? const Center(child: CupertinoActivityIndicator())
|
||||
: _items.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _AccountCards(
|
||||
items: _items,
|
||||
cardWidth: cardWidth,
|
||||
onReorder: reorder,
|
||||
onAddAccount: _showAddAccountModal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
bool _checkReadyToManage() {
|
||||
if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).wallet_is_syncing,
|
||||
alertContent: S.of(context).cannot_manage_accounts_during_sync,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: Navigator.of(context).pop,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _showAddAccountModal() async {
|
||||
if (!_checkReadyToManage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final modal = AccountCreationModal(viewModel: widget.accountEditOrCreateViewModel);
|
||||
final res = await showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
expand: true,
|
||||
enableDrag: false,
|
||||
builder: (_) => Material(child: modal),
|
||||
);
|
||||
if (res == true && mounted) {
|
||||
await widget.accountListViewModel.reload();
|
||||
final accounts = widget.accountListViewModel.accounts;
|
||||
if (accounts.isNotEmpty) {
|
||||
await widget.accountListViewModel.select(
|
||||
accounts.reduce((a, b) => a.id > b.id ? a : b),
|
||||
);
|
||||
}
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
await loadCards();
|
||||
await saveCardOrder();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openArchivedAccounts() async {
|
||||
await Navigator.of(context).push<void>(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
child: HiddenAccountsPage(
|
||||
accountListViewModel: widget.accountListViewModel,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
Future<void> _openCardCustomizer() async {
|
||||
if (!_checkReadyToManage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final account = _items.last.accountListItem;
|
||||
await widget.accountListViewModel.select(account);
|
||||
if (!mounted) return;
|
||||
|
||||
final archived = await CardCustomizer.show(
|
||||
context: context,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
account: account,
|
||||
canHide: _items.length > 1,
|
||||
asPage: true,
|
||||
);
|
||||
if (archived && _items.length > 1) {
|
||||
final nextAccount = _items[_items.length - 2].accountListItem;
|
||||
await widget.accountListViewModel.select(
|
||||
widget.accountListViewModel.accounts
|
||||
.firstWhereOrNull((item) => item.id == nextAccount.id) ??
|
||||
nextAccount,
|
||||
);
|
||||
}
|
||||
await widget.accountListViewModel.reload();
|
||||
await widget.dashboardViewModel.loadCardDesigns();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
void reorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final AccountCustomizerListItem item = _items.removeAt(oldIndex);
|
||||
_items.insert(newIndex, item);
|
||||
});
|
||||
|
||||
// necessary to copy all this to keep constant constructor for BalanceCard
|
||||
for (int i = 0; i < _items.length; i++) {
|
||||
_items[i] = AccountCustomizerListItem(
|
||||
card: BalanceCard(
|
||||
accountName: _items[i].card.accountName,
|
||||
balance: _items[i].card.balance,
|
||||
accountIndex: _items[i].card.accountIndex,
|
||||
accountBalance: _items[i].card.accountBalance,
|
||||
fiatBalance: _items[i].card.fiatBalance,
|
||||
assetName: _items[i].card.assetName,
|
||||
capitalizeAssetName: _items[i].card.capitalizeAssetName,
|
||||
designSwitchDuration: _items[i].card.designSwitchDuration,
|
||||
onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null,
|
||||
selected: i == _items.length - 1,
|
||||
width: _items[i].card.width,
|
||||
design: _items[i].card.design,
|
||||
),
|
||||
accountListItem: _items[i].accountListItem,
|
||||
settings: _items[i].settings,
|
||||
);
|
||||
}
|
||||
|
||||
if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
|
||||
widget.accountListViewModel.select(_items[_items.length - 1].accountListItem);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveCardOrder() async {
|
||||
if (!_isMultiAccountsEnabled) return;
|
||||
await BalanceCardStyleSettings.setOrder(_walletInfoId, {
|
||||
for (int position = 0; position < _items.length; position++)
|
||||
_items[position].accountListItem.id: position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountCards extends StatelessWidget {
|
||||
const _AccountCards({
|
||||
required this.items,
|
||||
required this.cardWidth,
|
||||
required this.onReorder,
|
||||
required this.onAddAccount,
|
||||
});
|
||||
|
||||
static const double _kStackVisibleFactor = 0.2;
|
||||
|
||||
final List<AccountCustomizerListItem> items;
|
||||
final double cardWidth;
|
||||
final ReorderCallback onReorder;
|
||||
final VoidCallback onAddAccount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Stack(
|
||||
children: [
|
||||
ReorderableListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 196),
|
||||
scrollController: ModalScrollController.of(context),
|
||||
onReorder: onReorder,
|
||||
proxyDecorator: (child, index, animation) => AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, _) {
|
||||
final animValue = Curves.easeOutCubic.transform(animation.value);
|
||||
final scale = lerpDouble(1, 1.05, animValue)!;
|
||||
|
||||
return Opacity(
|
||||
opacity: 1 - animValue.clamp(0.0, 0.1),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: cardWidth,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: items[index].card,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final selectedItemIndex = items.length - 1;
|
||||
|
||||
return Container(
|
||||
key: ValueKey(items[index].accountListItem.id),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: selectedItemIndex == index,
|
||||
label: "${items[index].accountListItem.id + 1}. "
|
||||
"${items[index].accountListItem.label.trim().isEmpty ? S.of(context).unnamed_account : items[index].accountListItem.label}",
|
||||
onTap: () => onReorder(index, items.length),
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTap: () {
|
||||
onReorder(index, items.length);
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: _kStackVisibleFactor,
|
||||
child: items[index].card,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
onTap: onAddAccount,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add,
|
||||
size: 28,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
S.of(context).add_account,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import "package:hash_wallet/core/wallet_loading_service.dart";
|
||||
import "package:hash_wallet/store/app_store.dart";
|
||||
import "package:cw_core/exceptions.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
|
||||
class WalletSwitchService {
|
||||
WalletSwitchService({
|
||||
required WalletLoadingService walletLoadingService,
|
||||
required AppStore appStore,
|
||||
}) : _walletLoadingService = walletLoadingService,
|
||||
_appStore = appStore;
|
||||
|
||||
final AppStore _appStore;
|
||||
final WalletLoadingService _walletLoadingService;
|
||||
|
||||
Future<void> switchToWallet(WalletInfo walletInfo) async {
|
||||
final wallet = await _walletLoadingService.load(walletInfo.type, walletInfo.name);
|
||||
|
||||
// load() recovers from a corrupted wallet by opening any other wallet it can,
|
||||
// so the one it returns may not be the one that was requested.
|
||||
if (wallet.name != walletInfo.name || wallet.type != walletInfo.type) {
|
||||
throw WalletSwitchException(
|
||||
"wallet switch loaded ${wallet.name} instead of ${walletInfo.name}",
|
||||
);
|
||||
}
|
||||
|
||||
await _appStore.changeCurrentWallet(wallet);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,51 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:hash_wallet/monero/monero.dart';
|
||||
import 'package:hash_wallet/wownero/wownero.dart';
|
||||
import "package:bloc/bloc.dart";
|
||||
import "package:hash_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:hash_wallet/bitcoin/bitcoin.dart";
|
||||
import "package:hash_wallet/monero/monero.dart";
|
||||
import "package:hash_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import 'package:flutter/src/painting/gradient.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import "package:flutter/src/painting/gradient.dart";
|
||||
import "package:meta/meta.dart";
|
||||
|
||||
part 'card_customizer_event.dart';
|
||||
part 'card_customizer_state.dart';
|
||||
part "card_customizer_event.dart";
|
||||
part "card_customizer_state.dart";
|
||||
|
||||
class CardCustomizerBlocParams {
|
||||
CardCustomizerBlocParams({required this.lightningMode, required this.canHide, this.amountDisplayMode});
|
||||
|
||||
final bool lightningMode;
|
||||
final BitcoinAmountDisplayMode? amountDisplayMode;
|
||||
final bool canHide;
|
||||
}
|
||||
|
||||
class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState> {
|
||||
final WalletBase _wallet;
|
||||
final bool lightningMode;
|
||||
final bool displaySats;
|
||||
|
||||
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false})
|
||||
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false, this.canHide = false})
|
||||
: super(CardCustomizerNotLoaded(
|
||||
0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0)) {
|
||||
0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0,),) {
|
||||
on<_Init>(_init);
|
||||
on<CardDesignSelected>(_onDesignSelected);
|
||||
on<ColorSelected>(_onColorSelected);
|
||||
on<IconStyleSelected>(_onIconStyleSelected);
|
||||
on<DesignSaved>(_onDesignSaved);
|
||||
on<AccountHidden>(_onAccountHidden);
|
||||
on<AccountNameChanged>(_onAccountNameChanged);
|
||||
|
||||
add(_Init());
|
||||
}
|
||||
|
||||
final WalletBase _wallet;
|
||||
final bool lightningMode;
|
||||
final bool displaySats;
|
||||
final bool canHide;
|
||||
|
||||
bool get _accountNameEnabled => !lightningMode &&
|
||||
(_wallet.hasNativeAccounts || _wallet.walletInfo.isMultiAccountsEnabled == true);
|
||||
|
||||
List<Gradient> _updateAvailableColors(CardDesign currentDesign) {
|
||||
final list = List<Gradient>.from(CardDesign.allGradients, growable: true);
|
||||
if (CardDesign.specialDesignsForCurrencies[_wallet.currency] != null) {
|
||||
@@ -37,139 +54,245 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
|
||||
return list;
|
||||
}
|
||||
|
||||
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) async {
|
||||
return (await BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex));
|
||||
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) =>
|
||||
BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex);
|
||||
|
||||
Future<int> _cardOrderFor(BalanceCardStyleSettings? currentDesignSettings) async {
|
||||
if (currentDesignSettings != null) {
|
||||
return currentDesignSettings.cardOrder;
|
||||
}
|
||||
|
||||
var highest = -1;
|
||||
for (final setting in await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId)) {
|
||||
if (setting.cardOrder > highest) {
|
||||
highest = setting.cardOrder;
|
||||
}
|
||||
}
|
||||
return highest + 1;
|
||||
}
|
||||
|
||||
List<CardDesign> _initAvailableDesigns({bool lightningMode = false}) {
|
||||
final List<CardDesign> ret = List<CardDesign>.empty(growable: true);
|
||||
final curr = lightningMode ? CryptoCurrency.btcln : _wallet.currency;
|
||||
|
||||
ret.add(CardDesign.gradientOnlyDesign);
|
||||
ret.add(CardDesign.forCurrencyIcon(curr));
|
||||
|
||||
if (CardDesign.specialDesignsForCurrencies[curr] != null)
|
||||
if (CardDesign.specialDesignsForCurrencies[curr] != null) {
|
||||
ret.add(CardDesign.forCurrencySpecial(curr));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int _initSelectedDesign(CardDesign currentDesign) {
|
||||
if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgIcon)
|
||||
if (currentDesign.backgroundType == CardDesignBackgroundTypes.gradientOnly) {
|
||||
return 0;
|
||||
else if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgFull)
|
||||
}
|
||||
if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgIcon) {
|
||||
return 1;
|
||||
else
|
||||
}
|
||||
if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgFull) {
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int _initSelectedIconIndex(
|
||||
BalanceCardStyleSettings? settings,
|
||||
List<CardIconPath> availableIconPaths,
|
||||
) {
|
||||
if (settings == null || availableIconPaths.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
if (settings.iconStyleIndex >= availableIconPaths.length) {
|
||||
return 0;
|
||||
}
|
||||
return settings.iconStyleIndex;
|
||||
}
|
||||
|
||||
int _initSelectedColor(CardDesign currentDesign) {
|
||||
int ret = CardDesign.allGradients.indexOf(currentDesign.gradient);
|
||||
if (ret == -1) {
|
||||
// special design with its own color. select last color in list.
|
||||
return CardDesign.allGradients.length;
|
||||
} else if (ret == -1) {
|
||||
// no color selected, select default.
|
||||
return 0;
|
||||
} else {
|
||||
// select whatever's selected.
|
||||
return ret;
|
||||
}
|
||||
final ret = CardDesign.allGradients.indexOf(currentDesign.gradient);
|
||||
return ret == -1 ? CardDesign.allGradients.length : ret;
|
||||
}
|
||||
|
||||
void _init(_Init event, Emitter<CardCustomizerState> emit) async {
|
||||
late final account;
|
||||
if (_wallet.type == WalletType.monero) {
|
||||
account = monero!.getCurrentAccount(_wallet);
|
||||
} else if (_wallet.type == WalletType.wownero) {
|
||||
account = wownero!.getCurrentAccount(_wallet);
|
||||
} else {
|
||||
account = null;
|
||||
}
|
||||
final accountName = (account?.label ?? "") as String;
|
||||
late final int accountIndex;
|
||||
if (account != null) {
|
||||
accountIndex = account.id as int;
|
||||
} else if (lightningMode) {
|
||||
accountIndex = 0;
|
||||
} else {
|
||||
accountIndex = -1;
|
||||
}
|
||||
Future<void> _init(_Init event, Emitter<CardCustomizerState> emit) async {
|
||||
final accountInfo = await _getCurrentAccountInfo();
|
||||
final accountName = accountInfo.accountName;
|
||||
final accountIndex = accountInfo.accountIndex;
|
||||
final curr = lightningMode ? CryptoCurrency.btcln : _wallet.currency;
|
||||
final currentDesignSettings = await _loadCurrentDesignSettings(accountIndex);
|
||||
final currentDesign = CardDesign.fromStyleSettings(
|
||||
currentDesignSettings, lightningMode ? CryptoCurrency.btcln : _wallet.currency);
|
||||
final currentDesign = CardDesign.fromStyleSettings(currentDesignSettings, curr);
|
||||
final availableDesigns = _initAvailableDesigns(lightningMode: lightningMode);
|
||||
final availableColors = _updateAvailableColors(currentDesign);
|
||||
final selectedDesign = _initSelectedDesign(currentDesign);
|
||||
final selectedDesignIndex = _initSelectedDesign(currentDesign);
|
||||
final selectedColor = _initSelectedColor(currentDesign);
|
||||
final availableIconPaths = CardDesign.iconPathsForWalletType(curr);
|
||||
final selectedIconIndex = _initSelectedIconIndex(currentDesignSettings, availableIconPaths);
|
||||
|
||||
emit(CardCustomizerInitial(selectedDesign, selectedColor, availableDesigns, availableColors,
|
||||
accountName, accountIndex, displaySats, currentDesignSettings?.cardOrder ?? 0));
|
||||
emit(CardCustomizerInitial(
|
||||
selectedDesignIndex,
|
||||
selectedColor,
|
||||
availableDesigns,
|
||||
availableColors,
|
||||
accountName,
|
||||
accountIndex,
|
||||
displaySats,
|
||||
await _cardOrderFor(currentDesignSettings),
|
||||
availableIconPaths: availableIconPaths,
|
||||
selectedIconIndex: selectedIconIndex,),);
|
||||
}
|
||||
|
||||
Future<({String accountName, int accountIndex})> _getCurrentAccountInfo() async {
|
||||
if (lightningMode) {
|
||||
return (accountName: "", accountIndex: -2);
|
||||
}
|
||||
|
||||
final account = await _resolveCurrentAccount();
|
||||
|
||||
if (!_accountNameEnabled) {
|
||||
return (accountName: "", accountIndex: account.accountIndex);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
Future<({String accountName, int accountIndex})> _resolveCurrentAccount() async {
|
||||
if (_wallet.type == WalletType.monero) {
|
||||
final account = monero!.getCurrentAccount(_wallet);
|
||||
return (accountName: account.label, accountIndex: account.id);
|
||||
}
|
||||
|
||||
if (_wallet.type == WalletType.wownero) {
|
||||
final account = wow.wownero!.getCurrentAccount(_wallet);
|
||||
return (accountName: account.label, accountIndex: account.id);
|
||||
}
|
||||
|
||||
if (_wallet.type == WalletType.bitcoin) {
|
||||
final account = await bitcoin!.getCurrentAccount(_wallet);
|
||||
return (accountName: account.label, accountIndex: account.id);
|
||||
}
|
||||
|
||||
return (accountName: "", accountIndex: -1);
|
||||
}
|
||||
|
||||
void _onDesignSelected(CardDesignSelected event, Emitter<CardCustomizerState> emit) {
|
||||
final newColors = _updateAvailableColors(state.availableDesigns[event.newDesignIndex]);
|
||||
late final int newColorIndex;
|
||||
if (newColors.length < state.availableColors.length) {
|
||||
if (newColors.isEmpty) {
|
||||
newColorIndex = 0;
|
||||
} else if (newColors.length < state.availableColors.length) {
|
||||
newColorIndex = 0;
|
||||
} else {
|
||||
newColorIndex = state.selectedColorIndex;
|
||||
newColorIndex = state.selectedColorIndex.clamp(0, newColors.length - 1);
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
selectedDesignIndex: event.newDesignIndex,
|
||||
availableColors: newColors,
|
||||
selectedColorIndex: newColorIndex));
|
||||
selectedColorIndex: newColorIndex,),);
|
||||
}
|
||||
|
||||
void _onColorSelected(ColorSelected event, Emitter<CardCustomizerState> emit) {
|
||||
emit(state.copyWith(selectedColorIndex: event.newColorIndex));
|
||||
}
|
||||
|
||||
void _onIconStyleSelected(IconStyleSelected event, Emitter<CardCustomizerState> emit) {
|
||||
emit(state.copyWith(selectedIconIndex: event.iconIndex));
|
||||
}
|
||||
|
||||
void _onAccountNameChanged(AccountNameChanged event, Emitter<CardCustomizerState> emit) {
|
||||
emit(state.copyWith(accountName: event.newAccountName));
|
||||
}
|
||||
|
||||
void _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) {
|
||||
BalanceCardStyleSettings.fromCardDesign(_wallet.walletInfo.internalId, state.accountIndex,
|
||||
state.cardOrder, state.selectedDesign)
|
||||
.insert()
|
||||
.then((value) {
|
||||
emit(CardCustomizerSaved(
|
||||
state.selectedDesignIndex,
|
||||
state.selectedColorIndex,
|
||||
state.availableDesigns,
|
||||
state.availableColors,
|
||||
state.accountName,
|
||||
state.accountIndex,
|
||||
state.displaySats,
|
||||
state.cardOrder));
|
||||
});
|
||||
saveAccountName();
|
||||
Future<void> _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) async {
|
||||
final currentSettings = await _loadCurrentDesignSettings(state.accountIndex);
|
||||
await BalanceCardStyleSettings.fromCardDesign(
|
||||
walletInfoId: _wallet.walletInfo.internalId,
|
||||
accountIndex: state.accountIndex,
|
||||
cardOrder: state.cardOrder,
|
||||
design: state.selectedDesign,
|
||||
iconStyleIndex: state.selectedIconIndex,
|
||||
gradientIndexOverride: state.selectedColorIndex,
|
||||
hidden: currentSettings?.hidden ?? false,
|
||||
).insert();
|
||||
await saveAccountName();
|
||||
|
||||
emit(
|
||||
CardCustomizerSaved(
|
||||
state.selectedDesignIndex,
|
||||
state.selectedColorIndex,
|
||||
state.availableDesigns,
|
||||
state.availableColors,
|
||||
state.accountName,
|
||||
state.accountIndex,
|
||||
state.displaySats,
|
||||
state.cardOrder,
|
||||
availableIconPaths: state.availableIconPaths,
|
||||
selectedIconIndex: state.selectedIconIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onAccountHidden(AccountHidden event, Emitter<CardCustomizerState> emit) async {
|
||||
await BalanceCardStyleSettings.fromCardDesign(
|
||||
walletInfoId: _wallet.walletInfo.internalId,
|
||||
accountIndex: state.accountIndex,
|
||||
cardOrder: state.cardOrder,
|
||||
design: state.selectedDesign,
|
||||
iconStyleIndex: state.selectedIconIndex,
|
||||
gradientIndexOverride: state.selectedColorIndex,
|
||||
hidden: true,
|
||||
).insert();
|
||||
|
||||
emit(
|
||||
CardCustomizerSaved(
|
||||
state.selectedDesignIndex,
|
||||
state.selectedColorIndex,
|
||||
state.availableDesigns,
|
||||
state.availableColors,
|
||||
state.accountName,
|
||||
state.accountIndex,
|
||||
state.displaySats,
|
||||
state.cardOrder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveAccountName() async {
|
||||
if (!_accountNameEnabled) return;
|
||||
|
||||
if (_wallet.type == WalletType.monero) {
|
||||
await saveMoneroAccountName();
|
||||
}
|
||||
|
||||
if (_wallet.type == WalletType.wownero) {
|
||||
} else if (_wallet.type == WalletType.wownero) {
|
||||
await saveWowneroAccountName();
|
||||
} else if (_wallet.type == WalletType.bitcoin) {
|
||||
await saveBitcoinAccountName();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveMoneroAccountName() async {
|
||||
final MoneroAccountList moneroAccountList = monero!.getAccountList(_wallet);
|
||||
await moneroAccountList.setLabelAccount(_wallet,
|
||||
accountIndex: state.accountIndex, label: state.accountName);
|
||||
accountIndex: state.accountIndex, label: state.accountName,);
|
||||
|
||||
await _wallet.save();
|
||||
}
|
||||
|
||||
Future<void> saveWowneroAccountName() async {
|
||||
final WowneroAccountList wowneroAccountList = wownero!.getAccountList(_wallet);
|
||||
await wowneroAccountList.setLabelAccount(_wallet,
|
||||
accountIndex: state.accountIndex, label: state.accountName);
|
||||
final accountList = wow.wownero!.getAccountList(_wallet);
|
||||
await accountList.setLabelAccount(
|
||||
_wallet,
|
||||
accountIndex: state.accountIndex,
|
||||
label: state.accountName,
|
||||
);
|
||||
|
||||
await _wallet.save();
|
||||
}
|
||||
Future<void> saveBitcoinAccountName() async {
|
||||
await _wallet.walletInfo.renameAccount(
|
||||
accountIndex: state.accountIndex,
|
||||
label: state.accountName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
part of 'card_customizer_bloc.dart';
|
||||
part of "card_customizer_bloc.dart";
|
||||
|
||||
@immutable
|
||||
sealed class CardCustomizerEvent {}
|
||||
@@ -6,21 +6,29 @@ sealed class CardCustomizerEvent {}
|
||||
class _Init extends CardCustomizerEvent {}
|
||||
|
||||
class CardDesignSelected extends CardCustomizerEvent {
|
||||
final int newDesignIndex;
|
||||
|
||||
CardDesignSelected(this.newDesignIndex);
|
||||
final int newDesignIndex;
|
||||
}
|
||||
|
||||
class ColorSelected extends CardCustomizerEvent {
|
||||
final int newColorIndex;
|
||||
|
||||
ColorSelected(this.newColorIndex);
|
||||
final int newColorIndex;
|
||||
}
|
||||
|
||||
class AccountNameChanged extends CardCustomizerEvent {
|
||||
final String newAccountName;
|
||||
|
||||
AccountNameChanged(this.newAccountName);
|
||||
final String newAccountName;
|
||||
}
|
||||
|
||||
class DesignSaved extends CardCustomizerEvent {}
|
||||
|
||||
class IconStyleSelected extends CardCustomizerEvent {
|
||||
|
||||
IconStyleSelected(this.iconIndex);
|
||||
final int iconIndex;
|
||||
}
|
||||
|
||||
class AccountHidden extends CardCustomizerEvent {}
|
||||
|
||||
@@ -9,12 +9,40 @@ sealed class CardCustomizerState {
|
||||
final bool displaySats;
|
||||
final List<CardDesign> availableDesigns;
|
||||
final List<Gradient> availableColors;
|
||||
final List<CardIconPath> availableIconPaths;
|
||||
final int selectedIconIndex;
|
||||
|
||||
CardCustomizerState(this.selectedDesignIndex, this.selectedColorIndex, this.availableDesigns,
|
||||
this.availableColors, this.accountName, this.accountIndex, this.displaySats, this.cardOrder);
|
||||
CardCustomizerState(
|
||||
this.selectedDesignIndex,
|
||||
this.selectedColorIndex,
|
||||
this.availableDesigns,
|
||||
this.availableColors,
|
||||
this.accountName,
|
||||
this.accountIndex,
|
||||
this.displaySats,
|
||||
this.cardOrder, {
|
||||
this.availableIconPaths = const [],
|
||||
this.selectedIconIndex = 0,
|
||||
});
|
||||
|
||||
CardDesign get selectedDesign {
|
||||
return availableDesigns[selectedDesignIndex].withGradient(selectedColor);
|
||||
final gradient = selectedColor;
|
||||
final baseDesign = availableDesigns[selectedDesignIndex];
|
||||
|
||||
CardDesign design;
|
||||
if (CardDesign.preferredColorCombinations.containsKey(gradient)) {
|
||||
design = baseDesign.withGradient(gradient);
|
||||
} else {
|
||||
final specialDesign = availableDesigns.length > 2 ? availableDesigns.last : null;
|
||||
final textColors = specialDesign?.colors ?? baseDesign.colors;
|
||||
design = baseDesign.withGradientAndColorCombination(gradient, textColors);
|
||||
}
|
||||
|
||||
if (design.backgroundType == CardDesignBackgroundTypes.svgIcon &&
|
||||
availableIconPaths.isNotEmpty) {
|
||||
design = design.withIcon(availableIconPaths[selectedIconIndex]);
|
||||
}
|
||||
return design;
|
||||
}
|
||||
|
||||
CardCustomizerState copyWith({
|
||||
@@ -25,6 +53,8 @@ sealed class CardCustomizerState {
|
||||
String? accountName,
|
||||
int? accountIndex,
|
||||
int? cardOrder,
|
||||
List<CardIconPath>? availableIconPaths,
|
||||
int? selectedIconIndex,
|
||||
});
|
||||
|
||||
Gradient get selectedColor => availableColors[selectedColorIndex];
|
||||
@@ -49,7 +79,9 @@ final class CardCustomizerNotLoaded extends CardCustomizerState {
|
||||
List<Gradient>? availableColors,
|
||||
String? accountName,
|
||||
int? accountIndex,
|
||||
int? cardOrder}) {
|
||||
int? cardOrder,
|
||||
List<CardIconPath>? availableIconPaths,
|
||||
int? selectedIconIndex}) {
|
||||
// this is never gonna be copied. it's near-instantly replaced with initial
|
||||
throw UnimplementedError();
|
||||
}
|
||||
@@ -64,9 +96,12 @@ final class CardCustomizerInitial extends CardCustomizerState {
|
||||
String accountName,
|
||||
int accountIndex,
|
||||
bool displaySats,
|
||||
int cardOrder,
|
||||
) : super(selectedDesignIndex, selectedColorIndex, availableDesigns, availableColors, accountName,
|
||||
accountIndex, displaySats, cardOrder);
|
||||
int cardOrder, {
|
||||
List<CardIconPath> availableIconPaths = const [],
|
||||
int selectedIconIndex = 0,
|
||||
}) : super(selectedDesignIndex, selectedColorIndex, availableDesigns, availableColors,
|
||||
accountName, accountIndex, displaySats, cardOrder,
|
||||
availableIconPaths: availableIconPaths, selectedIconIndex: selectedIconIndex);
|
||||
|
||||
CardCustomizerInitial copyWith({
|
||||
int? selectedDesignIndex,
|
||||
@@ -77,6 +112,8 @@ final class CardCustomizerInitial extends CardCustomizerState {
|
||||
int? accountIndex,
|
||||
bool? displaySats,
|
||||
int? cardOrder,
|
||||
List<CardIconPath>? availableIconPaths,
|
||||
int? selectedIconIndex,
|
||||
}) {
|
||||
return CardCustomizerInitial(
|
||||
selectedDesignIndex ?? this.selectedDesignIndex,
|
||||
@@ -87,6 +124,8 @@ final class CardCustomizerInitial extends CardCustomizerState {
|
||||
accountIndex ?? this.accountIndex,
|
||||
displaySats ?? this.displaySats,
|
||||
cardOrder ?? this.cardOrder,
|
||||
availableIconPaths: availableIconPaths ?? this.availableIconPaths,
|
||||
selectedIconIndex: selectedIconIndex ?? this.selectedIconIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -100,7 +139,9 @@ final class CardCustomizerSaved extends CardCustomizerState {
|
||||
super.accountName,
|
||||
super.accountIndex,
|
||||
super.displaySats,
|
||||
super.cardOrder);
|
||||
super.cardOrder,
|
||||
{super.availableIconPaths,
|
||||
super.selectedIconIndex});
|
||||
|
||||
@override
|
||||
CardCustomizerState copyWith(
|
||||
@@ -111,7 +152,9 @@ final class CardCustomizerSaved extends CardCustomizerState {
|
||||
String? accountName,
|
||||
int? accountIndex,
|
||||
bool? displaySats,
|
||||
int? cardOrder}) {
|
||||
int? cardOrder,
|
||||
List<CardIconPath>? availableIconPaths,
|
||||
int? selectedIconIndex}) {
|
||||
return CardCustomizerSaved(
|
||||
selectedDesignIndex ?? this.selectedDesignIndex,
|
||||
selectedColorIndex ?? this.selectedColorIndex,
|
||||
@@ -121,6 +164,8 @@ final class CardCustomizerSaved extends CardCustomizerState {
|
||||
accountIndex ?? this.accountIndex,
|
||||
displaySats ?? this.displaySats,
|
||||
cardOrder ?? this.cardOrder,
|
||||
availableIconPaths: availableIconPaths ?? this.availableIconPaths,
|
||||
selectedIconIndex: selectedIconIndex ?? this.selectedIconIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:bloc/bloc.dart";
|
||||
import "package:bloc_concurrency/bloc_concurrency.dart";
|
||||
import "package:hash_wallet/core/utilities.dart";
|
||||
import "package:hash_wallet/entities/fiat_api_mode.dart";
|
||||
import "package:hash_wallet/entities/fiat_currency.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/charts_asset.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/price_store.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/chart_range.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_data.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_data_sort_criteria.dart";
|
||||
import "package:hash_wallet/store/app_store.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
part "charts_event.dart";
|
||||
|
||||
part "charts_state.dart";
|
||||
|
||||
class ChartsBloc extends Bloc<ChartsEvent, ChartsState> {
|
||||
ChartsBloc({required this.priceStore, required this.appStore}) : super(const ChartsInitial()) {
|
||||
on<RangeChanged>(_onRangeChanged, transformer: sequential());
|
||||
on<SortingCriteriumChanged>(_onSortingCriteriumChanged);
|
||||
on<CurrencyAdded>(_onCurrencyAdded, transformer: sequential());
|
||||
on<CurrencyRemoved>(_onCurrencyRemoved, transformer: sequential());
|
||||
on<CurrencyPinned>(_onCurrencyPinned, transformer: sequential());
|
||||
on<PageRefreshed>(_onPageRefreshed, transformer: sequential());
|
||||
on<PageLoadStarted>(_onPageLoadStarted, transformer: restartable());
|
||||
on<Init>(_init, transformer: sequential());
|
||||
}
|
||||
|
||||
final PriceStore priceStore;
|
||||
final AppStore appStore;
|
||||
|
||||
Future<void> _init(Init event, Emitter<ChartsState> emit) async {
|
||||
|
||||
if(appStore.settingsStore.fiatApiMode == FiatApiMode.disabled) {
|
||||
emit(const ChartsDisabled());
|
||||
return;
|
||||
}
|
||||
|
||||
if (state is! ChartsInitial) {
|
||||
emit(const ChartsInitial());
|
||||
}
|
||||
|
||||
final assets = await ChartsAsset.get();
|
||||
|
||||
if (assets.isEmpty) {
|
||||
// generally this shouldn't happen, but i wanna make sure we can recover from a broken db
|
||||
assets.add(const ChartsAsset(asset: CryptoCurrency.btc, isFavorite: true));
|
||||
unawaited(assets.first.insert());
|
||||
}
|
||||
|
||||
if (assets.firstWhereOrNull((item) => item.isFavorite) == null) {
|
||||
assets.first = ChartsAsset(asset: assets.first.asset, isFavorite: true);
|
||||
unawaited(assets.first.insert());
|
||||
}
|
||||
|
||||
emit(
|
||||
ChartsLoading(
|
||||
pinnedCurrency: assets.firstWhere((item) => item.isFavorite).asset,
|
||||
currencies: assets.map((item) => item.asset).toList(),
|
||||
range: ChartRange.thirtyDays,
|
||||
sortCriterium: PriceDataSortCriterium.all.first,
|
||||
),
|
||||
);
|
||||
add(PageLoadStarted());
|
||||
}
|
||||
|
||||
Future<void> _onPageLoadStarted(PageLoadStarted event, Emitter<ChartsState> emit) async {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
final Map<CryptoCurrency, List<PriceData>> data = {};
|
||||
final currencies = s.currencies.toList();
|
||||
final range = s.range;
|
||||
for (final curr in currencies) {
|
||||
data[curr] = await priceStore.getPrices(appStore.settingsStore.fiatCurrency, curr, range);
|
||||
}
|
||||
emit(
|
||||
ChartsLoaded(
|
||||
pinnedCurrency: s.pinnedCurrency,
|
||||
prices: data,
|
||||
range: s.range,
|
||||
sortCriterium: s.sortCriterium,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
throw Exception("attempted price load without currency data");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRangeChanged(
|
||||
RangeChanged event,
|
||||
Emitter<ChartsState> emit,
|
||||
) async {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
emit(s.toLoading().copyWith(range: event.newRange));
|
||||
add(PageLoadStarted());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSortingCriteriumChanged(
|
||||
SortingCriteriumChanged event,
|
||||
Emitter<ChartsState> emit,
|
||||
) async {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
emit(s.copyWith(sortCriterium: event.newCriterium));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCurrencyAdded(
|
||||
CurrencyAdded event,
|
||||
Emitter<ChartsState> emit,
|
||||
) async {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
final newCurrencies = s.currencies..add(event.currency);
|
||||
await ChartsAsset(asset: event.currency, isFavorite: false).insert();
|
||||
emit(s.toLoading().copyWith(currencies: newCurrencies));
|
||||
add(PageLoadStarted());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCurrencyRemoved(
|
||||
CurrencyRemoved event,
|
||||
Emitter<ChartsState> emit,
|
||||
) async {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
final newCurrencies = s.currencies..remove(event.currency);
|
||||
|
||||
if (newCurrencies.isEmpty) {
|
||||
throw Exception(
|
||||
"removed the last currency ${kDebugMode ? "- your ui should block this! what did you do?" : ""}",
|
||||
);
|
||||
}
|
||||
|
||||
final CryptoCurrency newPin;
|
||||
if (s.pinnedCurrency == event.currency) {
|
||||
newPin = newCurrencies.first;
|
||||
await ChartsAsset(asset: s.pinnedCurrency, isFavorite: false).insert();
|
||||
await ChartsAsset(asset: newPin, isFavorite: true).insert();
|
||||
} else {
|
||||
newPin = s.pinnedCurrency;
|
||||
}
|
||||
|
||||
await ChartsAsset(asset: event.currency, isFavorite: false).remove();
|
||||
|
||||
emit(s.toLoading().copyWith(currencies: newCurrencies, pinnedCurrency: newPin));
|
||||
add(PageLoadStarted());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCurrencyPinned(
|
||||
CurrencyPinned event,
|
||||
Emitter<ChartsState> emit,
|
||||
) async {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
if (s.pinnedCurrency == event.currency) {
|
||||
return;
|
||||
}
|
||||
await ChartsAsset(asset: s.pinnedCurrency, isFavorite: false).insert();
|
||||
await ChartsAsset(asset: event.currency, isFavorite: true).insert();
|
||||
emit(s.toLoading().copyWith(pinnedCurrency: event.currency));
|
||||
add(PageLoadStarted());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPageRefreshed(
|
||||
PageRefreshed event,
|
||||
Emitter<ChartsState> emit,
|
||||
) async {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
emit(s.toLoading());
|
||||
add(PageLoadStarted());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
part of 'charts_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class ChartsEvent {
|
||||
const ChartsEvent();
|
||||
}
|
||||
|
||||
class RangeChanged extends ChartsEvent {
|
||||
const RangeChanged({required this.newRange});
|
||||
|
||||
final ChartRange newRange;
|
||||
}
|
||||
|
||||
class SortingCriteriumChanged extends ChartsEvent {
|
||||
const SortingCriteriumChanged({required this.newCriterium});
|
||||
|
||||
final PriceDataSortCriterium newCriterium;
|
||||
}
|
||||
|
||||
class CurrencyAdded extends ChartsEvent {
|
||||
const CurrencyAdded({required this.currency});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
}
|
||||
|
||||
class CurrencyRemoved extends ChartsEvent {
|
||||
const CurrencyRemoved({required this.currency});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
}
|
||||
|
||||
class CurrencyPinned extends ChartsEvent {
|
||||
const CurrencyPinned({required this.currency});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
}
|
||||
|
||||
class PageRefreshed extends ChartsEvent {}
|
||||
|
||||
class PageLoadStarted extends ChartsEvent {}
|
||||
|
||||
class Init extends ChartsEvent {}
|
||||
@@ -0,0 +1,136 @@
|
||||
part of "charts_bloc.dart";
|
||||
|
||||
@immutable
|
||||
sealed class ChartsState {
|
||||
const ChartsState();
|
||||
}
|
||||
|
||||
final class ChartsInitial extends ChartsState {
|
||||
const ChartsInitial();
|
||||
}
|
||||
|
||||
final class ChartsDisabled extends ChartsState {
|
||||
const ChartsDisabled();
|
||||
}
|
||||
|
||||
abstract final class ChartsStateWithData extends ChartsState {
|
||||
const ChartsStateWithData({
|
||||
required this.pinnedCurrency,
|
||||
required this.range,
|
||||
required this.sortCriterium,
|
||||
});
|
||||
|
||||
final CryptoCurrency pinnedCurrency;
|
||||
|
||||
List<CryptoCurrency> get currencies;
|
||||
|
||||
final PriceDataSortCriterium sortCriterium;
|
||||
final ChartRange range;
|
||||
|
||||
Money? priceFor(CryptoCurrency curr) => null;
|
||||
|
||||
String get fiatTicker => "";
|
||||
|
||||
bool get hasSingleCurrency => currencies.length == 1;
|
||||
|
||||
ChartsStateWithData copyWith({
|
||||
CryptoCurrency? pinnedCurrency,
|
||||
ChartRange? range,
|
||||
PriceDataSortCriterium? sortCriterium,
|
||||
});
|
||||
|
||||
ChartsLoading toLoading() => ChartsLoading(
|
||||
pinnedCurrency: pinnedCurrency,
|
||||
currencies: currencies,
|
||||
range: range,
|
||||
sortCriterium: sortCriterium,
|
||||
);
|
||||
}
|
||||
|
||||
final class ChartsLoading extends ChartsStateWithData {
|
||||
const ChartsLoading({
|
||||
required super.pinnedCurrency,
|
||||
required this.currencies,
|
||||
required super.range,
|
||||
required super.sortCriterium,
|
||||
});
|
||||
|
||||
@override
|
||||
final List<CryptoCurrency> currencies;
|
||||
|
||||
@override
|
||||
ChartsLoading copyWith({
|
||||
CryptoCurrency? pinnedCurrency,
|
||||
List<CryptoCurrency>? currencies,
|
||||
ChartRange? range,
|
||||
PriceDataSortCriterium? sortCriterium,
|
||||
}) =>
|
||||
ChartsLoading(
|
||||
pinnedCurrency: pinnedCurrency ?? this.pinnedCurrency,
|
||||
currencies: currencies ?? this.currencies,
|
||||
range: range ?? this.range,
|
||||
sortCriterium: sortCriterium ?? this.sortCriterium,
|
||||
);
|
||||
}
|
||||
|
||||
final class ChartsLoaded extends ChartsStateWithData {
|
||||
const ChartsLoaded({
|
||||
required super.pinnedCurrency,
|
||||
required Map<CryptoCurrency, List<PriceData>> prices,
|
||||
required super.range,
|
||||
required super.sortCriterium,
|
||||
}) : _prices = prices;
|
||||
final Map<CryptoCurrency, List<PriceData>> _prices;
|
||||
|
||||
@override
|
||||
String get fiatTicker => _prices[_prices.keys.first]?.firstOrNull?.base.name ?? "";
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get currencies {
|
||||
final list = _prices.keys.toList();
|
||||
list.sort((a, b) => sortCriterium.comparator(changeDataFor(a), changeDataFor(b), a, b));
|
||||
return list;
|
||||
}
|
||||
|
||||
@override
|
||||
Money? priceFor(CryptoCurrency curr) => _prices[curr]?.lastOrNull?.quote;
|
||||
|
||||
PriceChangeData changeDataFor(CryptoCurrency curr) {
|
||||
final latestPrice = _prices[curr]?.lastOrNull?.quote ?? Money.zero(FiatCurrency.usd);
|
||||
final secondLatestPrice = _prices[curr]?.firstOrNull?.quote ?? Money.zero(FiatCurrency.usd);
|
||||
|
||||
final direction =
|
||||
latestPrice >= secondLatestPrice ? PriceChangeDirection.up : PriceChangeDirection.down;
|
||||
final double percentage;
|
||||
if(secondLatestPrice.isZero) {
|
||||
percentage = 0;
|
||||
} else {
|
||||
percentage =
|
||||
(((double.parse(latestPrice.toString()) - double.parse(secondLatestPrice.toString())) /
|
||||
double.parse(secondLatestPrice.toString())) * 100).abs();
|
||||
}
|
||||
final amount = (latestPrice - secondLatestPrice).abs();
|
||||
|
||||
return PriceChangeData(
|
||||
direction: direction,
|
||||
amount: amount,
|
||||
percentage: percentage.toStringAsFixed(2),
|
||||
);
|
||||
}
|
||||
|
||||
List<PriceData> dataFor(CryptoCurrency curr) => _prices[curr] ?? [];
|
||||
|
||||
@override
|
||||
ChartsLoaded copyWith({
|
||||
CryptoCurrency? pinnedCurrency,
|
||||
Map<CryptoCurrency, List<PriceData>>? prices,
|
||||
ChartRange? range,
|
||||
PriceDataSortCriterium? sortCriterium,
|
||||
}) =>
|
||||
ChartsLoaded(
|
||||
pinnedCurrency: pinnedCurrency ?? this.pinnedCurrency,
|
||||
prices: prices ?? _prices,
|
||||
range: range ?? this.range,
|
||||
sortCriterium: sortCriterium ?? this.sortCriterium,
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/entities/lnurlpay_record.dart';
|
||||
import 'package:hash_wallet/core/address_resolver/lnurl_pay/lnurlpay_record.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:hash_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ArchiveConfirmationContent extends StatelessWidget {
|
||||
const ArchiveConfirmationContent({
|
||||
required this.account,
|
||||
required this.fiatBalance,
|
||||
required this.isFunded,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final String? fiatBalance;
|
||||
final bool isFunded;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isFunded) ...[
|
||||
Text(
|
||||
S.of(context).archive_account_funds_title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.customColors.warningOutlineColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AccountFundsSummary(
|
||||
account: account,
|
||||
fiatBalance: fiatBalance,
|
||||
borderColor: context.customColors.warningOutlineColor,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).archive_account_move_funds,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: context.customColors.warningOutlineColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
S.of(context).archive_account_funded_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
AccountSummary(account: account),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).archive_account_empty_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
S.of(context).archive_account_restore_hint,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class AccountSummary extends StatelessWidget {
|
||||
const AccountSummary({required this.account});
|
||||
|
||||
final AccountListItem account;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label = account.label.trim();
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/account.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "${account.id + 1}. ",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
TextSpan(
|
||||
text: label.isEmpty ? S.of(context).unnamed_account : label,
|
||||
),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountFundsSummary extends StatelessWidget {
|
||||
const AccountFundsSummary({
|
||||
required this.account,
|
||||
required this.fiatBalance,
|
||||
required this.borderColor,
|
||||
});
|
||||
|
||||
final AccountListItem account;
|
||||
final String? fiatBalance;
|
||||
final Color borderColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
border: Border.all(color: borderColor),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: account.balance.currency.iconPath ??
|
||||
"assets/new-ui/crypto_full_icons/${account.balance.currency.name.toLowerCase()}.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MoneyText(
|
||||
account.balance,
|
||||
fractionalDigits: account.balance.decimals,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (fiatBalance != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
fiatBalance!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import "package:hash_wallet/core/address_resolver/address_resolver_service.dart";
|
||||
import "package:hash_wallet/core/anypay/anypay_models.dart";
|
||||
import "package:hash_wallet/core/anypay/anypay_service.dart";
|
||||
import "package:hash_wallet/core/auth_service.dart";
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/evm/evm.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/pages/swap_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/network_decision_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
|
||||
import "package:hash_wallet/reactions/wallet_connect.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:hash_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_wallet/view_model/exchange/exchange_view_model.dart";
|
||||
import "package:hash_wallet/view_model/send/send_view_model.dart";
|
||||
import "package:hash_wallet/view_model/wallet_switcher_view_model.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/unspent_coin_type.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class AnyPayFormFill {
|
||||
const AnyPayFormFill({
|
||||
required this.request,
|
||||
this.token,
|
||||
this.fallbackCurrency,
|
||||
this.amountOverride,
|
||||
});
|
||||
|
||||
final AnyPayRequest request;
|
||||
final CryptoCurrency? token;
|
||||
final CryptoCurrency? fallbackCurrency;
|
||||
final String? amountOverride;
|
||||
}
|
||||
|
||||
class AnyPayFlow {
|
||||
AnyPayFlow({
|
||||
required this.anyPayService,
|
||||
required this.sendViewModel,
|
||||
required this.authService,
|
||||
required this.walletSwitcherViewModel,
|
||||
});
|
||||
|
||||
final AnyPayService anyPayService;
|
||||
final SendViewModel sendViewModel;
|
||||
final AuthService authService;
|
||||
final WalletSwitcherViewModel walletSwitcherViewModel;
|
||||
|
||||
bool _isSwitchingWallet = false;
|
||||
|
||||
bool get isSwitchingWallet => _isSwitchingWallet;
|
||||
|
||||
Future<AnyPayFormFill?> handleEvaluation(
|
||||
BuildContext context,
|
||||
AnyPayEvaluation evaluation, {
|
||||
CryptoCurrency? fallbackCurrency,
|
||||
}) async {
|
||||
final request = evaluation.request;
|
||||
|
||||
if (!request.rawInput.toLowerCase().startsWith("ethereum:") &&
|
||||
(request.rawInput.contains("@") || request.address.contains("@"))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await _handleDecision(context, evaluation, fallbackCurrency);
|
||||
} catch (e) {
|
||||
printV("Payment flow error: $e");
|
||||
return AnyPayFormFill(request: request);
|
||||
}
|
||||
}
|
||||
|
||||
Future<AnyPayFormFill?> _handleDecision(
|
||||
BuildContext context,
|
||||
AnyPayEvaluation evaluation,
|
||||
CryptoCurrency? fallbackCurrency,
|
||||
) async {
|
||||
final request = evaluation.request;
|
||||
|
||||
switch (evaluation.decision) {
|
||||
case final AnyPayApplyToCurrentWallet decision:
|
||||
if (request.isLightning) {
|
||||
_enterLightningMode();
|
||||
}
|
||||
return AnyPayFormFill(
|
||||
request: request,
|
||||
token: decision.token,
|
||||
fallbackCurrency: fallbackCurrency ?? decision.fallbackCurrency,
|
||||
amountOverride: decision.amountOverride,
|
||||
);
|
||||
case AnyPayEvmNetworkChoice():
|
||||
return _pickEvmNetwork(context, request);
|
||||
case final AnyPayCrossChainPayment decision:
|
||||
return _handleCrossChain(context, request, decision, fallbackCurrency);
|
||||
case final AnyPayUnsupportedNetwork decision:
|
||||
_showError(
|
||||
context,
|
||||
S.of(context).unsupported_network_requested(decision.chainId.toString()),
|
||||
);
|
||||
return null;
|
||||
case AnyPayUnsupportedToken():
|
||||
_showError(context, S.of(context).unsupported_token_requested);
|
||||
return null;
|
||||
case AnyPayEmptyInput():
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<AnyPayFormFill?> _pickEvmNetwork(BuildContext context, AnyPayRequest request) async {
|
||||
final chains = evm!.getAllChains();
|
||||
final networks = chains.map((chain) {
|
||||
final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? sendViewModel.wallet.type;
|
||||
return RecipientNetworkItem(
|
||||
chainId: chain.chainId,
|
||||
name: chain.name,
|
||||
iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
|
||||
chainBadgeIconPath: symbolIconPathForWalletType(walletType),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final selectedChainId =
|
||||
await EvmAddressDetectedSheet.show(context: context, networks: networks);
|
||||
if (!context.mounted || selectedChainId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final target = chains.firstWhere(
|
||||
(chain) => chain.chainId == selectedChainId,
|
||||
orElse: () => chains.first,
|
||||
);
|
||||
|
||||
final evaluation = await anyPayService.evaluateForEvmChain(request, target.chainId);
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
return handleEvaluation(context, evaluation, fallbackCurrency: target.currency);
|
||||
}
|
||||
|
||||
Future<AnyPayFormFill?> _handleCrossChain(
|
||||
BuildContext context,
|
||||
AnyPayRequest request,
|
||||
AnyPayCrossChainPayment decision,
|
||||
CryptoCurrency? fallbackCurrency,
|
||||
) async {
|
||||
final destinationType = decision.targetWalletType;
|
||||
final isEvmTarget = isEVMCompatibleChain(destinationType);
|
||||
final destinationNetworkName = networkDisplayName(destinationType, decision.targetChainId);
|
||||
final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
|
||||
|
||||
if (!decision.canSwap && !decision.hasCompatibleWallet) {
|
||||
_showError(
|
||||
context,
|
||||
S.of(context).swap_unavailable_needs_network_wallet(destinationNetworkName),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final currentType = sendViewModel.wallet.type;
|
||||
final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainId() : null;
|
||||
final currentNetworkName = networkDisplayName(currentType, currentChainId);
|
||||
final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
|
||||
|
||||
if (!decision.hasCompatibleWallet) {
|
||||
final swapConfirmTitle = isEvmTarget
|
||||
? S.of(context).swap_from_network(currentNetworkName)
|
||||
: S.of(context).network_address_detected(destinationNetworkName);
|
||||
final swapConfirmPrimary = isEvmTarget
|
||||
? S.of(context).continue_to_swap
|
||||
: S.of(context).swap_from_network(currentNetworkName);
|
||||
|
||||
await Navigator.of(context).push<void>(
|
||||
CupertinoPageRoute(
|
||||
builder: (pageContext) => NetworkDecisionPage(
|
||||
title: swapConfirmTitle,
|
||||
description: S.of(context).swap_from_network_description(
|
||||
destinationNetworkName,
|
||||
currentNetworkName,
|
||||
),
|
||||
destinationIconPath: destinationNetworkIcon,
|
||||
currentIconPath: currentNetworkIcon,
|
||||
primaryText: swapConfirmPrimary,
|
||||
primaryIconPath: isEvmTarget ? null : "assets/new-ui/swap_arrows.svg",
|
||||
onPrimary: () => _openSwap(pageContext, request, decision),
|
||||
secondaryText: S.of(context).cancel,
|
||||
),
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final decisionTitle = isEvmTarget
|
||||
? S.of(context).send_to_network(destinationNetworkName)
|
||||
: S.of(context).network_address_detected(destinationNetworkName);
|
||||
|
||||
final selectedWallet = await Navigator.of(context).push<WalletInfo>(
|
||||
CupertinoPageRoute(
|
||||
builder: (pageContext) => NetworkDecisionPage(
|
||||
title: decisionTitle,
|
||||
description: decision.canSwap
|
||||
? S.of(context).send_to_network_description(
|
||||
destinationNetworkName,
|
||||
currentNetworkName,
|
||||
)
|
||||
: S.of(context).swap_unavailable_switch_to_network(destinationNetworkName),
|
||||
destinationIconPath: destinationNetworkIcon,
|
||||
primaryText: S.of(context).switch_to_x_wallet(destinationNetworkName),
|
||||
primaryIconPath: "assets/new-ui/wallet_filled.svg",
|
||||
onPrimary: () => _pickDestinationWallet(pageContext, decision, destinationNetworkName),
|
||||
secondaryText: decision.canSwap
|
||||
? S.of(context).swap_from_network(currentNetworkName)
|
||||
: S.of(context).cancel,
|
||||
secondaryIconPath: decision.canSwap ? "assets/new-ui/swap_arrows.svg" : null,
|
||||
onSecondary: decision.canSwap ? () => _openSwap(pageContext, request, decision) : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedWallet == null || !context.mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _switchWallet(context, selectedWallet, request, decision, fallbackCurrency);
|
||||
}
|
||||
|
||||
Future<void> _pickDestinationWallet(
|
||||
BuildContext pageContext,
|
||||
AnyPayCrossChainPayment decision,
|
||||
String networkName,
|
||||
) async {
|
||||
WalletInfo? destinationWalletInfo;
|
||||
if (decision.wallets.length > 1) {
|
||||
destinationWalletInfo = await SwitchNetworkWalletPage.push(
|
||||
context: pageContext,
|
||||
networkName: networkName,
|
||||
targetIconPath: symbolIconPathForWalletType(decision.targetWalletType) ?? "",
|
||||
wallets: decision.wallets,
|
||||
);
|
||||
} else {
|
||||
destinationWalletInfo = decision.wallets.first;
|
||||
}
|
||||
|
||||
if (destinationWalletInfo == null || !pageContext.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(pageContext).pop(destinationWalletInfo);
|
||||
}
|
||||
|
||||
Future<AnyPayFormFill?> _switchWallet(
|
||||
BuildContext context,
|
||||
WalletInfo walletInfo,
|
||||
AnyPayRequest request,
|
||||
AnyPayCrossChainPayment decision,
|
||||
CryptoCurrency? fallbackCurrency,
|
||||
) async {
|
||||
if (request.isLightning) {
|
||||
_enterLightningMode();
|
||||
}
|
||||
|
||||
_isSwitchingWallet = true;
|
||||
BuildContext? loadingSheetContext;
|
||||
bool completedFlow = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted || completedFlow) {
|
||||
return;
|
||||
}
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (sheetContext) {
|
||||
if (completedFlow) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (Navigator.canPop(sheetContext)) {
|
||||
Navigator.of(sheetContext).pop();
|
||||
}
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
loadingSheetContext = sheetContext;
|
||||
return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
bool success = false;
|
||||
try {
|
||||
success = await anyPayService.switchWalletForPayment(
|
||||
walletInfo,
|
||||
chainId: decision.targetChainId,
|
||||
);
|
||||
} finally {
|
||||
_isSwitchingWallet = false;
|
||||
completedFlow = true;
|
||||
if (loadingSheetContext != null &&
|
||||
loadingSheetContext!.mounted &&
|
||||
Navigator.canPop(loadingSheetContext!)) {
|
||||
Navigator.of(loadingSheetContext!).pop();
|
||||
}
|
||||
loadingSheetContext = null;
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
_showError(context, S.of(context).network_switch_failed);
|
||||
return null;
|
||||
}
|
||||
|
||||
return AnyPayFormFill(
|
||||
request: request,
|
||||
token: decision.token,
|
||||
fallbackCurrency: fallbackCurrency ?? request.detection.detectedCurrency,
|
||||
amountOverride: decision.amountOverride,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openSwap(
|
||||
BuildContext presentContext,
|
||||
AnyPayRequest request,
|
||||
AnyPayCrossChainPayment decision,
|
||||
) async {
|
||||
if (!presentContext.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.hasContract && decision.token == null) {
|
||||
_showError(presentContext, S.of(presentContext).unsupported_token_requested);
|
||||
return;
|
||||
}
|
||||
|
||||
final intent = anyPayService.buildSwapIntent(
|
||||
request,
|
||||
targetWalletType: decision.targetWalletType,
|
||||
targetChainId: decision.targetChainId,
|
||||
token: decision.token,
|
||||
);
|
||||
|
||||
final page = NewSwapPage(
|
||||
getIt.get<ExchangeViewModel>(),
|
||||
authService,
|
||||
getIt.get<AddressResolverService>(),
|
||||
null,
|
||||
walletSwitcherViewModel: walletSwitcherViewModel,
|
||||
fromSend: SwapFromSendArgs.fromIntent(intent),
|
||||
balanceViewModel: sendViewModel.balanceViewModel,
|
||||
);
|
||||
await Navigator.of(presentContext).push<void>(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(bottom: false, child: page),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showError(BuildContext context, String message) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: message,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _enterLightningMode() {
|
||||
sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
|
||||
sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
|
||||
}
|
||||
|
||||
int _currentEvmChainId() {
|
||||
final wallet = sendViewModel.wallet;
|
||||
return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class EvmAddressDetectedSheet extends StatelessWidget {
|
||||
const EvmAddressDetectedSheet({super.key, required this.networks});
|
||||
|
||||
final List<RecipientNetworkItem> networks;
|
||||
|
||||
static Future<int?> show({
|
||||
required BuildContext context,
|
||||
required List<RecipientNetworkItem> networks,
|
||||
}) {
|
||||
return showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => EvmAddressDetectedSheet(networks: networks),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
StackedNetworkIcons(iconPaths: networks.map((n) => n.iconPath).toList()),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).evm_address_detected,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
S.of(context).select_recipient_network_for_tx,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NetworkListCard(
|
||||
rows: [
|
||||
for (final network in networks)
|
||||
RecipientNetworkListRow(
|
||||
item: network,
|
||||
imageOverride: network.chainBadgeIconPath,
|
||||
onTap: () => Navigator.of(context).pop(network.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StackedNetworkIcons extends StatelessWidget {
|
||||
const StackedNetworkIcons({
|
||||
required this.iconPaths,
|
||||
this.size = 50,
|
||||
this.step = 34,
|
||||
});
|
||||
|
||||
final List<String> iconPaths;
|
||||
final double size;
|
||||
final double step;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (iconPaths.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final width = size + (iconPaths.length - 1) * step;
|
||||
|
||||
return SizedBox(
|
||||
height: size,
|
||||
width: width,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (int i = 0; i < iconPaths.length; i++)
|
||||
Positioned(
|
||||
left: i * step,
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colors.surfaceContainer,
|
||||
border: Border.all(color: colors.surface, width: 2),
|
||||
),
|
||||
child: HashImageWidget(imageUrl: iconPaths[i], fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class NetworkDecisionPage extends StatelessWidget {
|
||||
const NetworkDecisionPage({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.destinationIconPath,
|
||||
required this.primaryText,
|
||||
required this.onPrimary,
|
||||
required this.secondaryText,
|
||||
this.currentIconPath,
|
||||
this.primaryIconPath,
|
||||
this.secondaryIconPath,
|
||||
this.onSecondary,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final String destinationIconPath;
|
||||
final String? currentIconPath;
|
||||
final String primaryText;
|
||||
final VoidCallback onPrimary;
|
||||
final String? primaryIconPath;
|
||||
final String secondaryText;
|
||||
final String? secondaryIconPath;
|
||||
final VoidCallback? onSecondary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final primaryIcon = primaryIconPath;
|
||||
final secondaryIcon = secondaryIconPath;
|
||||
return Material(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: _DecisionHeader(
|
||||
destinationIconPath: destinationIconPath,
|
||||
currentIconPath: currentIconPath,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
NewPrimaryButton(
|
||||
onPressed: onPrimary,
|
||||
image: primaryIcon != null
|
||||
? HashImageWidget(
|
||||
imageUrl: primaryIcon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
|
||||
)
|
||||
: null,
|
||||
text: primaryText,
|
||||
color: colors.primary,
|
||||
textColor: colors.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
NewPrimaryButton(
|
||||
onPressed: onSecondary ?? () => Navigator.of(context).maybePop(),
|
||||
image: secondaryIcon != null
|
||||
? HashImageWidget(
|
||||
imageUrl: secondaryIcon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
|
||||
)
|
||||
: null,
|
||||
text: secondaryText,
|
||||
color: colors.surfaceContainer,
|
||||
textColor: colors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DecisionHeader extends StatelessWidget {
|
||||
const _DecisionHeader({required this.destinationIconPath, this.currentIconPath});
|
||||
|
||||
final String destinationIconPath;
|
||||
final String? currentIconPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final current = currentIconPath;
|
||||
|
||||
if (current == null) {
|
||||
return HashImageWidget(
|
||||
imageUrl: destinationIconPath,
|
||||
width: 75,
|
||||
height: 75,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: current,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(current) ? colors.primary : null,
|
||||
),
|
||||
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
|
||||
HashImageWidget(
|
||||
imageUrl: destinationIconPath,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class RecipientNetworkSelector extends StatelessWidget {
|
||||
const RecipientNetworkSelector({
|
||||
required this.networkName,
|
||||
required this.networkIconPath,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String networkName;
|
||||
final String networkIconPath;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: "${S.of(context).network_prefix_on} $networkName",
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: ExcludeSemantics(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).network_prefix_on,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
HashImageWidget(
|
||||
imageUrl: networkIconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
fit: BoxFit.cover,
|
||||
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
networkName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.07,
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 18,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class RecipientNetworkItem {
|
||||
const RecipientNetworkItem({
|
||||
required this.chainId,
|
||||
required this.name,
|
||||
required this.iconPath,
|
||||
this.chainBadgeIconPath,
|
||||
});
|
||||
|
||||
final int chainId;
|
||||
final String name;
|
||||
final String iconPath;
|
||||
final String? chainBadgeIconPath;
|
||||
}
|
||||
|
||||
class SelectRecipientNetworkSheet extends StatelessWidget {
|
||||
const SelectRecipientNetworkSheet({
|
||||
super.key,
|
||||
required this.networks,
|
||||
required this.currentChainId,
|
||||
});
|
||||
|
||||
final List<RecipientNetworkItem> networks;
|
||||
final int currentChainId;
|
||||
|
||||
static Future<int?> show({
|
||||
required BuildContext context,
|
||||
required List<RecipientNetworkItem> networks,
|
||||
required int currentChainId,
|
||||
}) {
|
||||
return showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
enableDrag: false,
|
||||
isDismissible: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => SelectRecipientNetworkSheet(
|
||||
networks: networks,
|
||||
currentChainId: currentChainId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
RecipientNetworkItem? current;
|
||||
final compatible = <RecipientNetworkItem>[];
|
||||
for (final network in networks) {
|
||||
if (network.chainId == currentChainId && current == null) {
|
||||
current = network;
|
||||
} else {
|
||||
compatible.add(network);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/network_cube.svg",
|
||||
width: 75,
|
||||
height: 75,
|
||||
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).select_recipient_network,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
S.of(context).select_recipient_network_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (current != null) ...[
|
||||
NetworkListCard(
|
||||
rows: [
|
||||
RecipientNetworkListRow(
|
||||
item: current,
|
||||
subtitle: S.of(context).current_network,
|
||||
onTap: () => Navigator.of(context).pop(current!.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
if (compatible.isNotEmpty) ...[
|
||||
Text(
|
||||
S.of(context).compatible_networks,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.06),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
NetworkListCard(
|
||||
rows: [
|
||||
for (final network in compatible)
|
||||
RecipientNetworkListRow(
|
||||
item: network,
|
||||
onTap: () => Navigator.of(context).pop(network.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkListCard extends StatelessWidget {
|
||||
const NetworkListCard({required this.rows});
|
||||
|
||||
final List<Widget> rows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < rows.length; i++) ...[
|
||||
rows[i],
|
||||
if (i != rows.length - 1)
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: colors.surfaceContainerHigh,
|
||||
indent: 48,
|
||||
endIndent: 12,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RecipientNetworkListRow extends StatelessWidget {
|
||||
const RecipientNetworkListRow(
|
||||
{super.key, required this.item, required this.onTap, this.subtitle, this.imageOverride});
|
||||
|
||||
final String? subtitle;
|
||||
final String? imageOverride;
|
||||
final VoidCallback onTap;
|
||||
final RecipientNetworkItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: imageOverride ?? item.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
fit: BoxFit.cover,
|
||||
color: isMonochromeSymbolIcon(imageOverride ?? item.iconPath) ? colors.primary : null,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
item.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w400, letterSpacing: -0.07),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant, letterSpacing: -0.06, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, size: 16, color: colors.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/send_page/l2_action_wallet_selector.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SwitchNetworkWalletPage extends StatelessWidget {
|
||||
const SwitchNetworkWalletPage({
|
||||
required this.wallets,
|
||||
required this.networkName,
|
||||
required this.destinationIconPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String networkName;
|
||||
final String destinationIconPath;
|
||||
final List<WalletInfo> wallets;
|
||||
|
||||
static Future<WalletInfo?> push({
|
||||
required BuildContext context,
|
||||
required String networkName,
|
||||
required String targetIconPath,
|
||||
required List<WalletInfo> wallets,
|
||||
}) =>
|
||||
Navigator.of(context).push<WalletInfo>(
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => SwitchNetworkWalletPage(
|
||||
networkName: networkName,
|
||||
destinationIconPath: targetIconPath,
|
||||
wallets: wallets,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 124),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.arrow_forward, color: colors.primary, size: 32),
|
||||
const SizedBox(width: 8),
|
||||
HashImageWidget(
|
||||
imageUrl: destinationIconPath,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).switch_to_x_wallet(networkName),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).select_compatible_wallet,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: wallets.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (_, index) {
|
||||
final wallet = wallets[index];
|
||||
return WalletRow(
|
||||
currencyIconPath: getCryptoCurrencyIconForWalletListItem(wallet.type),
|
||||
walletName: wallet.name,
|
||||
onTap: () => Navigator.of(context).pop(wallet),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -60,67 +61,78 @@ class AppsWidget extends StatelessWidget {
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: onTap,
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: BorderSide(
|
||||
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
),
|
||||
padding: EdgeInsets.all(24),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
// The card is one node; the arrow glyph's "leaves the app" meaning
|
||||
// becomes a hint instead of an unlabeled icon.
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
hint: isLink == true ? S.of(context).opens_externally : null,
|
||||
child: TextButton(
|
||||
onPressed: onTap,
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: BorderSide(
|
||||
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
),
|
||||
padding: EdgeInsets.all(24),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: HashImageWidget(imageUrl: image, height: 54, width: 54),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 6.0,
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: ExcludeSemantics(
|
||||
child: HashImageWidget(imageUrl: image, height: 54, width: 54),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 6.0,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
SizedBox(),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
subTitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
SizedBox(),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
subTitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: Icon(
|
||||
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
)
|
||||
if (hint != null) ...[
|
||||
SizedBox(height: 10),
|
||||
hint!,
|
||||
]
|
||||
],
|
||||
),
|
||||
if (hint != null) ...[
|
||||
SizedBox(height: 10),
|
||||
hint!,
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -128,10 +140,16 @@ class AppsWidget extends StatelessWidget {
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: onClose,
|
||||
//color: Theme.of(context).colorScheme.onSurface,
|
||||
// Label the icon-only button without adding a visible tooltip.
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
label: S.of(context).close,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: onClose,
|
||||
//color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class BorderedSvgIcon extends StatelessWidget {
|
||||
const BorderedSvgIcon({
|
||||
required this.iconPath,
|
||||
required this.iconSize,
|
||||
Color? iconColor,
|
||||
Color? borderColor,
|
||||
this.borderSize = 6,
|
||||
super.key,
|
||||
}) : _iconColor = iconColor,
|
||||
_borderColor = borderColor;
|
||||
|
||||
final String iconPath;
|
||||
final Color? _iconColor;
|
||||
final double iconSize;
|
||||
final Color? _borderColor;
|
||||
final double borderSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = _iconColor ?? Theme.of(context).colorScheme.onSurface;
|
||||
final borderColor = _borderColor ?? Theme.of(context).colorScheme.surface;
|
||||
|
||||
return SizedBox(
|
||||
width: iconSize + borderSize * 2,
|
||||
height: iconSize + borderSize * 2,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: iconSize + borderSize * 2,
|
||||
height: iconSize + borderSize * 2,
|
||||
colorFilter: ColorFilter.mode(
|
||||
borderColor,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
colorFilter: ColorFilter.mode(
|
||||
iconColor,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,8 @@ class TransferHistoryRow extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: 'assets/images/crypto/${sourceChainName?.toLowerCase()}.webp',
|
||||
imageUrl:
|
||||
'assets/new-ui/crypto_full_icons/${sourceChainName?.toLowerCase()}.svg',
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
@@ -96,7 +97,8 @@ class TransferHistoryRow extends StatelessWidget {
|
||||
top: 14,
|
||||
left: 12,
|
||||
child: HashImageWidget(
|
||||
imageUrl: 'assets/images/crypto/${destChainName?.toLowerCase()}.webp',
|
||||
imageUrl:
|
||||
'assets/new-ui/crypto_full_icons/${destChainName?.toLowerCase()}.svg',
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/main.dart";
|
||||
import "package:hash_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class BuySellSelectorModal extends StatelessWidget {
|
||||
const BuySellSelectorModal({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceDim,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox.shrink(),
|
||||
Text(
|
||||
S.of(context).buy_or_sell,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
S.of(context).buy_or_sell_desc,
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
BuySellSelectorModalButton(
|
||||
title: S.of(context).buy_crypto,
|
||||
description: S.of(context).buy_crypto_desc,
|
||||
iconPath: "assets/new-ui/plus.svg",
|
||||
onTap: () => openBuySellPage(context, BuySellPageMode.buy),
|
||||
),
|
||||
BuySellSelectorModalButton(
|
||||
title: S.of(context).sell_crypto,
|
||||
description: S.of(context).sell_crypto_desc,
|
||||
iconPath: "assets/new-ui/sell.svg",
|
||||
onTap: () => openBuySellPage(context, BuySellPageMode.sell),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void openBuySellPage(BuildContext context, BuySellPageMode mode) {
|
||||
Navigator.of(context).pop();
|
||||
final page = getIt.get<NewBuySellAmountPage>(param1: NewBuySellParams(mode: mode));
|
||||
showModalBottomSheet(
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
context: navigatorKey.currentContext!,
|
||||
builder: (modalContext) => ModalNavigator(
|
||||
rootPage: page,
|
||||
parentContext: modalContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BuySellSelectorModalButton extends StatelessWidget {
|
||||
const BuySellSelectorModalButton({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.iconPath,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final String iconPath;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Row(
|
||||
spacing: 20,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: iconPath,
|
||||
width: 55,
|
||||
height: 55,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/wallet_type_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -40,15 +41,16 @@ class _ChangelogModalState extends State<ChangelogModal> {
|
||||
}
|
||||
|
||||
void loadChangelog() async {
|
||||
final fileName = isMoneroOnly ? "monerocom_changelog" : "changelog";
|
||||
String lang = WidgetsBinding.instance.platformDispatcher.locale.languageCode;
|
||||
if (lang == "und" ||
|
||||
lang.isEmpty ||
|
||||
!(await File("$changelogTextLocation/changelog_$lang.json").exists())) {
|
||||
!(await File("$changelogTextLocation/${fileName}_$lang.json").exists())) {
|
||||
lang = "en";
|
||||
}
|
||||
|
||||
final List<dynamic> changelog =
|
||||
jsonDecode(await rootBundle.loadString("$changelogTextLocation/changelog_$lang.json"))
|
||||
jsonDecode(await rootBundle.loadString("$changelogTextLocation/${fileName}_$lang.json"))
|
||||
as List<dynamic>;
|
||||
|
||||
for (final item in changelog) {
|
||||
@@ -170,34 +172,42 @@ class ChangelogItemWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.iconFilename.isNotEmpty)
|
||||
HashImageWidget(
|
||||
imageUrl: "$changelogIconLocation/${item.iconFilename}.svg",
|
||||
height: 36,
|
||||
width: 36,
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (item.title.isNotEmpty)
|
||||
Text(
|
||||
item.title,
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (item.description.isNotEmpty)
|
||||
Text(item.description,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
))
|
||||
],
|
||||
)
|
||||
],
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.iconFilename.isNotEmpty)
|
||||
HashImageWidget(
|
||||
imageUrl: "$changelogIconLocation/${item.iconFilename}.svg",
|
||||
height: 36,
|
||||
width: 36,
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (item.title.isNotEmpty)
|
||||
Text(
|
||||
item.title,
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (item.description.isNotEmpty)
|
||||
Text(
|
||||
item.description,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_data.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/change_pill.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/chart_modal.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_footer.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_menu.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
class ChartsAssetGrid extends StatelessWidget {
|
||||
const ChartsAssetGrid({required this.bloc, super.key});
|
||||
|
||||
final ChartsBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
|
||||
bloc: bloc,
|
||||
builder: (context, state) {
|
||||
if (state is ChartsStateWithData) {
|
||||
final currencies = state.currencies;
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: currencies.length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
mainAxisExtent: 105,),
|
||||
itemBuilder: (context, index) {
|
||||
final curr = currencies[index];
|
||||
return ChartsAssetCard(
|
||||
bloc: bloc,
|
||||
currency: curr,
|
||||
price: state.priceFor(curr)?.toStringWithPrecision(fractionalDigits: 2) ?? "...",
|
||||
ticker: state.fiatTicker,
|
||||
changeData: state is ChartsLoaded ? state.changeDataFor(curr) : null,
|
||||
favorite: curr == state.pinnedCurrency,
|
||||
isSingleCurrency: state.hasSingleCurrency,
|
||||
);
|
||||
},);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class ChartsAssetCard extends StatelessWidget {
|
||||
const ChartsAssetCard(
|
||||
{required this.bloc, super.key,
|
||||
required this.currency,
|
||||
required this.price,
|
||||
required this.ticker,
|
||||
this.changeData,
|
||||
required this.favorite,
|
||||
required this.isSingleCurrency,});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String price;
|
||||
final String ticker;
|
||||
final PriceChangeData? changeData;
|
||||
final bool favorite;
|
||||
final bool isSingleCurrency;
|
||||
final ChartsBloc bloc;
|
||||
|
||||
String get displayPrice {
|
||||
try {
|
||||
final priceDouble = double.parse(price);
|
||||
if (priceDouble > 10000) {
|
||||
return priceDouble.toStringAsFixed(0);
|
||||
} else {
|
||||
return priceDouble.toStringAsFixed(2);
|
||||
}
|
||||
} catch (_) {
|
||||
return price;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LongPressPopupBuilder(
|
||||
popup: LongPressMenu(items: [
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).favorite,
|
||||
iconPath: "assets/new-ui/favorite.svg",
|
||||
onSelected: () {
|
||||
bloc.add(CurrencyPinned(currency: currency));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
color: favorite ? Theme.of(context).colorScheme.error : null,),
|
||||
LongPressMenuItem(
|
||||
label: S.of(context).remove,
|
||||
iconPath: "assets/new-ui/address_hide.svg",
|
||||
onSelected: () {
|
||||
if (!isSingleCurrency) {
|
||||
bloc.add(CurrencyRemoved(currency: currency));
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
color: isSingleCurrency ? Theme.of(context).colorScheme.onSurfaceVariant : null,),
|
||||
],),
|
||||
footer:
|
||||
isSingleCurrency ? LongPressFooter(text: S.of(context).cannot_remove_last_asset) : null,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
unawaited(HapticFeedback.mediumImpact());
|
||||
final res = await showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChartModal(
|
||||
bloc: bloc,
|
||||
currency: currency,
|
||||
isFavorite: favorite,
|
||||
),);
|
||||
if (res != null && res is bool && res) {
|
||||
bloc.add(CurrencyPinned(currency: currency));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
width: 1, color: Theme.of(context).colorScheme.surfaceContainerHighest,),
|
||||
gradient: LinearGradient(colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary,
|
||||
], begin: Alignment.topCenter, end: Alignment.bottomCenter,),),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 5,
|
||||
children: [
|
||||
if (changeData != null)
|
||||
RotatedBox(
|
||||
quarterTurns:
|
||||
changeData!.direction == PriceChangeDirection.up ? 0 : 2,
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/price_change_arrow.svg",
|
||||
width: 8,
|
||||
height: 8,
|
||||
colorFilter: ColorFilter.mode(
|
||||
changeData!.direction.color, BlendMode.srcIn,),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
currency.title.toUpperCase(),
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
currency.fullName ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (changeData != null)
|
||||
ChangePill(
|
||||
changePercentage: changeData!.percentage,
|
||||
direction: changeData!.direction,),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
alignment: Alignment.centerRight,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
displayPrice,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
|
||||
),
|
||||
Text(
|
||||
ticker,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 20,),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_data_sort_criteria.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_menu.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/long_press_menu/long_press_popup.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
class ChartsAssetGridHeader extends StatelessWidget {
|
||||
const ChartsAssetGridHeader({
|
||||
required this.bloc, super.key,
|
||||
});
|
||||
|
||||
final ChartsBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
|
||||
bloc: bloc,
|
||||
builder: (context, state) {
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).followed_assets,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ModernButton.svg(
|
||||
size: 36,
|
||||
iconSize: 16,
|
||||
svgPath: "assets/new-ui/add.svg",
|
||||
onPressed: () => _showCurrencyPicker(context, bloc),
|
||||
),
|
||||
LongPressPopupBuilder(
|
||||
showOnTap: true,
|
||||
popup: LongPressMenu(
|
||||
items: PriceDataSortCriterium.all.map((item) {
|
||||
final isSelected =
|
||||
state is ChartsStateWithData && state.sortCriterium == item;
|
||||
|
||||
return LongPressMenuItem(
|
||||
label: item.name,
|
||||
iconPath: item.iconPath,
|
||||
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
onSelected: () {
|
||||
bloc.add(SortingCriteriumChanged(newCriterium: item));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
child: ModernButton.svg(
|
||||
size: 36,
|
||||
iconSize: 12,
|
||||
svgPath: "assets/new-ui/sort.svg",
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
void _showCurrencyPicker(BuildContext context, ChartsBloc bloc) {
|
||||
CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
args: CurrencyPickerArgs(
|
||||
items: CryptoCurrency.all,
|
||||
onSelected: (item) {
|
||||
bloc.add(CurrencyAdded(currency: item));
|
||||
},
|
||||
showStablesHeader: false,
|
||||
symbolResolver: (item) => item.symbol,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:hash_wallet/new-ui/model/charts/util/price_change_data.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/charts_page/change_pill.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChangeDisplay extends StatelessWidget {
|
||||
const ChangeDisplay({super.key, required this.changeData, required this.ticker});
|
||||
|
||||
final String ticker;
|
||||
final PriceChangeData changeData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text(
|
||||
"${changeData.direction.symbol}${ticker} ${changeData.amount.toStringWithPrecision(fractionalDigits: 2)}",
|
||||
style: TextStyle(fontSize: 16, color: changeData.direction.color),
|
||||
),
|
||||
ChangePill(changePercentage: changeData.percentage, direction: changeData.direction)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ChangePill extends StatelessWidget {
|
||||
const ChangePill({super.key, required this.changePercentage, required this.direction});
|
||||
|
||||
final String changePercentage;
|
||||
final PriceChangeDirection direction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: direction.color.withAlpha(52), borderRadius: BorderRadius.circular(999999)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 2.5, bottom: 2.5, left: 4, right: 8),
|
||||
child: Text(
|
||||
"${direction.symbol} $changePercentage%",
|
||||
style: TextStyle(color: direction.color),
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/change_display.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/chart_view.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/coin_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/price_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/range_selector.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/amount/money_double.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
import "package:intl/intl.dart";
|
||||
|
||||
class ChartHeader extends StatefulWidget {
|
||||
const ChartHeader({
|
||||
required this.bloc, super.key,
|
||||
required this.currency,
|
||||
required this.chartHeight,
|
||||
required this.chartPadding,
|
||||
required this.centered,
|
||||
required this.favorite,
|
||||
});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final double chartHeight;
|
||||
final double chartPadding;
|
||||
final bool centered;
|
||||
final bool favorite;
|
||||
final ChartsBloc bloc;
|
||||
|
||||
@override
|
||||
State<ChartHeader> createState() => _ChartHeaderState();
|
||||
}
|
||||
|
||||
class _ChartHeaderState extends State<ChartHeader> {
|
||||
String? _viewedPrice;
|
||||
String? _viewedTime;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<ChartsBloc, ChartsState>(
|
||||
bloc: widget.bloc,
|
||||
builder: (context, state) {
|
||||
if (state case final ChartsStateWithData s) {
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (!widget.favorite)
|
||||
HashImageWidget(
|
||||
imageUrl: widget.currency.iconPath ?? "",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
ChartViewCoinHeader(currency: widget.currency, isFavorite: widget.favorite),
|
||||
ChartViewPriceHeader(
|
||||
price: _viewedPrice ?? s.priceFor(widget.currency)?.toStringWithPrecision(fractionalDigits: 2) ?? "...",
|
||||
ticker: s.fiatTicker,
|
||||
highlight: _viewedPrice != null,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
// has to be constant-size, otherwise will jump around when loading
|
||||
height: 36,
|
||||
alignment: widget.centered ? Alignment.center : Alignment.centerLeft,
|
||||
child: (s is ChartsLoaded)
|
||||
? (_viewedPrice != null && _viewedTime != null)
|
||||
? Text(
|
||||
_viewedTime!,
|
||||
style: TextStyle(
|
||||
fontFamily: "IBM Plex Mono",
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
),
|
||||
)
|
||||
: ChangeDisplay(
|
||||
changeData: s.changeDataFor(widget.currency),
|
||||
ticker: s.fiatTicker,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
if (s is ChartsLoaded)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: widget.chartPadding),
|
||||
child: PriceChart(
|
||||
height: widget.chartHeight,
|
||||
prices: s.dataFor(widget.currency),
|
||||
direction: s.changeDataFor(widget.currency).direction,
|
||||
touchCallback: (event, response) {
|
||||
if (!event.isInterestedForInteractions) {
|
||||
setState(() {
|
||||
_viewedPrice = null;
|
||||
_viewedTime = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final newPrice =
|
||||
response?.lineBarSpots?.firstOrNull?.y.toStringAsFixed(2);
|
||||
final newTime = DateFormat("M/d/y HH:mm").format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
response?.lineBarSpots?.firstOrNull?.x.toInt() ?? 0,
|
||||
),
|
||||
);
|
||||
if (_viewedPrice != newPrice) {
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() {
|
||||
_viewedPrice = newPrice;
|
||||
});
|
||||
}
|
||||
if (_viewedTime != newTime) {
|
||||
setState(() {
|
||||
_viewedTime = newTime;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: widget.chartHeight + widget.chartPadding * 2,
|
||||
child: const Center(child: CupertinoActivityIndicator()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(128),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ChartRangeSelector(
|
||||
selectedRange: s.range,
|
||||
onRangeSelected: (range) =>
|
||||
widget.bloc.add(RangeChanged(newRange: range)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const CupertinoActivityIndicator();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import "package:hash_wallet/di.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/main.dart";
|
||||
import "package:hash_wallet/new-ui/modal_navigator.dart";
|
||||
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
|
||||
import "package:hash_wallet/new-ui/pages/swap_page.dart";
|
||||
import "package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/charts_page/chart_header.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/action_row/coin_action_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/src/screens/buy/buy_sell_page.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/store/app_store.dart";
|
||||
import "package:hash_wallet/view_model/buy/buy_sell_view_model.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class ChartModal extends StatelessWidget {
|
||||
const ChartModal({required this.bloc, required this.currency, required this.isFavorite, super.key});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isFavorite;
|
||||
final ChartsBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
padding: const EdgeInsets.only(top: 18, left: 18),
|
||||
),
|
||||
ChartHeader(
|
||||
bloc: bloc,
|
||||
currency: currency,
|
||||
chartHeight: 220,
|
||||
chartPadding: 32,
|
||||
centered: true,
|
||||
favorite: false,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: MediaQuery.of(context).size.width * 0.05,
|
||||
children: [
|
||||
if (canOpenBuySell) ...[
|
||||
CoinActionButton(
|
||||
icon: HashImageWidget(
|
||||
width: 36,
|
||||
height: 36,
|
||||
imageUrl: "assets/new-ui/plus.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
label: S.of(context).buy,
|
||||
action: () => openBuySellPage(context, BuySellPageMode.buy),
|
||||
),
|
||||
CoinActionButton(
|
||||
icon: HashImageWidget(
|
||||
width: 36,
|
||||
height: 36,
|
||||
imageUrl: "assets/new-ui/sell.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
label: S.of(context).sell,
|
||||
action: () => openBuySellPage(context, BuySellPageMode.sell),
|
||||
),
|
||||
],
|
||||
CoinActionButton(
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/exchange.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
label: S.of(context).swap,
|
||||
action: () {
|
||||
final page = getIt.get<NewSwapPage>(param2: currency);
|
||||
showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(85),
|
||||
builder: (context) => Material(
|
||||
child: ModalNavigator(
|
||||
rootPage: page,
|
||||
parentContext: context,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
CoinActionButton(
|
||||
icon: HashImageWidget(
|
||||
width: 36,
|
||||
height: 36,
|
||||
imageUrl: "assets/new-ui/favorite.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
isFavorite
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
gradientColors:
|
||||
isFavorite ? [const Color(0xFFDF2626), const Color(0xFF980F0F)] : null,
|
||||
label: S.of(context).favorite,
|
||||
action: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void openBuySellPage(BuildContext context, BuySellPageMode mode) {
|
||||
Navigator.of(context).pop();
|
||||
final page = getIt.get<NewBuySellAmountPage>(
|
||||
param1: NewBuySellParams(mode: mode, initialCurrency: currency));
|
||||
showModalBottomSheet(
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
context: navigatorKey.currentContext!,
|
||||
builder: (modalContext) => ModalNavigator(
|
||||
rootPage: page,
|
||||
parentContext: modalContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get canOpenBuySell => getIt.get<AppStore>().wallet!.balance.containsKey(currency);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
|
||||
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
|
||||
import "package:fl_chart/fl_chart.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class PriceChart extends StatelessWidget {
|
||||
const PriceChart({
|
||||
super.key,
|
||||
required this.height,
|
||||
required this.prices,
|
||||
required this.direction,
|
||||
required this.touchCallback,
|
||||
});
|
||||
|
||||
final List<PriceData> prices;
|
||||
final double height;
|
||||
final PriceChangeDirection direction;
|
||||
final Function(FlTouchEvent, LineTouchResponse?) touchCallback;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chartPoints = prices.map((entry) {
|
||||
final x = entry.time.millisecondsSinceEpoch.toDouble();
|
||||
final y = double.parse(entry.quote.toString());
|
||||
return FlSpot(x, y);
|
||||
}).toList();
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: const FlGridData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: chartPoints,
|
||||
gradient: LinearGradient(colors: [direction.color.withAlpha(25), direction.color]),
|
||||
barWidth: 1.5,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
),
|
||||
],
|
||||
lineTouchData: LineTouchData(
|
||||
enabled: true,
|
||||
getTouchLineStart: (barData, spotIndex) => -double.infinity,
|
||||
getTouchLineEnd: (barData, spotIndex) => double.infinity,
|
||||
getTouchedSpotIndicator: (barData, spotIndexes) => spotIndexes
|
||||
.map(
|
||||
(index) => TouchedSpotIndicatorData(
|
||||
FlLine(
|
||||
strokeWidth: 1,
|
||||
color: direction.color.withAlpha(80),
|
||||
dashArray: [4, 4],
|
||||
),
|
||||
const FlDotData(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipItems: (touchedSpots) => touchedSpots.map((spot) => null).toList(),
|
||||
),
|
||||
touchCallback: touchCallback,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ChartViewCoinHeader extends StatelessWidget {
|
||||
const ChartViewCoinHeader({required this.currency, required this.isFavorite, super.key});
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final bool isFavorite;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
mainAxisAlignment: isFavorite ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (isFavorite)
|
||||
HashImageWidget(
|
||||
imageUrl: currency.iconPath ?? "",
|
||||
width: 30,
|
||||
height: 30,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 5,
|
||||
children: [
|
||||
Text(
|
||||
currency.fullName ?? currency.title,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(9999999),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text(
|
||||
currency.title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isFavorite)
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/favorite.svg",
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ChartViewPriceHeader extends StatelessWidget {
|
||||
const ChartViewPriceHeader({
|
||||
required this.price,
|
||||
required this.ticker,
|
||||
required this.highlight,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String price;
|
||||
final String ticker;
|
||||
final bool highlight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
price,
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
color: highlight
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
ticker,
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
color: highlight
|
||||
? Theme.of(context).colorScheme.primary.withAlpha(128)
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import "package:hash_wallet/new-ui/model/charts/util/chart_range.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ChartRangeSelector extends StatelessWidget {
|
||||
const ChartRangeSelector({required this.selectedRange, required this.onRangeSelected, super.key});
|
||||
|
||||
final ChartRange selectedRange;
|
||||
final Function(ChartRange) onRangeSelected;
|
||||
|
||||
static const double optionSize = 36;
|
||||
static const double optionPadding = 24;
|
||||
static const Duration switchDuration = Duration(milliseconds: 250);
|
||||
|
||||
double pillPosition(int selectedIndex) => selectedIndex * (optionSize + optionPadding);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedIndex = ChartRange.ranges.indexOf(selectedRange);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
curve: Curves.easeOutCubic,
|
||||
left: pillPosition(selectedIndex),
|
||||
duration: switchDuration,
|
||||
child: Container(
|
||||
height: optionSize,
|
||||
width: optionSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: ChartRange.ranges.map((item) {
|
||||
final selected = selectedRange == item;
|
||||
final isFirst = ChartRange.ranges.indexOf(item) == 0;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (!selected) {
|
||||
onRangeSelected(item);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: optionPadding / 2,
|
||||
left: isFirst ? 0 : optionPadding / 2,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: optionSize,
|
||||
height: optionSize,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: switchDuration,
|
||||
style: TextStyle(
|
||||
fontWeight: selected ? FontWeight.w400 : FontWeight.w500,
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
child: Center(child: Text(item.displayText)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class CoinControlListItem extends StatelessWidget {
|
||||
required this.isLast,
|
||||
required this.isLoading,
|
||||
this.onCheckBoxTap,
|
||||
required this.hasCheckbox,
|
||||
});
|
||||
|
||||
final String note;
|
||||
@@ -31,6 +32,7 @@ class CoinControlListItem extends StatelessWidget {
|
||||
final bool isFirst;
|
||||
final bool isLast;
|
||||
final bool isLoading;
|
||||
final bool hasCheckbox;
|
||||
final Function()? onCheckBoxTap;
|
||||
|
||||
@override
|
||||
@@ -133,6 +135,10 @@ class CoinControlListItem extends StatelessWidget {
|
||||
return HashImageWidget(imageUrl: "assets/new-ui/frozen.svg");
|
||||
}
|
||||
|
||||
if (!hasCheckbox) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
return NewSimpleCheckbox(
|
||||
value: isSending,
|
||||
onChanged: (value) {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/store/settings_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class AccountsPromo extends StatelessWidget {
|
||||
const AccountsPromo({
|
||||
required this.settingsStore,
|
||||
required this.walletName,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
final String walletName;
|
||||
final VoidCallback onTap;
|
||||
|
||||
static bool supportsWallet(WalletType walletType) => walletType == WalletType.bitcoin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Observer(
|
||||
builder: (_) {
|
||||
if (settingsStore.accountsHomePromoDismissed) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 64),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/settings_row_icons/accounts.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).accounts_home_promo_title(walletName),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
S.of(context).accounts_home_promo_description,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Semantics(
|
||||
button: true,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => settingsStore.accountsHomePromoDismissed = true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text(
|
||||
S.of(context).do_not_show_anymore,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
import 'dart:math';
|
||||
import "dart:math";
|
||||
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import "package:hash_wallet/themes/core/theme_extension.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class CoinActionButton extends StatelessWidget {
|
||||
const CoinActionButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.action,
|
||||
super.key,
|
||||
this.gradientColors,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final String label;
|
||||
final VoidCallback action;
|
||||
final List<Color>? gradientColors;
|
||||
|
||||
static const sizeFactor = 0.16;
|
||||
|
||||
@@ -24,47 +25,58 @@ class CoinActionButton extends StatelessWidget {
|
||||
final size = MediaQuery.of(context).size.width * sizeFactor;
|
||||
final double effectiveSize = min(size, 80);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: effectiveSize,
|
||||
height: effectiveSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
action();
|
||||
},
|
||||
icon: icon,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
// The caption below the circle is the accessible name, so it is excluded
|
||||
// from the semantics tree to keep this a single button node.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: label,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: effectiveSize,
|
||||
height: effectiveSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: gradientColors ??
|
||||
[
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
label,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
action();
|
||||
},
|
||||
icon: ExcludeSemantics(child: icon),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
label,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,13 @@ import 'package:hash_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/lnurl.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
import '../../../pages/receive_page.dart';
|
||||
import '../../../pages/scan_page.dart';
|
||||
import 'coin_action_button.dart';
|
||||
|
||||
class CoinActionRow extends StatelessWidget {
|
||||
@@ -45,6 +44,7 @@ class CoinActionRow extends StatelessWidget {
|
||||
spacing: MediaQuery.of(context).size.width * 0.05,
|
||||
children: [
|
||||
CoinActionButton(
|
||||
key: ValueKey("home_page_send_button_key"),
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/send.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
@@ -54,7 +54,6 @@ class CoinActionRow extends StatelessWidget {
|
||||
),
|
||||
label: S.of(context).send,
|
||||
action: () {
|
||||
if (FeatureFlag.hasNewUiExtraPages) {
|
||||
final sendPage = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
unspentCoinType:
|
||||
@@ -75,14 +74,10 @@ class CoinActionRow extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
Map<String, dynamic>? args;
|
||||
if (lightningMode) args = {'coinTypeToSpendFrom': UnspentCoinType.lightning};
|
||||
Navigator.of(context).pushNamed(Routes.send, arguments: args);
|
||||
}
|
||||
},
|
||||
),
|
||||
CoinActionButton(
|
||||
key: ValueKey("home_page_receive_button_key"),
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/receive.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
@@ -92,7 +87,6 @@ class CoinActionRow extends StatelessWidget {
|
||||
),
|
||||
label: S.of(context).receive,
|
||||
action: () async {
|
||||
if (FeatureFlag.hasNewUiExtraPages) {
|
||||
final page = getIt.get<NewReceivePage>(param1: lightningMode);
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
@@ -101,21 +95,11 @@ class CoinActionRow extends StatelessWidget {
|
||||
return Material(child: ModalNavigator(parentContext: context, rootPage: page));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// ToDo: (Konsti) refactor as part of the derivation PR (I hate myself for it)
|
||||
if (lightningMode) {
|
||||
await getIt<WalletAddressListViewModel>().setAddressType(
|
||||
bitcoin!.getOptionToType(bitcoin!.getBitcoinLightningReceivePageOption()));
|
||||
} else {
|
||||
await getIt<WalletAddressListViewModel>().setAddressType(
|
||||
bitcoin!.getOptionToType(bitcoin!.getBitcoinSegwitPageOption()));
|
||||
}
|
||||
Navigator.of(context).pushNamed(Routes.addressPage);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (showSwap)
|
||||
CoinActionButton(
|
||||
key: ValueKey("home_page_swap_button_key"),
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/exchange.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
@@ -127,7 +111,6 @@ class CoinActionRow extends StatelessWidget {
|
||||
action: () {
|
||||
final page =
|
||||
getIt.get<NewSwapPage>(param2: lightningMode ? CryptoCurrency.btcln : null);
|
||||
if (FeatureFlag.hasNewUiExtraPages) {
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(85),
|
||||
@@ -137,12 +120,10 @@ class CoinActionRow extends StatelessWidget {
|
||||
parentContext: context,
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushNamed(Routes.exchange);
|
||||
}
|
||||
},
|
||||
),
|
||||
CoinActionButton(
|
||||
key: ValueKey("home_page_scan_button_key"),
|
||||
icon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/scan.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
@@ -159,71 +140,63 @@ class CoinActionRow extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _onPressedScan(BuildContext context) async {
|
||||
if (false && FeatureFlag.hasNewUiExtraPages) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FractionallySizedBox(
|
||||
heightFactor: 0.9,
|
||||
child: ScanPage(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final code = await presentQRScanner(context);
|
||||
final code = await presentQRScanner(context, showHelp: true);
|
||||
|
||||
if (code == null || code.isEmpty) return;
|
||||
if (code == null || code.isEmpty) return;
|
||||
|
||||
late final PaymentRequest req;
|
||||
var unspentCoinType = UnspentCoinType.any;
|
||||
if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(code) ?? 0));
|
||||
req = PaymentRequest(code, amount, "", "", "");
|
||||
} else if (SendViewModelBase.isLnurlInvoice(code)) {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
final amount = CryptoCurrency.btcln
|
||||
.formatAmount(BigInt.from(await LNURL.getPayRequestAmount(code) ?? 0));
|
||||
req = PaymentRequest(code, amount, "", "", "");
|
||||
} else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) {
|
||||
req = PaymentRequest(code, "", "", "", "");
|
||||
} else if (Uri.tryParse(code)?.scheme == "wc") {
|
||||
if (!isEVMCompatibleChain(walletType)) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: "WalletConnect",
|
||||
alertContent: S.of(context).switchToEVMCompatibleWallet,
|
||||
buttonText: "OK",
|
||||
buttonAction: Navigator.of(context).pop));
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
|
||||
PaymentRequest? req;
|
||||
String? rawInput;
|
||||
var unspentCoinType = UnspentCoinType.any;
|
||||
if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
final amount = getBolt11Amount(code)?.toString() ?? "0";
|
||||
req = PaymentRequest(code, amount, "", "", "");
|
||||
} else if (SendViewModelBase.isLnurlInvoice(code)) {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
final amount = (await LNURL.getPayRequestAmount(code))?.toString() ?? "0";
|
||||
req = PaymentRequest(code, amount, "", "", "");
|
||||
} else if (Uri.tryParse(code)?.scheme == "wc") {
|
||||
if (!isWalletConnectCompatibleChain(walletType)) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: "WalletConnect",
|
||||
alertContent:
|
||||
S.of(context).switchToWCCompatibleWallet(walletConnectCompatibleChainsLabel()),
|
||||
buttonText: "OK",
|
||||
buttonAction: Navigator.of(context).pop));
|
||||
return;
|
||||
} else {
|
||||
final uri = Uri.tryParse(code);
|
||||
if (uri == null) return;
|
||||
req = PaymentRequest.fromUri(uri);
|
||||
}
|
||||
|
||||
final sendPage = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
initialPaymentRequest: req,
|
||||
unspentCoinType: unspentCoinType,
|
||||
),
|
||||
);
|
||||
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
builder: (context) => Material(
|
||||
child: ModalNavigator(
|
||||
rootPage: sendPage,
|
||||
parentContext: context,
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
|
||||
return;
|
||||
} else if (!OpenCryptoPayService.isOpenCryptoPayQR(code) &&
|
||||
["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
|
||||
Navigator.of(context).pushNamed(Routes.newNode,
|
||||
arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
|
||||
return;
|
||||
} else {
|
||||
rawInput = code;
|
||||
}
|
||||
|
||||
final sendPage = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
initialPaymentRequest: req,
|
||||
initialRawInput: rawInput,
|
||||
unspentCoinType: unspentCoinType,
|
||||
),
|
||||
);
|
||||
|
||||
CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(60),
|
||||
builder: (context) => Material(
|
||||
child: ModalNavigator(
|
||||
rootPage: sendPage,
|
||||
parentContext: context,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:hash_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/receive_page.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/send_page.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/swap_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
@@ -61,6 +62,7 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingIcon: Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: Navigator.of(context).pop,
|
||||
padding: EdgeInsets.only(top: 12, right: 18),
|
||||
),
|
||||
@@ -77,7 +79,10 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
if (iconPath.isNotEmpty)
|
||||
HashImageWidget(imageUrl: iconPath, width: 75, height: 75)
|
||||
TokenImageWidget(
|
||||
imageUrl: iconPath,
|
||||
size: 75,
|
||||
)
|
||||
else
|
||||
Container(
|
||||
width: 75,
|
||||
@@ -352,17 +357,22 @@ class AssetDetailsModalBottomButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ModernButton.svg(
|
||||
onPressed: onPressed,
|
||||
svgPath: iconPath,
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
),
|
||||
Text(title)
|
||||
],
|
||||
// The caption is the button's accessible name, so it is excluded from the
|
||||
// semantics tree to keep the whole control a single button node.
|
||||
return MergeSemantics(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ModernButton.svg(
|
||||
onPressed: onPressed,
|
||||
svgPath: iconPath,
|
||||
size: 60,
|
||||
iconSize: 32,
|
||||
semanticLabel: title,
|
||||
),
|
||||
ExcludeSemantics(child: Text(title))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:math' show min;
|
||||
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AssetTile extends StatelessWidget {
|
||||
@@ -37,147 +37,140 @@ class AssetTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconPath = _getIconPath();
|
||||
final iconPath = balance.asset.iconPath ?? "";
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return AssetDetailsModal(
|
||||
showSwap: showSwap,
|
||||
showBridgeButton: showBridgeButton,
|
||||
asset: balance.asset,
|
||||
title: title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
chainTitle: "",
|
||||
subtitle: trailingText ?? _getChainTitle(),
|
||||
amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
|
||||
currencyTitle: balance.asset.title,
|
||||
fiatAmount: showSecondary
|
||||
? balance.fiatSecondAvailableBalance
|
||||
: balance.fiatAvailableBalance,
|
||||
iconPath: balance.asset.iconPath ?? "",
|
||||
chainIconPath: chainIconPath,
|
||||
mode: modalMode,
|
||||
wallet: wallet,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isFirst ? Radius.circular(18) : Radius.zero,
|
||||
bottom: isLast ? Radius.circular(18) : Radius.zero,
|
||||
),
|
||||
),
|
||||
// The row is one control: name, amount and fiat value merge into a single
|
||||
// button node that opens the asset details sheet.
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return AssetDetailsModal(
|
||||
showSwap: showSwap,
|
||||
showBridgeButton: showBridgeButton,
|
||||
asset: balance.asset,
|
||||
title: title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
chainTitle: "",
|
||||
subtitle: trailingText ?? _getChainTitle(),
|
||||
amount:
|
||||
showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
|
||||
currencyTitle: balance.asset.title,
|
||||
fiatAmount: showSecondary
|
||||
? balance.fiatSecondAvailableBalance
|
||||
: balance.fiatAvailableBalance,
|
||||
iconPath: balance.asset.iconPath ?? "",
|
||||
chainIconPath: chainIconPath,
|
||||
mode: modalMode,
|
||||
wallet: wallet,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: Stack(
|
||||
children: [
|
||||
if ((iconPath).isNotEmpty)
|
||||
HashImageWidget(imageUrl: iconPath)
|
||||
else
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isFirst ? Radius.circular(18) : Radius.zero,
|
||||
bottom: isLast ? Radius.circular(18) : Radius.zero,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Decorative: the asset name is already in the row text.
|
||||
ExcludeSemantics(
|
||||
child: iconPath.isNotEmpty
|
||||
? TokenImageWidget(
|
||||
imageUrl: iconPath,
|
||||
size: 36,
|
||||
)
|
||||
: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(99999)),
|
||||
child: Center(
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
balance.asset.name
|
||||
.substring(0, min(2, balance.asset.name.length)),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).colorScheme.onPrimary),
|
||||
)),
|
||||
),
|
||||
// if (chainIconPath.isNotEmpty)
|
||||
// Align(
|
||||
// alignment: Alignment.bottomRight,
|
||||
// child: Container(
|
||||
// decoration: ShapeDecoration(
|
||||
// shape: RoundedSuperellipseBorder(
|
||||
// borderRadius: BorderRadius.circular(5),side: BorderSide(color: Colors.black)),
|
||||
// color: Colors.white),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(2.0),
|
||||
// child: HashImageWidget(
|
||||
// imageUrl: chainIconPath,
|
||||
// width: 12,
|
||||
// height: 12,
|
||||
// colorFilter:
|
||||
// ColorFilter.mode(Colors.black, BlendMode.srcIn),
|
||||
// ),
|
||||
// )))
|
||||
],
|
||||
)),
|
||||
SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (trailingText != null)
|
||||
Text(
|
||||
trailingText!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
balance.asset.name
|
||||
.substring(0, min(2, balance.asset.name.length)),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (trailingText != null)
|
||||
Text(
|
||||
trailingText!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
showSecondary
|
||||
? balance.fiatSecondAvailableBalance
|
||||
: balance.fiatAvailableBalance,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -185,17 +178,5 @@ class AssetTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _getChainTitle() {
|
||||
try {
|
||||
return CryptoCurrency.fromString(wallet.currency.tag ?? wallet.currency.title).fullName ?? "";
|
||||
} catch (e) {
|
||||
return wallet.currency.fullName ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
String _getIconPath() {
|
||||
if (balance.asset == CryptoCurrency.baseEth) return "assets/new-ui/crypto_full_icons/base.svg";
|
||||
|
||||
return balance.asset.iconPath ?? "";
|
||||
}
|
||||
String _getChainTitle() => walletTypeToDisplayName(wallet.type);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_modal.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_top_bar.dart';
|
||||
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/nft_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'assets_section.dart';
|
||||
import 'history_section.dart';
|
||||
|
||||
class AssetsHistorySectionActionButton {
|
||||
final String title;
|
||||
final String iconPath;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const AssetsHistorySectionActionButton(this.title, this.iconPath, this.onPressed);
|
||||
}
|
||||
|
||||
class AssetsHistorySectionTab {
|
||||
final String title;
|
||||
final Widget content;
|
||||
final AssetsHistorySectionActionButton? actionButton;
|
||||
|
||||
AssetsHistorySectionTab(this.title, this.content);
|
||||
const AssetsHistorySectionTab(this.title, this.content, this.actionButton);
|
||||
}
|
||||
|
||||
class AssetsHistorySection extends StatefulWidget {
|
||||
@@ -32,22 +47,40 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
|
||||
void reloadTabs() {
|
||||
final oldTabLength = tabs.length;
|
||||
final hasAssetsTab = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled ||
|
||||
(widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled);
|
||||
final hasAssetsButton = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled;
|
||||
final hasNftTab = isNFTACtivatedChain(widget.dashboardViewModel.wallet.type,
|
||||
chainId: widget.dashboardViewModel.wallet.chainId);
|
||||
tabs = [
|
||||
if (widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled ||
|
||||
(widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled))
|
||||
if (hasAssetsTab)
|
||||
AssetsHistorySectionTab(
|
||||
S.current.assets,
|
||||
AssetsSection(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
)),
|
||||
),
|
||||
hasAssetsButton ? AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
|
||||
|
||||
() {Navigator.of(context).pushNamed(
|
||||
Routes.homeSettings,
|
||||
arguments: widget.dashboardViewModel.balanceViewModel,
|
||||
);
|
||||
}): null),
|
||||
AssetsHistorySectionTab(
|
||||
S.current.history,
|
||||
HistorySection(
|
||||
detailsAsPage: false,
|
||||
roundedTopSection: hasAssetsTab || hasNftTab,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
)),
|
||||
if (isNFTACtivatedChain(widget.dashboardViewModel.wallet.type,
|
||||
chainId: widget.dashboardViewModel.wallet.chainId))
|
||||
AssetsHistorySectionTab(S.current.nfts, NFTListingPage(nftViewModel: widget.nftViewModel))
|
||||
short: true,
|
||||
),
|
||||
AssetsHistorySectionActionButton(
|
||||
S.current.all_pascal_case, "assets/new-ui/arrow_right.svg", () {
|
||||
openHistoryModal(context);
|
||||
})),
|
||||
if (hasNftTab)
|
||||
AssetsHistorySectionTab(
|
||||
S.current.nfts, NFTListingPage(nftViewModel: widget.nftViewModel), null)
|
||||
];
|
||||
if (oldTabLength != tabs.length) {
|
||||
setState(() {
|
||||
@@ -71,19 +104,39 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
Widget build(BuildContext context) {
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
// if(tabs.length>1)
|
||||
AssetsTopBar(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
tabs: tabs.map((item) => item.title).toList(),
|
||||
onTabChange: (index) {
|
||||
setState(() {
|
||||
_selectedTab = index;
|
||||
});
|
||||
},
|
||||
selectedTab: _selectedTab,
|
||||
),
|
||||
if (tabs.length > 1)
|
||||
AssetsTopBar(
|
||||
onTransactionHistoryOpened: () => openHistoryModal(context),
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
tabs: tabs,
|
||||
onTabChange: (index) {
|
||||
setState(() {
|
||||
_selectedTab = index;
|
||||
});
|
||||
},
|
||||
selectedTab: _selectedTab,
|
||||
),
|
||||
if (tabs.length == 1)
|
||||
Observer(
|
||||
builder: (_) => HistoryTopBar(
|
||||
onTap: () => openHistoryModal(context),
|
||||
roundedBottom: widget.dashboardViewModel.itemsShort.isEmpty,
|
||||
),
|
||||
),
|
||||
tabs[_selectedTab].content
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> openHistoryModal(BuildContext context) async {
|
||||
await CupertinoScaffold.showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ModalNavigator(
|
||||
rootPage: Material(
|
||||
color: Colors.transparent,
|
||||
child: HistoryModal(dashboardViewModel: widget.dashboardViewModel)),
|
||||
parentContext: context,
|
||||
));
|
||||
widget.dashboardViewModel.changeAllFilterItems(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/line_tab_switcher.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/filter_widget.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -11,78 +8,99 @@ class AssetsTopBar extends StatelessWidget {
|
||||
const AssetsTopBar({
|
||||
super.key,
|
||||
required this.onTabChange,
|
||||
required this.onTransactionHistoryOpened,
|
||||
required this.selectedTab,
|
||||
required this.tabs,
|
||||
required this.dashboardViewModel,
|
||||
});
|
||||
|
||||
final void Function(int) onTabChange;
|
||||
final VoidCallback onTransactionHistoryOpened;
|
||||
final int selectedTab;
|
||||
final List<String> tabs;
|
||||
final List<AssetsHistorySectionTab> tabs;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsButtonText = _getSettingsButtonText();
|
||||
final hasTokenSettingsButton = settingsButtonText != null;
|
||||
final actionButton = tabs[selectedTab].actionButton;
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
bottom: 0,
|
||||
left: 12,
|
||||
right: 18,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (tabs.length > 1)
|
||||
LineTabSwitcher(
|
||||
tabs: tabs,
|
||||
onTabChange: onTabChange,
|
||||
selectedTab: selectedTab,
|
||||
)
|
||||
else
|
||||
SizedBox.shrink(),
|
||||
Opacity(
|
||||
opacity: hasTokenSettingsButton ? 1 : 0,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (tabs[selectedTab] == S.of(context).assets) {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.homeSettings,
|
||||
arguments: dashboardViewModel.balanceViewModel,
|
||||
);
|
||||
} else if (tabs[selectedTab] == S.of(context).history) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
FilterWidget(filterItems: dashboardViewModel.filterItems),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/options_slider.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
|
||||
Text(
|
||||
settingsButtonText ?? "",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
LineTabSwitcher(
|
||||
tabs: tabs.map((item) => item.title).toList(),
|
||||
onTabChange: onTabChange,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
key: ValueKey(selectedTab),
|
||||
spacing: 8,
|
||||
children: [
|
||||
// Built conditionally rather than rendered at opacity 0: an
|
||||
// invisible chip stayed focusable for screen readers. The
|
||||
// SizedBox keeps the 40px header height the invisible chip
|
||||
// used to occupy.
|
||||
if (actionButton == null)
|
||||
const SizedBox(height: 40)
|
||||
else
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
key: const ValueKey("assets_history_action_button_key"),
|
||||
onTap: actionButton.onPressed,
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (actionButton.title.isNotEmpty)
|
||||
Text(
|
||||
actionButton.title,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
imageUrl: actionButton.iconPath,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -90,16 +108,4 @@ class AssetsTopBar extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _getSettingsButtonText() {
|
||||
if (tabs[selectedTab] == S.current.assets &&
|
||||
dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled) {
|
||||
return S.current.tokens;
|
||||
}
|
||||
|
||||
if (tabs[selectedTab] == S.current.history) {
|
||||
return S.current.filters;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_swap_providers_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/select_deselect_all.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/filter_item.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class HistoryFiltersPage extends StatelessWidget {
|
||||
const HistoryFiltersPage({super.key, required this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).filters,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Observer(
|
||||
builder: (_) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: SelectDeselectAllBar(
|
||||
title: S.of(context).type,
|
||||
onSelected: dashboardViewModel.changeAllFilterItems),
|
||||
),
|
||||
),
|
||||
NewListSections(
|
||||
sections: {
|
||||
"": dashboardViewModel.filterItems.map((item) {
|
||||
if (item is SwapFilterItem) {
|
||||
final String subtitle;
|
||||
final Color subtitleColor;
|
||||
if (dashboardViewModel.tradeFilterStore.displayAllTrades) {
|
||||
subtitle = S.of(context).manage_providers;
|
||||
subtitleColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
} else if (dashboardViewModel
|
||||
.tradeFilterStore.enabledProvidersCount ==
|
||||
0) {
|
||||
subtitle = S.of(context).no_providers_selected;
|
||||
subtitleColor = Color(0xFFFFB84E);
|
||||
} else {
|
||||
subtitle = "${item.enabledProviders()} ${S.of(context).providers}";
|
||||
subtitleColor = Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
|
||||
return ListItemCheckbox(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) => HistorySwapProvidersPage(
|
||||
dashboardViewModel: dashboardViewModel)));
|
||||
},
|
||||
keyValue: item.caption,
|
||||
label: S.of(context).swap,
|
||||
value: item.value(),
|
||||
onChanged: (val) {
|
||||
if ((val &&
|
||||
dashboardViewModel
|
||||
.tradeFilterStore.enabledProvidersCount ==
|
||||
0) ||
|
||||
(!val &&
|
||||
dashboardViewModel
|
||||
.tradeFilterStore.enabledProvidersCount >
|
||||
0)) {
|
||||
dashboardViewModel.tradeFilterStore
|
||||
.toggleDisplayExchange(ExchangeProviderDescription.all);
|
||||
}
|
||||
},
|
||||
subtitle: subtitle,
|
||||
subtitleColor: subtitleColor,
|
||||
showArrow: true);
|
||||
}
|
||||
|
||||
return ListItemCheckbox(
|
||||
keyValue: item.caption,
|
||||
label: item.caption,
|
||||
value: item.value(),
|
||||
onChanged: (val) => item.onChanged());
|
||||
}).toList()
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: NewPrimaryButton(
|
||||
onPressed: () => dashboardViewModel.changeAllFilterItems(true),
|
||||
text: S.of(context).reset_filters,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.primary),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:hash_wallet/core/csv_export_service.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_filters_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_section.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class HistoryModal extends StatelessWidget {
|
||||
const HistoryModal({super.key, required this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
key: const ValueKey("history_modal_key"),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).history,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).maybePop,
|
||||
trailingIcon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/tx_export.svg",
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
trailingSemanticLabel: S.of(context).export_csv,
|
||||
onTrailingPressed: () =>
|
||||
getIt.get<CsvExportService>().exportToCsv(dashboardViewModel.items, context),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Material(
|
||||
child: CustomScrollView(physics: ClampingScrollPhysics(),
|
||||
controller: ModalScrollController.of(context),
|
||||
slivers: [
|
||||
HistorySection(
|
||||
detailsAsPage: true,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
short: false,
|
||||
roundedTopSection: true)
|
||||
])),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: IgnorePointer(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).viewPadding.bottom + 168,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: <Color>[
|
||||
Theme.of(context).colorScheme.surface.withAlpha(200),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(175),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(150),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(100),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(50),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(25),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(5),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 12,
|
||||
right: 12,
|
||||
bottom: MediaQuery.of(context).viewPadding.bottom + 24,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
HistoryFiltersPage(dashboardViewModel: dashboardViewModel)));
|
||||
},
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary, width: 1)),
|
||||
height: 56,
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/filter_options.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
S.of(context).filters,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/balance_display_mode.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_order_tile.dart';
|
||||
@@ -7,7 +8,6 @@ import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_tra
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/utils/date_formatter.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
|
||||
@@ -15,176 +15,223 @@ import 'package:hash_wallet/view_model/dashboard/order_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/trade_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/transaction_list_item.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class HistorySection extends StatelessWidget {
|
||||
const HistorySection({super.key, required this.dashboardViewModel});
|
||||
const HistorySection(
|
||||
{super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.short,
|
||||
required this.roundedTopSection,
|
||||
required this.detailsAsPage});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool short;
|
||||
final bool roundedTopSection;
|
||||
final bool detailsAsPage;
|
||||
|
||||
/// A history row is a single button node: every text inside it (direction,
|
||||
/// date, amounts) merges into one label.
|
||||
Widget _historyRow({required VoidCallback onTap, required Widget child}) => MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(onTap: onTap, child: child),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: EdgeInsets.only(left: 16.0, right: 16, top: short && roundedTopSection ? 18 : 0),
|
||||
sliver: Observer(
|
||||
builder: (_) => (dashboardViewModel.items.isEmpty)
|
||||
? SliverPadding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: (dashboardViewModel.status is SyncingSyncStatus)
|
||||
? SizedBox.shrink()
|
||||
: Center(
|
||||
child: Text(S.of(context).transactions_will_appear_here,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: dashboardViewModel.items.length,
|
||||
(context, index) => Observer(builder: (_) {
|
||||
final prevItem = index == 0 ? null : dashboardViewModel.items[index - 1];
|
||||
final topPadding = index == 0 ? 0.0 : 18.0;
|
||||
final item = dashboardViewModel.items[index];
|
||||
final nextItem = index == dashboardViewModel.items.length - 1
|
||||
? null
|
||||
: dashboardViewModel.items[index + 1];
|
||||
builder: (_) {
|
||||
final localeName = Localizations.localeOf(context).toString();
|
||||
final items = short ? dashboardViewModel.itemsShort : dashboardViewModel.items;
|
||||
|
||||
final roundedBottom = (nextItem == null || nextItem is DateSectionItem);
|
||||
final roundedTop = (prevItem == null || prevItem is DateSectionItem);
|
||||
return (items.isEmpty)
|
||||
? SliverPadding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: (dashboardViewModel.status is SyncingSyncStatus)
|
||||
? SizedBox.shrink()
|
||||
: Center(
|
||||
child: Text(S.of(context).transactions_will_appear_here,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SliverPadding(
|
||||
padding: EdgeInsets.only(bottom: short ? 0 : 144),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: items.length,
|
||||
(context, index) => Observer(builder: (_) {
|
||||
final prevItem = index == 0 ? null : items[index - 1];
|
||||
final topPadding = index == 0 ? 0.0 : 18.0;
|
||||
final item = items[index];
|
||||
final nextItem = index == items.length - 1 ? null : items[index + 1];
|
||||
|
||||
if (item is TransactionListItem) {
|
||||
final transaction = item.transaction;
|
||||
final transactionType = dashboardViewModel.getTransactionType(transaction);
|
||||
final roundedBottom = (nextItem == null || nextItem is DateSectionItem);
|
||||
final roundedTop = roundedTopSection &&
|
||||
(prevItem == null || prevItem is DateSectionItem);
|
||||
|
||||
if (item.hasTokens && item.assetOfTransaction == null) {
|
||||
return Container();
|
||||
}
|
||||
if (item is TransactionListItem) {
|
||||
final transaction = item.transaction;
|
||||
final transactionType =
|
||||
dashboardViewModel.getTransactionType(transaction);
|
||||
|
||||
CryptoCurrency? asset;
|
||||
if (transaction.additionalInfo["isLightning"] == true)
|
||||
asset = CryptoCurrency.btcln;
|
||||
else
|
||||
asset = item.assetOfTransaction;
|
||||
if (item.hasTokens && item.assetOfTransaction == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final page = getIt.get<TransactionDetailsModal>(param1: transaction);
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => page);
|
||||
},
|
||||
child: HistoryTile(
|
||||
title: item.formattedTitle + transactionType,
|
||||
date: DateFormat('HH:mm').format(transaction.date),
|
||||
amount: item.formattedCryptoAmount,
|
||||
amountFiat: item.formattedFiatAmount,
|
||||
hasTokens: item.hasTokens,
|
||||
chainIconPath: _getChainIconPath(),
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
direction: item.transaction.direction,
|
||||
pending: item.transaction.isPending,
|
||||
asset: asset,
|
||||
),
|
||||
);
|
||||
} else if (item is TradeListItem) {
|
||||
final trade = item.trade;
|
||||
final tradeFrom = trade.from;
|
||||
final tradeTo = trade.to;
|
||||
if (tradeFrom == null || tradeTo == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
CryptoCurrency? asset;
|
||||
if (transaction.additionalInfo["isLightning"] == true)
|
||||
asset = CryptoCurrency.btcln;
|
||||
else
|
||||
asset = item.assetOfTransaction;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.tradeDetails, arguments: trade),
|
||||
child: HistoryTradeTile(
|
||||
from: tradeFrom,
|
||||
to: tradeTo,
|
||||
provider: trade.provider,
|
||||
date:
|
||||
DateFormat('HH:mm').format(item.trade.createdAt ?? DateTime.now()),
|
||||
amount: trade.amountFormatted(),
|
||||
receiveAmount: trade.receiveAmountFormatted(),
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
swapState: trade.state,
|
||||
),
|
||||
);
|
||||
} else if (item is DateSectionItem) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding),
|
||||
child: Text(DateFormatter.convertDateTimeToReadableString(item.date),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)));
|
||||
} else if (item is OrderListItem) {
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.orderDetails, arguments: item.order),
|
||||
child: HistoryOrderTile(
|
||||
date: DateFormat('HH:mm').format(item.order.createdAt),
|
||||
amount: item.orderFormattedAmount,
|
||||
amountFiat: "USD 0.00",
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
),
|
||||
);
|
||||
} else if (item is PayjoinTransactionListItem) {
|
||||
final session = item.session;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.payjoinDetails,
|
||||
arguments: [item.sessionId, item.transaction],
|
||||
),
|
||||
child: PayjoinHistoryTile(
|
||||
createdAt: DateFormat('HH:mm').format(session.inProgressSince!),
|
||||
amount: dashboardViewModel.appStore.amountParsingProxy
|
||||
.getDisplayCryptoString(
|
||||
session.amount.toInt(), CryptoCurrency.btc),
|
||||
currency: item.transaction?.from ?? "BTC",
|
||||
state: item.status,
|
||||
isSending: session.isSenderSession,
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: !roundedBottom),
|
||||
);
|
||||
} else if (item is AnonpayTransactionListItem) {
|
||||
final transactionInfo = item.transaction;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo),
|
||||
child: AnonpayHistoryTile(
|
||||
provider: transactionInfo.provider,
|
||||
createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt),
|
||||
amount: transactionInfo.fiatAmount?.toString() ??
|
||||
(transactionInfo.amountTo?.toString() ?? ''),
|
||||
currency: transactionInfo.fiatAmount != null
|
||||
? transactionInfo.fiatEquiv ?? ''
|
||||
: CryptoCurrency.fromFullName(transactionInfo.coinTo)
|
||||
.name
|
||||
.toUpperCase(),
|
||||
roundedTop: roundedTop,
|
||||
return _historyRow(
|
||||
onTap: () {
|
||||
final page =
|
||||
getIt.get<TransactionDetailsModal>(param1: transaction);
|
||||
if (detailsAsPage) {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) => Material(child: page)));
|
||||
} else {
|
||||
showMaterialModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
FractionallySizedBox(heightFactor: 0.9, child: page));
|
||||
}
|
||||
},
|
||||
child: HistoryTile(
|
||||
key: ValueKey("home_page_transaction_${transaction.id}_key"),
|
||||
title: item.formattedTitle + transactionType,
|
||||
date: _formatTransactionDate(item.date, localeName),
|
||||
amount: item.formattedCryptoAmount,
|
||||
amountFiat: item.formattedFiatAmount,
|
||||
hasTokens: item.hasTokens,
|
||||
chainIconPath: _getChainIconPath(),
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: !roundedBottom));
|
||||
} else
|
||||
return Text(item.runtimeType.toString());
|
||||
}),
|
||||
),
|
||||
),
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
direction: item.transaction.direction,
|
||||
pending: item.transaction.isPending,
|
||||
asset: asset,
|
||||
),
|
||||
);
|
||||
} else if (item is TradeListItem) {
|
||||
final trade = item.trade;
|
||||
final tradeFrom = trade.from;
|
||||
final tradeTo = trade.to;
|
||||
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.tradeDetails, arguments: trade),
|
||||
child: HistoryTradeTile(
|
||||
from: tradeFrom,
|
||||
to: tradeTo,
|
||||
provider: trade.provider,
|
||||
date: _formatTransactionDate(
|
||||
item.trade.createdAt ?? DateTime.now(), localeName),
|
||||
amount: dashboardViewModel.balanceDisplayMode ==
|
||||
BalanceDisplayMode.hiddenBalance
|
||||
? "---"
|
||||
: trade.amountFormatted(),
|
||||
receiveAmount: dashboardViewModel.balanceDisplayMode ==
|
||||
BalanceDisplayMode.hiddenBalance
|
||||
? "---"
|
||||
: trade.receiveAmountFormatted(),
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
swapState: trade.state,
|
||||
),
|
||||
);
|
||||
} else if (item is SpecificDateSectionItem) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding),
|
||||
child: Text(item.text,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)));
|
||||
} else if (item is DateSectionItem) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: topPadding),
|
||||
child: Text(DateFormat("MMMM yyyy", localeName).format(item.date),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)));
|
||||
} else if (item is OrderListItem) {
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.orderDetails, arguments: item.order),
|
||||
child: HistoryOrderTile(
|
||||
date: _formatTransactionDate(item.order.createdAt, localeName),
|
||||
amount: item.orderFormattedAmount,
|
||||
amountFiat: "",
|
||||
roundedBottom: roundedBottom,
|
||||
roundedTop: roundedTop,
|
||||
bottomSeparator: !roundedBottom,
|
||||
),
|
||||
);
|
||||
} else if (item is PayjoinTransactionListItem) {
|
||||
final session = item.session;
|
||||
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.payjoinDetails,
|
||||
arguments: [item.sessionId, item.transaction],
|
||||
),
|
||||
child: PayjoinHistoryTile(
|
||||
createdAt:
|
||||
_formatTransactionDate(session.inProgressSince!, localeName),
|
||||
amount: dashboardViewModel.appStore.amountParsingProxy
|
||||
.asDisplayString(Money(session.amount, CryptoCurrency.btc)),
|
||||
currency: item.transaction?.from ?? "BTC",
|
||||
state: item.status,
|
||||
isSending: session.isSenderSession,
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: !roundedBottom),
|
||||
);
|
||||
} else if (item is AnonpayTransactionListItem) {
|
||||
final transactionInfo = item.transaction;
|
||||
|
||||
return _historyRow(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.anonPayDetailsPage,
|
||||
arguments: transactionInfo),
|
||||
child: AnonpayHistoryTile(
|
||||
provider: transactionInfo.provider,
|
||||
createdAt: _formatTransactionDate(
|
||||
transactionInfo.createdAt, localeName),
|
||||
amount: transactionInfo.fiatAmount?.toString() ??
|
||||
(transactionInfo.amountTo?.toString() ?? ''),
|
||||
currency: transactionInfo.fiatAmount != null
|
||||
? transactionInfo.fiatEquiv ?? ''
|
||||
: CryptoCurrency.fromFullName(transactionInfo.coinTo)
|
||||
.name
|
||||
.toUpperCase(),
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: !roundedBottom));
|
||||
} else
|
||||
return Text(item.runtimeType.toString());
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@@ -197,4 +244,36 @@ class HistorySection extends StatelessWidget {
|
||||
return dashboardViewModel.wallet.currency.chainIconPath ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTransactionDate(DateTime date, String localeName) {
|
||||
final time = DateFormat.Hm(localeName);
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final thatDay = DateTime(date.year, date.month, date.day);
|
||||
final daysAgo = today.difference(thatDay).inDays;
|
||||
|
||||
final timeStr = time.format(date);
|
||||
|
||||
if (daysAgo == 0) {
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
if (daysAgo == 1) {
|
||||
return "${S.current.yesterday}, $timeStr";
|
||||
}
|
||||
|
||||
if (daysAgo < 7) {
|
||||
final weekday = DateFormat.EEEE(localeName).format(date);
|
||||
return "$weekday, $timeStr";
|
||||
}
|
||||
|
||||
if (date.year == now.year) {
|
||||
final dayMonth = DateFormat("d MMMM", localeName).format(date);
|
||||
return "$dayMonth, $timeStr";
|
||||
}
|
||||
|
||||
final full = DateFormat("d MMM yyyy", localeName).format(date);
|
||||
return "$full, $timeStr";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/select_deselect_all.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/filter_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class HistorySwapProvidersPage extends StatelessWidget {
|
||||
const HistorySwapProvidersPage({super.key, required this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).filter_swap_providers,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Observer(
|
||||
builder: (_) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: SelectDeselectAllBar(
|
||||
title: S.of(context).swap_providers,
|
||||
onSelected: (val) {
|
||||
if ((val &&
|
||||
dashboardViewModel.tradeFilterStore.enabledProvidersCount ==
|
||||
0) ||
|
||||
(!val &&
|
||||
dashboardViewModel.tradeFilterStore.enabledProvidersCount >
|
||||
0)) {
|
||||
dashboardViewModel.tradeFilterStore
|
||||
.toggleDisplayExchange(ExchangeProviderDescription.all);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
NewListSections(sections: {
|
||||
"": dashboardViewModel.exchangeFilterItems
|
||||
.whereType<SwapProviderFilterItem>()
|
||||
.map((item) => ListItemCheckbox(
|
||||
iconPath: item.providerDescription.image,
|
||||
keyValue: item.caption,
|
||||
label: item.caption,
|
||||
value: item.value(),
|
||||
onChanged: (val) => dashboardViewModel.tradeFilterStore
|
||||
.toggleDisplayExchange(item.providerDescription)))
|
||||
.toList()
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
@@ -80,13 +81,13 @@ class HistoryTile extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.onInverseSurface, shape: BoxShape.circle),
|
||||
child: HashImageWidget(
|
||||
imageUrl: _getDirectionIconToken(),
|
||||
height: 14,
|
||||
width: 14,
|
||||
colorFilter: ColorFilter.mode(
|
||||
direction == TransactionDirection.outgoing
|
||||
? Theme.of(context).colorScheme.inverseSurface.withAlpha(175)
|
||||
: Colors.green,
|
||||
BlendMode.srcIn),
|
||||
width: 14,
|
||||
height: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -99,10 +100,9 @@ class HistoryTile extends StatelessWidget {
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: pending ? 0.5 : 1,
|
||||
child: HashImageWidget(
|
||||
child: TokenImageWidget(
|
||||
imageUrl: asset?.iconPath ?? "",
|
||||
width: 34,
|
||||
height: 34,
|
||||
size: 34,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
@@ -142,7 +142,8 @@ class HistoryTile extends StatelessWidget {
|
||||
date: date,
|
||||
amount: amount,
|
||||
amountFiat: amountFiat,
|
||||
leadingIcon: _getLeadingIcon(context),
|
||||
// Decorative: `title` already reads out sent/received/pending.
|
||||
leadingIcon: ExcludeSemantics(child: _getLeadingIcon(context)),
|
||||
primaryTextColor: _getPrimaryTextColor(),
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
|
||||
@@ -87,7 +87,7 @@ class HistoryTileBase extends StatelessWidget {
|
||||
bottomLeft: Radius.circular(roundedBottom ? 22 : 0),
|
||||
bottomRight: Radius.circular(roundedBottom ? 22 : 0),
|
||||
)),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -153,7 +153,7 @@ class HistoryTileBase extends StatelessWidget {
|
||||
),
|
||||
if (bottomSeparator)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 56, right: 16),
|
||||
child: Container(
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HistoryTopBar extends StatelessWidget {
|
||||
const HistoryTopBar({super.key, required this.onTap, required this.roundedBottom});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final bool roundedBottom;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).history,
|
||||
onTap: onTap,
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
key: const ValueKey("history_top_bar_key"),
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 12,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox.shrink(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).history),
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_right.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (!roundedBottom)
|
||||
Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
|
||||
)
|
||||
else
|
||||
Container(height: 2)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
required this.roundedTop,
|
||||
required this.roundedBottom,
|
||||
required this.bottomSeparator,
|
||||
required this.from,
|
||||
required this.to,
|
||||
this.from,
|
||||
this.to,
|
||||
required this.swapState,
|
||||
required this.provider});
|
||||
|
||||
final CryptoCurrency from;
|
||||
final CryptoCurrency to;
|
||||
final CryptoCurrency? from;
|
||||
final CryptoCurrency? to;
|
||||
final ExchangeProviderDescription provider;
|
||||
final String date;
|
||||
final String amount;
|
||||
@@ -32,6 +32,10 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
final TradeState swapState;
|
||||
|
||||
Widget _getLeadingStack(BuildContext context) {
|
||||
if (from == null || to == null) {
|
||||
return SizedBox(width: 50, height: 50);
|
||||
}
|
||||
|
||||
double currencyIconSize = 22.0;
|
||||
|
||||
return SizedBox(
|
||||
@@ -40,7 +44,7 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: _getIconPath(from), width: currencyIconSize, height: currencyIconSize),
|
||||
imageUrl: _getIconPath(from!), width: currencyIconSize, height: currencyIconSize),
|
||||
Positioned(
|
||||
top: currencyIconSize / 2,
|
||||
left: currencyIconSize / 2,
|
||||
@@ -50,7 +54,7 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
Border.all(width: 2, color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
shape: BoxShape.circle),
|
||||
child: HashImageWidget(
|
||||
imageUrl: _getIconPath(to),
|
||||
imageUrl: _getIconPath(to!),
|
||||
width: currencyIconSize,
|
||||
height: currencyIconSize,
|
||||
),
|
||||
@@ -85,8 +89,10 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500)),
|
||||
Text(from.title, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
if (fromChainIcon.isNotEmpty)
|
||||
if (from?.title != null)
|
||||
Text(from!.title,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
if (fromChainIcon?.isNotEmpty ?? false)
|
||||
HashImageWidget(
|
||||
imageUrl: fromChainIcon,
|
||||
width: 12,
|
||||
@@ -106,10 +112,17 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
receiveAmount.withMaxDecimals(8),
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
to.title,
|
||||
),
|
||||
if (toChainIcon.isNotEmpty) HashImageWidget(imageUrl: toChainIcon, width: 12, height: 12)
|
||||
if (to != null)
|
||||
Text(
|
||||
to!.title,
|
||||
),
|
||||
if (toChainIcon?.isNotEmpty ?? false)
|
||||
HashImageWidget(
|
||||
imageUrl: toChainIcon,
|
||||
width: 12,
|
||||
height: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
)
|
||||
],
|
||||
),
|
||||
leadingIcon: _getLeadingStack(context),
|
||||
@@ -137,7 +150,8 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
return "";
|
||||
}
|
||||
|
||||
String _getChainIcon(CryptoCurrency currency) {
|
||||
String? _getChainIcon(CryptoCurrency? currency) {
|
||||
if (currency == null) return null;
|
||||
try {
|
||||
if (currency.title.isNotEmpty) {
|
||||
final parsedCurrency =
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart';
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/transaction_details/confirmations_list_item.dart';
|
||||
import 'package:hash_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
import 'package:hash_wallet/src/screens/transaction_details/address_list_item.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/utils/address_formatter.dart';
|
||||
import 'package:hash_wallet/view_model/transaction_details_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart";
|
||||
import "package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/copy_wrapper.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/src/screens/transaction_details/address_list_item.dart";
|
||||
import "package:hash_wallet/src/screens/transaction_details/confirmations_list_item.dart";
|
||||
import "package:hash_wallet/src/screens/transaction_details/transaction_details_list_item.dart";
|
||||
import "package:hash_wallet/src/widgets/new_list_row/new_list_section.dart";
|
||||
import "package:hash_wallet/utils/address_formatter.dart";
|
||||
import "package:hash_wallet/view_model/transaction_details_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
|
||||
class TransactionDetailsModal extends StatefulWidget {
|
||||
const TransactionDetailsModal({super.key, required this.transactionDetailsViewModel});
|
||||
const TransactionDetailsModal(
|
||||
{
|
||||
required this.transactionDetailsViewModel,
|
||||
this.highlightNoteField = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TransactionDetailsViewModel transactionDetailsViewModel;
|
||||
final bool highlightNoteField;
|
||||
|
||||
@override
|
||||
State<TransactionDetailsModal> createState() => _TransactionDetailsModalState();
|
||||
@@ -37,191 +45,237 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
|
||||
widget.transactionDetailsViewModel.updateNote(noteController.text);
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.highlightNoteField) {
|
||||
noteFocusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.9,
|
||||
minChildSize: 0.25,
|
||||
maxChildSize: 0.9,
|
||||
snap: true,
|
||||
snapSizes: const [0.9],
|
||||
builder: (context, controller) => SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(25))),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).transaction,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: widget.transactionDetailsViewModel.transactionAsset
|
||||
.iconPath ??
|
||||
"",
|
||||
width: 64,
|
||||
height: 64),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.formattedTitle +
|
||||
widget.transactionDetailsViewModel.formattedStatus,
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.formattedCryptoAmount,
|
||||
style: TextStyle(fontSize: 28),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
NewListSections(sections: {
|
||||
"": widget.transactionDetailsViewModel.items
|
||||
.map((item) {
|
||||
if (item.value.isEmpty) return null;
|
||||
|
||||
final shouldBuildBottomWidget =
|
||||
item.value.length > 25;
|
||||
|
||||
return ListItemRegularRow(
|
||||
copyableText: item.value,
|
||||
showArrow: false,
|
||||
keyValue:
|
||||
((item.key as ValueKey?)?.value as String?) ??
|
||||
item.title,
|
||||
label: item.title,
|
||||
trailingWidget: shouldBuildBottomWidget
|
||||
? null
|
||||
: _buildTrailingWIdget(item),
|
||||
bottomWidget: shouldBuildBottomWidget
|
||||
? _buildBottomWidget(item)
|
||||
: null);
|
||||
})
|
||||
.whereType<ListItem>()
|
||||
.toList(),
|
||||
}),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).note),
|
||||
TextField(
|
||||
focusNode: noteFocusNode,
|
||||
controller: noteController,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).add_a_note,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => NewListSections(sections: {
|
||||
"view tx": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "view tx on",
|
||||
label: widget.transactionDetailsViewModel
|
||||
.explorerDescription,
|
||||
onTap: widget
|
||||
.transactionDetailsViewModel.launchExplorer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
trailingIconPath: "assets/new-ui/link_arrow.svg",
|
||||
trailingIconSize: 8)
|
||||
],
|
||||
if (widget.transactionDetailsViewModel.canReplaceByFee)
|
||||
"rbf": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "replace by fee",
|
||||
label: S.of(context).bump_fee,
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.bumpFeePage, arguments: [
|
||||
widget.transactionDetailsViewModel
|
||||
.transactionInfo,
|
||||
widget.transactionDetailsViewModel
|
||||
.rawTransaction
|
||||
]);
|
||||
})
|
||||
]
|
||||
}),
|
||||
)
|
||||
],
|
||||
Widget build(BuildContext context) => SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).transaction,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Column(
|
||||
children: [
|
||||
TokenImageWidget(
|
||||
imageUrl:
|
||||
widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "",
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.formattedTitle +
|
||||
widget.transactionDetailsViewModel.formattedStatus,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => CopyWrapper(
|
||||
requireLongPress: true,
|
||||
data: ClipboardData(
|
||||
text: widget.transactionDetailsViewModel.transactionCopyAmount,
|
||||
),
|
||||
builder: (context, copied) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
key: ValueKey(copied),
|
||||
copied
|
||||
? S.of(context).copied
|
||||
: widget.transactionDetailsViewModel.transactionAmount,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
color: copied
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
NewListSections(
|
||||
sections: {
|
||||
"": widget.transactionDetailsViewModel.items
|
||||
.map((item) {
|
||||
if (item.value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final shouldBuildBottomWidget = item.value.length > 25;
|
||||
|
||||
return ListItemRegularRow(
|
||||
copyableText: item.value,
|
||||
showArrow: false,
|
||||
keyValue: ((item.key as ValueKey?)?.value as String?) ??
|
||||
item.title,
|
||||
label: item.title,
|
||||
trailingWidget: shouldBuildBottomWidget
|
||||
? null
|
||||
: _buildTrailingWidget(item),
|
||||
bottomWidget: shouldBuildBottomWidget
|
||||
? _buildBottomWidget(item)
|
||||
: null,
|
||||
);
|
||||
})
|
||||
.whereType<ListItem>()
|
||||
.toList(),
|
||||
},
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).note),
|
||||
TextField(
|
||||
focusNode: noteFocusNode,
|
||||
controller: noteController,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).add_a_note,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => NewListSections(
|
||||
sections: {
|
||||
"view tx": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "view tx on",
|
||||
label: widget
|
||||
.transactionDetailsViewModel.explorerDescription,
|
||||
onTap: widget.transactionDetailsViewModel.launchExplorer,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
trailingIconPath: "assets/new-ui/link_arrow.svg",
|
||||
trailingIconSize: 8,
|
||||
),
|
||||
],
|
||||
if (widget.transactionDetailsViewModel.canReplaceByFee)
|
||||
"rbf": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "replace by fee",
|
||||
label: S.of(context).bump_fee,
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.bumpFeePage,
|
||||
arguments: [
|
||||
widget
|
||||
.transactionDetailsViewModel.transactionInfo,
|
||||
widget.transactionDetailsViewModel.rawTransaction,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).viewPadding.bottom)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
SizedBox(height: MediaQuery.of(context).viewPadding.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildTrailingWIdget(TransactionDetailsListItem item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: switch (item.runtimeType) {
|
||||
ConfirmationsListItem => Row(
|
||||
children: [
|
||||
Text((item as ConfirmationsListItem).current.toString(),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
|
||||
if (item.needed > 0)
|
||||
Text("/${item.needed}",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
),
|
||||
),
|
||||
_ => Text(
|
||||
item.value,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildTrailingWidget(TransactionDetailsListItem item) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: switch (item.runtimeType) {
|
||||
ConfirmationsListItem => Row(
|
||||
children: [
|
||||
Text(
|
||||
(item as ConfirmationsListItem).current.toString(),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
if (item.needed > 0)
|
||||
Text(
|
||||
"/${item.needed}",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
_ => Text(
|
||||
item.value,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildBottomWidget(TransactionDetailsListItem item) {
|
||||
return switch (item.runtimeType) {
|
||||
AddressListItem => AddressFormatter.buildSegmentedAddress(
|
||||
address: item.value,
|
||||
evenTextStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "IBM Plex Mono",
|
||||
color: Theme.of(context).colorScheme.onSurface)),
|
||||
AddressListItem => _segmentedAddressList(item.value),
|
||||
_ => Text(
|
||||
item.value,
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Widget _segmentedAddressList(String value) {
|
||||
final style = TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "IBM Plex Mono",
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
final lines = value
|
||||
.split(RegExp(r'[,\n]'))
|
||||
.map((final line) => line.trim())
|
||||
.where((final line) => line.isNotEmpty)
|
||||
.toList();
|
||||
if (lines.length <= 1) {
|
||||
return AddressFormatter.buildSegmentedAddress(
|
||||
address: lines.isNotEmpty ? lines.first : value.trim(),
|
||||
evenTextStyle: style,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final line in lines)
|
||||
AddressFormatter.buildSegmentedAddress(address: line, evenTextStyle: style),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/money/money_text.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class BalanceCardAction {
|
||||
final String label;
|
||||
@@ -23,7 +24,7 @@ class BalanceCard extends StatelessWidget {
|
||||
this.borderRadius = 20,
|
||||
this.selected = false,
|
||||
this.accountName = "",
|
||||
this.accountBalance = "",
|
||||
this.accountBalance,
|
||||
this.balance = "",
|
||||
this.fiatBalance = "",
|
||||
this.assetName = "",
|
||||
@@ -38,7 +39,7 @@ class BalanceCard extends StatelessWidget {
|
||||
final double width;
|
||||
final double borderRadius;
|
||||
final Gradient? gradient;
|
||||
final String accountBalance;
|
||||
final Money? accountBalance;
|
||||
final String accountName;
|
||||
final String balance;
|
||||
final String fiatBalance;
|
||||
@@ -63,7 +64,7 @@ class BalanceCard extends StatelessWidget {
|
||||
|
||||
final leadText = fiatFirst ? S.of(context).wallet_balance : accountName;
|
||||
|
||||
final bool showText = accountBalance.isNotEmpty ||
|
||||
final bool showText = accountBalance != null ||
|
||||
leadText.isNotEmpty ||
|
||||
balance.isNotEmpty ||
|
||||
fiatBalance.isNotEmpty ||
|
||||
@@ -92,15 +93,18 @@ class BalanceCard extends StatelessWidget {
|
||||
? ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
key: ValueKey(design.imagePath),
|
||||
child: HashImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
// Purely decorative card artwork.
|
||||
child: ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('svgFullOff'),
|
||||
key: ValueKey("svgFullOff"),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -115,7 +119,7 @@ class BalanceCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (leadText.isNotEmpty || accountBalance.isNotEmpty)
|
||||
if (leadText.isNotEmpty || accountBalance != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -147,9 +151,16 @@ class BalanceCard extends StatelessWidget {
|
||||
AnimatedOpacity(
|
||||
opacity: selected ? 0 : 1,
|
||||
duration: textFadeDuration,
|
||||
child: Text(
|
||||
accountBalance,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 14),
|
||||
// Opacity alone keeps the text readable by screen
|
||||
// readers, so drop it while it is invisible.
|
||||
child: ExcludeSemantics(
|
||||
excluding: selected,
|
||||
child: MoneyText.optional(
|
||||
accountBalance,
|
||||
fractionalDigits: accountBalance?.decimals ?? 8,
|
||||
showSymbol: false,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -157,40 +168,51 @@ class BalanceCard extends StatelessWidget {
|
||||
AnimatedOpacity(
|
||||
opacity: selected ? 1 : 0,
|
||||
duration: textFadeDuration,
|
||||
child: AnimatedSwitcher(
|
||||
duration: designSwitchDuration,
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
// Only the selected card's balance is visible, so only it
|
||||
// may be announced.
|
||||
child: ExcludeSemantics(
|
||||
excluding: !selected,
|
||||
child: AnimatedSwitcher(
|
||||
duration: designSwitchDuration,
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: design.colors.textColor,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.4),
|
||||
child: !fiatFirst && balance.isEmpty
|
||||
? MoneyText.optional(
|
||||
accountBalance,
|
||||
fractionalDigits: accountBalance?.decimals ?? 8,
|
||||
showSymbol: false,
|
||||
)
|
||||
: Text(fiatFirst ? fiatBalance : balance),
|
||||
),
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: design.colors.textColorSecondary,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.4),
|
||||
child: Text(resolvedAssetName),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: design.colors.textColor,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.4),
|
||||
child: Text(fiatFirst ? fiatBalance : balance),
|
||||
),
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: designSwitchDuration,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: design.colors.textColorSecondary,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.4),
|
||||
child: Text(resolvedAssetName),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -203,17 +225,19 @@ class BalanceCard extends StatelessWidget {
|
||||
child: AnimatedSwitcher(
|
||||
duration: designSwitchDuration,
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
final currentKey = currentChild?.key;
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
for (final child in previousChildren)
|
||||
if (child.key != currentKey) child,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
key: ValueKey(fiatFirst ? balance : fiatBalance),
|
||||
fiatFirst ? "$assetName $balance" : "$fiatCurrencyTitle $fiatBalance",
|
||||
fiatFirst ? "$assetName $balance" : fiatBalance,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -251,18 +275,12 @@ class BalanceCard extends StatelessWidget {
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
child: design.backgroundType == CardDesignBackgroundTypes.svgIcon
|
||||
? HashImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
key: const ValueKey('svgIcon'),
|
||||
height: iconWidth,
|
||||
width: iconWidth,
|
||||
colorFilter: ColorFilter.mode(
|
||||
design.colors.backgroundImageColor.withAlpha(80),
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
// Purely decorative card artwork.
|
||||
? ExcludeSemantics(
|
||||
child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('svgIconOff'),
|
||||
key: ValueKey("svgIconOff"),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -276,18 +294,28 @@ class BalanceCard extends StatelessWidget {
|
||||
child: AnimatedOpacity(
|
||||
duration: designSwitchDuration,
|
||||
opacity: onCustomizeTapped == null ? 0 : 1,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onCustomizeTapped,
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Center(
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/3dots_vertical.svg",
|
||||
alignment: Alignment.topRight,
|
||||
colorFilter:
|
||||
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
|
||||
// Faded out and inert: must not be a focusable phantom control.
|
||||
child: ExcludeSemantics(
|
||||
excluding: onCustomizeTapped == null,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: S.of(context).wallet_menu,
|
||||
onTap: onCustomizeTapped,
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onCustomizeTapped,
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Center(
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/3dots_vertical.svg",
|
||||
alignment: Alignment.topRight,
|
||||
colorFilter:
|
||||
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -299,28 +327,58 @@ class BalanceCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector(
|
||||
Widget getBalanceCardActionButton(BalanceCardAction action) => Semantics(
|
||||
button: true,
|
||||
label: action.label,
|
||||
onTap: action.onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: design.colors.backgroundImageColor.withAlpha(75),
|
||||
borderRadius: BorderRadius.circular(10000000),
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
action.label,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 16),
|
||||
),
|
||||
child: ExcludeSemantics(
|
||||
child: GestureDetector(
|
||||
onTap: action.onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: design.colors.backgroundImageColor.withAlpha(75),
|
||||
borderRadius: BorderRadius.circular(10000000),
|
||||
),
|
||||
Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
|
||||
],
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
action.label,
|
||||
style: TextStyle(color: design.colors.textColor, fontSize: 16),
|
||||
),
|
||||
),
|
||||
Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _CornerSvgIcon extends StatelessWidget {
|
||||
const _CornerSvgIcon({required this.design, required this.iconWidth});
|
||||
|
||||
final CardDesign design;
|
||||
final double iconWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HashImageWidget(
|
||||
imageUrl: design.imagePath,
|
||||
key: ValueKey(design.imagePath),
|
||||
height: iconWidth,
|
||||
width: iconWidth,
|
||||
colorFilter: design.preColoredIcon
|
||||
? null
|
||||
: ColorFilter.mode(
|
||||
design.colors.backgroundImageColor.withValues(alpha: 0.33),
|
||||
BlendMode.dstIn,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'package:hash_wallet/core/utilities.dart';
|
||||
import 'package:cw_core/balance_card_layout.dart';
|
||||
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
@@ -8,11 +10,12 @@ import 'package:hash_wallet/entities/provider_types.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/send_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/payment_request.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
@@ -29,14 +32,14 @@ import 'balance_card.dart';
|
||||
class CardsView extends StatefulWidget {
|
||||
const CardsView(
|
||||
{super.key,
|
||||
required this.dashboardViewModel,
|
||||
required this.accountListViewModel,
|
||||
required this.lightningMode,
|
||||
required this.onCompactModeBackgroundCardsTapped,
|
||||
required this.onCustomizeTapped});
|
||||
required this.dashboardViewModel,
|
||||
required this.accountListViewModel,
|
||||
required this.lightningMode,
|
||||
required this.onCompactModeBackgroundCardsTapped,
|
||||
required this.onCustomizeTapped});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final MoneroAccountListViewModel? accountListViewModel;
|
||||
final WalletAccountListViewModel? accountListViewModel;
|
||||
final VoidCallback onCompactModeBackgroundCardsTapped;
|
||||
final VoidCallback onCustomizeTapped;
|
||||
final bool lightningMode;
|
||||
@@ -48,22 +51,39 @@ class CardsView extends StatefulWidget {
|
||||
class _CardsViewState extends State<CardsView> {
|
||||
late int _selectedIndex;
|
||||
bool isFirstBuild = true;
|
||||
ReactionDisposer? _cardOrderReaction;
|
||||
|
||||
int get _fallbackSelectedIndex =>
|
||||
max(0, widget.dashboardViewModel.cardOrder.length - 1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
|
||||
reaction(
|
||||
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
|
||||
(_) => setState(() {
|
||||
_selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
|
||||
}));
|
||||
_selectedIndex = _fallbackSelectedIndex;
|
||||
_cardOrderReaction = reaction(
|
||||
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
|
||||
(_) {
|
||||
if (mounted) setState(() => _selectedIndex = _fallbackSelectedIndex);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cardOrderReaction?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const Duration animDuration = Duration(milliseconds: 200);
|
||||
static const int compactModeTreshold = 4;
|
||||
static const int maxCards = 5;
|
||||
|
||||
/// No account name on the card: Lightning card, or Bitcoin with multi-accounts off.
|
||||
bool get _hideAccountInfo =>
|
||||
widget.lightningMode ||
|
||||
(widget.dashboardViewModel.wallet.type == WalletType.bitcoin &&
|
||||
!widget.dashboardViewModel.isMultiAccountsEnabled);
|
||||
|
||||
Widget _buildCard(int visualIndex, int realIndex, int numCards, double parentWidth,
|
||||
Map<int, int> order, bool compactMode, double overlapAmount) {
|
||||
final baseTop = overlapAmount * (numCards - 1);
|
||||
@@ -74,11 +94,32 @@ class _CardsViewState extends State<CardsView> {
|
||||
|
||||
final top = baseTop - (howFarBehind * overlapAmount);
|
||||
|
||||
final double cardWidth = MediaQuery.of(context).size.width * 0.878;
|
||||
final double effectiveCardWidth = min(cardWidth, 768);
|
||||
|
||||
final left = (parentWidth - effectiveCardWidth) / 2.0;
|
||||
|
||||
final isSelected = _selectedIndex == visualIndex;
|
||||
final accounts = widget.accountListViewModel?.accounts;
|
||||
final account = accounts?.firstWhereOrNull((account) => account.id == realIndex);
|
||||
final cardLabel = !_hideAccountInfo && account != null ? account.label : S.of(context).balance;
|
||||
|
||||
Future<void> onCardTap() async {
|
||||
// printV(visualIndex);
|
||||
if (compactMode && visualIndex != 0) {
|
||||
widget.onCompactModeBackgroundCardsTapped();
|
||||
} else if (!compactMode) {
|
||||
if (account != null && !_hideAccountInfo) {
|
||||
await widget.accountListViewModel!.select(account);
|
||||
}
|
||||
if (mounted) setState(() => _selectedIndex = visualIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void onCardLongPress() {
|
||||
if (_selectedIndex == visualIndex) {
|
||||
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
|
||||
}
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
|
||||
return AnimatedPositioned(
|
||||
key: ValueKey("$visualIndex $realIndex"),
|
||||
duration: animDuration,
|
||||
@@ -89,125 +130,117 @@ class _CardsViewState extends State<CardsView> {
|
||||
duration: animDuration,
|
||||
curve: Curves.easeOut,
|
||||
scale: scale,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
// printV(visualIndex);
|
||||
if (compactMode && visualIndex != 0) {
|
||||
widget.onCompactModeBackgroundCardsTapped();
|
||||
} else if (!compactMode) {
|
||||
setState(() {
|
||||
if (widget.accountListViewModel != null)
|
||||
widget.accountListViewModel!
|
||||
.select(widget.accountListViewModel!.accounts[realIndex]);
|
||||
_selectedIndex = visualIndex;
|
||||
});
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (_selectedIndex == visualIndex) {
|
||||
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
|
||||
}
|
||||
;
|
||||
HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: Observer(builder: (_) {
|
||||
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
|
||||
return Container();
|
||||
}
|
||||
final account = widget.accountListViewModel?.accounts[realIndex];
|
||||
// The card is the tap target; the balances and the card's own buttons stay
|
||||
// reachable as children of this node.
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: isSelected,
|
||||
label: cardLabel,
|
||||
hint: isSelected
|
||||
? (widget.dashboardViewModel.balanceViewModel.displayMode ==
|
||||
BalanceDisplayMode.hiddenBalance
|
||||
? S.of(context).long_press_show_balance
|
||||
: S.of(context).long_press_hide_balance)
|
||||
: null,
|
||||
onTap: onCardTap,
|
||||
onLongPress: isSelected ? onCardLongPress : null,
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: true,
|
||||
onTap: onCardTap,
|
||||
onLongPress: onCardLongPress,
|
||||
child: Observer(builder: (_) {
|
||||
final account = widget.accountListViewModel?.accounts
|
||||
.firstWhereOrNull((account) => account.id == realIndex);
|
||||
|
||||
// The second balance should always be the lightning balance
|
||||
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
|
||||
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
|
||||
.getMainBalanceRecord(widget.lightningMode);
|
||||
// The second balance should always be the lightning balance
|
||||
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
|
||||
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
|
||||
.getMainBalanceRecord(widget.lightningMode);
|
||||
|
||||
late final String walletBalance;
|
||||
late final String walletFiatBalance;
|
||||
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
|
||||
if (widget.dashboardViewModel.balanceViewModel.displayMode ==
|
||||
BalanceDisplayMode.hiddenBalance) {
|
||||
walletBalance = '●●●●●●';
|
||||
walletFiatBalance = '●●●●●●';
|
||||
late final String walletBalance;
|
||||
late final String walletFiatBalance;
|
||||
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
|
||||
if (widget.dashboardViewModel.balanceViewModel.displayMode ==
|
||||
BalanceDisplayMode.hiddenBalance) {
|
||||
walletBalance = '●●●●●●';
|
||||
walletFiatBalance = '●●●●●●';
|
||||
} else {
|
||||
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
|
||||
}
|
||||
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
|
||||
walletBalance = "";
|
||||
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
|
||||
} else {
|
||||
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
|
||||
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
|
||||
}
|
||||
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
|
||||
walletBalance = "";
|
||||
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
|
||||
} else {
|
||||
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
|
||||
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalanceRaw ?? "0.00";
|
||||
}
|
||||
|
||||
// the card designs is empty if widget gets built before it loads.
|
||||
// should get populated before user sees anything
|
||||
final CardDesign cardDesign;
|
||||
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
|
||||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
|
||||
cardDesign = CardDesign.genericDefault;
|
||||
else if (widget.lightningMode)
|
||||
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
|
||||
else
|
||||
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
|
||||
// the card designs is empty if widget gets built before it loads.
|
||||
// should get populated before user sees anything
|
||||
final designIndex = widget.dashboardViewModel.cardAccountIndices.indexOf(
|
||||
widget.lightningMode ? -2 : realIndex,
|
||||
);
|
||||
final cardDesign = designIndex < 0 ||
|
||||
designIndex >= widget.dashboardViewModel.cardDesigns.length
|
||||
? CardDesign.genericDefault
|
||||
: widget.dashboardViewModel.cardDesigns[designIndex];
|
||||
|
||||
final String accountName;
|
||||
final String accountBalance;
|
||||
if (account == null) {
|
||||
accountName = "";
|
||||
accountBalance = "";
|
||||
} else {
|
||||
accountName = account.label;
|
||||
accountBalance = account.balance ?? "0.00";
|
||||
}
|
||||
final showAccount = account != null && !_hideAccountInfo;
|
||||
final accountName = showAccount ? account.label : "";
|
||||
final accountBalance = showAccount ? account.balance : null;
|
||||
|
||||
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
|
||||
? ""
|
||||
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
|
||||
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
|
||||
? ""
|
||||
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
|
||||
|
||||
final List<BalanceCardAction> actions = widget.lightningMode
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.bitcoin_lightning_deposit,
|
||||
icon: Icons.arrow_downward,
|
||||
onTap: depositToL2,
|
||||
),
|
||||
BalanceCardAction(
|
||||
label: S.current.bitcoin_lightning_withdraw,
|
||||
icon: Icons.arrow_upward,
|
||||
onTap: withdrawFromL2,
|
||||
)
|
||||
]
|
||||
: (widget.dashboardViewModel.isEnabledTradeAction &&
|
||||
ProvidersHelper.getAvailableBuyProviderTypes().isNotEmpty)
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.buy,
|
||||
icon: Icons.arrow_forward_ios_rounded,
|
||||
iconSize: 12,
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage),
|
||||
)
|
||||
]
|
||||
: [];
|
||||
final List<BalanceCardAction> actions = widget.lightningMode
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.bitcoin_lightning_deposit,
|
||||
icon: Icons.arrow_downward,
|
||||
onTap: depositToL2,
|
||||
),
|
||||
BalanceCardAction(
|
||||
label: S.current.bitcoin_lightning_withdraw,
|
||||
icon: Icons.arrow_upward,
|
||||
onTap: withdrawFromL2,
|
||||
)
|
||||
]
|
||||
: (widget.dashboardViewModel.isEnabledTradeAction &&
|
||||
ProvidersHelper.getAvailableBuyProviderTypes().isNotEmpty)
|
||||
? [
|
||||
BalanceCardAction(
|
||||
label: S.current.buy,
|
||||
icon: Icons.arrow_forward_ios_rounded,
|
||||
iconSize: 12,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context, builder: (context) => BuySellSelectorModal());
|
||||
},
|
||||
)
|
||||
]
|
||||
: [];
|
||||
|
||||
return BalanceCard(
|
||||
width: effectiveCardWidth,
|
||||
accountName: accountName,
|
||||
accountBalance: accountBalance,
|
||||
designSwitchDuration: Duration(milliseconds: 150),
|
||||
assetName: assetName,
|
||||
capitalizeAssetName: _shouldCapitalizeAssetName(),
|
||||
balance: walletBalance,
|
||||
fiatCurrencyTitle: walletBalanceRecord?.fiatCurrencyTicker ??
|
||||
widget.dashboardViewModel.settingsStore.fiatCurrency.title,
|
||||
fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
|
||||
fiatBalance: walletFiatBalance,
|
||||
selected: _selectedIndex == visualIndex,
|
||||
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
|
||||
design: cardDesign,
|
||||
actions: actions,
|
||||
);
|
||||
}),
|
||||
return BalanceCard(
|
||||
width: effectiveCardWidth,
|
||||
accountName: accountName,
|
||||
accountBalance: accountBalance,
|
||||
designSwitchDuration: Duration(milliseconds: 150),
|
||||
assetName: assetName,
|
||||
capitalizeAssetName: _shouldCapitalizeAssetName(),
|
||||
balance: walletBalance,
|
||||
fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
|
||||
widget.dashboardViewModel.settingsStore.fiatCurrency.title,
|
||||
fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
|
||||
fiatBalance: walletFiatBalance,
|
||||
selected: _selectedIndex == visualIndex,
|
||||
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
|
||||
design: cardDesign,
|
||||
actions: actions,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -234,45 +267,46 @@ class _CardsViewState extends State<CardsView> {
|
||||
}
|
||||
}
|
||||
|
||||
double get effectiveCardWidth => min(MediaQuery.of(context).size.width * 0.878,
|
||||
responsiveLayoutUtil.shouldRenderMobileUI ? 768 : 512);
|
||||
|
||||
double _getBoxHeight(int numCards, double overlapAmount) {
|
||||
final double cardWidth = MediaQuery.of(context).size.width * 0.878;
|
||||
final double effectiveCardWidth = min(cardWidth, 768);
|
||||
return
|
||||
/* height of initial card */
|
||||
(2 / 3.2) * (effectiveCardWidth) +
|
||||
/* height of bg card * amount of bg cards */
|
||||
overlapAmount * ((numCards) - 1);
|
||||
/* height of initial card */
|
||||
(2 / 3.2) * (effectiveCardWidth) +
|
||||
/* height of bg card * amount of bg cards */
|
||||
overlapAmount * ((numCards) - 1);
|
||||
}
|
||||
|
||||
|
||||
int? _visualIndexForSelectedAccount(Map<int, int> order) {
|
||||
final vm = widget.accountListViewModel;
|
||||
if (vm == null || _hideAccountInfo) return null;
|
||||
|
||||
final selectedId = vm.selectedAccount?.id;
|
||||
for (final entry in order.entries) {
|
||||
if (entry.value == selectedId) return entry.key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(builder: (context) {
|
||||
Widget build(BuildContext context) => Observer(builder: (context) {
|
||||
final parentWidth = MediaQuery.of(context).size.width;
|
||||
final children = <Widget>[];
|
||||
|
||||
int numCards = widget.dashboardViewModel.wallet.type == WalletType.bitcoin
|
||||
? 1
|
||||
: widget.dashboardViewModel.cardDesigns.length;
|
||||
if (numCards == 0) numCards = 1;
|
||||
|
||||
if (_selectedIndex >= (numCards)) {
|
||||
_selectedIndex = 0;
|
||||
final stack = _hideAccountInfo
|
||||
? const [0]
|
||||
: BalanceCardLayout.stackFromOrder(widget.dashboardViewModel.cardOrder);
|
||||
if (stack.isEmpty) return const SizedBox.shrink();
|
||||
final numCards = stack.length;
|
||||
if (_selectedIndex < 0 || _selectedIndex >= numCards) {
|
||||
_selectedIndex = numCards - 1;
|
||||
}
|
||||
final order = {for (int i = 0; i < stack.length; i++) i: stack[i]};
|
||||
|
||||
Map<int, int> order = widget.dashboardViewModel.cardOrder.length != numCards
|
||||
? Map<int, int>.fromEntries(
|
||||
List.generate(numCards, (i) => MapEntry(i, i)),
|
||||
)
|
||||
: widget.dashboardViewModel.cardOrder;
|
||||
|
||||
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
|
||||
int visualIndex = (_selectedIndex - i + numCards) % numCards;
|
||||
if (order[visualIndex] == null) {
|
||||
order = Map<int, int>.fromEntries(
|
||||
List.generate(numCards, (i) => MapEntry(i, i)),
|
||||
);
|
||||
}
|
||||
}
|
||||
final followIndex = _visualIndexForSelectedAccount(order);
|
||||
if (followIndex != null) _selectedIndex = followIndex;
|
||||
|
||||
final bool compactMode = numCards >= compactModeTreshold;
|
||||
final double overlapAmount = compactMode ? 5.0 : 46.0;
|
||||
@@ -281,14 +315,6 @@ class _CardsViewState extends State<CardsView> {
|
||||
|
||||
int realIndex = order[visualIndex]!;
|
||||
|
||||
if (visualIndex == _selectedIndex &&
|
||||
widget.accountListViewModel != null &&
|
||||
realIndex < widget.accountListViewModel!.accounts.length &&
|
||||
widget.accountListViewModel?.selected.label !=
|
||||
widget.accountListViewModel?.accounts[realIndex].label) {
|
||||
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
|
||||
}
|
||||
|
||||
children.add(_buildCard(
|
||||
visualIndex, realIndex, numCards, parentWidth, order, compactMode, overlapAmount));
|
||||
}
|
||||
@@ -306,7 +332,6 @@ class _CardsViewState extends State<CardsView> {
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> depositToL2() async {
|
||||
PaymentRequest? paymentRequest = null;
|
||||
@@ -318,19 +343,19 @@ class _CardsViewState extends State<CardsView> {
|
||||
}
|
||||
} else if (widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
final depositAddress =
|
||||
await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
|
||||
await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
|
||||
if ((depositAddress?.isNotEmpty ?? false)) {
|
||||
paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress"));
|
||||
}
|
||||
}
|
||||
|
||||
if (FeatureFlag.hasNewUiExtraPages && widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
if (widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
final page = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
initialPaymentRequest: paymentRequest,
|
||||
unspentCoinType: UnspentCoinType.nonMweb,
|
||||
mode: SendPageModes.lightningDeposit,
|
||||
));
|
||||
initialPaymentRequest: paymentRequest,
|
||||
unspentCoinType: UnspentCoinType.nonMweb,
|
||||
mode: SendPageModes.lightningDeposit,
|
||||
));
|
||||
showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(128),
|
||||
@@ -371,13 +396,13 @@ class _CardsViewState extends State<CardsView> {
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
}
|
||||
|
||||
if (FeatureFlag.hasNewUiExtraPages && widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
if (widget.dashboardViewModel.type == WalletType.bitcoin) {
|
||||
final page = getIt.get<NewSendPage>(
|
||||
param1: SendPageParams(
|
||||
initialPaymentRequest: paymentRequest,
|
||||
unspentCoinType: unspentCoinType,
|
||||
mode: SendPageModes.lightningWithdrawal,
|
||||
));
|
||||
initialPaymentRequest: paymentRequest,
|
||||
unspentCoinType: unspentCoinType,
|
||||
mode: SendPageModes.lightningWithdrawal,
|
||||
));
|
||||
showCupertinoModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withAlpha(128),
|
||||
|
||||
@@ -21,51 +21,63 @@ class MwebAd extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
|
||||
child: Container(
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(
|
||||
S.of(context).mweb_ad,
|
||||
softWrap: true,
|
||||
style: TextStyle(fontSize: 12),
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
|
||||
child: Container(
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(
|
||||
S.of(context).mweb_ad,
|
||||
softWrap: true,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
size: 16,
|
||||
Icons.arrow_forward_ios,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
size: 16,
|
||||
Icons.arrow_forward_ios,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => dashboardViewModel.dismissMwebAd(false),
|
||||
child: Text(
|
||||
S.of(context).do_not_show_anymore,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => dashboardViewModel.dismissMwebAd(false),
|
||||
child: Text(
|
||||
S.of(context).do_not_show_anymore,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class SeedBackupReminderCard extends StatelessWidget {
|
||||
const SeedBackupReminderCard({required this.dashboardViewModel, required this.onTap, super.key});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Observer(
|
||||
builder: (_) {
|
||||
if (!dashboardViewModel.shouldShowSeedBackupReminder) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18, top: 24),
|
||||
child: MergeSemantics(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/key.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).back_up_recovery_phrase,
|
||||
style: textTheme.labelLarge?.copyWith(
|
||||
color: colors.onSurface,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
S.of(context).back_up_recovery_phrase_description,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
letterSpacing: -0.06,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.chevron_right, size: 16, color: colors.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum _IconShape {
|
||||
alreadyCircular,
|
||||
clipOnly,
|
||||
hollowNeedsBackdrop,
|
||||
}
|
||||
|
||||
class TokenImageWidget extends StatefulWidget {
|
||||
const TokenImageWidget({
|
||||
super.key,
|
||||
required this.imageUrl,
|
||||
required this.size,
|
||||
this.errorWidget,
|
||||
this.semanticsLabel,
|
||||
});
|
||||
|
||||
final String imageUrl;
|
||||
final double size;
|
||||
final Widget? errorWidget;
|
||||
|
||||
/// Accessible name for the token artwork. Leave `null` (the default) when the
|
||||
/// token is already named by adjacent text, so the image stays decorative.
|
||||
final String? semanticsLabel;
|
||||
|
||||
@override
|
||||
State<TokenImageWidget> createState() => _TokenImageWidgetState();
|
||||
}
|
||||
|
||||
class _TokenImageWidgetState extends State<TokenImageWidget> {
|
||||
static final Map<String, _IconShape> _shapeCache = {};
|
||||
|
||||
_IconShape _shape = _IconShape.clipOnly;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_resolveShape();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TokenImageWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.imageUrl != widget.imageUrl) {
|
||||
_resolveShape();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resolveShape() async {
|
||||
final url = widget.imageUrl;
|
||||
|
||||
final cached = _shapeCache[url];
|
||||
if (cached != null) {
|
||||
if (mounted && _shape != cached) {
|
||||
setState(() => _shape = cached);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final shape = await _analyzeShape(url);
|
||||
_shapeCache[url] = shape;
|
||||
if (mounted) {
|
||||
setState(() => _shape = shape);
|
||||
}
|
||||
}
|
||||
|
||||
Future<_IconShape> _analyzeShape(String url) async {
|
||||
if (url.toLowerCase().endsWith('.svg')) return _IconShape.alreadyCircular;
|
||||
|
||||
final ImageProvider provider;
|
||||
if (url.startsWith('assets/')) {
|
||||
provider = AssetImage(url);
|
||||
} else if (url.startsWith('http')) {
|
||||
provider = NetworkImage(url);
|
||||
} else {
|
||||
return _IconShape.clipOnly;
|
||||
}
|
||||
|
||||
try {
|
||||
final completer = Completer<ui.Image>();
|
||||
final stream = provider.resolve(ImageConfiguration.empty);
|
||||
late final ImageStreamListener listener;
|
||||
listener = ImageStreamListener(
|
||||
(info, _) {
|
||||
if (!completer.isCompleted) completer.complete(info.image);
|
||||
},
|
||||
onError: (error, _) {
|
||||
if (!completer.isCompleted) completer.completeError(error);
|
||||
},
|
||||
);
|
||||
stream.addListener(listener);
|
||||
|
||||
final image = await completer.future;
|
||||
stream.removeListener(listener);
|
||||
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
if (byteData == null) return _IconShape.clipOnly;
|
||||
|
||||
final totalPixels = image.width * image.height;
|
||||
var transparentPixels = 0;
|
||||
for (var i = 0; i < totalPixels; i++) {
|
||||
final alpha = byteData.getUint8(i * 4 + 3);
|
||||
if (alpha < 128) transparentPixels++;
|
||||
}
|
||||
|
||||
final ratio = transparentPixels / totalPixels;
|
||||
|
||||
if (ratio > 0.15 && ratio < 0.30) {
|
||||
return _IconShape.alreadyCircular;
|
||||
}
|
||||
|
||||
if (ratio >= 0.30) {
|
||||
return _IconShape.hollowNeedsBackdrop;
|
||||
}
|
||||
|
||||
return _IconShape.clipOnly;
|
||||
} catch (e) {
|
||||
printV('TokenImageWidget: failed to analyze $url: $e');
|
||||
return _IconShape.clipOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final image = SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: HashImageWidget(
|
||||
imageUrl: widget.imageUrl,
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.high,
|
||||
errorWidget: widget.errorWidget,
|
||||
semanticsLabel: widget.semanticsLabel,
|
||||
),
|
||||
);
|
||||
|
||||
switch (_shape) {
|
||||
case _IconShape.alreadyCircular:
|
||||
return image;
|
||||
case _IconShape.clipOnly:
|
||||
return ClipOval(child: image);
|
||||
case _IconShape.hollowNeedsBackdrop:
|
||||
return Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
),
|
||||
child: image,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -9,28 +10,36 @@ class ChainIcon extends StatelessWidget {
|
||||
{super.key,
|
||||
required this.iconPath,
|
||||
required this.dashboardViewModel,
|
||||
required this.isSyncHeavy});
|
||||
required this.isSyncHeavy,
|
||||
required this.showSyncedMessage});
|
||||
|
||||
final String iconPath;
|
||||
final bool isSyncHeavy;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final bool showSyncedMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
final progress = dashboardViewModel.status.progress();
|
||||
final done = !isSyncHeavy || progress >= 1;
|
||||
final done = !showSyncedMessage && (!isSyncHeavy || progress >= 1);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 100),
|
||||
opacity: done ? 0 : 1,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
color: Color(0xFFFFB84E),
|
||||
strokeWidth: 2,
|
||||
// Faded out means "nothing to report", so it must leave the tree too.
|
||||
child: ExcludeSemantics(
|
||||
excluding: done || showSyncedMessage,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
color: showSyncedMessage ? Color(0xFFFF12A439) : Color(0xFFFFB84E),
|
||||
strokeWidth: 2,
|
||||
semanticsLabel: S.of(context).synchronizing,
|
||||
semanticsValue: "${(progress * 100).round()}%",
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedScale(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user