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,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';
|
||||
|
||||
Reference in New Issue
Block a user