* Initial ionia service

* Ionia manage card UI (#374)

* design ui for cakepay

* Add manage cards page ui

* create auth ui for ionia

* add authentication logic

* implement user create card

* Add ionia merchant sevic

* Add anypay. Add purschase gift card.

* display virtual card (#385)

* display virtual card

* fix formatting

* Remove IoniaMerchantService from IoniaViewModel

* Add hex and txKey for monero pending transaction.

* Changed monero version and monero repo to cake tech.

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

* Fix for get transactions for hidden addresses for electrum wallet

* Add ionia categories.

* Add anypay commited info for payments.

* Update UI with new fixes (#400)

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

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

* refactor tips (#406)

* refactor tips

* refactor ionia tips implementation

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

* Implement show purchased cards

* fix padding

* Fixes for getting of purchased gift cards.

* Implement gift card details screen (#408)

* Implement gift card details screen

* Add redeem for ionia gift cards

* Fix navigation after ionia opt redirection.

* Fix update gift cards list.

* Add payment status update for ionia.

* Add usage instruction to gift card.

* Add copy for ionia gift card info.

* Change version for Cake Wallet ios.

* Add localisation (#414)

* Fixes for fiat amounts for ionia.

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

* Change text on marketplace

* fix build issues

* fix build

* UI fixes for ionia.

* UI fixes for ionia. (#421)

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

* update welcome text

* Update localization

* Cw 133 (#422)

* UI fixes for ionia.

* Fixes for display card item on gift cards screen.

* Fix signup page (#419)

* Changed tips for ionia.

* Cw 132 (#425)

* UI fixes for ionia.

* Changed tips for ionia.

* Cw 131 (#426)

* UI fixes for ionia.

* Changed tips for ionia.

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

* Change ionia heading font style (#427)

* Fix for AddressResolver in di

* Changed build number for Cake Wallet ios.

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

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

* fix terms and condition scroll

* fix color issues

* reuse

* refactor  widget

* Remove IoniaTokenService

* Change api for ionia to staging

* Update versions for Cake Wallet for android and ios.

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

* Change build versions for Cake Wallet

* Add ionia sign in.

* Update for discounts and statuses for ionia merch.

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

* Fixed formatting for display ionia discounts.

* Fix merchant.discount.toStringAsFixed issue

* Add savingsPercentage to ionia merch discount.

* Change build number for Cake Wallet ios and android.

* Disable ionia for haven (#440)

Co-authored-by: Godwin Asuquo <41484542+godilite@users.noreply.github.com>
This commit is contained in:
mkyq 2022-07-28 18:03:16 +01:00 committed by GitHub
parent b72443a8c4
commit 418c9563fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
115 changed files with 7726 additions and 184 deletions

BIN
assets/images/airplane.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

BIN
assets/images/category.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

BIN
assets/images/copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

BIN
assets/images/delivery.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

BIN
assets/images/filter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

BIN
assets/images/food.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

BIN
assets/images/gaming.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

BIN
assets/images/global.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

BIN
assets/images/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/images/tshirt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

BIN
assets/images/wifi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -2,8 +2,9 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_core/output_info.dart'; import 'package:cw_core/output_info.dart';
class BitcoinTransactionCredentials { class BitcoinTransactionCredentials {
BitcoinTransactionCredentials(this.outputs, this.priority); BitcoinTransactionCredentials(this.outputs, {this.priority, this.feeRate});
final List<OutputInfo> outputs; final List<OutputInfo> outputs;
BitcoinTransactionPriority priority; final BitcoinTransactionPriority priority;
final int feeRate;
} }

View file

@ -208,8 +208,14 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
} }
amount = credentialsAmount; amount = credentialsAmount;
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
outputsCount: outputs.length + 1); if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate, amount,
outputsCount: outputs.length + 1);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
outputsCount: outputs.length + 1);
}
} else { } else {
final output = outputs.first; final output = outputs.first;
credentialsAmount = !output.sendAll credentialsAmount = !output.sendAll
@ -223,9 +229,14 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
amount = output.sendAll || allAmount - credentialsAmount < minAmount amount = output.sendAll || allAmount - credentialsAmount < minAmount
? allAmount ? allAmount
: credentialsAmount; : credentialsAmount;
fee = output.sendAll || amount == allAmount
? allAmountFee if (output.sendAll || amount == allAmount) {
: calculateEstimatedFee(transactionCredentials.priority, amount); fee = allAmountFee;
} else if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate, amount);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount);
}
} }
if (fee == 0) { if (fee == 0) {
@ -296,7 +307,14 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
final estimatedSize = final estimatedSize =
estimatedTransactionSize(inputs.length, outputs.length + 1); estimatedTransactionSize(inputs.length, outputs.length + 1);
final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize; var feeAmount = 0;
if (transactionCredentials.feeRate != null) {
feeAmount = transactionCredentials.feeRate * estimatedSize;
} else {
feeAmount = feeRate(transactionCredentials.priority) * estimatedSize;
}
final changeValue = totalInputAmount - amount - feeAmount; final changeValue = totalInputAmount - amount - feeAmount;
if (changeValue > minAmount) { if (changeValue > minAmount) {
@ -346,45 +364,57 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
int outputsCount) => int outputsCount) =>
feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
int feeAmountWithFeeRate(int feeRate, int inputsCount,
int outputsCount) =>
feeRate * estimatedTransactionSize(inputsCount, outputsCount);
@override @override
int calculateEstimatedFee(TransactionPriority priority, int amount, int calculateEstimatedFee(TransactionPriority priority, int amount,
{int outputsCount}) { {int outputsCount}) {
if (priority is BitcoinTransactionPriority) { if (priority is BitcoinTransactionPriority) {
int inputsCount = 0; return calculateEstimatedFeeWithFeeRate(
feeRate(priority),
if (amount != null) { amount,
int totalValue = 0; outputsCount: outputsCount);
for (final input in unspentCoins) {
if (totalValue >= amount) {
break;
}
if (input.isSending) {
totalValue += input.value;
inputsCount += 1;
}
}
if (totalValue < amount) return 0;
} else {
for (final input in unspentCoins) {
if (input.isSending) {
inputsCount += 1;
}
}
}
// If send all, then we have no change value
final _outputsCount = outputsCount ?? (amount != null ? 2 : 1);
return feeAmountForPriority(
priority, inputsCount, _outputsCount);
} }
return 0; return 0;
} }
int calculateEstimatedFeeWithFeeRate(int feeRate, int amount,
{int outputsCount}) {
int inputsCount = 0;
if (amount != null) {
int totalValue = 0;
for (final input in unspentCoins) {
if (totalValue >= amount) {
break;
}
if (input.isSending) {
totalValue += input.value;
inputsCount += 1;
}
}
if (totalValue < amount) return 0;
} else {
for (final input in unspentCoins) {
if (input.isSending) {
inputsCount += 1;
}
}
}
// If send all, then we have no change value
final _outputsCount = outputsCount ?? (amount != null ? 2 : 1);
return feeAmountWithFeeRate(
feeRate, inputsCount, _outputsCount);
}
@override @override
Future<void> save() async { Future<void> save() async {
final path = await makePath(); final path = await makePath();
@ -525,10 +555,6 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
final addressHashes = <String, BitcoinAddressRecord>{}; final addressHashes = <String, BitcoinAddressRecord>{};
final normalizedHistories = <Map<String, dynamic>>[]; final normalizedHistories = <Map<String, dynamic>>[];
walletAddresses.addresses.forEach((addressRecord) { walletAddresses.addresses.forEach((addressRecord) {
if (addressRecord.isHidden) {
return;
}
final sh = scriptHash(addressRecord.address, networkType: networkType); final sh = scriptHash(addressRecord.address, networkType: networkType);
addressHashes[sh] = addressRecord; addressHashes[sh] = addressRecord;
}); });

View file

@ -24,6 +24,9 @@ class PendingBitcoinTransaction with PendingTransaction {
@override @override
String get id => _tx.getId(); String get id => _tx.getId();
@override
String get hex => _tx.toHex();
@override @override
String get amountFormatted => bitcoinAmountToString(amount: amount); String get amountFormatted => bitcoinAmountToString(amount: amount);

View file

@ -2,6 +2,7 @@ mixin PendingTransaction {
String get id; String get id;
String get amountFormatted; String get amountFormatted;
String get feeFormatted; String get feeFormatted;
String get hex;
Future<void> commit(); Future<void> commit();
} }

View file

@ -22,6 +22,9 @@ class PendingHavenTransaction with PendingTransaction {
@override @override
String get id => pendingTransactionDescription.hash; String get id => pendingTransactionDescription.hash;
@override
String get hex => '';
@override @override
String get amountFormatted => AmountConverter.amountIntToString( String get amountFormatted => AmountConverter.amountIntToString(
cryptoCurrency, pendingTransactionDescription.amount); cryptoCurrency, pendingTransactionDescription.amount);

View file

@ -169,13 +169,6 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.1" version: "0.0.1"
cw_monero:
dependency: "direct main"
description:
path: "../cw_monero"
relative: true
source: path
version: "0.0.1"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:

View file

@ -166,6 +166,8 @@ extern "C"
uint64_t amount; uint64_t amount;
uint64_t fee; uint64_t fee;
char *hash; char *hash;
char *hex;
char *txKey;
Monero::PendingTransaction *transaction; Monero::PendingTransaction *transaction;
PendingTransactionRaw(Monero::PendingTransaction *_transaction) PendingTransactionRaw(Monero::PendingTransaction *_transaction)
@ -174,6 +176,8 @@ extern "C"
amount = _transaction->amount(); amount = _transaction->amount();
fee = _transaction->fee(); fee = _transaction->fee();
hash = strdup(_transaction->txid()[0].c_str()); hash = strdup(_transaction->txid()[0].c_str());
hex = strdup(_transaction->hex()[0].c_str());
txKey = strdup(_transaction->txKey()[0].c_str());
} }
}; };
@ -228,8 +232,6 @@ extern "C"
bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error) bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error)
{ {
Monero::WalletManagerFactory::setLogLevel(4);
Monero::NetworkType _networkType = static_cast<Monero::NetworkType>(networkType); Monero::NetworkType _networkType = static_cast<Monero::NetworkType>(networkType);
Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager();
Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType);

View file

@ -10,14 +10,30 @@ class PendingTransactionRaw extends Struct {
Pointer<Utf8> hash; Pointer<Utf8> hash;
Pointer<Utf8> hex;
Pointer<Utf8> txKey;
String getHash() => Utf8.fromUtf8(hash); String getHash() => Utf8.fromUtf8(hash);
String getHex() => Utf8.fromUtf8(hex);
String getKey() => Utf8.fromUtf8(txKey);
} }
class PendingTransactionDescription { class PendingTransactionDescription {
PendingTransactionDescription({this.amount, this.fee, this.hash, this.pointerAddress}); PendingTransactionDescription({
this.amount,
this.fee,
this.hash,
this.hex,
this.txKey,
this.pointerAddress});
final int amount; final int amount;
final int fee; final int fee;
final String hash; final String hash;
final String hex;
final String txKey;
final int pointerAddress; final int pointerAddress;
} }

View file

@ -104,6 +104,8 @@ PendingTransactionDescription createTransactionSync(
amount: pendingTransactionRawPointer.ref.amount, amount: pendingTransactionRawPointer.ref.amount,
fee: pendingTransactionRawPointer.ref.fee, fee: pendingTransactionRawPointer.ref.fee,
hash: pendingTransactionRawPointer.ref.getHash(), hash: pendingTransactionRawPointer.ref.getHash(),
hex: pendingTransactionRawPointer.ref.getHex(),
txKey: pendingTransactionRawPointer.ref.getKey(),
pointerAddress: pendingTransactionRawPointer.address); pointerAddress: pendingTransactionRawPointer.address);
} }
@ -157,6 +159,8 @@ PendingTransactionDescription createTransactionMultDestSync(
amount: pendingTransactionRawPointer.ref.amount, amount: pendingTransactionRawPointer.ref.amount,
fee: pendingTransactionRawPointer.ref.fee, fee: pendingTransactionRawPointer.ref.fee,
hash: pendingTransactionRawPointer.ref.getHash(), hash: pendingTransactionRawPointer.ref.getHash(),
hex: pendingTransactionRawPointer.ref.getHex(),
txKey: pendingTransactionRawPointer.ref.getKey(),
pointerAddress: pendingTransactionRawPointer.address); pointerAddress: pendingTransactionRawPointer.address);
} }

View file

@ -22,6 +22,11 @@ class PendingMoneroTransaction with PendingTransaction {
@override @override
String get id => pendingTransactionDescription.hash; String get id => pendingTransactionDescription.hash;
@override
String get hex => pendingTransactionDescription.hex;
String get txKey => pendingTransactionDescription.txKey;
@override @override
String get amountFormatted => AmountConverter.amountIntToString( String get amountFormatted => AmountConverter.amountIntToString(
CryptoCurrency.xmr, pendingTransactionDescription.amount); CryptoCurrency.xmr, pendingTransactionDescription.amount);

View file

@ -57,6 +57,8 @@ PODS:
- Flutter - Flutter
- cw_shared_external/Sodium (0.0.1): - cw_shared_external/Sodium (0.0.1):
- Flutter - Flutter
- device_display_brightness (0.0.1):
- Flutter
- devicelocale (0.0.1): - devicelocale (0.0.1):
- Flutter - Flutter
- DKImagePickerController/Core (4.3.2): - DKImagePickerController/Core (4.3.2):
@ -134,6 +136,7 @@ DEPENDENCIES:
- cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_haven (from `.symlinks/plugins/cw_haven/ios`)
- cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_monero (from `.symlinks/plugins/cw_monero/ios`)
- cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`)
- device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`)
- devicelocale (from `.symlinks/plugins/devicelocale/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`)
- esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
@ -174,6 +177,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/cw_monero/ios" :path: ".symlinks/plugins/cw_monero/ios"
cw_shared_external: cw_shared_external:
:path: ".symlinks/plugins/cw_shared_external/ios" :path: ".symlinks/plugins/cw_shared_external/ios"
device_display_brightness:
:path: ".symlinks/plugins/device_display_brightness/ios"
devicelocale: devicelocale:
:path: ".symlinks/plugins/devicelocale/ios" :path: ".symlinks/plugins/devicelocale/ios"
esys_flutter_share: esys_flutter_share:
@ -211,6 +216,7 @@ SPEC CHECKSUMS:
cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a
cw_monero: 88c5e7aa596c6848330750f5f8bcf05fb9c66375 cw_monero: 88c5e7aa596c6848330750f5f8bcf05fb9c66375
cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 cw_shared_external: 2972d872b8917603478117c9957dfca611845a92
device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7
devicelocale: b22617f40038496deffba44747101255cee005b0 devicelocale: b22617f40038496deffba44747101255cee005b0
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179

