Merge branch 'cake-tech:dev' into dev

This commit is contained in:
BSN ∞/21M
2026-07-28 07:12:20 +02:00
committed by GitHub
14 changed files with 1335 additions and 31 deletions
+54 -22
View File
@@ -1,13 +1,12 @@
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/format_fixed.dart';
import 'package:cw_core/parse_fixed.dart';
import "package:cw_core/crypto_amount_format.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/format_fixed.dart";
import "package:cw_core/parse_fixed.dart";
export "money_local.dart";
class Money implements Comparable<Money> {
final BigInt amount;
final Currency currency;
const Money(this.amount, this.currency);
factory Money.zero(Currency currency) => Money(BigInt.zero, currency);
@@ -38,6 +37,9 @@ class Money implements Comparable<Money> {
return amount != null ? Money(amount, currency) : null;
}
final BigInt amount;
final Currency currency;
/// Returns the sign of this [BigInt] amount.
/// Returns 0 for zero, -1 for values less than zero and +1 for values
/// greater than zero.
@@ -139,7 +141,9 @@ class Money implements Comparable<Money> {
///
/// The result is again [Money].
Money operator /(BigInt other) {
if (other == BigInt.zero) throw Exception('Division by zero.');
if (other == BigInt.zero) {
throw Exception("Division by zero.");
}
final neg = (amount.isNegative) ^ (other.isNegative);
final A = amount.abs();
@@ -163,7 +167,9 @@ class Money implements Comparable<Money> {
Money copyWith({BigInt? amount, Currency? currency}) {
if (currency != null && amount == null && currency.decimals != this.currency.decimals) {
return Money(
_transformAmount(this.amount, this.currency.decimals, currency.decimals), currency);
_transformAmount(this.amount, this.currency.decimals, currency.decimals),
currency,
);
}
return Money(amount ?? this.amount, currency ?? this.currency);
@@ -173,16 +179,21 @@ class Money implements Comparable<Money> {
Money _withAmount(BigInt amount) => Money(amount, currency);
void _assertSameCurrency(Money other, [String? message]) {
if (currency != other.currency)
if (currency != other.currency) {
throw ArgumentError(message ?? "Cannot operate with money values in different currencies.");
}
}
BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) {
if (sourceDecimals == targetDecimals) return source;
if (sourceDecimals == targetDecimals) {
return source;
}
if (sourceDecimals > targetDecimals) {
return parseFixed(
formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals), targetDecimals);
formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals),
targetDecimals,
);
} else {
return parseFixed(formatFixed(source, sourceDecimals), targetDecimals);
}
@@ -198,18 +209,39 @@ class Money implements Comparable<Money> {
@override
String toString() => formatFixed(amount, currency.decimals);
String toStringWithSymbol(
{int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) =>
"${toStringWithPrecision(fractionalDigits: fractionalDigits, trimZeros: trimZeros, useBaseUnit: useBaseUnit)} ${_getSymbol(useBaseUnit)}";
String toStringWithSymbol({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
bool withSymbolPrefix = false,
}) {
final amount = toStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit,
);
final symbol = getSymbol(useBaseUnit: useBaseUnit);
String toStringWithPrecision(
{int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) =>
formatFixed(amount, useBaseUnit ? 0 : currency.decimals,
fractionalDigits: fractionalDigits, trimZeros: trimZeros);
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
}
String toStringWithPrecision({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
}) =>
formatFixed(
amount,
useBaseUnit ? 0 : currency.decimals,
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
);
// To Override the symbol with the ticker of the base unit
String _getSymbol(bool useBaseUnit) {
if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return "sats";
String getSymbol({required bool useBaseUnit}) {
if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
return "sats";
}
return currency.symbol;
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
import 'dart:math';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/currency.dart';
import "dart:math";
import "package:cw_core/amount/money.dart";
import "package:cw_core/currency.dart";
extension ToMoney on double {
/// Turn a double representation of a currency amount to a proper Money representation
+47
View File
@@ -0,0 +1,47 @@
import "package:cw_core/amount/money.dart";
import "package:intl/intl.dart";
extension WithLocalSeparator on Money {
String toLocalStringWithSymbol({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
bool withSymbolPrefix = false,
String? locale,
}) {
final amount = toLocalStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit,
locale: locale,
);
final symbol = getSymbol(useBaseUnit: useBaseUnit);
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
}
String toLocalStringWithPrecision({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
String? locale,
}) =>
_withLocalSeparator(
toStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit,
),
locale: locale,
);
String _withLocalSeparator(String amount, {String? locale}) {
final isNegative = amount.startsWith("-");
final formater = NumberFormat("#,###", locale);
final parts = (isNegative ? amount.substring(1) : amount).split(".");
final formatted = [formater.format(int.tryParse(parts.first) ?? 0), ...parts.sublist(1)]
.join(formater.symbols.DECIMAL_SEP);
return isNegative ? "-$formatted" : formatted;
}
}
+128
View File
@@ -0,0 +1,128 @@
import "package:cw_core/amount/money.dart";
import "package:cw_core/amount/money_local.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter_test/flutter_test.dart";
import "package:intl/intl.dart";
final _btc012 = Money.fromInt(12345678, CryptoCurrency.btc); // 0.12345678 BTC
final _btcTrailing = Money.fromInt(12000000, CryptoCurrency.btc); // 0.12 BTC
final _btcGrouping = Money.fromInt(123450000000, CryptoCurrency.btc); // 1234.5 BTC
final _xmrHalf = Money.fromInt(500000000000, CryptoCurrency.xmr); // 0.5 XMR
void main() {
group("toLocalStringWithPrecision", () {
test("localizes grouping (en_US)", () {
expect(_btcGrouping.toLocalStringWithPrecision(locale: "en_US"), "1,234.5");
});
test("no grouping for sub-1 values", () {
expect(_btc012.toLocalStringWithPrecision(locale: "en_US"), "0.12345678");
});
test("swaps grouping/decimal separators (de_DE)", () {
expect(_btcGrouping.toLocalStringWithPrecision(locale: "de_DE"), "1.234,5");
});
test("base unit renders integer sats with grouping", () {
expect(
_btc012.toLocalStringWithPrecision(useBaseUnit: true, locale: "en_US"),
"12,345,678",
);
});
test("fractionalDigits truncates (does not round)", () {
// 0.12345678 -> 0.12 (NOT 0.13).
expect(_btc012.toLocalStringWithPrecision(fractionalDigits: 2, locale: "en_US"), "0.12");
});
test("trimZeros:false keeps trailing zeros", () {
expect(
_btcTrailing.toLocalStringWithPrecision(trimZeros: false, locale: "en_US"),
"0.12000000",
);
});
test("respects a higher-precision currency (XMR)", () {
expect(_xmrHalf.toLocalStringWithPrecision(locale: "en_US"), "0.5");
});
test("falls back to the ambient Intl locale when locale is null", () {
final previous = Intl.defaultLocale;
addTearDown(() => Intl.defaultLocale = previous);
Intl.defaultLocale = "de_DE";
// Proves the extension passes null through to NumberFormat rather than
// hardcoding a locale.
expect(_btcGrouping.toLocalStringWithPrecision(), "1.234,5");
});
});
group("toLocalStringWithSymbol", () {
test("suffixes the symbol by default", () {
expect(_btcGrouping.toLocalStringWithSymbol(locale: "en_US"), "1,234.5 BTC");
});
test("prefixes the symbol when withSymbolPrefix is true", () {
expect(
_btcGrouping.toLocalStringWithSymbol(withSymbolPrefix: true, locale: "en_US"),
"BTC 1,234.5",
);
});
test("suffixes the base-unit ticker", () {
expect(
_btc012.toLocalStringWithSymbol(useBaseUnit: true, locale: "en_US"),
"12,345,678 sats",
);
});
test("prefixes the base-unit ticker", () {
expect(
_btc012.toLocalStringWithSymbol(
useBaseUnit: true,
withSymbolPrefix: true,
locale: "en_US",
),
"sats 12,345,678",
);
});
test("prefix respects de_DE separators", () {
expect(
_btcGrouping.toLocalStringWithSymbol(withSymbolPrefix: true, locale: "de_DE"),
"BTC 1.234,5",
);
});
test("uses the currency symbol for non-BTC", () {
expect(_xmrHalf.toLocalStringWithSymbol(locale: "en_US"), "0.5 XMR");
});
test("forwards fractionalDigits to the precision string", () {
expect(_btc012.toLocalStringWithSymbol(fractionalDigits: 2, locale: "en_US"), "0.12 BTC");
});
test("forwards trimZeros to the precision string", () {
expect(
_btcTrailing.toLocalStringWithSymbol(trimZeros: false, locale: "en_US"),
"0.12000000 BTC",
);
});
});
group("negative amounts", () {
test("keeps the sign when the integer part is non-zero", () {
expect((-_btcGrouping).toLocalStringWithSymbol(locale: "en_US"), "-1,234.5 BTC");
});
// NOTE: currently fails. For |amount| < 1 the sign is dropped:
// formatFixed yields "-0.5", but _withLocalSeparator re-parses the integer
// part and int.tryParse("-0") == 0, so the "-" is lost -> "0.5 XMR".
// Fix in _withLocalSeparator by capturing the sign before parsing, e.g.
// final negative = amount.startsWith("-");
// ... then re-apply it to the formatted result.
test("keeps the sign for sub-1 amounts", () {
expect((-_xmrHalf).toLocalStringWithSymbol(locale: "en_US"), "-0.5 XMR");
});
});
}
+30 -4
View File
@@ -278,10 +278,14 @@ void main() {
});
test("handles exact division perfectly", () {
expect((Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount,
BigInt.from(5));
expect((Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount,
BigInt.from(-5));
expect(
(Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount,
BigInt.from(5),
);
expect(
(Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount,
BigInt.from(-5),
);
});
});
});
@@ -346,6 +350,28 @@ void main() {
expect(money.currency.decimals, equals(8));
expect(money.toStringWithSymbol(useBaseUnit: true), "100000000 sats");
});
group("withSymbolPrefix", () {
test("with fractionalDigits and padded with zeros", () {
final money = Money.parse("1", CryptoCurrency.btc);
expect(money.amount, equals(BigInt.parse("100000000")));
expect(money.currency.decimals, equals(8));
expect(
money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false, withSymbolPrefix: true),
"BTC 1.00000",
);
});
test("using base unit sats", () {
final money = Money.parse("1", CryptoCurrency.btc);
expect(money.amount, equals(BigInt.parse("100000000")));
expect(money.currency.decimals, equals(8));
expect(
money.toStringWithSymbol(useBaseUnit: true, withSymbolPrefix: true),
"sats 100000000",
);
});
});
});
});
}
+4 -2
View File
@@ -23,6 +23,7 @@ import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/trade_legacy.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/locales/locale.dart';
import "package:cake_wallet/new-ui/widgets/money/money_settings_provider.dart";
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/reactions/bootstrap.dart';
import 'package:cake_wallet/router.dart' as Router;
@@ -425,8 +426,9 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: child!),
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: MoneySettingsProvider(settingsStore: settingsStore, child: child!),
),
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
@@ -0,0 +1,165 @@
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/currency.dart";
import "package:flutter/widgets.dart";
import "package:flutter_bloc/flutter_bloc.dart";
/// The [CurrencySymbolText] widget displays [Currency.symbol] based on
/// usersettings as a formated string of text with single style.
class CurrencySymbolText extends StatelessWidget {
const CurrencySymbolText(
this.currency, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaler,
this.maxLines,
this.semanticsLabel,
this.semanticsIdentifier,
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
this.useBaseUnit,
});
/// The currency to display.
final Currency currency;
/// If non-null, the style to use for this text.
///
/// If the style's "inherit" property is true, the style will be merged with
/// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
/// replace the closest enclosing [DefaultTextStyle].
///
/// The user or platform may override this [style]'s [TextStyle.fontWeight],
/// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing]
/// via a [MediaQuery] ancestor's [MediaQueryData.boldText],
/// [MediaQueryData.lineHeightScaleFactorOverride],
/// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride]
/// regardless of its [TextStyle.inherit] value.
final TextStyle? style;
/// The user or platform may override this [strutStyle]'s [StrutStyle.height]
/// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride].
final StrutStyle? strutStyle;
/// How the text should be aligned horizontally.
final TextAlign? textAlign;
/// The directionality of the text.
///
/// This decides how [textAlign] values like [TextAlign.start] and
/// [TextAlign.end] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the [data] is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// Defaults to the ambient [Directionality], if any.
final TextDirection? textDirection;
/// Used to select a font when the same Unicode character can
/// be rendered differently, depending on the locale.
///
/// It's rarely necessary to set this property. By default its value
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
///
/// See [RenderParagraph.locale] for more information.
final Locale? locale;
/// Whether the text should break at soft line breaks.
///
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
final bool? softWrap;
/// How visual overflow should be handled.
///
/// If this is null [TextStyle.overflow] will be used, otherwise the value
/// from the nearest [DefaultTextStyle] ancestor will be used.
final TextOverflow? overflow;
final TextScaler? textScaler;
/// An optional maximum number of lines for the text to span, wrapping if necessary.
/// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow].
///
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
/// edge of the box.
///
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
/// an explicit number for its [DefaultTextStyle.maxLines], then the
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
/// widget directly to entirely override the [DefaultTextStyle].
final int? maxLines;
/// An alternative semantics label for this text.
///
/// If present, the semantics of this widget will contain this value instead
/// of the actual text. This will overwrite any of the semantics labels applied
/// directly to the [TextSpan]s.
///
/// This is useful for replacing abbreviations or shorthands with the full
/// text value:
///
/// ```dart
/// const Text(r'$$', semanticsLabel: 'Double dollars')
/// ```
final String? semanticsLabel;
/// A unique identifier for the semantics node for this widget.
///
/// This is useful for cases where the text widget needs to have a uniquely
/// identifiable ID that is recognized through the automation tools without
/// having a dependency on the actual content of the text that can possibly be
/// dynamic in nature.
final String? semanticsIdentifier;
final TextWidthBasis? textWidthBasis;
final TextHeightBehavior? textHeightBehavior;
/// The color to use when painting the selection.
///
/// This is ignored if [SelectionContainer.maybeOf] returns null
/// in the [BuildContext] of the [Text] widget.
///
/// If null, the ambient [DefaultSelectionStyle] is used (if any); failing
/// that, the selection color defaults to [DefaultSelectionStyle.defaultColor]
/// (semi-transparent grey).
final Color? selectionColor;
/// Show the amount in the base unit format of [Money.currency]
///
/// If null, the displayAmountsInSatoshi setting is used.
final bool? useBaseUnit;
@override
Widget build(BuildContext context) => BlocBuilder<MoneySettingsCubit, MoneySettingsState>(
builder: (context, state) => Text(
state.getSymbol(currency, overrideSettings: useBaseUnit),
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
locale: locale,
softWrap: softWrap,
overflow: overflow,
textScaler: textScaler,
maxLines: maxLines,
semanticsLabel: semanticsLabel,
semanticsIdentifier: semanticsIdentifier,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
selectionColor: selectionColor,
),
);
}
@@ -0,0 +1,78 @@
import "package:bloc/bloc.dart";
import "package:cake_wallet/entities/balance_display_mode.dart";
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
import "package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart";
import "package:cake_wallet/store/settings_store.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:mobx/mobx.dart";
class MoneySettingsCubit extends Cubit<MoneySettingsState> {
MoneySettingsCubit(SettingsStore _settingsStore)
: super(
MoneySettingsState(
bitcoinAmountDisplayMode: _settingsStore.displayAmountsInSatoshi,
displayMode: _settingsStore.balanceDisplayMode,
),
) {
_bitcoinAmountDisplayModeDisposer = reaction(
(_) => _settingsStore.displayAmountsInSatoshi,
(displayMode) => emit(state.copyWith(bitcoinAmountDisplayMode: displayMode)),
);
_displayModeDisposer = reaction(
(_) => _settingsStore.balanceDisplayMode,
(displayMode) => emit(state.copyWith(displayMode: displayMode)),
);
}
late final ReactionDisposer _bitcoinAmountDisplayModeDisposer;
late final ReactionDisposer _displayModeDisposer;
@override
Future<void> close() {
if (!_bitcoinAmountDisplayModeDisposer.reaction.isDisposed) {
_bitcoinAmountDisplayModeDisposer.reaction.dispose();
}
if (!_displayModeDisposer.reaction.isDisposed) {
_displayModeDisposer.reaction.dispose();
}
return super.close();
}
}
class MoneySettingsState {
const MoneySettingsState({required this.bitcoinAmountDisplayMode, required this.displayMode});
final BitcoinAmountDisplayMode bitcoinAmountDisplayMode;
final BalanceDisplayMode displayMode;
bool useBaseUnit(Currency currency) =>
([CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency) &&
bitcoinAmountDisplayMode == BitcoinAmountDisplayMode.satoshi) ||
(CryptoCurrency.btcln == currency &&
bitcoinAmountDisplayMode == BitcoinAmountDisplayMode.satoshiForLightning);
bool get isHidden => displayMode == BalanceDisplayMode.hiddenBalance;
String getSymbol(Currency currency, {bool? overrideSettings}) {
if (overrideSettings == null) {
return useBaseUnit(currency) ? "sats" : currency.symbol.safeSubString(0, 8);
}
if (overrideSettings == true && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
return "sats";
}
return currency.symbol.safeSubString(0, 8);
}
MoneySettingsState copyWith({
BitcoinAmountDisplayMode? bitcoinAmountDisplayMode,
BalanceDisplayMode? displayMode,
}) =>
MoneySettingsState(
bitcoinAmountDisplayMode: bitcoinAmountDisplayMode ?? this.bitcoinAmountDisplayMode,
displayMode: displayMode ?? this.displayMode,
);
}
@@ -0,0 +1,15 @@
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cake_wallet/store/settings_store.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
class MoneySettingsProvider extends StatelessWidget {
const MoneySettingsProvider({required this.settingsStore, this.child, super.key});
final Widget? child;
final SettingsStore settingsStore;
@override
Widget build(BuildContext context) =>
BlocProvider(create: (_) => MoneySettingsCubit(settingsStore), child: child);
}
+256
View File
@@ -0,0 +1,256 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cw_core/amount/money.dart";
import "package:flutter/widgets.dart";
import "package:flutter_bloc/flutter_bloc.dart";
/// The [MoneyText] widget displays [Money] as a formated string of text with single style.
class MoneyText extends StatelessWidget {
const MoneyText(
this.amount, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaler,
this.maxLines,
this.semanticsLabel,
this.semanticsIdentifier,
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
this.isHiddenAmount,
this.useBaseUnit,
this.fractionalDigits = 8,
this.showSymbol = true,
this.withSymbolPrefix = false,
this.trimZeros = true,
});
/// The [MoneyText.optional] returns a widget displaying [Money] as a
/// formated string or an [SizedBox.shrink].
static Widget optional(
Money? amount, {
Key? key,
TextStyle? style,
StrutStyle? strutStyle,
TextAlign? textAlign,
TextDirection? textDirection,
Locale? locale,
bool? softWrap,
TextOverflow? overflow,
TextScaler? textScaler,
int? maxLines,
String? semanticsLabel,
String? semanticsIdentifier,
TextWidthBasis? textWidthBasis,
TextHeightBehavior? textHeightBehavior,
Color? selectionColor,
bool? isHiddenAmount,
bool? useBaseUnit,
int fractionalDigits = 8,
bool showSymbol = true,
bool withSymbolPrefix = false,
bool trimZeros = true,
}) =>
amount != null
? MoneyText(
amount,
key: key,
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
locale: locale,
softWrap: softWrap,
overflow: overflow,
textScaler: textScaler,
maxLines: maxLines,
semanticsLabel: semanticsLabel,
semanticsIdentifier: semanticsIdentifier,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
selectionColor: selectionColor,
isHiddenAmount: isHiddenAmount,
useBaseUnit: useBaseUnit,
fractionalDigits: fractionalDigits,
showSymbol: showSymbol,
withSymbolPrefix: withSymbolPrefix,
trimZeros: trimZeros,
)
: const SizedBox.shrink();
/// The amount to display.
final Money amount;
/// If non-null, the style to use for this text.
///
/// If the style's "inherit" property is true, the style will be merged with
/// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
/// replace the closest enclosing [DefaultTextStyle].
///
/// The user or platform may override this [style]'s [TextStyle.fontWeight],
/// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing]
/// via a [MediaQuery] ancestor's [MediaQueryData.boldText],
/// [MediaQueryData.lineHeightScaleFactorOverride],
/// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride]
/// regardless of its [TextStyle.inherit] value.
final TextStyle? style;
/// The user or platform may override this [strutStyle]'s [StrutStyle.height]
/// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride].
final StrutStyle? strutStyle;
/// How the text should be aligned horizontally.
final TextAlign? textAlign;
/// The directionality of the text.
///
/// This decides how [textAlign] values like [TextAlign.start] and
/// [TextAlign.end] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the [data] is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// Defaults to the ambient [Directionality], if any.
final TextDirection? textDirection;
/// Used to select a font when the same Unicode character can
/// be rendered differently, depending on the locale.
///
/// It's rarely necessary to set this property. By default its value
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
///
/// See [RenderParagraph.locale] for more information.
final Locale? locale;
/// Whether the text should break at soft line breaks.
///
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
final bool? softWrap;
/// How visual overflow should be handled.
///
/// If this is null [TextStyle.overflow] will be used, otherwise the value
/// from the nearest [DefaultTextStyle] ancestor will be used.
final TextOverflow? overflow;
final TextScaler? textScaler;
/// An optional maximum number of lines for the text to span, wrapping if necessary.
/// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow].
///
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
/// edge of the box.
///
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
/// an explicit number for its [DefaultTextStyle.maxLines], then the
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
/// widget directly to entirely override the [DefaultTextStyle].
final int? maxLines;
/// An alternative semantics label for this text.
///
/// If present, the semantics of this widget will contain this value instead
/// of the actual text. This will overwrite any of the semantics labels applied
/// directly to the [TextSpan]s.
///
/// This is useful for replacing abbreviations or shorthands with the full
/// text value:
///
/// ```dart
/// const Text(r'$$', semanticsLabel: 'Double dollars')
/// ```
final String? semanticsLabel;
/// A unique identifier for the semantics node for this widget.
///
/// This is useful for cases where the text widget needs to have a uniquely
/// identifiable ID that is recognized through the automation tools without
/// having a dependency on the actual content of the text that can possibly be
/// dynamic in nature.
final String? semanticsIdentifier;
final TextWidthBasis? textWidthBasis;
final TextHeightBehavior? textHeightBehavior;
/// The color to use when painting the selection.
///
/// This is ignored if [SelectionContainer.maybeOf] returns null
/// in the [BuildContext] of the [Text] widget.
///
/// If null, the ambient [DefaultSelectionStyle] is used (if any); failing
/// that, the selection color defaults to [DefaultSelectionStyle.defaultColor]
/// (semi-transparent grey).
final Color? selectionColor;
/// Show the amount in the base unit format of [Money.currency]
///
/// If null, the displayAmountsInSatoshi setting is used.
final bool? useBaseUnit;
/// A limit for the display of the fractional digits of the amount
final int fractionalDigits;
/// Hide the amount
///
/// If null, the value from the balanceDisplayMode setting is used.
final bool? isHiddenAmount;
/// Show the currency symbol
final bool showSymbol;
/// Prefix the amount with the currency symbol if [showSymbol] is true
final bool withSymbolPrefix;
/// Trim the zeros at the end of an amount
final bool trimZeros;
@override
Widget build(BuildContext context) => BlocBuilder<MoneySettingsCubit, MoneySettingsState>(
builder: (context, state) => Text(
isHiddenAmount ?? state.isHidden
? "●●●●●●"
: showSymbol
? amount.toLocalStringWithSymbol(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit ?? state.useBaseUnit(amount.currency),
withSymbolPrefix: withSymbolPrefix,
locale: (locale ?? Localizations.localeOf(context)).toString(),
)
: amount.toLocalStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit ?? state.useBaseUnit(amount.currency),
locale: (locale ?? Localizations.localeOf(context)).toString(),
),
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
locale: locale,
softWrap: softWrap,
overflow: overflow,
textScaler: textScaler,
maxLines: maxLines,
semanticsLabel: isHiddenAmount ?? state.isHidden
? (semanticsLabel ?? S.of(context).amount_hidden)
: semanticsLabel,
semanticsIdentifier: semanticsIdentifier,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
selectionColor: selectionColor,
),
);
}
+1
View File
@@ -64,6 +64,7 @@
"already_your_username": "Das ist bereits Ihr Benutzername!",
"always": "Immer",
"amount": "Betrag: ",
"amount_hidden": "Versteckter Betrag",
"amount_is_below_minimum_limit": "Ihr Guthaben nach Gebühren wäre geringer als der für den Tausch erforderliche Mindestbetrag (${min})",
"amount_is_estimate": "Der Empfangsbetrag ist eine Schätzung",
"amount_is_guaranteed": "Der Empfangsbetrag ist garantiert",
+1
View File
@@ -64,6 +64,7 @@
"already_your_username": "This is already your username!",
"always": "Always",
"amount": "Amount: ",
"amount_hidden": "Amount hidden",
"amount_is_below_minimum_limit": "Your balance after fees would be less than the minimum amount needed for the exchange (${min})",
"amount_is_estimate": "The receive amount is an estimate",
"amount_is_guaranteed": "The receive amount is guaranteed",
@@ -0,0 +1,213 @@
import "package:cake_wallet/entities/balance_display_mode.dart";
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
import "package:cake_wallet/new-ui/widgets/money/currency_symbol_text.dart";
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
import "package:flutter_test/flutter_test.dart";
class FakeMoneySettingsCubit extends Cubit<MoneySettingsState> implements MoneySettingsCubit {
FakeMoneySettingsCubit(super.initialState);
void setState(MoneySettingsState state) => emit(state);
}
const _bitcoinMode = MoneySettingsState(
bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.bitcoin,
displayMode: BalanceDisplayMode.fullBalance,
);
const _satoshiMode = MoneySettingsState(
bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.satoshi,
displayMode: BalanceDisplayMode.fullBalance,
);
const _lnSatoshiMode = MoneySettingsState(
bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.satoshiForLightning,
displayMode: BalanceDisplayMode.fullBalance,
);
/// A non-BTC currency whose symbol exceeds the 8-char cap. `raw: -1` keeps it
/// distinct from every real currency, so it never counts as BTC/BTCLN.
const _longSymbol = CryptoCurrency(title: "SUPERLONGTOKEN", name: "superlongtoken", decimals: 8);
Future<FakeMoneySettingsCubit> _pump(
WidgetTester tester,
Widget child,
MoneySettingsState state,
) async {
final cubit = FakeMoneySettingsCubit(state);
addTearDown(cubit.close);
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<MoneySettingsCubit>.value(
value: cubit,
child: Scaffold(body: Center(child: child)),
),
),
);
return cubit;
}
void main() {
group("useBaseUnit == null (follows settings)", () {
testWidgets("BTC in satoshi mode -> sats", (tester) async {
await _pump(tester, const CurrencySymbolText(CryptoCurrency.btc), _satoshiMode);
expect(find.text("sats"), findsOneWidget);
});
testWidgets("BTC in bitcoin mode -> BTC", (tester) async {
await _pump(tester, const CurrencySymbolText(CryptoCurrency.btc), _bitcoinMode);
expect(find.text("BTC"), findsOneWidget);
});
testWidgets("non-BTC currency ignores satoshi mode", (tester) async {
await _pump(tester, const CurrencySymbolText(CryptoCurrency.xmr), _satoshiMode);
expect(find.text("XMR"), findsOneWidget);
});
testWidgets("BTCLN in LN-satoshi mode -> sats", (tester) async {
await _pump(
tester,
const CurrencySymbolText(CryptoCurrency.btcln),
_lnSatoshiMode,
);
expect(find.text("sats"), findsOneWidget);
});
testWidgets("BTCLN in bitcoin mode -> BTC", (tester) async {
await _pump(tester, const CurrencySymbolText(CryptoCurrency.btcln), _bitcoinMode);
// BTCLN's title/symbol is "BTC".
expect(find.text("BTC"), findsOneWidget);
});
});
group("useBaseUnit == true (force base unit where supported)", () {
testWidgets("BTC -> sats regardless of settings", (tester) async {
await _pump(
tester,
const CurrencySymbolText(CryptoCurrency.btc, useBaseUnit: true),
_bitcoinMode, // settings say bitcoin, but override wins
);
expect(find.text("sats"), findsOneWidget);
});
testWidgets("BTCLN -> sats", (tester) async {
await _pump(
tester,
const CurrencySymbolText(CryptoCurrency.btcln, useBaseUnit: true),
_bitcoinMode,
);
expect(find.text("sats"), findsOneWidget);
});
testWidgets("non-BTC currency keeps its symbol even when forced", (tester) async {
await _pump(
tester,
const CurrencySymbolText(CryptoCurrency.xmr, useBaseUnit: true),
_satoshiMode,
);
expect(find.text("XMR"), findsOneWidget);
});
});
group("useBaseUnit == false (always the symbol)", () {
testWidgets("BTC -> BTC even in satoshi mode", (tester) async {
await _pump(
tester,
const CurrencySymbolText(CryptoCurrency.btc, useBaseUnit: false),
_satoshiMode,
);
expect(find.text("BTC"), findsOneWidget);
expect(find.text("sats"), findsNothing);
});
testWidgets("BTCLN -> BTC even in LN-satoshi mode", (tester) async {
await _pump(
tester,
const CurrencySymbolText(CryptoCurrency.btcln, useBaseUnit: false),
_lnSatoshiMode,
);
expect(find.text("BTC"), findsOneWidget);
expect(find.text("sats"), findsNothing);
});
testWidgets("non-BTC currency -> symbol", (tester) async {
await _pump(
tester,
const CurrencySymbolText(CryptoCurrency.xmr, useBaseUnit: false),
_bitcoinMode,
);
expect(find.text("XMR"), findsOneWidget);
});
});
group("symbol formatting", () {
testWidgets("caps symbols longer than 8 characters", (tester) async {
await _pump(
tester,
const CurrencySymbolText(_longSymbol, useBaseUnit: false),
_bitcoinMode,
);
// "SUPERLONGTOKEN" -> first 8 chars.
expect(find.text("SUPERLON"), findsOneWidget);
});
});
group("rebuild on cubit state change", () {
testWidgets("re-renders the symbol when settings change", (tester) async {
final cubit = await _pump(
tester,
const CurrencySymbolText(CryptoCurrency.btc),
_bitcoinMode,
);
expect(find.text("BTC"), findsOneWidget);
cubit.setState(_satoshiMode);
await tester.pump();
expect(find.text("BTC"), findsNothing);
expect(find.text("sats"), findsOneWidget);
});
});
group("forwarding to the inner Text", () {
testWidgets("passes through Text properties incl. semanticsIdentifier", (tester) async {
await _pump(
tester,
const CurrencySymbolText(
CryptoCurrency.btc,
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
style: TextStyle(fontSize: 18),
semanticsIdentifier: "currency-symbol",
),
_bitcoinMode,
);
final text = tester.widget<Text>(
find.descendant(of: find.byType(CurrencySymbolText), matching: find.byType(Text)),
);
expect(text.maxLines, 2);
expect(text.textAlign, TextAlign.center);
expect(text.overflow, TextOverflow.fade);
expect(text.style?.fontSize, 18);
expect(text.semanticsIdentifier, "currency-symbol");
});
});
}
@@ -0,0 +1,340 @@
import "package:cake_wallet/entities/balance_display_mode.dart";
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cake_wallet/new-ui/widgets/money/money_text.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
import "package:flutter_test/flutter_test.dart";
class FakeMoneySettingsCubit extends Cubit<MoneySettingsState> implements MoneySettingsCubit {
FakeMoneySettingsCubit(super.initialState);
void setState(MoneySettingsState state) => emit(state);
}
/// Visible, BTC shown as bitcoin (not base unit).
const _visibleBtc = MoneySettingsState(
bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.bitcoin,
displayMode: BalanceDisplayMode.fullBalance,
);
/// Hidden balance.
const _hiddenBtc = MoneySettingsState(
bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.bitcoin,
displayMode: BalanceDisplayMode.hiddenBalance,
);
/// Visible, satoshi mode -> BTC/BTCLN use base unit.
const _sats = MoneySettingsState(
bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.satoshi,
displayMode: BalanceDisplayMode.fullBalance,
);
/// Visible, LN-only satoshi mode -> only BTCLN uses base unit.
const _lnSats = MoneySettingsState(
bitcoinAmountDisplayMode: BitcoinAmountDisplayMode.satoshiForLightning,
displayMode: BalanceDisplayMode.fullBalance,
);
// --- Money fixtures --------------------------------------------------------
final _btc012 = Money.fromInt(12345678, CryptoCurrency.btc); // 0.12345678 BTC
final _btcTrailing = Money.fromInt(12000000, CryptoCurrency.btc); // 0.12 BTC
final _btcGrouping = Money.fromInt(123450000000, CryptoCurrency.btc); // 1234.5 BTC
final _xmrHalf = Money.fromInt(500000000000, CryptoCurrency.xmr); // 0.5 XMR
final _btclnAmount = Money.fromInt(12345678, CryptoCurrency.btcln); // 0.12345678 BTC (LN)
const _hidden = "●●●●●●";
Future<FakeMoneySettingsCubit> _pump(
WidgetTester tester,
Widget child,
MoneySettingsState state,
) async {
final cubit = FakeMoneySettingsCubit(state);
addTearDown(cubit.close);
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<MoneySettingsCubit>.value(
value: cubit,
child: Scaffold(body: Center(child: child)),
),
),
);
return cubit;
}
void main() {
group("hidden amount", () {
testWidgets("isHiddenAmount:true hides even when state is visible", (tester) async {
await _pump(tester, MoneyText(_btc012, isHiddenAmount: true), _visibleBtc);
expect(find.text(_hidden), findsOneWidget);
expect(find.text("0.12345678 BTC"), findsNothing);
});
testWidgets("isHiddenAmount:false overrides a hidden state", (tester) async {
await _pump(tester, MoneyText(_btc012, isHiddenAmount: false), _hiddenBtc);
expect(find.text("0.12345678 BTC"), findsOneWidget);
expect(find.text(_hidden), findsNothing);
});
testWidgets("isHiddenAmount:null falls back to state.isHidden (hidden)", (tester) async {
await _pump(tester, MoneyText(_btc012), _hiddenBtc);
expect(find.text(_hidden), findsOneWidget);
});
testWidgets("isHiddenAmount:null falls back to state.isHidden (visible)", (tester) async {
await _pump(tester, MoneyText(_btc012), _visibleBtc);
expect(find.text("0.12345678 BTC"), findsOneWidget);
expect(find.text(_hidden), findsNothing);
});
});
group("symbol vs precision", () {
testWidgets("showSymbol:true appends the currency symbol", (tester) async {
await _pump(tester, MoneyText(_btc012), _visibleBtc);
expect(find.text("0.12345678 BTC"), findsOneWidget);
});
testWidgets("showSymbol:false omits the currency symbol", (tester) async {
await _pump(tester, MoneyText(_btc012, showSymbol: false), _visibleBtc);
expect(find.text("0.12345678"), findsOneWidget);
expect(find.text("0.12345678 BTC"), findsNothing);
});
});
group("withSymbolPrefix", () {
test("defaults to false on the constructor", () {
expect(MoneyText(_btc012).withSymbolPrefix, false);
});
test("defaults to false via optional", () {
expect((MoneyText.optional(_btc012) as MoneyText).withSymbolPrefix, false);
});
testWidgets("ignored when showSymbol is false", (tester) async {
await _pump(
tester,
MoneyText(_btc012, showSymbol: false, withSymbolPrefix: true),
_visibleBtc,
);
expect(find.text("0.12345678"), findsOneWidget);
expect(find.text("BTC 0.12345678"), findsNothing);
expect(find.text("0.12345678 BTC"), findsNothing);
});
testWidgets("prefixes the symbol before the amount", (tester) async {
await _pump(tester, MoneyText(_btc012, withSymbolPrefix: true), _visibleBtc);
expect(find.text("BTC 0.12345678"), findsOneWidget);
});
testWidgets("prefixed amount is still localized (en_US grouping)", (tester) async {
await _pump(tester, MoneyText(_btcGrouping, withSymbolPrefix: true), _visibleBtc);
expect(find.text("BTC 1,234.5"), findsOneWidget);
});
testWidgets("prefixed amount respects an explicit de_DE locale", (tester) async {
await _pump(
tester,
MoneyText(_btcGrouping, withSymbolPrefix: true, locale: const Locale("de", "DE")),
_visibleBtc,
);
expect(find.text("BTC 1.234,5"), findsOneWidget);
});
testWidgets("prefixes the base-unit ticker (sats)", (tester) async {
await _pump(
tester,
MoneyText(_btc012, withSymbolPrefix: true, useBaseUnit: true),
_visibleBtc,
);
expect(find.text("sats 12,345,678"), findsOneWidget);
});
});
group("base unit (useBaseUnit)", () {
testWidgets("explicit useBaseUnit:true renders sats", (tester) async {
await _pump(tester, MoneyText(_btc012, useBaseUnit: true), _visibleBtc);
// 12345678 sats, localized grouping applied by withLocalSeperator.
expect(find.text("12,345,678 sats"), findsOneWidget);
});
testWidgets("explicit useBaseUnit:false overrides a satoshi state", (tester) async {
await _pump(tester, MoneyText(_btc012, useBaseUnit: false), _sats);
expect(find.text("0.12345678 BTC"), findsOneWidget);
});
testWidgets("useBaseUnit:null -> state decides (BTC in satoshi mode -> sats)", (tester) async {
await _pump(tester, MoneyText(_btc012), _sats);
expect(find.text("12,345,678 sats"), findsOneWidget);
});
testWidgets("useBaseUnit:null -> state decides (BTC in bitcoin mode -> BTC)", (tester) async {
await _pump(tester, MoneyText(_btc012), _visibleBtc);
expect(find.text("0.12345678 BTC"), findsOneWidget);
});
testWidgets("non-BTC currency ignores satoshi mode", (tester) async {
await _pump(tester, MoneyText(_xmrHalf), _sats);
expect(find.text("0.5 XMR"), findsOneWidget);
});
testWidgets("LN-satoshi mode makes BTCLN use base unit", (tester) async {
await _pump(tester, MoneyText(_btclnAmount), _lnSats);
expect(find.text("12,345,678 sats"), findsOneWidget);
});
testWidgets("LN-satoshi mode leaves plain BTC untouched", (tester) async {
await _pump(tester, MoneyText(_btc012), _lnSats);
expect(find.text("0.12345678 BTC"), findsOneWidget);
});
});
group("fractionalDigits & trimZeros", () {
testWidgets("fractionalDigits truncates (does not round)", (tester) async {
await _pump(tester, MoneyText(_btc012, fractionalDigits: 2), _visibleBtc);
// 0.12345678 truncated to 2 digits -> 0.12 (NOT 0.13).
expect(find.text("0.12 BTC"), findsOneWidget);
});
testWidgets("trimZeros:true (default) removes trailing zeros", (tester) async {
await _pump(tester, MoneyText(_btcTrailing), _visibleBtc);
expect(find.text("0.12 BTC"), findsOneWidget);
});
testWidgets("trimZeros:false keeps trailing zeros", (tester) async {
await _pump(tester, MoneyText(_btcTrailing, trimZeros: false), _visibleBtc);
expect(find.text("0.12000000 BTC"), findsOneWidget);
});
});
group("locale & separators", () {
testWidgets('default locale (en_US) uses "," grouping and "." decimal', (tester) async {
await _pump(tester, MoneyText(_btcGrouping), _visibleBtc);
expect(find.text("1,234.5 BTC"), findsOneWidget);
});
testWidgets("explicit de_DE locale swaps grouping/decimal separators", (tester) async {
await _pump(
tester,
MoneyText(_btcGrouping, locale: const Locale("de", "DE")),
_visibleBtc,
);
expect(find.text("1.234,5 BTC"), findsOneWidget);
});
});
group("rebuild on cubit state change", () {
testWidgets("re-renders when the settings state changes", (tester) async {
final cubit = await _pump(tester, MoneyText(_btc012), _visibleBtc);
expect(find.text("0.12345678 BTC"), findsOneWidget);
cubit.setState(_hiddenBtc);
await tester.pump();
expect(find.text("0.12345678 BTC"), findsNothing);
expect(find.text(_hidden), findsOneWidget);
});
});
group("forwarding to the inner Text", () {
testWidgets("passes through Text properties incl. semanticsIdentifier", (tester) async {
await _pump(
tester,
MoneyText(
_btc012,
maxLines: 3,
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 42),
semanticsIdentifier: "via-ctor",
),
_visibleBtc,
);
final text = tester.widget<Text>(
find.descendant(of: find.byType(MoneyText), matching: find.byType(Text)),
);
expect(text.maxLines, 3);
expect(text.textAlign, TextAlign.right);
expect(text.overflow, TextOverflow.ellipsis);
expect(text.style?.fontSize, 42);
expect(text.semanticsIdentifier, "via-ctor");
});
});
group("MoneyText.optional", () {
test("returns SizedBox.shrink for a null amount", () {
final widget = MoneyText.optional(null);
expect(widget, isA<SizedBox>());
final box = widget as SizedBox;
expect(box.width, 0.0);
expect(box.height, 0.0);
});
test("returns a MoneyText for a non-null amount", () {
expect(MoneyText.optional(_btc012), isA<MoneyText>());
});
test("forwards money-specific parameters to MoneyText", () {
final widget = MoneyText.optional(
_btc012,
showSymbol: false,
fractionalDigits: 3,
trimZeros: false,
isHiddenAmount: true,
useBaseUnit: true,
withSymbolPrefix: true,
) as MoneyText;
expect(widget.showSymbol, false);
expect(widget.fractionalDigits, 3);
expect(widget.trimZeros, false);
expect(widget.isHiddenAmount, true);
expect(widget.useBaseUnit, true);
expect(widget.withSymbolPrefix, true);
});
testWidgets("renders identically to a direct MoneyText", (tester) async {
await _pump(tester, MoneyText.optional(_btc012), _visibleBtc);
expect(find.byType(MoneyText), findsOneWidget);
expect(find.text("0.12345678 BTC"), findsOneWidget);
});
test(
"forwards semanticsIdentifier to MoneyText",
() {
final widget = MoneyText.optional(_btc012, semanticsIdentifier: "sid") as MoneyText;
expect(widget.semanticsIdentifier, "sid");
},
);
});
}