Skip to content

Commit 8efe8a4

Browse files
authored
Merge pull request #1786 from SatoshiPortal/recipients-reuse-improvements
Recipients reuse improvements
2 parents 9f62191 + e5a685b commit 8efe8a4

44 files changed

Lines changed: 762 additions & 613 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lib/core/exchange/data/datasources/bullbitcoin_api_datasource.dart

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -405,23 +405,29 @@ class BullbitcoinApiDatasource implements BitcoinPriceDatasource {
405405
}
406406
if (error != null) {
407407
final reason = error['data']['reason'];
408-
final limitReason = reason['limit'];
409-
if (limitReason != null) {
410-
final isBelowLimit =
411-
limitReason['conditionalOperator'] == 'GREATER_THAN_OR_EQUAL';
412-
final limitAmount = limitReason['amount'] as String;
413-
final limitCurrency = limitReason['currencyCode'] as String;
414-
if (isBelowLimit) {
415-
throw BullBitcoinApiMinAmountException(
416-
minAmount: double.parse(limitAmount),
417-
currency: limitCurrency,
418-
);
419-
} else {
420-
throw BullBitcoinApiMaxAmountException(
421-
maxAmount: double.parse(limitAmount),
422-
currency: limitCurrency,
423-
);
408+
if (reason != null) {
409+
final limitReason = reason['limit'];
410+
if (limitReason != null) {
411+
final isBelowLimit =
412+
limitReason['conditionalOperator'] == 'GREATER_THAN_OR_EQUAL';
413+
final limitAmount = limitReason['amount'] as String;
414+
final limitCurrency = limitReason['currencyCode'] as String;
415+
if (isBelowLimit) {
416+
throw BullBitcoinApiMinAmountException(
417+
minAmount: double.parse(limitAmount),
418+
currency: limitCurrency,
419+
);
420+
} else {
421+
throw BullBitcoinApiMaxAmountException(
422+
maxAmount: double.parse(limitAmount),
423+
currency: limitCurrency,
424+
);
425+
}
424426
}
427+
} else {
428+
throw Exception(
429+
'Failed to create sell to recipient order: ${error['message']}',
430+
);
425431
}
426432
}
427433

@@ -776,10 +782,7 @@ class BullbitcoinApiDatasource implements BitcoinPriceDatasource {
776782
'id': 1,
777783
'method': 'createMyKYCIDDocument',
778784
'params': {
779-
'element': {
780-
'idTypeCode': docType,
781-
'sourceDetail': sourceDetail,
782-
},
785+
'element': {'idTypeCode': docType, 'sourceDetail': sourceDetail},
783786
},
784787
}),
785788
});

lib/core/widgets/cards/action_card.dart