View file

@ -0,0 +1,5 @@
class AnyPayChain {
static const xmr = 'XMR';
static const btc = 'BTC';
static const ltc = 'LTC';
}

View file

@ -0,0 +1,64 @@
import 'package:cake_wallet/anypay/any_pay_chain.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_core/monero_amount_format.dart';
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart';
class AnyPayPayment {
AnyPayPayment({
@required this.time,
@required this.expires,
@required this.memo,
@required this.paymentUrl,
@required this.paymentId,
@required this.chain,
@required this.network,
@required this.instructions});
factory AnyPayPayment.fromMap(Map<String, dynamic> obj) {
final instructions = (obj['instructions'] as List<dynamic>)
.map((dynamic instruction) => AnyPayPaymentInstruction.fromMap(instruction as Map<String, dynamic>))
.toList();
return AnyPayPayment(
time: DateTime.parse(obj['time'] as String),
expires: DateTime.parse(obj['expires'] as String),
memo: obj['memo'] as String,
paymentUrl: obj['paymentUrl'] as String,
paymentId: obj['paymentId'] as String,
chain: obj['chain'] as String,
network: obj['network'] as String,
instructions: instructions);
}
final DateTime time;
final DateTime expires;
final String memo;
final String paymentUrl;
final String paymentId;
final String chain;
final String network;
final List<AnyPayPaymentInstruction> instructions;
String get totalAmount {
final total = instructions
.fold<int>(0, (int acc, instruction) => acc + instruction.outputs
.fold<int>(0, (int outAcc, out) => outAcc + out.amount));
switch (chain) {
case AnyPayChain.xmr:
return moneroAmountToString(amount: total);
case AnyPayChain.btc:
return bitcoinAmountToString(amount: total);
case AnyPayChain.ltc:
return bitcoinAmountToString(amount: total);
default:
return null;
}
}
List<String> get outAddresses {
return instructions
.map((instuction) => instuction.outputs.map((out) => out.address))
.expand((e) => e)
.toList();
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,9 @@
import 'package:flutter/foundation.dart';
class AnyPayTransaction {
const AnyPayTransaction(this.tx, {@required this.id, @required this.key});
final String tx;
final String id;
final String key;
}

View file

@ -0,0 +1,92 @@
import 'dart:convert';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/anypay/any_pay_payment.dart';
import 'package:cake_wallet/anypay/any_pay_trasnaction.dart';
class AnyPayApi {
static const contentTypePaymentRequest = 'application/payment-request';
static const contentTypePayment = 'application/payment';
static const xPayproVersion = '2';
static String chainByScheme(String scheme) {
switch (scheme.toLowerCase()) {
case 'monero':
return CryptoCurrency.xmr.title;
case 'bitcoin':
return CryptoCurrency.btc.title;
case 'litecoin':
return CryptoCurrency.ltc.title;
default:
return '';
}
}
static CryptoCurrency currencyByScheme(String scheme) {
switch (scheme.toLowerCase()) {
case 'monero':
return CryptoCurrency.xmr;
case 'bitcoin':
return CryptoCurrency.btc;
case 'litecoin':
return CryptoCurrency.ltc;
default:
return null;
}
}
Future<AnyPayPayment> paymentRequest(String uri) async {
final fragments = uri.split(':?r=');
final scheme = fragments.first;
final url = fragments[1];
final headers = <String, String>{
'Content-Type': contentTypePaymentRequest,
'X-Paypro-Version': xPayproVersion,
'Accept': '*/*',};
final body = <String, dynamic>{
'chain': chainByScheme(scheme),
'currency': currencyByScheme(scheme).title};
final response = await post(url, headers: headers, body: utf8.encode(json.encode(body)));
if (response.statusCode != 200) {
return null;
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
return AnyPayPayment.fromMap(decodedBody);
}
Future<AnyPayPaymentCommittedInfo> payment(
String uri,
{@required String chain,
@required String currency,
@required List<AnyPayTransaction> transactions}) async {
final headers = <String, String>{
'Content-Type': contentTypePayment,
'X-Paypro-Version': xPayproVersion,
'Accept': '*/*',};
final body = <String, dynamic>{
'chain': chain,
'currency': currency,
'transactions': transactions.map((tx) => {'tx': tx.tx, 'tx_hash': tx.id, 'tx_key': tx.key}).toList()};
final response = await post(uri, headers: headers, body: utf8.encode(json.encode(body)));
if (response.statusCode == 400) {
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
throw Exception(decodedBody['message'] as String);
}
if (response.statusCode != 200) {
throw Exception('Unexpected response');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
return AnyPayPaymentCommittedInfo(
uri: uri,
currency: currency,
chain: chain,
transactions: transactions,
memo: decodedBody['memo'] as String);
}
}

View file

@ -55,7 +55,7 @@ class CWBitcoin extends Bitcoin {
} }
@override @override
Object createBitcoinTransactionCredentials(List<Output> outputs, TransactionPriority priority) Object createBitcoinTransactionCredentials(List<Output> outputs, {TransactionPriority priority, int feeRate})
=> BitcoinTransactionCredentials( => BitcoinTransactionCredentials(
outputs.map((out) => OutputInfo( outputs.map((out) => OutputInfo(
fiatAmount: out.fiatAmount, fiatAmount: out.fiatAmount,
@ -67,7 +67,15 @@ class CWBitcoin extends Bitcoin {
isParsedAddress: out.isParsedAddress, isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount)) formattedCryptoAmount: out.formattedCryptoAmount))
.toList(), .toList(),
priority as BitcoinTransactionPriority); priority: priority != null ? priority as BitcoinTransactionPriority : null,
feeRate: feeRate);
@override
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs, {TransactionPriority priority, int feeRate})
=> BitcoinTransactionCredentials(
outputs,
priority: priority != null ? priority as BitcoinTransactionPriority : null,
feeRate: feeRate);
@override @override
List<String> getAddresses(Object wallet) { List<String> getAddresses(Object wallet) {

View file

@ -0,0 +1,11 @@
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
class EmailValidator extends TextValidator {
EmailValidator()
: super(
errorMessage: 'Invalid email address',
pattern:
'^[^@]+@[^@]+\.[^@]+',
);
}

View file

@ -1,10 +1,27 @@
import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/wake_lock.dart'; import 'package:cake_wallet/entities/wake_lock.dart';
import 'package:cake_wallet/ionia/ionia_anypay.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart';
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:cake_wallet/ionia/ionia_api.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart';
import 'package:cake_wallet/src/screens/ionia/ionia.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart';
import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/backup_service.dart';
import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_service.dart';
@ -100,6 +117,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -123,6 +141,12 @@ import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart';
import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart';
import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart';
@ -261,7 +285,6 @@ Future setup(
fiatConvertationStore: getIt.get<FiatConversionStore>())); fiatConvertationStore: getIt.get<FiatConversionStore>()));
getIt.registerFactory(() => DashboardViewModel( getIt.registerFactory(() => DashboardViewModel(
balanceViewModel: getIt.get<BalanceViewModel>(), balanceViewModel: getIt.get<BalanceViewModel>(),
appStore: getIt.get<AppStore>(), appStore: getIt.get<AppStore>(),
tradesStore: getIt.get<TradesStore>(), tradesStore: getIt.get<TradesStore>(),
@ -560,10 +583,6 @@ Future setup(
getIt.registerFactory(() => BackupPage(getIt.get<BackupViewModel>())); getIt.registerFactory(() => BackupPage(getIt.get<BackupViewModel>()));
getIt.registerFactory(() => EditBackupPasswordViewModel(
getIt.get<FlutterSecureStorage>(), getIt.get<SecretStore>())
..init());
getIt.registerFactory( getIt.registerFactory(
() => EditBackupPasswordPage(getIt.get<EditBackupPasswordViewModel>())); () => EditBackupPasswordPage(getIt.get<EditBackupPasswordViewModel>()));
@ -596,10 +615,7 @@ Future setup(
final url = args.first as String; final url = args.first as String;
final buyViewModel = args[1] as BuyViewModel; final buyViewModel = args[1] as BuyViewModel;
return BuyWebViewPage( return BuyWebViewPage(buyViewModel: buyViewModel, ordersStore: getIt.get<OrdersStore>(), url: url);
buyViewModel: buyViewModel,
ordersStore: getIt.get<OrdersStore>(),
url: url);
}); });
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) { getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) {
@ -649,6 +665,102 @@ Future setup(
getIt.registerFactoryParam<FullscreenQRPage, String, bool>( getIt.registerFactoryParam<FullscreenQRPage, String, bool>(
(String qrData, bool isLight) => FullscreenQRPage(qrData: qrData, isLight: isLight,)); (String qrData, bool isLight) => FullscreenQRPage(qrData: qrData, isLight: isLight,));
getIt.registerFactory(() => IoniaApi());
getIt.registerFactory(() => AnyPayApi());
getIt.registerFactory<IoniaService>(
() => IoniaService(getIt.get<FlutterSecureStorage>(), getIt.get<IoniaApi>()));
getIt.registerFactory<IoniaAnyPay>(
() => IoniaAnyPay(
getIt.get<IoniaService>(),
getIt.get<AnyPayApi>(),
getIt.get<AppStore>().wallet));
getIt.registerFactory<IoniaFilterViewModel>(() => IoniaFilterViewModel());
getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get<IoniaService>()));
getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get<IoniaService>()));
getIt.registerFactoryParam<IoniaMerchPurchaseViewModel, double, IoniaMerchant>((double amount, merchant) {
return IoniaMerchPurchaseViewModel(
ioniaAnyPayService: getIt.get<IoniaAnyPay>(),
amount: amount,
ioniaMerchant: merchant,
);
});
getIt.registerFactoryParam<IoniaBuyCardViewModel, IoniaMerchant, void>((IoniaMerchant merchant, _) {
return IoniaBuyCardViewModel(ioniaMerchant: merchant);
});
getIt.registerFactory(() => IoniaAccountViewModel(ioniaService: getIt.get<IoniaService>()));
getIt.registerFactory(() => IoniaCreateAccountPage(getIt.get<IoniaAuthViewModel>()));
getIt.registerFactory(() => IoniaLoginPage(getIt.get<IoniaAuthViewModel>()));
getIt.registerFactoryParam<IoniaVerifyIoniaOtp, List, void>((List args, _) {
final email = args.first as String;
final isSignIn = args[1] as bool;
return IoniaVerifyIoniaOtp(getIt.get<IoniaAuthViewModel>(), email, isSignIn);
});
getIt.registerFactory(() => IoniaWelcomePage(getIt.get<IoniaGiftCardsListViewModel>()));
getIt.registerFactoryParam<IoniaBuyGiftCardPage, List, void>((List args, _) {
final merchant = args.first as IoniaMerchant;
return IoniaBuyGiftCardPage(getIt.get<IoniaBuyCardViewModel>(param1: merchant));
});
getIt.registerFactoryParam<IoniaBuyGiftCardDetailPage, List, void>((List args, _) {
final amount = args.first as double;
final merchant = args.last as IoniaMerchant;
return IoniaBuyGiftCardDetailPage(getIt.get<IoniaMerchPurchaseViewModel>(param1: amount, param2: merchant));
});
getIt.registerFactoryParam<IoniaGiftCardDetailsViewModel, IoniaGiftCard, void>((IoniaGiftCard giftCard, _) {
return IoniaGiftCardDetailsViewModel(
ioniaService: getIt.get<IoniaService>(),
giftCard: giftCard);
});
getIt.registerFactoryParam<IoniaGiftCardDetailPage, IoniaGiftCard, void>((IoniaGiftCard giftCard, _) {
return IoniaGiftCardDetailPage(getIt.get<IoniaGiftCardDetailsViewModel>(param1: giftCard));
});
getIt.registerFactoryParam<IoniaCustomTipPage, List, void>((List args, _) {
final amount = args.first as String;
final merchant = args.last as IoniaMerchant;
return IoniaCustomTipPage(getIt.get<IoniaMerchPurchaseViewModel>(param1: amount, param2: merchant));
});
getIt.registerFactory(() => IoniaManageCardsPage(getIt.get<IoniaGiftCardsListViewModel>()));
getIt.registerFactory(() => IoniaDebitCardPage(getIt.get<IoniaGiftCardsListViewModel>()));
getIt.registerFactory(() => IoniaActivateDebitCardPage(getIt.get<IoniaGiftCardsListViewModel>()));
getIt.registerFactory(() => IoniaAccountPage(getIt.get<IoniaAccountViewModel>()));
getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get<IoniaAccountViewModel>()));
getIt.registerFactoryParam<IoniaPaymentStatusViewModel, IoniaAnyPayPaymentInfo, AnyPayPaymentCommittedInfo>(
(IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo)
=> IoniaPaymentStatusViewModel(
getIt.get<IoniaService>(),
paymentInfo: paymentInfo,
committedInfo: committedInfo));
getIt.registerFactoryParam<IoniaPaymentStatusPage, IoniaAnyPayPaymentInfo, AnyPayPaymentCommittedInfo>(
(IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo)
=> IoniaPaymentStatusPage(getIt.get<IoniaPaymentStatusViewModel>(param1: paymentInfo, param2: committedInfo)));
_isSetupFinished = true; _isSetupFinished = true;
} }

View file

@ -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;
}

View file

