Cw 314 trocador receive screen update (#823)

* Change receive screen ui

* Upgrade flutter packages

* revert Upgrade flutter packages

* revert Upgrade flutter packages

* Adjust flow for anon invoice page navigation

* Add receive screen ui

* Implement anonpay invoice

* Add invoice detail to transactions page

* Implement donation link

* Fix transaction filter and details view

* Save donation link

* Fix transaction display issues

* Fix formatting

* Fix merge conflict

* Fix localization

* Fix transaction amount display

* Fix transaction limit for fiat

* Update fix from code review

* Fix issues from code review

* Make amountTo nullable to avoid potential

* Remove encoding for description in donation link

* Remove optional params from request

* Fix QR image version

* Refactor QRCode, fix issues from code review

* Pass version to QRCode full page

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Godwin Asuquo
2023-03-24 17:26:42 +02:00
committed by GitHub
co-authored by OmarHatem
parent 893a267de6
commit 3006679560
67 changed files with 2510 additions and 296 deletions
+211
View File
@@ -0,0 +1,211 @@
import 'dart:convert';
import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/anonpay/anonpay_request.dart';
import 'package:cake_wallet/anonpay/anonpay_status_response.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:http/http.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
class AnonPayApi {
const AnonPayApi({
this.useTorOnly = false,
required this.wallet,
});
final bool useTorOnly;
final WalletBase wallet;
static const anonpayRef = secrets.anonPayReferralCode;
static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion';
static const clearNetAuthority = 'trocador.app';
static const markup = secrets.trocadorExchangeMarkup;
static const anonPayPath = '/anonpay';
static const anonPayStatus = '/anonpay/status';
static const coinPath = 'api/coin';
static const apiKey = secrets.trocadorApiKey;
Future<AnonpayStatusResponse> paymentStatus(String id) async {
final authority = await _getAuthority();
final response = await get(Uri.https(authority, "$anonPayStatus/$id"));
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final status = responseJSON['Status'] as String;
final fiatAmount = responseJSON['Fiat_Amount'] as double?;
final fiatEquiv = responseJSON['Fiat_Equiv'] as String?;
final amountTo = responseJSON['AmountTo'] as double?;
final coinTo = responseJSON['CoinTo'] as String;
final address = responseJSON['Address'] as String;
return AnonpayStatusResponse(
status: status,
fiatAmount: fiatAmount,
amountTo: amountTo,
coinTo: coinTo,
address: address,
fiatEquiv: fiatEquiv,
);
}
Future<AnonpayInvoiceInfo> createInvoice(AnonPayRequest request) async {
final description = Uri.encodeComponent(request.description);
final body = <String, dynamic>{
'ticker_to': request.cryptoCurrency.title.toLowerCase(),
'network_to': _networkFor(request.cryptoCurrency),
'address': request.address,
'name': request.name,
'description': description,
'email': request.email,
'ref': anonpayRef,
'markup': markup,
'direct': 'False',
};
if (request.amount != null) {
body['amount'] = request.amount;
}
if (request.fiatEquivalent != null) {
body['fiat_equiv'] = request.fiatEquivalent;
}
final authority = await _getAuthority();
final response = await get(Uri.https(authority, anonPayPath, body));
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final id = responseJSON['ID'] as String;
final url = responseJSON['url'] as String;
final urlOnion = responseJSON['url_onion'] as String;
final statusUrl = responseJSON['status_url'] as String;
final statusUrlOnion = responseJSON['status_url_onion'] as String;
final statusInfo = await paymentStatus(id);
return AnonpayInvoiceInfo(
invoiceId: id,
clearnetUrl: url,
onionUrl: urlOnion,
status: statusInfo.status,
fiatAmount: statusInfo.fiatAmount,
fiatEquiv: statusInfo.fiatEquiv,
amountTo: statusInfo.amountTo,
coinTo: statusInfo.coinTo,
address: statusInfo.address,
clearnetStatusUrl: statusUrl,
onionStatusUrl: statusUrlOnion,
walletId: wallet.id,
createdAt: DateTime.now(),
provider: 'Trocador AnonPay invoice',
);
}
Future<AnonpayDonationLinkInfo> generateDonationLink(AnonPayRequest request) async {
final body = <String, dynamic>{
'ticker_to': request.cryptoCurrency.title.toLowerCase(),
'network_to': _networkFor(request.cryptoCurrency),
'address': request.address,
'ref': anonpayRef,
'direct': 'True',
};
if (request.name.isNotEmpty) {
body['name'] = request.name;
}
if (request.description.isNotEmpty) {
body['description'] = request.description;
}
if (request.email.isNotEmpty) {
body['email'] = request.email;
}
final clearnetUrl = Uri.https(clearNetAuthority, anonPayPath, body);
final onionUrl = Uri.https(onionApiAuthority, anonPayPath, body);
return AnonpayDonationLinkInfo(
clearnetUrl: clearnetUrl.toString(),
onionUrl: onionUrl.toString(),
address: request.address,
);
}
Future<Limits> fetchLimits({
FiatCurrency? fiatCurrency,
required CryptoCurrency cryptoCurrency,
}) async {
double fiatRate = 0.0;
if (fiatCurrency != null) {
fiatRate = await FiatConversionService.fetchPrice(
crypto: cryptoCurrency,
fiat: fiatCurrency,
torOnly: useTorOnly,
);
}
final params = <String, String>{
'api_key': apiKey,
'ticker': cryptoCurrency.title.toLowerCase(),
'name': cryptoCurrency.name,
};
final String apiAuthority = await _getAuthority();
final uri = Uri.https(apiAuthority, coinPath, params);
final response = await get(uri);
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body) as List<dynamic>;
if (responseJSON.isEmpty) {
throw Exception('No data');
}
final coinJson = responseJSON.first as Map<String, dynamic>;
final minimum = coinJson['minimum'] as double;
final maximum = coinJson['maximum'] as double;
if (fiatCurrency != null) {
return Limits(
min: double.tryParse((minimum * fiatRate).toStringAsFixed(2)),
max: double.tryParse((maximum * fiatRate).toStringAsFixed(2)),
);
}
return Limits(
min: minimum,
max: maximum,
);
}
String _networkFor(CryptoCurrency currency) {
switch (currency) {
case CryptoCurrency.usdt:
return CryptoCurrency.btc.title.toLowerCase();
default:
return currency.tag != null ? _normalizeTag(currency.tag!) : 'Mainnet';
}
}
String _normalizeTag(String tag) {
switch (tag) {
case 'ETH':
return 'ERC20';
default:
return tag.toLowerCase();
}
}
Future<String> _getAuthority() async {
try {
if (useTorOnly) {
return onionApiAuthority;
}
final uri = Uri.https(onionApiAuthority, '/anonpay');
await get(uri);
return onionApiAuthority;
} catch (e) {
return clearNetAuthority;
}
}
}
@@ -0,0 +1,13 @@
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
class AnonpayDonationLinkInfo implements AnonpayInfoBase{
final String clearnetUrl;
final String onionUrl;
final String address;
AnonpayDonationLinkInfo({
required this.clearnetUrl,
required this.onionUrl,
required this.address,
});
}
+5
View File
@@ -0,0 +1,5 @@
abstract class AnonpayInfoBase {
String get clearnetUrl;
String get onionUrl;
String get address;
}
+57
View File
@@ -0,0 +1,57 @@
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cw_core/keyable.dart';
import 'package:hive/hive.dart';
part 'anonpay_invoice_info.g.dart';
@HiveType(typeId: AnonpayInvoiceInfo.typeId)
class AnonpayInvoiceInfo extends HiveObject with Keyable implements AnonpayInfoBase {
@HiveField(0)
final String invoiceId;
@HiveField(1)
String status;
@HiveField(2)
final double? fiatAmount;
@HiveField(3)
final String? fiatEquiv;
@HiveField(4)
final double? amountTo;
@HiveField(5)
final String coinTo;
@HiveField(6)
final String address;
@HiveField(7)
final String clearnetUrl;
@HiveField(8)
final String onionUrl;
@HiveField(9)
final String clearnetStatusUrl;
@HiveField(10)
final String onionStatusUrl;
@HiveField(11)
final DateTime createdAt;
@HiveField(12)
final String walletId;
@HiveField(13)
final String provider;
static const typeId = 10;
static const boxName = 'AnonpayInvoiceInfo';
AnonpayInvoiceInfo({
required this.invoiceId,
required this.clearnetUrl,
required this.onionUrl,
required this.clearnetStatusUrl,
required this.onionStatusUrl,
required this.status,
this.fiatAmount,
this.fiatEquiv,
this.amountTo,
required this.coinTo,
required this.address,
required this.createdAt,
required this.walletId,
required this.provider,
});
}
+21
View File
@@ -0,0 +1,21 @@
import 'package:cw_core/crypto_currency.dart';
class AnonPayRequest {
CryptoCurrency cryptoCurrency;
String address;
String name;
String? amount;
String email;
String description;
String? fiatEquivalent;
AnonPayRequest({
required this.cryptoCurrency,
required this.address,
required this.name,
required this.email,
this.amount,
required this.description,
this.fiatEquivalent,
});
}
+17
View File
@@ -0,0 +1,17 @@
class AnonpayStatusResponse {
final String status;
final double? fiatAmount;
final String? fiatEquiv;
final double? amountTo;
final String coinTo;
final String address;
const AnonpayStatusResponse({
required this.status,
this.fiatAmount,
this.fiatEquiv,
this.amountTo,
required this.coinTo,
required this.address,
});
}
+70 -5
View File
@@ -1,10 +1,18 @@
import 'package:cake_wallet/anonpay/anonpay_api.dart';
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
import 'package:cake_wallet/entities/wake_lock.dart';
import 'package:cake_wallet/ionia/ionia_anypay.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/ionia/ionia_tip.dart';
import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart';
import 'package:cake_wallet/src/screens/buy/onramper_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
@@ -13,7 +21,11 @@ import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dar
import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart';
import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart';
@@ -176,6 +188,7 @@ late Box<ExchangeTemplate> _exchangeTemplates;
late Box<TransactionDescription> _transactionDescriptionBox;
late Box<Order> _ordersSource;
late Box<UnspentCoinsInfo>? _unspentCoinsInfoSource;
late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
Future setup(
{required Box<WalletInfo> walletInfoSource,
@@ -186,7 +199,9 @@ Future setup(
required Box<ExchangeTemplate> exchangeTemplates,
required Box<TransactionDescription> transactionDescriptionBox,
required Box<Order> ordersSource,
Box<UnspentCoinsInfo>? unspentCoinsInfoSource}) async {
Box<UnspentCoinsInfo>? unspentCoinsInfoSource,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource
}) async {
_walletInfoSource = walletInfoSource;
_nodeSource = nodeSource;
_contactSource = contactSource;
@@ -196,6 +211,7 @@ Future setup(
_transactionDescriptionBox = transactionDescriptionBox;
_ordersSource = ordersSource;
_unspentCoinsInfoSource = unspentCoinsInfoSource;
_anonpayInvoiceInfoSource = anonpayInvoiceInfoSource;
if (!_isSetupFinished) {
getIt.registerSingletonAsync<SharedPreferences>(
@@ -240,6 +256,8 @@ Future setup(
appStore: getIt.get<AppStore>(),
secureStorage: getIt.get<FlutterSecureStorage>())
..init());
getIt.registerSingleton<AnonpayTransactionsStore>(AnonpayTransactionsStore(
anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource));
final secretStore =
await SecretStoreBase.load(getIt.get<FlutterSecureStorage>());
@@ -306,7 +324,9 @@ Future setup(
transactionFilterStore: getIt.get<TransactionFilterStore>(),
settingsStore: settingsStore,
yatStore: getIt.get<YatStore>(),
ordersStore: getIt.get<OrdersStore>()));
ordersStore: getIt.get<OrdersStore>(),
anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>())
);
getIt.registerFactory<AuthService>(() => AuthService(
secureStorage: getIt.get<FlutterSecureStorage>(),
@@ -360,11 +380,37 @@ Future setup(
BalancePage(dashboardViewModel: getIt.get<DashboardViewModel>(), settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory<DashboardPage>(() => DashboardPage( balancePage: getIt.get<BalancePage>(), walletViewModel: getIt.get<DashboardViewModel>(), addressListViewModel: getIt.get<WalletAddressListViewModel>()));
getIt.registerFactoryParam<ReceiveOptionViewModel, ReceivePageOption?, void>((pageOption, _) => ReceiveOptionViewModel(
getIt.get<AppStore>().wallet!, pageOption));
getIt.registerFactoryParam<AnonInvoicePageViewModel, List<dynamic>, void>((args, _) {
final address = args.first as String;
final pageOption = args.last as ReceivePageOption;
return AnonInvoicePageViewModel(
getIt.get<AnonPayApi>(),
address,
getIt.get<SettingsStore>(),
getIt.get<AppStore>().wallet!,
_anonpayInvoiceInfoSource,
getIt.get<SharedPreferences>(),
pageOption,
);
});
getIt.registerFactoryParam<AnonPayInvoicePage, List<dynamic>, void>((List<dynamic> args, _) {
final pageOption = args.last as ReceivePageOption;
return AnonPayInvoicePage(
getIt.get<AnonInvoicePageViewModel>(param1: args),
getIt.get<ReceiveOptionViewModel>(param1: pageOption));
});
getIt.registerFactory<ReceivePage>(() => ReceivePage(
addressListViewModel: getIt.get<WalletAddressListViewModel>()));
getIt.registerFactory<AddressPage>(() => AddressPage(
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
walletViewModel: getIt.get<DashboardViewModel>()));
walletViewModel: getIt.get<DashboardViewModel>(),
receiveOptionViewModel: getIt.get<ReceiveOptionViewModel>()));
getIt.registerFactoryParam<WalletAddressEditOrCreateViewModel, WalletAddressListItem?, void>(
(WalletAddressListItem? item, _) => WalletAddressEditOrCreateViewModel(
@@ -716,8 +762,8 @@ Future setup(
getIt.registerFactory(() => AddressResolver(yatService: getIt.get<YatService>(),
walletType: getIt.get<AppStore>().wallet!.type));
getIt.registerFactoryParam<FullscreenQRPage, String, bool>(
(String qrData, bool isLight) => FullscreenQRPage(qrData: qrData, isLight: isLight,));
getIt.registerFactoryParam<FullscreenQRPage, String, int?>(
(String qrData, int? version) => FullscreenQRPage(qrData: qrData, version: version,));
getIt.registerFactory(() => IoniaApi());
@@ -823,6 +869,25 @@ Future setup(
getIt.registerFactory(() => IoniaAccountPage(getIt.get<IoniaAccountViewModel>()));
getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get<IoniaAccountViewModel>()));
getIt.registerFactory(() => AnonPayApi(useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly,
wallet: getIt.get<AppStore>().wallet!)
);
getIt.registerFactoryParam<AnonpayDetailsViewModel, AnonpayInvoiceInfo, void>(
(AnonpayInvoiceInfo anonpayInvoiceInfo, _)
=> AnonpayDetailsViewModel(
anonPayApi: getIt.get<AnonPayApi>(),
anonpayInvoiceInfo: anonpayInvoiceInfo,
settingsStore: getIt.get<SettingsStore>(),
));
getIt.registerFactoryParam<AnonPayReceivePage, AnonpayInfoBase, void>(
(AnonpayInfoBase anonpayInvoiceInfo, _) => AnonPayReceivePage(invoiceInfo: anonpayInvoiceInfo));
getIt.registerFactoryParam<AnonpayDetailsPage, AnonpayInvoiceInfo, void>(
(AnonpayInvoiceInfo anonpayInvoiceInfo, _)
=> AnonpayDetailsPage(anonpayDetailsViewModel: getIt.get<AnonpayDetailsViewModel>(param1: anonpayInvoiceInfo)));
getIt.registerFactoryParam<IoniaPaymentStatusViewModel, IoniaAnyPayPaymentInfo, AnyPayPaymentCommittedInfo>(
(IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo)
+11 -1
View File
@@ -1,6 +1,7 @@
import 'package:cw_core/currency.dart';
import 'package:cw_core/enumerable_item.dart';
class FiatCurrency extends EnumerableItem<String> with Serializable<String> {
class FiatCurrency extends EnumerableItem<String> with Serializable<String> implements Currency {
const FiatCurrency({required String symbol, required this.countryCode, required this.fullName}) : super(title: symbol, raw: symbol);
final String countryCode;
@@ -118,4 +119,13 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> {
@override
int get hashCode => raw.hashCode ^ title.hashCode;
@override
String get name => raw;
@override
String? get tag => null;
@override
String get iconPath => "assets/images/flags/$countryCode.png";
}
+2
View File
@@ -37,4 +37,6 @@ class PreferencesKey {
=> '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
static const exchangeProvidersSelection = 'exchange-providers-selection';
static const clearnetDonationLink = 'clearnet_donation_link';
static const onionDonationLink = 'onion_donation_link';
}
+23
View File
@@ -0,0 +1,23 @@
enum ReceivePageOption {
mainnet,
anonPayInvoice,
anonPayDonationLink;
@override
String toString() {
String label = '';
switch (this) {
case ReceivePageOption.mainnet:
label = 'Mainnet';
break;
case ReceivePageOption.anonPayInvoice:
label = 'Trocador AnonPay Invoice';
break;
case ReceivePageOption.anonPayDonationLink:
label = 'Trocador AnonPay Donation Link';
break;
}
return label;
}
}
+9
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/language_service.dart';
import 'package:cake_wallet/buy/order.dart';
@@ -100,6 +101,10 @@ Future<void> main() async {
Hive.registerAdapter(UnspentCoinsInfoAdapter());
}
if (!Hive.isAdapterRegistered(AnonpayInvoiceInfo.typeId)) {
Hive.registerAdapter(AnonpayInvoiceInfoAdapter());
}
final secureStorage = FlutterSecureStorage();
final transactionDescriptionsBoxKey = await getEncryptionKey(
secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
@@ -120,6 +125,7 @@ Future<void> main() async {
final templates = await Hive.openBox<Template>(Template.boxName);
final exchangeTemplates =
await Hive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
final anonpayInvoiceInfo = await Hive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
Box<UnspentCoinsInfo>? unspentCoinsInfoSource;
if (!isMoneroOnly) {
@@ -139,6 +145,7 @@ Future<void> main() async {
exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 19);
runApp(App());
}, (error, stackTrace) async {
@@ -158,6 +165,7 @@ Future<void> initialSetup(
required Box<ExchangeTemplate> exchangeTemplates,
required Box<TransactionDescription> transactionDescriptions,
required FlutterSecureStorage secureStorage,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
Box<UnspentCoinsInfo>? unspentCoinsInfoSource,
int initialMigrationVersion = 15}) async {
LanguageService.loadLocaleList();
@@ -178,6 +186,7 @@ Future<void> initialSetup(
exchangeTemplates: exchangeTemplates,
transactionDescriptionBox: transactionDescriptions,
ordersSource: ordersSource,
anonpayInvoiceInfoSource: anonpayInvoiceInfo,
unspentCoinsInfoSource: unspentCoinsInfoSource,
);
await bootstrap(navigatorKey);
+20 -2
View File
@@ -1,10 +1,15 @@
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/onramper_page.dart';
import 'package:cake_wallet/src/screens/buy/pre_order_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
@@ -440,7 +445,8 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) =>
getIt.get<FullscreenQRPage>(
param1: args['qrData'] as String,
param2: args['isLight'] as bool,
param2: args['version'] as int?,
));
case Routes.ioniaWelcomePage:
@@ -514,7 +520,19 @@ Route<dynamic> createRoute(RouteSettings settings) {
getIt.get<AdvancedPrivacySettingsViewModel>(param1: type),
getIt.get<NodeCreateOrEditViewModel>(param1: type),
));
case Routes.anonPayInvoicePage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<AnonPayInvoicePage>(param1: args));
case Routes.anonPayReceivePage:
final anonInvoiceViewData = settings.arguments as AnonpayInfoBase;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<AnonPayReceivePage>(param1: anonInvoiceViewData));
case Routes.anonPayDetailsPage:
final anonInvoiceViewData = settings.arguments as AnonpayInvoiceInfo;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<AnonpayDetailsPage>(param1: anonInvoiceViewData));
default:
return MaterialPageRoute<void>(
builder: (_) => Scaffold(
+3
View File
@@ -82,4 +82,7 @@ class Routes {
static const displaySettingsPage = '/display_settings_page';
static const otherSettingsPage = '/other_settings_page';
static const advancedPrivacySettings = '/advanced_privacy_settings';
static const anonPayInvoicePage = '/anon_pay_invoice_page';
static const anonPayReceivePage = '/anon_pay_receive_page';
static const anonPayDetailsPage = '/anon_pay_details_page';
}
@@ -0,0 +1,56 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_list_card.dart';
import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class AnonpayDetailsPage extends BasePage {
AnonpayDetailsPage({required this.anonpayDetailsViewModel});
@override
String get title => S.current.invoice_details;
final AnonpayDetailsViewModel anonpayDetailsViewModel;
@override
Widget body(BuildContext context) {
return SectionStandardList(
context: context,
sectionCount: 1,
itemCounter: (int _) => anonpayDetailsViewModel.items.length,
itemBuilder: (_, __, index) {
final item = anonpayDetailsViewModel.items[index];
if (item is DetailsListStatusItem) {
return StandardListStatusRow(title: item.title, value: item.value);
}
if (item is TradeDetailsListCardItem) {
return TradeDetailsStandardListCard(
id: item.id,
create: item.createdAt,
pair: item.pair,
currentTheme: anonpayDetailsViewModel.settingsStore.currentTheme.type,
onTap: item.onTap,
);
}
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
},
child: ListRow(title: '${item.title}:', value: item.value),
);
});
}
}
+3 -2
View File
@@ -1,6 +1,7 @@
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -153,8 +154,8 @@ class ContactPage extends BasePage {
items: contactViewModel.currencies,
title: S.of(context).please_select,
hintText: S.of(context).search_currency,
onItemSelected: (CryptoCurrency item) =>
contactViewModel.currency = item),
onItemSelected: (Currency item) =>
contactViewModel.currency = item as CryptoCurrency),
context: context);
}
@@ -1,9 +1,14 @@
import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
@@ -13,24 +18,25 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/di.dart';
class AddressPage extends BasePage {
AddressPage({
required this.addressListViewModel,
required this.walletViewModel})
: _cryptoAmountFocus = FocusNode();
required this.walletViewModel,
required this.receiveOptionViewModel,
}) : _cryptoAmountFocus = FocusNode();
final WalletAddressListViewModel addressListViewModel;
final DashboardViewModel walletViewModel;
final ReceiveOptionViewModel receiveOptionViewModel;
final FocusNode _cryptoAmountFocus;
@override
String get title => S.current.receive;
@override
Color get backgroundLightColor => currentTheme.type == ThemeType.bright
? Colors.transparent : Colors.white;
Color get backgroundLightColor =>
currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white;
@override
Color get backgroundDarkColor => Colors.transparent;
@@ -38,11 +44,15 @@ class AddressPage extends BasePage {
@override
bool get resizeToAvoidBottomInset => false;
bool effectsInstalled = false;
@override
Widget leading(BuildContext context) {
final _backButton = Icon(Icons.arrow_back_ios,
color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!,
size: 16,);
final _backButton = Icon(
Icons.arrow_back_ios,
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
size: 16,
);
return SizedBox(
height: 37,
@@ -61,16 +71,8 @@ class AddressPage extends BasePage {
}
@override
Widget middle(BuildContext context) {
return Text(
title,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
fontFamily: 'Lato',
color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!),
);
}
Widget middle(BuildContext context) =>
PresentReceiveOptionPicker(receiveOptionViewModel: receiveOptionViewModel);
@override
Widget Function(BuildContext, Widget) get rootWrapper =>
@@ -85,53 +87,56 @@ class AddressPage extends BasePage {
@override
Widget? trailing(BuildContext context) {
final shareImage =
Image.asset('assets/images/share.png',
final shareImage = Image.asset('assets/images/share.png',
color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!);
return !addressListViewModel.hasAddressList ? Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
iconSize: 25,
onPressed: () {
ShareUtil.share(
text: addressListViewModel.address.address,
context: context,
);
},
icon: shareImage,
),
) : null;
return !addressListViewModel.hasAddressList
? Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
iconSize: 25,
onPressed: () {
ShareUtil.share(
text: addressListViewModel.address.address,
context: context,
);
},
icon: shareImage,
),
)
: null;
}
@override
Widget body(BuildContext context) {
_setEffects(context);
autorun((_) async {
if (!walletViewModel.isOutdatedElectrumWallet
|| !walletViewModel.settingsStore.shouldShowReceiveWarning) {
if (!walletViewModel.isOutdatedElectrumWallet ||
!walletViewModel.settingsStore.shouldShowReceiveWarning) {
return;
}
await Future<void>.delayed(Duration(seconds: 1));
if (context.mounted) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).pre_seed_title,
alertContent: S.of(context).outdated_electrum_wallet_receive_warning,
leftButtonText: S.of(context).understand,
actionLeftButton: () => Navigator.of(context).pop(),
rightButtonText: S.of(context).do_not_show_me,
actionRightButton: () {
walletViewModel.settingsStore.setShouldShowReceiveWarning(false);
Navigator.of(context).pop();
});
});
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).pre_seed_title,
alertContent: S.of(context).outdated_electrum_wallet_receive_warning,
leftButtonText: S.of(context).understand,
actionLeftButton: () => Navigator.of(context).pop(),
rightButtonText: S.of(context).do_not_show_me,
actionRightButton: () {
walletViewModel.settingsStore.setShouldShowReceiveWarning(false);
Navigator.of(context).pop();
});
});
}
});
@@ -141,8 +146,7 @@ class AddressPage extends BasePage {
tapOutsideToDismiss: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor:
Theme.of(context).accentTextTheme!.bodyText1!.backgroundColor!,
keyboardBarColor: Theme.of(context).accentTextTheme.bodyText1!.backgroundColor!,
nextFocus: false,
actions: [
KeyboardActionsItem(
@@ -154,29 +158,25 @@ class AddressPage extends BasePage {
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Column(
children: <Widget>[
Expanded(
child: Observer(builder: (_) => QRWidget(
addressListViewModel: addressListViewModel,
amountTextFieldFocusNode: _cryptoAmountFocus,
isAmountFieldShow: !addressListViewModel.hasAccounts,
isLight: walletViewModel.settingsStore.currentTheme.type == ThemeType.light))
Expanded(
child: Observer(builder: (_) => QRWidget(
addressListViewModel: addressListViewModel,
amountTextFieldFocusNode: _cryptoAmountFocus,
isAmountFieldShow: !addressListViewModel.hasAccounts,
isLight: walletViewModel.settingsStore.currentTheme.type == ThemeType.light))
),
Observer(builder: (_) {
return addressListViewModel.hasAddressList
? GestureDetector(
onTap: () =>
Navigator.of(context).pushNamed(Routes.receive),
onTap: () => Navigator.of(context).pushNamed(Routes.receive),
child: Container(
height: 50,
padding: EdgeInsets.only(left: 24, right: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(25)),
borderRadius: BorderRadius.all(Radius.circular(25)),
border: Border.all(
color:
Theme.of(context).textTheme!.subtitle1!.color!,
width: 1),
color: Theme.of(context).textTheme.subtitle1!.color!, width: 1),
color: Theme.of(context).buttonColor),
child: Row(
mainAxisSize: MainAxisSize.max,
@@ -185,42 +185,79 @@ class AddressPage extends BasePage {
Observer(
builder: (_) => Text(
addressListViewModel.hasAccounts
? S
.of(context)
.accounts_subaddresses
? S.of(context).accounts_subaddresses
: S.of(context).addresses,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.accentTextTheme!
.accentTextTheme
.headline2!
.backgroundColor!),
)),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context)
.accentTextTheme!
.headline2!
.backgroundColor!,
color:
Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
)
],
),
),
)
: Text(
S.of(context).electrum_address_disclaimer,
: Text(S.of(context).electrum_address_disclaimer,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Theme.of(context)
.accentTextTheme!
.headline3!
.backgroundColor!));
color: Theme.of(context).accentTextTheme.headline3!.backgroundColor!));
})
],
),
));
}
void _setEffects(BuildContext context) {
if (effectsInstalled) {
return;
}
reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) {
Navigator.pop(context);
switch (option) {
case ReceivePageOption.anonPayInvoice:
Navigator.pushReplacementNamed(
context,
Routes.anonPayInvoicePage,
arguments: [addressListViewModel.address.address, option],
);
break;
case ReceivePageOption.anonPayDonationLink:
final sharedPreferences = getIt.get<SharedPreferences>();
final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink);
if (clearnetUrl != null && onionUrl != null) {
Navigator.pushReplacementNamed(
context,
Routes.anonPayReceivePage,
arguments: AnonpayDonationLinkInfo(
clearnetUrl: clearnetUrl,
onionUrl: onionUrl,
address: addressListViewModel.address.address,
),
);
} else {
Navigator.pushReplacementNamed(
context,
Routes.anonPayInvoicePage,
arguments: [addressListViewModel.address.address, option],
);
}
break;
default:
}
});
effectsInstalled = true;
}
}
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
class AnonpayTransactionRow extends StatelessWidget {
AnonpayTransactionRow({
required this.provider,
required this.createdAt,
required this.currency,
required this.onTap,
required this.amount,
});
final VoidCallback? onTap;
final String provider;
final String createdAt;
final String amount;
final String currency;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getImage(),
SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
Text(provider,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!)),
Text(amount + ' ' + currency,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!))
]),
SizedBox(height: 5),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
Text(createdAt,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.overline!.backgroundColor!))
])
],
))
],
),
));
}
Widget _getImage() => ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset('assets/images/trocador.png', width: 36, height: 36));
}
@@ -0,0 +1,140 @@
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/rounded_checkbox.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
class PresentReceiveOptionPicker extends StatelessWidget {
PresentReceiveOptionPicker({required this.receiveOptionViewModel});
final ReceiveOptionViewModel receiveOptionViewModel;
@override
Widget build(BuildContext context) {
final arrowBottom =
Image.asset('assets/images/arrow_bottom_purple_icon.png', color: Colors.white, height: 6);
return TextButton(
onPressed: () => _showPicker(context),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
splashFactory: NoSplash.splashFactory,
foregroundColor: MaterialStateProperty.all(Colors.transparent),
overlayColor: MaterialStateProperty.all(Colors.transparent),
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
S.current.receive,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
fontFamily: 'Lato',
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!),
),
Observer(
builder: (_) => Text(receiveOptionViewModel.selectedReceiveOption.toString(),
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.headline5!.color!)))
],
),
SizedBox(width: 5),
Padding(
padding: EdgeInsets.only(top: 12),
child: arrowBottom,
)
],
),
);
}
void _showPicker(BuildContext context) async {
await showPopUp<void>(
builder: (BuildContext popUpContext) => Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
body: AlertBackground(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(),
Container(
margin: EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context).backgroundColor,
),
child: Padding(
padding: const EdgeInsets.only(top: 24, bottom: 24),
child: (ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: receiveOptionViewModel.options.length,
itemBuilder: (_, index) {
final option = receiveOptionViewModel.options[index];
return InkWell(
onTap: () => receiveOptionViewModel.selectReceiveOption(option),
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: Observer(builder: (_) {
final value = receiveOptionViewModel.selectedReceiveOption;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(option.toString(),
textAlign: TextAlign.left,
style: textSmall(
color: Theme.of(context).primaryTextTheme.headline6!.color!,
).copyWith(
fontWeight:
value == option ? FontWeight.w800 : FontWeight.w500,
)),
RoundedCheckbox(
value: value == option,
)
],
);
}),
),
);
},
separatorBuilder: (_, index) => SizedBox(height: 30),
)),
),
),
Spacer(),
Container(
margin: EdgeInsets.only(bottom: 40),
child: InkWell(
onTap: () => Navigator.pop(context),
child: CircleAvatar(
child: Icon(
Icons.close,
color: Palette.darkBlueCraiola,
),
backgroundColor: Colors.white,
),
),
)
],
),
),
),
context: context,
);
}
}
@@ -1,5 +1,8 @@
import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -22,102 +25,104 @@ class TransactionsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: 24,
bottom: 24
),
padding: EdgeInsets.only(top: 24, bottom: 24),
child: Column(
children: <Widget>[
HeaderRow(dashboardViewModel: dashboardViewModel),
Expanded(
child: Observer(
builder: (_) {
final items = dashboardViewModel.items;
Expanded(child: Observer(builder: (_) {
final items = dashboardViewModel.items;
return items?.isNotEmpty ?? false
? ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return items.isNotEmpty
? ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
final item = items[index];
if (item is DateSectionItem) {
return DateSectionRaw(date: item.date);
}
if (item is TransactionListItem) {
final transaction = item.transaction;
return Observer(
builder: (_) => TransactionRow(
onTap: () => Navigator.of(context).pushNamed(
Routes.transactionDetails,
arguments: transaction),
direction: transaction.direction,
formattedDate: DateFormat('HH:mm')
.format(transaction.date),
formattedAmount: item.formattedCryptoAmount,
formattedFiatAmount:
dashboardViewModel.balanceViewModel.isFiatDisabled
? '' : item.formattedFiatAmount,
isPending: transaction.isPending,
title: item.formattedTitle + item.formattedStatus));
}
if (item is TradeListItem) {
final trade = item.trade;
return Observer(builder: (_) => TradeRow(
onTap: () => Navigator.of(context).pushNamed(
Routes.tradeDetails,
arguments: trade),
provider: trade.provider,
from: trade.from,
to: trade.to,
createdAtFormattedDate:
trade.createdAt != null
? DateFormat('HH:mm').format(trade.createdAt!)
: null,
formattedAmount: item.tradeFormattedAmount
));
}
if (item is OrderListItem) {
final order = item.order;
return Observer(builder: (_) => OrderRow(
onTap: () => Navigator.of(context).pushNamed(
Routes.orderDetails,
arguments: order),
provider: order.provider,
from: order.from!,
to: order.to!,
createdAtFormattedDate:
DateFormat('HH:mm').format(order.createdAt),
formattedAmount: item.orderFormattedAmount,
));
}
return Container(
color: Colors.transparent,
height: 1);
if (item is DateSectionItem) {
return DateSectionRaw(date: item.date);
}
)
: Center(
if (item is TransactionListItem) {
final transaction = item.transaction;
return Observer(
builder: (_) => TransactionRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.transactionDetails, arguments: transaction),
direction: transaction.direction,
formattedDate: DateFormat('HH:mm').format(transaction.date),
formattedAmount: item.formattedCryptoAmount,
formattedFiatAmount:
dashboardViewModel.balanceViewModel.isFiatDisabled
? ''
: item.formattedFiatAmount,
isPending: transaction.isPending,
title: item.formattedTitle + item.formattedStatus));
}
if (item is AnonpayTransactionListItem) {
final transactionInfo = item.transaction;
return AnonpayTransactionRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo),
currency: transactionInfo.fiatAmount != null
? transactionInfo.fiatEquiv ?? ''
: CryptoCurrency.fromFullName(transactionInfo.coinTo)
.name
.toUpperCase(),
provider: transactionInfo.provider,
amount: transactionInfo.fiatAmount?.toString() ??
(transactionInfo.amountTo?.toString() ?? ''),
createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt),
);
}
if (item is TradeListItem) {
final trade = item.trade;
return Observer(
builder: (_) => TradeRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.tradeDetails, arguments: trade),
provider: trade.provider,
from: trade.from,
to: trade.to,
createdAtFormattedDate: trade.createdAt != null
? DateFormat('HH:mm').format(trade.createdAt!)
: null,
formattedAmount: item.tradeFormattedAmount));
}
if (item is OrderListItem) {
final order = item.order;
return Observer(
builder: (_) => OrderRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.orderDetails, arguments: order),
provider: order.provider,
from: order.from!,
to: order.to!,
createdAtFormattedDate:
DateFormat('HH:mm').format(order.createdAt),
formattedAmount: item.orderFormattedAmount,
));
}
return Container(color: Colors.transparent, height: 1);
})
: Center(
child: Text(
S.of(context).placeholder_transactions,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).primaryTextTheme!
.overline!.decorationColor!
),
fontSize: 14,
color: Theme.of(context).primaryTextTheme.overline!.decorationColor!),
),
);
}
)
)
}))
],
),
);
}
}
}
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker_item_widget.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/picker_item.dart';
import 'package:cake_wallet/src/widgets/alert_close_button.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -20,9 +21,9 @@ class CurrencyPicker extends StatefulWidget {
this.isConvertFrom = false});
int selectedAtIndex;
final List<CryptoCurrency> items;
final List<Currency> items;
final String? title;
final Function(CryptoCurrency) onItemSelected;
final Function(Currency) onItemSelected;
final bool isMoneroWallet;
final bool isConvertFrom;
final String? hintText;
@@ -38,13 +39,13 @@ class CurrencyPickerState extends State<CurrencyPicker> {
subPickerItemsList = items,
appBarTextStyle =
TextStyle(fontSize: 20, fontFamily: 'Lato', backgroundColor: Colors.transparent, color: Colors.white),
pickerItemsList = <PickerItem<CryptoCurrency>>[];
pickerItemsList = <PickerItem<Currency>>[];
List<PickerItem<CryptoCurrency>> pickerItemsList;
List<CryptoCurrency> items;
List<PickerItem<Currency>> pickerItemsList;
List<Currency> items;
bool isSearchBarActive;
String textFieldValue;
List<CryptoCurrency> subPickerItemsList;
List<Currency> subPickerItemsList;
TextStyle appBarTextStyle;
void cleanSubPickerItemsList() => subPickerItemsList = items;
@@ -54,7 +55,7 @@ class CurrencyPickerState extends State<CurrencyPicker> {
if (subString.isNotEmpty) {
subPickerItemsList = items
.where((element) =>
(element.title != null ? element.title.toLowerCase().contains(subString.toLowerCase()) : false) ||
element.name.toLowerCase().contains(subString.toLowerCase()) ||
(element.tag != null ? element.tag!.toLowerCase().contains(subString.toLowerCase()) : false) ||
(element.fullName != null ? element.fullName!.toLowerCase().contains(subString.toLowerCase()) : false))
.toList();
@@ -139,7 +140,7 @@ class CurrencyPickerState extends State<CurrencyPicker> {
AspectRatio(
aspectRatio: 6,
child: PickerItemWidget(
title: items[widget.selectedAtIndex].title,
title: items[widget.selectedAtIndex].name,
iconPath: items[widget.selectedAtIndex].iconPath,
isSelected: true,
tag: items[widget.selectedAtIndex].tag,
@@ -32,12 +32,12 @@ class PickerItemWidget extends StatelessWidget {
width: 20.0,
),
),
const SizedBox(width: 6),
const SizedBox(width: 12),
Expanded(
child: Row(
children: [
Text(
title,
title.toUpperCase(),
style: TextStyle(
color: isSelected ? Palette.blueCraiola : Theme.of(context).primaryTextTheme!.headline6!.color!,
fontSize: isSelected ? 16 : 14.0,
@@ -1,6 +1,5 @@
import 'package:cw_core/currency.dart';
import 'package:flutter/material.dart';
import 'package:cw_core/crypto_currency.dart';
import 'picker_item.dart';
import 'currency_picker_item_widget.dart';
class CurrencyPickerWidget extends StatelessWidget {
@@ -14,7 +13,7 @@ class CurrencyPickerWidget extends StatelessWidget {
final int crossAxisCount;
final int selectedAtIndex;
final Function pickListItem;
final List<CryptoCurrency> pickerItemsList;
final List<Currency> pickerItemsList;
final ScrollController _scrollController = ScrollController();
@@ -39,8 +38,8 @@ class CurrencyPickerWidget extends StatelessWidget {
onTap: () {
pickListItem(index);
},
title: pickerItemsList[index].title,
iconPath: pickerItemsList[index].iconPath,
title: pickerItemsList[index].name,
iconPath: pickerItemsList[index].iconPath,
tag: pickerItemsList[index].tag,
);
}),
@@ -4,6 +4,7 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -501,9 +502,9 @@ class ExchangeCardState extends State<ExchangeCard> {
hintText: S.of(context).search_currency,
isMoneroWallet: _isMoneroWallet,
isConvertFrom: widget.hasRefundAddress,
onItemSelected: (CryptoCurrency item) =>
onItemSelected: (Currency item) =>
widget.onCurrencySelected != null
? widget.onCurrencySelected(item)
? widget.onCurrencySelected(item as CryptoCurrency)
: null),
context: context);
}
@@ -0,0 +1,222 @@
import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart';
import 'package:cake_wallet/src/screens/receive/widgets/anonpay_input_form.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AnonPayInvoicePage extends BasePage {
AnonPayInvoicePage(
this.anonInvoicePageViewModel,
this.receiveOptionViewModel,
) : _amountFocusNode = FocusNode() {
_nameController.text = anonInvoicePageViewModel.receipientName;
_descriptionController.text = anonInvoicePageViewModel.description;
_emailController.text = anonInvoicePageViewModel.receipientEmail;
}
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _descriptionController = TextEditingController();
final _amountController = TextEditingController();
final FocusNode _amountFocusNode;
final AnonInvoicePageViewModel anonInvoicePageViewModel;
final ReceiveOptionViewModel receiveOptionViewModel;
final _formKey = GlobalKey<FormState>();
bool effectsInstalled = false;
@override
Color get titleColor => Colors.white;
@override
bool get resizeToAvoidBottomInset => false;
@override
bool get extendBodyBehindAppBar => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.transparent;
@override
Widget middle(BuildContext context) =>
PresentReceiveOptionPicker(receiveOptionViewModel: receiveOptionViewModel);
@override
Widget trailing(BuildContext context) => TrailButton(
caption: S.of(context).clear,
onPressed: () {
_formKey.currentState?.reset();
anonInvoicePageViewModel.reset();
});
@override
Widget body(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context));
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).accentTextTheme.bodyText1!.backgroundColor!,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _amountFocusNode,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
]),
child: Container(
color: Theme.of(context).backgroundColor,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(
colors: [
Theme.of(context).primaryTextTheme.subtitle2!.color!,
Theme.of(context).primaryTextTheme.subtitle2!.decorationColor!,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Observer(builder: (_) {
return Padding(
padding: EdgeInsets.fromLTRB(24, 100, 24, 0),
child: AnonInvoiceForm(
nameController: _nameController,
descriptionController: _descriptionController,
amountController: _amountController,
emailController: _emailController,
depositAmountFocus: _amountFocusNode,
formKey: _formKey,
isInvoice: receiveOptionViewModel.selectedReceiveOption ==
ReceivePageOption.anonPayInvoice,
anonInvoicePageViewModel: anonInvoicePageViewModel,
),
);
}),
),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Observer(builder: (_) {
final isInvoice =
receiveOptionViewModel.selectedReceiveOption == ReceivePageOption.anonPayInvoice;
return Column(
children: <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 15),
child: Center(
child: Text(
isInvoice
? S.of(context).anonpay_description("an invoice", "pay")
: S.of(context).anonpay_description("a donation link", "donate"),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).primaryTextTheme.headline1!.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 12),
),
),
),
LoadingPrimaryButton(
text:
isInvoice ? S.of(context).create_invoice : S.of(context).create_donation_link,
onPressed: () {
anonInvoicePageViewModel.setRequestParams(
inputAmount: _amountController.text,
inputName: _nameController.text,
inputEmail: _emailController.text,
inputDescription: _descriptionController.text,
);
if (anonInvoicePageViewModel.receipientEmail.isNotEmpty &&
_formKey.currentState != null &&
!_formKey.currentState!.validate()) {
return;
}
if (isInvoice) {
anonInvoicePageViewModel.createInvoice();
} else {
anonInvoicePageViewModel.generateDonationLink();
}
},
color: Theme.of(context).accentTextTheme.bodyText1!.color!,
textColor: Colors.white,
isLoading: anonInvoicePageViewModel.state is IsExecutingState,
),
],
);
}),
),
),
);
}
void _setReactions(BuildContext context) {
if (effectsInstalled) {
return;
}
reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) {
Navigator.pop(context);
switch (option) {
case ReceivePageOption.mainnet:
Navigator.popAndPushNamed(context, Routes.addressPage);
break;
case ReceivePageOption.anonPayDonationLink:
final sharedPreferences = getIt.get<SharedPreferences>();
final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink);
if (clearnetUrl != null && onionUrl != null) {
Navigator.pushReplacementNamed(context, Routes.anonPayReceivePage,
arguments: AnonpayDonationLinkInfo(
clearnetUrl: clearnetUrl,
onionUrl: onionUrl,
address: anonInvoicePageViewModel.address,
));
}
break;
default:
}
});
reaction((_) => anonInvoicePageViewModel.state, (ExecutionState state) {
if (state is ExecutedSuccessfullyState) {
Navigator.pushNamed(context, Routes.anonPayReceivePage, arguments: state.payload);
}
if (state is FailureState) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: state.error.toString(),
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
});
effectsInstalled = true;
}
}
@@ -0,0 +1,181 @@
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/anonpay_status_section.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/screens/receive/widgets/copy_link_item.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:device_display_brightness/device_display_brightness.dart';
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart' as qr;
class AnonPayReceivePage extends BasePage {
final AnonpayInfoBase invoiceInfo;
AnonPayReceivePage({required this.invoiceInfo});
@override
String get title => S.current.receive;
@override
Color get backgroundLightColor =>
currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white;
@override
Color get backgroundDarkColor => Colors.transparent;
@override
bool get resizeToAvoidBottomInset => false;
@override
Widget leading(BuildContext context) {
final _backButton = Icon(
Icons.arrow_back_ios,
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
size: 16,
);
return SizedBox(
height: 37,
width: 37,
child: ButtonTheme(
minWidth: double.minPositive,
child: TextButton(
onPressed: () =>
Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false),
child: _backButton),
),
);
}
@override
Widget middle(BuildContext context) {
return Column(
children: [
Text(
title,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
fontFamily: 'Lato',
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!),
),
Text(
invoiceInfo is AnonpayInvoiceInfo
? ReceivePageOption.anonPayInvoice.toString()
: ReceivePageOption.anonPayDonationLink.toString(),
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.headline5!.color!),
)
],
);
}
@override
Widget? trailing(BuildContext context) {
if (invoiceInfo is AnonpayInvoiceInfo) {
return null;
}
return Material(
color: Colors.transparent,
child: IconButton(
onPressed: () => Navigator.popAndPushNamed(
context,
Routes.anonPayInvoicePage,
arguments: [invoiceInfo.address, ReceivePageOption.anonPayDonationLink],
),
icon: Icon(
Icons.edit,
color: Theme.of(context).accentTextTheme.caption!.color!,
size: 22.0,
),
),
);
}
@override
Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) => Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [
Theme.of(context).accentColor,
Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).primaryColor,
], begin: Alignment.topRight, end: Alignment.bottomLeft)),
child: scaffold);
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: 24),
if (invoiceInfo is AnonpayInvoiceInfo)
AnonInvoiceStatusSection(invoiceInfo: invoiceInfo as AnonpayInvoiceInfo),
Padding(
padding: EdgeInsets.fromLTRB(24, 50, 24, 24),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.5,
),
child: GestureDetector(
onTap: () async {
final double brightness = await DeviceDisplayBrightness.getBrightness();
// ignore: unawaited_futures
DeviceDisplayBrightness.setBrightness(1.0);
await Navigator.pushNamed(
context,
Routes.fullscreenQR,
arguments: {
'qrData': invoiceInfo.clearnetUrl,
'version': qr.QrVersions.auto,
},
);
// ignore: unawaited_futures
DeviceDisplayBrightness.setBrightness(brightness);
},
child: Hero(
tag: Key(invoiceInfo.clearnetUrl),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!,
),
),
child: QrImage(
data: invoiceInfo.clearnetUrl,
version: qr.QrVersions.auto,
),
),
),
),
),
),
),
),
SizedBox(height: 24),
Column(
children: [
CopyLinkItem(url: invoiceInfo.clearnetUrl, title: S.of(context).clearnet_link),
SizedBox(height: 16),
CopyLinkItem(url: invoiceInfo.onionUrl, title: S.of(context).onion_link),
],
),
SizedBox(height: 100),
],
),
);
}
}
@@ -4,10 +4,10 @@ import 'package:flutter/material.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class FullscreenQRPage extends BasePage {
FullscreenQRPage({required this.qrData, required this.isLight});
FullscreenQRPage({required this.qrData, int? this.version});
final bool isLight;
final String qrData;
final int? version;
@override
Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white;
@@ -71,7 +71,7 @@ class FullscreenQRPage extends BasePage {
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(width: 3, color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!)),
child: QrImage(data: qrData),
child: QrImage(data: qrData, version: version),
),
),
),
@@ -0,0 +1,153 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class AnonpayCurrencyInputField extends StatelessWidget {
const AnonpayCurrencyInputField(
{super.key,
required this.onTapPicker,
required this.selectedCurrency,
required this.focusNode,
required this.controller,
required this.minAmount,
required this.maxAmount});
final Function() onTapPicker;
final Currency selectedCurrency;
final FocusNode focusNode;
final TextEditingController controller;
final String minAmount;
final String maxAmount;
@override
Widget build(BuildContext context) {
final arrowBottomPurple = Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Colors.white,
height: 8,
);
return Column(
children: [
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!,
width: 1)),
),
child: Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
children: [
Container(
padding: EdgeInsets.only(right: 8),
height: 32,
child: InkWell(
onTap: onTapPicker,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.only(right: 5),
child: arrowBottomPurple,
),
Text(selectedCurrency.name.toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white))
]),
),
),
selectedCurrency.tag != null
? Padding(
padding: const EdgeInsets.only(right: 3.0),
child: Container(
height: 32,
decoration: BoxDecoration(
color: Theme.of(context).primaryTextTheme.headline4!.color!,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Center(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(
selectedCurrency.tag!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.headline4!
.decorationColor!,
),
),
),
),
),
)
: Container(),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Text(':',
style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white)),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: BaseTextFormField(
focusNode: focusNode,
controller: controller,
textInputAction: TextInputAction.next,
enabled: true,
textAlign: TextAlign.left,
keyboardType:
TextInputType.numberWithOptions(signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))
],
hintText: '0.0000',
borderColor: Colors.transparent,
//widget.borderColor,
textStyle: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
placeholderTextStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).accentTextTheme.headline1!.decorationColor!,
),
validator: null,
),
),
],
),
),
],
),
)),
Container(
height: 15,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).min_value(minAmount, selectedCurrency.toString()),
style: TextStyle(
fontSize: 10,
height: 1.2,
color: Theme.of(context).accentTextTheme.headline1!.decorationColor!),
),
SizedBox(width: 10),
Text(S.of(context).max_value(maxAmount, selectedCurrency.toString()),
style: TextStyle(
fontSize: 10,
height: 1.2,
color: Theme.of(context).accentTextTheme.headline1!.decorationColor!)),
],
),
)
],
);
}
}
@@ -0,0 +1,132 @@
import 'package:cake_wallet/core/email_validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/screens/receive/widgets/anonpay_currency_input_field.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class AnonInvoiceForm extends StatelessWidget {
AnonInvoiceForm({
super.key,
required this.formKey,
required this.anonInvoicePageViewModel,
required this.isInvoice,
required this.amountController,
required this.nameController,
required this.emailController,
required this.descriptionController,
required this.depositAmountFocus,
}) : _nameFocusNode = FocusNode(),
_emailFocusNode = FocusNode(),
_descriptionFocusNode = FocusNode();
final TextEditingController amountController;
final TextEditingController nameController;
final TextEditingController emailController;
final TextEditingController descriptionController;
final AnonInvoicePageViewModel anonInvoicePageViewModel;
final FocusNode depositAmountFocus;
final FocusNode _nameFocusNode;
final FocusNode _emailFocusNode;
final FocusNode _descriptionFocusNode;
final GlobalKey<FormState> formKey;
final bool isInvoice;
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
isInvoice ? S.of(context).invoice_details : S.of(context).donation_link_details,
style: textMediumSemiBold(),
),
if (isInvoice)
Observer(builder: (_) {
return AnonpayCurrencyInputField(
onTapPicker: () => _presentPicker(context),
controller: amountController,
focusNode: depositAmountFocus,
maxAmount: anonInvoicePageViewModel.maximum?.toString() ?? '...',
minAmount: anonInvoicePageViewModel.minimum?.toString() ?? '...',
selectedCurrency: anonInvoicePageViewModel.selectedCurrency,
);
}),
SizedBox(
height: 24,
),
BaseTextFormField(
controller: nameController,
focusNode: _nameFocusNode,
borderColor: Theme.of(context).accentTextTheme.headline6!.backgroundColor,
suffixIcon: SizedBox(width: 36),
hintText: S.of(context).optional_name,
textInputAction: TextInputAction.next,
placeholderTextStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).accentTextTheme.headline1!.decorationColor!,
),
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
validator: null,
),
SizedBox(
height: 24,
),
BaseTextFormField(
controller: descriptionController,
focusNode: _descriptionFocusNode,
textInputAction: TextInputAction.next,
borderColor: Theme.of(context).accentTextTheme.headline6!.backgroundColor,
suffixIcon: SizedBox(width: 36),
hintText: S.of(context).optional_description,
placeholderTextStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).accentTextTheme.headline1!.decorationColor!,
),
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
validator: null,
),
SizedBox(height: 24),
BaseTextFormField(
controller: emailController,
textInputAction: TextInputAction.next,
focusNode: _emailFocusNode,
borderColor: Theme.of(context).accentTextTheme.headline6!.backgroundColor,
suffixIcon: SizedBox(width: 36),
keyboardType: TextInputType.emailAddress,
hintText: S.of(context).optional_email_hint,
placeholderTextStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).accentTextTheme.headline1!.decorationColor!,
),
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
validator: EmailValidator(),
),
SizedBox(
height: 52,
),
],
));
}
void _presentPicker(BuildContext context) {
showPopUp<void>(
builder: (_) => CurrencyPicker(
selectedAtIndex: anonInvoicePageViewModel.selectedCurrencyIndex,
items: anonInvoicePageViewModel.currencies,
hintText: S.of(context).search_currency,
onItemSelected: anonInvoicePageViewModel.selectCurrency,
),
context: context,
);
}
}
@@ -0,0 +1,87 @@
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
class AnonInvoiceStatusSection extends StatelessWidget {
const AnonInvoiceStatusSection({
super.key,
required this.invoiceInfo,
});
final AnonpayInvoiceInfo invoiceInfo;
@override
Widget build(BuildContext context) {
return Container(
width: 200,
padding: EdgeInsets.all(19),
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
borderRadius: BorderRadius.circular(30),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.current.status,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).primaryTextTheme.headline1!.decorationColor!,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Theme.of(context).accentTextTheme.headline3!.color!,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SyncIndicatorIcon(
boolMode: false,
value: invoiceInfo.status ?? '',
size: 6,
),
SizedBox(width: 5),
Text(
invoiceInfo.status ?? '',
style: textSmallSemiBold(
color: Theme.of(context).primaryTextTheme.headline6!.color,
),
)
],
),
)
],
),
SizedBox(height: 27),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).primaryTextTheme.headline1!.decorationColor!,
),
),
Text(
invoiceInfo.invoiceId ?? '',
style: textSmallSemiBold(
color: Theme.of(context).primaryTextTheme.headline6!.color,
),
),
],
),
],
),
);
}
}
@@ -0,0 +1,56 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
class CopyLinkItem extends StatelessWidget {
const CopyLinkItem({super.key, required this.url, required this.title});
final String url;
final String title;
@override
Widget build(BuildContext context) {
final copyImage = Image.asset('assets/images/copy_address.png',
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: textMedium(
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
),
),
SizedBox(width: 50),
Row(
children: [
InkWell(
onTap: () {
Clipboard.setData(ClipboardData(text: url));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
child: copyImage,
),
SizedBox(width: 20),
IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
iconSize: 25,
onPressed: () => Share.share(url),
icon: Icon(
Icons.share,
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
),
)
],
)
],
);
}
}
@@ -5,13 +5,13 @@ class QrImage extends StatelessWidget {
QrImage({
required this.data,
this.size = 100.0,
this.version = 9, // Previous value: 7 something happened after flutter upgrade monero wallets addresses are longer than ver. 7 ???
this.version,
this.errorCorrectionLevel = qr.QrErrorCorrectLevel.L,
});
final double size;
final String data;
final int version;
final int? version;
final int errorCorrectionLevel;
@override
@@ -19,7 +19,7 @@ class QrImage extends StatelessWidget {
return qr.QrImage(
data: data,
errorCorrectionLevel: errorCorrectionLevel,
version: version,
version: version ?? 9, // Previous value: 7 something happened after flutter upgrade monero wallets addresses are longer than ver. 7 ???
size: size,
foregroundColor: Colors.black,
backgroundColor: Colors.white,
+71 -66
View File
@@ -3,7 +3,6 @@ import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:device_display_brightness/device_display_brightness.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -16,11 +15,12 @@ class QRWidget extends StatelessWidget {
QRWidget(
{required this.addressListViewModel,
required this.isLight,
this.qrVersion,
this.isAmountFieldShow = false,
this.amountTextFieldFocusNode})
: amountController = TextEditingController(),
_formKey = GlobalKey<FormState>() {
amountController.addListener(() => addressListViewModel.amount =
amountController.addListener(() => addressListViewModel?.amount =
_formKey.currentState!.validate() ? amountController.text : '');
}
@@ -30,6 +30,7 @@ class QRWidget extends StatelessWidget {
final FocusNode? amountTextFieldFocusNode;
final GlobalKey<FormState> _formKey;
final bool isLight;
final int? qrVersion;
@override
Widget build(BuildContext context) {
@@ -57,46 +58,48 @@ class QRWidget extends StatelessWidget {
children: <Widget>[
Spacer(flex: 3),
Observer(
builder: (_) => Flexible(
flex: 5,
child: GestureDetector(
onTap: () async {
// Get the current brightness:
final double brightness = await DeviceDisplayBrightness.getBrightness();
builder: (_) {
return Flexible(
flex: 5,
child: GestureDetector(
onTap: () async {
// Get the current brightness:
final double brightness = await DeviceDisplayBrightness.getBrightness();
// ignore: unawaited_futures
DeviceDisplayBrightness.setBrightness(1.0);
await Navigator.pushNamed(
context,
Routes.fullscreenQR,
arguments: {
'qrData': addressListViewModel.uri.toString(),
'isLight': isLight,
},
);
// ignore: unawaited_futures
DeviceDisplayBrightness.setBrightness(brightness);
},
child: Hero(
tag: Key(addressListViewModel.uri.toString()),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!,
// ignore: unawaited_futures
DeviceDisplayBrightness.setBrightness(1.0);
await Navigator.pushNamed(
context,
Routes.fullscreenQR,
arguments: {
'qrData': addressListViewModel.uri.toString(),
},
);
// ignore: unawaited_futures
DeviceDisplayBrightness.setBrightness(brightness);
},
child: Hero(
tag: Key(addressListViewModel.uri.toString()),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color:
Theme.of(context).accentTextTheme!.headline2!.backgroundColor!,
),
),
child: QrImage(data: addressListViewModel.uri.toString(), version: qrVersion),
),
child: QrImage(data: addressListViewModel.uri.toString()),
),
),
),
),
),
),
);
}
),
Spacer(flex: 3)
],
@@ -120,8 +123,9 @@ class QRWidget extends StatelessWidget {
hintText: S.of(context).receive_amount,
textColor: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!,
borderColor: Theme.of(context).textTheme!.headline5!.decorationColor!,
validator: AmountValidator(currency:
walletTypeToCryptoCurrency(addressListViewModel.type), isAutovalidate: true),
validator: AmountValidator(
currency: walletTypeToCryptoCurrency(addressListViewModel!.type),
isAutovalidate: true),
// FIX-ME: Check does it equal to autovalidate: true,
autovalidateMode: AutovalidateMode.always,
placeholderTextStyle: TextStyle(
@@ -135,39 +139,40 @@ class QRWidget extends StatelessWidget {
],
),
),
Padding(
padding: EdgeInsets.only(top: 8, bottom: 8),
child: Builder(
builder: (context) => Observer(
builder: (context) => GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: addressListViewModel.address.address));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: Text(
addressListViewModel.address.address,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!),
Padding(
padding: EdgeInsets.only(top: 8, bottom: 8),
child: Builder(
builder: (context) => Observer(
builder: (context) => GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: addressListViewModel!.address.address));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: Text(
addressListViewModel!.address.address,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color:
Theme.of(context).accentTextTheme!.headline2!.backgroundColor!),
),
),
),
Padding(
padding: EdgeInsets.only(left: 12),
child: copyImage,
)
],
Padding(
padding: EdgeInsets.only(left: 12),
child: copyImage,
)
],
),
),
),
),
),
)
)
],
);
}
@@ -33,7 +33,6 @@ class WalletKeysPage extends BasePage {
Routes.fullscreenQR,
arguments: {
'qrData': (await walletKeysViewModel.url).toString(),
'isLight': true,
},
);
// ignore: unawaited_futures
@@ -0,0 +1,34 @@
import 'dart:async';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
part 'anonpay_transactions_store.g.dart';
class AnonpayTransactionsStore = AnonpayTransactionsStoreBase with _$AnonpayTransactionsStore;
abstract class AnonpayTransactionsStoreBase with Store {
AnonpayTransactionsStoreBase({
required this.anonpayInvoiceInfoSource,
}) : transactions = <AnonpayTransactionListItem>[] {
anonpayInvoiceInfoSource.watch().listen(
(_) async => await updateTransactionList(),
);
updateTransactionList();
}
Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource;
@observable
List<AnonpayTransactionListItem> transactions;
@action
Future<void> updateTransactionList() async {
transactions = anonpayInvoiceInfoSource.values
.map(
(transaction) => AnonpayTransactionListItem(transaction: transaction),
)
.toList();
}
}
@@ -1,8 +1,8 @@
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
import 'package:cake_wallet/generated/i18n.dart';
part 'transaction_filter_store.g.dart';
@@ -57,8 +57,8 @@ abstract class TransactionFilterStoreBase with Store {
@action
void changeEndDate(DateTime date) => endDate = date;
List<TransactionListItem> filtered({required List<TransactionListItem> transactions}) {
var _transactions = <TransactionListItem>[];
List<ActionListItem> filtered({required List<ActionListItem> transactions}) {
var _transactions = <ActionListItem>[];
final needToFilter = !displayAll ||
(startDate != null && endDate != null);
@@ -67,16 +67,26 @@ abstract class TransactionFilterStoreBase with Store {
var allowed = true;
if (allowed && startDate != null && endDate != null) {
if(item is TransactionListItem){
allowed = (startDate?.isBefore(item.transaction.date) ?? false)
&& (endDate?.isAfter(item.transaction.date) ?? false);
}else if(item is AnonpayTransactionListItem){
allowed = (startDate?.isBefore(item.transaction.createdAt) ?? false)
&& (endDate?.isAfter(item.transaction.createdAt) ?? false);
}
}
if (allowed && (!displayAll)) {
if(item is TransactionListItem){
allowed = (displayOutgoing &&
item.transaction.direction ==
TransactionDirection.outgoing) ||
(displayIncoming &&
item.transaction.direction == TransactionDirection.incoming);
} else if(item is AnonpayTransactionListItem){
allowed = displayIncoming;
}
}
return allowed;
@@ -0,0 +1,186 @@
import 'package:cake_wallet/anonpay/anonpay_api.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/anonpay/anonpay_request.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'anon_invoice_page_view_model.g.dart';
class AnonInvoicePageViewModel = AnonInvoicePageViewModelBase with _$AnonInvoicePageViewModel;
abstract class AnonInvoicePageViewModelBase with Store {
AnonInvoicePageViewModelBase(
this.anonPayApi,
this.address,
this.settingsStore,
this._wallet,
this._anonpayInvoiceInfoSource,
this.sharedPreferences,
this.pageOption,
) : receipientEmail = '',
receipientName = '',
description = '',
amount = '',
state = InitialExecutionState(),
selectedCurrency = walletTypeToCryptoCurrency(_wallet.type),
cryptoCurrency = walletTypeToCryptoCurrency(_wallet.type) {
_getPreviousDonationLink();
_fetchLimits();
}
List<Currency> get currencies => [walletTypeToCryptoCurrency(_wallet.type), ...FiatCurrency.all];
final AnonPayApi anonPayApi;
final String address;
final SettingsStore settingsStore;
final WalletBase _wallet;
final Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
final SharedPreferences sharedPreferences;
final ReceivePageOption pageOption;
@observable
Currency selectedCurrency;
CryptoCurrency cryptoCurrency;
@observable
String receipientEmail;
@observable
String receipientName;
@observable
String description;
@observable
String amount;
@observable
ExecutionState state;
@computed
int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency);
@observable
double? minimum;
@observable
double? maximum;
@action
void selectCurrency(Currency currency) {
selectedCurrency = currency;
maximum = minimum = null;
if (currency is CryptoCurrency) {
cryptoCurrency = currency;
} else {
cryptoCurrency = walletTypeToCryptoCurrency(_wallet.type);
}
_fetchLimits();
}
@action
Future<void> createInvoice() async {
state = IsExecutingState();
if (amount.isNotEmpty) {
final amountInCrypto = double.parse(amount);
if (minimum != null && amountInCrypto < minimum!) {
state = FailureState('Amount is too small');
return;
}
if (maximum != null && amountInCrypto > maximum!) {
state = FailureState('Amount is too big');
return;
}
}
final result = await anonPayApi.createInvoice(AnonPayRequest(
cryptoCurrency: cryptoCurrency,
address: address,
amount: amount.isEmpty ? null : amount,
description: description,
email: receipientEmail,
name: receipientName,
fiatEquivalent:
selectedCurrency is FiatCurrency ? (selectedCurrency as FiatCurrency).raw : null,
));
_anonpayInvoiceInfoSource.add(result);
state = ExecutedSuccessfullyState(payload: result);
}
@action
void setRequestParams({
required String inputAmount,
required String inputName,
required String inputEmail,
required String inputDescription,
}) {
receipientName = inputName;
receipientEmail = inputEmail;
description = inputDescription;
amount = inputAmount;
}
@action
Future<void> generateDonationLink() async {
state = IsExecutingState();
final result = await anonPayApi.generateDonationLink(AnonPayRequest(
cryptoCurrency: cryptoCurrency,
address: address,
description: description,
email: receipientEmail,
name: receipientName,
));
await sharedPreferences.setString(PreferencesKey.clearnetDonationLink, result.clearnetUrl);
await sharedPreferences.setString(PreferencesKey.onionDonationLink, result.onionUrl);
state = ExecutedSuccessfullyState(payload: result);
}
Future<void> _fetchLimits() async {
final limit = await anonPayApi.fetchLimits(
cryptoCurrency: cryptoCurrency,
fiatCurrency: selectedCurrency is FiatCurrency ? selectedCurrency as FiatCurrency : null,
);
minimum = limit.min;
maximum = limit.max != null ? limit.max! / 4 : null;
}
@action
void reset() {
selectedCurrency = walletTypeToCryptoCurrency(_wallet.type);
cryptoCurrency = walletTypeToCryptoCurrency(_wallet.type);
receipientEmail = '';
receipientName = '';
description = '';
amount = '';
_fetchLimits();
}
Future<void> _getPreviousDonationLink() async {
if (pageOption == ReceivePageOption.anonPayDonationLink) {
final donationLink = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
if (donationLink != null) {
final url = Uri.parse(donationLink);
url.queryParameters.forEach((key, value) {
if (key == 'name') receipientName = value;
if (key == 'email') receipientEmail = value;
if (key == 'description') description = Uri.decodeComponent(value);
});
}
}
}
}
@@ -0,0 +1,78 @@
import 'dart:async';
import 'package:cake_wallet/anonpay/anonpay_api.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'anonpay_details_view_model.g.dart';
class AnonpayDetailsViewModel = AnonpayDetailsViewModelBase with _$AnonpayDetailsViewModel;
abstract class AnonpayDetailsViewModelBase with Store {
AnonpayDetailsViewModelBase(
{required this.anonPayApi,
required AnonpayInvoiceInfo anonpayInvoiceInfo,
required this.settingsStore})
: items = ObservableList<StandartListItem>(),
invoiceDetail = anonpayInvoiceInfo {
_updateItems();
_updateInvoiceDetail();
timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateInvoiceDetail());
}
final AnonPayApi anonPayApi;
final SettingsStore settingsStore;
final AnonpayInvoiceInfo invoiceDetail;
final ObservableList<StandartListItem> items;
Timer? timer;
@action
Future<void> _updateInvoiceDetail() async {
try {
final data = await anonPayApi.paymentStatus(invoiceDetail.invoiceId);
invoiceDetail.status = data.status;
_updateItems();
} catch (e) {
print(e.toString());
}
}
void _updateItems() {
final dateFormat = DateFormatter.withCurrentLocal();
items.clear();
items.addAll([
DetailsListStatusItem(title: S.current.status, value: invoiceDetail.status),
TradeDetailsListCardItem(
id: invoiceDetail.invoiceId,
createdAt: dateFormat.format(invoiceDetail.createdAt).toString(),
pair: (invoiceDetail.fiatAmount != null)
? "→ ${invoiceDetail.fiatAmount} ${invoiceDetail.fiatEquiv ?? ''}"
: '→ ${invoiceDetail.amountTo ?? ''} ${CryptoCurrency.fromFullName(invoiceDetail.coinTo).name.toUpperCase()}',
onTap: (BuildContext context) {
Clipboard.setData(ClipboardData(text: '${invoiceDetail.invoiceId}'));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
),
StandartListItem(title: S.current.trade_details_provider, value: invoiceDetail.provider)
]);
items.add(TrackTradeListItem(
title: 'Track',
value: invoiceDetail.clearnetStatusUrl,
onTap: () => launchUrlString(invoiceDetail.clearnetStatusUrl)));
}
}
@@ -0,0 +1,11 @@
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
class AnonpayTransactionListItem extends ActionListItem {
AnonpayTransactionListItem({required this.transaction});
final AnonpayInvoiceInfo transaction;
@override
DateTime get date => transaction.createdAt;
}
@@ -1,5 +1,7 @@
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/balance.dart';
@@ -48,7 +50,9 @@ abstract class DashboardViewModelBase with Store {
required this.transactionFilterStore,
required this.settingsStore,
required this.yatStore,
required this.ordersStore})
required this.ordersStore,
required this.anonpayTransactionsStore,
})
: isOutdatedElectrumWallet = false,
hasSellAction = false,
isEnabledSellAction = false,
@@ -227,6 +231,11 @@ abstract class DashboardViewModelBase with Store {
List<OrderListItem> get orders => ordersStore.orders
.where((item) => item.order.walletId == wallet.id)
.toList();
@computed
List<AnonpayTransactionListItem> get anonpayTransactons => anonpayTransactionsStore.transactions
.where((item) => item.transaction.walletId == wallet.id)
.toList();
@computed
double get price => balanceViewModel.price;
@@ -235,7 +244,7 @@ abstract class DashboardViewModelBase with Store {
List<ActionListItem> get items {
final _items = <ActionListItem>[];
_items.addAll(transactionFilterStore.filtered(transactions: transactions));
_items.addAll(transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons]));
_items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
_items.addAll(orders);
@@ -262,6 +271,8 @@ abstract class DashboardViewModelBase with Store {
TradeFilterStore tradeFilterStore;
AnonpayTransactionsStore anonpayTransactionsStore;
TransactionFilterStore transactionFilterStore;
Map<String, List<FilterItem>> filterItems;
@@ -0,0 +1,34 @@
import 'package:cake_wallet/entities/receive_page_option.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
part 'receive_option_view_model.g.dart';
class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionViewModel;
abstract class ReceiveOptionViewModelBase with Store {
ReceiveOptionViewModelBase(this._wallet, this.initialPageOption)
: selectedReceiveOption = initialPageOption ?? ReceivePageOption.mainnet,
_options = [] {
final walletType = _wallet.type;
_options =
walletType == WalletType.haven ? [ReceivePageOption.mainnet] : ReceivePageOption.values;
}
final WalletBase _wallet;
final ReceivePageOption? initialPageOption;
List<ReceivePageOption> _options;
@observable
ReceivePageOption selectedReceiveOption;
List<ReceivePageOption> get options => _options;
@action
void selectReceiveOption(ReceivePageOption option) {
selectedReceiveOption = option;
}
}