CW-1409-Update-Cakepay-cards-to-use-all-variants (#3112)
* add prepaid range support for CakePay cards * use selected denomination for order creation * fix Flexible widget issue * forces to use prod api * auto-reformat * fix the input string format [skip ci] Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * fix the value string format [skip ci] Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * fix the nextamount string format Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Serhii committed
Jul 15, 2026 at 15:57 UTC
6fc27dd3a0fe94cf5b92368b27a0e159aa679134
7 files changed
+366
-66
lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
+33
-18
@@ -8,6 +8,7 @@ import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dar
8
import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
9
import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart';
10
import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
11
+import 'package:cake_wallet/cake_pay/src/widgets/prepaid_range_amount_widget.dart';
12
import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
13
import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
14
import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
@@ -147,24 +148,38 @@ class CakePayBuyCardPage extends BasePage {
148
),
149
bottomCardChild: Padding(
150
padding: const EdgeInsets.symmetric(horizontal: 24),
150
- child: card.denominationItems.isNotEmpty
151
- ? DenominationsAmountWidget(
152
- fiatCurrency: card.fiatCurrency.title,
153
- denominations: card.denominationItems,
154
- amountFieldFocus: _amountFieldFocus,
155
- amountController: _amountController,
156
- quantityFieldFocus: _quantityFieldFocus,
157
- quantityController: _quantityController,
158
- onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
159
- onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
160
- cakePayBuyCardViewModel: cakePayBuyCardViewModel)
161
- : EnterAmountWidget(
162
- minValue: card.minValue ?? '-',
163
- maxValue: card.maxValue ?? '-',
164
- fiatCurrency: card.fiatCurrency.title,
165
- amountFieldFocus: _amountFieldFocus,
166
- amountController: _amountController,
167
- onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))),
151
+ child: Column(children: [
152
+ if (card.prepaidRange.isNotEmpty)
153
+ PrepaidRangeAmountWidget(
154
+ fiatCurrency: card.fiatCurrency.title,
155
+ prepaidRanges: card.prepaidRange,
156
+ amountFieldFocus: _amountFieldFocus,
157
+ amountController: _amountController,
158
+ cakePayBuyCardViewModel: cakePayBuyCardViewModel,
159
+ onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
160
+ )
161
+ else if (card.denominationItems.isNotEmpty)
162
+ DenominationsAmountWidget(
163
+ fiatCurrency: card.fiatCurrency.title,
164
+ denominations: card.denominationItems,
165
+ amountFieldFocus: _amountFieldFocus,
166
+ amountController: _amountController,
167
+ quantityFieldFocus: _quantityFieldFocus,
168
+ quantityController: _quantityController,
169
+ onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
170
+ onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
171
+ cakePayBuyCardViewModel: cakePayBuyCardViewModel,
172
+ )
173
+ else
174
+ EnterAmountWidget(
175
+ minValue: card.minValue ?? '-',
176
+ maxValue: card.maxValue ?? '-',
177
+ fiatCurrency: card.fiatCurrency.title,
178
+ amountFieldFocus: _amountFieldFocus,
179
+ amountController: _amountController,
180
+ onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
181
+ ),
182
+ ]))),
183
Expanded(
184
flex: 2,
185
child: Padding(
lib/cake_pay/src/models/cake_pay_card.dart
+108
-21
@@ -1,8 +1,21 @@
1
+import 'package:cake_wallet/core/utilities.dart';
2
import 'package:cake_wallet/entities/fiat_currency.dart';
3
4
+enum CakePayCardType {
5
+ prepaid('Prepaid Cards'),
6
+ gift('gift'),
7
+ onDemand('on_demand'),
8
+ custom('custom');
9
+
10
+ const CakePayCardType(this.apiValue);
11
+
12
+ final String apiValue;
13
+}
14
+
15
class CakePayCard {
16
final int id;
17
final String name;
18
+ final CakePayCardType? type;
19
final String? description;
20
final String? termsAndConditions;
21
final String? howToUse;
@@ -15,26 +28,77 @@ class CakePayCard {
28
final String? minValue;
29
final String? maxValue;
30
final List<Denomination> denominationItems;
31
+ final List<PrepaidRange> prepaidRange;
32
19
- CakePayCard({
20
- required this.id,
21
- required this.name,
22
- this.description,
23
- this.termsAndConditions,
24
- this.howToUse,
25
- this.expiryAndValidity,
26
- this.cardImageUrl,
27
- this.country,
28
- required this.fiatCurrency,
29
- this.minValueUsd,
30
- this.maxValueUsd,
31
- this.minValue,
32
- this.maxValue,
33
+ CakePayCard(
34
+ {required this.id,
35
+ required this.name,
36
+ this.type,
37
+ this.description,
38
+ this.termsAndConditions,
39
+ this.howToUse,
40
+ this.expiryAndValidity,
41
+ this.cardImageUrl,
42
+ this.country,
43
+ required this.fiatCurrency,
44
+ this.minValueUsd,
45
+ this.maxValueUsd,
46
+ this.minValue,
47
+ this.maxValue,
48
+ List<Denomination>? denominationItems,
49
+ List<PrepaidRange>? prepaidRange})
50
+ : denominationItems = denominationItems ?? const [],
51
+ prepaidRange = prepaidRange ?? const [];
52
+
53
+ CakePayCard copyWith({
54
+ int? id,
55
+ String? name,
56
+ CakePayCardType? type,
57
+ String? description,
58
+ String? termsAndConditions,
59
+ String? howToUse,
60
+ String? expiryAndValidity,
61
+ String? cardImageUrl,
62
+ String? country,
63
+ FiatCurrency? fiatCurrency,
64
+ String? minValueUsd,
65
+ String? maxValueUsd,
66
+ String? minValue,
67
+ String? maxValue,
68
List<Denomination>? denominationItems,
34
- }) : denominationItems = denominationItems ?? const [];
69
+ List<PrepaidRange>? prepaidRange,
70
+ }) {
71
+ return CakePayCard(
72
+ id: id ?? this.id,
73
+ name: name ?? this.name,
74
+ type: type ?? this.type,
75
+ description: description ?? this.description,
76
+ termsAndConditions: termsAndConditions ?? this.termsAndConditions,
77
+ howToUse: howToUse ?? this.howToUse,
78
+ expiryAndValidity: expiryAndValidity ?? this.expiryAndValidity,
79
+ cardImageUrl: cardImageUrl ?? this.cardImageUrl,
80
+ country: country ?? this.country,
81
+ fiatCurrency: fiatCurrency ?? this.fiatCurrency,
82
+ minValueUsd: minValueUsd ?? this.minValueUsd,
83
+ maxValueUsd: maxValueUsd ?? this.maxValueUsd,
84
+ minValue: minValue ?? this.minValue,
85
+ maxValue: maxValue ?? this.maxValue,
86
+ denominationItems: denominationItems ?? this.denominationItems,
87
+ prepaidRange: prepaidRange ?? this.prepaidRange,
88
+ );
89
+ }
90
+
91
+ static CakePayCardType? cakePayCardTypeFromApi(String? value) {
92
+ if (value == null) return null;
93
+ final normalized = value.trim().toLowerCase();
94
+ return CakePayCardType.values.firstWhereOrNull(
95
+ (e) => e.apiValue.toLowerCase() == normalized,
96
+ );
97
+ }
98
99
factory CakePayCard.fromJson(Map<String, dynamic> json) {
100
final name = stripHtmlIfNeeded(json['name'] as String? ?? '');
101
+ final typeString = json['type'] as String? ?? '';
102
final description = stripHtmlIfNeeded(json['description'] as String? ?? '');
103
final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
104
final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
@@ -63,9 +127,12 @@ class CakePayCard {
127
}
128
}
129
130
+ final cakePayCardType = cakePayCardTypeFromApi(typeString);
131
+
132
return CakePayCard(
133
id: json['id'] as int? ?? 0,
134
name: name,
135
+ type: cakePayCardType,
136
description: description,
137
termsAndConditions: termsAndConditions,
138
howToUse: howToUse,
@@ -103,12 +170,6 @@ class Denomination {
170
this.usdValue,
171
});
172
106
- static double? _toDouble(dynamic v) {
107
- if (v == null) return null;
108
- if (v is num) return v.toDouble();
109
- return double.tryParse(v.toString());
110
- }
111
-
173
factory Denomination.fromJson(Map<String, dynamic> json) {
174
return Denomination(
175
value: _toDouble(json['value']) ?? 0,
@@ -117,3 +178,29 @@ class Denomination {
178
);
179
}
180
}
181
+
182
+class PrepaidRange {
183
+ final double minValue;
184
+ final double maxValue;
185
+ final String rangeId;
186
+
187
+ PrepaidRange({
188
+ required this.minValue,
189
+ required this.maxValue,
190
+ required this.rangeId,
191
+ });
192
+
193
+ factory PrepaidRange.fromCard(CakePayCard card) {
194
+ return PrepaidRange(
195
+ minValue: _toDouble(card.minValue) ?? 0,
196
+ maxValue: _toDouble(card.maxValue) ?? 0,
197
+ rangeId: card.id.toString(),
198
+ );
199
+ }
200
+}
201
+
202
+double? _toDouble(dynamic v) {
203
+ if (v == null) return null;
204
+ if (v is num) return v.toDouble();
205
+ return double.tryParse(v.toString());
206
+}
lib/cake_pay/src/models/cake_pay_vendor.dart
+20
-3
@@ -31,9 +31,26 @@ class CakePayVendor {
31
32
if (cardsJson != null && cardsJson.isNotEmpty) {
33
try {
34
- cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) {
35
- return Country.normalizeName(card['country'] as String) == country;
36
- }) as Map<String, dynamic>);
34
+ final cards = cardsJson
35
+ .map((cardJson) => CakePayCard.fromJson(cardJson as Map<String, dynamic>))
36
+ .toList();
37
+ final cardsForSelectedCountry =
38
+ cards.where((card) => Country.normalizeName(card.country ?? '') == country).toList();
39
+
40
+ if (cardsForSelectedCountry.isNotEmpty) {
41
+ final isPrepaid =
42
+ cardsForSelectedCountry.every((card) => card.type == CakePayCardType.prepaid);
43
+
44
+ if (isPrepaid) {
45
+ final firstCard = cardsForSelectedCountry.first;
46
+ final prepaidRanges = cardsForSelectedCountry.map(PrepaidRange.fromCard).toList()
47
+ ..sort((a, b) => a.minValue.compareTo(b.minValue));
48
+
49
+ cardForVendor = firstCard.copyWith(prepaidRange: prepaidRanges);
50
+ } else {
51
+ cardForVendor = cardsForSelectedCountry.first;
52
+ }
53
+ }
54
} catch (e) {
55
printV('Error parsing card for vendor: $e');
56
}
lib/cake_pay/src/services/cake_pay_api.dart
+1
-1
@@ -8,7 +8,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
8
import 'package:cw_core/utils/print_verbose.dart';
9
10
class CakePayApi {
11
- static const testBaseUri = FeatureFlag.hasDevOptions;
11
+ static const testBaseUri = false; //FeatureFlag.hasDevOptions;
12
13
static const baseTestCakePayUri = 'api-stg.cakepay.com';
14
static const baseProdCakePayUri = 'api-prod.cakepay.com';
lib/cake_pay/src/widgets/prepaid_range_amount_widget.dart
new
+134
@@ -0,0 +1,134 @@
1
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
2
+import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
3
+import 'package:flutter/material.dart';
4
+
5
+import 'enter_amount_widget.dart';
6
+
7
+class PrepaidRangeAmountWidget extends StatefulWidget {
8
+ const PrepaidRangeAmountWidget({
9
+ required this.fiatCurrency,
10
+ required this.prepaidRanges,
11
+ required this.amountFieldFocus,
12
+ required this.amountController,
13
+ required this.cakePayBuyCardViewModel,
14
+ required this.onAmountChanged,
15
+ });
16
+
17
+ final String fiatCurrency;
18
+ final List<PrepaidRange> prepaidRanges;
19
+ final FocusNode amountFieldFocus;
20
+ final TextEditingController amountController;
21
+ final CakePayBuyCardViewModel cakePayBuyCardViewModel;
22
+ final Function(String) onAmountChanged;
23
+
24
+ @override
25
+ State<PrepaidRangeAmountWidget> createState() => _PrepaidRangeAmountWidgetState();
26
+}
27
+
28
+class _PrepaidRangeAmountWidgetState extends State<PrepaidRangeAmountWidget> {
29
+ late PrepaidRange _selectedRange;
30
+
31
+ @override
32
+ void initState() {
33
+ super.initState();
34
+ _selectedRange = widget.prepaidRanges.first;
35
+ _applySelectedRange(forceAmountUpdate: widget.amountController.text.isEmpty);
36
+ }
37
+
38
+ @override
39
+ Widget build(BuildContext context) {
40
+ return Column(
41
+ crossAxisAlignment: CrossAxisAlignment.start,
42
+ children: [
43
+ Text(
44
+ 'Select range:',
45
+ style: TextStyle(
46
+ color: Theme.of(context).textTheme.titleLarge!.color!,
47
+ fontSize: 16,
48
+ fontWeight: FontWeight.w600,
49
+ ),
50
+ ),
51
+ const SizedBox(height: 8),
52
+ DropdownButtonFormField<PrepaidRange>(
53
+ value: _selectedRange,
54
+ isExpanded: true,
55
+ decoration: InputDecoration(
56
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
57
+ filled: true,
58
+ fillColor: Theme.of(context).cardColor,
59
+ border: OutlineInputBorder(
60
+ borderRadius: BorderRadius.circular(10),
61
+ ),
62
+ enabledBorder: OutlineInputBorder(
63
+ borderRadius: BorderRadius.circular(10),
64
+ borderSide: BorderSide(
65
+ color: Theme.of(context).colorScheme.onSurface.withAlpha(40),
66
+ ),
67
+ ),
68
+ focusedBorder: OutlineInputBorder(
69
+ borderRadius: BorderRadius.circular(10),
70
+ borderSide: BorderSide(
71
+ color: Theme.of(context).colorScheme.primary,
72
+ ),
73
+ ),
74
+ ),
75
+ items: widget.prepaidRanges
76
+ .map(
77
+ (range) => DropdownMenuItem<PrepaidRange>(
78
+ value: range,
79
+ child: Text(_rangeLabel(range)),
80
+ ),
81
+ )
82
+ .toList(),
83
+ onChanged: (range) {
84
+ if (range == null) return;
85
+ setState(() => _selectedRange = range);
86
+ widget.cakePayBuyCardViewModel.onPrepaidRangeChanged(range);
87
+ _applySelectedRange(forceAmountUpdate: true);
88
+ },
89
+ ),
90
+ const SizedBox(height: 12),
91
+ EnterAmountWidget(
92
+ minValue: _formatAmount(_selectedRange.minValue),
93
+ maxValue: _formatAmount(_selectedRange.maxValue),
94
+ fiatCurrency: widget.fiatCurrency,
95
+ amountFieldFocus: widget.amountFieldFocus,
96
+ amountController: widget.amountController,
97
+ onAmountChanged: (value) {
98
+ widget.onAmountChanged(value);
99
+ widget.cakePayBuyCardViewModel.selectedDenomination = (
100
+ value.replaceAll(',', '.'),
101
+ int.tryParse(_selectedRange.rangeId),
102
+ );
103
+ },
104
+ ),
105
+ ],
106
+ );
107
+ }
108
+
109
+ void _applySelectedRange({required bool forceAmountUpdate}) {
110
+ final currentAmount = double.tryParse(widget.amountController.text);
111
+ final isCurrentAmountInRange = currentAmount != null &&
112
+ currentAmount >= _selectedRange.minValue &&
113
+ currentAmount <= _selectedRange.maxValue;
114
+
115
+ final nextAmount = forceAmountUpdate || !isCurrentAmountInRange
116
+ ? _formatAmount(_selectedRange.minValue)
117
+ : widget.amountController.text;
118
+
119
+ widget.amountController.text = nextAmount;
120
+ widget.onAmountChanged(nextAmount);
121
+ widget.cakePayBuyCardViewModel.selectedDenomination = (
122
+ nextAmount.replaceAll(',', '.'),
123
+ int.tryParse(_selectedRange.rangeId),
124
+ );
125
+ }
126
+
127
+ String _rangeLabel(PrepaidRange range) {
128
+ return '${widget.fiatCurrency} ${_formatAmount(range.minValue)} - ${_formatAmount(range.maxValue)}';
129
+ }
130
+
131
+ String _formatAmount(double value) {
132
+ return value % 1 == 0 ? value.toInt().toString() : value.toStringAsFixed(2);
133
+ }
134
+}
lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart
+31
-17
@@ -12,32 +12,46 @@ class RoundedOverlayCards extends StatelessWidget {
12
@override
13
Widget build(BuildContext context) {
14
final screenHeight = MediaQuery.of(context).size.height;
15
+
16
return ClipRRect(
17
borderRadius:
18
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
19
child: Container(
19
- height: screenHeight * 0.50,
20
decoration: BoxDecoration(
21
- borderRadius: BorderRadius.only(
22
- bottomLeft: Radius.circular(24),
23
- bottomRight: Radius.circular(24),
24
- ),
25
- color: Theme.of(context).colorScheme.surfaceContainer),
21
+ borderRadius: const BorderRadius.only(
22
+ bottomLeft: Radius.circular(24),
23
+ bottomRight: Radius.circular(24),
24
+ ),
25
+ color: Theme.of(context).colorScheme.surfaceContainer,
26
+ ),
27
child: Column(
28
+ mainAxisSize: MainAxisSize.min,
29
children: [
28
- ClipRRect(
29
- borderRadius: BorderRadius.only(
30
- bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
30
+ Flexible(
31
+ child: ClipRRect(
32
+ borderRadius: const BorderRadius.only(
33
+ bottomLeft: Radius.circular(25.0),
34
+ bottomRight: Radius.circular(25.0),
35
+ ),
36
child: Container(
32
- decoration: BoxDecoration(
33
- borderRadius: BorderRadius.only(
34
- bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
35
- color: Theme.of(context).colorScheme.surfaceContainerLow,
37
+ decoration: BoxDecoration(
38
+ borderRadius: const BorderRadius.only(
39
+ bottomLeft: Radius.circular(24),
40
+ bottomRight: Radius.circular(24),
41
),
37
- height: screenHeight * 0.38,
38
- width: double.infinity,
39
- child: topCardChild)),
40
- bottomCardChild,
42
+ color: Theme.of(context).colorScheme.surfaceContainerLow,
43
+ ),
44
+ constraints: BoxConstraints(
45
+ maxHeight: screenHeight * 0.38,
46
+ ),
47
+ width: double.infinity,
48
+ child: topCardChild,
49
+ ),
50
+ ),
51
+ ),
52
+ Flexible(
53
+ child: bottomCardChild,
54
+ ),
55
],
56
),
57
),
lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart
+39
-6
@@ -33,8 +33,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
33
: 0,
34
quantity = 1,
35
card = vendor.card!,
36
- min = _toDouble(vendor.card!.minValue) ?? 0,
37
- max = _toDouble(vendor.card!.maxValue) ?? 0 {
36
+ min = vendor.card!.prepaidRange.isNotEmpty
37
+ ? vendor.card!.prepaidRange.first.minValue
38
+ : _toDouble(vendor.card!.minValue) ?? 0,
39
+ max = vendor.card!.prepaidRange.isNotEmpty
40
+ ? vendor.card!.prepaidRange.first.maxValue
41
+ : _toDouble(vendor.card!.maxValue) ?? 0 {
42
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
43
}
44
@@ -46,8 +50,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
50
final CakePayVendor vendor;
51
final SendViewModel sendViewModel;
52
final CakePayService _cakePayService;
49
- final double max;
50
- final double min;
53
final CakePayCard card;
54
final WalletType walletType;
55
final Box<Order> orders;
@@ -59,6 +61,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
61
bool confirmsNoVpn = false;
62
bool confirmsVoidedRefund = false;
63
bool confirmsTermsAgreed = false;
64
+ double max;
65
+ double min;
66
(String, int?) selectedDenomination = ('', null);
67
68
String simulatedResponse = '';
@@ -136,10 +140,24 @@ abstract class CakePayBuyCardViewModelBase with Store {
140
@action
141
void onQuantityChanged(int? input) => quantity = input ?? 1;
142
143
+ void onPrepaidRangeChanged(PrepaidRange range) {
144
+ min = range.minValue;
145
+ max = range.maxValue;
146
+ selectedDenomination = (range.minValue.toString(), int.tryParse(range.rangeId));
147
+
148
+ if (amount < min || amount > max) {
149
+ amount = min;
150
+ }
151
+ }
152
+
153
@action
154
void onAmountChanged(String input) {
155
if (input.isEmpty) return;
156
amount = double.parse(input.replaceAll(',', '.'));
157
+
158
+ if (card.prepaidRange.isNotEmpty) {
159
+ selectedDenomination = (input.replaceAll(',', '.'), selectedDenomination.$2);
160
+ }
161
}
162
163
@action
@@ -153,10 +171,25 @@ abstract class CakePayBuyCardViewModelBase with Store {
171
sendViewModel.state =
172
FailureState('Unsupported wallet type, please use Bitcoin, Monero, Litecoin or Zcash.');
173
}
174
+
175
+ final isPrepaidRangeSelected = card.prepaidRange.isNotEmpty;
176
+
177
+ int selectedCardId = card.id;
178
+ String selectedPrice = amount.toString();
179
+
180
+ if (isPrepaidRangeSelected) {
181
+ selectedCardId = selectedDenomination.$2 ?? card.id;
182
+ selectedPrice =
183
+ selectedDenomination.$1.isNotEmpty ? selectedDenomination.$1 : amount.toString();
184
+ } else if (isDenominationSelected) {
185
+ selectedCardId = selectedDenomination.$2 ?? card.id;
186
+ selectedPrice = selectedDenomination.$1;
187
+ }
188
+
189
try {
190
order = await _cakePayService.createOrder(
158
- cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id,
159
- price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(),
191
+ cardId: selectedCardId,
192
+ price: selectedPrice,
193
quantity: quantity,
194
confirmsNoVpn: confirmsNoVpn,
195
confirmsVoidedRefund: confirmsVoidedRefund,