@ -0,0 +1,92 @@
import 'package:flutter/foundation.dart';
import 'package:cw_core/monero_amount_format.dart';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/anypay/any_pay_payment.dart';
import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart';
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/anypay/any_pay_chain.dart';
import 'package:cake_wallet/anypay/any_pay_trasnaction.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
import 'package:cake_wallet/ionia/ionia_order.dart';
class IoniaAnyPay {
IoniaAnyPay(this.ioniaService, this.anyPayApi, this.wallet);
final IoniaService ioniaService;
final AnyPayApi anyPayApi;
final WalletBase wallet;
Future<IoniaAnyPayPaymentInfo> purchase({
@required String merchId,
@required double amount}) async {
final invoice = await ioniaService.purchaseGiftCard(
merchId: merchId,
amount: amount,
currency: wallet.currency.title.toUpperCase());
final anypayPayment = await anyPayApi.paymentRequest(invoice.uri);
return IoniaAnyPayPaymentInfo(invoice, anypayPayment);
}
Future<AnyPayPaymentCommittedInfo> commitInvoice(AnyPayPayment payment) async {
final transactionCredentials = payment.instructions
.where((instruction) => instruction.type == AnyPayPaymentInstruction.transactionType)
.map((AnyPayPaymentInstruction instruction) {
switch(payment.chain.toUpperCase()) {
case AnyPayChain.xmr:
return monero.createMoneroTransactionCreationCredentialsRaw(
outputs: instruction.outputs.map((out) =>
OutputInfo(
isParsedAddress: false,
address: out.address,
cryptoAmount: moneroAmountToString(amount: out.amount),
sendAll: false)).toList(),
priority: MoneroTransactionPriority.medium); // FIXME: HARDCODED PRIORITY
case AnyPayChain.btc:
return bitcoin.createBitcoinTransactionCredentialsRaw(
instruction.outputs.map((out) =>
OutputInfo(
isParsedAddress: false,
address: out.address,
formattedCryptoAmount: out.amount,
sendAll: false)).toList(),
feeRate: instruction.requiredFeeRate);
case AnyPayChain.ltc:
return bitcoin.createBitcoinTransactionCredentialsRaw(
instruction.outputs.map((out) =>
OutputInfo(
isParsedAddress: false,
address: out.address,
formattedCryptoAmount: out.amount,
sendAll: false)).toList(),
feeRate: instruction.requiredFeeRate);
default:
throw Exception('Incorrect transaction chain: ${payment.chain.toUpperCase()}');
}
});
final transactions = (await Future.wait(transactionCredentials
.map((Object credentials) async => await wallet.createTransaction(credentials))))
.map((PendingTransaction pendingTransaction) {
switch (payment.chain.toUpperCase()){
case AnyPayChain.xmr:
final ptx = monero.pendingTransactionInfo(pendingTransaction);
return AnyPayTransaction(ptx['hex'], id: ptx['id'], key: ptx['key']);
default:
return AnyPayTransaction(pendingTransaction.hex, id: pendingTransaction.id, key: null);
}
})
.toList();
return await anyPayApi.payment(
payment.paymentUrl,
chain: payment.chain,
currency: payment.chain,
transactions: transactions);
}
}

444
lib/ionia/ionia_api.dart Normal file
View file

@ -0,0 +1,444 @@
import 'dart:convert';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/ionia/ionia_order.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/ionia/ionia_user_credentials.dart';
import 'package:cake_wallet/ionia/ionia_virtual_card.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
class IoniaApi {
static const baseUri = 'apistaging.ionia.io';
static const pathPrefix = 'cake';
static final createUserUri = Uri.https(baseUri, '/$pathPrefix/CreateUser');
static final verifyEmailUri = Uri.https(baseUri, '/$pathPrefix/VerifyEmail');
static final signInUri = Uri.https(baseUri, '/$pathPrefix/SignIn');
static final createCardUri = Uri.https(baseUri, '/$pathPrefix/CreateCard');
static final getCardsUri = Uri.https(baseUri, '/$pathPrefix/GetCards');
static final getMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchants');
static final getMerchantsByFilterUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchantsByFilter');
static final getPurchaseMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/PurchaseGiftCard');
static final getCurrentUserGiftCardSummariesUrl = Uri.https(baseUri, '/$pathPrefix/GetCurrentUserGiftCardSummaries');
static final changeGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/ChargeGiftCard');
static final getGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/GetGiftCard');
static final getPaymentStatusUrl = Uri.https(baseUri, '/$pathPrefix/PaymentStatus');
// Create user
Future<String> createUser(String email, {@required String clientId}) async {
final headers = <String, String>{'clientId': clientId};
final query = <String, String>{'emailAddress': email};
final uri = createUserUri.replace(queryParameters: query);
final response = await put(uri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(data['ErrorMessage'] as String);
}
return data['username'] as String;
}
// Verify email
Future<IoniaUserCredentials> verifyEmail({
@required String username,
@required String email,
@required String code,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'EmailAddress': email};
final query = <String, String>{'verificationCode': code};
final uri = verifyEmailUri.replace(queryParameters: query);
final response = await put(uri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(bodyJson['ErrorMessage'] as String);
}
final password = data['password'] as String;
username = data['username'] as String;
return IoniaUserCredentials(username, password);
}
// Sign In
Future<String> signIn(String email, {@required String clientId}) async {
final headers = <String, String>{'clientId': clientId};
final query = <String, String>{'emailAddress': email};
final uri = signInUri.replace(queryParameters: query);
final response = await put(uri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(data['ErrorMessage'] as String);
}
return data['username'] as String;
}
// Get virtual card
Future<IoniaVirtualCard> getCards({
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password};
final response = await post(getCardsUri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(data['message'] as String);
}
final virtualCard = data['VirtualCard'] as Map<String, Object>;
return IoniaVirtualCard.fromMap(virtualCard);
}
// Create virtual card
Future<IoniaVirtualCard> createCard({
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password};
final response = await post(createCardUri, headers: headers);
if (response.statusCode != 200) {
// throw exception
return null;
}
final bodyJson = json.decode(response.body) as Map<String, Object>;
final data = bodyJson['Data'] as Map<String, Object>;
final isSuccessful = bodyJson['Successful'] as bool;
if (!isSuccessful) {
throw Exception(data['message'] as String);
}
return IoniaVirtualCard.fromMap(data);
}
// Get Merchants
Future<List<IoniaMerchant>> getMerchants({
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password};
final response = await post(getMerchantsUrl, headers: headers);
if (response.statusCode != 200) {
return [];
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
return [];
}
final data = decodedBody['Data'] as List<dynamic>;
return data.map((dynamic e) {
try {
final element = e as Map<String, dynamic>;
return IoniaMerchant.fromJsonMap(element);
} catch(_) {
return null;
}
}).where((e) => e != null)
.toList();
}
// Get Merchants By Filter
Future<List<IoniaMerchant>> getMerchantsByFilter({
@required String username,
@required String password,
@required String clientId,
String search,
List<IoniaCategory> categories,
int merchantFilterType = 0}) async {
// MerchantFilterType: {All = 0, Nearby = 1, Popular = 2, Online = 3, MyFaves = 4, Search = 5}
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{'MerchantFilterType': merchantFilterType};
if (search != null) {
body['SearchCriteria'] = search;
}
if (categories != null) {
body['Categories'] = categories
.map((e) => e.ids)
.expand((e) => e)
.toList();
}
final response = await post(getMerchantsByFilterUrl, headers: headers, body: json.encode(body));
if (response.statusCode != 200) {
return [];
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
return [];
}
final data = decodedBody['Data'] as List<dynamic>;
return data.map((dynamic e) {
try {
final element = e['Merchant'] as Map<String, dynamic>;
return IoniaMerchant.fromJsonMap(element);
} catch(_) {
return null;
}
}).where((e) => e != null)
.toList();
}
// Purchase Gift Card
Future<IoniaOrder> purchaseGiftCard({
@required String merchId,
@required double amount,
@required String currency,
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{
'Amount': amount,
'Currency': currency,
'MerchantId': merchId};
final response = await post(getPurchaseMerchantsUrl, headers: headers, body: json.encode(body));
if (response.statusCode != 200) {
throw Exception('Unexpected response');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
throw Exception(decodedBody['ErrorMessage'] as String);
}
final data = decodedBody['Data'] as Map<String, dynamic>;
return IoniaOrder.fromMap(data);
}
// Get Current User Gift Card Summaries
Future<List<IoniaGiftCard>> getCurrentUserGiftCardSummaries({
@required String username,
@required String password,
@required String clientId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password};
final response = await post(getCurrentUserGiftCardSummariesUrl, headers: headers);
if (response.statusCode != 200) {
return [];
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
return [];
}
final data = decodedBody['Data'] as List<dynamic>;
return data.map((dynamic e) {
try {
final element = e as Map<String, dynamic>;
return IoniaGiftCard.fromJsonMap(element);
} catch(e) {
return null;
}
}).where((e) => e != null)
.toList();
}
// Charge Gift Card
Future<void> chargeGiftCard({
@required String username,
@required String password,
@required String clientId,
@required int giftCardId,
@required double amount}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{
'Id': giftCardId,
'Amount': amount};
final response = await post(
changeGiftCardUrl,
headers: headers,
body: json.encode(body));
if (response.statusCode != 200) {
throw Exception('Failed to update Gift Card with ID ${giftCardId};Incorrect response status: ${response.statusCode};');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
final data = decodedBody['Data'] as Map<String, dynamic>;
final msg = data['Message'] as String ?? '';
if (msg.isNotEmpty) {
throw Exception(msg);
}
throw Exception('Failed to update Gift Card with ID ${giftCardId};');
}
}
// Get Gift Card
Future<IoniaGiftCard> getGiftCard({
@required String username,
@required String password,
@required String clientId,
@required int id}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{'Id': id};
final response = await post(
getGiftCardUrl,
headers: headers,
body: json.encode(body));
if (response.statusCode != 200) {
throw Exception('Failed to get Gift Card with ID ${id};Incorrect response status: ${response.statusCode};');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
final msg = decodedBody['ErrorMessage'] as String ?? '';
if (msg.isNotEmpty) {
throw Exception(msg);
}
throw Exception('Failed to get Gift Card with ID ${id};');
}
final data = decodedBody['Data'] as Map<String, dynamic>;
return IoniaGiftCard.fromJsonMap(data);
}
// Payment Status
Future<int> getPaymentStatus({
@required String username,
@required String password,
@required String clientId,
@required String orderId,
@required String paymentId}) async {
final headers = <String, String>{
'clientId': clientId,
'username': username,
'password': password,
'Content-Type': 'application/json'};
final body = <String, dynamic>{
'order_id': orderId,
'paymentId': paymentId};
final response = await post(
getPaymentStatusUrl,
headers: headers,
body: json.encode(body));
if (response.statusCode != 200) {
throw Exception('Failed to get Payment Status for order_id ${orderId} paymentId ${paymentId};Incorrect response status: ${response.statusCode};');
}
final decodedBody = json.decode(response.body) as Map<String, dynamic>;
final isSuccessful = decodedBody['Successful'] as bool ?? false;
if (!isSuccessful) {
final msg = decodedBody['ErrorMessage'] as String ?? '';
if (msg.isNotEmpty) {
throw Exception(msg);
}
throw Exception('Failed to get Payment Status for order_id ${orderId} paymentId ${paymentId}');
}
final data = decodedBody['Data'] as Map<String, dynamic>;
return data['gift_card_id'] as int;
}
}

View file

@ -0,0 +1,18 @@
class IoniaCategory {
const IoniaCategory({this.index, this.title, this.ids, this.iconPath});
static const allCategories = <IoniaCategory>[all, apparel, onlineOnly, food, entertainment, delivery, travel];
static const all = IoniaCategory(index: 0, title: 'All', ids: [], iconPath: 'assets/images/category.png');
static const apparel = IoniaCategory(index: 1, title: 'Apparel', ids: [1], iconPath: 'assets/images/tshirt.png');
static const onlineOnly = IoniaCategory(index: 2, title: 'Online Only', ids: [13, 43], iconPath: 'assets/images/global.png');
static const food = IoniaCategory(index: 3, title: 'Food', ids: [4], iconPath: 'assets/images/food.png');
static const entertainment = IoniaCategory(index: 4, title: 'Entertainment', ids: [5], iconPath: 'assets/images/gaming.png');
static const delivery = IoniaCategory(index: 5, title: 'Delivery', ids: [114, 109], iconPath: 'assets/images/delivery.png');
static const travel = IoniaCategory(index: 6, title: 'Travel', ids: [12], iconPath: 'assets/images/airplane.png');
final int index;
final String title;
final List<int> ids;
final String iconPath;
}

View file

@ -0,0 +1,58 @@
import 'package:cake_wallet/ionia/ionia_virtual_card.dart';
import 'package:flutter/material.dart';
abstract class IoniaCreateAccountState {}
class IoniaInitialCreateState extends IoniaCreateAccountState {}
class IoniaCreateStateSuccess extends IoniaCreateAccountState {}
class IoniaCreateStateLoading extends IoniaCreateAccountState {}
class IoniaCreateStateFailure extends IoniaCreateAccountState {
IoniaCreateStateFailure({@required this.error});
final String error;
}
abstract class IoniaOtpState {}
class IoniaOtpValidating extends IoniaOtpState {}
class IoniaOtpSuccess extends IoniaOtpState {}
class IoniaOtpSendDisabled extends IoniaOtpState {}
class IoniaOtpSendEnabled extends IoniaOtpState {}
class IoniaOtpFailure extends IoniaOtpState {
IoniaOtpFailure({@required this.error});
final String error;
}
class IoniaCreateCardState {}
class IoniaCreateCardSuccess extends IoniaCreateCardState {}
class IoniaCreateCardLoading extends IoniaCreateCardState {}
class IoniaCreateCardFailure extends IoniaCreateCardState {
IoniaCreateCardFailure({@required this.error});
final String error;
}
class IoniaFetchCardState {}
class IoniaNoCardState extends IoniaFetchCardState {}
class IoniaFetchingCard extends IoniaFetchCardState {}
class IoniaFetchCardFailure extends IoniaFetchCardState {}
class IoniaCardSuccess extends IoniaFetchCardState {
IoniaCardSuccess({@required this.card});
final IoniaVirtualCard card;
}

View file

@ -0,0 +1,69 @@
import 'dart:convert';
import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart';
import 'package:flutter/foundation.dart';
class IoniaGiftCard {
IoniaGiftCard({
@required this.id,
@required this.merchantId,
@required this.legalName,
@required this.systemName,
@required this.barcodeUrl,
@required this.cardNumber,
@required this.cardPin,
@required this.instructions,
@required this.tip,
@required this.purchaseAmount,
@required this.actualAmount,
@required this.totalTransactionAmount,
@required this.totalDashTransactionAmount,
@required this.remainingAmount,
@required this.createdDateFormatted,
@required this.lastTransactionDateFormatted,
@required this.isActive,
@required this.isEmpty,
@required this.logoUrl});
factory IoniaGiftCard.fromJsonMap(Map<String, dynamic> element) {
return IoniaGiftCard(
id: element['Id'] as int,
merchantId: element['MerchantId'] as int,
legalName: element['LegalName'] as String,
systemName: element['SystemName'] as String,
barcodeUrl: element['BarcodeUrl'] as String,
cardNumber: element['CardNumber'] as String,
cardPin: element['CardPin'] as String,
tip: element['Tip'] as double,
purchaseAmount: element['PurchaseAmount'] as double,
actualAmount: element['ActualAmount'] as double,
totalTransactionAmount: element['TotalTransactionAmount'] as double,
totalDashTransactionAmount: element['TotalDashTransactionAmount'] as double,
remainingAmount: element['RemainingAmount'] as double,
isActive: element['IsActive'] as bool,
isEmpty: element['IsEmpty'] as bool,
logoUrl: element['LogoUrl'] as String,
createdDateFormatted: element['CreatedDate'] as String,
lastTransactionDateFormatted: element['LastTransactionDate'] as String,
instructions: IoniaGiftCardInstruction.parseListOfInstructions(element['PaymentInstructions'] as String));
}
final int id;
final int merchantId;
final String legalName;
final String systemName;
final String barcodeUrl;
final String cardNumber;
final String cardPin;
final List<IoniaGiftCardInstruction> instructions;
final double tip;
final double purchaseAmount;
final double actualAmount;
final double totalTransactionAmount;
final double totalDashTransactionAmount;
final double remainingAmount;
final String createdDateFormatted;
final String lastTransactionDateFormatted;
final bool isActive;
final bool isEmpty;
final String logoUrl;
}

View file

@ -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;
}

