Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/core/errors/send_errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ class SwapCreationException extends BullException {
SwapCreationException(super.message);
}

class InsufficientFundsSwapException extends SwapCreationException {
InsufficientFundsSwapException() : super('Insufficient Funds');
}

class InsufficientBalanceException extends BullException {
InsufficientBalanceException(super.message);
}
Expand Down
24 changes: 17 additions & 7 deletions lib/features/swap/presentation/transfer_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,12 @@ class TransferBloc extends Bloc<TransferEvent, TransferState> {
TransferAmountChanged event,
Emitter<TransferState> emit,
) async {
emit(state.copyWith(amount: event.amount));
emit(
state.copyWith(
amount: event.amount,
swapCreationException: null,
),
);
}

Future<void> _onSwapCreated(
Expand All @@ -340,6 +345,13 @@ class TransferBloc extends Bloc<TransferEvent, TransferState> {
? int.parse(event.amount)
: ConvertAmount.btcToSats(double.parse(event.amount));

// Insufficient balance is surfaced as an inline form error on the
// amount field (see SwapAmountInput); stop here so no swap is created.
final balanceSat = state.fromWallet?.balanceSat.toInt() ?? 0;
if (inputAmountSat > balanceSat) {
return;
}

int paymentAmountSat = inputAmountSat;
if (state.receiveExactAmount && !state.isSameChainTransfer) {
final swapFees = state.swapFees;
Expand Down Expand Up @@ -643,13 +655,11 @@ class TransferBloc extends Bloc<TransferEvent, TransferState> {
),
);
} catch (e) {
final errorMessage = _isInsufficientFundsException(e)
? 'Insufficient Balance To Cover Fees And Amount'
: e.toString();
final swapCreationException = _isInsufficientFundsException(e)
? InsufficientFundsSwapException()
: SwapCreationException(e.toString());
emit(
state.copyWith(
swapCreationException: SwapCreationException(errorMessage),
),
state.copyWith(swapCreationException: swapCreationException),
);
} finally {
emit(state.copyWith(isCreatingSwap: false, continueClicked: false));
Expand Down
15 changes: 14 additions & 1 deletion lib/features/swap/presentation/transfer_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ sealed class TransferState with _$TransferState {
}

int get inputAmountSat {
if (amount.isEmpty) return 0;
if (bitcoinUnit == BitcoinUnit.sats) {
return int.tryParse(amount) ?? 0;
} else {
Expand Down Expand Up @@ -132,6 +131,10 @@ sealed class TransferState with _$TransferState {
return fromWallet?.isLiquid == false;
}

bool get shouldShowReceiveExactAmount {
return fromWallet != null && !isSameChainTransfer;
}

int get selectedUtxoTotalSat {
return selectedUtxos.fold(
0,
Expand Down Expand Up @@ -182,6 +185,16 @@ sealed class TransferState with _$TransferState {
}
}

bool get isInsufficientBalance {
if (fromWallet == null) return false;
if (inputAmountSat <= 0) return false;
return inputAmountSat > fromWallet!.balanceSat.toInt();
}

bool get hasAmountError {
return amountValidationError != null || isInsufficientBalance;
}

String? get amountValidationError {
if (amount.isEmpty) return null;

Expand Down
11 changes: 8 additions & 3 deletions lib/features/swap/ui/pages/swap_in_progress_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,14 @@ class SwapInProgressPage extends StatelessWidget {
if (swap?.status != SwapStatus.completed) ...[
if (swap?.status != SwapStatus.completed) ...[
InfoCard(
description:
'${context.loc.swapDoNotUninstallWarning}\n\n'
'${context.loc.transactionSwapOpenWithin24h}',
description: context.loc.swapDoNotUninstallWarning,
tagColor: context.appColors.tertiary,
bgColor: context.appColors.warningContainer,
boldDescription: true,
),
const Gap(12),
InfoCard(
description: context.loc.transactionSwapOpenWithin24h,
tagColor: context.appColors.tertiary,
bgColor: context.appColors.warningContainer,
boldDescription: true,
Expand Down
17 changes: 10 additions & 7 deletions lib/features/swap/ui/pages/swap_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,12 @@ class SwapPageState extends State<SwapPage> {
if (swapCreationError == null) {
return const SizedBox.shrink();
}
final message =
swapCreationError is InsufficientFundsSwapException
? context.loc.swapInsufficientFunds
: swapCreationError.message;
return Text(
swapCreationError.message,
message,
style: context.font.labelLarge?.copyWith(
color: context.appColors.error,
),
Expand All @@ -228,9 +232,7 @@ class SwapPageState extends State<SwapPage> {
},
),
BlocSelector<TransferBloc, TransferState, bool>(
selector: (state) =>
state.shouldShowAdvancedOptions &&
!state.isSameChainTransfer,
selector: (state) => state.shouldShowReceiveExactAmount,
builder: (context, showReceiveExactAmount) {
if (!showReceiveExactAmount) {
return const SizedBox.shrink();
Expand Down Expand Up @@ -311,13 +313,14 @@ class SwapPageState extends State<SwapPage> {
selector: (state) =>
state.isStarting ||
state.isCreatingSwap ||
state.continueClicked,
builder: (context, isLoading) {
state.continueClicked ||
state.hasAmountError,
builder: (context, disabled) {
return BBButton.big(
label: context.loc.swapContinueButton,
bgColor: context.appColors.secondary,
textColor: context.appColors.onSecondary,
disabled: isLoading,
disabled: disabled,
onPressed: () {
if (!_formKey.currentState!.validate()) {
return;
Expand Down
7 changes: 5 additions & 2 deletions lib/features/swap/ui/widgets/swap_amount_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class SwapAmountInput extends StatelessWidget {
final amountValidationError = context.select(
(TransferBloc bloc) => bloc.state.amountValidationError,
);
final isInsufficientBalance = context.select(
(TransferBloc bloc) => bloc.state.isInsufficientBalance,
);

return Column(
crossAxisAlignment: .start,
Expand Down Expand Up @@ -103,10 +106,10 @@ class SwapAmountInput extends StatelessWidget {
),
),
),
if (amountValidationError != null) ...[
if (amountValidationError != null || isInsufficientBalance) ...[
const Gap(8),
Text(
amountValidationError,
amountValidationError ?? context.loc.swapInsufficientFunds,
style: context.font.labelLarge?.copyWith(
color: context.appColors.error,
),
Expand Down
143 changes: 98 additions & 45 deletions lib/features/wallet/ui/widgets/backup_warning_overlay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,53 +53,106 @@ class _BackupWarningBlockerState extends State<_BackupWarningBlocker> {
child: Align(
alignment: Alignment.bottomCenter,
child: SafeArea(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: context.appColors.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * 0.85,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
BBText(
context.loc.backupWarningTitle,
style: context.font.headlineMedium,
color: context.appColors.onSurface,
),
const Gap(16),
BBText(
context.loc.backupWarningDescription,
style: context.font.bodyMedium,
color: context.appColors.onSurface,
),
const Gap(24),
BBButton.big(
label: context.loc.backupWarningBackupNow,
onPressed: () {
context.pushNamed(
BackupSettingsSubroute.backupOptions.name,
);
},
bgColor: context.appColors.onSurface,
textColor: context.appColors.surface,
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: context.appColors.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
const Gap(12),
BBButton.big(
label: context.loc.backupWarningBackupLater,
onPressed: () {
context
.read<WalletBloc>()
.add(const DismissBackupWarning());
},
bgColor: context.appColors.surface,
textColor: context.appColors.onSurface,
outlined: true,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
BBText(
context.loc.backupWarningTitle,
style: context.font.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
color: context.appColors.onSurface,
),
const Gap(16),
BBText(
context.loc.backupWarningDescription,
style: context.font.bodyMedium,
color: context.appColors.onSurface,
),
const Gap(8),
for (final reason in [
context.loc.backupWarningLoseReasonLostPhone,
context.loc.backupWarningLoseReasonDeletedApp,
context.loc.backupWarningLoseReasonCriticalIssue,
context.loc.backupWarningLoseReasonKeystore,
context.loc.backupWarningLoseReasonCloudRestore,
])
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BBText(
'• ',
style: context.font.bodyMedium,
color: context.appColors.onSurface,
),
Expanded(
child: BBText(
reason,
style: context.font.bodyMedium,
color: context.appColors.onSurface,
),
),
],
),
),
const Gap(8),
BBText(
context.loc.backupWarningNoRecovery,
style: context.font.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
color: context.appColors.onSurface,
),
],
),
),
),
const Gap(24),
BBButton.big(
label: context.loc.backupWarningBackupNow,
onPressed: () {
context.pushNamed(
BackupSettingsSubroute.backupOptions.name,
);
},
bgColor: context.appColors.onSurface,
textColor: context.appColors.surface,
),
const Gap(12),
BBButton.big(
label: context.loc.backupWarningBackupLater,
onPressed: () {
context
.read<WalletBloc>()
.add(const DismissBackupWarning());
},
bgColor: context.appColors.surface,
textColor: context.appColors.onSurface,
outlined: true,
),
],
),
),
),
),
Expand Down
8 changes: 8 additions & 0 deletions localization/app_ar.arb
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"backupWarningDescription": "بدون نسخة احتياطية، ستفقد إمكانية الوصول إلى عملات البيتكوين الخاصة بك في الحالات التالية:",
"backupWarningLoseReasonLostPhone": "فقدان هاتفك",
"backupWarningLoseReasonDeletedApp": "حذف التطبيق",
"backupWarningLoseReasonCriticalIssue": "حدوث مشكلة خطيرة في التطبيق أو الجهاز",
"backupWarningLoseReasonKeystore": "يقوم جهازك بإبطال مخزن المفاتيح (keystore) عند تغيير طريقة قفل الشاشة أو مصادقة الجهاز",
"backupWarningLoseReasonCloudRestore": "استعادة جهازك من نسخة احتياطية على السحابة",
"backupWarningNoRecovery": "لا توجد طريقة لاستعادة محفظتك بدون نسخة احتياطية.",
"ledgerHelpTitle": "Ledger Troubleshooting",
"@ledgerHelpTitle": {
"description": "Title for Ledger troubleshooting help modal"
Expand Down Expand Up @@ -500,6 +507,7 @@
}
},
"swapReceiveExactAmountLabel": "المبلغ المستلم",
"swapInsufficientFunds": "أموال غير كافية",
"@swapReceiveExactAmountLabel": {
"description": "Label when receive exact amount toggle is on"
},
Expand Down
12 changes: 8 additions & 4 deletions localization/app_as.arb
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"backupWarningDescription": "বেকআপ অবিহনে, তলত উল্লেখ কৰা ক্ষেত্ৰত আপুনি আপোনাৰ bitcoin-লৈ প্ৰৱেশ হেৰুৱাব:",
"backupWarningLoseReasonLostPhone": "আপুনি আপোনাৰ ফোন হেৰুৱালে",
"backupWarningLoseReasonDeletedApp": "আপুনি এপটো মচিলে",
"backupWarningLoseReasonCriticalIssue": "কোনো গুৰুতৰ এপ্প বা ডিভাইচ সম্পৰ্কীয় সমস্যা হ'লে",
"backupWarningLoseReasonKeystore": "আপুনি লকস্ক্ৰীন বা ডিভাইচ অথেণ্টিকেশ্বন সলালে আপোনাৰ ডিভাইচে কীষ্টোৰ (keystore) অবৈধ কৰি দিয়ে",
"backupWarningLoseReasonCloudRestore": "ক্লাউড বেকআপৰ পৰা আপোনাৰ ডিভাইচ ৰিষ্টোৰ কৰা",
"backupWarningNoRecovery": "বেকআপ অবিহনে আপোনাৰ wallet পুনৰুদ্ধাৰ কৰাৰ কোনো উপায় নাই।",
"appInitErrorTitle": "এপটো আৰম্ভ হোৱাত ব্যৰ্থ হ'ল",
"@appInitErrorTitle": {
"description": "Title shown on the fatal init error screen in main.dart"
Expand Down Expand Up @@ -1929,6 +1936,7 @@
"description": "Label for amount input section"
},
"swapReceiveExactAmountLabel": "নিৰ্দিষ্ট পৰিমাণ গ্ৰহণ কৰক",
"swapInsufficientFunds": "অপৰ্যাপ্ত পুঁজি",
"@swapReceiveExactAmountLabel": {
"description": "Label when receive exact amount toggle is on"
},
Expand Down Expand Up @@ -9604,10 +9612,6 @@
"@backupWarningTitle": {
"description": "Title for the backup warning bottom sheet"
},
"backupWarningDescription": "বেকআপ অবিহনে, আপোনাৰ ফোন হেৰুৱালে, এপটো মচিলে, বা গুৰুতৰ এপ্প বা ডিভাইচ সম্পৰ্কীয় সমস্যাত পৰিলে আপুনি আপোনাৰ bitcoin-লৈ প্ৰৱেশ হেৰুৱাব। বেকআপ অবিহনে আপোনাৰ wallet পুনৰুদ্ধাৰ কৰাৰ কোনো উপায় নাই।",
"@backupWarningDescription": {
"description": "Description explaining why backup is important"
},
"backupWarningBackupNow": "হয়",
"@backupWarningBackupNow": {
"description": "Button to navigate to backup options"
Expand Down
8 changes: 8 additions & 0 deletions localization/app_bg.arb
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"backupWarningDescription": "Без резервно копие ще загубите достъп до своите биткоини, ако:",
"backupWarningLoseReasonLostPhone": "Загубите телефона си",
"backupWarningLoseReasonDeletedApp": "Изтриете приложението",
"backupWarningLoseReasonCriticalIssue": "Възникне критичен проблем с приложението или устройството",
"backupWarningLoseReasonKeystore": "Устройството анулира keystore при смяна на заключения екран или удостоверяването на устройството",
"backupWarningLoseReasonCloudRestore": "Възстановяване на устройството от резервно копие в облака",
"backupWarningNoRecovery": "Без резервно копие няма начин да възстановите портфейла си.",
"broadcastSignedTxBroadcastError": "Неуспешно излъчване на транзакцията",
"translationWarningTitle": "Преводи, генерирани от ИИ",
"@translationWarningTitle": {
Expand Down Expand Up @@ -683,6 +690,7 @@
}
},
"swapReceiveExactAmountLabel": "Получете точната сума",
"swapInsufficientFunds": "Недостатъчни средства",
"@swapReceiveExactAmountLabel": {
"description": "Label when receive exact amount toggle is on"
},
Expand Down
Loading