Ionia (#437)
* 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>wow-support
After Width: | Height: | Size: 960 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 556 B |
After Width: | Height: | Size: 454 B |
After Width: | Height: | Size: 556 B |
After Width: | Height: | Size: 728 B |
After Width: | Height: | Size: 504 B |
After Width: | Height: | Size: 710 B |
After Width: | Height: | Size: 705 B |
After Width: | Height: | Size: 997 B |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 536 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 583 B |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,5 @@
|
||||
class AnyPayChain {
|
||||
static const xmr = 'XMR';
|
||||
static const btc = 'BTC';
|
||||
static const ltc = 'LTC';
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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:
|
||||
'^[^@]+@[^@]+\.[^@]+',
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
];
|
||||
}
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
class IoniaUserCredentials {
|
||||
const IoniaUserCredentials(this.username, this.password);
|
||||
|
||||
final String username;
|
||||
final String password;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|