View file

@ -0,0 +1,176 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart';
class IoniaMerchant {
IoniaMerchant({
@required this.id,
@required this.legalName,
@required this.systemName,
@required this.description,
@required this.website,
@required this.termsAndConditions,
@required this.logoUrl,
@required this.cardImageUrl,
@required this.cardholderAgreement,
@required this.purchaseFee,
@required this.revenueShare,
@required this.marketingFee,
@required this.minimumDiscount,
@required this.level1,
@required this.level2,
@required this.level3,
@required this.level4,
@required this.level5,
@required this.level6,
@required this.level7,
@required this.isActive,
@required this.isDeleted,
@required this.isOnline,
@required this.isPhysical,
@required this.isVariablePurchase,
@required this.minimumCardPurchase,
@required this.maximumCardPurchase,
@required this.acceptsTips,
@required this.createdDateFormatted,
@required this.createdBy,
@required this.isRegional,
@required this.modifiedDateFormatted,
@required this.modifiedBy,
@required this.usageInstructions,
@required this.usageInstructionsBak,
@required this.paymentGatewayId,
@required this.giftCardGatewayId,
@required this.isHtmlDescription,
@required this.purchaseInstructions,
@required this.balanceInstructions,
@required this.amountPerCard,
@required this.processingMessage,
@required this.hasBarcode,
@required this.hasInventory,
@required this.isVoidable,
@required this.receiptMessage,
@required this.cssBorderCode,
@required this.instructions,
@required this.alderSku,
@required this.ngcSku,
@required this.acceptedCurrency,
@required this.deepLink,
@required this.isPayLater,
@required this.savingsPercentage});
factory IoniaMerchant.fromJsonMap(Map<String, dynamic> element) {
return IoniaMerchant(
id: element["Id"] as int,
legalName: element["LegalName"] as String,
systemName: element["SystemName"] as String,
description: element["Description"] as String,
website: element["Website"] as String,
termsAndConditions: element["TermsAndConditions"] as String,
logoUrl: element["LogoUrl"] as String,
cardImageUrl: element["CardImageUrl"] as String,
cardholderAgreement: element["CardholderAgreement"] as String,
purchaseFee: element["PurchaseFee"] as double,
revenueShare: element["RevenueShare"] as double,
marketingFee: element["MarketingFee"] as double,
minimumDiscount: element["MinimumDiscount"] as double,
level1: element["Level1"] as double,
level2: element["Level2"] as double,
level3: element["Level3"] as double,
level4: element["Level4"] as double,
level5: element["Level5"] as double,
level6: element["Level6"] as double,
level7: element["Level7"] as double,
isActive: element["IsActive"] as bool,
isDeleted: element["IsDeleted"] as bool,
isOnline: element["IsOnline"] as bool,
isPhysical: element["IsPhysical"] as bool,
isVariablePurchase: element["IsVariablePurchase"] as bool,
minimumCardPurchase: element["MinimumCardPurchase"] as double,
maximumCardPurchase: element["MaximumCardPurchase"] as double,
acceptsTips: element["AcceptsTips"] as bool,
createdDateFormatted: element["CreatedDate"] as String,
createdBy: element["CreatedBy"] as int,
isRegional: element["IsRegional"] as bool,
modifiedDateFormatted: element["ModifiedDate"] as String,
modifiedBy: element["ModifiedBy"] as int,
usageInstructions: element["UsageInstructions"] as String,
usageInstructionsBak: element["UsageInstructionsBak"] as String,
paymentGatewayId: element["PaymentGatewayId"] as int,
giftCardGatewayId: element["GiftCardGatewayId"] as int ,
isHtmlDescription: element["IsHtmlDescription"] as bool,
purchaseInstructions: element["PurchaseInstructions"] as String,
balanceInstructions: element["BalanceInstructions"] as String,
amountPerCard: element["AmountPerCard"] as double,
processingMessage: element["ProcessingMessage"] as String,
hasBarcode: element["HasBarcode"] as bool,
hasInventory: element["HasInventory"] as bool,
isVoidable: element["IsVoidable"] as bool,
receiptMessage: element["ReceiptMessage"] as String,
cssBorderCode: element["CssBorderCode"] as String,
instructions: IoniaGiftCardInstruction.parseListOfInstructions(element['PaymentInstructions'] as String),
alderSku: element["AlderSku"] as String,
ngcSku: element["NgcSku"] as String,
acceptedCurrency: element["AcceptedCurrency"] as String,
deepLink: element["DeepLink"] as String,
isPayLater: element["IsPayLater"] as bool,
savingsPercentage: element["SavingsPercentage"] as double);
}
final int id;
final String legalName;
final String systemName;
final String description;
final String website;
final String termsAndConditions;
final String logoUrl;
final String cardImageUrl;
final String cardholderAgreement;
final double purchaseFee;
final double revenueShare;
final double marketingFee;
final double minimumDiscount;
final double level1;
final double level2;
final double level3;
final double level4;
final double level5;
final double level6;
final double level7;
final bool isActive;
final bool isDeleted;
final bool isOnline;
final bool isPhysical;
final bool isVariablePurchase;
final double minimumCardPurchase;
final double maximumCardPurchase;
final bool acceptsTips;
final String createdDateFormatted;
final int createdBy;
final bool isRegional;
final String modifiedDateFormatted;
final int modifiedBy;
final String usageInstructions;
final String usageInstructionsBak;
final int paymentGatewayId;
final int giftCardGatewayId;
final bool isHtmlDescription;
final String purchaseInstructions;
final String balanceInstructions;
final double amountPerCard;
final String processingMessage;
final bool hasBarcode;
final bool hasInventory;
final bool isVoidable;
final String receiptMessage;
final String cssBorderCode;
final List<IoniaGiftCardInstruction> instructions;
final String alderSku;
final String ngcSku;
final String acceptedCurrency;
final String deepLink;
final bool isPayLater;
final double savingsPercentage;
double get discount => savingsPercentage;
}

View file

@ -0,0 +1,23 @@
import 'package:flutter/foundation.dart';
class IoniaOrder {
IoniaOrder({@required this.id,
@required this.uri,
@required this.currency,
@required this.amount,
@required this.paymentId});
factory IoniaOrder.fromMap(Map<String, dynamic> obj) {
return IoniaOrder(
id: obj['order_id'] as String,
uri: obj['uri'] as String,
currency: obj['currency'] as String,
amount: obj['amount'] as double,
paymentId: obj['paymentId'] as String);
}
final String id;
final String uri;
final String currency;
final double amount;
final String paymentId;
}

View file

@ -0,0 +1,172 @@
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/ionia/ionia_order.dart';
import 'package:cake_wallet/ionia/ionia_virtual_card.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/ionia/ionia_api.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
class IoniaService {
IoniaService(this.secureStorage, this.ioniaApi);
static const ioniaEmailStorageKey = 'ionia_email';
static const ioniaUsernameStorageKey = 'ionia_username';
static const ioniaPasswordStorageKey = 'ionia_password';
static String get clientId => secrets.ioniaClientId;
final FlutterSecureStorage secureStorage;
final IoniaApi ioniaApi;
// Create user
Future<void> createUser(String email) async {
final username = await ioniaApi.createUser(email, clientId: clientId);
await secureStorage.write(key: ioniaEmailStorageKey, value: email);
await secureStorage.write(key: ioniaUsernameStorageKey, value: username);
}
// Verify email
Future<void> verifyEmail(String code) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final email = await secureStorage.read(key: ioniaEmailStorageKey);
final credentials = await ioniaApi.verifyEmail(email: email, username: username, code: code, clientId: clientId);
await secureStorage.write(key: ioniaPasswordStorageKey, value: credentials.password);
await secureStorage.write(key: ioniaUsernameStorageKey, value: credentials.username);
}
// Sign In
Future<void> signIn(String email) async {
final username = await ioniaApi.signIn(email, clientId: clientId);
await secureStorage.write(key: ioniaEmailStorageKey, value: email);
await secureStorage.write(key: ioniaUsernameStorageKey, value: username);
}
Future<String> getUserEmail() async {
return secureStorage.read(key: ioniaEmailStorageKey);
}
// Check is user logined
Future<bool> isLogined() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey) ?? '';
final password = await secureStorage.read(key: ioniaPasswordStorageKey) ?? '';
return username.isNotEmpty && password.isNotEmpty;
}
// Logout
Future<void> logout() async {
await secureStorage.delete(key: ioniaUsernameStorageKey);
await secureStorage.delete(key: ioniaPasswordStorageKey);
}
// Create virtual card
Future<IoniaVirtualCard> createCard() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.createCard(username: username, password: password, clientId: clientId);
}
// Get virtual card
Future<IoniaVirtualCard> getCard() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getCards(username: username, password: password, clientId: clientId);
}
// Get Merchants
Future<List<IoniaMerchant>> getMerchants() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getMerchants(username: username, password: password, clientId: clientId);
}
// Get Merchants By Filter
Future<List<IoniaMerchant>> getMerchantsByFilter({
String search,
List<IoniaCategory> categories,
int merchantFilterType = 0}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getMerchantsByFilter(
username: username,
password: password,
clientId: clientId,
search: search,
categories: categories,
merchantFilterType: merchantFilterType);
}
// Purchase Gift Card
Future<IoniaOrder> purchaseGiftCard({
@required String merchId,
@required double amount,
@required String currency}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.purchaseGiftCard(
merchId: merchId,
amount: amount,
currency: currency,
username: username,
password: password,
clientId: clientId);
}
// Get Current User Gift Card Summaries
Future<List<IoniaGiftCard>> getCurrentUserGiftCardSummaries() async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getCurrentUserGiftCardSummaries(username: username, password: password, clientId: clientId);
}
// Charge Gift Card
Future<void> chargeGiftCard({
@required int giftCardId,
@required double amount}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
await ioniaApi.chargeGiftCard(
username: username,
password: password,
clientId: clientId,
giftCardId: giftCardId,
amount: amount);
}
// Redeem
Future<void> redeem(IoniaGiftCard giftCard) async {
await chargeGiftCard(giftCardId: giftCard.id, amount: giftCard.remainingAmount);
}
// Get Gift Card
Future<IoniaGiftCard> getGiftCard({@required int id}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getGiftCard(username: username, password: password, clientId: clientId,id: id);
}
// Payment Status
Future<int> getPaymentStatus({
@required String orderId,
@required String paymentId}) async {
final username = await secureStorage.read(key: ioniaUsernameStorageKey);
final password = await secureStorage.read(key: ioniaPasswordStorageKey);
return ioniaApi.getPaymentStatus(username: username, password: password, clientId: clientId, orderId: orderId, paymentId: paymentId);
}
}

12
lib/ionia/ionia_tip.dart Normal file
View file

@ -0,0 +1,12 @@
class IoniaTip {
const IoniaTip({this.originalAmount, this.percentage});
final double originalAmount;
final double percentage;
double get additionalAmount => double.parse((originalAmount * percentage / 100).toStringAsFixed(2));
static const tipList = [
IoniaTip(originalAmount: 0, percentage: 0),
IoniaTip(originalAmount: 10, percentage: 10),
IoniaTip(originalAmount: 20, percentage: 20)
];
}

View file

@ -0,0 +1,43 @@
import 'package:flutter/foundation.dart';
import 'dart:convert';
class IoniaTokenData {
IoniaTokenData({@required this.accessToken, @required this.tokenType, @required this.expiredAt});
factory IoniaTokenData.fromJson(String source) {
final decoded = json.decode(source) as Map<String, dynamic>;
final accessToken = decoded['access_token'] as String;
final expiresIn = decoded['expires_in'] as int;
final tokenType = decoded['token_type'] as String;
final expiredAtInMilliseconds = decoded['expired_at'] as int;
DateTime expiredAt;
if (expiredAtInMilliseconds != null) {
expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtInMilliseconds);
} else {
expiredAt = DateTime.now().add(Duration(seconds: expiresIn));
}
return IoniaTokenData(
accessToken: accessToken,
tokenType: tokenType,
expiredAt: expiredAt);
}
final String accessToken;
final String tokenType;
final DateTime expiredAt;
bool get isExpired => DateTime.now().isAfter(expiredAt);
@override
String toString() => '$tokenType $accessToken';
String toJson() {
return json.encode(<String, dynamic>{
'access_token': accessToken,
'token_type': tokenType,
'expired_at': expiredAt.millisecondsSinceEpoch
});
}
}

View file

@ -0,0 +1,6 @@
class IoniaUserCredentials {
const IoniaUserCredentials(this.username, this.password);
final String username;
final String password;
}

View file

@ -0,0 +1,43 @@
import 'package:flutter/foundation.dart';
class IoniaVirtualCard {
IoniaVirtualCard({
@required this.token,
@required this.createdAt,
@required this.lastFour,
@required this.state,
@required this.pan,
@required this.cvv,
@required this.expirationMonth,
@required this.expirationYear,
@required this.fundsLimit,
@required this.spendLimit});
factory IoniaVirtualCard.fromMap(Map<String, Object> source) {
final created = source['created'] as String;
final createdAt = DateTime.tryParse(created);
return IoniaVirtualCard(
token: source['token'] as String,
createdAt: createdAt,
lastFour: source['lastFour'] as String,
state: source['state'] as String,
pan: source['pan'] as String,
cvv: source['cvv'] as String,
expirationMonth: source['expirationMonth'] as String,
expirationYear: source['expirationYear'] as String,
fundsLimit: source['FundsLimit'] as double,
spendLimit: source['spend_limit'] as double);
}
final String token;
final String lastFour;
final String state;
final String pan;
final String cvv;
final String expirationMonth;
final String expirationYear;
final DateTime createdAt;
final double fundsLimit;
final double spendLimit;
}

