diff --git a/packages/espressocash_app/assets/icons/money.svg b/packages/espressocash_app/assets/icons/money.svg new file mode 100644 index 0000000000..9a45dd6558 --- /dev/null +++ b/packages/espressocash_app/assets/icons/money.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/espressocash_app/lib/features/accounts/models/account.dart b/packages/espressocash_app/lib/features/accounts/models/account.dart index a7603b4698..dc17b9e94e 100644 --- a/packages/espressocash_app/lib/features/accounts/models/account.dart +++ b/packages/espressocash_app/lib/features/accounts/models/account.dart @@ -25,3 +25,9 @@ class AccessMode with _$AccessMode { const factory AccessMode.seedInputted() = _SeedInputted; const factory AccessMode.created() = _AccountCreated; } + +extension AccessModeExt on AccessMode { + bool get isLoaded => this is _Loaded; + bool get isSeedInputted => this is _SeedInputted; + bool get isAccountCreated => this is _AccountCreated; +} diff --git a/packages/espressocash_app/lib/features/authenticated/widgets/investment_header.dart b/packages/espressocash_app/lib/features/authenticated/widgets/investment_header.dart index 6b7c705d51..45bb50af2d 100644 --- a/packages/espressocash_app/lib/features/authenticated/widgets/investment_header.dart +++ b/packages/espressocash_app/lib/features/authenticated/widgets/investment_header.dart @@ -4,6 +4,7 @@ import '../../../l10n/l10n.dart'; import '../../../ui/button.dart'; import '../../../ui/colors.dart'; import '../../../ui/info_icon.dart'; +import '../../stellar_recovery/widgets/stellar_recovery_notice.dart'; import 'balance_amount.dart'; class InvestmentHeader extends StatefulWidget { @@ -33,6 +34,7 @@ class _InvestmentHeaderState extends State { const SizedBox(height: 4), const BalanceAmount(), const SizedBox(height: 12), + const StellarRecoveryNotice(), ], ), ), diff --git a/packages/espressocash_app/lib/features/intercom/services/intercom_service.dart b/packages/espressocash_app/lib/features/intercom/services/intercom_service.dart index 65fcb3e996..f5c07e2aac 100644 --- a/packages/espressocash_app/lib/features/intercom/services/intercom_service.dart +++ b/packages/espressocash_app/lib/features/intercom/services/intercom_service.dart @@ -36,6 +36,10 @@ class IntercomService implements Disposable { void updateCountry(String? countryCode) => Intercom.instance .updateUser(customAttributes: {'countryCode': countryCode}); + void updateStellarAddress(String address) => Intercom.instance.updateUser( + customAttributes: {'stellarAddress': address}, + ); + @override Future onDispose() => Intercom.instance.logout(); } diff --git a/packages/espressocash_app/lib/features/stellar/service/stellar_account_service.dart b/packages/espressocash_app/lib/features/stellar/service/stellar_account_service.dart index 6c6dcc8379..fac79ad34e 100644 --- a/packages/espressocash_app/lib/features/stellar/service/stellar_account_service.dart +++ b/packages/espressocash_app/lib/features/stellar/service/stellar_account_service.dart @@ -3,14 +3,20 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import '../../accounts/auth_scope.dart'; import '../../analytics/analytics_manager.dart'; +import '../../intercom/services/intercom_service.dart'; import '../models/stellar_wallet.dart'; @Singleton(scope: authScope) class StellarAccountService { - const StellarAccountService(this._stellarWallet, this._analyticsManager); + const StellarAccountService( + this._stellarWallet, + this._analyticsManager, + this._intercomService, + ); final StellarWallet _stellarWallet; final AnalyticsManager _analyticsManager; + final IntercomService _intercomService; @postConstruct void init() { @@ -20,6 +26,7 @@ class StellarAccountService { (scope) => scope.setExtra('stellarWalletAddress', address), ); _analyticsManager.setStellarAddress(address); + _intercomService.updateStellarAddress(address); } @disposeMethod diff --git a/packages/espressocash_app/lib/features/stellar_recovery/models/recovery_state.dart b/packages/espressocash_app/lib/features/stellar_recovery/models/recovery_state.dart new file mode 100644 index 0000000000..6dcbc7ceee --- /dev/null +++ b/packages/espressocash_app/lib/features/stellar_recovery/models/recovery_state.dart @@ -0,0 +1,37 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../currency/models/amount.dart'; + +part 'recovery_state.freezed.dart'; + +@Freezed(map: FreezedMapOptions.none, when: FreezedWhenOptions.none) +sealed class StellarRecoveryState with _$StellarRecoveryState { + const factory StellarRecoveryState.none() = RecoveryNone; + + const factory StellarRecoveryState.pending({ + required CryptoAmount amount, + }) = RecoveryPending; + + const factory StellarRecoveryState.processing({ + CryptoAmount? amount, + String? txId, + }) = RecoveryProcessing; + + const factory StellarRecoveryState.completed({ + required CryptoAmount amount, + required String txId, + }) = RecoveryCompleted; + + const factory StellarRecoveryState.failed() = RecoveryFailed; + + const factory StellarRecoveryState.dismissed() = RecoveryDismissed; +} + +extension StellarRecoveryStateX on StellarRecoveryState { + CryptoAmount? get amount => switch (this) { + RecoveryPending(:final amount) => amount, + RecoveryProcessing(:final amount) => amount, + RecoveryCompleted(:final amount) => amount, + _ => null, + }; +} diff --git a/packages/espressocash_app/lib/features/stellar_recovery/screens/recover_stellar_screen.dart b/packages/espressocash_app/lib/features/stellar_recovery/screens/recover_stellar_screen.dart new file mode 100644 index 0000000000..dee046464a --- /dev/null +++ b/packages/espressocash_app/lib/features/stellar_recovery/screens/recover_stellar_screen.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; + +import '../../../di.dart'; +import '../../../gen/assets.gen.dart'; +import '../../../l10n/device_locale.dart'; +import '../../../l10n/l10n.dart'; +import '../../../ui/app_bar.dart'; +import '../../../ui/button.dart'; +import '../../../ui/colors.dart'; +import '../../conversion_rates/widgets/extensions.dart'; +import '../models/recovery_state.dart'; +import '../service/recovery_service.dart'; + +class RecoverStellarScreen extends StatelessWidget { + const RecoverStellarScreen({super.key, required this.onConfirmed}); + + final VoidCallback onConfirmed; + + static void push( + BuildContext context, { + required VoidCallback onConfirmed, + }) => + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => RecoverStellarScreen( + onConfirmed: onConfirmed, + ), + ), + ); + + void _handleRecoverPressed() { + sl().recover(); + onConfirmed(); + } + + @override + Widget build(BuildContext context) => Scaffold( + backgroundColor: CpColors.yellowSplashBackgroundColor, + appBar: CpAppBar( + scrolledUnderColor: CpColors.yellowSplashBackgroundColor, + title: Text(context.l10n.moneyRecoveryTitle), + ), + extendBodyBehindAppBar: true, + extendBody: true, + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.all(32), + child: CpButton( + onPressed: _handleRecoverPressed, + text: context.l10n.moneyRecoveryBtn, + ), + ), + ), + body: Stack( + children: [ + Align( + child: Assets.images.dollarBg.image( + fit: BoxFit.fitHeight, + height: double.infinity, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.icons.money.svg( + height: 90, + width: 90, + ), + const SizedBox(height: 24), + Text( + context.l10n.moneyRecoveryContent( + sl() + .value + .amount + ?.format(context.locale, maxDecimals: 2) ?? + '-', + ), + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 32, + height: 0.95, + ), + ), + const SizedBox(height: 12), + Text( + context.l10n.moneyRecoverySubContent, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + const SizedBox(height: 6), + Text( + context.l10n.moneyRecoveryDisclaimer, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ); +} diff --git a/packages/espressocash_app/lib/features/stellar_recovery/service/recovery_service.dart b/packages/espressocash_app/lib/features/stellar_recovery/service/recovery_service.dart new file mode 100644 index 0000000000..20d464eda5 --- /dev/null +++ b/packages/espressocash_app/lib/features/stellar_recovery/service/recovery_service.dart @@ -0,0 +1,259 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:espressocash_api/espressocash_api.dart'; +import 'package:flutter/foundation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart' hide Currency; + +import '../../accounts/auth_scope.dart'; +import '../../accounts/models/account.dart'; +import '../../accounts/models/ec_wallet.dart'; +import '../../balances/services/refresh_balance.dart'; +import '../../currency/models/amount.dart'; +import '../../currency/models/currency.dart'; +import '../../ramp/partners/moneygram/data/allbridge_client.dart'; +import '../../ramp/partners/moneygram/data/allbridge_dto.dart'; +import '../../stellar/models/stellar_wallet.dart'; +import '../../stellar/service/stellar_client.dart'; +import '../../transactions/models/tx_results.dart'; +import '../../transactions/services/tx_confirm.dart'; +import '../models/recovery_state.dart'; + +@Singleton(scope: authScope) +class StellarRecoveryService extends ValueNotifier { + StellarRecoveryService( + this._ecWallet, + this._stellarWallet, + this._stellarClient, + this._ecClient, + this._account, + this._storage, + this._allbridgeApiClient, + this._txConfirm, + this._refreshBalance, + ) : super(const StellarRecoveryState.none()) { + addListener(_updateStorage); + } + + final MyAccount _account; + final ECWallet _ecWallet; + final StellarWallet _stellarWallet; + + final StellarClient _stellarClient; + final EspressoCashClient _ecClient; + final AllbridgeApiClient _allbridgeApiClient; + + final SharedPreferences _storage; + final TxConfirm _txConfirm; + final RefreshBalance _refreshBalance; + + StreamSubscription? _watcher; + + @PostConstruct() + void init() { + value = _getInitialState(); + + switch (value) { + case RecoveryNone(): + _checkAndInitiatePendingRecovery(); + case RecoveryProcessing(): + _watchBridgeTx(); + case RecoveryPending(): + case RecoveryCompleted(): + case RecoveryFailed(): + case RecoveryDismissed(): + break; + } + } + + StellarRecoveryState _getInitialState() { + final status = _storage.getString(_stellarRecoveryStatusKey); + final amount = _storage.getInt(_stellarRecoveryAmountKey).toCryptoAmount; + final txId = _storage.getString(_stellarRecoveryTxIdKey) ?? ''; + + return switch (status) { + 'pending' => StellarRecoveryState.pending(amount: amount), + 'processing' => + StellarRecoveryState.processing(amount: amount, txId: txId), + 'completed' => StellarRecoveryState.completed(amount: amount, txId: txId), + 'failed' => const StellarRecoveryState.failed(), + 'dismissed' => const StellarRecoveryState.dismissed(), + _ => const StellarRecoveryState.none(), + }; + } + + Future _checkAndInitiatePendingRecovery() async { + if (!_account.accessMode.isSeedInputted) return; + + final usdcBalance = await _stellarClient.getUsdcBalance(); + + if (usdcBalance == null || usdcBalance.isEmpty) return; + + final fee = await _ecClient.calculateMoneygramFee( + MoneygramFeeRequestDto( + type: RampTypeDto.onRamp, + amount: usdcBalance.toString(), + ), + ); + + final amount = Amount.fromDecimal( + value: Decimal.parse(fee.totalAmount), + currency: Currency.usdc, + ) as CryptoAmount; + + value = StellarRecoveryState.pending(amount: amount); + } + + Future recover() async { + if (value is! RecoveryPending && value is! RecoveryFailed) return; + + value = const StellarRecoveryState.processing(); + + try { + final usdcBalance = await _stellarClient.getUsdcBalance(); + if (usdcBalance == null || usdcBalance.isEmpty) { + value = const StellarRecoveryState.none(); + + return; + } + + final amount = Amount.fromDecimal( + value: Decimal.parse(usdcBalance.toString()), + currency: Currency.usdc, + ) as CryptoAmount; + + final hash = await _initiateSwapToSolana(amount); + + value = StellarRecoveryState.processing(amount: value.amount, txId: hash); + _watchBridgeTx(); + } on Exception { + value = const StellarRecoveryState.failed(); + } + } + + Future _initiateSwapToSolana(CryptoAmount amount) async { + final bridgeTx = await _ecClient + .swapToSolana( + SwapToSolanaRequestDto( + amount: (amount.value - 10).toString(), + stellarSenderAddress: _stellarWallet.address, + solanaReceiverAddress: _ecWallet.address, + ), + ) + .then((e) => e.encodedTx); + + final hash = await _stellarClient.submitTransactionFromXdrString(bridgeTx); + if (hash == null) throw Exception(); + + final result = await _stellarClient.pollStatus(hash); + if (result?.status != GetTransactionResponse.STATUS_SUCCESS) { + throw Exception(); + } + + return hash; + } + + void dismiss() { + if (value is! RecoveryCompleted) { + return; + } + + value = const StellarRecoveryState.dismissed(); + } + + void _watchBridgeTx() { + _watcher = + Stream.periodic(const Duration(seconds: 15)).listen((_) async { + final txId = switch (value) { + RecoveryProcessing(:final txId) => txId, + _ => null, + }; + + if (txId == null) { + return; + } + + final response = await _allbridgeApiClient.fetchStatus( + chain: Chain.stellar, + hash: txId, + ); + + final status = response?.receive; + + if (status == null) { + return; + } + + final solanaTxId = status.txId; + + final waitResult = await _txConfirm(txId: solanaTxId); + if (waitResult != const TxWaitSuccess()) { + return; + } + + _refreshBalance(); + + value = StellarRecoveryState.completed( + amount: value.amount ?? + const CryptoAmount(value: 0, cryptoCurrency: Currency.usdc), + txId: txId, + ); + + await _watcher?.cancel(); + }); + } + + void _updateStorage() { + switch (value) { + case RecoveryPending(:final amount): + _storage + ..setString(_stellarRecoveryStatusKey, 'pending') + ..setInt(_stellarRecoveryAmountKey, amount.value); + case RecoveryProcessing(:final txId): + _storage + ..setString(_stellarRecoveryStatusKey, 'processing') + ..setString(_stellarRecoveryTxIdKey, txId ?? ''); + case RecoveryCompleted(): + _storage.setString(_stellarRecoveryStatusKey, 'completed'); + case RecoveryFailed(): + _storage.setString(_stellarRecoveryStatusKey, 'failed'); + case RecoveryDismissed(): + _storage.setString(_stellarRecoveryStatusKey, 'dismissed'); + case RecoveryNone(): + break; + } + } + + @override + @disposeMethod + void dispose() { + removeListener(_updateStorage); + _watcher?.cancel(); + + _storage + ..remove(_stellarRecoveryStatusKey) + ..remove(_stellarRecoveryAmountKey) + ..remove(_stellarRecoveryTxIdKey); + super.dispose(); + } +} + +extension on double { + bool get isEmpty => this <= _minimumAmount; +} + +extension on int? { + CryptoAmount get toCryptoAmount => CryptoAmount( + value: this ?? 0, + cryptoCurrency: Currency.usdc, + ); +} + +// Cannot bridge less than this amount +const _minimumAmount = 2.0; + +const _stellarRecoveryStatusKey = 'stellarRecoveryStatus'; +const _stellarRecoveryAmountKey = 'stellarRecoveryAmount'; +const _stellarRecoveryTxIdKey = 'stellarRecoveryTxId'; diff --git a/packages/espressocash_app/lib/features/stellar_recovery/widgets/stellar_recovery_notice.dart b/packages/espressocash_app/lib/features/stellar_recovery/widgets/stellar_recovery_notice.dart new file mode 100644 index 0000000000..6014ca15a2 --- /dev/null +++ b/packages/espressocash_app/lib/features/stellar_recovery/widgets/stellar_recovery_notice.dart @@ -0,0 +1,214 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import '../../../di.dart'; +import '../../../gen/assets.gen.dart'; +import '../../../l10n/device_locale.dart'; +import '../../../l10n/l10n.dart'; +import '../../../ui/colors.dart'; +import '../../../ui/info_widget.dart'; +import '../../conversion_rates/widgets/extensions.dart'; +import '../../currency/models/amount.dart'; +import '../models/recovery_state.dart'; +import '../screens/recover_stellar_screen.dart'; +import '../service/recovery_service.dart'; + +class StellarRecoveryNotice extends StatefulWidget { + const StellarRecoveryNotice({super.key}); + + @override + State createState() => _StellarRecoveryNoticeState(); +} + +class _StellarRecoveryNoticeState extends State { + late final Future _recoveryServiceFuture; + bool _isVisible = true; + + @override + void initState() { + super.initState(); + _recoveryServiceFuture = sl.getAsync(); + } + + void _handleRecoverPressed() => RecoverStellarScreen.push( + context, + onConfirmed: () { + Navigator.of(context).pop(); + }, + ); + + void _handleHideNoticePressed() { + setState(() { + _isVisible = false; + }); + + if (sl().value is RecoveryCompleted) { + sl().dismiss(); + } + } + + @override + Widget build(BuildContext context) => _isVisible + ? FutureBuilder( + future: _recoveryServiceFuture, + builder: (context, snapshot) { + final recoveryService = snapshot.data; + + if (snapshot.connectionState != ConnectionState.done) { + return const SizedBox.shrink(); + } + + return snapshot.hasError || recoveryService == null + ? const SizedBox.shrink() + : ListenableBuilder( + listenable: recoveryService, + builder: (context, child) { + Widget notice(Widget child) => _RecoveryNoticeContent( + onClosePressed: _handleHideNoticePressed, + child: child, + ); + + return switch (recoveryService.value) { + RecoveryNone() || + RecoveryDismissed() => + const SizedBox.shrink(), + RecoveryPending() => notice( + _Pending(onRecoverPressed: _handleRecoverPressed), + ), + RecoveryProcessing() => notice(const _Processing()), + RecoveryCompleted(:final amount) => + notice(_Completed(amount: amount)), + RecoveryFailed() => notice( + _Failed(onRecoverPressed: _handleRecoverPressed), + ), + }; + }, + ); + }, + ) + : const SizedBox.shrink(); +} + +class _Pending extends StatelessWidget { + const _Pending({required this.onRecoverPressed}); + + final VoidCallback onRecoverPressed; + + @override + Widget build(BuildContext context) => Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${context.l10n.moneyRecoveryNoticeTitle} ', + ), + TextSpan( + text: context.l10n.moneyRecoveryNoticeAction, + style: const TextStyle( + color: CpColors.yellowColor, + ), + recognizer: TapGestureRecognizer()..onTap = onRecoverPressed, + ), + ], + ), + ); +} + +class _Processing extends StatelessWidget { + const _Processing(); + + @override + Widget build(BuildContext context) => Text(context.l10n.moneyRecoveryPending); +} + +class _Completed extends StatelessWidget { + const _Completed({required this.amount}); + + final CryptoAmount amount; + + @override + Widget build(BuildContext context) => Text( + context.l10n.moneyRecoverySuccess( + amount.format(context.locale, maxDecimals: 2), + ), + ); +} + +class _Failed extends StatelessWidget { + const _Failed({required this.onRecoverPressed}); + + final VoidCallback onRecoverPressed; + + @override + Widget build(BuildContext context) => Text.rich( + TextSpan( + children: [ + TextSpan(text: '${context.l10n.moneyRecoveryFailure} '), + TextSpan( + text: context.l10n.moneyRecoveryNoticeAction, + style: const TextStyle( + color: CpColors.yellowColor, + ), + recognizer: TapGestureRecognizer()..onTap = onRecoverPressed, + ), + ], + ), + ); +} + +class _RecoveryNoticeContent extends StatelessWidget { + const _RecoveryNoticeContent({ + required this.onClosePressed, + required this.child, + }); + + final VoidCallback onClosePressed; + final Widget child; + + @override + Widget build(BuildContext context) => DefaultTextStyle( + style: const TextStyle( + color: Colors.white, + fontSize: 14.5, + fontWeight: FontWeight.w500, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Center( + child: SizedBox( + width: 360, + child: CpInfoWidget( + message: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 2, + ), + child: Center(child: child), + ), + ), + GestureDetector( + onTap: onClosePressed, + child: SizedBox.square( + dimension: 12, + child: Assets.icons.closeButtonIcon.svg( + color: Colors.white, + ), + ), + ), + ], + ), + infoRadius: 12, + iconSize: 12, + variant: CpInfoVariant.black, + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + ), + ), + ), + ), + ); +} diff --git a/packages/espressocash_app/lib/l10n/intl_en.arb b/packages/espressocash_app/lib/l10n/intl_en.arb index 2dcdc84fce..a0133f2604 100644 --- a/packages/espressocash_app/lib/l10n/intl_en.arb +++ b/packages/espressocash_app/lib/l10n/intl_en.arb @@ -860,6 +860,40 @@ "@onRampCancelTitle": {}, "onRampCancelSubtitle": "Are you sure you want to cancel this deposit?", "@onRampCancelSubtitle": {}, + "moneyRecoveryTitle": "MONEY RECOVERY", + "@moneyRecoveryTitle": {}, + "moneyRecoveryBtn": "Recover", + "@moneyRecoveryBtn": {}, + "moneyRecoveryContent": "{amount} in unclaimed money has been detected and is ready for recovery.", + "@moneyRecoveryContent": { + "placeholders": { + "amount": { + "type": "String" + } + } + }, + "moneyRecoverySubContent": "The recovered amount will be added to your Espresso Cash account balance.", + "@moneyRecoverySubContent": {}, + "moneyRecoveryDisclaimer": "*This can take up to 3 minutes to complete.", + "@moneyRecoveryDisclaimer": {}, + "moneyRecoverySuccessNotice": "Success! Money will be recovered soon", + "@moneyRecoverySuccessNotice": {}, + "moneyRecoveryNoticeTitle": "Unclaimed money has been detected.", + "@moneyRecoveryNoticeTitle": {}, + "moneyRecoveryNoticeAction": "Click here to recover.", + "@moneyRecoveryNoticeAction": {}, + "moneyRecoveryPending": "Your money is being recovered. Please wait a moment...", + "@moneyRecoveryPending": {}, + "moneyRecoveryFailure": "There was an issue recovering your money.", + "@moneyRecoveryFailure": {}, + "moneyRecoverySuccess": "{amount} has been successfully added to your balance.", + "@moneyRecoverySuccess": { + "placeholders": { + "amount": { + "type": "String" + } + } + }, "offRampWithdrawalInProgress": "Withdrawing in progress...", "@offRampWithdrawalInProgress": {}, "moneygramCashAvailable": "Cash is available at Moneygram location", diff --git a/packages/espressocash_app/lib/ui/info_icon.dart b/packages/espressocash_app/lib/ui/info_icon.dart index 7419657cfa..c73ee30ee0 100644 --- a/packages/espressocash_app/lib/ui/info_icon.dart +++ b/packages/espressocash_app/lib/ui/info_icon.dart @@ -10,7 +10,7 @@ class CpInfoIcon extends StatelessWidget { this.height = 20, }); final Color iconColor; - final double height; + final double? height; @override Widget build(BuildContext context) => Assets.icons.info.svg( diff --git a/packages/espressocash_app/lib/ui/info_widget.dart b/packages/espressocash_app/lib/ui/info_widget.dart index 8e1c732da9..c64dbf4cac 100644 --- a/packages/espressocash_app/lib/ui/info_widget.dart +++ b/packages/espressocash_app/lib/ui/info_widget.dart @@ -12,11 +12,15 @@ class CpInfoWidget extends StatelessWidget { required this.message, this.variant = CpInfoVariant.light, this.padding = const EdgeInsets.all(24), + this.infoRadius = 14, + this.iconSize = 20, }); final Widget message; final EdgeInsetsGeometry padding; final CpInfoVariant variant; + final double infoRadius; + final double? iconSize; Color get _iconColor { switch (variant) { @@ -37,9 +41,9 @@ class CpInfoWidget extends StatelessWidget { Padding( padding: const EdgeInsets.only(right: 8), child: CircleAvatar( - maxRadius: 14, + maxRadius: infoRadius, backgroundColor: CpColors.yellowColor, - child: CpInfoIcon(iconColor: _iconColor), + child: CpInfoIcon(iconColor: _iconColor, height: iconSize), ), ), Flexible(