Lines changed: 73 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import 'package:bb_mobile/features/buy/ui/buy_router.dart';
66
import 'package:bb_mobile/features/exchange/presentation/exchange_cubit.dart';
77
import 'package:bb_mobile/features/exchange/ui/exchange_router.dart';
88
import 'package:bb_mobile/features/pay/ui/pay_router.dart';
9-
import 'package:bb_mobile/features/recipients/frameworks/ui/routing/recipients_router.dart';
10-
import 'package:bb_mobile/features/recipients/interface_adapters/presenters/models/recipient_view_model.dart';
119
import 'package:bb_mobile/features/sell/ui/sell_router.dart';
1210
import 'package:bb_mobile/features/settings/presentation/bloc/settings_cubit.dart';
1311
import 'package:bb_mobile/features/swap/ui/swap_router.dart';
@@ -37,10 +35,7 @@ class _ActionRow extends StatelessWidget {
3735
decoration: BoxDecoration(
3836
color: context.appColors.background,
3937
border: Border(
40-
bottom: BorderSide(
41-
color: context.appColors.outline,
42-
width: 1,
43-
),
38+
bottom: BorderSide(color: context.appColors.outline, width: 1),
4439
),
4540
),
4641
child: Material(
@@ -50,116 +45,92 @@ class _ActionRow extends StatelessWidget {
5045
child: SizedBox(
5146
height: 80,
5247
child: Row(
53-
children: [
54-
_ActionButton(
55-
icon: Assets.icons.btc.path,
56-
label: 'Buy',
57-
onPressed: () {
58-
if (Platform.isIOS) {
59-
final isSuperuser =
60-
context.read<SettingsCubit>().state.isSuperuser ?? false;
61-
if (isSuperuser) {
62-
context.pushNamed(BuyRoute.buy.name);
63-
} else {
64-
context.goNamed(ExchangeRoute.exchangeLanding.name);
65-
}
66-
} else {
67-
context.pushNamed(BuyRoute.buy.name);
68-
}
69-
},
70-
position: _ButtonPosition.first,
71-
disabled: false,
72-
),
73-
const Gap(1),
74-
_ActionButton(
75-
icon: Assets.icons.dollar.path,
76-
label: 'Sell',
77-
onPressed: () {
78-
if (Platform.isIOS) {
79-
final isSuperuser =
80-
context.read<SettingsCubit>().state.isSuperuser ?? false;
81-
if (isSuperuser) {
82-
context.pushNamed(SellRoute.sell.name);
48+
children: [
49+
_ActionButton(
50+
icon: Assets.icons.btc.path,
51+
label: 'Buy',
52+
onPressed: () {
53+
if (Platform.isIOS) {
54+
final isSuperuser =
55+
context.read<SettingsCubit>().state.isSuperuser ??
56+
false;
57+
if (isSuperuser) {
58+
context.pushNamed(BuyRoute.buy.name);
59+
} else {
60+
context.goNamed(ExchangeRoute.exchangeLanding.name);
61+
}
8362
} else {
84-
context.goNamed(ExchangeRoute.exchangeLanding.name);
63+
context.pushNamed(BuyRoute.buy.name);
8564
}
86-
} else {
87-
context.pushNamed(SellRoute.sell.name);
88-
}
89-
},
90-
position: _ButtonPosition.middle,
91-
disabled: false,
92-
),
93-
const Gap(1),
94-
_ActionButton(
95-
icon: Assets.icons.rightArrow.path,
96-
label: 'Pay',
97-
onPressed: () {
98-
final notLoggedIn =
99-
context.read<ExchangeCubit>().state.notLoggedIn;
100-
101-
if (notLoggedIn) {
102-
context.goNamed(ExchangeRoute.exchangeLanding.name);
103-
} else {
65+
},
66+
position: _ButtonPosition.first,
67+
disabled: false,
68+
),
69+
const Gap(1),
70+
_ActionButton(
71+
icon: Assets.icons.dollar.path,
72+
label: 'Sell',
73+
onPressed: () {
10474
if (Platform.isIOS) {
10575
final isSuperuser =
10676
context.read<SettingsCubit>().state.isSuperuser ??
10777
false;
10878
if (isSuperuser) {
109-
// Reuse the recipients screen to select a recipient before
110-
// navigating to the Pay screen.
111-
context.pushNamed(
112-
RecipientsRoute.recipients.name,
113-
extra: RecipientsRouteExtra(
114-
onRecipientSelected: (
115-
RecipientViewModel recipient,
116-
) async {
117-
await context.pushNamed(
118-
PayRoute.pay.name,
119-
extra: recipient,
120-
);
121-
},
122-
),
123-
);
79+
context.pushNamed(SellRoute.sell.name);
12480
} else {
12581
context.goNamed(ExchangeRoute.exchangeLanding.name);
12682
}
12783
} else {
128-
// Reuse the recipients screen to select a recipient before
129-
// navigating to the Pay screen.
130-
context.pushNamed(
131-
RecipientsRoute.recipients.name,
132-
extra: RecipientsRouteExtra(
133-
onRecipientSelected: (
134-
RecipientViewModel recipient,
135-
) async {
136-
await context.pushNamed(
137-
PayRoute.pay.name,
138-
extra: recipient,
139-
);
140-
},
141-
),
142-
);
84+
context.pushNamed(SellRoute.sell.name);
85+
}
86+
},
87+
position: _ButtonPosition.middle,
88+
disabled: false,
89+
),
90+
const Gap(1),
91+
_ActionButton(
92+
icon: Assets.icons.rightArrow.path,
93+
label: 'Pay',
94+
onPressed: () {
95+
final notLoggedIn = context
96+
.read<ExchangeCubit>()
97+
.state
98+
.notLoggedIn;
99+
100+
if (notLoggedIn) {
101+
context.goNamed(ExchangeRoute.exchangeLanding.name);
102+
} else {
103+
if (Platform.isIOS) {
104+
final isSuperuser =
105+
context.read<SettingsCubit>().state.isSuperuser ??
106+
false;
107+
if (isSuperuser) {
108+
context.pushNamed(PayRoute.pay.name);
109+
} else {
110+
context.goNamed(ExchangeRoute.exchangeLanding.name);
111+
}
112+
} else {
113+
context.pushNamed(PayRoute.pay.name);
114+
}
143115
}
144-
}
145-
},
146-
position: _ButtonPosition.middle,
147-
disabled: false,
148-
),
149-
const Gap(1),
150-
_ActionButton(
151-
icon: Assets.icons.swap.path,
152-
label: 'Transfer',
153-
onPressed: () {
154-
context.pushNamed(SwapRoute.swap.name);
155-
},
156-
position: _ButtonPosition.last,
157-
disabled: false,
158-
),
159-
],
116+
},
117+
position: _ButtonPosition.middle,
118+
disabled: false,
119+
),
120+
const Gap(1),
121+
_ActionButton(
122+
icon: Assets.icons.swap.path,
123+
label: 'Transfer',
124+
onPressed: () {
125+
context.pushNamed(SwapRoute.swap.name);
126+
},
127+
position: _ButtonPosition.last,
128+
disabled: false,
129+
),
130+
],
131+
),
160132
),
161133
),
162-
),
163134
);
164135
}
165136
}