View file

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/entities/language_service.dart';
import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
import 'package:cake_wallet/ionia/ionia_merchant.dart';
import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -174,7 +176,8 @@ Future<void> initialSetup(
exchangeTemplates: exchangeTemplates, exchangeTemplates: exchangeTemplates,
transactionDescriptionBox: transactionDescriptions, transactionDescriptionBox: transactionDescriptions,
ordersSource: ordersSource, ordersSource: ordersSource,
unspentCoinsInfoSource: unspentCoinsInfoSource); unspentCoinsInfoSource: unspentCoinsInfoSource,
);
await bootstrap(navigatorKey); await bootstrap(navigatorKey);
monero?.onStartup(); monero?.onStartup();
} }

View file

@ -2,7 +2,7 @@ part of 'monero.dart';
class CWMoneroAccountList extends MoneroAccountList { class CWMoneroAccountList extends MoneroAccountList {
CWMoneroAccountList(this._wallet); CWMoneroAccountList(this._wallet);
Object _wallet; final Object _wallet;
@override @override
@computed @computed
@ -39,13 +39,13 @@ class CWMoneroAccountList extends MoneroAccountList {
@override @override
Future<void> addAccount(Object wallet, {String label}) async { Future<void> addAccount(Object wallet, {String label}) async {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.accountList.addAccount(label: label); await moneroWallet.walletAddresses.accountList.addAccount(label: label);
} }
@override @override
Future<void> setLabelAccount(Object wallet, {int accountIndex, String label}) async { Future<void> setLabelAccount(Object wallet, {int accountIndex, String label}) async {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.accountList await moneroWallet.walletAddresses.accountList
.setLabelAccount( .setLabelAccount(
accountIndex: accountIndex, accountIndex: accountIndex,
label: label); label: label);
@ -95,7 +95,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
@override @override
Future<void> addSubaddress(Object wallet, {int accountIndex, String label}) async { Future<void> addSubaddress(Object wallet, {int accountIndex, String label}) async {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.subaddressList await moneroWallet.walletAddresses.subaddressList
.addSubaddress( .addSubaddress(
accountIndex: accountIndex, accountIndex: accountIndex,
label: label); label: label);
@ -105,7 +105,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList {
Future<void> setLabelSubaddress(Object wallet, Future<void> setLabelSubaddress(Object wallet,
{int accountIndex, int addressIndex, String label}) async { {int accountIndex, int addressIndex, String label}) async {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.subaddressList await moneroWallet.walletAddresses.subaddressList
.setLabelSubaddress( .setLabelSubaddress(
accountIndex: accountIndex, accountIndex: accountIndex,
addressIndex: addressIndex, addressIndex: addressIndex,
@ -140,35 +140,43 @@ class CWMonero extends Monero {
return CWMoneroAccountList(wallet); return CWMoneroAccountList(wallet);
} }
@override
MoneroSubaddressList getSubaddressList(Object wallet) { MoneroSubaddressList getSubaddressList(Object wallet) {
return CWMoneroSubaddressList(wallet); return CWMoneroSubaddressList(wallet);
} }
@override
TransactionHistoryBase getTransactionHistory(Object wallet) { TransactionHistoryBase getTransactionHistory(Object wallet) {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
return moneroWallet.transactionHistory; return moneroWallet.transactionHistory;
} }
@override
MoneroWalletDetails getMoneroWalletDetails(Object wallet) { MoneroWalletDetails getMoneroWalletDetails(Object wallet) {
return CWMoneroWalletDetails(wallet); return CWMoneroWalletDetails(wallet);
} }
@override
int getHeigthByDate({DateTime date}) { int getHeigthByDate({DateTime date}) {
return getMoneroHeigthByDate(date: date); return getMoneroHeigthByDate(date: date);
} }
@override
TransactionPriority getDefaultTransactionPriority() { TransactionPriority getDefaultTransactionPriority() {
return MoneroTransactionPriority.slow; return MoneroTransactionPriority.slow;
} }
@override
TransactionPriority deserializeMoneroTransactionPriority({int raw}) { TransactionPriority deserializeMoneroTransactionPriority({int raw}) {
return MoneroTransactionPriority.deserialize(raw: raw); return MoneroTransactionPriority.deserialize(raw: raw);
} }
@override
List<TransactionPriority> getTransactionPriorities() { List<TransactionPriority> getTransactionPriorities() {
return MoneroTransactionPriority.all; return MoneroTransactionPriority.all;
} }
@override
List<String> getMoneroWordList(String language) { List<String> getMoneroWordList(String language) {
switch (language.toLowerCase()) { switch (language.toLowerCase()) {
case 'english': case 'english':
@ -196,14 +204,15 @@ class CWMonero extends Monero {
} }
} }
@override
WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ WalletCredentials createMoneroRestoreWalletFromKeysCredentials({
String name, String name,
String spendKey, String spendKey,
String viewKey, String viewKey,
String address, String address,
String password, String password,
String language, String language,
int height}) { int height}) {
return MoneroRestoreWalletFromKeysCredentials( return MoneroRestoreWalletFromKeysCredentials(
name: name, name: name,
spendKey: spendKey, spendKey: spendKey,
@ -214,6 +223,7 @@ class CWMonero extends Monero {
height: height); height: height);
} }
@override
WalletCredentials createMoneroRestoreWalletFromSeedCredentials({String name, String password, int height, String mnemonic}) { WalletCredentials createMoneroRestoreWalletFromSeedCredentials({String name, String password, int height, String mnemonic}) {
return MoneroRestoreWalletFromSeedCredentials( return MoneroRestoreWalletFromSeedCredentials(
name: name, name: name,
@ -222,6 +232,7 @@ class CWMonero extends Monero {
mnemonic: mnemonic); mnemonic: mnemonic);
} }
@override
WalletCredentials createMoneroNewWalletCredentials({String name, String password, String language}) { WalletCredentials createMoneroNewWalletCredentials({String name, String password, String language}) {
return MoneroNewWalletCredentials( return MoneroNewWalletCredentials(
name: name, name: name,
@ -229,6 +240,7 @@ class CWMonero extends Monero {
language: language); language: language);
} }
@override
Map<String, String> getKeys(Object wallet) { Map<String, String> getKeys(Object wallet) {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
final keys = moneroWallet.keys; final keys = moneroWallet.keys;
@ -239,6 +251,7 @@ class CWMonero extends Monero {
'publicViewKey': keys.publicViewKey}; 'publicViewKey': keys.publicViewKey};
} }
@override
Object createMoneroTransactionCreationCredentials({List<Output> outputs, TransactionPriority priority}) { Object createMoneroTransactionCreationCredentials({List<Output> outputs, TransactionPriority priority}) {
return MoneroTransactionCreationCredentials( return MoneroTransactionCreationCredentials(
outputs: outputs.map((out) => OutputInfo( outputs: outputs.map((out) => OutputInfo(
@ -254,49 +267,72 @@ class CWMonero extends Monero {
priority: priority as MoneroTransactionPriority); priority: priority as MoneroTransactionPriority);
} }
@override
Object createMoneroTransactionCreationCredentialsRaw({List<OutputInfo> outputs, TransactionPriority priority}) {
return MoneroTransactionCreationCredentials(
outputs: outputs,
priority: priority as MoneroTransactionPriority);
}
@override
String formatterMoneroAmountToString({int amount}) { String formatterMoneroAmountToString({int amount}) {
return moneroAmountToString(amount: amount); return moneroAmountToString(amount: amount);
} }
@override
double formatterMoneroAmountToDouble({int amount}) { double formatterMoneroAmountToDouble({int amount}) {
return moneroAmountToDouble(amount: amount); return moneroAmountToDouble(amount: amount);
} }
@override
int formatterMoneroParseAmount({String amount}) { int formatterMoneroParseAmount({String amount}) {
return moneroParseAmount(amount: amount); return moneroParseAmount(amount: amount);
} }
@override
Account getCurrentAccount(Object wallet) { Account getCurrentAccount(Object wallet) {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
final acc = moneroWallet.walletAddresses.account; final acc = moneroWallet.walletAddresses.account;
return Account(id: acc.id, label: acc.label); return Account(id: acc.id, label: acc.label);
} }
@override
void setCurrentAccount(Object wallet, int id, String label) { void setCurrentAccount(Object wallet, int id, String label) {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.account = monero_account.Account(id: id, label: label); moneroWallet.walletAddresses.account = monero_account.Account(id: id, label: label);
} }
@override
void onStartup() { void onStartup() {
monero_wallet_api.onStartup(); monero_wallet_api.onStartup();
} }
@override
int getTransactionInfoAccountId(TransactionInfo tx) { int getTransactionInfoAccountId(TransactionInfo tx) {
final moneroTransactionInfo = tx as MoneroTransactionInfo; final moneroTransactionInfo = tx as MoneroTransactionInfo;
return moneroTransactionInfo.accountIndex; return moneroTransactionInfo.accountIndex;
} }
@override
WalletService createMoneroWalletService(Box<WalletInfo> walletInfoSource) { WalletService createMoneroWalletService(Box<WalletInfo> walletInfoSource) {
return MoneroWalletService(walletInfoSource); return MoneroWalletService(walletInfoSource);
} }
@override
String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
return moneroWallet.getTransactionAddress(accountIndex, addressIndex); return moneroWallet.getTransactionAddress(accountIndex, addressIndex);
} }
@override
String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex) { String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex) {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
return moneroWallet.getSubaddressLabel(accountIndex, addressIndex); return moneroWallet.getSubaddressLabel(accountIndex, addressIndex);
} }
@override
Map<String, String> pendingTransactionInfo(Object transaction) {
final ptx = transaction as PendingMoneroTransaction;
return {'id': ptx.id, 'hex': ptx.hex, 'key': ptx.txKey};
}
} }

View file

@ -4,6 +4,10 @@ import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; import 'package:cake_wallet/src/screens/buy/pre_order_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart';
import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart';
@ -63,6 +67,10 @@ import 'package:flutter/services.dart';
import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_wallet/wallet_types.g.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart';
import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
import 'package:cake_wallet/src/screens/ionia/ionia.dart';
import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart';
import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
RouteSettings currentRouteSettings; RouteSettings currentRouteSettings;
@ -401,6 +409,58 @@ Route<dynamic> createRoute(RouteSettings settings) {
param2: args['isLight'] as bool, param2: args['isLight'] as bool,
)); ));
case Routes.ioniaWelcomePage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaWelcomePage>());
case Routes.ioniaLoginPage:
return CupertinoPageRoute<void>( builder: (_) => getIt.get<IoniaLoginPage>());
case Routes.ioniaCreateAccountPage:
return CupertinoPageRoute<void>( builder: (_) => getIt.get<IoniaCreateAccountPage>());
case Routes.ioniaManageCardsPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaManageCardsPage>());
case Routes.ioniaBuyGiftCardPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaBuyGiftCardPage>(param1: args));
case Routes.ioniaBuyGiftCardDetailPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaBuyGiftCardDetailPage>(param1: args));
case Routes.ioniaVerifyIoniaOtpPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) =>getIt.get<IoniaVerifyIoniaOtp>(param1: args));
case Routes.ioniaDebitCardPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaDebitCardPage>());
case Routes.ioniaActivateDebitCardPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaActivateDebitCardPage>());
case Routes.ioniaAccountPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaAccountPage>());
case Routes.ioniaAccountCardsPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaAccountCardsPage>());
case Routes.ioniaCustomTipPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) =>getIt.get<IoniaCustomTipPage>(param1: args));
case Routes.ioniaGiftCardDetailPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaGiftCardDetailPage>(param1: args.first));
case Routes.ioniaPaymentStatusPage:
final args = settings.arguments as List;
final paymentInfo = args.first as IoniaAnyPayPaymentInfo;
final commitedInfo = args[1] as AnyPayPaymentCommittedInfo;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaPaymentStatusPage>(
param1: paymentInfo,
param2: commitedInfo));
default: default:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (_) => Scaffold( builder: (_) => Scaffold(

View file

@ -60,4 +60,18 @@ class Routes {
static const moneroNewWalletFromWelcome = '/monero_new_wallet'; static const moneroNewWalletFromWelcome = '/monero_new_wallet';
static const addressPage = '/address_page'; static const addressPage = '/address_page';
static const fullscreenQR = '/fullscreen_qr'; static const fullscreenQR = '/fullscreen_qr';
} static const ioniaWelcomePage = '/cake_pay_welcome_page';
static const ioniaCreateAccountPage = '/cake_pay_create_account_page';
static const ioniaLoginPage = '/cake_pay_login_page';
static const ioniaManageCardsPage = '/manage_cards_page';
static const ioniaBuyGiftCardPage = '/buy_gift_card_page';
static const ioniaBuyGiftCardDetailPage = '/buy_gift_card_detail_page';
static const ioniaVerifyIoniaOtpPage = '/cake_pay_verify_otp_page';
static const ioniaDebitCardPage = '/debit_card_page';
static const ioniaActivateDebitCardPage = '/activate_debit_card_page';
static const ioniaAccountPage = 'ionia_account_page';
static const ioniaAccountCardsPage = 'ionia_account_cards_page';
static const ioniaCustomTipPage = 'ionia_custom_tip_page';
static const ioniaGiftCardDetailPage = '/ionia_gift_card_detail_page';
static const ioniaPaymentStatusPage = '/ionia_payment_status_page';
}

View file

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/yat/yat_popup.dart';
import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; import 'package:cake_wallet/src/screens/yat_emoji_id.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_base.dart';
@ -14,19 +14,15 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/router.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
class DashboardPage extends BasePage { class DashboardPage extends BasePage {
DashboardPage({ DashboardPage({
@ -85,7 +81,7 @@ class DashboardPage extends BasePage {
final DashboardViewModel walletViewModel; final DashboardViewModel walletViewModel;
final WalletAddressListViewModel addressListViewModel; final WalletAddressListViewModel addressListViewModel;
final controller = PageController(initialPage: 0); final controller = PageController(initialPage: 1);
var pages = <Widget>[]; var pages = <Widget>[];
bool _isEffectsInstalled = false; bool _isEffectsInstalled = false;
@ -221,7 +217,7 @@ class DashboardPage extends BasePage {
if (_isEffectsInstalled) { if (_isEffectsInstalled) {
return; return;
} }
pages.add(MarketPlacePage(dashboardViewModel: walletViewModel));
pages.add(balancePage); pages.add(balancePage);
pages.add(TransactionsPage(dashboardViewModel: walletViewModel)); pages.add(TransactionsPage(dashboardViewModel: walletViewModel));
_isEffectsInstalled = true; _isEffectsInstalled = true;

View file

@ -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);
}
}
}

View file

@ -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],
);
}

View file

@ -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],
);
}

