-
Notifications
You must be signed in to change notification settings - Fork 77
Expand file tree
/
Copy pathpay_state.dart
More file actions
319 lines (292 loc) · 10.8 KB
/
Copy pathpay_state.dart
File metadata and controls
319 lines (292 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
part of 'pay_bloc.dart';
@freezed
sealed class PayState with _$PayState {
const factory PayState.recipientSelection({
UserSummary? userSummary,
@Default(false) bool isLoadingUserSummary,
PayError? error,
}) = PayRecipientSelectionState;
const factory PayState.amountInput({
required RecipientViewModel selectedRecipient,
required UserSummary userSummary,
PayError? error,
}) = PayAmountInputState;
const factory PayState.walletSelection({
required RecipientViewModel selectedRecipient,
required UserSummary userSummary,
required FiatAmount amount,
String? paymentDescription,
@Default(false) bool isCreatingPayOrder,
PayError? error,
}) = PayWalletSelectionState;
const factory PayState.payment({
required RecipientViewModel selectedRecipient,
required UserSummary userSummary,
required FiatAmount amount,
String? paymentDescription,
Wallet? selectedWallet,
required FiatPaymentOrder payOrder,
@Default(false) bool isConfirmingPayment,
@Default(false) bool isPolling,
PayError? error,
int? absoluteFees,
@Default([]) List<WalletUtxo> utxos,
@Default([]) List<WalletUtxo> selectedUtxos,
@Default(true) bool replaceByFee,
double? exchangeRateEstimate,
// Bitcoin fee selection (#2521), mirroring SellPaymentState: the payin is
// built at the rate picked in the shared fee modal, not a hardcoded
// Fastest.
FeeOptions? bitcoinFees,
NetworkFee? customFee,
@Default(FeeSelection.fastest) FeeSelection selectedFeeOption,
// Arm/disarm snapshot for the custom-fee field: typing commits `custom` so
// the preset tiles deselect, and dismissal either finalizes the value or
// rolls back to these. Internal to the modal flow; the UI must not read
// them.
FeeSelection? armPriorSelection,
NetworkFee? armPriorCustomFee,
// Real fees read from unsigned PSBTs, one slot per tier, so the modal never
// shows rate × vsize arithmetic. Display only: the confirmation rebuilds
// the payin at the committed rate rather than broadcasting a cached PSBT,
// because a price-lock refresh can move the payin amount.
@Default(BitcoinFeePreviewCache.empty)
BitcoinFeePreviewCache feePreviewCache,
// vsize of the last payin build — needed to express an absolute custom fee
// as a rate for the relay-floor checks.
int? bitcoinTxSize,
}) = PayPaymentState;
const factory PayState.success({required FiatPaymentOrder payOrder}) =
PaySuccessState;
const PayState._();
UserSummary? get userSummary => switch (this) {
PayRecipientSelectionState(:final userSummary) => userSummary,
PayAmountInputState(:final userSummary) => userSummary,
PayWalletSelectionState(:final userSummary) => userSummary,
PayPaymentState(:final userSummary) => userSummary,
PaySuccessState() => null,
};
bool get isFullyVerifiedKycLevel =>
userSummary?.isFullyVerifiedKycLevel == true;
bool get isLimitedKycLevel => userSummary?.isLimitedKycLevel == true;
bool get isLightKycLevel => userSummary?.isLightKycLevel == true;
bool isKycOk({FiatCurrency? currency}) {
final effectiveCurrency = currency ?? this.currency;
return userSummary?.isKycOk(effectiveCurrency) ?? false;
}
bool isAmountExceeded(double amount, {FiatCurrency? currency}) {
final effectiveCurrency = currency ?? this.currency;
return userSummary?.isAmountExceeded(amount, effectiveCurrency) ?? false;
}
bool needsKycUpgrade(double amount, {FiatCurrency? currency}) {
final effectiveCurrency = currency ?? this.currency;
return userSummary?.needsKycUpgrade(amount, effectiveCurrency) ?? true;
}
FiatCurrency get currency => switch (this) {
PayRecipientSelectionState(:final userSummary) => userSummary != null
? FiatCurrency.fromCode(userSummary.currency!)
: FiatCurrency.cad,
PayAmountInputState(:final selectedRecipient) => FiatCurrency.fromCode(
selectedRecipient.currencyCode,
),
PayWalletSelectionState(:final selectedRecipient) => FiatCurrency.fromCode(
selectedRecipient.currencyCode,
),
PayPaymentState(:final payOrder) => FiatCurrency.fromCode(
payOrder.payoutCurrency,
),
PaySuccessState(:final payOrder) => FiatCurrency.fromCode(
payOrder.payoutCurrency,
),
};
// Backward step: drop forward state, reset transients on the destination.
PayRecipientSelectionState? get cleanRecipientSelectionState => switch (this) {
final PayRecipientSelectionState s => s.copyWith(
isLoadingUserSummary: false,
error: null,
),
PayAmountInputState(:final userSummary) => PayRecipientSelectionState(
userSummary: userSummary,
),
PayWalletSelectionState(:final userSummary) => PayRecipientSelectionState(
userSummary: userSummary,
),
PayPaymentState(:final userSummary) => PayRecipientSelectionState(
userSummary: userSummary,
),
PaySuccessState() => null,
};
PayAmountInputState? get cleanAmountInputState => switch (this) {
final PayAmountInputState s => s.copyWith(error: null),
PayWalletSelectionState(:final selectedRecipient, :final userSummary) =>
PayAmountInputState(
selectedRecipient: selectedRecipient,
userSummary: userSummary,
),
PayPaymentState(:final selectedRecipient, :final userSummary) =>
PayAmountInputState(
selectedRecipient: selectedRecipient,
userSummary: userSummary,
),
_ => null,
};
PayWalletSelectionState? get cleanWalletSelectionState => switch (this) {
final PayWalletSelectionState s => s.copyWith(
isCreatingPayOrder: false,
error: null,
),
PayPaymentState(
:final selectedRecipient,
:final userSummary,
:final amount,
:final paymentDescription,
) =>
PayWalletSelectionState(
selectedRecipient: selectedRecipient,
userSummary: userSummary,
amount: amount,
paymentDescription: paymentDescription,
),
_ => null,
};
// Same-type reset: preserve all data fields, clear only transient UI flags.
// Using copyWith here makes new fields safe-by-default — adding a field to
// PayPaymentState can no longer silently drop it (was issue #2007).
PayPaymentState? get cleanPaymentState => switch (this) {
final PayPaymentState s => s.copyWith(
isConfirmingPayment: false,
isPolling: false,
error: null,
),
_ => null,
};
}
extension PayRecipientSelectionStateX on PayRecipientSelectionState {
PayAmountInputState toAmountInputState({
required RecipientViewModel selectedRecipient,
}) {
if (userSummary == null) {
throw StateError('Cannot create amount input state without user summary');
}
return PayAmountInputState(
selectedRecipient: selectedRecipient,
userSummary: userSummary!,
);
}
}
extension PayAmountInputStateX on PayAmountInputState {
PayWalletSelectionState toWalletSelectionState({
required FiatAmount amount,
String? paymentDescription,
}) {
return PayWalletSelectionState(
userSummary: userSummary,
amount: amount,
selectedRecipient: selectedRecipient,
paymentDescription: paymentDescription,
isCreatingPayOrder: false,
);
}
}
extension PayWalletSelectionStateX on PayWalletSelectionState {
PayPaymentState toSendPaymentState({
required Wallet selectedWallet,
required FiatPaymentOrder payOrder,
List<WalletUtxo>? utxos,
int? absoluteFees,
double? exchangeRateEstimate,
FeeOptions? bitcoinFees,
int? bitcoinTxSize,
}) {
return PayPaymentState(
userSummary: userSummary,
amount: amount,
selectedRecipient: selectedRecipient,
paymentDescription: paymentDescription,
selectedWallet: selectedWallet,
payOrder: payOrder,
absoluteFees: absoluteFees,
exchangeRateEstimate: exchangeRateEstimate,
utxos: utxos ?? [],
bitcoinFees: bitcoinFees,
bitcoinTxSize: bitcoinTxSize,
);
}
PayPaymentState toReceivePaymentState({
required FiatPaymentOrder payOrder,
double? exchangeRateEstimate,
}) {
return PayPaymentState(
userSummary: userSummary,
amount: amount,
selectedRecipient: selectedRecipient,
paymentDescription: paymentDescription,
selectedWallet: null,
payOrder: payOrder,
exchangeRateEstimate: exchangeRateEstimate,
);
}
}
extension PayPaymentStateX on PayPaymentState {
PaySuccessState toSuccessState({required FiatPaymentOrder payOrder}) {
return PaySuccessState(payOrder: payOrder);
}
bool get isInternalWallet => selectedWallet != null;
bool get isExternalWallet => selectedWallet == null;
bool get canConfirmPayment => isInternalWallet && selectedUtxos.isNotEmpty;
bool get isProcessing => isConfirmingPayment || isPolling;
/// Fee the payin must be built at, resolved from the committed selection.
/// Null while the presets have not loaded (or custom was selected with no
/// value) — the caller decides, rather than silently reverting to Fastest,
/// which is the bug #2521 describes.
NetworkFee? get selectedFee => switch (selectedFeeOption) {
FeeSelection.fastest => bitcoinFees?.fastest,
FeeSelection.economic => bitcoinFees?.economic,
FeeSelection.slow => bitcoinFees?.slow,
FeeSelection.custom => customFee,
};
/// Fee selection is only editable before the confirmation starts: the
/// transaction on its way to the network was built at the committed rate, so
/// rebuilding under it would pay a different fee than the one shown. Liquid
/// payins have no fee choice at all.
bool get canEditFees =>
!isConfirmingPayment &&
selectedWallet != null &&
!selectedWallet!.isLiquid;
String get bip21InvoiceData {
final order = payOrder;
String invoiceString = '';
switch (order.payinMethod) {
case OrderPaymentMethod.bitcoin:
if (order.bitcoinAddress != null) {
final amountBtc = order.payinAmount;
final address = order.bitcoinAddress!;
final bip21Uri = Bip21Uri(
scheme: 'bitcoin',
address: address,
amount: amountBtc,
);
invoiceString = bip21Uri.toString();
}
case OrderPaymentMethod.liquid:
if (order.liquidAddress != null) {
final amountBtc = order.payinAmount;
final address = order.liquidAddress!;
final bip21Uri = Bip21Uri(
scheme: 'liquidnetwork',
address: address,
amount: amountBtc,
options: {'assetid': AssetConstants.lbtcMainnet},
);
invoiceString = bip21Uri.toString();
}
case OrderPaymentMethod.lnInvoice:
if (order.lightningInvoice != null) {
invoiceString = order.lightningInvoice!;
}
default:
break;
}
return invoiceString;
}
}