Skip to content

Commit 4f316bc

Browse files
committed
fix: transfer flow should allow recieve exact amount for liquid. insufficient balance errors should be part of form validation. continue should be disabled until form is validated. Split warning into two boxes. Split backup warning into bull points.
1 parent 52ce42a commit 4f316bc

35 files changed

Lines changed: 416 additions & 117 deletions

lib/core/errors/send_errors.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ class SwapCreationException extends BullException {
44
SwapCreationException(super.message);
55
}
66

7+
class InsufficientFundsSwapException extends SwapCreationException {
8+
InsufficientFundsSwapException() : super('Insufficient Funds');
9+
}
10+
711
class InsufficientBalanceException extends BullException {
812
InsufficientBalanceException(super.message);
913
}

lib/core/utils/logger.dart

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,21 @@ export 'package:logging_colorful/logging_colorful.dart';
1515
// `dep.Logger.root.onRecord` subscription before attaching a new one —
1616
// preventing the duplicate-listener leak that occurred when both
1717
// instances stayed subscribed to the same broadcast stream.
18-
Logger log = Logger.replace(directory: Directory.current);
18+
//
19+
// The placeholder is created with `persistToFile: false`: `Directory.current`
20+
// is the read-only filesystem root on iOS (`/`), so attempting to write
21+
// `//bull_logs.tsv` fails with a "Read-only file system" error. Early lines
22+
// are emitted to the console only; file persistence begins once `initLogs`
23+
// installs a logger anchored at the writable documents directory.
24+
Logger log = Logger.replace(
25+
directory: Directory.current,
26+
persistToFile: false,
27+
);
1928