View file

@ -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);
}

View file

@ -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)
],
)
],
),
);
}
}

View file

@ -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,
),
);
},
);
}
}

View file

@ -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,
),
),
);
}
}

View file

@ -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(),
);
},
);
}
}

View file

@ -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,
),
),
);
}
}

View file

@ -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),
],
),
),
),
);
}
}

View file

@ -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),
],
),
),
),
);
}
}

View file

@ -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),
),
],
);
}
}

View file

@ -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,
);
});
}
}

View file

@ -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,
),
),
);
}
}

View file

@ -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));
});
}
}

View file

@ -0,0 +1,9 @@
export 'auth/ionia_welcome_page.dart';
export 'auth/ionia_create_account_page.dart';
export 'auth/ionia_login_page.dart';
export 'auth/ionia_verify_otp_page.dart';
export 'cards/ionia_activate_debit_card_page.dart';
export 'cards/ionia_buy_card_detail_page.dart';
export 'cards/ionia_manage_cards_page.dart';
export 'cards/ionia_debit_card_page.dart';
export 'cards/ionia_buy_gift_card.dart';

View file

@ -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),
),
);
}
}

View file

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
class CardMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
);
}
}

View file

@ -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,
),
)),
),
));
}
}

View file

@ -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,
),
),
)
],
),
),
);
}
}

View file

@ -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,
),
),
)
],
),
),
);
}
}

View file

@ -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,
),
),
],
)
],
));
}
}

View file

@ -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();
}
}

View file

@ -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,
),
],
),
);
}
}

View file

@ -10,7 +10,10 @@ class AlertWithTwoActions extends BaseAlertDialog {
@required this.rightButtonText, @required this.rightButtonText,
@required this.actionLeftButton, @required this.actionLeftButton,
@required this.actionRightButton, @required this.actionRightButton,
this.alertBarrierDismissible = true this.alertBarrierDismissible = true,
this.isDividerExist = false,
this.leftActionColor,
this.rightActionColor,
}); });
final String alertTitle; final String alertTitle;
@ -20,6 +23,9 @@ class AlertWithTwoActions extends BaseAlertDialog {
final VoidCallback actionLeftButton; final VoidCallback actionLeftButton;
final VoidCallback actionRightButton; final VoidCallback actionRightButton;
final bool alertBarrierDismissible; final bool alertBarrierDismissible;
final Color leftActionColor;
final Color rightActionColor;
final bool isDividerExist;
@override @override
String get titleText => alertTitle; String get titleText => alertTitle;
@ -35,4 +41,10 @@ class AlertWithTwoActions extends BaseAlertDialog {
VoidCallback get actionRight => actionRightButton; VoidCallback get actionRight => actionRightButton;
@override @override
bool get barrierDismissible => alertBarrierDismissible; bool get barrierDismissible => alertBarrierDismissible;
} @override
Color get leftButtonColor => leftActionColor;
@override
Color get rightButtonColor => rightActionColor;
@override
bool get isDividerExists => isDividerExist;
}

View file

@ -46,30 +46,28 @@ class BaseAlertDialog extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: Container( child: Container(
height: 52, height: 52,
padding: EdgeInsets.only(left: 6, right: 6), padding: EdgeInsets.only(left: 6, right: 6),
color: Theme.of(context).accentTextTheme.body2.decorationColor, color: Theme.of(context).accentTextTheme.body2.decorationColor,
child: ButtonTheme( child: ButtonTheme(
minWidth: double.infinity, minWidth: double.infinity,
child: FlatButton( child: FlatButton(
onPressed: actionLeft, onPressed: actionLeft,
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
splashColor: Colors.transparent, splashColor: Colors.transparent,
child: Text( child: Text(
leftActionButtonText, leftActionButtonText,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.body2 color: Theme.of(context).primaryTextTheme.body2.backgroundColor,
.backgroundColor, decoration: TextDecoration.none,
decoration: TextDecoration.none, ),
), )),
)), ),
), )),
)
),
Container( Container(
width: 1, width: 1,
height: 52, height: 52,
@ -77,30 +75,28 @@ class BaseAlertDialog extends StatelessWidget {
), ),
Flexible( Flexible(
child: Container( child: Container(
height: 52, height: 52,
padding: EdgeInsets.only(left: 6, right: 6), padding: EdgeInsets.only(left: 6, right: 6),
color: Theme.of(context).accentTextTheme.body1.backgroundColor, color: Theme.of(context).accentTextTheme.body1.backgroundColor,
child: ButtonTheme( child: ButtonTheme(
minWidth: double.infinity, minWidth: double.infinity,
child: FlatButton( child: FlatButton(
onPressed: actionRight, onPressed: actionRight,
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
splashColor: Colors.transparent, splashColor: Colors.transparent,
child: Text( child: Text(
rightActionButtonText, rightActionButtonText,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.body1 color: Theme.of(context).primaryTextTheme.body1.backgroundColor,
.backgroundColor, decoration: TextDecoration.none,
decoration: TextDecoration.none, ),
), )),
)), ),
), )),
)
),
], ],
); );
} }
@ -108,9 +104,7 @@ class BaseAlertDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () => barrierDismissible onTap: () => barrierDismissible ? Navigator.of(context).pop() : null,
? Navigator.of(context).pop()
: null,
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,
child: BackdropFilter( child: BackdropFilter(
@ -136,14 +130,14 @@ class BaseAlertDialog extends StatelessWidget {
child: title(context), child: title(context),
), ),
isDividerExists isDividerExists
? Padding( ? Padding(
padding: EdgeInsets.only(top: 16, bottom: 8), padding: EdgeInsets.only(top: 16, bottom: 8),
child: Container( child: Container(
height: 1, height: 1,
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
), ),
) )
: Offstage(), : Offstage(),
Padding( Padding(
padding: EdgeInsets.fromLTRB(24, 8, 24, 32), padding: EdgeInsets.fromLTRB(24, 8, 24, 32),
child: content(context), child: content(context),
@ -166,4 +160,4 @@ class BaseAlertDialog extends StatelessWidget {
), ),
); );
} }
} }

View file

@ -5,13 +5,19 @@ class CakeScrollbar extends StatelessWidget {
@required this.backgroundHeight, @required this.backgroundHeight,
@required this.thumbHeight, @required this.thumbHeight,
@required this.fromTop, @required this.fromTop,
this.rightOffset = 6 this.rightOffset = 6,
this.backgroundColor,
this.thumbColor,
this.width = 6,
}); });
final double backgroundHeight; final double backgroundHeight;
final double thumbHeight; final double thumbHeight;
final double fromTop; final double fromTop;
final double width;
final double rightOffset; final double rightOffset;
final Color backgroundColor;
final Color thumbColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -19,11 +25,10 @@ class CakeScrollbar extends StatelessWidget {
right: rightOffset, right: rightOffset,
child: Container( child: Container(
height: backgroundHeight, height: backgroundHeight,
width: 6, width: width,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).textTheme.body1.decorationColor, color: backgroundColor ?? Theme.of(context).textTheme.body1.decorationColor,
borderRadius: BorderRadius.all(Radius.circular(3)) borderRadius: BorderRadius.all(Radius.circular(3))),
),
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
AnimatedPositioned( AnimatedPositioned(
@ -31,16 +36,14 @@ class CakeScrollbar extends StatelessWidget {
top: fromTop, top: fromTop,
child: Container( child: Container(
height: thumbHeight, height: thumbHeight,
width: 6.0, width: width,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).textTheme.body1.color, color: thumbColor ?? Theme.of(context).textTheme.body1.color,
borderRadius: BorderRadius.all(Radius.circular(3)) borderRadius: BorderRadius.all(Radius.circular(3))),
),
), ),
) )
], ],
), ),
) ));
);
} }
} }

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
class DiscountBadge extends StatelessWidget {
const DiscountBadge({
Key key,
@required this.percentage,
this.discountBackground,
}) : super(key: key);
final double percentage;
final AssetImage discountBackground;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
S.of(context).discount(percentage.toStringAsFixed(2)),
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
fontFamily: 'Lato',
),
),
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image: discountBackground ?? AssetImage('assets/images/badge_discount.png'),
),
),
);
}
}

View file

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
class MarketPlaceItem extends StatelessWidget {
MarketPlaceItem({
@required this.onTap,
@required this.title,
@required this.subTitle,
});
final VoidCallback onTap;
final String title;
final String subTitle;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Stack(
children: [
Container(
padding: EdgeInsets.all(20),
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).textTheme.title.backgroundColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.20),
),
),
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: Theme.of(context)
.accentTextTheme
.display3
.backgroundColor,
fontSize: 24,
fontWeight: FontWeight.w900,
),
),
SizedBox(height: 5),
Text(
subTitle,
style: TextStyle(
color: Theme.of(context)
.accentTextTheme
.display3
.backgroundColor,
fontWeight: FontWeight.w500,
fontFamily: 'Lato'),
)
],
),
),
],
),
);
}
}

59
lib/typography.dart Normal file
View file

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
const latoFont = "Lato";
TextStyle textXxSmall({Color color}) => _cakeRegular(10, color);
TextStyle textXxSmallSemiBold({Color color}) => _cakeSemiBold(10, color);
TextStyle textXSmall({Color color}) => _cakeRegular(12, color);
TextStyle textXSmallSemiBold({Color color}) => _cakeSemiBold(12, color);
TextStyle textSmall({Color color}) => _cakeRegular(14, color);
TextStyle textSmallSemiBold({Color color}) => _cakeSemiBold(14, color);
TextStyle textMedium({Color color}) => _cakeRegular(16, color);
TextStyle textMediumBold({Color color}) => _cakeBold(16, color);
TextStyle textMediumSemiBold({Color color}) => _cakeSemiBold(22, color);
TextStyle textLarge({Color color}) => _cakeRegular(18, color);
TextStyle textLargeSemiBold({Color color}) => _cakeSemiBold(24, color);
TextStyle textXLarge({Color color}) => _cakeRegular(32, color);
TextStyle textXLargeSemiBold({Color color}) => _cakeSemiBold(32, color);
TextStyle _cakeRegular(double size, Color color) => _textStyle(
size: size,
fontWeight: FontWeight.normal,
color: color,
);
TextStyle _cakeBold(double size, Color color) => _textStyle(
size: size,
fontWeight: FontWeight.w900,
color: color,
);
TextStyle _cakeSemiBold(double size, Color color) => _textStyle(
size: size,
fontWeight: FontWeight.w700,
color: color,
);
TextStyle _textStyle({
@required double size,
@required FontWeight fontWeight,
Color color,
}) =>
TextStyle(
fontFamily: latoFont,
fontSize: size,
fontWeight: fontWeight,
color: color ?? Colors.white,
);

View file

@ -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();
}
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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());
}
}
}

View file