lib/features/pay/pay_locator.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import 'package:bb_mobile/core/wallet/domain/usecases/get_wallet_utxos_usecase.d
1111
import 'package:bb_mobile/features/pay/domain/create_pay_order_usecase.dart';
1212
import 'package:bb_mobile/features/pay/domain/refresh_pay_order_usecase.dart';
1313
import 'package:bb_mobile/features/pay/presentation/pay_bloc.dart';
14-
import 'package:bb_mobile/features/recipients/interface_adapters/presenters/models/recipient_view_model.dart';
1514
import 'package:bb_mobile/features/send/domain/usecases/calculate_bitcoin_absolute_fees_usecase.dart';
1615
import 'package:bb_mobile/features/send/domain/usecases/calculate_liquid_absolute_fees_usecase.dart';
1716

@@ -54,9 +53,8 @@ class PayLocator {
5453
}
5554

5655
static void registerBlocs(GetIt locator) {
57-
locator.registerFactoryParam<PayBloc, RecipientViewModel, void>(
58-
(recipient, _) => PayBloc(
59-
recipient: recipient,
56+
locator.registerFactory<PayBloc>(
57+
() => PayBloc(
6058
getExchangeUserSummaryUsecase: locator<GetExchangeUserSummaryUsecase>(),
6159
placePayOrderUsecase: locator<PlacePayOrderUsecase>(),
6260
refreshPayOrderUsecase: locator<RefreshPayOrderUsecase>(),

lib/features/pay/presentation/pay_bloc.dart

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ part 'pay_state.dart';
3737

3838
class PayBloc extends Bloc<PayEvent, PayState> {
3939
PayBloc({
40-
required RecipientViewModel recipient,
4140
required GetExchangeUserSummaryUsecase getExchangeUserSummaryUsecase,
4241
required PlacePayOrderUsecase placePayOrderUsecase,
4342
required RefreshPayOrderUsecase refreshPayOrderUsecase,
@@ -76,8 +75,9 @@ class PayBloc extends Bloc<PayEvent, PayState> {
7675
_getAddressAtIndexUsecase = getAddressAtIndexUsecase,
7776
_getWalletUtxosUsecase = getWalletUtxosUsecase,
7877
_getOrderUsecase = getOrderUsecase,
79-
super(PayAmountInputState(selectedRecipient: recipient)) {
78+
super(PayRecipientSelectionState()) {
8079
on<PayStarted>(_onStarted);
80+
on<PayRecipientSelected>(_onRecipientSelected);
8181
on<PayAmountInputContinuePressed>(_onAmountInputContinuePressed);
8282
on<PayWalletSelected>(_onWalletSelected);
8383
on<PayExternalWalletNetworkSelected>(_onExternalWalletNetworkSelected);
@@ -111,31 +111,55 @@ class PayBloc extends Bloc<PayEvent, PayState> {
111111
Timer? _pollingTimer;
112112

113113
Future<void> _onStarted(PayStarted event, Emitter<PayState> emit) async {
114-
final amountInputState = state.cleanAmountInputState;
115-
emit(amountInputState!.copyWith(isLoadingUserSummary: true));
114+
final recipientSelectionState = state.cleanRecipientSelectionState;
115+
emit(recipientSelectionState!.copyWith(isLoadingUserSummary: true));
116116
try {
117117
final userSummary = await _getExchangeUserSummaryUsecase.execute();
118118

119-
emit(amountInputState.copyWith(userSummary: userSummary));
119+
emit(recipientSelectionState.copyWith(userSummary: userSummary));
120120
} on ApiKeyException catch (e) {
121121
// Handle API key error by showing error in current state
122122

123123
emit(
124-
(state as PayAmountInputState).copyWith(
124+
recipientSelectionState.copyWith(
125125
error: PayError.unexpected(message: e.message),
126126
),
127127
);
128128
} on GetExchangeUserSummaryException catch (e) {
129129
emit(
130-
(state as PayAmountInputState).copyWith(
130+
recipientSelectionState.copyWith(
131131
error: PayError.unexpected(message: e.message),
132132
),
133133
);
134134
} finally {
135-
emit(
136-
(state as PayAmountInputState).copyWith(isLoadingUserSummary: false),
137-
);
135+
if (state is PayRecipientSelectionState) {
136+
emit(
137+
(state as PayRecipientSelectionState).copyWith(
138+
isLoadingUserSummary: false,
139+
),
140+
);
141+
}
142+
}
143+
}
144+
145+
Future<void> _onRecipientSelected(
146+
PayRecipientSelected event,
147+
Emitter<PayState> emit,
148+
) async {
149+
final recipientSelectionState = state.cleanRecipientSelectionState;
150+
if (recipientSelectionState == null) {
151+
log.severe('Expected to be on PayRecipientSelectionState but on: $state');
152+
return;
138153
}
154+
155+
// First emit the recipient selection state again since we went back to it,
156+
// So that the change to amount input state can be listened to properly.
157+
emit(recipientSelectionState);
158+
159+
final amountInputState = recipientSelectionState.toAmountInputState(
160+
selectedRecipient: event.recipient,
161+
);
162+
emit(amountInputState);
139163
}
140164

141165
Future<void> _onAmountInputContinuePressed(
@@ -159,7 +183,10 @@ class PayBloc extends Bloc<PayEvent, PayState> {
159183

160184
final fiatAmount = FiatAmount(amount);
161185

162-
emit(amountInputState.toWalletSelectionState(amount: fiatAmount));
186+
final walletSelectionState = amountInputState.toWalletSelectionState(
187+
amount: fiatAmount,
188+
);
189+
emit(walletSelectionState);
163190
}
164191

165192
// From Sell: Select internal wallet, calculate fees, create pay order
@@ -172,6 +199,7 @@ class PayBloc extends Bloc<PayEvent, PayState> {
172199
log.severe('Expected to be on PayWalletSelectionState but on: $state');
173200
return;
174201
}
202+
175203
emit(walletSelectionState.copyWith(isCreatingPayOrder: true));
176204

177205
int requiredAmountSat;
@@ -306,6 +334,7 @@ class PayBloc extends Bloc<PayEvent, PayState> {
306334
log.severe('Expected to be on PayWalletSelectionState but on: $state');
307335
return;
308336
}
337+
309338
emit(walletSelectionState.copyWith(isCreatingPayOrder: true));
310339

311340
try {

0 commit comments

Comments
 (0)