* Initial ionia service

* Ionia manage card UI (#374)

* design ui for cakepay

* Add manage cards page ui

* create auth ui for ionia

* add authentication logic

* implement user create card

* Add ionia merchant sevic

* Add anypay. Add purschase gift card.

* display virtual card (#385)

* display virtual card

* fix formatting

* Remove IoniaMerchantService from IoniaViewModel

* Add hex and txKey for monero pending transaction.

* Changed monero version and monero repo to cake tech.

* Add anypay payment. Add filter by search for ionia, add get purchased items for ionia.

* Fix for get transactions for hidden addresses for electrum wallet

* Add ionia categories.

* Add anypay commited info for payments.

* Update UI with new fixes (#400)

* Change ionia base url. Add exception throwing for error messaging for some of ionia calls.

* CW-102 fix logic for ionia issues (#403)

* refactor tips (#406)

* refactor tips

* refactor ionia tips implementation

* Cw 115 implement gift cards list for ionia (#405)

* Implement show purchased cards

* fix padding

* Fixes for getting of purchased gift cards.

* Implement gift card details screen (#408)

* Implement gift card details screen

* Add redeem for ionia gift cards

* Fix navigation after ionia opt redirection.

* Fix update gift cards list.

* Add payment status update for ionia.

* Add usage instruction to gift card.

* Add copy for ionia gift card info.

* Change version for Cake Wallet ios.

* Add localisation (#414)

* Fixes for fiat amounts for ionia.

* CW-128 marketplace screen text changes (#416)

* Change text on marketplace

* fix build issues

* fix build

* UI fixes for ionia.

* UI fixes for ionia. (#421)

* CW-129 ionia welcome screen text changes (#418)

* update welcome text

* Update localization

* Cw 133 (#422)

* UI fixes for ionia.

* Fixes for display card item on gift cards screen.

* Fix signup page (#419)

* Changed tips for ionia.

* Cw 132 (#425)

* UI fixes for ionia.

* Changed tips for ionia.

* Cw 131 (#426)

* UI fixes for ionia.

* Changed tips for ionia.

* Fixes for IoniaBuyGiftCardDetailPage screen. Renamed 'Manage Cards' to 'Gift Cards'. Hide discount badge label for 0 discount.

* Change ionia heading font style (#427)

* Fix for AddressResolver in di

* Changed build number for Cake Wallet ios.

* fix currency format for card details and routing for mark as redeemed (#431)

* fix terms and condition overflow in ionia (#430)

* fix terms and condition scroll

* fix color issues

* reuse

* refactor  widget

* Remove IoniaTokenService

* Change api for ionia to staging

* Update versions for Cake Wallet for android and ios.

* Fixes for instructions. Remove diplay error on payment status screen.

* Change build versions for Cake Wallet

* Add ionia sign in.

* Update for discounts and statuses for ionia merch.

* Fixes for qr/barcode on ionia gift card screen.

* Fixed formatting for display ionia discounts.

* Fix merchant.discount.toStringAsFixed issue

* Add savingsPercentage to ionia merch discount.

* Change build number for Cake Wallet ios and android.

* Disable ionia for haven (#440)

Co-authored-by: Godwin Asuquo <41484542+godilite@users.noreply.github.com>
This commit is contained in:
mkyq
2022-07-28 18:03:16 +01:00
committed by GitHub
co-authored by Godwin Asuquo
parent b72443a8c4
commit 418c9563fe
115 changed files with 7726 additions and 184 deletions
+5
View File
@@ -0,0 +1,5 @@
class AnyPayChain {
static const xmr = 'XMR';
static const btc = 'BTC';
static const ltc = 'LTC';
}
+64
View File
@@ -0,0 +1,64 @@
import 'package:cake_wallet/anypay/any_pay_chain.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_core/monero_amount_format.dart';
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart';
class AnyPayPayment {
AnyPayPayment({
@required this.time,
@required this.expires,
@required this.memo,
@required this.paymentUrl,
@required this.paymentId,
@required this.chain,
@required this.network,
@required this.instructions});
factory AnyPayPayment.fromMap(Map<String, dynamic> obj) {
final instructions = (obj['instructions'] as List<dynamic>)
.map((dynamic instruction) => AnyPayPaymentInstruction.fromMap(instruction as Map<String, dynamic>))
.toList();
return AnyPayPayment(
time: DateTime.parse(obj['time'] as String),
expires: DateTime.parse(obj['expires'] as String),
memo: obj['memo'] as String,
paymentUrl: obj['paymentUrl'] as String,
paymentId: obj['paymentId'] as String,
chain: obj['chain'] as String,
network: obj['network'] as String,
instructions: instructions);
}
final DateTime time;
final DateTime expires;
final String memo;
final String paymentUrl;
final String paymentId;
final String chain;
final String network;
final List<AnyPayPaymentInstruction> instructions;
String get totalAmount {
final total = instructions
.fold<int>(0, (int acc, instruction) => acc + instruction.outputs
.fold<int>(0, (int outAcc, out) => outAcc + out.amount));
switch (chain) {
case AnyPayChain.xmr:
return moneroAmountToString(amount: total);
case AnyPayChain.btc:
return bitcoinAmountToString(amount: total);
case AnyPayChain.ltc:
return bitcoinAmountToString(amount: total);
default:
return null;
}
}
List<String> get outAddresses {
return instructions
.map((instuction) => instuction.outputs.map((out) => out.address))
.expand((e) => e)
.toList();
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/anypay/any_pay_trasnaction.dart';
class AnyPayPaymentCommittedInfo {
const AnyPayPaymentCommittedInfo({
@required this.uri,
@required this.currency,
@required this.chain,
@required this.transactions,
@required this.memo});
final String uri;
final String currency;
final String chain;
final List<AnyPayTransaction> transactions;
final String memo;
}
@@ -0,0 +1,32 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/anypay/any_pay_payment_instruction_output.dart';
class AnyPayPaymentInstruction {
AnyPayPaymentInstruction({
@required this.type,
@required this.requiredFeeRate,
@required this.txKey,
@required this.txHash,
@required this.outputs});
factory AnyPayPaymentInstruction.fromMap(Map<String, dynamic> obj) {
final outputs = (obj['outputs'] as List<dynamic>)
.map((dynamic out) =>
AnyPayPaymentInstructionOutput.fromMap(out as Map<String, dynamic>))
.toList();
return AnyPayPaymentInstruction(
type: obj['type'] as String,
requiredFeeRate: obj['requiredFeeRate'] as int,
txKey: obj['tx_key'] as bool,
txHash: obj['tx_hash'] as bool,
outputs: outputs);
}
static const transactionType = 'transaction';
final String type;
final int requiredFeeRate;
final bool txKey;
final bool txHash;
final List<AnyPayPaymentInstructionOutput> outputs;
}
@@ -0,0 +1,10 @@
class AnyPayPaymentInstructionOutput {
const AnyPayPaymentInstructionOutput(this.address, this.amount);
factory AnyPayPaymentInstructionOutput.fromMap(Map<String, dynamic> obj) {
return AnyPayPaymentInstructionOutput(obj['address'] as String, obj['amount'] as int);
}
final String address;
final int amount;
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:flutter/foundation.dart';
class AnyPayTransaction {
const AnyPayTransaction(this.tx, {@required this.id, @required this.key});
final String tx;
final String id;
final String key;
}
+92
View File
@@ -0,0 +1,92 @@
import 'dart:convert';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/anypay/any_pay_payment.dart';
import 'package:cake_wallet/anypay/any_pay_trasnaction.dart';
class AnyPayApi {
static const contentTypePaymentRequest = 'application/payment-request';
static const contentTypePayment = 'application/payment';
static const xPayproVersion = '2';
static String chainByScheme(String scheme) {
switch (scheme.toLowerCase()) {
case 'monero':
return CryptoCurrency.xmr.title;
case 'bitcoin':
return CryptoCurrency.btc.title;
case 'litecoin':
return CryptoCurrency.ltc.title;
default:
return '';
}
}
static CryptoCurrency currencyByScheme(String scheme) {
switch (scheme.toLowerCase()) {
case 'monero':
return CryptoCurrency.xmr;
case 'bitcoin':
return CryptoCurrency.btc;
case 'litecoin':
return CryptoCurrency.ltc;
default:
return null;
}
}
Future<AnyPayPayment> paymentRequest(String uri) async {
final fragments = uri.split(':?r=');
final scheme = fragments.first;
final url = fragments[1];
final headers = <String, String>{
'Content-Type': contentTypePaymentRequest,
'X-Paypro-Version': xPayproVersion,
'Accept': '*/*',};
final body = <String, dynamic>{
'chain': chainByScheme(scheme),
'currency': currencyByScheme(scheme).title};
final response = await post(url, headers: headers, body: utf8.encode(json.encode(body)));
if (response.statusCode != 200) {
return null;
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
return AnyPayPayment.fromMap(decodedBody);
}
Future<AnyPayPaymentCommittedInfo> payment(
String uri,
{@required String chain,
@required String currency,
@required List<AnyPayTransaction> transactions}) async {
final headers = <String, String>{
'Content-Type': contentTypePayment,
'X-Paypro-Version': xPayproVersion,
'Accept': '*/*',};
final body = <String, dynamic>{
'chain': chain,
'currency': currency,
'transactions': transactions.map((tx) => {'tx': tx.tx, 'tx_hash': tx.id, 'tx_key': tx.key}).toList()};
final response = await post(uri, headers: headers, body: utf8.encode(json.encode(body)));
if (response.statusCode == 400) {
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
throw Exception(decodedBody['message'] as String);
}
if (response.statusCode != 200) {
throw Exception('Unexpected response');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
return AnyPayPaymentCommittedInfo(
uri: uri,
currency: currency,
chain: chain,
transactions: transactions,
memo: decodedBody['memo'] as String);
}
}
+10 -2
View File
@@ -55,7 +55,7 @@ class CWBitcoin extends Bitcoin {
}
@override
Object createBitcoinTransactionCredentials(List<Output> outputs, TransactionPriority priority)
Object createBitcoinTransactionCredentials(List<Output> outputs, {TransactionPriority priority, int feeRate})
=> BitcoinTransactionCredentials(
outputs.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
@@ -67,7 +67,15 @@ class CWBitcoin extends Bitcoin {
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
.toList(),
priority as BitcoinTransactionPriority);
priority: priority != null ? priority as BitcoinTransactionPriority : null,
feeRate: feeRate);
@override
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs, {TransactionPriority priority, int feeRate})
=> BitcoinTransactionCredentials(
outputs,
priority: priority != null ? priority as BitcoinTransactionPriority : null,
feeRate: feeRate);
@override
List<String> getAddresses(Object wallet) {
+11
View File
@@ -0,0 +1,11 @@
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
class EmailValidator extends TextValidator {
EmailValidator()
: super(
errorMessage: 'Invalid email address',
pattern:
'^[^@]+@[^@]+\.[^@]+',
);
}
+121 -9
View File
@@ -1,10 +1,27 @@
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/wake_lock.dart';
import 'package:cake_wallet/ionia/ionia_anypay.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.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_filter_view_model.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:cake_wallet/ionia/ionia_api.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart';
import 'package:cake_wallet/src/screens/ionia/ionia.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart';
import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cake_wallet/core/backup_service.dart';
import 'package:cw_core/wallet_service.dart';
@@ -100,6 +117,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_restore_view_model.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart';
@@ -123,6 +141,12 @@ import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart';
import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart';
import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
@@ -261,7 +285,6 @@ Future setup(
fiatConvertationStore: getIt.get<FiatConversionStore>()));
getIt.registerFactory(() => DashboardViewModel(
balanceViewModel: getIt.get<BalanceViewModel>(),
appStore: getIt.get<AppStore>(),
tradesStore: getIt.get<TradesStore>(),
@@ -560,10 +583,6 @@ Future setup(
getIt.registerFactory(() => BackupPage(getIt.get<BackupViewModel>()));
getIt.registerFactory(() => EditBackupPasswordViewModel(
getIt.get<FlutterSecureStorage>(), getIt.get<SecretStore>())
..init());
getIt.registerFactory(
() => EditBackupPasswordPage(getIt.get<EditBackupPasswordViewModel>()));
@@ -596,10 +615,7 @@ Future setup(
final url = args.first as String;
final buyViewModel = args[1] as BuyViewModel;
return BuyWebViewPage(
buyViewModel: buyViewModel,
ordersStore: getIt.get<OrdersStore>(),
url: url);
return BuyWebViewPage(buyViewModel: buyViewModel, ordersStore: getIt.get<OrdersStore>(), url: url);
});
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) {
@@ -649,6 +665,102 @@ Future setup(
getIt.registerFactoryParam<FullscreenQRPage, String, bool>(
(String qrData, bool isLight) => FullscreenQRPage(qrData: qrData, isLight: isLight,));
getIt.registerFactory(() => IoniaApi());
getIt.registerFactory(() => AnyPayApi());
getIt.registerFactory<IoniaService>(
() => IoniaService(getIt.get<FlutterSecureStorage>(), getIt.get<IoniaApi>()));
getIt.registerFactory<IoniaAnyPay>(
() => IoniaAnyPay(
getIt.get<IoniaService>(),
getIt.get<AnyPayApi>(),
getIt.get<AppStore>().wallet));
getIt.registerFactory<IoniaFilterViewModel>(() => IoniaFilterViewModel());
getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get<IoniaService>()));
getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get<IoniaService>()));
getIt.registerFactoryParam<IoniaMerchPurchaseViewModel, double, IoniaMerchant>((double amount, merchant) {
return IoniaMerchPurchaseViewModel(
ioniaAnyPayService: getIt.get<IoniaAnyPay>(),
amount: amount,
ioniaMerchant: merchant,
);
});
getIt.registerFactoryParam<IoniaBuyCardViewModel, IoniaMerchant, void>((IoniaMerchant merchant, _) {
return IoniaBuyCardViewModel(ioniaMerchant: merchant);
});
getIt.registerFactory(() => IoniaAccountViewModel(ioniaService: getIt.get<IoniaService>()));
getIt.registerFactory(() => IoniaCreateAccountPage(getIt.get<IoniaAuthViewModel>()));
getIt.registerFactory(() => IoniaLoginPage(getIt.get<IoniaAuthViewModel>()));
getIt.registerFactoryParam<IoniaVerifyIoniaOtp, List, void>((List args, _) {
final email = args.first as String;
final isSignIn = args[1] as bool;
return IoniaVerifyIoniaOtp(getIt.get<IoniaAuthViewModel>(), email, isSignIn);
});
getIt.registerFactory(() => IoniaWelcomePage(getIt.get<IoniaGiftCardsListViewModel>()));
getIt.registerFactoryParam<IoniaBuyGiftCardPage, List, void>((List args, _) {
final merchant = args.first as IoniaMerchant;
return IoniaBuyGiftCardPage(getIt.get<IoniaBuyCardViewModel>(param1: merchant));
});
getIt.registerFactoryParam<IoniaBuyGiftCardDetailPage, List, void>((List args, _) {
final amount = args.first as double;
final merchant = args.last as IoniaMerchant;
return IoniaBuyGiftCardDetailPage(getIt.get<IoniaMerchPurchaseViewModel>(param1: amount, param2: merchant));
});
getIt.registerFactoryParam<IoniaGiftCardDetailsViewModel, IoniaGiftCard, void>((IoniaGiftCard giftCard, _) {
return IoniaGiftCardDetailsViewModel(
ioniaService: getIt.get<IoniaService>(),
giftCard: giftCard);
});
getIt.registerFactoryParam<IoniaGiftCardDetailPage, IoniaGiftCard, void>((IoniaGiftCard giftCard, _) {
return IoniaGiftCardDetailPage(getIt.get<IoniaGiftCardDetailsViewModel>(param1: giftCard));
});
getIt.registerFactoryParam<IoniaCustomTipPage, List, void>((List args, _) {
final amount = args.first as String;
final merchant = args.last as IoniaMerchant;
return IoniaCustomTipPage(getIt.get<IoniaMerchPurchaseViewModel>(param1: amount, param2: merchant));
});
getIt.registerFactory(() => IoniaManageCardsPage(getIt.get<IoniaGiftCardsListViewModel>()));
getIt.registerFactory(() => IoniaDebitCardPage(getIt.get<IoniaGiftCardsListViewModel>()));
getIt.registerFactory(() => IoniaActivateDebitCardPage(getIt.get<IoniaGiftCardsListViewModel>()));
getIt.registerFactory(() => IoniaAccountPage(getIt.get<IoniaAccountViewModel>()));
getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get<IoniaAccountViewModel>()));
getIt.registerFactoryParam<IoniaPaymentStatusViewModel, IoniaAnyPayPaymentInfo, AnyPayPaymentCommittedInfo>(
(IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo)
=> IoniaPaymentStatusViewModel(
getIt.get<IoniaService>(),
paymentInfo: paymentInfo,
committedInfo: committedInfo));
getIt.registerFactoryParam<IoniaPaymentStatusPage, IoniaAnyPayPaymentInfo, AnyPayPaymentCommittedInfo>(
(IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo)
=> IoniaPaymentStatusPage(getIt.get<IoniaPaymentStatusViewModel>(param1: paymentInfo, param2: committedInfo)));
_isSetupFinished = true;
}
@@ -0,0 +1,9 @@
import 'package:cake_wallet/anypay/any_pay_payment.dart';
import 'package:cake_wallet/ionia/ionia_order.dart';
class IoniaAnyPayPaymentInfo {
const IoniaAnyPayPaymentInfo(this.ioniaOrder, this.anyPayPayment);
final IoniaOrder ioniaOrder;
final AnyPayPayment anyPayPayment;
}
+92
View File
@@ -0,0 +1,92 @@
import 'package:flutter/foundation.dart';
import 'package:cw_core/monero_amount_format.dart';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/anypay/any_pay_payment.dart';
import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/anypay/any_pay_chain.dart';
import 'package:cake_wallet/anypay/any_pay_trasnaction.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
import 'package:cake_wallet/ionia/ionia_order.dart';
class IoniaAnyPay {
IoniaAnyPay(this.ioniaService, this.anyPayApi, this.wallet);
final IoniaService ioniaService;
final AnyPayApi anyPayApi;
final WalletBase wallet;
Future<IoniaAnyPayPaymentInfo> purchase({
@required String merchId,
@required double amount}) async {
final invoice = await ioniaService.purchaseGiftCard(
merchId: merchId,
amount: amount,
currency: wallet.currency.title.toUpperCase());
final anypayPayment = await anyPayApi.paymentRequest(invoice.uri);
return IoniaAnyPayPaymentInfo(invoice, anypayPayment);
}
Future<AnyPayPaymentCommittedInfo> commitInvoice(AnyPayPayment payment) async {
final transactionCredentials = payment.instructions
.where((instruction) => instruction.type == AnyPayPaymentInstruction.transactionType)
.map((AnyPayPaymentInstruction instruction) {
switch(payment.chain.toUpperCase()) {
case AnyPayChain.xmr:
return monero.createMoneroTransactionCreationCredentialsRaw(
outputs: instruction.outputs.map((out) =>
OutputInfo(
isParsedAddress: false,
address: out.address,
cryptoAmount: moneroAmountToString(amount: out.amount),
sendAll: false)).toList(),
priority: MoneroTransactionPriority.medium); // FIXME: HARDCODED PRIORITY
case AnyPayChain.btc:
return bitcoin.createBitcoinTransactionCredentialsRaw(
instruction.outputs.map((out) =>
OutputInfo(
isParsedAddress: false,
address: out.address,
formattedCryptoAmount: out.amount,
sendAll: false)).toList(),
feeRate: instruction.requiredFeeRate);
case AnyPayChain.ltc:
return bitcoin.createBitcoinTransactionCredentialsRaw(
instruction.outputs.map((out) =>
OutputInfo(
isParsedAddress: false,
address: out.address,
formattedCryptoAmount: out.amount,
sendAll: false)).toList(),
feeRate: instruction.requiredFeeRate);
default:
throw Exception('Incorrect transaction chain: ${payment.chain.toUpperCase()}');
}
});
final transactions = (await Future.wait(transactionCredentials
.map((Object credentials) async => await wallet.createTransaction(credentials))))
.map((PendingTransaction pendingTransaction) {
switch (payment.chain.toUpperCase()){
case AnyPayChain.xmr:
final ptx = monero.pendingTransactionInfo(pendingTransaction);
return AnyPayTransaction(ptx['hex'], id: ptx['id'], key: ptx['key']);
default:
return AnyPayTransaction(pendingTransaction.hex, id: pendingTransaction.id, key: null);
}
})
.toList();
return await anyPayApi.payment(
payment.paymentUrl,
chain: payment.chain,
currency: payment.chain,
transactions: transactions);
}
}
+444
View File
@@ -0,0 +1,444 @@
import 'dart:convert';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/ionia/ionia_order.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/ionia/ionia_user_credentials.dart';
import 'package:cake_wallet/ionia/ionia_virtual_card.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
class IoniaApi {
static const baseUri = 'apistaging.ionia.io';
static const pathPrefix = 'cake';
static final createUserUri = Uri.https(baseUri, '/$pathPrefix/CreateUser');
static final verifyEmailUri = Uri.https(baseUri, '/$pathPrefix/VerifyEmail');
static final signInUri = Uri.https(baseUri, '/$pathPrefix/SignIn');
static final createCardUri = Uri.https(baseUri, '/$pathPrefix/CreateCard');
static final getCardsUri = Uri.https(baseUri, '/$pathPrefix/GetCards');
static final getMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchants');
static final getMerchantsByFilterUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchantsByFilter');
static final getPurchaseMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/PurchaseGiftCard');
static final getCurrentUserGiftCardSummariesUrl = Uri.https(baseUri, '/$pathPrefix/GetCurrentUserGiftCardSummaries');
static final changeGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/ChargeGiftCard');
static final getGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/GetGiftCard');
static final getPaymentStatusUrl = Uri.https(baseUri, '/$pathPrefix/PaymentStatus');
// Create user
Future<String> createUser(String email, {@required String clientId}) async {
final headers = <String, String>{'clientId': clientId};
final query = <String, String>{'emailAddress': email};
final uri = createUserUri.replace(queryParameters: query);
final response = await put(uri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(data['ErrorMessage'] as String);
}
return data['username'] as String;
}
// Verify email
Future<IoniaUserCredentials> verifyEmail({
@required String username,
@required String email,
@required String code,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'EmailAddress': email};
final query = <String, String>{'verificationCode': code};
final uri = verifyEmailUri.replace(queryParameters: query);
final response = await put(uri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(bodyJson['ErrorMessage'] as String);
}
final password = data['password'] as String;
username = data['username'] as String;
return IoniaUserCredentials(username, password);
}
// Sign In
Future<String> signIn(String email, {@required String clientId}) async {
final headers = <String, String>{'clientId': clientId};
final query = <String, String>{'emailAddress': email};
final uri = signInUri.replace(queryParameters: query);
final response = await put(uri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(data['ErrorMessage'] as String);
}
return data['username'] as String;
}
// Get virtual card
Future<IoniaVirtualCard> getCards({
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password};
final response = await post(getCardsUri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(data['message'] as String);
}
final virtualCard = data['VirtualCard'] as Map<String, Object>;
return IoniaVirtualCard.fromMap(virtualCard);
}
// Create virtual card
Future<IoniaVirtualCard> createCard({
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password};
final response = await post(createCardUri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(data['message'] as String);
}
return IoniaVirtualCard.fromMap(data);
}
// Get Merchants
Future<List<IoniaMerchant>> getMerchants({
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password};
final response = await post(getMerchantsUrl, headers: headers);
if (response.statusCode != 200) {
return [];
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
return [];
}
final data = decodedBody['Data'] as List<dynamic>;
return data.map((dynamic e) {
try {
final element = e as Map<String, dynamic>;
return IoniaMerchant.fromJsonMap(element);
} catch(_) {
return null;
}
}).where((e) => e != null)
.toList();
}
// Get Merchants By Filter
Future<List<IoniaMerchant>> getMerchantsByFilter({
@required String username,
@required String password,
@required String clientId,
String search,
List<IoniaCategory> categories,
int merchantFilterType = 0}) async {
// MerchantFilterType: {All = 0, Nearby = 1, Popular = 2, Online = 3, MyFaves = 4, Search = 5}
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{'MerchantFilterType': merchantFilterType};
if (search != null) {
body['SearchCriteria'] = search;
}
if (categories != null) {
body['Categories'] = categories
.map((e) => e.ids)
.expand((e) => e)
.toList();
}
final response = await post(getMerchantsByFilterUrl, headers: headers, body: json.encode(body));
if (response.statusCode != 200) {
return [];
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
return [];
}
final data = decodedBody['Data'] as List<dynamic>;
return data.map((dynamic e) {
try {
final element = e['Merchant'] as Map<String, dynamic>;
return IoniaMerchant.fromJsonMap(element);
} catch(_) {
return null;
}
}).where((e) => e != null)
.toList();
}
// Purchase Gift Card
Future<IoniaOrder> purchaseGiftCard({
@required String merchId,
@required double amount,
@required String currency,
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{
'Amount': amount,
'Currency': currency,
'MerchantId': merchId};
final response = await post(getPurchaseMerchantsUrl, headers: headers, body: json.encode(body));
if (response.statusCode != 200) {
throw Exception('Unexpected response');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
throw Exception(decodedBody['ErrorMessage'] as String);
}
final data = decodedBody['Data'] as Map<String, dynamic>;
return IoniaOrder.fromMap(data);
}
// Get Current User Gift Card Summaries
Future<List<IoniaGiftCard>> getCurrentUserGiftCardSummaries({
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password};
final response = await post(getCurrentUserGiftCardSummariesUrl, headers: headers);
if (response.statusCode != 200) {
return [];
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
return [];
}
final data = decodedBody['Data'] as List<dynamic>;
return data.map((dynamic e) {
try {
final element = e as Map<String, dynamic>;
return IoniaGiftCard.fromJsonMap(element);
} catch(e) {
return null;
}
}).where((e) => e != null)
.toList();
}
// Charge Gift Card
Future<void> chargeGiftCard({
@required String username,
@required String password,
@required String clientId,
@required int giftCardId,
@required double amount}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{
'Id': giftCardId,
'Amount': amount};
final response = await post(
changeGiftCardUrl,
headers: headers,
body: json.encode(body));
if (response.statusCode != 200) {
throw Exception('Failed to update Gift Card with ID ${giftCardId};Incorrect response status: ${response.statusCode};');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
final data = decodedBody['Data'] as Map<String, dynamic>;
final msg = data['Message'] as String ?? '';
if (msg.isNotEmpty) {
throw Exception(msg);
}
throw Exception('Failed to update Gift Card with ID ${giftCardId};');
}
}
// Get Gift Card
Future<IoniaGiftCard> getGiftCard({
@required String username,
@required String password,
@required String clientId,
@required int id}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{'Id': id};
final response = await post(
getGiftCardUrl,
headers: headers,
body: json.encode(body));
if (response.statusCode != 200) {
throw Exception('Failed to get Gift Card with ID ${id};Incorrect response status: ${response.statusCode};');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
final msg = decodedBody['ErrorMessage'] as String ?? '';
if (msg.isNotEmpty) {
throw Exception(msg);
}
throw Exception('Failed to get Gift Card with ID ${id};');
}
final data = decodedBody['Data'] as Map<String, dynamic>;
return IoniaGiftCard.fromJsonMap(data);
}
// Payment Status
Future<int> getPaymentStatus({
@required String username,
@required String password,
@required String clientId,
@required String orderId,
@required String paymentId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{
'order_id': orderId,
'paymentId': paymentId};
final response = await post(
getPaymentStatusUrl,
headers: headers,
body: json.encode(body));
if (response.statusCode != 200) {
throw Exception('Failed to get Payment Status for order_id ${orderId} paymentId ${paymentId};Incorrect response status: ${response.statusCode};');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
final msg = decodedBody['ErrorMessage'] as String ?? '';
if (msg.isNotEmpty) {
throw Exception(msg);
}
throw Exception('Failed to get Payment Status for order_id ${orderId} paymentId ${paymentId}');
}
final data = decodedBody['Data'] as Map<String, dynamic>;
return data['gift_card_id'] as int;
}
}
+18
View File
@@ -0,0 +1,18 @@
class IoniaCategory {
const IoniaCategory({this.index, this.title, this.ids, this.iconPath});
static const allCategories = <IoniaCategory>[all, apparel, onlineOnly, food, entertainment, delivery, travel];
static const all = IoniaCategory(index: 0, title: 'All', ids: [], iconPath: 'assets/images/category.png');
static const apparel = IoniaCategory(index: 1, title: 'Apparel', ids: [1], iconPath: 'assets/images/tshirt.png');
static const onlineOnly = IoniaCategory(index: 2, title: 'Online Only', ids: [13, 43], iconPath: 'assets/images/global.png');
static const food = IoniaCategory(index: 3, title: 'Food', ids: [4], iconPath: 'assets/images/food.png');
static const entertainment = IoniaCategory(index: 4, title: 'Entertainment', ids: [5], iconPath: 'assets/images/gaming.png');
static const delivery = IoniaCategory(index: 5, title: 'Delivery', ids: [114, 109], iconPath: 'assets/images/delivery.png');
static const travel = IoniaCategory(index: 6, title: 'Travel', ids: [12], iconPath: 'assets/images/airplane.png');
final int index;
final String title;
final List<int> ids;
final String iconPath;
}
+58
View File
@@ -0,0 +1,58 @@
import 'package:cake_wallet/ionia/ionia_virtual_card.dart';
import 'package:flutter/material.dart';
abstract class IoniaCreateAccountState {}
class IoniaInitialCreateState extends IoniaCreateAccountState {}
class IoniaCreateStateSuccess extends IoniaCreateAccountState {}
class IoniaCreateStateLoading extends IoniaCreateAccountState {}
class IoniaCreateStateFailure extends IoniaCreateAccountState {
IoniaCreateStateFailure({@required this.error});
final String error;
}
abstract class IoniaOtpState {}
class IoniaOtpValidating extends IoniaOtpState {}
class IoniaOtpSuccess extends IoniaOtpState {}
class IoniaOtpSendDisabled extends IoniaOtpState {}
class IoniaOtpSendEnabled extends IoniaOtpState {}
class IoniaOtpFailure extends IoniaOtpState {
IoniaOtpFailure({@required this.error});
final String error;
}
class IoniaCreateCardState {}
class IoniaCreateCardSuccess extends IoniaCreateCardState {}
class IoniaCreateCardLoading extends IoniaCreateCardState {}
class IoniaCreateCardFailure extends IoniaCreateCardState {
IoniaCreateCardFailure({@required this.error});
final String error;
}
class IoniaFetchCardState {}
class IoniaNoCardState extends IoniaFetchCardState {}
class IoniaFetchingCard extends IoniaFetchCardState {}
class IoniaFetchCardFailure extends IoniaFetchCardState {}
class IoniaCardSuccess extends IoniaFetchCardState {
IoniaCardSuccess({@required this.card});
final IoniaVirtualCard card;
}
+69
View File
@@ -0,0 +1,69 @@
import 'dart:convert';
import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart';
import 'package:flutter/foundation.dart';
class IoniaGiftCard {
IoniaGiftCard({
@required this.id,
@required this.merchantId,
@required this.legalName,
@required this.systemName,
@required this.barcodeUrl,
@required this.cardNumber,
@required this.cardPin,
@required this.instructions,
@required this.tip,
@required this.purchaseAmount,
@required this.actualAmount,
@required this.totalTransactionAmount,
@required this.totalDashTransactionAmount,
@required this.remainingAmount,
@required this.createdDateFormatted,
@required this.lastTransactionDateFormatted,
@required this.isActive,
@required this.isEmpty,
@required this.logoUrl});
factory IoniaGiftCard.fromJsonMap(Map<String, dynamic> element) {
return IoniaGiftCard(
id: element['Id'] as int,
merchantId: element['MerchantId'] as int,
legalName: element['LegalName'] as String,
systemName: element['SystemName'] as String,
barcodeUrl: element['BarcodeUrl'] as String,
cardNumber: element['CardNumber'] as String,
cardPin: element['CardPin'] as String,
tip: element['Tip'] as double,
purchaseAmount: element['PurchaseAmount'] as double,
actualAmount: element['ActualAmount'] as double,
totalTransactionAmount: element['TotalTransactionAmount'] as double,
totalDashTransactionAmount: element['TotalDashTransactionAmount'] as double,
remainingAmount: element['RemainingAmount'] as double,
isActive: element['IsActive'] as bool,
isEmpty: element['IsEmpty'] as bool,
logoUrl: element['LogoUrl'] as String,
createdDateFormatted: element['CreatedDate'] as String,
lastTransactionDateFormatted: element['LastTransactionDate'] as String,
instructions: IoniaGiftCardInstruction.parseListOfInstructions(element['PaymentInstructions'] as String));
}
final int id;
final int merchantId;
final String legalName;
final String systemName;
final String barcodeUrl;
final String cardNumber;
final String cardPin;
final List<IoniaGiftCardInstruction> instructions;
final double tip;
final double purchaseAmount;
final double actualAmount;
final double totalTransactionAmount;
final double totalDashTransactionAmount;
final double remainingAmount;
final String createdDateFormatted;
final String lastTransactionDateFormatted;
final bool isActive;
final bool isEmpty;
final String logoUrl;
}
@@ -0,0 +1,28 @@
import 'dart:convert';
import 'package:intl/intl.dart' show toBeginningOfSentenceCase;
class IoniaGiftCardInstruction {
IoniaGiftCardInstruction(this.header, this.body);
factory IoniaGiftCardInstruction.fromJsonMap(Map<String, dynamic> element) {
return IoniaGiftCardInstruction(
toBeginningOfSentenceCase(element['title'] as String ?? ''),
element['description'] as String);
}
static List<IoniaGiftCardInstruction> parseListOfInstructions(String instructionsJSON) {
List<IoniaGiftCardInstruction> instructions = <IoniaGiftCardInstruction>[];
if (instructionsJSON.isNotEmpty) {
final decodedInstructions = json.decode(instructionsJSON) as List<dynamic>;
instructions = decodedInstructions
.map((dynamic e) =>IoniaGiftCardInstruction.fromJsonMap(e as Map<String, dynamic>))
.toList();
}
return instructions;
}
final String header;
final String body;
}
+176
View File
@@ -0,0 +1,176 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart';
class IoniaMerchant {
IoniaMerchant({
@required this.id,
@required this.legalName,
@required this.systemName,
@required this.description,
@required this.website,
@required this.termsAndConditions,
@required this.logoUrl,
@required this.cardImageUrl,
@required this.cardholderAgreement,
@required this.purchaseFee,
@required this.revenueShare,
@required this.marketingFee,
@required this.minimumDiscount,
@required this.level1,
@required this.level2,
@required this.level3,
@required this.level4,
@required this.level5,
@required this.level6,
@required this.level7,
@required this.isActive,
@required this.isDeleted,
@required this.isOnline,
@required this.isPhysical,
@required this.isVariablePurchase,
@required this.minimumCardPurchase,
@required this.maximumCardPurchase,
@required this.acceptsTips,
@required this.createdDateFormatted,
@required this.createdBy,
@required this.isRegional,
@required this.modifiedDateFormatted,
@required this.modifiedBy,
@required this.usageInstructions,
@required this.usageInstructionsBak,
@required this.paymentGatewayId,
@required this.giftCardGatewayId,
@required this.isHtmlDescription,
@required this.purchaseInstructions,
@required this.balanceInstructions,
@required this.amountPerCard,
@required this.processingMessage,
@required this.hasBarcode,
@required this.hasInventory,
@required this.isVoidable,
@required this.receiptMessage,
@required this.cssBorderCode,
@required this.instructions,
@required this.alderSku,
@required this.ngcSku,
@required this.acceptedCurrency,
@required this.deepLink,
@required this.isPayLater,
@required this.savingsPercentage});
factory IoniaMerchant.fromJsonMap(Map<String, dynamic> element) {
return IoniaMerchant(
id: element["Id"] as int,
legalName: element["LegalName"] as String,
systemName: element["SystemName"] as String,
description: element["Description"] as String,
website: element["Website"] as String,
termsAndConditions: element["TermsAndConditions"] as String,
logoUrl: element["LogoUrl"] as String,
cardImageUrl: element["CardImageUrl"] as String,
cardholderAgreement: element["CardholderAgreement"] as String,
purchaseFee: element["PurchaseFee"] as double,
revenueShare: element["RevenueShare"] as double,
marketingFee: element["MarketingFee"] as double,
minimumDiscount: element["MinimumDiscount"] as double,
level1: element["Level1"] as double,
level2: element["Level2"] as double,
level3: element["Level3"] as double,
level4: element["Level4"] as double,
level5: element["Level5"] as double,
level6: element["Level6"] as double,
level7: element["Level7"] as double,
isActive: element["IsActive"] as bool,
isDeleted: element["IsDeleted"] as bool,
isOnline: element["IsOnline"] as bool,
isPhysical: element["IsPhysical"] as bool,
isVariablePurchase: element["IsVariablePurchase"] as bool,
minimumCardPurchase: element["MinimumCardPurchase"] as double,
maximumCardPurchase: element["MaximumCardPurchase"] as double,
acceptsTips: element["AcceptsTips"] as bool,
createdDateFormatted: element["CreatedDate"] as String,
createdBy: element["CreatedBy"] as int,
isRegional: element["IsRegional"] as bool,
modifiedDateFormatted: element["ModifiedDate"] as String,
modifiedBy: element["ModifiedBy"] as int,
usageInstructions: element["UsageInstructions"] as String,
usageInstructionsBak: element["UsageInstructionsBak"] as String,
paymentGatewayId: element["PaymentGatewayId"] as int,
giftCardGatewayId: element["GiftCardGatewayId"] as int ,
isHtmlDescription: element["IsHtmlDescription"] as bool,
purchaseInstructions: element["PurchaseInstructions"] as String,
balanceInstructions: element["BalanceInstructions"] as String,
amountPerCard: element["AmountPerCard"] as double,
processingMessage: element["ProcessingMessage"] as String,
hasBarcode: element["HasBarcode"] as bool,
hasInventory: element["HasInventory"] as bool,
isVoidable: element["IsVoidable"] as bool,
receiptMessage: element["ReceiptMessage"] as String,
cssBorderCode: element["CssBorderCode"] as String,
instructions: IoniaGiftCardInstruction.parseListOfInstructions(element['PaymentInstructions'] as String),
alderSku: element["AlderSku"] as String,
ngcSku: element["NgcSku"] as String,
acceptedCurrency: element["AcceptedCurrency"] as String,
deepLink: element["DeepLink"] as String,
isPayLater: element["IsPayLater"] as bool,
savingsPercentage: element["SavingsPercentage"] as double);
}
final int id;
final String legalName;
final String systemName;
final String description;
final String website;
final String termsAndConditions;
final String logoUrl;
final String cardImageUrl;
final String cardholderAgreement;
final double purchaseFee;
final double revenueShare;
final double marketingFee;
final double minimumDiscount;
final double level1;
final double level2;
final double level3;
final double level4;
final double level5;
final double level6;
final double level7;
final bool isActive;
final bool isDeleted;
final bool isOnline;
final bool isPhysical;
final bool isVariablePurchase;
final double minimumCardPurchase;
final double maximumCardPurchase;
final bool acceptsTips;
final String createdDateFormatted;
final int createdBy;
final bool isRegional;
final String modifiedDateFormatted;
final int modifiedBy;
final String usageInstructions;
final String usageInstructionsBak;
final int paymentGatewayId;
final int giftCardGatewayId;
final bool isHtmlDescription;
final String purchaseInstructions;
final String balanceInstructions;
final double amountPerCard;
final String processingMessage;
final bool hasBarcode;
final bool hasInventory;
final bool isVoidable;
final String receiptMessage;
final String cssBorderCode;
final List<IoniaGiftCardInstruction> instructions;
final String alderSku;
final String ngcSku;
final String acceptedCurrency;
final String deepLink;
final bool isPayLater;
final double savingsPercentage;
double get discount => savingsPercentage;
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/foundation.dart';
class IoniaOrder {
IoniaOrder({@required this.id,
@required this.uri,
@required this.currency,
@required this.amount,
@required this.paymentId});
factory IoniaOrder.fromMap(Map<String, dynamic> obj) {
return IoniaOrder(
id: obj['order_id'] as String,
uri: obj['uri'] as String,
currency: obj['currency'] as String,
amount: obj['amount'] as double,
paymentId: obj['paymentId'] as String);
}
final String id;
final String uri;
final String currency;
final double amount;
final String paymentId;
}
+172
View File
@@ -0,0 +1,172 @@
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/ionia/ionia_order.dart';
import 'package:cake_wallet/ionia/ionia_virtual_card.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/ionia/ionia_api.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
class IoniaService {
IoniaService(this.secureStorage, this.ioniaApi);
static const ioniaEmailStorageKey = 'ionia_email';
static const ioniaUsernameStorageKey = 'ionia_username';
static const ioniaPasswordStorageKey = 'ionia_password';
static String get clientId => secrets.ioniaClientId;
final FlutterSecureStorage secureStorage;
final IoniaApi ioniaApi;
// Create user
Future<void> createUser(String email) async {
final username = await ioniaApi.createUser(email, clientId: clientId);
await secureStorage.write(key: ioniaEmailStorageKey, value: email);
await secureStorage.write(key: ioniaUsernameStorageKey, value: username);
}
// Verify email
Future<void> verifyEmail(String code) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final email = await secureStorage.read(key: ioniaEmailStorageKey);
final credentials = await ioniaApi.verifyEmail(email: email, username: username, code: code, clientId: clientId);
await secureStorage.write(key: ioniaPasswordStorageKey, value: credentials.password);
await secureStorage.write(key: ioniaUsernameStorageKey, value: credentials.username);
}
// Sign In
Future<void> signIn(String email) async {
final username = await ioniaApi.signIn(email, clientId: clientId);
await secureStorage.write(key: ioniaEmailStorageKey, value: email);
await secureStorage.write(key: ioniaUsernameStorageKey, value: username);
}
Future<String> getUserEmail() async {
return secureStorage.read(key: ioniaEmailStorageKey);
}
// Check is user logined
Future<bool> isLogined() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey) ?? '';
final password = await secureStorage.read(key: ioniaPasswordStorageKey) ?? '';
return username.isNotEmpty && password.isNotEmpty;
}
// Logout
Future<void> logout() async {
await secureStorage.delete(key: ioniaUsernameStorageKey);
await secureStorage.delete(key: ioniaPasswordStorageKey);
}
// Create virtual card
Future<IoniaVirtualCard> createCard() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.createCard(username: username, password: password, clientId: clientId);
}
// Get virtual card
Future<IoniaVirtualCard> getCard() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getCards(username: username, password: password, clientId: clientId);
}
// Get Merchants
Future<List<IoniaMerchant>> getMerchants() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getMerchants(username: username, password: password, clientId: clientId);
}
// Get Merchants By Filter
Future<List<IoniaMerchant>> getMerchantsByFilter({
String search,
List<IoniaCategory> categories,
int merchantFilterType = 0}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getMerchantsByFilter(
username: username,
password: password,
clientId: clientId,
search: search,
categories: categories,
merchantFilterType: merchantFilterType);
}
// Purchase Gift Card
Future<IoniaOrder> purchaseGiftCard({
@required String merchId,
@required double amount,
@required String currency}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.purchaseGiftCard(
merchId: merchId,
amount: amount,
currency: currency,
username: username,
password: password,
clientId: clientId);
}
// Get Current User Gift Card Summaries
Future<List<IoniaGiftCard>> getCurrentUserGiftCardSummaries() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getCurrentUserGiftCardSummaries(username: username, password: password, clientId: clientId);
}
// Charge Gift Card
Future<void> chargeGiftCard({
@required int giftCardId,
@required double amount}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
await ioniaApi.chargeGiftCard(
username: username,
password: password,
clientId: clientId,
giftCardId: giftCardId,
amount: amount);
}
// Redeem
Future<void> redeem(IoniaGiftCard giftCard) async {
await chargeGiftCard(giftCardId: giftCard.id, amount: giftCard.remainingAmount);
}
// Get Gift Card
Future<IoniaGiftCard> getGiftCard({@required int id}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getGiftCard(username: username, password: password, clientId: clientId,id: id);
}
// Payment Status
Future<int> getPaymentStatus({
@required String orderId,
@required String paymentId}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getPaymentStatus(username: username, password: password, clientId: clientId, orderId: orderId, paymentId: paymentId);
}
}
+12
View File
@@ -0,0 +1,12 @@
class IoniaTip {
const IoniaTip({this.originalAmount, this.percentage});
final double originalAmount;
final double percentage;
double get additionalAmount => double.parse((originalAmount * percentage / 100).toStringAsFixed(2));
static const tipList = [
IoniaTip(originalAmount: 0, percentage: 0),
IoniaTip(originalAmount: 10, percentage: 10),
IoniaTip(originalAmount: 20, percentage: 20)
];
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:flutter/foundation.dart';
import 'dart:convert';
class IoniaTokenData {
IoniaTokenData({@required this.accessToken, @required this.tokenType, @required this.expiredAt});
factory IoniaTokenData.fromJson(String source) {
final decoded = json.decode(source) as Map<String, dynamic>;
final accessToken = decoded['access_token'] as String;
final expiresIn = decoded['expires_in'] as int;
final tokenType = decoded['token_type'] as String;
final expiredAtInMilliseconds = decoded['expired_at'] as int;
DateTime expiredAt;
if (expiredAtInMilliseconds != null) {
expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtInMilliseconds);
} else {
expiredAt = DateTime.now().add(Duration(seconds: expiresIn));
}
return IoniaTokenData(
accessToken: accessToken,
tokenType: tokenType,
expiredAt: expiredAt);
}
final String accessToken;
final String tokenType;
final DateTime expiredAt;
bool get isExpired => DateTime.now().isAfter(expiredAt);
@override
String toString() => '$tokenType $accessToken';
String toJson() {
return json.encode(<String, dynamic>{
'access_token': accessToken,
'token_type': tokenType,
'expired_at': expiredAt.millisecondsSinceEpoch
});
}
}
+6
View File
@@ -0,0 +1,6 @@
class IoniaUserCredentials {
const IoniaUserCredentials(this.username, this.password);
final String username;
final String password;
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:flutter/foundation.dart';
class IoniaVirtualCard {
IoniaVirtualCard({
@required this.token,
@required this.createdAt,
@required this.lastFour,
@required this.state,
@required this.pan,
@required this.cvv,
@required this.expirationMonth,
@required this.expirationYear,
@required this.fundsLimit,
@required this.spendLimit});
factory IoniaVirtualCard.fromMap(Map<String, Object> source) {
final created = source['created'] as String;
final createdAt = DateTime.tryParse(created);
return IoniaVirtualCard(
token: source['token'] as String,
createdAt: createdAt,
lastFour: source['lastFour'] as String,
state: source['state'] as String,
pan: source['pan'] as String,
cvv: source['cvv'] as String,
expirationMonth: source['expirationMonth'] as String,
expirationYear: source['expirationYear'] as String,
fundsLimit: source['FundsLimit'] as double,
spendLimit: source['spend_limit'] as double);
}
final String token;
final String lastFour;
final String state;
final String pan;
final String cvv;
final String expirationMonth;
final String expirationYear;
final DateTime createdAt;
final double fundsLimit;
final double spendLimit;
}
+4 -1
View File
@@ -2,6 +2,8 @@ import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/language_service.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -174,7 +176,8 @@ Future<void> initialSetup(
exchangeTemplates: exchangeTemplates,
transactionDescriptionBox: transactionDescriptions,
ordersSource: ordersSource,
unspentCoinsInfoSource: unspentCoinsInfoSource);
unspentCoinsInfoSource: unspentCoinsInfoSource,
);
await bootstrap(navigatorKey);
monero?.onStartup();
}
+47 -11
View File
@@ -2,7 +2,7 @@ part of 'monero.dart';
class CWMoneroAccountList extends MoneroAccountList {
CWMoneroAccountList(this._wallet);
Object _wallet;
final Object _wallet;
@override
@computed
@@ -39,13 +39,13 @@ class CWMoneroAccountList extends MoneroAccountList {
@override
Future<void> addAccount(Object wallet, {String label}) async {
final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.accountList.addAccount(label: label);
await moneroWallet.walletAddresses.accountList.addAccount(label: label);
}
@override
Future<void> setLabelAccount(Object wallet, {int accountIndex, String label}) async {
final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.accountList
await moneroWallet.walletAddresses.accountList
.setLabelAccount(
accountIndex: accountIndex,
label: label);
@@ -95,7 +95,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
@override
Future<void> addSubaddress(Object wallet, {int accountIndex, String label}) async {
final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.subaddressList
await moneroWallet.walletAddresses.subaddressList
.addSubaddress(
accountIndex: accountIndex,
label: label);
@@ -105,7 +105,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
Future<void> setLabelSubaddress(Object wallet,
{int accountIndex, int addressIndex, String label}) async {
final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.subaddressList
await moneroWallet.walletAddresses.subaddressList
.setLabelSubaddress(
accountIndex: accountIndex,
addressIndex: addressIndex,
@@ -140,35 +140,43 @@ class CWMonero extends Monero {
return CWMoneroAccountList(wallet);
}
@override
MoneroSubaddressList getSubaddressList(Object wallet) {
return CWMoneroSubaddressList(wallet);
}
@override
TransactionHistoryBase getTransactionHistory(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.transactionHistory;
}
@override
MoneroWalletDetails getMoneroWalletDetails(Object wallet) {
return CWMoneroWalletDetails(wallet);
}
@override
int getHeigthByDate({DateTime date}) {
return getMoneroHeigthByDate(date: date);
}
@override
TransactionPriority getDefaultTransactionPriority() {
return MoneroTransactionPriority.slow;
}
@override
TransactionPriority deserializeMoneroTransactionPriority({int raw}) {
return MoneroTransactionPriority.deserialize(raw: raw);
}
@override
List<TransactionPriority> getTransactionPriorities() {
return MoneroTransactionPriority.all;
}
@override
List<String> getMoneroWordList(String language) {
switch (language.toLowerCase()) {
case 'english':
@@ -196,14 +204,15 @@ class CWMonero extends Monero {
}
}
@override
WalletCredentials createMoneroRestoreWalletFromKeysCredentials({
String name,
String spendKey,
String viewKey,
String address,
String password,
String language,
int height}) {
String spendKey,
String viewKey,
String address,
String password,
String language,
int height}) {
return MoneroRestoreWalletFromKeysCredentials(
name: name,
spendKey: spendKey,
@@ -214,6 +223,7 @@ class CWMonero extends Monero {
height: height);
}
@override
WalletCredentials createMoneroRestoreWalletFromSeedCredentials({String name, String password, int height, String mnemonic}) {
return MoneroRestoreWalletFromSeedCredentials(
name: name,
@@ -222,6 +232,7 @@ class CWMonero extends Monero {
mnemonic: mnemonic);
}
@override
WalletCredentials createMoneroNewWalletCredentials({String name, String password, String language}) {
return MoneroNewWalletCredentials(
name: name,
@@ -229,6 +240,7 @@ class CWMonero extends Monero {
language: language);
}
@override
Map<String, String> getKeys(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
final keys = moneroWallet.keys;
@@ -239,6 +251,7 @@ class CWMonero extends Monero {
'publicViewKey': keys.publicViewKey};
}
@override
Object createMoneroTransactionCreationCredentials({List<Output> outputs, TransactionPriority priority}) {
return MoneroTransactionCreationCredentials(
outputs: outputs.map((out) => OutputInfo(
@@ -254,49 +267,72 @@ class CWMonero extends Monero {
priority: priority as MoneroTransactionPriority);
}
@override
Object createMoneroTransactionCreationCredentialsRaw({List<OutputInfo> outputs, TransactionPriority priority}) {
return MoneroTransactionCreationCredentials(
outputs: outputs,
priority: priority as MoneroTransactionPriority);
}
@override
String formatterMoneroAmountToString({int amount}) {
return moneroAmountToString(amount: amount);
}
@override
double formatterMoneroAmountToDouble({int amount}) {
return moneroAmountToDouble(amount: amount);
}
@override
int formatterMoneroParseAmount({String amount}) {
return moneroParseAmount(amount: amount);
}
@override
Account getCurrentAccount(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
final acc = moneroWallet.walletAddresses.account;
return Account(id: acc.id, label: acc.label);
}
@override
void setCurrentAccount(Object wallet, int id, String label) {
final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.account = monero_account.Account(id: id, label: label);
}
@override
void onStartup() {
monero_wallet_api.onStartup();
}
@override
int getTransactionInfoAccountId(TransactionInfo tx) {
final moneroTransactionInfo = tx as MoneroTransactionInfo;
return moneroTransactionInfo.accountIndex;
}
@override
WalletService createMoneroWalletService(Box<WalletInfo> walletInfoSource) {
return MoneroWalletService(walletInfoSource);
}
@override
String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.getTransactionAddress(accountIndex, addressIndex);
}
@override
String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.getSubaddressLabel(accountIndex, addressIndex);
}
@override
Map<String, String> pendingTransactionInfo(Object transaction) {
final ptx = transaction as PendingMoneroTransaction;
return {'id': ptx.id, 'hex': ptx.hex, 'key': ptx.txKey};
}
}
+60
View File
@@ -4,6 +4,10 @@ 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/pre_order_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart';
import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart';
@@ -63,6 +67,10 @@ import 'package:flutter/services.dart';
import 'package:cake_wallet/wallet_types.g.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart';
import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
import 'package:cake_wallet/src/screens/ionia/ionia.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
RouteSettings currentRouteSettings;
@@ -401,6 +409,58 @@ Route<dynamic> createRoute(RouteSettings settings) {
param2: args['isLight'] as bool,
));
case Routes.ioniaWelcomePage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaWelcomePage>());
case Routes.ioniaLoginPage:
return CupertinoPageRoute<void>( builder: (_) => getIt.get<IoniaLoginPage>());
case Routes.ioniaCreateAccountPage:
return CupertinoPageRoute<void>( builder: (_) => getIt.get<IoniaCreateAccountPage>());
case Routes.ioniaManageCardsPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaManageCardsPage>());
case Routes.ioniaBuyGiftCardPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaBuyGiftCardPage>(param1: args));
case Routes.ioniaBuyGiftCardDetailPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaBuyGiftCardDetailPage>(param1: args));
case Routes.ioniaVerifyIoniaOtpPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) =>getIt.get<IoniaVerifyIoniaOtp>(param1: args));
case Routes.ioniaDebitCardPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaDebitCardPage>());
case Routes.ioniaActivateDebitCardPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaActivateDebitCardPage>());
case Routes.ioniaAccountPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaAccountPage>());
case Routes.ioniaAccountCardsPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaAccountCardsPage>());
case Routes.ioniaCustomTipPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) =>getIt.get<IoniaCustomTipPage>(param1: args));
case Routes.ioniaGiftCardDetailPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaGiftCardDetailPage>(param1: args.first));
case Routes.ioniaPaymentStatusPage:
final args = settings.arguments as List;
final paymentInfo = args.first as IoniaAnyPayPaymentInfo;
final commitedInfo = args[1] as AnyPayPaymentCommittedInfo;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaPaymentStatusPage>(
param1: paymentInfo,
param2: commitedInfo));
default:
return MaterialPageRoute<void>(
builder: (_) => Scaffold(
+15 -1
View File
@@ -60,4 +60,18 @@ class Routes {
static const moneroNewWalletFromWelcome = '/monero_new_wallet';
static const addressPage = '/address_page';
static const fullscreenQR = '/fullscreen_qr';
}
static const ioniaWelcomePage = '/cake_pay_welcome_page';
static const ioniaCreateAccountPage = '/cake_pay_create_account_page';
static const ioniaLoginPage = '/cake_pay_login_page';
static const ioniaManageCardsPage = '/manage_cards_page';
static const ioniaBuyGiftCardPage = '/buy_gift_card_page';
static const ioniaBuyGiftCardDetailPage = '/buy_gift_card_detail_page';
static const ioniaVerifyIoniaOtpPage = '/cake_pay_verify_otp_page';
static const ioniaDebitCardPage = '/debit_card_page';
static const ioniaActivateDebitCardPage = '/activate_debit_card_page';
static const ioniaAccountPage = 'ionia_account_page';
static const ioniaAccountCardsPage = 'ionia_account_cards_page';
static const ioniaCustomTipPage = 'ionia_custom_tip_page';
static const ioniaGiftCardDetailPage = '/ionia_gift_card_detail_page';
static const ioniaPaymentStatusPage = '/ionia_payment_status_page';
}
@@ -1,8 +1,8 @@
import 'dart:async';
import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/yat/yat_popup.dart';
import 'package:cake_wallet/src/screens/yat_emoji_id.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/themes/theme_base.dart';
@@ -14,19 +14,15 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/router.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
class DashboardPage extends BasePage {
DashboardPage({
@@ -85,7 +81,7 @@ class DashboardPage extends BasePage {
final DashboardViewModel walletViewModel;
final WalletAddressListViewModel addressListViewModel;
final controller = PageController(initialPage: 0);
final controller = PageController(initialPage: 1);
var pages = <Widget>[];
bool _isEffectsInstalled = false;
@@ -221,7 +217,7 @@ class DashboardPage extends BasePage {
if (_isEffectsInstalled) {
return;
}
pages.add(MarketPlacePage(dashboardViewModel: walletViewModel));
pages.add(balancePage);
pages.add(TransactionsPage(dashboardViewModel: walletViewModel));
_isEffectsInstalled = true;
@@ -0,0 +1,80 @@
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/market_place_item.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
class MarketPlacePage extends StatelessWidget {
MarketPlacePage({@required this.dashboardViewModel});
final DashboardViewModel dashboardViewModel;
final _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: RawScrollbar(
thumbColor: Colors.white.withOpacity(0.15),
radius: Radius.circular(20),
isAlwaysShown: true,
thickness: 2,
controller: _scrollController,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 50),
Text(
S.of(context).market_place,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w500,
color: Theme.of(context).accentTextTheme.display3.backgroundColor,
),
),
Expanded(
child: ListView(
controller: _scrollController,
children: <Widget>[
SizedBox(height: 20),
MarketPlaceItem(
onTap: () =>_navigatorToGiftCardsPage(context),
title: S.of(context).cake_pay_title,
subTitle: S.of(context).cake_pay_subtitle,
),
],
),
),
],
),
),
),
);
}
void _navigatorToGiftCardsPage(BuildContext context) {
final walletType = dashboardViewModel.type;
switch (walletType) {
case WalletType.haven:
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).gift_cards_unavailable,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
break;
default:
Navigator.of(context).pushNamed(Routes.ioniaWelcomePage);
}
}
}
@@ -0,0 +1,154 @@
import 'package:cake_wallet/core/email_validator.dart';
import 'package:cake_wallet/ionia/ionia_create_state.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
class IoniaCreateAccountPage extends BasePage {
IoniaCreateAccountPage(this._authViewModel)
: _emailFocus = FocusNode(),
_emailController = TextEditingController(),
_formKey = GlobalKey<FormState>() {
_emailController.text = _authViewModel.email;
_emailController.addListener(() => _authViewModel.email = _emailController.text);
}
final IoniaAuthViewModel _authViewModel;
final GlobalKey<FormState> _formKey;
final FocusNode _emailFocus;
final TextEditingController _emailController;
static const privacyPolicyUrl = 'https://ionia.docsend.com/view/jaqsmbq9w7dzvnqf';
static const termsAndConditionsUrl = 'https://ionia.docsend.com/view/hi9awnwxr6mqgiqj';
@override
Widget middle(BuildContext context) {
return Text(
S.current.sign_up,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor,
),
);
}
@override
Widget body(BuildContext context) {
reaction((_) => _authViewModel.createUserState, (IoniaCreateAccountState state) {
if (state is IoniaCreateStateFailure) {
_onCreateUserFailure(context, state.error);
}
if (state is IoniaCreateStateSuccess) {
_onCreateSuccessful(context, _authViewModel);
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Form(
key: _formKey,
child: BaseTextFormField(
hintText: S.of(context).email_address,
focusNode: _emailFocus,
validator: EmailValidator(),
keyboardType: TextInputType.emailAddress,
controller: _emailController,
),
),
bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24),
bottomSection: Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.of(context).create_account,
onPressed: () async {
if (!_formKey.currentState.validate()) {
return;
}
await _authViewModel.createUser(_emailController.text);
},
isLoading: _authViewModel.createUserState is IoniaCreateStateLoading,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
),
SizedBox(
height: 20,
),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: S.of(context).agree_to,
style: TextStyle(
color: Color(0xff7A93BA),
fontSize: 12,
fontFamily: 'Lato',
),
children: [
TextSpan(
text: S.of(context).settings_terms_and_conditions,
style: TextStyle(
color: Theme.of(context).accentTextTheme.body2.color,
fontWeight: FontWeight.w700,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
if (await canLaunch(termsAndConditionsUrl)) await launch(termsAndConditionsUrl);
},
),
TextSpan(text: ' ${S.of(context).and} '),
TextSpan(
text: S.of(context).privacy_policy,
style: TextStyle(
color: Theme.of(context).accentTextTheme.body2.color,
fontWeight: FontWeight.w700,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
if (await canLaunch(privacyPolicyUrl)) await launch(privacyPolicyUrl);
}),
TextSpan(text: ' ${S.of(context).by_cake_pay}'),
],
),
),
],
),
],
),
);
}
void _onCreateUserFailure(BuildContext context, String error) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.create_account,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
void _onCreateSuccessful(BuildContext context, IoniaAuthViewModel authViewModel) => Navigator.pushNamed(
context,
Routes.ioniaVerifyIoniaOtpPage,
arguments: [authViewModel.email, false],
);
}
@@ -0,0 +1,112 @@
import 'package:cake_wallet/core/email_validator.dart';
import 'package:cake_wallet/ionia/ionia_create_state.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class IoniaLoginPage extends BasePage {
IoniaLoginPage(this._authViewModel)
: _formKey = GlobalKey<FormState>(),
_emailController = TextEditingController() {
_emailController.text = _authViewModel.email;
_emailController.addListener(() => _authViewModel.email = _emailController.text);
}
final GlobalKey<FormState> _formKey;
final IoniaAuthViewModel _authViewModel;
@override
Color get titleColor => Colors.black;
final TextEditingController _emailController;
@override
Widget middle(BuildContext context) {
return Text(
S.current.login,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor,
),
);
}
@override
Widget body(BuildContext context) {
reaction((_) => _authViewModel.signInState, (IoniaCreateAccountState state) {
if (state is IoniaCreateStateFailure) {
_onLoginUserFailure(context, state.error);
}
if (state is IoniaCreateStateSuccess) {
_onLoginSuccessful(context, _authViewModel);
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Form(
key: _formKey,
child: BaseTextFormField(
hintText: S.of(context).email_address,
keyboardType: TextInputType.emailAddress,
validator: EmailValidator(),
controller: _emailController,
),
),
bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24),
bottomSection: Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.of(context).login,
onPressed: () async {
if (!_formKey.currentState.validate()) {
return;
}
await _authViewModel.signIn(_emailController.text);
},
isLoading: _authViewModel.signInState is IoniaCreateStateLoading,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
),
SizedBox(
height: 20,
),
],
),
],
),
);
}
void _onLoginUserFailure(BuildContext context, String error) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.login,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
void _onLoginSuccessful(BuildContext context, IoniaAuthViewModel authViewModel) => Navigator.pushNamed(
context,
Routes.ioniaVerifyIoniaOtpPage,
arguments: [authViewModel.email, true],
);
}
@@ -0,0 +1,151 @@
import 'package:cake_wallet/ionia/ionia_create_state.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
import 'package:flutter/material.dart';
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';
class IoniaVerifyIoniaOtp extends BasePage {
IoniaVerifyIoniaOtp(this._authViewModel, this._email, this.isSignIn)
: _codeController = TextEditingController(),
_codeFocus = FocusNode() {
_codeController.addListener(() {
final otp = _codeController.text;
_authViewModel.otp = otp;
if (otp.length > 3) {
_authViewModel.otpState = IoniaOtpSendEnabled();
} else {
_authViewModel.otpState = IoniaOtpSendDisabled();
}
});
}
final IoniaAuthViewModel _authViewModel;
final bool isSignIn;
final String _email;
@override
Widget middle(BuildContext context) {
return Text(
S.current.verification,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor,
),
);
}
final TextEditingController _codeController;
final FocusNode _codeFocus;
@override
Widget body(BuildContext context) {
reaction((_) => _authViewModel.otpState, (IoniaOtpState state) {
if (state is IoniaOtpFailure) {
_onOtpFailure(context, state.error);
}
if (state is IoniaOtpSuccess) {
_onOtpSuccessful(context);
}
});
return KeyboardActions(
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).accentTextTheme.body2.backgroundColor,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _codeFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
]),
child: Container(
height: 0,
color: Theme.of(context).backgroundColor,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Column(
children: [
BaseTextFormField(
hintText: S.of(context).enter_code,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
focusNode: _codeFocus,
controller: _codeController,
),
SizedBox(height: 14),
Text(
S.of(context).fill_code,
style: TextStyle(color: Color(0xff7A93BA), fontSize: 12),
),
SizedBox(height: 34),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(S.of(context).dont_get_code),
SizedBox(width: 20),
InkWell(
onTap: () => isSignIn
? _authViewModel.signIn(_email)
: _authViewModel.createUser(_email),
child: Text(
S.of(context).resend_code,
style: textSmallSemiBold(color: Palette.blueCraiola),
),
),
],
),
],
),
bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24),
bottomSection: Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.of(context).continue_text,
onPressed: () async => await _authViewModel.verifyEmail(_codeController.text),
isDisabled: _authViewModel.otpState is IoniaOtpSendDisabled,
isLoading: _authViewModel.otpState is IoniaOtpValidating,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
),
SizedBox(height: 20),
],
),
],
),
),
),
);
}
void _onOtpFailure(BuildContext context, String error) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.verification,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
void _onOtpSuccessful(BuildContext context) =>
Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst);
}
@@ -0,0 +1,104 @@
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:mobx/mobx.dart';
class IoniaWelcomePage extends BasePage {
IoniaWelcomePage(this._cardsListViewModel);
@override
Widget middle(BuildContext context) {
return Text(
S.current.welcome_to_cakepay,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor,
),
);
}
final IoniaGiftCardsListViewModel _cardsListViewModel;
@override
Widget body(BuildContext context) {
reaction((_) => _cardsListViewModel.isLoggedIn, (bool state) {
if (state) {
Navigator.pushReplacementNamed(context, Routes.ioniaManageCardsPage);
}
});
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
SizedBox(height: 100),
Text(
S.of(context).about_cake_pay,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
fontFamily: 'Lato',
color: Theme.of(context).primaryTextTheme.title.color,
),
),
SizedBox(height: 20),
Text(
S.of(context).cake_pay_account_note,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
fontFamily: 'Lato',
color: Theme.of(context).primaryTextTheme.title.color,
),
),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
PrimaryButton(
text: S.of(context).create_account,
onPressed: () => Navigator.of(context).pushNamed(Routes.ioniaCreateAccountPage),
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
SizedBox(
height: 16,
),
Text(
S.of(context).already_have_account,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
fontFamily: 'Lato',
color: Theme.of(context).primaryTextTheme.title.color,
),
),
SizedBox(height: 8),
InkWell(
onTap: () => Navigator.of(context).pushNamed(Routes.ioniaLoginPage),
child: Text(
S.of(context).login,
style: TextStyle(
color: Palette.blueCraiola,
fontSize: 18,
letterSpacing: 1.5,
fontWeight: FontWeight.w900,
),
),
),
SizedBox(height: 20)
],
)
],
),
);
}
}
@@ -0,0 +1,181 @@
import 'dart:ffi';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class IoniaAccountCardsPage extends BasePage {
IoniaAccountCardsPage(this.ioniaAccountViewModel);
final IoniaAccountViewModel ioniaAccountViewModel;
@override
Widget middle(BuildContext context) {
return Text(
S.of(context).cards,
style: textLargeSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor,
),
);
}
@override
Widget body(BuildContext context) {
return _IoniaCardTabs(ioniaAccountViewModel);
}
}
class _IoniaCardTabs extends StatefulWidget {
_IoniaCardTabs(this.ioniaAccountViewModel);
final IoniaAccountViewModel ioniaAccountViewModel;
@override
_IoniaCardTabsState createState() => _IoniaCardTabsState();
}
class _IoniaCardTabsState extends State<_IoniaCardTabs> with SingleTickerProviderStateMixin {
TabController _tabController;
@override
void initState() {
_tabController = TabController(length: 2, vsync: this);
super.initState();
}
@override
void dispose() {
super.dispose();
_tabController.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 45,
width: 230,
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(
25.0,
),
),
child: Theme(
data: ThemeData(primaryTextTheme: TextTheme(body2: TextStyle(backgroundColor: Colors.transparent))),
child: TabBar(
controller: _tabController,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(
25.0,
),
color: Theme.of(context).accentTextTheme.body2.color,
),
labelColor: Theme.of(context).primaryTextTheme.display4.backgroundColor,
unselectedLabelColor: Theme.of(context).primaryTextTheme.title.color,
tabs: [
Tab(
text: S.of(context).active,
),
Tab(
text: S.of(context).redeemed,
),
],
),
),
),
SizedBox(height: 16),
Expanded(
child: Observer(builder: (_) {
final viewModel = widget.ioniaAccountViewModel;
return TabBarView(
controller: _tabController,
children: [
_IoniaCardListView(
emptyText: S.of(context).gift_card_balance_note,
merchList: viewModel.activeMechs,
onTap: (giftCard) {
Navigator.pushNamed(
context,
Routes.ioniaGiftCardDetailPage,
arguments: [giftCard])
.then((_) => viewModel.updateUserGiftCards());
}),
_IoniaCardListView(
emptyText: S.of(context).gift_card_redeemed_note,
merchList: viewModel.redeemedMerchs,
onTap: (giftCard) {
Navigator.pushNamed(
context,
Routes.ioniaGiftCardDetailPage,
arguments: [giftCard])
.then((_) => viewModel.updateUserGiftCards());
}),
],
);
}),
),
],
),
);
}
}
class _IoniaCardListView extends StatelessWidget {
_IoniaCardListView({
Key key,
@required this.emptyText,
@required this.merchList,
@required this.onTap,
}) : super(key: key);
final String emptyText;
final List<IoniaGiftCard> merchList;
final void Function(IoniaGiftCard giftCard) onTap;
@override
Widget build(BuildContext context) {
return merchList.isEmpty
? Center(
child: Text(
emptyText,
textAlign: TextAlign.center,
style: textSmall(
color: Theme.of(context).primaryTextTheme.overline.color,
),
),
)
: ListView.builder(
itemCount: merchList.length,
itemBuilder: (context, index) {
final merchant = merchList[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: CardItem(
onTap: () => onTap?.call(merchant),
title: merchant.legalName,
backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1),
discount: 0,
discountBackground: AssetImage('assets/images/red_badge_discount.png'),
titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor,
subtitleColor: Theme.of(context).hintColor,
subTitle: '',
logoUrl: merchant.logoUrl,
),
);
},
);
}
}
@@ -0,0 +1,181 @@
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/ionia_tile.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class IoniaAccountPage extends BasePage {
IoniaAccountPage(this.ioniaAccountViewModel);
final IoniaAccountViewModel ioniaAccountViewModel;
@override
Widget middle(BuildContext context) {
return Text(
S.current.account,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor,
),
);
}
@override
Widget body(BuildContext context) {
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Column(
children: [
_GradiantContainer(
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Observer(
builder: (_) => RichText(
text: TextSpan(
text: '${ioniaAccountViewModel.countOfMerch}',
style: textLargeSemiBold(),
children: [
TextSpan(
text: ' ${S.of(context).active_cards}',
style: textSmall(color: Colors.white.withOpacity(0.7))),
],
),
)),
InkWell(
onTap: () {
Navigator.pushNamed(context, Routes.ioniaAccountCardsPage)
.then((_) => ioniaAccountViewModel.updateUserGiftCards());
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
S.of(context).view_all,
style: textSmallSemiBold(),
),
),
)
],
),
),
SizedBox(height: 8),
//Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// _GradiantContainer(
// padding: EdgeInsets.all(16),
// width: deviceWidth * 0.28,
// content: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// S.of(context).total_saving,
// style: textSmall(),
// ),
// SizedBox(height: 8),
// Text(
// '\$100',
// style: textMediumSemiBold(),
// ),
// ],
// ),
// ),
// _GradiantContainer(
// padding: EdgeInsets.all(16),
// width: deviceWidth * 0.28,
// content: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// S.of(context).last_30_days,
// style: textSmall(),
// ),
// SizedBox(height: 8),
// Text(
// '\$100',
// style: textMediumSemiBold(),
// ),
// ],
// ),
// ),
// _GradiantContainer(
// padding: EdgeInsets.all(16),
// width: deviceWidth * 0.28,
// content: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// S.of(context).avg_savings,
// style: textSmall(),
// ),
// SizedBox(height: 8),
// Text(
// '10%',
// style: textMediumSemiBold(),
// ),
// ],
// ),
// ),
// ],
//),
SizedBox(height: 40),
Observer(
builder: (_) => IoniaTile(title: S.of(context).email_address, subTitle: ioniaAccountViewModel.email),
),
Divider()
],
),
bottomSectionPadding: EdgeInsets.all(30),
bottomSection: Column(
children: [
PrimaryButton(
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
text: S.of(context).logout,
onPressed: () {
ioniaAccountViewModel.logout();
Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false);
},
),
],
),
);
}
}
class _GradiantContainer extends StatelessWidget {
const _GradiantContainer({
Key key,
@required this.content,
this.padding,
this.width,
}) : super(key: key);
final Widget content;
final EdgeInsets padding;
final double width;
@override
Widget build(BuildContext context) {
return Container(
child: content,
width: width,
padding: padding ?? EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [
Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).accentColor,
],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
),
),
);
}
}
@@ -0,0 +1,114 @@
import 'package:cake_wallet/ionia/ionia_create_state.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:mobx/mobx.dart';
class IoniaActivateDebitCardPage extends BasePage {
IoniaActivateDebitCardPage(this._cardsListViewModel);
final IoniaGiftCardsListViewModel _cardsListViewModel;
@override
Widget middle(BuildContext context) {
return Text(
S.current.debit_card,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor,
),
);
}
@override
Widget body(BuildContext context) {
reaction((_) => _cardsListViewModel.createCardState, (IoniaCreateCardState state) {
if (state is IoniaCreateCardFailure) {
_onCreateCardFailure(context, state.error);
}
if (state is IoniaCreateCardSuccess) {
_onCreateCardSuccess(context);
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
SizedBox(height: 16),
Text(S.of(context).debit_card_terms),
SizedBox(height: 24),
Text(S.of(context).please_reference_document),
SizedBox(height: 40),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: [
TextIconButton(
label: S.current.cardholder_agreement,
onTap: () {},
),
SizedBox(
height: 24,
),
TextIconButton(
label: S.current.e_sign_consent,
onTap: () {},
),
],
),
),
],
),
),
bottomSection: LoadingPrimaryButton(
onPressed: () {
_cardsListViewModel.createCard();
},
isLoading: _cardsListViewModel.createCardState is IoniaCreateCardLoading,
text: S.of(context).agree_and_continue,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
);
}
void _onCreateCardFailure(BuildContext context, String errorMessage) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.error,
alertContent: errorMessage,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
void _onCreateCardSuccess(BuildContext context) {
Navigator.pushNamed(
context,
Routes.ioniaDebitCardPage,
);
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).congratulations,
alertContent: S.of(context).you_now_have_debit_card,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}
@@ -0,0 +1,492 @@
import 'dart:ui';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/ionia/ionia_tip.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/confirm_modal.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/ionia_alert_model.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/discount_badge.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class IoniaBuyGiftCardDetailPage extends BasePage {
IoniaBuyGiftCardDetailPage(this.ioniaPurchaseViewModel);
final IoniaMerchPurchaseViewModel ioniaPurchaseViewModel;
@override
Widget middle(BuildContext context) {
return Text(
ioniaPurchaseViewModel.ioniaMerchant.legalName,
style: textMediumSemiBold(color: Theme.of(context).accentTextTheme.display4.backgroundColor),
);
}
@override
Widget trailing(BuildContext context)
=> ioniaPurchaseViewModel.ioniaMerchant.discount > 0
? DiscountBadge(percentage: ioniaPurchaseViewModel.ioniaMerchant.discount)
: null;
@override
Widget body(BuildContext context) {
reaction((_) => ioniaPurchaseViewModel.invoiceCreationState, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
});
}
});
reaction((_) => ioniaPurchaseViewModel.invoiceCommittingState, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
});
}
if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushReplacementNamed(
Routes.ioniaPaymentStatusPage,
arguments: [
ioniaPurchaseViewModel.paymentInfo,
ioniaPurchaseViewModel.committedInfo]);
});
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Observer(builder: (_) {
final tipAmount = ioniaPurchaseViewModel.tipAmount;
return Column(
children: [
SizedBox(height: 36),
Container(
padding: EdgeInsets.symmetric(vertical: 24),
margin: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [
Theme.of(context).primaryTextTheme.subhead.color,
Theme.of(context).primaryTextTheme.subhead.decorationColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
children: [
Text(
S.of(context).gift_card_amount,
style: textSmall(),
),
SizedBox(height: 4),
Text(
'\$${ioniaPurchaseViewModel.giftCardAmount.toStringAsFixed(2)}',
style: textXLargeSemiBold(),
),
SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context).bill_amount,
style: textSmall(),
),
SizedBox(height: 4),
Text(
'\$${ioniaPurchaseViewModel.billAmount.toStringAsFixed(2)}',
style: textLargeSemiBold(),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
S.of(context).tip,
style: textSmall(),
),
SizedBox(height: 4),
Text(
'\$${tipAmount.toStringAsFixed(2)}',
style: textLargeSemiBold(),
),
],
),
],
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context).tip,
style: TextStyle(
color: Theme.of(context).primaryTextTheme.title.color,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
SizedBox(height: 4),
Observer(
builder: (_) => TipButtonGroup(
selectedTip: ioniaPurchaseViewModel.selectedTip.percentage,
tipsList: ioniaPurchaseViewModel.tips,
onSelect: (value) => ioniaPurchaseViewModel.addTip(value),
),
)
],
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: TextIconButton(
label: S.of(context).how_to_use_card,
onTap: () => _showHowToUseCard(context, ioniaPurchaseViewModel.ioniaMerchant),
),
),
],
);
}),
bottomSection: Column(
children: [
Padding(
padding: EdgeInsets.only(bottom: 12),
child: Observer(builder: (_) {
return LoadingPrimaryButton(
isLoading: ioniaPurchaseViewModel.invoiceCreationState is IsExecutingState ||
ioniaPurchaseViewModel.invoiceCommittingState is IsExecutingState,
onPressed: () => purchaseCard(context),
text: S.of(context).purchase_gift_card,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
);
}),
),
SizedBox(height: 8),
InkWell(
onTap: () => _showTermsAndCondition(context),
child: Text(S.of(context).settings_terms_and_conditions,
style: textMediumSemiBold(
color: Theme.of(context).primaryTextTheme.body1.color,
).copyWith(fontSize: 12)),
),
SizedBox(height: 16)
],
),
);
}
void _showTermsAndCondition(BuildContext context) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return IoniaAlertModal(
title: S.of(context).settings_terms_and_conditions,
content: Align(
alignment: Alignment.bottomLeft,
child: Text(
ioniaPurchaseViewModel.ioniaMerchant.termsAndConditions,
style: textMedium(
color: Theme.of(context).textTheme.display2.color,
),
),
),
actionTitle: S.of(context).agree,
showCloseButton: false,
heightFactor: 0.6,
);
});
}
Future<void> purchaseCard(BuildContext context) async {
await ioniaPurchaseViewModel.createInvoice();
if (ioniaPurchaseViewModel.invoiceCreationState is ExecutedSuccessfullyState) {
await _presentSuccessfulInvoiceCreationPopup(context);
}
}
void _showHowToUseCard(
BuildContext context,
IoniaMerchant merchant,
) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return IoniaAlertModal(
title: S.of(context).how_to_use_card,
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: merchant.instructions
.map((instruction) {
return [
Padding(
padding: EdgeInsets.all(10),
child: Text(
instruction.header,
style: textLargeSemiBold(
color: Theme.of(context).textTheme.display2.color,
),
)),
Text(
instruction.body,
style: textMedium(
color: Theme.of(context).textTheme.display2.color,
),
)
];
})
.expand((e) => e)
.toList()),
actionTitle: S.current.send_got_it,
);
});
}
Future<void> _presentSuccessfulInvoiceCreationPopup(BuildContext context) async {
final amount = ioniaPurchaseViewModel.invoice.totalAmount;
final addresses = ioniaPurchaseViewModel.invoice.outAddresses;
await showPopUp<void>(
context: context,
builder: (_) {
return IoniaConfirmModal(
alertTitle: S.of(context).confirm_sending,
alertContent: Container(
height: 200,
padding: EdgeInsets.all(15),
child: Column(children: [
Row(children: [
Text(S.of(context).payment_id,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none)),
Text(ioniaPurchaseViewModel.invoice.paymentId,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none))
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
SizedBox(height: 10),
Row(children: [
Text(S.of(context).amount,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none)),
Text('$amount ${ioniaPurchaseViewModel.invoice.chain}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none))
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
SizedBox(height: 25),
Row(children: [
Text(S.of(context).recipient_address,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none))
], mainAxisAlignment: MainAxisAlignment.center),
Expanded(
child: ListView.builder(
itemBuilder: (_, int index) {
return Text(addresses[index],
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none));
},
itemCount: addresses.length,
physics: NeverScrollableScrollPhysics()))
])),
rightButtonText: S.of(context).ok,
leftButtonText: S.of(context).cancel,
leftActionColor: Color(0xffFF6600),
rightActionColor: Theme.of(context).accentTextTheme.body2.color,
actionRightButton: () async {
Navigator.of(context).pop();
await ioniaPurchaseViewModel.commitPaymentInvoice();
},
actionLeftButton: () => Navigator.of(context).pop());
},
);
}
}
class TipButtonGroup extends StatelessWidget {
const TipButtonGroup({
Key key,
@required this.selectedTip,
@required this.onSelect,
@required this.tipsList,
}) : super(key: key);
final Function(IoniaTip) onSelect;
final double selectedTip;
final List<IoniaTip> tipsList;
bool _isSelected(double value) => selectedTip == value;
@override
Widget build(BuildContext context) {
return Container(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: tipsList.length,
itemBuilder: (BuildContext context, int index) {
final tip = tipsList[index];
return Padding(
padding: EdgeInsets.only(right: 5),
child: TipButton(
isSelected: _isSelected(tip.percentage),
onTap: () => onSelect(tip),
caption: '${tip.percentage}%',
subTitle: '\$${tip.additionalAmount}',
));
}));
}
}
class TipButton extends StatelessWidget {
const TipButton({
@required this.caption,
this.subTitle,
@required this.onTap,
this.isSelected = false,
});
final String caption;
final String subTitle;
final bool isSelected;
final void Function() onTap;
bool isDark(BuildContext context) => Theme.of(context).brightness == Brightness.dark;
Color captionTextColor(BuildContext context) {
if (isDark(context)) {
return Theme.of(context).primaryTextTheme.title.color;
}
return isSelected
? Theme.of(context).accentTextTheme.title.color
: Theme.of(context).primaryTextTheme.title.color;
}
Color subTitleTextColor(BuildContext context) {
if (isDark(context)) {
return Theme.of(context).primaryTextTheme.title.color;
}
return isSelected
? Theme.of(context).accentTextTheme.title.color
: Theme.of(context).primaryTextTheme.overline.color;
}
Color backgroundColor(BuildContext context) {
if (isDark(context)) {
return isSelected
? null
: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.01);
}
return isSelected
? null
: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1);
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
height: 49,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(caption,
style: textSmallSemiBold(
color: captionTextColor(context))),
if (subTitle != null) ...[
SizedBox(height: 4),
Text(
subTitle,
style: textXxSmallSemiBold(
color: subTitleTextColor(context),
),
),
]
],
),
padding: EdgeInsets.symmetric(horizontal: 18, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: backgroundColor(context),
gradient: isSelected
? LinearGradient(
colors: [
Theme.of(context).primaryTextTheme.subhead.color,
Theme.of(context).primaryTextTheme.subhead.decorationColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
),
),
);
}
}
@@ -0,0 +1,185 @@
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:cake_wallet/generated/i18n.dart';
class IoniaBuyGiftCardPage extends BasePage {
IoniaBuyGiftCardPage(
this.ioniaBuyCardViewModel,
) : _amountFieldFocus = FocusNode(),
_amountController = TextEditingController() {
_amountController.addListener(() {
ioniaBuyCardViewModel.onAmountChanged(_amountController.text);
});
}
final IoniaBuyCardViewModel ioniaBuyCardViewModel;
@override
String get title => S.current.enter_amount;
@override
Color get titleColor => Colors.white;
@override
bool get extendBodyBehindAppBar => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.transparent;
Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939);
final TextEditingController _amountController;
final FocusNode _amountFieldFocus;
@override
Widget body(BuildContext context) {
final _width = MediaQuery.of(context).size.width;
final merchant = ioniaBuyCardViewModel.ioniaMerchant;
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).accentTextTheme.body2.backgroundColor,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _amountFieldFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
]),
child: Container(
color: Theme.of(context).backgroundColor,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 25),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme.subhead.color,
Theme.of(context).primaryTextTheme.subhead.decorationColor,
], begin: Alignment.topLeft, end: Alignment.bottomRight),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 150),
BaseTextFormField(
controller: _amountController,
focusNode: _amountFieldFocus,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')),
WhitelistingTextInputFormatter(RegExp(r'^\d+(\.|\,)?\d{0,2}'))],
hintText: '1000',
placeholderTextStyle: TextStyle(
color: Theme.of(context).primaryTextTheme.headline.color,
fontWeight: FontWeight.w600,
fontSize: 36,
),
borderColor: Theme.of(context).primaryTextTheme.headline.color,
textColor: Colors.white,
textStyle: TextStyle(
color: Colors.white,
fontSize: 36,
),
suffixIcon: SizedBox(
width: _width / 6,
),
prefixIcon: Padding(
padding: EdgeInsets.only(
top: 5.0,
left: _width / 4,
),
child: Text(
'USD: ',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 36,
),
),
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context).min_amount(merchant.minimumCardPurchase.toStringAsFixed(2)),
style: TextStyle(
color: Theme.of(context).primaryTextTheme.headline.color,
),
),
Text(
S.of(context).max_amount(merchant.maximumCardPurchase.toStringAsFixed(2)),
style: TextStyle(
color: Theme.of(context).primaryTextTheme.headline.color,
),
),
],
),
SizedBox(height: 24),
],
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: CardItem(
title: merchant.legalName,
backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1),
discount: merchant.discount,
titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor,
subtitleColor: Theme.of(context).hintColor,
subTitle: merchant.isOnline ? S.of(context).online : S.of(context).offline,
logoUrl: merchant.logoUrl,
),
)
],
),
bottomSection: Column(
children: [
Observer(builder: (_) {
return Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
onPressed: () => Navigator.of(context).pushNamed(
Routes.ioniaBuyGiftCardDetailPage,
arguments: [
ioniaBuyCardViewModel.amount,
ioniaBuyCardViewModel.ioniaMerchant,
],
),
text: S.of(context).continue_text,
isDisabled: !ioniaBuyCardViewModel.isEnablePurchase,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
);
}),
SizedBox(height: 30),
],
),
),
),
);
}
}
@@ -0,0 +1,176 @@
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:cake_wallet/generated/i18n.dart';
class IoniaCustomTipPage extends BasePage {
IoniaCustomTipPage(
this.ioniaPurchaseViewModel,
) : _amountFieldFocus = FocusNode(),
_amountController = TextEditingController() {
_amountController.addListener(() {
// ioniaPurchaseViewModel.onTipChanged(_amountController.text);
});
}
final IoniaMerchPurchaseViewModel ioniaPurchaseViewModel;
@override
String get title => S.current.enter_amount;
@override
Color get titleColor => Colors.white;
@override
bool get extendBodyBehindAppBar => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.transparent;
Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939);
final TextEditingController _amountController;
final FocusNode _amountFieldFocus;
@override
Widget body(BuildContext context) {
final _width = MediaQuery.of(context).size.width;
final merchant = ioniaPurchaseViewModel.ioniaMerchant;
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).accentTextTheme.body2.backgroundColor,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _amountFieldFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
]),
child: Container(
color: Theme.of(context).backgroundColor,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 25),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme.subhead.color,
Theme.of(context).primaryTextTheme.subhead.decorationColor,
], begin: Alignment.topLeft, end: Alignment.bottomRight),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 150),
BaseTextFormField(
controller: _amountController,
focusNode: _amountFieldFocus,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\-|\ ]'))],
hintText: '1000',
placeholderTextStyle: TextStyle(
color: Theme.of(context).primaryTextTheme.headline.color,
fontWeight: FontWeight.w500,
fontSize: 36,
),
borderColor: Theme.of(context).primaryTextTheme.headline.color,
textColor: Colors.white,
textStyle: TextStyle(
color: Colors.white,
fontSize: 36,
),
suffixIcon: SizedBox(
width: _width / 6,
),
prefixIcon: Padding(
padding: EdgeInsets.only(
top: 5.0,
left: _width / 4,
),
child: Text(
'USD: ',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 36,
),
),
),
),
SizedBox(height: 8),
Observer(builder: (_) {
if (ioniaPurchaseViewModel.percentage == 0.0) {
return SizedBox.shrink();
}
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: '\$${_amountController.text}',
style: TextStyle(
color: Theme.of(context).primaryTextTheme.headline.color,
),
children: [
TextSpan(text: ' ${S.of(context).is_percentage} '),
TextSpan(text: '${ioniaPurchaseViewModel.percentage}%'),
TextSpan(text: ' ${S.of(context).percentageOf(ioniaPurchaseViewModel.amount.toString())} '),
],
),
);
}),
SizedBox(height: 24),
],
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: CardItem(
title: merchant.legalName,
backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1),
discount: 0.0,
titleColor: Theme.of(context).accentTextTheme.display4.backgroundColor,
subtitleColor: Theme.of(context).hintColor,
subTitle: merchant.isOnline ? S.of(context).online : S.of(context).offline,
logoUrl: merchant.logoUrl,
),
)
],
),
bottomSection: Column(
children: [
Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
onPressed: () {
Navigator.of(context).pop(_amountController.text);
},
text: S.of(context).add_tip,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
),
SizedBox(height: 30),
],
),
),
),
);
}
}
@@ -0,0 +1,379 @@
import 'package:cake_wallet/ionia/ionia_create_state.dart';
import 'package:cake_wallet/ionia/ionia_virtual_card.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class IoniaDebitCardPage extends BasePage {
final IoniaGiftCardsListViewModel _cardsListViewModel;
IoniaDebitCardPage(this._cardsListViewModel);
@override
Widget middle(BuildContext context) {
return Text(
S.current.debit_card,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor,
),
);
}
@override
Widget body(BuildContext context) {
return Observer(
builder: (_) {
final cardState = _cardsListViewModel.cardState;
if (cardState is IoniaFetchingCard) {
return Center(child: CircularProgressIndicator());
}
if (cardState is IoniaCardSuccess) {
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Padding(
padding: const EdgeInsets.all(16.0),
child: _IoniaDebitCard(
cardInfo: cardState.card,
),
),
bottomSection: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Text(
S.of(context).billing_address_info,
style: textSmall(color: Theme.of(context).textTheme.display1.color),
textAlign: TextAlign.center,
),
),
SizedBox(height: 24),
PrimaryButton(
text: S.of(context).order_physical_card,
onPressed: () {},
color: Color(0xffE9F2FC),
textColor: Theme.of(context).textTheme.display2.color,
),
SizedBox(height: 8),
PrimaryButton(
text: S.of(context).add_value,
onPressed: () {},
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
SizedBox(height: 16)
],
),
);
}
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_IoniaDebitCard(isCardSample: true),
SizedBox(height: 40),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: [
TextIconButton(
label: S.current.how_to_use_card,
onTap: () => _showHowToUseCard(context),
),
SizedBox(
height: 24,
),
TextIconButton(
label: S.current.frequently_asked_questions,
onTap: () {},
),
],
),
),
SizedBox(height: 50),
Container(
padding: EdgeInsets.all(20),
margin: EdgeInsets.all(8),
width: double.infinity,
decoration: BoxDecoration(
color: Color.fromRGBO(233, 242, 252, 1),
borderRadius: BorderRadius.circular(20),
),
child: RichText(
text: TextSpan(
text: S.of(context).get_a,
style: textMedium(color: Theme.of(context).textTheme.display2.color),
children: [
TextSpan(
text: S.of(context).digital_and_physical_card,
style: textMediumBold(color: Theme.of(context).textTheme.display2.color),
),
TextSpan(
text: S.of(context).get_card_note,
)
],
)),
),
],
),
),
bottomSectionPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 32,
),
bottomSection: PrimaryButton(
text: S.of(context).activate,
onPressed: () => _showHowToUseCard(context, activate: true),
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
),
);
},
);
}
void _showHowToUseCard(BuildContext context, {bool activate = false}) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertBackground(
child: Material(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(height: 10),
Container(
padding: EdgeInsets.only(top: 24, left: 24, right: 24),
margin: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
borderRadius: BorderRadius.circular(30),
),
child: Column(
children: [
Text(
S.of(context).how_to_use_card,
style: textLargeSemiBold(
color: Theme.of(context).textTheme.body1.color,
),
),
SizedBox(height: 24),
Align(
alignment: Alignment.bottomLeft,
child: Text(
S.of(context).signup_for_card_accept_terms,
style: textSmallSemiBold(
color: Theme.of(context).textTheme.display2.color,
),
),
),
SizedBox(height: 24),
_TitleSubtitleTile(
title: S.of(context).add_fund_to_card('1000'),
subtitle: S.of(context).use_card_info_two,
),
SizedBox(height: 21),
_TitleSubtitleTile(
title: S.of(context).use_card_info_three,
subtitle: S.of(context).optionally_order_card,
),
SizedBox(height: 35),
PrimaryButton(
onPressed: () => activate
? Navigator.pushNamed(context, Routes.ioniaActivateDebitCardPage)
: Navigator.pop(context),
text: S.of(context).send_got_it,
color: Color.fromRGBO(233, 242, 252, 1),
textColor: Theme.of(context).textTheme.display2.color,
),
SizedBox(height: 21),
],
),
),
InkWell(
onTap: () => Navigator.pop(context),
child: Container(
margin: EdgeInsets.only(bottom: 40),
child: CircleAvatar(
child: Icon(
Icons.close,
color: Colors.black,
),
backgroundColor: Colors.white,
),
),
)
],
),
),
);
});
}
}
class _IoniaDebitCard extends StatefulWidget {
final bool isCardSample;
final IoniaVirtualCard cardInfo;
const _IoniaDebitCard({
Key key,
this.isCardSample = false,
this.cardInfo,
}) : super(key: key);
@override
_IoniaDebitCardState createState() => _IoniaDebitCardState();
}
class _IoniaDebitCardState extends State<_IoniaDebitCard> {
bool _showDetails = false;
void _toggleVisibility() {
setState(() => _showDetails = !_showDetails);
}
String _formatPan(String pan) {
if (pan == null) return '';
return pan.replaceAllMapped(RegExp(r'.{4}'), (match) => '${match.group(0)} ');
}
String get _getLast4 => widget.isCardSample ? '0000' : widget.cardInfo.pan.substring(widget.cardInfo.pan.length - 5);
String get _getSpendLimit => widget.isCardSample ? '10000' : widget.cardInfo.spendLimit.toStringAsFixed(2);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 19),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
colors: [
Theme.of(context).primaryTextTheme.subhead.color,
Theme.of(context).primaryTextTheme.subhead.decorationColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.current.cakepay_prepaid_card,
style: textSmall(),
),
Image.asset(
'assets/images/mastercard.png',
width: 54,
),
],
),
Text(
widget.isCardSample ? S.of(context).upto(_getSpendLimit) : '\$$_getSpendLimit',
style: textXLargeSemiBold(),
),
SizedBox(height: 16),
Text(
_showDetails ? _formatPan(widget.cardInfo.pan) : '**** **** **** $_getLast4',
style: textMediumSemiBold(),
),
SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.isCardSample)
Text(
S.current.no_id_needed,
style: textMediumBold(),
)
else ...[
Column(
children: [
Text(
'CVV',
style: textXSmallSemiBold(),
),
SizedBox(height: 4),
Text(
_showDetails ? widget.cardInfo.cvv : '***',
style: textMediumSemiBold(),
)
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context).expires,
style: textXSmallSemiBold(),
),
SizedBox(height: 4),
Text(
'${widget.cardInfo.expirationMonth ?? S.of(context).mm}/${widget.cardInfo.expirationYear ?? S.of(context).yy}',
style: textMediumSemiBold(),
)
],
),
]
],
),
if (!widget.isCardSample) ...[
SizedBox(height: 8),
Center(
child: InkWell(
onTap: () => _toggleVisibility(),
child: Text(
_showDetails ? S.of(context).hide_details : S.of(context).show_details,
style: textSmall(),
),
),
),
],
],
),
);
}
}
class _TitleSubtitleTile extends StatelessWidget {
final String title;
final String subtitle;
const _TitleSubtitleTile({
Key key,
@required this.title,
@required this.subtitle,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textSmallSemiBold(color: Theme.of(context).textTheme.display2.color),
),
SizedBox(height: 4),
Text(
subtitle,
style: textSmall(color: Theme.of(context).textTheme.display2.color),
),
],
);
}
}
@@ -0,0 +1,188 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/ionia_alert_model.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/ionia_tile.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class IoniaGiftCardDetailPage extends BasePage {
IoniaGiftCardDetailPage(this.viewModel);
final IoniaGiftCardDetailsViewModel viewModel;
@override
Widget leading(BuildContext context) {
if (ModalRoute.of(context).isFirst) {
return null;
}
final _backButton = Icon(
Icons.arrow_back_ios,
color: Theme.of(context).primaryTextTheme.title.color,
size: 16,
);
return Padding(
padding: const EdgeInsets.only(left: 10.0),
child: SizedBox(
height: 37,
width: 37,
child: ButtonTheme(
minWidth: double.minPositive,
child: FlatButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.all(0),
onPressed: () => onClose(context),
child: _backButton),
),
),
);
}
@override
Widget middle(BuildContext context) {
return Text(
viewModel.giftCard.legalName,
style: textMediumSemiBold(color: Theme.of(context).accentTextTheme.display4.backgroundColor),
);
}
@override
Widget body(BuildContext context) {
reaction((_) => viewModel.redeemState, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
});
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Column(
children: [
if (viewModel.giftCard.barcodeUrl != null && viewModel.giftCard.barcodeUrl.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 24,
),
child: Image.network(viewModel.giftCard.barcodeUrl),
),
SizedBox(height: 24),
buildIoniaTile(
context,
title: S.of(context).gift_card_number,
subTitle: viewModel.giftCard.cardNumber,
),
if (viewModel.giftCard.cardPin?.isNotEmpty ?? false)
...[Divider(height: 30),
buildIoniaTile(
context,
title: S.of(context).pin_number,
subTitle: viewModel.giftCard.cardPin,
)],
Divider(height: 30),
Observer(builder: (_) =>
buildIoniaTile(
context,
title: S.of(context).amount,
subTitle: viewModel.giftCard.remainingAmount.toStringAsFixed(2) ?? '0.00',
)),
Divider(height: 50),
TextIconButton(
label: S.of(context).how_to_use_card,
onTap: () => _showHowToUseCard(context, viewModel.giftCard),
),
],
),
bottomSection: Padding(
padding: EdgeInsets.only(bottom: 12),
child: Observer(builder: (_) {
if (!viewModel.giftCard.isEmpty) {
return LoadingPrimaryButton(
isLoading: viewModel.redeemState is IsExecutingState,
onPressed: () => viewModel.redeem().then((_){
Navigator.of(context).pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst);
}),
text: S.of(context).mark_as_redeemed,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white);
}
return Container();
})),
);
}
Widget buildIoniaTile(BuildContext context, {@required String title, @required String subTitle}) {
return IoniaTile(
title: title,
subTitle: subTitle,
onTap: () {
Clipboard.setData(ClipboardData(text: subTitle));
showBar<void>(context,
S.of(context).transaction_details_copied(title));
});
}
void _showHowToUseCard(
BuildContext context,
IoniaGiftCard merchant,
) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return IoniaAlertModal(
title: S.of(context).how_to_use_card,
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: viewModel.giftCard.instructions
.map((instruction) {
return [
Padding(
padding: EdgeInsets.all(10),
child: Text(
instruction.header,
style: textLargeSemiBold(
color: Theme.of(context).textTheme.display2.color,
),
)),
Text(
instruction.body,
style: textMedium(
color: Theme.of(context).textTheme.display2.color,
),
)
];
})
.expand((e) => e)
.toList()),
actionTitle: S.of(context).send_got_it,
);
});
}
}
@@ -0,0 +1,340 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/card_menu.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/ionia_filter_modal.dart';
import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/debounce.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class IoniaManageCardsPage extends BasePage {
IoniaManageCardsPage(this._cardsListViewModel) {
_searchController.addListener(() {
if (_searchController.text != _cardsListViewModel.searchString) {
_searchDebounce.run(() {
_cardsListViewModel.searchMerchant(_searchController.text);
});
}
});
}
final IoniaGiftCardsListViewModel _cardsListViewModel;
final _searchDebounce = Debounce(Duration(milliseconds: 500));
final _searchController = TextEditingController();
@override
Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white;
@override
Color get backgroundDarkColor => Colors.transparent;
@override
Color get titleColor => currentTheme.type == ThemeType.bright ? Colors.white : Colors.black;
@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
bool get resizeToAvoidBottomInset => false;
@override
Widget get endDrawer => CardMenu();
@override
Widget leading(BuildContext context) {
final _backButton = Icon(
Icons.arrow_back_ios,
color: Theme.of(context).accentTextTheme.display3.backgroundColor,
size: 16,
);
return SizedBox(
height: 37,
width: 37,
child: ButtonTheme(
minWidth: double.minPositive,
child: FlatButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.all(0),
onPressed: () => Navigator.pop(context),
child: _backButton),
),
);
}
@override
Widget middle(BuildContext context) {
return Text(
S.of(context).gift_cards,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display3.backgroundColor,
),
);
}
@override
Widget trailing(BuildContext context) {
return _TrailingIcon(
asset: 'assets/images/profile.png',
onPressed: () => Navigator.pushNamed(context, Routes.ioniaAccountPage),
);
}
@override
Widget body(BuildContext context) {
final filterIcon = InkWell(
onTap: () async {
final selectedFilters = await showCategoryFilter(context, _cardsListViewModel);
_cardsListViewModel.setSelectedFilter(selectedFilters);
},
child: Image.asset(
'assets/images/filter.png',
color: Theme.of(context).textTheme.caption.decorationColor,
));
return Padding(
padding: const EdgeInsets.all(14.0),
child: Column(
children: [
Container(
padding: EdgeInsets.only(left: 2, right: 22),
height: 32,
child: Row(
children: [
Expanded(
child: _SearchWidget(
controller: _searchController,
)),
SizedBox(width: 10),
Container(
width: 32,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
border: Border.all(
color: Colors.white.withOpacity(0.2),
),
borderRadius: BorderRadius.circular(10),
),
child: filterIcon,
)
],
),
),
SizedBox(height: 8),
Expanded(
child: IoniaManageCardsPageBody(
cardsListViewModel: _cardsListViewModel,
),
),
],
),
);
}
Future<List<IoniaCategory>> showCategoryFilter(
BuildContext context,
IoniaGiftCardsListViewModel viewModel,
) async {
return await showPopUp<List<IoniaCategory>>(
context: context,
builder: (BuildContext context) {
return IoniaFilterModal(
filterViewModel: getIt.get<IoniaFilterViewModel>(),
selectedCategories: viewModel.selectedFilters,
);
},
);
}
}
class IoniaManageCardsPageBody extends StatefulWidget {
const IoniaManageCardsPageBody({
Key key,
@required this.cardsListViewModel,
}) : super(key: key);
final IoniaGiftCardsListViewModel cardsListViewModel;
@override
_IoniaManageCardsPageBodyState createState() => _IoniaManageCardsPageBodyState();
}
class _IoniaManageCardsPageBodyState extends State<IoniaManageCardsPageBody> {
double get backgroundHeight => MediaQuery.of(context).size.height * 0.75;
double thumbHeight = 72;
bool get isAlwaysShowScrollThumb => merchantsList == null ? false : merchantsList.length > 3;
List<IoniaMerchant> get merchantsList => widget.cardsListViewModel.ioniaMerchants;
final _scrollController = ScrollController();
@override
void initState() {
_scrollController.addListener(() {
final scrollOffsetFromTop = _scrollController.hasClients
? (_scrollController.offset / _scrollController.position.maxScrollExtent * (backgroundHeight - thumbHeight))
: 0.0;
widget.cardsListViewModel.setScrollOffsetFromTop(scrollOffsetFromTop);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) => Stack(children: [
ListView.separated(
padding: EdgeInsets.only(left: 2, right: 22),
controller: _scrollController,
itemCount: merchantsList.length,
separatorBuilder: (_, __) => SizedBox(height: 4),
itemBuilder: (_, index) {
final merchant = merchantsList[index];
var subTitle = '';
if (merchant.isOnline) {
subTitle += S.of(context).online;
}
if (merchant.isPhysical) {
if (subTitle.isNotEmpty) {
subTitle = '$subTitle & ';
}
subTitle = '${subTitle}${S.of(context).in_store}';
}
return CardItem(
logoUrl: merchant.logoUrl,
onTap: () {
Navigator.of(context).pushNamed(Routes.ioniaBuyGiftCardPage, arguments: [merchant]);
},
title: merchant.legalName,
subTitle: subTitle,
backgroundColor: Theme.of(context).textTheme.title.backgroundColor,
titleColor: Theme.of(context).accentTextTheme.display3.backgroundColor,
subtitleColor: Theme.of(context).accentTextTheme.display2.backgroundColor,
discount: merchant.discount,
);
},
),
isAlwaysShowScrollThumb
? CakeScrollbar(
backgroundHeight: backgroundHeight,
thumbHeight: thumbHeight,
rightOffset: 1,
width: 3,
backgroundColor: Theme.of(context).textTheme.caption.decorationColor.withOpacity(0.05),
thumbColor: Theme.of(context).textTheme.caption.decorationColor.withOpacity(0.5),
fromTop: widget.cardsListViewModel.scrollOffsetFromTop,
)
: Offstage()
]),
);
}
}
class _SearchWidget extends StatelessWidget {
const _SearchWidget({
Key key,
@required this.controller,
}) : super(key: key);
final TextEditingController controller;
@override
Widget build(BuildContext context) {
final searchIcon = Padding(
padding: EdgeInsets.all(8),
child: Image.asset(
'assets/images/mini_search_icon.png',
color: Theme.of(context).textTheme.caption.decorationColor,
),
);
return TextField(
style: TextStyle(color: Colors.white),
controller: controller,
decoration: InputDecoration(
filled: true,
contentPadding: EdgeInsets.only(
top: 10,
left: 10,
),
fillColor: Colors.white.withOpacity(0.15),
hintText: S.of(context).search,
hintStyle: TextStyle(
color: Colors.white.withOpacity(0.6),
),
alignLabelWithHint: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
suffixIcon: searchIcon,
border: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white.withOpacity(0.2),
),
borderRadius: BorderRadius.circular(10),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white.withOpacity(0.2),
),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white.withOpacity(0.2)),
borderRadius: BorderRadius.circular(10),
)),
);
}
}
class _TrailingIcon extends StatelessWidget {
final String asset;
final VoidCallback onPressed;
const _TrailingIcon({this.asset, this.onPressed});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerRight,
width: 25,
child: FlatButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.all(0),
onPressed: onPressed,
child: Image.asset(
asset,
color: Theme.of(context).accentTextTheme.display3.backgroundColor,
),
),
);
}
}
@@ -0,0 +1,217 @@
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class IoniaPaymentStatusPage extends BasePage {
IoniaPaymentStatusPage(this.viewModel);
final IoniaPaymentStatusViewModel viewModel;
@override
Widget middle(BuildContext context) {
return Text(
S.of(context).generating_gift_card,
textAlign: TextAlign.center,
style: textMediumSemiBold(
color: Theme.of(context).accentTextTheme.display4.backgroundColor));
}
@override
Widget body(BuildContext context) {
return _IoniaPaymentStatusPageBody(viewModel);
}
}
class _IoniaPaymentStatusPageBody extends StatefulWidget {
_IoniaPaymentStatusPageBody(this.viewModel);
final IoniaPaymentStatusViewModel viewModel;
@override
_IoniaPaymentStatusPageBodyBodyState createState() => _IoniaPaymentStatusPageBodyBodyState();
}
class _IoniaPaymentStatusPageBodyBodyState extends State<_IoniaPaymentStatusPageBody> {
ReactionDisposer _onGiftCardReaction;
@override
void initState() {
if (widget.viewModel.giftCard != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context)
.pushReplacementNamed(Routes.ioniaGiftCardDetailPage, arguments: [widget.viewModel.giftCard]);
});
}
_onGiftCardReaction = reaction((_) => widget.viewModel.giftCard, (IoniaGiftCard giftCard) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context)
.pushReplacementNamed(Routes.ioniaGiftCardDetailPage, arguments: [giftCard]);
});
});
super.initState();
}
@override
void dispose() {
_onGiftCardReaction?.reaction?.dispose();
widget.viewModel.timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(children: [
Padding(
padding: EdgeInsets.only(right: 10),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.green),
height: 10,
width: 10)),
Text(
S.of(context).awaiting_payment_confirmation,
style: textLargeSemiBold(
color: Theme.of(context).primaryTextTheme.title.color))
]),
SizedBox(height: 40),
Row(children: [
SizedBox(width: 20),
Expanded(child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
...widget.viewModel
.committedInfo
.transactions
.map((transaction) => buildDescriptionTileWithCopy(context, S.of(context).transaction_details_transaction_id, transaction.id)),
Divider(height: 30),
buildDescriptionTileWithCopy(context, S.of(context).order_id, widget.viewModel.paymentInfo.ioniaOrder.id),
Divider(height: 30),
buildDescriptionTileWithCopy(context, S.of(context).payment_id, widget.viewModel.paymentInfo.ioniaOrder.paymentId),
]))
]),
SizedBox(height: 40),
Observer(builder: (_) {
if (widget.viewModel.giftCard != null) {
return Container(
padding: EdgeInsets.only(top: 40),
child: Row(children: [
Padding(
padding: EdgeInsets.only(right: 10,),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.green),
height: 10,
width: 10)),
Text(
S.of(context).gift_card_is_generated,
style: textLargeSemiBold(
color: Theme.of(context).primaryTextTheme.title.color))
]));
}
return Row(children: [
Padding(
padding: EdgeInsets.only(right: 10),
child: Observer(builder: (_) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: widget.viewModel.giftCard == null ? Colors.grey : Colors.green),
height: 10,
width: 10);
})),
Text(
S.of(context).generating_gift_card,
style: textLargeSemiBold(
color: Theme.of(context).primaryTextTheme.title.color))]);
}),
],
),
bottomSection: Padding(
padding: EdgeInsets.only(bottom: 12),
child: Column(children: [
Container(
padding: EdgeInsets.only(left: 40, right: 40, bottom: 20),
child: Text(
S.of(context).proceed_after_one_minute,
style: textMedium(
color: Theme.of(context).primaryTextTheme.title.color,
).copyWith(fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
)),
Observer(builder: (_) {
if (widget.viewModel.giftCard != null) {
return PrimaryButton(
onPressed: () => Navigator.of(context)
.pushReplacementNamed(
Routes.ioniaGiftCardDetailPage,
arguments: [widget.viewModel.giftCard]),
text: S.of(context).open_gift_card,
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white);
}
return PrimaryButton(
onPressed: () => Navigator.of(context).pushNamed(Routes.support),
text: S.of(context).contact_support,
color: Theme.of(context).accentTextTheme.caption.color,
textColor: Theme.of(context).primaryTextTheme.title.color);
})
])
),
);
}
Widget buildDescriptionTile(BuildContext context, String title, String subtitle, VoidCallback onTap) {
return GestureDetector(
onTap: () => onTap(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textXSmall(
color: Theme.of(context).primaryTextTheme.overline.color,
),
),
SizedBox(height: 8),
Text(
subtitle,
style: textMedium(
color: Theme.of(context).primaryTextTheme.title.color,
),
),
],
));
}
Widget buildDescriptionTileWithCopy(BuildContext context, String title, String subtitle) {
return buildDescriptionTile(context, title, subtitle, () {
Clipboard.setData(ClipboardData(text: subtitle));
showBar<void>(context,
S.of(context).transaction_details_copied(title));
});
}
}
+9
View File
@@ -0,0 +1,9 @@
export 'auth/ionia_welcome_page.dart';
export 'auth/ionia_create_account_page.dart';
export 'auth/ionia_login_page.dart';
export 'auth/ionia_verify_otp_page.dart';
export 'cards/ionia_activate_debit_card_page.dart';
export 'cards/ionia_buy_card_detail_page.dart';
export 'cards/ionia_manage_cards_page.dart';
export 'cards/ionia_debit_card_page.dart';
export 'cards/ionia_buy_gift_card.dart';
@@ -0,0 +1,139 @@
import 'package:cake_wallet/src/widgets/discount_badge.dart';
import 'package:flutter/material.dart';
class CardItem extends StatelessWidget {
CardItem({
@required this.title,
@required this.subTitle,
@required this.backgroundColor,
@required this.titleColor,
@required this.subtitleColor,
this.discountBackground,
this.onTap,
this.logoUrl,
this.discount,
});
final VoidCallback onTap;
final String title;
final String subTitle;
final String logoUrl;
final double discount;
final Color backgroundColor;
final Color titleColor;
final Color subtitleColor;
final AssetImage discountBackground;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Stack(
children: [
Container(
padding: EdgeInsets.all(12),
width: double.infinity,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.20),
),
),
child: Row(
children: [
if (logoUrl != null) ...[
ClipOval(
child: Image.network(
logoUrl,
width: 40.0,
height: 40.0,
fit: BoxFit.cover,
loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent loadingProgress) {
if (loadingProgress == null) {
return child;
} else {
return _PlaceholderContainer(text: 'Logo');
}
},
errorBuilder: (_, __, ___) => _PlaceholderContainer(text: '!'),
),
),
SizedBox(width: 5),
],
Column(
crossAxisAlignment: (subTitle?.isEmpty ?? false)
? CrossAxisAlignment.center
: CrossAxisAlignment.start,
children: [
SizedBox(
width: 200,
child: Text(
title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: titleColor,
fontSize: 20,
fontWeight: FontWeight.w900,
),
),
),
if (subTitle?.isNotEmpty ?? false)
Padding(
padding: EdgeInsets.only(top: 5),
child: Text(
subTitle,
style: TextStyle(
color: subtitleColor,
fontWeight: FontWeight.w500,
fontFamily: 'Lato')),
)
],
),
],
),
),
if (discount != 0.0)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 20.0),
child: DiscountBadge(
percentage: discount,
discountBackground: discountBackground,
),
),
),
],
),
);
}
}
class _PlaceholderContainer extends StatelessWidget {
const _PlaceholderContainer({@required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Container(
height: 42,
width: 42,
child: Center(
child: Text(
text,
style: TextStyle(
color: Colors.black,
fontSize: 12,
fontWeight: FontWeight.w900,
),
),
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(100),
),
);
}
}
@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
class CardMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
);
}
}
@@ -0,0 +1,148 @@
import 'dart:ui';
import 'package:cake_wallet/palette.dart';
import 'package:flutter/material.dart';
class IoniaConfirmModal extends StatelessWidget {
IoniaConfirmModal({
@required this.alertTitle,
@required this.alertContent,
@required this.leftButtonText,
@required this.rightButtonText,
@required this.actionLeftButton,
@required this.actionRightButton,
this.leftActionColor,
this.rightActionColor,
this.hideActions = false,
});
final String alertTitle;
final Widget alertContent;
final String leftButtonText;
final String rightButtonText;
final VoidCallback actionLeftButton;
final VoidCallback actionRightButton;
final Color leftActionColor;
final Color rightActionColor;
final bool hideActions;
Widget actionButtons(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IoniaActionButton(
buttonText: leftButtonText,
action: actionLeftButton,
backgoundColor: leftActionColor,
),
Container(
width: 1,
height: 52,
color: Theme.of(context).dividerColor,
),
IoniaActionButton(
buttonText: rightButtonText,
action: actionRightButton,
backgoundColor: rightActionColor,
),
],
);
}
Widget title(BuildContext context) {
return Text(
alertTitle,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.title.color,
decoration: TextDecoration.none,
),
);
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.transparent,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
child: Container(
decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)),
child: Center(
child: GestureDetector(
onTap: () => null,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
width: 327,
color: Theme.of(context).accentTextTheme.title.decorationColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.fromLTRB(24, 20, 24, 0),
child: title(context),
),
Padding(
padding: EdgeInsets.only(top: 16, bottom: 8),
child: Container(
height: 1,
color: Theme.of(context).dividerColor,
),
),
alertContent,
actionButtons(context),
],
),
),
),
),
),
),
),
);
}
}
class IoniaActionButton extends StatelessWidget {
const IoniaActionButton({
@required this.buttonText,
@required this.action,
this.backgoundColor,
});
final String buttonText;
final VoidCallback action;
final Color backgoundColor;
@override
Widget build(BuildContext context) {
return Flexible(
child: Container(
height: 52,
padding: EdgeInsets.only(left: 6, right: 6),
color: backgoundColor,
child: ButtonTheme(
minWidth: double.infinity,
child: FlatButton(
onPressed: action,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: Text(
buttonText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: backgoundColor != null ? Colors.white : Theme.of(context).primaryTextTheme.body1.backgroundColor,
decoration: TextDecoration.none,
),
)),
),
));
}
}
@@ -0,0 +1,86 @@
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
class IoniaAlertModal extends StatelessWidget {
const IoniaAlertModal({
Key key,
@required this.title,
@required this.content,
@required this.actionTitle,
this.heightFactor = 0.4,
this.showCloseButton = true,
}) : super(key: key);
final String title;
final Widget content;
final String actionTitle;
final bool showCloseButton;
final double heightFactor;
@override
Widget build(BuildContext context) {
return AlertBackground(
child: Material(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Spacer(),
Container(
padding: EdgeInsets.only(top: 24, left: 24, right: 24),
margin: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
borderRadius: BorderRadius.circular(30),
),
child: Column(
children: [
if (title.isNotEmpty)
Text(
title,
style: textLargeSemiBold(
color: Theme.of(context).textTheme.body1.color,
),
),
Container(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * heightFactor),
child: ListView(
children: [
content,
SizedBox(height: 35),
],
),
),
PrimaryButton(
onPressed: () => Navigator.pop(context),
text: actionTitle,
color: Theme.of(context).accentTextTheme.caption.color,
textColor: Theme.of(context).primaryTextTheme.title.color,
),
SizedBox(height: 21),
],
),
),
Spacer(),
if(showCloseButton)
InkWell(
onTap: () => Navigator.pop(context),
child: Container(
margin: EdgeInsets.only(bottom: 40),
child: CircleAvatar(
child: Icon(
Icons.close,
color: Colors.black,
),
backgroundColor: Colors.white,
),
),
)
],
),
),
);
}
}
@@ -0,0 +1,134 @@
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/src/screens/ionia/widgets/rounded_checkbox.dart';
import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class IoniaFilterModal extends StatelessWidget {
IoniaFilterModal({
@required this.filterViewModel,
@required this.selectedCategories,
}) {
filterViewModel.setSelectedCategories(this.selectedCategories);
}
final IoniaFilterViewModel filterViewModel;
final List<IoniaCategory> selectedCategories;
@override
Widget build(BuildContext context) {
final searchIcon = Padding(
padding: EdgeInsets.all(10),
child: Image.asset(
'assets/images/mini_search_icon.png',
color: Theme.of(context).accentColor,
),
);
return Scaffold(
resizeToAvoidBottomInset: false,
body: AlertBackground(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(height: 10),
Container(
padding: EdgeInsets.only(top: 24, bottom: 20),
margin: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
borderRadius: BorderRadius.circular(30),
),
child: Column(
children: [
SizedBox(
height: 40,
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: TextField(
onChanged: filterViewModel.onSearchFilter,
style: textMedium(
color: Theme.of(context).primaryTextTheme.title.color,
),
decoration: InputDecoration(
filled: true,
prefixIcon: searchIcon,
hintText: S.of(context).search_category,
contentPadding: EdgeInsets.only(bottom: 5),
fillColor: Theme.of(context).textTheme.subhead.backgroundColor,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(8),
),
),
),
),
),
SizedBox(height: 10),
Divider(thickness: 2),
SizedBox(height: 24),
Observer(builder: (_) {
return ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: filterViewModel.ioniaCategories.length,
itemBuilder: (_, index) {
final category = filterViewModel.ioniaCategories[index];
return Padding(
padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
child: InkWell(
onTap: () => filterViewModel.selectFilter(category),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
category.iconPath,
color: Theme.of(context).primaryTextTheme.title.color,
),
SizedBox(width: 10),
Text(category.title,
style: textSmall(
color: Theme.of(context).primaryTextTheme.title.color,
).copyWith(fontWeight: FontWeight.w500)),
],
),
Observer(builder: (_) {
final value = filterViewModel.selectedIndices;
return RoundedCheckbox(
value: value.contains(category.index),
);
}),
],
),
),
);
},
);
}),
],
),
),
InkWell(
onTap: () => Navigator.pop(context, filterViewModel.selectedCategories),
child: Container(
margin: EdgeInsets.only(bottom: 40),
child: CircleAvatar(
child: Icon(
Icons.close,
color: Colors.black,
),
backgroundColor: Colors.white,
),
),
)
],
),
),
);
}
}
@@ -0,0 +1,44 @@
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
class IoniaTile extends StatelessWidget {
const IoniaTile({
Key key,
@required this.title,
@required this.subTitle,
this.onTap,
}) : super(key: key);
final VoidCallback onTap;
final String title;
final String subTitle;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onTap(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textXSmall(
color: Theme.of(context).primaryTextTheme.overline.color,
),
),
SizedBox(height: 8),
Text(
subTitle,
style: textMediumBold(
color: Theme.of(context).primaryTextTheme.title.color,
),
),
],
)
],
));
}
}
@@ -0,0 +1,27 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class RoundedCheckbox extends StatelessWidget {
RoundedCheckbox({Key key, @required this.value}) : super(key: key);
final bool value;
@override
Widget build(BuildContext context) {
return value
? Container(
height: 20.0,
width: 20.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(50.0)),
color: Theme.of(context).accentTextTheme.body2.color,
),
child: Icon(
Icons.check,
color: Theme.of(context).backgroundColor,
size: 14.0,
))
: Offstage();
}
}
@@ -0,0 +1,35 @@
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
class TextIconButton extends StatelessWidget {
final String label;
final VoidCallback onTap;
const TextIconButton({
Key key,
this.label,
this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return
InkWell(
onTap: onTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: textMediumSemiBold(
color: Theme.of(context).primaryTextTheme.title.color,
),
),
Icon(
Icons.chevron_right_rounded,
color: Theme.of(context).primaryTextTheme.title.color,
),
],
),
);
}
}
+14 -2
View File
@@ -10,7 +10,10 @@ class AlertWithTwoActions extends BaseAlertDialog {
@required this.rightButtonText,
@required this.actionLeftButton,
@required this.actionRightButton,
this.alertBarrierDismissible = true
this.alertBarrierDismissible = true,
this.isDividerExist = false,
this.leftActionColor,
this.rightActionColor,
});
final String alertTitle;
@@ -20,6 +23,9 @@ class AlertWithTwoActions extends BaseAlertDialog {
final VoidCallback actionLeftButton;
final VoidCallback actionRightButton;
final bool alertBarrierDismissible;
final Color leftActionColor;
final Color rightActionColor;
final bool isDividerExist;
@override
String get titleText => alertTitle;
@@ -35,4 +41,10 @@ class AlertWithTwoActions extends BaseAlertDialog {
VoidCallback get actionRight => actionRightButton;
@override
bool get barrierDismissible => alertBarrierDismissible;
}
@override
Color get leftButtonColor => leftActionColor;
@override
Color get rightButtonColor => rightActionColor;
@override
bool get isDividerExists => isDividerExist;
}
+54 -60
View File
@@ -46,30 +46,28 @@ class BaseAlertDialog extends StatelessWidget {
children: <Widget>[
Flexible(
child: Container(
height: 52,
padding: EdgeInsets.only(left: 6, right: 6),
color: Theme.of(context).accentTextTheme.body2.decorationColor,
child: ButtonTheme(
minWidth: double.infinity,
child: FlatButton(
onPressed: actionLeft,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: Text(
leftActionButtonText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.body2
.backgroundColor,
decoration: TextDecoration.none,
),
)),
),
)
),
height: 52,
padding: EdgeInsets.only(left: 6, right: 6),
color: Theme.of(context).accentTextTheme.body2.decorationColor,
child: ButtonTheme(
minWidth: double.infinity,
child: FlatButton(
onPressed: actionLeft,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: Text(
leftActionButtonText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.body2.backgroundColor,
decoration: TextDecoration.none,
),
)),
),
)),
Container(
width: 1,
height: 52,
@@ -77,30 +75,28 @@ class BaseAlertDialog extends StatelessWidget {
),
Flexible(
child: Container(
height: 52,
padding: EdgeInsets.only(left: 6, right: 6),
color: Theme.of(context).accentTextTheme.body1.backgroundColor,
child: ButtonTheme(
minWidth: double.infinity,
child: FlatButton(
onPressed: actionRight,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: Text(
rightActionButtonText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.body1
.backgroundColor,
decoration: TextDecoration.none,
),
)),
),
)
),
height: 52,
padding: EdgeInsets.only(left: 6, right: 6),
color: Theme.of(context).accentTextTheme.body1.backgroundColor,
child: ButtonTheme(
minWidth: double.infinity,
child: FlatButton(
onPressed: actionRight,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: Text(
rightActionButtonText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.body1.backgroundColor,
decoration: TextDecoration.none,
),
)),
),
)),
],
);
}
@@ -108,9 +104,7 @@ class BaseAlertDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => barrierDismissible
? Navigator.of(context).pop()
: null,
onTap: () => barrierDismissible ? Navigator.of(context).pop() : null,
child: Container(
color: Colors.transparent,
child: BackdropFilter(
@@ -136,14 +130,14 @@ class BaseAlertDialog extends StatelessWidget {
child: title(context),
),
isDividerExists
? Padding(
padding: EdgeInsets.only(top: 16, bottom: 8),
child: Container(
height: 1,
color: Theme.of(context).dividerColor,
),
)
: Offstage(),
? Padding(
padding: EdgeInsets.only(top: 16, bottom: 8),
child: Container(
height: 1,
color: Theme.of(context).dividerColor,
),
)
: Offstage(),
Padding(
padding: EdgeInsets.fromLTRB(24, 8, 24, 32),
child: content(context),
@@ -166,4 +160,4 @@ class BaseAlertDialog extends StatelessWidget {
),
);
}
}
}
+15 -12
View File
@@ -5,13 +5,19 @@ class CakeScrollbar extends StatelessWidget {
@required this.backgroundHeight,
@required this.thumbHeight,
@required this.fromTop,
this.rightOffset = 6
this.rightOffset = 6,
this.backgroundColor,
this.thumbColor,
this.width = 6,
});
final double backgroundHeight;
final double thumbHeight;
final double fromTop;
final double width;
final double rightOffset;
final Color backgroundColor;
final Color thumbColor;
@override
Widget build(BuildContext context) {
@@ -19,11 +25,10 @@ class CakeScrollbar extends StatelessWidget {
right: rightOffset,
child: Container(
height: backgroundHeight,
width: 6,
width: width,
decoration: BoxDecoration(
color: Theme.of(context).textTheme.body1.decorationColor,
borderRadius: BorderRadius.all(Radius.circular(3))
),
color: backgroundColor ?? Theme.of(context).textTheme.body1.decorationColor,
borderRadius: BorderRadius.all(Radius.circular(3))),
child: Stack(
children: <Widget>[
AnimatedPositioned(
@@ -31,16 +36,14 @@ class CakeScrollbar extends StatelessWidget {
top: fromTop,
child: Container(
height: thumbHeight,
width: 6.0,
width: width,
decoration: BoxDecoration(
color: Theme.of(context).textTheme.body1.color,
borderRadius: BorderRadius.all(Radius.circular(3))
),
color: thumbColor ?? Theme.of(context).textTheme.body1.color,
borderRadius: BorderRadius.all(Radius.circular(3))),
),
)
],
),
)
);
));
}
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
class DiscountBadge extends StatelessWidget {
const DiscountBadge({
Key key,
@required this.percentage,
this.discountBackground,
}) : super(key: key);
final double percentage;
final AssetImage discountBackground;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
S.of(context).discount(percentage.toStringAsFixed(2)),
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
fontFamily: 'Lato',
),
),
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image: discountBackground ?? AssetImage('assets/images/badge_discount.png'),
),
),
);
}
}
+66
View File
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
class MarketPlaceItem extends StatelessWidget {
MarketPlaceItem({
@required this.onTap,
@required this.title,
@required this.subTitle,
});
final VoidCallback onTap;
final String title;
final String subTitle;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Stack(
children: [
Container(
padding: EdgeInsets.all(20),
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).textTheme.title.backgroundColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.20),
),
),
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: Theme.of(context)
.accentTextTheme
.display3
.backgroundColor,
fontSize: 24,
fontWeight: FontWeight.w900,
),
),
SizedBox(height: 5),
Text(
subTitle,
style: TextStyle(
color: Theme.of(context)
.accentTextTheme
.display3
.backgroundColor,
fontWeight: FontWeight.w500,
fontFamily: 'Lato'),
)
],
),
),
],
),
);
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
const latoFont = "Lato";
TextStyle textXxSmall({Color color}) => _cakeRegular(10, color);
TextStyle textXxSmallSemiBold({Color color}) => _cakeSemiBold(10, color);
TextStyle textXSmall({Color color}) => _cakeRegular(12, color);
TextStyle textXSmallSemiBold({Color color}) => _cakeSemiBold(12, color);
TextStyle textSmall({Color color}) => _cakeRegular(14, color);
TextStyle textSmallSemiBold({Color color}) => _cakeSemiBold(14, color);
TextStyle textMedium({Color color}) => _cakeRegular(16, color);
TextStyle textMediumBold({Color color}) => _cakeBold(16, color);
TextStyle textMediumSemiBold({Color color}) => _cakeSemiBold(22, color);
TextStyle textLarge({Color color}) => _cakeRegular(18, color);
TextStyle textLargeSemiBold({Color color}) => _cakeSemiBold(24, color);
TextStyle textXLarge({Color color}) => _cakeRegular(32, color);
TextStyle textXLargeSemiBold({Color color}) => _cakeSemiBold(32, color);
TextStyle _cakeRegular(double size, Color color) => _textStyle(
size: size,
fontWeight: FontWeight.normal,
color: color,
);
TextStyle _cakeBold(double size, Color color) => _textStyle(
size: size,
fontWeight: FontWeight.w900,
color: color,
);
TextStyle _cakeSemiBold(double size, Color color) => _textStyle(
size: size,
fontWeight: FontWeight.w700,
color: color,
);
TextStyle _textStyle({
@required double size,
@required FontWeight fontWeight,
Color color,
}) =>
TextStyle(
fontFamily: latoFont,
fontSize: size,
fontWeight: fontWeight,
color: color ?? Colors.white,
);
@@ -0,0 +1,44 @@
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
part 'ionia_account_view_model.g.dart';
class IoniaAccountViewModel = IoniaAccountViewModelBase with _$IoniaAccountViewModel;
abstract class IoniaAccountViewModelBase with Store {
IoniaAccountViewModelBase({this.ioniaService}) {
email = '';
giftCards = [];
ioniaService.getUserEmail().then((email) => this.email = email);
updateUserGiftCards();
}
final IoniaService ioniaService;
@observable
String email;
@observable
List<IoniaGiftCard> giftCards;
@computed
int get countOfMerch => giftCards.where((giftCard) => !giftCard.isEmpty).length;
@computed
List<IoniaGiftCard> get activeMechs => giftCards.where((giftCard) => !giftCard.isEmpty).toList();
@computed
List<IoniaGiftCard> get redeemedMerchs => giftCards.where((giftCard) => giftCard.isEmpty).toList();
@action
void logout() {
ioniaService.logout();
}
@action
Future<void> updateUserGiftCards() async {
giftCards = await ioniaService.getCurrentUserGiftCardSummaries();
}
}
@@ -0,0 +1,67 @@
import 'package:cake_wallet/ionia/ionia_create_state.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:mobx/mobx.dart';
part 'ionia_auth_view_model.g.dart';
class IoniaAuthViewModel = IoniaAuthViewModelBase with _$IoniaAuthViewModel;
abstract class IoniaAuthViewModelBase with Store {
IoniaAuthViewModelBase({this.ioniaService}):
createUserState = IoniaInitialCreateState(),
signInState = IoniaInitialCreateState(),
otpState = IoniaOtpSendDisabled();
final IoniaService ioniaService;
@observable
IoniaCreateAccountState createUserState;
@observable
IoniaCreateAccountState signInState;
@observable
IoniaOtpState otpState;
@observable
String email;
@observable
String otp;
@action
Future<void> verifyEmail(String code) async {
try {
otpState = IoniaOtpValidating();
await ioniaService.verifyEmail(code);
otpState = IoniaOtpSuccess();
} catch (_) {
otpState = IoniaOtpFailure(error: 'Invalid OTP. Try again');
}
}
@action
Future<void> createUser(String email) async {
try {
createUserState = IoniaCreateStateLoading();
await ioniaService.createUser(email);
createUserState = IoniaCreateStateSuccess();
} catch (e) {
createUserState = IoniaCreateStateFailure(error: e.toString());
}
}
@action
Future<void> signIn(String email) async {
try {
signInState = IoniaCreateStateLoading();
await ioniaService.signIn(email);
signInState = IoniaCreateStateSuccess();
} catch (e) {
signInState = IoniaCreateStateFailure(error: e.toString());
}
}
}
@@ -0,0 +1,31 @@
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:mobx/mobx.dart';
part 'ionia_buy_card_view_model.g.dart';
class IoniaBuyCardViewModel = IoniaBuyCardViewModelBase with _$IoniaBuyCardViewModel;
abstract class IoniaBuyCardViewModelBase with Store {
IoniaBuyCardViewModelBase({this.ioniaMerchant}) {
isEnablePurchase = false;
amount = 0;
}
final IoniaMerchant ioniaMerchant;
@observable
double amount;
@observable
bool isEnablePurchase;
@action
void onAmountChanged(String input) {
if (input.isEmpty) return;
amount = double.parse(input.replaceAll(',', '.'));
final min = ioniaMerchant.minimumCardPurchase;
final max = ioniaMerchant.maximumCardPurchase;
isEnablePurchase = amount >= min && amount <= max;
}
}
@@ -0,0 +1,58 @@
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:mobx/mobx.dart';
part 'ionia_filter_view_model.g.dart';
class IoniaFilterViewModel = IoniaFilterViewModelBase with _$IoniaFilterViewModel;
abstract class IoniaFilterViewModelBase with Store {
IoniaFilterViewModelBase() {
selectedIndices = ObservableList<int>();
ioniaCategories = IoniaCategory.allCategories;
}
List<IoniaCategory> get selectedCategories => ioniaCategories.where(_isSelected).toList();
@observable
ObservableList<int> selectedIndices;
@observable
List<IoniaCategory> ioniaCategories;
@action
void selectFilter(IoniaCategory ioniaCategory) {
if (ioniaCategory == IoniaCategory.all && !selectedIndices.contains(0)) {
selectedIndices.clear();
selectedIndices.add(0);
return;
}
if (selectedIndices.contains(ioniaCategory.index) && ioniaCategory.index != 0) {
selectedIndices.remove(ioniaCategory.index);
return;
}
selectedIndices.add(ioniaCategory.index);
selectedIndices.remove(0);
}
@action
void onSearchFilter(String text) {
if (text.isEmpty) {
ioniaCategories = IoniaCategory.allCategories;
} else {
ioniaCategories = IoniaCategory.allCategories
.where(
(e) => e.title.toLowerCase().contains(text.toLowerCase()),
)
.toList();
}
}
@action
void setSelectedCategories(List<IoniaCategory> selectedCategories) {
selectedIndices = ObservableList.of(selectedCategories.map((e) => e.index));
}
bool _isSelected(IoniaCategory ioniaCategory) {
return selectedIndices.contains(ioniaCategory.index);
}
}
@@ -0,0 +1,35 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:mobx/mobx.dart';
part 'ionia_gift_card_details_view_model.g.dart';
class IoniaGiftCardDetailsViewModel = IoniaGiftCardDetailsViewModelBase with _$IoniaGiftCardDetailsViewModel;
abstract class IoniaGiftCardDetailsViewModelBase with Store {
IoniaGiftCardDetailsViewModelBase({this.ioniaService, this.giftCard}) {
redeemState = InitialExecutionState();
}
final IoniaService ioniaService;
@observable
IoniaGiftCard giftCard;
@observable
ExecutionState redeemState;
@action
Future<void> redeem() async {
try {
redeemState = IsExecutingState();
await ioniaService.redeem(giftCard);
giftCard = await ioniaService.getGiftCard(id: giftCard.id);
redeemState = ExecutedSuccessfullyState();
} catch(e) {
redeemState = FailureState(e.toString());
}
}
}
@@ -0,0 +1,103 @@
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:cake_wallet/ionia/ionia_create_state.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/ionia/ionia_virtual_card.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
part 'ionia_gift_cards_list_view_model.g.dart';
class IoniaGiftCardsListViewModel = IoniaGiftCardsListViewModelBase with _$IoniaGiftCardsListViewModel;
abstract class IoniaGiftCardsListViewModelBase with Store {
IoniaGiftCardsListViewModelBase({
@required this.ioniaService,
}) :
cardState = IoniaNoCardState(),
ioniaMerchants = [],
scrollOffsetFromTop = 0.0 {
selectedFilters = [];
_getAuthStatus().then((value) => isLoggedIn = value);
_getMerchants();
}
final IoniaService ioniaService;
List<IoniaMerchant> ioniaMerchantList;
String searchString;
List<IoniaCategory> selectedFilters;
@observable
double scrollOffsetFromTop;
@observable
IoniaCreateCardState createCardState;
@observable
IoniaFetchCardState cardState;
@observable
List<IoniaMerchant> ioniaMerchants;
@observable
bool isLoggedIn;
Future<bool> _getAuthStatus() async {
return await ioniaService.isLogined();
}
@action
Future<IoniaVirtualCard> createCard() async {
createCardState = IoniaCreateCardLoading();
try {
final card = await ioniaService.createCard();
createCardState = IoniaCreateCardSuccess();
return card;
} on Exception catch (e) {
createCardState = IoniaCreateCardFailure(error: e.toString());
}
return null;
}
@action
void searchMerchant(String text) {
if (text.isEmpty) {
ioniaMerchants = ioniaMerchantList;
return;
}
searchString = text;
ioniaService.getMerchantsByFilter(search: searchString).then((value) {
ioniaMerchants = value;
});
}
Future<void> _getCard() async {
cardState = IoniaFetchingCard();
try {
final card = await ioniaService.getCard();
cardState = IoniaCardSuccess(card: card);
} catch (_) {
cardState = IoniaFetchCardFailure();
}
}
void _getMerchants() {
ioniaService.getMerchantsByFilter(categories: selectedFilters).then((value) {
ioniaMerchants = ioniaMerchantList = value;
});
}
@action
void setSelectedFilter(List<IoniaCategory> filters) {
selectedFilters = filters;
_getMerchants();
}
void setScrollOffsetFromTop(double scrollOffset) {
scrollOffsetFromTop = scrollOffset;
}
}
@@ -0,0 +1,58 @@
import 'dart:async';
import 'package:mobx/mobx.dart';
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
part 'ionia_payment_status_view_model.g.dart';
class IoniaPaymentStatusViewModel = IoniaPaymentStatusViewModelBase with _$IoniaPaymentStatusViewModel;
abstract class IoniaPaymentStatusViewModelBase with Store {
IoniaPaymentStatusViewModelBase(
this.ioniaService,{
@required this.paymentInfo,
@required this.committedInfo}) {
_timer = Timer.periodic(updateTime, (timer) async {
await updatePaymentStatus();
if (giftCard != null) {
timer?.cancel();
}
});
}
static const updateTime = Duration(seconds: 3);
final IoniaService ioniaService;
final IoniaAnyPayPaymentInfo paymentInfo;
final AnyPayPaymentCommittedInfo committedInfo;
@observable
IoniaGiftCard giftCard;
@observable
String error;
Timer get timer => _timer;
Timer _timer;
@action
Future<void> updatePaymentStatus() async {
try {
final giftCardId = await ioniaService.getPaymentStatus(
orderId: paymentInfo.ioniaOrder.id,
paymentId: paymentInfo.ioniaOrder.paymentId);
if (giftCardId != null) {
giftCard = await ioniaService.getGiftCard(id: giftCardId);
}
} catch (e) {
error = e.toString();
}
}
}
@@ -0,0 +1,94 @@
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/anypay/any_pay_payment.dart';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/ionia/ionia_anypay.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/ionia/ionia_tip.dart';
import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
part 'ionia_purchase_merch_view_model.g.dart';
class IoniaMerchPurchaseViewModel = IoniaMerchPurchaseViewModelBase with _$IoniaMerchPurchaseViewModel;
abstract class IoniaMerchPurchaseViewModelBase with Store {
IoniaMerchPurchaseViewModelBase({
@required this.ioniaAnyPayService,
@required this.amount,
@required this.ioniaMerchant,
}) {
tipAmount = 0.0;
percentage = 0.0;
tips = <IoniaTip>[
IoniaTip(percentage: 0, originalAmount: amount),
IoniaTip(percentage: 15, originalAmount: amount),
IoniaTip(percentage: 18, originalAmount: amount),
IoniaTip(percentage: 20, originalAmount: amount),
];
selectedTip = tips.first;
}
final double amount;
List<IoniaTip> tips;
@observable
IoniaTip selectedTip;
final IoniaMerchant ioniaMerchant;
final IoniaAnyPay ioniaAnyPayService;
IoniaAnyPayPaymentInfo paymentInfo;
AnyPayPayment get invoice => paymentInfo?.anyPayPayment;
AnyPayPaymentCommittedInfo committedInfo;
@observable
ExecutionState invoiceCreationState;
@observable
ExecutionState invoiceCommittingState;
@observable
double percentage;
@computed
double get giftCardAmount => double.parse((amount + tipAmount).toStringAsFixed(2));
@computed
double get billAmount => double.parse((giftCardAmount * (1 - (ioniaMerchant.discount / 100))).toStringAsFixed(2));
@observable
double tipAmount;
@action
void addTip(IoniaTip tip) {
tipAmount = tip.additionalAmount;
selectedTip = tip;
}
@action
Future<void> createInvoice() async {
try {
invoiceCreationState = IsExecutingState();
paymentInfo = await ioniaAnyPayService.purchase(merchId: ioniaMerchant.id.toString(), amount: giftCardAmount);
invoiceCreationState = ExecutedSuccessfullyState();
} catch (e) {
invoiceCreationState = FailureState(e.toString());
}
}
@action
Future<void> commitPaymentInvoice() async {
try {
invoiceCommittingState = IsExecutingState();
committedInfo = await ioniaAnyPayService.commitInvoice(invoice);
invoiceCommittingState = ExecutedSuccessfullyState(payload: committedInfo);
} catch (e) {
invoiceCommittingState = FailureState(e.toString());
}
}
}
+2 -2
View File
@@ -222,11 +222,11 @@ abstract class SendViewModelBase with Store {
case WalletType.bitcoin:
final priority = _settingsStore.priority[_wallet.type];
return bitcoin.createBitcoinTransactionCredentials(outputs, priority);
return bitcoin.createBitcoinTransactionCredentials(outputs, priority: priority);
case WalletType.litecoin:
final priority = _settingsStore.priority[_wallet.type];
return bitcoin.createBitcoinTransactionCredentials(outputs, priority);
return bitcoin.createBitcoinTransactionCredentials(outputs, priority: priority);
case WalletType.monero:
final priority = _settingsStore.priority[_wallet.type];