@ -222,11 +222,11 @@ abstract class SendViewModelBase with Store {
case WalletType.bitcoin: case WalletType.bitcoin:
final priority = _settingsStore.priority[_wallet.type]; final priority = _settingsStore.priority[_wallet.type];
return bitcoin.createBitcoinTransactionCredentials(outputs, priority); return bitcoin.createBitcoinTransactionCredentials(outputs, priority: priority);
case WalletType.litecoin: case WalletType.litecoin:
final priority = _settingsStore.priority[_wallet.type]; final priority = _settingsStore.priority[_wallet.type];
return bitcoin.createBitcoinTransactionCredentials(outputs, priority); return bitcoin.createBitcoinTransactionCredentials(outputs, priority: priority);
case WalletType.monero: case WalletType.monero:
final priority = _settingsStore.priority[_wallet.type]; final priority = _settingsStore.priority[_wallet.type];

View file

@ -534,5 +534,103 @@
"search_currency": "Währung suchen", "search_currency": "Währung suchen",
"new_template" : "neue Vorlage", "new_template" : "neue Vorlage",
"electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin",
"wallet_name_exists": "Wallet mit diesem Namen existiert bereits" "wallet_name_exists": "Wallet mit diesem Namen existiert bereits",
"market_place": "Marktplatz",
"cake_pay_title": "Cake Pay-Geschenkkarten",
"cake_pay_subtitle": "Geschenkkarten kaufen und sofort einlösen",
"about_cake_pay": "Mit Cake Pay können Sie ganz einfach Geschenkkarten mit virtuellen Vermögenswerten kaufen, die Sie sofort bei über 150.000 Händlern in den Vereinigten Staaten ausgeben können.",
"cake_pay_account_note": "Erstellen Sie ein Konto, um die verfügbaren Karten zu sehen. Einige sind sogar mit Rabatt erhältlich!",
"already_have_account": "Sie haben bereits ein Konto?",
"create_account": "Konto erstellen",
"privacy_policy": "Datenschutzrichtlinie",
"welcome_to_cakepay": "Willkommen bei Cake Pay!",
"sign_up": "Anmelden",
"forgot_password": "Passwort vergessen",
"reset_password": "Passwort zurücksetzen",
"gift_cards": "Geschenkkarten",
"setup_your_debit_card": "Richten Sie Ihre Debitkarte ein",
"no_id_required": "Keine ID erforderlich. Upgraden und überall ausgeben",
"how_to_use_card": "Wie man diese Karte benutzt",
"purchase_gift_card": "Geschenkkarte kaufen",
"verification": "Verifizierung",
"fill_code": "Geben Sie den Bestätigungscode ein, den Sie per E-Mail erhalten haben",
"dont_get_code": "Kein Code?",
"resend_code": "Bitte erneut senden",
"debit_card": "Debitkarte",
"cakepay_prepaid_card": "CakePay-Prepaid-Debitkarte",
"no_id_needed": "Keine ID erforderlich!",
"frequently_asked_questions": "Häufig gestellte Fragen",
"debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Geldbörse unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.",
"Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.",
"cardholder_agreement": "Karteninhabervertrag",
"e_sign_consent": "E-Sign-Zustimmung",
"agree_and_continue": "Zustimmen & fortfahren",
"email_address": "E-Mail-Adresse",
"agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ",
"und": "und",
"enter_code": "Code eingeben",
"congratulations": "Glückwunsch!",
"you_now_have_debit_card": "Sie haben jetzt eine Debitkarte",
"min_amount": "Min: ${value}",
"max_amount": "Max: ${value}",
"enter_amount": "Betrag eingeben",
"billing_address_info": "Wenn Sie nach einer Rechnungsadresse gefragt werden, geben Sie bitte Ihre Lieferadresse an",
"order_physical_card": "Physische Karte bestellen",
"add_value": "Wert hinzufügen",
"activate": "aktivieren",
"get_a": "Hole ein",
"digital_and_physical_card": "digitale en fysieke prepaid debetkaart",
"get_card_note": " die u kunt herladen met digitale valuta. Geen aanvullende informatie nodig!",
"signup_for_card_accept_terms": "Meld je aan voor de kaart en accepteer de voorwaarden.",
"add_fund_to_card": "Voeg prepaid tegoed toe aan de kaarten (tot ${value})",
"use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.",
"use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.",
"optionally_order_card": "Optioneel een fysieke kaart bestellen.",
"hide_details" : "Details verbergen",
"show_details" : "Toon details",
"upto": "tot ${value}",
"discount": "Bespaar ${value}%",
"gift_card_amount": "Bedrag cadeaubon",
"bill_amount": "Bill bedrag",
"you_pay": "U betaalt",
"tip": "Tip:",
"custom": "aangepast",
"by_cake_pay": "door Cake Pay",
"expires": "Verloopt",
"mm": "MM",
"yy": "JJ",
"online": "online",
"offline": "Offline",
"gift_card_number": "Cadeaukaartnummer",
"pin_number": "PIN-nummer",
"total_saving": "Totale besparingen",
"last_30_days": "Laatste 30 dagen",
"avg_savings": "Gem. besparingen",
"view_all": "Alles bekijken",
"active_cards": "Actieve kaarten",
"delete_account": "Account verwijderen",
"cards": "Kaarten",
"active": "Actief",
"redeemed": "Verzilverd",
"gift_card_balance_note": "Cadeaukaarten met een resterend saldo verschijnen hier",
"gift_card_redeemed_note": "Cadeaubonnen die je hebt ingewisseld, verschijnen hier",
"logout": "Uitloggen",
"add_tip": "Tip toevoegen",
"percentageOf": "van ${amount}",
"is_percentage": "is",
"search_category": "Zoek categorie",
"mark_as_redeemed": "Markeer als ingewisseld",
"more_options": "Meer opties",
"waiting_payment_confirmation": "In afwachting van betalingsbevestiging",
"transaction_sent_notice": "Als het scherm na 1 minuut niet verder gaat, controleer dan een blokverkenner en je e-mail.",
"agree": "mee eens",
"in_store": "In winkel",
"generating_gift_card": "Cadeaubon genereren",
"payment_was_received": "Uw betaling is ontvangen.",
"proceed_after_one_minute": "Als het scherm na 1 minuut niet verder gaat, controleer dan uw e-mail.",
"order_id": "Bestell-ID",
"gift_card_is_generated": "Geschenkkarte wird generiert",
"open_gift_card": "Geschenkkarte öffnen",
"contact_support": "Support kontaktieren",
"gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden"
} }

View file

@ -245,7 +245,7 @@
"settings_only_transactions" : "Only transactions", "settings_only_transactions" : "Only transactions",
"settings_none" : "None", "settings_none" : "None",
"settings_support" : "Support", "settings_support" : "Support",
"settings_terms_and_conditions" : "Terms and conditions", "settings_terms_and_conditions" : "Terms and Conditions",
"pin_is_incorrect" : "PIN is incorrect", "pin_is_incorrect" : "PIN is incorrect",
@ -534,5 +534,103 @@
"search_currency": "Search currency", "search_currency": "Search currency",
"new_template" : "New Template", "new_template" : "New Template",
"electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work",
"wallet_name_exists": "Wallet with that name has already existed" "wallet_name_exists": "Wallet with that name has already existed",
"market_place": "Marketplace",
"cake_pay_title": "Cake Pay Gift Cards",
"cake_pay_subtitle": "Buy gift cards and redeem instantly",
"about_cake_pay": "Cake Pay allows you to easily buy gift cards with virtual assets, spendable instantly at over 150,000 merchants in the United States.",
"cake_pay_account_note": "Make an account to see the available cards. Some are even available at a discount!",
"already_have_account": "Already have an account?",
"create_account": "Create Account",
"privacy_policy": "Privacy Policy",
"welcome_to_cakepay": "Welcome to Cake Pay!",
"sign_up": "Sign Up",
"forgot_password": "Forgot Password",
"reset_password": "Reset Password",
"gift_cards": "Gift Cards",
"setup_your_debit_card": "Set up your debit card",
"no_id_required": "No ID required. Top up and spend anywhere",
"how_to_use_card": "How to use this card",
"purchase_gift_card": "Purchase Gift Card",
"verification": "Verification",
"fill_code": "Please fill in the verification code provided to your email",
"dont_get_code": "Don't get code?",
"resend_code": "Please resend it",
"debit_card": "Debit Card",
"cakepay_prepaid_card": "CakePay Prepaid Debit Card",
"no_id_needed": "No ID needed!",
"frequently_asked_questions": "Frequently asked questions",
"debit_card_terms": "The storage and usage of your payment card number (and credentials corresponding to your payment card number) in this digital wallet are subject to the Terms and Conditions of the applicable cardholder agreement with the payment card issuer, as in effect from time to time.",
"please_reference_document": "Please reference the documents below for more information.",
"cardholder_agreement": "Cardholder Agreement",
"e_sign_consent": "E-Sign Consent",
"agree_and_continue": "Agree & Continue",
"email_address": "Email Address",
"agree_to": "By creating account you agree to the ",
"and": "and",
"enter_code": "Enter code",
"congratulations": "Congratulations!",
"you_now_have_debit_card": "You now have a debit card",
"min_amount" : "Min: ${value}",
"max_amount" : "Max: ${value}",
"enter_amount": "Enter Amount",
"billing_address_info": "If asked for a billing address, provide your shipping address",
"order_physical_card": "Order Physical Card",
"add_value": "Add value",
"activate": "Activate",
"get_a": "Get a ",
"digital_and_physical_card": " digital and physical prepaid debit card",
"get_card_note": " that you can reload with digital currencies. No additional information needed!",
"signup_for_card_accept_terms": "Sign up for the card and accept the terms.",
"add_fund_to_card": "Add prepaid funds to the cards (up to ${value})",
"use_card_info_two": "Funds are converted to USD when the held in the prepaid account, not in digital currencies.",
"use_card_info_three": "Use the digital card online or with contactless payment methods.",
"optionally_order_card": "Optionally order a physical card.",
"hide_details" : "Hide Details",
"show_details" : "Show Details",
"upto": "up to ${value}",
"discount": "Save ${value}%",
"gift_card_amount": "Gift Card Amount",
"bill_amount": "Bill amount",
"you_pay": "You pay",
"tip": "Tip:",
"custom": "custom",
"by_cake_pay": "by Cake Pay",
"expires": "Expires",
"mm": "MM",
"yy": "YY",
"online": "Online",
"offline": "Offline",
"gift_card_number": "Gift card number",
"pin_number": "PIN number",
"total_saving": "Total Savings",
"last_30_days": "Last 30 days",
"avg_savings": "Avg. savings",
"view_all": "View all",
"active_cards": "Active cards",
"delete_account": "Delete Account",
"cards": "Cards",
"active": "Active",
"redeemed": "Redeemed",
"gift_card_balance_note": "Gift cards with a balance remaining will appear here",
"gift_card_redeemed_note": "Gift cards youve redeemed will appear here",
"logout": "Logout",
"add_tip": "Add Tip",
"percentageOf": "of ${amount}",
"is_percentage": "is",
"search_category": "Search category",
"mark_as_redeemed": "Mark As Redeemed",
"more_options": "More Options",
"awaiting_payment_confirmation": "Awaiting payment confirmation",
"transaction_sent_notice": "If the screen doesnt proceed after 1 minute, check a block explorer and your email.",
"agree": "Agree",
"in_store": "In Store",
"generating_gift_card": "Generating Gift Card",
"payment_was_received": "Your payment was received.",
"proceed_after_one_minute": "If the screen doesnt proceed after 1 minute, check your email.",
"order_id": "Order ID",
"gift_card_is_generated": "Gift Card is generated",
"open_gift_card": "Open Gift Card",
"contact_support": "Contact Support",
"gift_cards_unavailable": "Gift cards are available to purchase only through Monero, Bitcoin, and Litecoin at this time"
} }

View file

@ -534,5 +534,103 @@
"search_currency": "Moneda de búsqueda", "search_currency": "Moneda de búsqueda",
"new_template" : "Nueva plantilla", "new_template" : "Nueva plantilla",
"electrum_address_disclaimer": "Generamos nuevas direcciones cada vez que usa una, pero las direcciones anteriores siguen funcionando", "electrum_address_disclaimer": "Generamos nuevas direcciones cada vez que usa una, pero las direcciones anteriores siguen funcionando",
"wallet_name_exists": "Wallet con ese nombre ya ha existido" "wallet_name_exists": "Wallet con ese nombre ya ha existido",
"market_place": "Mercado",
"cake_pay_title": "Tarjetas de regalo Cake Pay",
"cake_pay_subtitle": "Compra tarjetas de regalo y canjéalas al instante",
"about_cake_pay": "Cake Pay le permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150 000 comerciantes en los Estados Unidos.",
"cake_pay_account_note": "Crea una cuenta para ver las tarjetas disponibles. ¡Algunas incluso están disponibles con descuento!",
"already_have_account": "¿Ya tienes una cuenta?",
"create_account": "Crear Cuenta",
"privacy_policy": "Política de privacidad",
"welcome_to_cakepay": "¡Bienvenido a Cake Pay!",
"sign_up": "Registrarse",
"forgot_password": "Olvidé mi contraseña",
"reset_password": "Restablecer contraseña",
"gift_cards": "Tarjetas de regalo",
"setup_your_debit_card": "Configura tu tarjeta de débito",
"no_id_required": "No se requiere identificación. Recargue y gaste en cualquier lugar",
"how_to_use_card": "Cómo usar esta tarjeta",
"purchase_gift_card": "Comprar tarjeta de regalo",
"verification": "Verificación",
"fill_code": "Por favor complete el código de verificación proporcionado a su correo electrónico",
"dont_get_code": "¿No obtienes el código?",
"resend_code": "Por favor reenvíalo",
"debit_card": "Tarjeta de Débito",
"cakepay_prepaid_card": "Tarjeta de Débito Prepago CakePay",
"no_id_needed": "¡No se necesita identificación!",
"frequently_asked_questions": "Preguntas frecuentes",
"debit_card_terms": "El almacenamiento y el uso de su número de tarjeta de pago (y las credenciales correspondientes a su número de tarjeta de pago) en esta billetera digital están sujetos a los Términos y condiciones del acuerdo del titular de la tarjeta aplicable con el emisor de la tarjeta de pago, en vigor desde tiempo al tiempo.",
"please_reference_document": "Consulte los documentos a continuación para obtener más información.",
"cardholder_agreement": "Acuerdo del titular de la tarjeta",
"e_sign_consent": "Consentimiento de firma electrónica",
"agree_and_continue": "Aceptar y continuar",
"email_address": "Dirección de correo electrónico",
"agree_to": "Al crear una cuenta, aceptas ",
"and": "y",
"enter_code": "Ingresar código",
"congratulations": "Felicidades!",
"you_now_have_debit_card": "Ahora tiene una tarjeta de débito",
"min_amount" : "Mínimo: ${value}",
"max_amount" : "Máx: ${value}",
"enter_amount": "Ingrese la cantidad",
"billing_address_info": "Si se le solicita una dirección de facturación, proporcione su dirección de envío",
"order_physical_card": "Pedir tarjeta física",
"add_value": "Añadir valor",
"activate": "Activar",
"get_a": "Obtener un",
"digital_and_physical_card": " tarjeta de débito prepago digital y física",
"get_card_note": " que puedes recargar con monedas digitales. ¡No se necesita información adicional!",
"signup_for_card_accept_terms": "Regístrese para obtener la tarjeta y acepte los términos.",
"add_fund_to_card": "Agregar fondos prepagos a las tarjetas (hasta ${value})",
"use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.",
"use_card_info_three": "Utilice la tarjeta digital en línea o con métodos de pago sin contacto.",
"optionally_order_card": "Opcionalmente pide una tarjeta física.",
"hide_details" : "Ocultar detalles",
"show_details": "Mostrar detalles",
"upto": "hasta ${value}",
"discount": "Ahorra ${value}%",
"gift_card_amount": "Cantidad de la tarjeta de regalo",
"bill_amount": "Importe de la factura",
"you_pay": "Tú pagas",
"tip": "Consejo:",
"personalizado": "personalizado",
"by_cake_pay": "por Cake Pay",
"expires": "Caduca",
"mm": "mm",
"yy": "YY",
"online": "En línea",
"offline": "fuera de línea",
"gift_card_number": "Número de tarjeta de regalo",
"pin_number": "Número PIN",
"total_saving": "Ahorro Total",
"last_30_days": "Últimos 30 días",
"avg_savings": "Ahorro promedio",
"view_all": "Ver todo",
"active_cards": "Tarjetas activas",
"delete_account": "Eliminar cuenta",
"cards": "Cartas",
"active": "Activo",
"redeemed": "Redimido",
"gift_card_balance_note": "Las tarjetas de regalo con saldo restante aparecerán aquí",
"gift_card_redeemed_note": "Las tarjetas de regalo que hayas canjeado aparecerán aquí",
"logout": "Cerrar sesión",
"add_tip": "Agregar sugerencia",
"percentageOf": "de ${amount}",
"is_percentage": "es",
"search_category": "Categoría de búsqueda",
"mark_as_redeemed": "Marcar como canjeado",
"more_options": "Más Opciones",
"awaiting_payment_confirmation": "Esperando confirmación de pago",
"transaction_sent_notice": "Si la pantalla no continúa después de 1 minuto, revisa un explorador de bloques y tu correo electrónico.",
"agree": "De acuerdo",
"in_store": "En la tienda",
"generating_gift_card": "Generando tarjeta de regalo",
"payment_was_received": "Su pago fue recibido.",
"proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.",
"order_id": "Identificación del pedido",
"gift_card_is_generated": "Se genera la tarjeta de regalo",
"open_gift_card": "Abrir tarjeta de regalo",
"contact_support": "Contactar con Soporte",
"gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento"
} }

