mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'cake-tech:dev' into dev
This commit is contained in:
@@ -634,10 +634,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1100,10 +1100,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
torch_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1322,5 +1322,5 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
|
||||
class ExchangeRate {
|
||||
/// The currency being priced (e.g. BTC in a BTC/USD pair).
|
||||
final Currency base;
|
||||
|
||||
/// The price of one whole unit of [base], in the quote currency
|
||||
/// (e.g. 45000 USD in a BTC/USD pair).
|
||||
final Money quote;
|
||||
|
||||
const ExchangeRate({required this.base, required this.quote});
|
||||
|
||||
/// Converts [amount] between the base and quote currencies.
|
||||
///
|
||||
/// Results are truncated toward zero.
|
||||
///
|
||||
/// Throws an [ArgumentError] if [amount]'s currency is not part of
|
||||
/// this pair.
|
||||
Money convert(Money amount) {
|
||||
if (base == quote.currency && base == amount.currency) return amount;
|
||||
|
||||
final scale = BigInt.from(10).pow(base.decimals);
|
||||
|
||||
if (amount.currency == base) {
|
||||
if (quote.isZero) return Money.zero(quote.currency);
|
||||
|
||||
return Money(amount.amount * quote.amount ~/ scale, quote.currency);
|
||||
}
|
||||
|
||||
if (amount.currency == quote.currency) {
|
||||
if (quote.isZero) return Money.zero(base);
|
||||
|
||||
return Money(amount.amount * scale ~/ quote.amount, base);
|
||||
}
|
||||
|
||||
throw ArgumentError(
|
||||
"Unable to convert ${amount.currency.symbol} in ${base.symbol}/${quote.currency.symbol} pair");
|
||||
}
|
||||
}
|
||||
@@ -394,10 +394,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -738,10 +738,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
torch_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -838,5 +838,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import 'package:cw_core/amount/exchange_rate.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('ExchangeRate', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money.parse("60000", EUR),
|
||||
);
|
||||
|
||||
test('convert base currency to quote currency: 1 BTC to 60.000 EUR', () {
|
||||
final result = rate.convert(Money.parse('1', CryptoCurrency.btc));
|
||||
expect(result, Money.parse("60000.00", EUR));
|
||||
});
|
||||
|
||||
test('convert base currency to quote currency: 0.5 BTC to 30.000 EUR', () {
|
||||
final result = rate.convert(Money.parse('0.5', CryptoCurrency.btc));
|
||||
expect(result, Money.parse("30000.00", EUR));
|
||||
});
|
||||
|
||||
test('convert quote currency to base currency: 60.000 EUR to 1 BTC', () {
|
||||
final result = rate.convert(Money.parse("60000.00", EUR));
|
||||
expect(result, Money.parse('1', CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('convert quote currency to base currency: 30 EUR to 0.0005 BTC', () {
|
||||
final result = rate.convert(Money.parse("30.00", EUR));
|
||||
expect(result, Money.parse('0.0005', CryptoCurrency.btc));
|
||||
});
|
||||
});
|
||||
|
||||
group('Edge Cases', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money.parse("60000", EUR),
|
||||
);
|
||||
|
||||
test('Converting zero turns into zero', () {
|
||||
expect(rate.convert(Money.zero(CryptoCurrency.btc)), Money.zero(EUR));
|
||||
expect(rate.convert(Money.zero(EUR)), Money.zero(CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('truncate to smallest unit', () {
|
||||
final result = rate.convert(Money(BigInt.one, CryptoCurrency.btc)); // 1 sat
|
||||
expect(result, Money.zero(EUR));
|
||||
});
|
||||
|
||||
test('Truncate base unit of base currency', () {
|
||||
final result = rate.convert(Money(BigInt.one, EUR));
|
||||
expect(result, Money.fromInt(16, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('No int overflow with large numbers', () {
|
||||
final allBtc = Money.parse('21000000', CryptoCurrency.btc);
|
||||
final result = rate.convert(allBtc);
|
||||
expect(result, Money(BigInt.from(126000000000000), EUR));
|
||||
});
|
||||
|
||||
test('truncation for negative amounts', () {
|
||||
final result = rate.convert(Money.fromInt(-1, EUR));
|
||||
expect(result, Money.fromInt(-16, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('Base currency without decimals places: JPY', () {
|
||||
final rate = ExchangeRate(
|
||||
base: JPY,
|
||||
quote: Money.fromInt(1, EUR),
|
||||
);
|
||||
|
||||
expect(rate.convert(Money.fromInt(500, JPY)), Money.fromInt(500, EUR));
|
||||
expect(rate.convert(Money.fromInt(500, EUR)), Money.fromInt(500, JPY));
|
||||
});
|
||||
|
||||
test('Base currency with 18 decimals places: ETH', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.eth,
|
||||
quote: Money.fromInt(300000, EUR),
|
||||
);
|
||||
|
||||
expect(rate.convert(Money.parse('1', CryptoCurrency.eth)), Money.fromInt(300000, EUR));
|
||||
expect(rate.convert(Money.fromInt(3000, EUR)), Money.parse('0.01', CryptoCurrency.eth));
|
||||
});
|
||||
|
||||
test('Smalles base unit is less than smalles base unit of quote', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.eth,
|
||||
quote: Money.fromInt(300000, EUR),
|
||||
);
|
||||
expect(rate.convert(Money(BigInt.one, CryptoCurrency.eth)), Money(BigInt.zero, EUR));
|
||||
});
|
||||
|
||||
test('Cannot convert currency outside of pair', () {
|
||||
expect(() => rate.convert(Money.parse("60000", USD)), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('handle quote being 0 correctly', () {
|
||||
final zeroRate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money(BigInt.zero, EUR),
|
||||
);
|
||||
expect(zeroRate.convert(Money.fromInt(100, CryptoCurrency.btc)), Money(BigInt.zero, EUR));
|
||||
expect(zeroRate.convert(Money.fromInt(100, EUR)), Money(BigInt.zero, CryptoCurrency.btc));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class FiatCurrency implements Currency {
|
||||
const FiatCurrency({
|
||||
required this.symbol,
|
||||
required this.countryCode,
|
||||
required this.fullName,
|
||||
this.decimals = 2,
|
||||
});
|
||||
|
||||
final String countryCode;
|
||||
|
||||
@override
|
||||
final String fullName;
|
||||
|
||||
@override
|
||||
final int decimals;
|
||||
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
@override
|
||||
String? get iconPath => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get name => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String? get tag => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Money parseAmount(String value) => Money.parse(value, this);
|
||||
|
||||
@override
|
||||
Money? tryParseAmount(String value) => Money.tryParse(value, this);
|
||||
}
|
||||
|
||||
const EUR = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro");
|
||||
const USD = FiatCurrency(symbol: 'USD', countryCode: "usd", fullName: "US Dollar");
|
||||
const JPY = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen", decimals: 0);
|
||||
@@ -417,10 +417,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -769,10 +769,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
torch_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -505,10 +505,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -906,10 +906,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
torch_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1048,5 +1048,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
||||
@@ -462,10 +462,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -887,10 +887,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
torch_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -417,10 +417,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -786,10 +786,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
torch_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -422,10 +422,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -783,10 +783,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
torch_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dar
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/prepaid_range_amount_widget.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
|
||||
@@ -147,24 +148,38 @@ class CakePayBuyCardPage extends BasePage {
|
||||
),
|
||||
bottomCardChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: card.denominationItems.isNotEmpty
|
||||
? DenominationsAmountWidget(
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
denominations: card.denominationItems,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
quantityFieldFocus: _quantityFieldFocus,
|
||||
quantityController: _quantityController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
|
||||
cakePayBuyCardViewModel: cakePayBuyCardViewModel)
|
||||
: EnterAmountWidget(
|
||||
minValue: card.minValue ?? '-',
|
||||
maxValue: card.maxValue ?? '-',
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))),
|
||||
child: Column(children: [
|
||||
if (card.prepaidRange.isNotEmpty)
|
||||
PrepaidRangeAmountWidget(
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
prepaidRanges: card.prepaidRange,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
)
|
||||
else if (card.denominationItems.isNotEmpty)
|
||||
DenominationsAmountWidget(
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
denominations: card.denominationItems,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
quantityFieldFocus: _quantityFieldFocus,
|
||||
quantityController: _quantityController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
|
||||
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
|
||||
)
|
||||
else
|
||||
EnterAmountWidget(
|
||||
minValue: card.minValue ?? '-',
|
||||
maxValue: card.maxValue ?? '-',
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
),
|
||||
]))),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import 'package:cake_wallet/core/utilities.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
|
||||
enum CakePayCardType {
|
||||
prepaid('Prepaid Cards'),
|
||||
gift('gift'),
|
||||
onDemand('on_demand'),
|
||||
custom('custom');
|
||||
|
||||
const CakePayCardType(this.apiValue);
|
||||
|
||||
final String apiValue;
|
||||
}
|
||||
|
||||
class CakePayCard {
|
||||
final int id;
|
||||
final String name;
|
||||
final CakePayCardType? type;
|
||||
final String? description;
|
||||
final String? termsAndConditions;
|
||||
final String? howToUse;
|
||||
@@ -15,26 +28,77 @@ class CakePayCard {
|
||||
final String? minValue;
|
||||
final String? maxValue;
|
||||
final List<Denomination> denominationItems;
|
||||
final List<PrepaidRange> prepaidRange;
|
||||
|
||||
CakePayCard({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.termsAndConditions,
|
||||
this.howToUse,
|
||||
this.expiryAndValidity,
|
||||
this.cardImageUrl,
|
||||
this.country,
|
||||
required this.fiatCurrency,
|
||||
this.minValueUsd,
|
||||
this.maxValueUsd,
|
||||
this.minValue,
|
||||
this.maxValue,
|
||||
CakePayCard(
|
||||
{required this.id,
|
||||
required this.name,
|
||||
this.type,
|
||||
this.description,
|
||||
this.termsAndConditions,
|
||||
this.howToUse,
|
||||
this.expiryAndValidity,
|
||||
this.cardImageUrl,
|
||||
this.country,
|
||||
required this.fiatCurrency,
|
||||
this.minValueUsd,
|
||||
this.maxValueUsd,
|
||||
this.minValue,
|
||||
this.maxValue,
|
||||
List<Denomination>? denominationItems,
|
||||
List<PrepaidRange>? prepaidRange})
|
||||
: denominationItems = denominationItems ?? const [],
|
||||
prepaidRange = prepaidRange ?? const [];
|
||||
|
||||
CakePayCard copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
CakePayCardType? type,
|
||||
String? description,
|
||||
String? termsAndConditions,
|
||||
String? howToUse,
|
||||
String? expiryAndValidity,
|
||||
String? cardImageUrl,
|
||||
String? country,
|
||||
FiatCurrency? fiatCurrency,
|
||||
String? minValueUsd,
|
||||
String? maxValueUsd,
|
||||
String? minValue,
|
||||
String? maxValue,
|
||||
List<Denomination>? denominationItems,
|
||||
}) : denominationItems = denominationItems ?? const [];
|
||||
List<PrepaidRange>? prepaidRange,
|
||||
}) {
|
||||
return CakePayCard(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
description: description ?? this.description,
|
||||
termsAndConditions: termsAndConditions ?? this.termsAndConditions,
|
||||
howToUse: howToUse ?? this.howToUse,
|
||||
expiryAndValidity: expiryAndValidity ?? this.expiryAndValidity,
|
||||
cardImageUrl: cardImageUrl ?? this.cardImageUrl,
|
||||
country: country ?? this.country,
|
||||
fiatCurrency: fiatCurrency ?? this.fiatCurrency,
|
||||
minValueUsd: minValueUsd ?? this.minValueUsd,
|
||||
maxValueUsd: maxValueUsd ?? this.maxValueUsd,
|
||||
minValue: minValue ?? this.minValue,
|
||||
maxValue: maxValue ?? this.maxValue,
|
||||
denominationItems: denominationItems ?? this.denominationItems,
|
||||
prepaidRange: prepaidRange ?? this.prepaidRange,
|
||||
);
|
||||
}
|
||||
|
||||
static CakePayCardType? cakePayCardTypeFromApi(String? value) {
|
||||
if (value == null) return null;
|
||||
final normalized = value.trim().toLowerCase();
|
||||
return CakePayCardType.values.firstWhereOrNull(
|
||||
(e) => e.apiValue.toLowerCase() == normalized,
|
||||
);
|
||||
}
|
||||
|
||||
factory CakePayCard.fromJson(Map<String, dynamic> json) {
|
||||
final name = stripHtmlIfNeeded(json['name'] as String? ?? '');
|
||||
final typeString = json['type'] as String? ?? '';
|
||||
final description = stripHtmlIfNeeded(json['description'] as String? ?? '');
|
||||
final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
|
||||
final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
|
||||
@@ -63,9 +127,12 @@ class CakePayCard {
|
||||
}
|
||||
}
|
||||
|
||||
final cakePayCardType = cakePayCardTypeFromApi(typeString);
|
||||
|
||||
return CakePayCard(
|
||||
id: json['id'] as int? ?? 0,
|
||||
name: name,
|
||||
type: cakePayCardType,
|
||||
description: description,
|
||||
termsAndConditions: termsAndConditions,
|
||||
howToUse: howToUse,
|
||||
@@ -103,12 +170,6 @@ class Denomination {
|
||||
this.usdValue,
|
||||
});
|
||||
|
||||
static double? _toDouble(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString());
|
||||
}
|
||||
|
||||
factory Denomination.fromJson(Map<String, dynamic> json) {
|
||||
return Denomination(
|
||||
value: _toDouble(json['value']) ?? 0,
|
||||
@@ -117,3 +178,29 @@ class Denomination {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PrepaidRange {
|
||||
final double minValue;
|
||||
final double maxValue;
|
||||
final String rangeId;
|
||||
|
||||
PrepaidRange({
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
required this.rangeId,
|
||||
});
|
||||
|
||||
factory PrepaidRange.fromCard(CakePayCard card) {
|
||||
return PrepaidRange(
|
||||
minValue: _toDouble(card.minValue) ?? 0,
|
||||
maxValue: _toDouble(card.maxValue) ?? 0,
|
||||
rangeId: card.id.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double? _toDouble(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString());
|
||||
}
|
||||
|
||||
@@ -31,9 +31,26 @@ class CakePayVendor {
|
||||
|
||||
if (cardsJson != null && cardsJson.isNotEmpty) {
|
||||
try {
|
||||
cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) {
|
||||
return Country.normalizeName(card['country'] as String) == country;
|
||||
}) as Map<String, dynamic>);
|
||||
final cards = cardsJson
|
||||
.map((cardJson) => CakePayCard.fromJson(cardJson as Map<String, dynamic>))
|
||||
.toList();
|
||||
final cardsForSelectedCountry =
|
||||
cards.where((card) => Country.normalizeName(card.country ?? '') == country).toList();
|
||||
|
||||
if (cardsForSelectedCountry.isNotEmpty) {
|
||||
final isPrepaid =
|
||||
cardsForSelectedCountry.every((card) => card.type == CakePayCardType.prepaid);
|
||||
|
||||
if (isPrepaid) {
|
||||
final firstCard = cardsForSelectedCountry.first;
|
||||
final prepaidRanges = cardsForSelectedCountry.map(PrepaidRange.fromCard).toList()
|
||||
..sort((a, b) => a.minValue.compareTo(b.minValue));
|
||||
|
||||
cardForVendor = firstCard.copyWith(prepaidRange: prepaidRanges);
|
||||
} else {
|
||||
cardForVendor = cardsForSelectedCountry.first;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Error parsing card for vendor: $e');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
||||
class CakePayApi {
|
||||
static const testBaseUri = FeatureFlag.hasDevOptions;
|
||||
static const testBaseUri = false; //FeatureFlag.hasDevOptions;
|
||||
|
||||
static const baseTestCakePayUri = 'api-stg.cakepay.com';
|
||||
static const baseProdCakePayUri = 'api-prod.cakepay.com';
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
|
||||
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'enter_amount_widget.dart';
|
||||
|
||||
class PrepaidRangeAmountWidget extends StatefulWidget {
|
||||
const PrepaidRangeAmountWidget({
|
||||
required this.fiatCurrency,
|
||||
required this.prepaidRanges,
|
||||
required this.amountFieldFocus,
|
||||
required this.amountController,
|
||||
required this.cakePayBuyCardViewModel,
|
||||
required this.onAmountChanged,
|
||||
});
|
||||
|
||||
final String fiatCurrency;
|
||||
final List<PrepaidRange> prepaidRanges;
|
||||
final FocusNode amountFieldFocus;
|
||||
final TextEditingController amountController;
|
||||
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
|
||||
final Function(String) onAmountChanged;
|
||||
|
||||
@override
|
||||
State<PrepaidRangeAmountWidget> createState() => _PrepaidRangeAmountWidgetState();
|
||||
}
|
||||
|
||||
class _PrepaidRangeAmountWidgetState extends State<PrepaidRangeAmountWidget> {
|
||||
late PrepaidRange _selectedRange;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedRange = widget.prepaidRanges.first;
|
||||
_applySelectedRange(forceAmountUpdate: widget.amountController.text.isEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Select range:',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<PrepaidRange>(
|
||||
value: _selectedRange,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha(40),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
items: widget.prepaidRanges
|
||||
.map(
|
||||
(range) => DropdownMenuItem<PrepaidRange>(
|
||||
value: range,
|
||||
child: Text(_rangeLabel(range)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (range) {
|
||||
if (range == null) return;
|
||||
setState(() => _selectedRange = range);
|
||||
widget.cakePayBuyCardViewModel.onPrepaidRangeChanged(range);
|
||||
_applySelectedRange(forceAmountUpdate: true);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
EnterAmountWidget(
|
||||
minValue: _formatAmount(_selectedRange.minValue),
|
||||
maxValue: _formatAmount(_selectedRange.maxValue),
|
||||
fiatCurrency: widget.fiatCurrency,
|
||||
amountFieldFocus: widget.amountFieldFocus,
|
||||
amountController: widget.amountController,
|
||||
onAmountChanged: (value) {
|
||||
widget.onAmountChanged(value);
|
||||
widget.cakePayBuyCardViewModel.selectedDenomination = (
|
||||
value.replaceAll(',', '.'),
|
||||
int.tryParse(_selectedRange.rangeId),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _applySelectedRange({required bool forceAmountUpdate}) {
|
||||
final currentAmount = double.tryParse(widget.amountController.text);
|
||||
final isCurrentAmountInRange = currentAmount != null &&
|
||||
currentAmount >= _selectedRange.minValue &&
|
||||
currentAmount <= _selectedRange.maxValue;
|
||||
|
||||
final nextAmount = forceAmountUpdate || !isCurrentAmountInRange
|
||||
? _formatAmount(_selectedRange.minValue)
|
||||
: widget.amountController.text;
|
||||
|
||||
widget.amountController.text = nextAmount;
|
||||
widget.onAmountChanged(nextAmount);
|
||||
widget.cakePayBuyCardViewModel.selectedDenomination = (
|
||||
nextAmount.replaceAll(',', '.'),
|
||||
int.tryParse(_selectedRange.rangeId),
|
||||
);
|
||||
}
|
||||
|
||||
String _rangeLabel(PrepaidRange range) {
|
||||
return '${widget.fiatCurrency} ${_formatAmount(range.minValue)} - ${_formatAmount(range.maxValue)}';
|
||||
}
|
||||
|
||||
String _formatAmount(double value) {
|
||||
return value % 1 == 0 ? value.toInt().toString() : value.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
@@ -12,32 +12,46 @@ class RoundedOverlayCards extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
|
||||
child: Container(
|
||||
height: screenHeight * 0.50,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
|
||||
Flexible(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25.0),
|
||||
bottomRight: Radius.circular(25.0),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
height: screenHeight * 0.38,
|
||||
width: double.infinity,
|
||||
child: topCardChild)),
|
||||
bottomCardChild,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: screenHeight * 0.38,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: topCardChild,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: bottomCardChild,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,8 +33,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
: 0,
|
||||
quantity = 1,
|
||||
card = vendor.card!,
|
||||
min = _toDouble(vendor.card!.minValue) ?? 0,
|
||||
max = _toDouble(vendor.card!.maxValue) ?? 0 {
|
||||
min = vendor.card!.prepaidRange.isNotEmpty
|
||||
? vendor.card!.prepaidRange.first.minValue
|
||||
: _toDouble(vendor.card!.minValue) ?? 0,
|
||||
max = vendor.card!.prepaidRange.isNotEmpty
|
||||
? vendor.card!.prepaidRange.first.maxValue
|
||||
: _toDouble(vendor.card!.maxValue) ?? 0 {
|
||||
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
|
||||
}
|
||||
|
||||
@@ -46,8 +50,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
final CakePayVendor vendor;
|
||||
final SendViewModel sendViewModel;
|
||||
final CakePayService _cakePayService;
|
||||
final double max;
|
||||
final double min;
|
||||
final CakePayCard card;
|
||||
final WalletType walletType;
|
||||
final Box<Order> orders;
|
||||
@@ -59,6 +61,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
bool confirmsNoVpn = false;
|
||||
bool confirmsVoidedRefund = false;
|
||||
bool confirmsTermsAgreed = false;
|
||||
double max;
|
||||
double min;
|
||||
(String, int?) selectedDenomination = ('', null);
|
||||
|
||||
String simulatedResponse = '';
|
||||
@@ -136,10 +140,24 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
@action
|
||||
void onQuantityChanged(int? input) => quantity = input ?? 1;
|
||||
|
||||
void onPrepaidRangeChanged(PrepaidRange range) {
|
||||
min = range.minValue;
|
||||
max = range.maxValue;
|
||||
selectedDenomination = (range.minValue.toString(), int.tryParse(range.rangeId));
|
||||
|
||||
if (amount < min || amount > max) {
|
||||
amount = min;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void onAmountChanged(String input) {
|
||||
if (input.isEmpty) return;
|
||||
amount = double.parse(input.replaceAll(',', '.'));
|
||||
|
||||
if (card.prepaidRange.isNotEmpty) {
|
||||
selectedDenomination = (input.replaceAll(',', '.'), selectedDenomination.$2);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -153,10 +171,25 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
sendViewModel.state =
|
||||
FailureState('Unsupported wallet type, please use Bitcoin, Monero, Litecoin or Zcash.');
|
||||
}
|
||||
|
||||
final isPrepaidRangeSelected = card.prepaidRange.isNotEmpty;
|
||||
|
||||
int selectedCardId = card.id;
|
||||
String selectedPrice = amount.toString();
|
||||
|
||||
if (isPrepaidRangeSelected) {
|
||||
selectedCardId = selectedDenomination.$2 ?? card.id;
|
||||
selectedPrice =
|
||||
selectedDenomination.$1.isNotEmpty ? selectedDenomination.$1 : amount.toString();
|
||||
} else if (isDenominationSelected) {
|
||||
selectedCardId = selectedDenomination.$2 ?? card.id;
|
||||
selectedPrice = selectedDenomination.$1;
|
||||
}
|
||||
|
||||
try {
|
||||
order = await _cakePayService.createOrder(
|
||||
cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id,
|
||||
price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(),
|
||||
cardId: selectedCardId,
|
||||
price: selectedPrice,
|
||||
quantity: quantity,
|
||||
confirmsNoVpn: confirmsNoVpn,
|
||||
confirmsVoidedRefund: confirmsVoidedRefund,
|
||||
|
||||
Reference in New Issue
Block a user