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(