View file

@ -243,7 +243,7 @@
"settings_only_transactions" : "Seulement les transactions", "settings_only_transactions" : "Seulement les transactions",
"settings_none" : "Rien", "settings_none" : "Rien",
"settings_support" : "Support", "settings_support" : "Support",
"settings_terms_and_conditions" : "Termes et conditions", "settings_terms_and_conditions" : "Termes et Conditions",
"pin_is_incorrect" : "Le code PIN est incorrect", "pin_is_incorrect" : "Le code PIN est incorrect",
@ -532,5 +532,103 @@
"search_currency": "Devise de recherche", "search_currency": "Devise de recherche",
"new_template" : "Nouveau Modèle", "new_template" : "Nouveau Modèle",
"electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner", "electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner",
"wallet_name_exists": "Le portefeuille portant ce nom existe déjà" "wallet_name_exists": "Le portefeuille portant ce nom existe déjà",
"market_place": "Place de marché",
"cake_pay_title": "Cartes cadeaux Cake Pay",
"cake_pay_subtitle": "Achetez des cartes-cadeaux et échangez-les instantanément",
"about_cake_pay": "Cake Pay vous permet d'acheter facilement des cartes-cadeaux avec des actifs virtuels, utilisables instantanément chez plus de 150 000 marchands aux États-Unis.",
"cake_pay_account_note": "Créez un compte pour voir les cartes disponibles. Certaines sont même disponibles à prix réduit !",
"already_have_account": "Vous avez déjà un compte ?",
"create_account": "Créer un compte",
"privacy_policy": "Politique de confidentialité",
"welcome_to_cakepay": "Bienvenue sur Cake Pay!",
"sign_up": "S'inscrire",
"forgot_password": "Mot de passe oublié",
"reset_password": "Réinitialiser le mot de passe",
"manage_cards": "Cartes cadeaux",
"setup_your_debit_card": "Configurer votre carte de débit",
"no_id_required": "Aucune pièce d'identité requise. Rechargez et dépensez n'importe où",
"how_to_use_card": "Comment utiliser cette carte",
"purchase_gift_card": "Acheter une carte-cadeau",
"verification": "Vérification",
"fill_code": "Veuillez remplir le code de vérification fourni sur votre e-mail",
"dont_get_code": "Vous ne recevez pas le code ?",
"resend_code": "Veuillez le renvoyer",
"debit_card": "Carte de débit",
"cakepay_prepaid_card": "Carte de débit prépayée CakePay",
"no_id_needed": "Aucune pièce d'identité nécessaire !",
"frequently_asked_questions": "Foire aux questions",
"debit_card_terms": "Le stockage et l'utilisation de votre numéro de carte de paiement (et des informations d'identification correspondant à votre numéro de carte de paiement) dans ce portefeuille numérique sont soumis aux conditions générales de l'accord du titulaire de carte applicable avec l'émetteur de la carte de paiement, en vigueur à partir de de temps en temps.",
"please_reference_document": "Veuillez vous référer aux documents ci-dessous pour plus d'informations.",
"cardholder_agreement": "Contrat de titulaire de carte",
"e_sign_consent": "Consentement de signature électronique",
"agree_and_continue": "Accepter et continuer",
"email_address": "Adresse e-mail",
"agree_to": "En créant un compte, vous acceptez les ",
"and": "et",
"enter_code": "Entrez le code",
"congratulations": "Félicitations !",
"you_now_have_debit_card": "Vous avez maintenant une carte de débit",
"min_amount" : "Min : ${value}",
"max_amount" : "Max : ${value}",
"enter_amount": "Entrez le montant",
"billing_address_info": "Si une adresse de facturation vous est demandée, indiquez votre adresse de livraison",
"order_physical_card": "Commander une carte physique",
"add_value": "Ajouter une valeur",
"activate": "Activer",
"get_a": "Obtenir un ",
"digital_and_physical_card": "carte de débit prépayée numérique et physique",
"get_card_note": " que vous pouvez recharger avec des devises numériques. Aucune information supplémentaire n'est nécessaire !",
"signup_for_card_accept_terms": "Inscrivez-vous pour la carte et acceptez les conditions.",
"add_fund_to_card": "Ajouter des fonds prépayés aux cartes (jusqu'à ${value})",
"use_card_info_two": "Les fonds sont convertis en USD lorsqu'ils sont détenus sur le compte prépayé, et non en devises numériques.",
"use_card_info_three": "Utilisez la carte numérique en ligne ou avec des méthodes de paiement sans contact.",
"optionally_order_card": "Commander éventuellement une carte physique.",
"hide_details" : "Masquer les détails",
"show_details" : "Afficher les détails",
"upto": "jusqu'à ${value}",
"discount": "Économisez ${value}%",
"gift_card_amount": "Montant de la carte-cadeau",
"bill_amount": "Montant de la facture",
"you_pay": "Vous payez",
"tip": "Astuce :",
"custom": "personnalisé",
"by_cake_pay": "par Cake Pay",
"expire": "Expire",
"mm": "MM",
"yy": "AA",
"online": "En ligne",
"offline": "Hors ligne",
"gift_card_number": "Numéro de carte cadeau",
"pin_number": "Numéro PIN",
"total_saving": "Économies totales",
"last_30_days": "30 derniers jours",
"avg_savings": "Économies moy.",
"view_all": "Voir tout",
"active_cards": "Cartes actives",
"delete_account": "Supprimer le compte",
"cards": "Cartes",
"active": "Actif",
"redeemed": "racheté",
"gift_card_balance_note": "Les cartes-cadeaux avec un solde restant apparaîtront ici",
"gift_card_redeemed_note": "Les cartes-cadeaux que vous avez utilisées apparaîtront ici",
"logout": "Déconnexion",
"add_tip": "Ajouter une astuce",
"percentageOf": "sur ${amount}",
"is_percentage": "est",
"search_category": "Catégorie de recherche",
"mark_as_redeemed": "Marquer comme échangé",
"more_options": "Plus d'options",
"awaiting_payment_confirmation": "En attente de confirmation de paiement",
"transaction_sent_notice": "Si l'écran ne continue pas après 1 minute, vérifiez un explorateur de blocs et votre e-mail.",
"agree": "d'accord",
"in_store": "En magasin",
"generating_gift_card": "Génération d'une carte-cadeau",
"payment_was_received": "Votre paiement a été reçu.",
"proceed_after_one_minute": "Si l'écran ne s'affiche pas après 1 minute, vérifiez vos e-mails.",
"order_id": "Numéro de commande",
"gift_card_is_generated": "La carte-cadeau est générée",
"open_gift_card": "Ouvrir la carte-cadeau",
"contact_support": "Contacter l'assistance",
"gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment"
} }

View file

@ -534,5 +534,103 @@
"search_currency": "मुद्रा खोजें", "search_currency": "मुद्रा खोजें",
"new_template" : "नया टेम्पलेट", "new_template" : "नया टेम्पलेट",
"electrum_address_disclaimer": "हर बार जब आप एक का उपयोग करते हैं तो हम नए पते उत्पन्न करते हैं, लेकिन पिछले पते काम करना जारी रखते हैं", "electrum_address_disclaimer": "हर बार जब आप एक का उपयोग करते हैं तो हम नए पते उत्पन्न करते हैं, लेकिन पिछले पते काम करना जारी रखते हैं",
"wallet_name_exists": "उस नाम वाला वॉलेट पहले से मौजूद है" "wallet_name_exists": "उस नाम वाला वॉलेट पहले से मौजूद है",
"market_place": "मार्केटप्लेस",
"cake_pay_title": "केक पे गिफ्ट कार्ड्स",
"cake_pay_subtitle": "उपहार कार्ड खरीदें और तुरंत रिडीम करें",
"about_cake_pay": "केक पे आपको वर्चुअल संपत्ति के साथ आसानी से उपहार कार्ड खरीदने की अनुमति देता है, जिसे संयुक्त राज्य में 150,000 से अधिक व्यापारियों पर तुरंत खर्च किया जा सकता है।",
"cake_pay_account_note": "उपलब्ध कार्ड देखने के लिए एक खाता बनाएं। कुछ छूट पर भी उपलब्ध हैं!",
"ready_have_account": "क्या आपके पास पहले से ही एक खाता है?",
"create_account": "खाता बनाएं",
"privacy_policy": "गोपनीयता नीति",
"welcome_to_cakepay": "केकपे में आपका स्वागत है!",
"sign_up": "साइन अप करें",
"forgot_password": "पासवर्ड भूल गए",
"reset_password": "पासवर्ड रीसेट करें",
"gift_cards": "उपहार कार्ड",
"setup_your_debit_card": "अपना डेबिट कार्ड सेट करें",
"no_id_required": "कोई आईडी आवश्यक नहीं है। टॉप अप करें और कहीं भी खर्च करें",
"how_to_use_card": "इस कार्ड का उपयोग कैसे करें",
"purchase_gift_card": "गिफ्ट कार्ड खरीदें",
"verification": "सत्यापन",
"fill_code": "कृपया अपने ईमेल पर प्रदान किया गया सत्यापन कोड भरें",
"dont_get_code": "कोड नहीं मिला?",
"resend_code": "कृपया इसे फिर से भेजें",
"debit_card": "डेबिट कार्ड",
"cakepay_prepaid_card": "केकपे प्रीपेड डेबिट कार्ड",
"no_id_needed": "कोई आईडी नहीं चाहिए!",
"frequently_asked_questions": "अक्सर पूछे जाने वाले प्रश्न",
"debit_card_terms": "इस डिजिटल वॉलेट में आपके भुगतान कार्ड नंबर (और आपके भुगतान कार्ड नंबर से संबंधित क्रेडेंशियल) का भंडारण और उपयोग भुगतान कार्ड जारीकर्ता के साथ लागू कार्डधारक समझौते के नियमों और शर्तों के अधीन है, जैसा कि प्रभावी है समय - समय पर।",
"please_reference_document": "कृपया अधिक जानकारी के लिए नीचे दिए गए दस्तावेज़ देखें।",
"cardholder_agreement": "कार्डधारक अनुबंध",
"e_sign_consent": "ई-साइन सहमति",
"agree_and_continue": "सहमत और जारी रखें",
"email_address": "ईमेल पता",
"agree_to": "खाता बनाकर आप इससे सहमत होते हैं ",
"and": "और",
"enter_code": "कोड दर्ज करें",
"congratulations":"बधाई!",
"you_now_have_debit_card": "अब आपके पास डेबिट कार्ड है",
"min_amount" : "न्यूनतम: ${value}",
"max_amount" : "अधिकतम: ${value}",
"enter_amount": "राशि दर्ज करें",
"billing_address_info": "यदि बिलिंग पता मांगा जाए, तो अपना शिपिंग पता प्रदान करें",
"order_physical_card": "फिजिकल कार्ड ऑर्डर करें",
"add_value": "मूल्य जोड़ें",
"activate": "सक्रिय करें",
"get_a": "एक प्राप्त करें",
"digital_and_physical_card": "डिजिटल और भौतिक प्रीपेड डेबिट कार्ड",
"get_card_note": " कि आप डिजिटल मुद्राओं के साथ पुनः लोड कर सकते हैं। कोई अतिरिक्त जानकारी की आवश्यकता नहीं है!",
"signup_for_card_accept_terms": "कार्ड के लिए साइन अप करें और शर्तें स्वीकार करें।",
"add_fund_to_card": "कार्ड में प्रीपेड धनराशि जोड़ें (${value} तक)",
"use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।",
"use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।",
"optionally_order_card": "वैकल्पिक रूप से एक भौतिक कार्ड ऑर्डर करें।",
"hide_details": "विवरण छुपाएं",
"show_details": "विवरण दिखाएं",
"upto": "${value} तक",
"discount": "${value}% बचाएं",
"gift_card_amount": "गिफ्ट कार्ड राशि",
"bill_amount": "बिल राशि",
"you_pay": "आप भुगतान करते हैं",
"tip": "टिप:",
"custom": "कस्टम",
"by_cake_pay": "केकपे द्वारा",
"expires": "समाप्त हो जाता है",
"mm": "एमएम",
"yy": "वाईवाई",
"online": "ऑनलाइन",
"offline": "ऑफ़लाइन",
"gift_card_number": "गिफ्ट कार्ड नंबर",
"pin_number": "पिन नंबर",
"total_saving": "कुल बचत",
"last_30_days": "पिछले 30 दिन",
"avg_savings": "औसत बचत",
"view_all": "सभी देखें",
"active_cards": "सक्रिय कार्ड",
"delete_account": "खाता हटाएं",
"cards": "कार्ड",
"active": "सक्रिय",
"redeemed": "रिडीम किया गया",
"gift_card_balance_note": "गिफ्ट कार्ड शेष राशि के साथ यहां दिखाई देंगे",
"gift_card_redeemed_note": "आपके द्वारा भुनाए गए उपहार कार्ड यहां दिखाई देंगे",
"logout": "लॉगआउट",
"add_tip": "टिप जोड़ें",
"percentageOf": "${amount} का",
"is_percentage": "है",
"search_category": "खोज श्रेणी",
"mark_as_redeemed": "रिडीम किए गए के रूप में चिह्नित करें",
"more_options": "और विकल्प",
"awaiting_payment_confirmation": "भुगतान की पुष्टि की प्रतीक्षा में",
"transaction_sent_notice": "अगर 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो ब्लॉक एक्सप्लोरर और अपना ईमेल देखें।",
"agree": "सहमत",
"in_store": "स्टोर में",
"generating_gift_card": "गिफ्ट कार्ड जनरेट कर रहा है",
"Payment_was_received": "आपका भुगतान प्राप्त हो गया था।",
"proceed_after_one_minute": "यदि 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो अपना ईमेल देखें।",
"order_id": "ऑर्डर आईडी",
"gift_card_is_generated": "गिफ्ट कार्ड जनरेट हुआ",
"open_gift_card": "गिफ्ट कार्ड खोलें",
"contact_support": "सहायता से संपर्क करें",
"gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं"
} }

Some files were not shown because too many files have changed in this diff Show more