2029
class Logger {
2130
final Directory dir;
2231
final dep.LoggerColorful logger;
32+
final bool persistToFile;
2333

2434
static const _logFilename = 'bull_logs.tsv';
2535
static const _maxLogSizeKb = 100;
@@ -39,7 +49,7 @@ class Logger {
3949

4050
File get logsFile => File('${dir.path}/$_logFilename');
4151

42-
Logger._(this.dir, this.logger) {
52+
Logger._(this.dir, this.logger, {this.persistToFile = true}) {
4353
dep.Logger.root.level = dep.Level.ALL;
4454

4555
_subscription = dep.Logger.root.onRecord.listen((record) {
@@ -70,14 +80,19 @@ class Logger {
7080
/// subscription. Idempotent: safe to call when no prior instance
7181
/// exists. Callers must reassign the top-level [log] holder to the
7282
/// returned instance — `Bull.initLogs` is the canonical caller.
73-
static Logger replace({String name = 'Logger', required Directory directory}) {
83+
static Logger replace({
84+
String name = 'Logger',
85+
required Directory directory,
86+
bool persistToFile = true,
87+
}) {
7488
_current?._subscription?.cancel();
7589
_current?._subscription = null;
7690
final next = Logger._(
7791
directory,
7892
// iOS emulator doesn't support colors –> https://github.com/flutter/flutter/issues/20663
7993
// We don't want colors in release mode either
8094
dep.LoggerColorful(name, disabledColors: Platform.isIOS || kReleaseMode),
95+
persistToFile: persistToFile,
8196
);
8297
_current = next;
8398
return next;
@@ -341,6 +356,10 @@ class Logger {
341356
}
342357

343358
void _queueWrite(String log, {bool flush = false}) {
359+
// The placeholder logger has no writable directory (anchored at the
360+
// read-only filesystem root); skip disk writes and rely on the
361+
// console output emitted by the listener / `_emitDirect`.
362+
if (!persistToFile) return;
344363
unawaited(
345364
_enqueue(() async {
346365
_ensureSinkOpen();

lib/features/swap/presentation/transfer_bloc.dart

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,12 @@ class TransferBloc extends Bloc<TransferEvent, TransferState> {
317317
TransferAmountChanged event,
318318
Emitter<TransferState> emit,
319319
) async {
320-
emit(state.copyWith(amount: event.amount));
320+
emit(
321+
state.copyWith(
322+
amount: event.amount,
323+
swapCreationException: null,
324+
),
325+
);
321326
}
322327

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

348+
// Insufficient balance is surfaced as an inline form error on the
349+
// amount field (see SwapAmountInput); stop here so no swap is created.
350+
final balanceSat = state.fromWallet?.balanceSat.toInt() ?? 0;
351+
if (inputAmountSat > balanceSat) {
352+
return;
353+
}
354+
343355
int paymentAmountSat = inputAmountSat;
344356
if (state.receiveExactAmount && !state.isSameChainTransfer) {
345357
final swapFees = state.swapFees;
@@ -643,13 +655,11 @@ class TransferBloc extends Bloc<TransferEvent, TransferState> {
643655
),
644656
);
645657
} catch (e) {
646-
final errorMessage = _isInsufficientFundsException(e)
647-
? 'Insufficient Balance To Cover Fees And Amount'
648-
: e.toString();
658+
final swapCreationException = _isInsufficientFundsException(e)
659+
? InsufficientFundsSwapException()
660+
: SwapCreationException(e.toString());
649661
emit(
650-
state.copyWith(
651-
swapCreationException: SwapCreationException(errorMessage),
652-
),
662+
state.copyWith(swapCreationException: swapCreationException),
653663
);
654664
} finally {
655665
emit(state.copyWith(isCreatingSwap: false, continueClicked: false));

lib/features/swap/presentation/transfer_state.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ sealed class TransferState with _$TransferState {
132132
return fromWallet?.isLiquid == false;
133133
}
134134

135+
bool get shouldShowReceiveExactAmount {
136+
return fromWallet != null && !isSameChainTransfer;
137+
}
138+
135139
int get selectedUtxoTotalSat {
136140
return selectedUtxos.fold(
137141
0,
@@ -182,6 +186,16 @@ sealed class TransferState with _$TransferState {
182186
}
183187
}
184188

189+
bool get isInsufficientBalance {
190+
if (fromWallet == null) return false;
191+
if (amount.isEmpty || inputAmountSat <= 0) return false;
192+
return inputAmountSat > fromWallet!.balanceSat.toInt();
193+
}
194+
195+
bool get hasAmountError {
196+
return amountValidationError != null || isInsufficientBalance;
197+
}
198+
185199
String? get amountValidationError {
186200
if (amount.isEmpty) return null;
187201

lib/features/swap/ui/pages/swap_in_progress_page.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,14 @@ class SwapInProgressPage extends StatelessWidget {
128128
if (swap?.status != SwapStatus.completed) ...[
129129
if (swap?.status != SwapStatus.completed) ...[
130130
InfoCard(
131-
description:
132-
'${context.loc.swapDoNotUninstallWarning}\n\n'
133-
'${context.loc.transactionSwapOpenWithin24h}',
131+
description: context.loc.swapDoNotUninstallWarning,
132+
tagColor: context.appColors.tertiary,
133+
bgColor: context.appColors.warningContainer,
134+
boldDescription: true,
135+
),
136+
const Gap(12),
137+
InfoCard(
138+
description: context.loc.transactionSwapOpenWithin24h,
134139
tagColor: context.appColors.tertiary,
135140
bgColor: context.appColors.warningContainer,
136141
boldDescription: true,

lib/features/swap/ui/pages/swap_page.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,12 @@ class SwapPageState extends State<SwapPage> {
218218
if (swapCreationError == null) {
219219
return const SizedBox.shrink();
220220
}
221+
final message =
222+
swapCreationError is InsufficientFundsSwapException
223+
? context.loc.swapInsufficientFunds
224+
: swapCreationError.message;
221225
return Text(
222-
swapCreationError.message,
226+
message,
223227
style: context.font.labelLarge?.copyWith(
224228
color: context.appColors.error,
225229
),
@@ -228,9 +232,7 @@ class SwapPageState extends State<SwapPage> {
228232
},
229233
),
230234
BlocSelector<TransferBloc, TransferState, bool>(
231-
selector: (state) =>
232-
state.shouldShowAdvancedOptions &&
233-
!state.isSameChainTransfer,
235+
selector: (state) => state.shouldShowReceiveExactAmount,
234236
builder: (context, showReceiveExactAmount) {
235237
if (!showReceiveExactAmount) {
236238
return const SizedBox.shrink();
@@ -311,13 +313,14 @@ class SwapPageState extends State<SwapPage> {
311313
selector: (state) =>
312314
state.isStarting ||
313315
state.isCreatingSwap ||
314-
state.continueClicked,
315-
builder: (context, isLoading) {
316+
state.continueClicked ||
317+
state.hasAmountError,
318+
builder: (context, disabled) {
316319
return BBButton.big(
317320
label: context.loc.swapContinueButton,
318321
bgColor: context.appColors.secondary,
319322
textColor: context.appColors.onSecondary,
320-
disabled: isLoading,
323+
disabled: disabled,
321324
onPressed: () {
322325
if (!_formKey.currentState!.validate()) {
323326
return;

lib/features/swap/ui/widgets/swap_amount_input.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class SwapAmountInput extends StatelessWidget {
3434
final amountValidationError = context.select(
3535
(TransferBloc bloc) => bloc.state.amountValidationError,
3636
);
37+
final isInsufficientBalance = context.select(
38+
(TransferBloc bloc) => bloc.state.isInsufficientBalance,
39+
);
3740

3841
return Column(
3942
crossAxisAlignment: .start,
@@ -103,10 +106,10 @@ class SwapAmountInput extends StatelessWidget {
103106
),
104107
),
105108
),
106-
if (amountValidationError != null) ...[
109+
if (amountValidationError != null || isInsufficientBalance) ...[
107110
const Gap(8),
108111
Text(
109-
amountValidationError,
112+
amountValidationError ?? context.loc.swapInsufficientFunds,
110113
style: context.font.labelLarge?.copyWith(
111114
color: context.appColors.error,
112115
),

lib/features/wallet/ui/widgets/backup_warning_overlay.dart

Lines changed: 98 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -53,53 +53,106 @@ class _BackupWarningBlockerState extends State<_BackupWarningBlocker> {
5353
child: Align(
5454
alignment: Alignment.bottomCenter,
5555
child: SafeArea(
56-
child: Container(
57-
padding: const EdgeInsets.all(24),
58-
decoration: BoxDecoration(
59-
color: context.appColors.surface,
60-
borderRadius: const BorderRadius.vertical(
61-
top: Radius.circular(16),
62-
),
56+
child: ConstrainedBox(
57+
constraints: BoxConstraints(
58+
maxHeight: MediaQuery.sizeOf(context).height * 0.85,
6359
),
64-
child: Column(
65-
mainAxisSize: MainAxisSize.min,
66-
crossAxisAlignment: CrossAxisAlignment.stretch,
67-
children: [
68-
BBText(
69-
context.loc.backupWarningTitle,
70-
style: context.font.headlineMedium,
71-
color: context.appColors.onSurface,
72-
),
73-
const Gap(16),
74-
BBText(
75-
context.loc.backupWarningDescription,
76-
style: context.font.bodyMedium,
77-
color: context.appColors.onSurface,
78-
),
79-
const Gap(24),
80-
BBButton.big(
81-
label: context.loc.backupWarningBackupNow,
82-
onPressed: () {
83-
context.pushNamed(
84-
BackupSettingsSubroute.backupOptions.name,
85-
);
86-
},
87-
bgColor: context.appColors.onSurface,
88-
textColor: context.appColors.surface,
60+
child: Container(
61+
padding: const EdgeInsets.all(24),
62+
decoration: BoxDecoration(
63+
color: context.appColors.surface,
64+
borderRadius: const BorderRadius.vertical(
65+
top: Radius.circular(16),
8966
),
90-
const Gap(12),
91-
BBButton.big(
92-
label: context.loc.backupWarningBackupLater,
93-
onPressed: () {
94-
context
95-
.read<WalletBloc>()
96-
.add(const DismissBackupWarning());
97-
},
98-
bgColor: context.appColors.surface,
99-
textColor: context.appColors.onSurface,
100-
outlined: true,
101-
),
102-
],
67+
),
68+
child: Column(
69+
mainAxisSize: MainAxisSize.min,
70+
crossAxisAlignment: CrossAxisAlignment.stretch,
71+
children: [
72+
Flexible(
73+
child: SingleChildScrollView(
74+
child: Column(
75+
mainAxisSize: MainAxisSize.min,
76+
crossAxisAlignment: CrossAxisAlignment.stretch,
77+
children: [
78+
BBText(
79+
context.loc.backupWarningTitle,
80+
style: context.font.headlineMedium?.copyWith(
81+
fontWeight: FontWeight.bold,
82+
),
83+
color: context.appColors.onSurface,
84+
),
85+
const Gap(16),
86+
BBText(
87+
context.loc.backupWarningDescription,
88+
style: context.font.bodyMedium,
89+
color: context.appColors.onSurface,
90+
),
91+
const Gap(8),
92+
for (final reason in [
93+
context.loc.backupWarningLoseReasonLostPhone,
94+
context.loc.backupWarningLoseReasonDeletedApp,
95+
context.loc.backupWarningLoseReasonCriticalIssue,
96+
context.loc.backupWarningLoseReasonKeystore,
97+
context.loc.backupWarningLoseReasonCloudRestore,
98+
])
99+
Padding(
100+
padding: const EdgeInsets.only(bottom: 6),
101+
child: Row(
102+
crossAxisAlignment: CrossAxisAlignment.start,
103+
children: [
104+
BBText(
105+
'• ',
106+
style: context.font.bodyMedium,
107+
color: context.appColors.onSurface,
108+
),
109+
Expanded(
110+
child: BBText(
111+
reason,
112+
style: context.font.bodyMedium,
113+
color: context.appColors.onSurface,
114+
),
115+
),
116+
],
117+
),
118+
),
119+
const Gap(8),
120+
BBText(
121+
context.loc.backupWarningNoRecovery,
122+
style: context.font.bodyMedium?.copyWith(
123+
fontWeight: FontWeight.bold,
124+
),
125+
color: context.appColors.onSurface,
126+
),
127+
],
128+
),
129+
),
130+
),
131+
const Gap(24),
132+
BBButton.big(
133+
label: context.loc.backupWarningBackupNow,
134+
onPressed: () {
135+
context.pushNamed(
136+
BackupSettingsSubroute.backupOptions.name,
137+
);
138+
},
139+
bgColor: context.appColors.onSurface,
140+
textColor: context.appColors.surface,
141+
),
142+
const Gap(12),
143+
BBButton.big(
144+
label: context.loc.backupWarningBackupLater,
145+
onPressed: () {
146+
context
147+
.read<WalletBloc>()
148+
.add(const DismissBackupWarning());
149+
},
150+
bgColor: context.appColors.surface,
151+
textColor: context.appColors.onSurface,
152+
outlined: true,
153+
),
154+
],
155+
),
103156
),
104157
),
105158
),

0 commit comments

Comments
 (0)