diff --git a/assets/images/airplane.png b/assets/images/airplane.png new file mode 100644 index 000000000..6be73be94 Binary files /dev/null and b/assets/images/airplane.png differ diff --git a/assets/images/badge_discount.png b/assets/images/badge_discount.png new file mode 100644 index 000000000..64c8789c5 Binary files /dev/null and b/assets/images/badge_discount.png differ diff --git a/assets/images/card.png b/assets/images/card.png new file mode 100644 index 000000000..58935bdac Binary files /dev/null and b/assets/images/card.png differ diff --git a/assets/images/category.png b/assets/images/category.png new file mode 100644 index 000000000..75abd8a74 Binary files /dev/null and b/assets/images/category.png differ diff --git a/assets/images/copy.png b/assets/images/copy.png new file mode 100644 index 000000000..41d18c74a Binary files /dev/null and b/assets/images/copy.png differ diff --git a/assets/images/delivery.png b/assets/images/delivery.png new file mode 100644 index 000000000..1f67e3f8f Binary files /dev/null and b/assets/images/delivery.png differ diff --git a/assets/images/filter.png b/assets/images/filter.png new file mode 100644 index 000000000..dc47944c0 Binary files /dev/null and b/assets/images/filter.png differ diff --git a/assets/images/food.png b/assets/images/food.png new file mode 100644 index 000000000..64d7a76a7 Binary files /dev/null and b/assets/images/food.png differ diff --git a/assets/images/gaming.png b/assets/images/gaming.png new file mode 100644 index 000000000..63be2322f Binary files /dev/null and b/assets/images/gaming.png differ diff --git a/assets/images/global.png b/assets/images/global.png new file mode 100644 index 000000000..1ab559b6d Binary files /dev/null and b/assets/images/global.png differ diff --git a/assets/images/mastercard.png b/assets/images/mastercard.png new file mode 100644 index 000000000..2a80f52fe Binary files /dev/null and b/assets/images/mastercard.png differ diff --git a/assets/images/mini_search_icon.png b/assets/images/mini_search_icon.png new file mode 100644 index 000000000..d9d71a4d7 Binary files /dev/null and b/assets/images/mini_search_icon.png differ diff --git a/assets/images/profile.png b/assets/images/profile.png new file mode 100644 index 000000000..d7dfe2508 Binary files /dev/null and b/assets/images/profile.png differ diff --git a/assets/images/red_badge_discount.png b/assets/images/red_badge_discount.png new file mode 100644 index 000000000..4f5dc56a2 Binary files /dev/null and b/assets/images/red_badge_discount.png differ diff --git a/assets/images/tshirt.png b/assets/images/tshirt.png new file mode 100644 index 000000000..cb20f60a4 Binary files /dev/null and b/assets/images/tshirt.png differ diff --git a/assets/images/wifi.png b/assets/images/wifi.png new file mode 100644 index 000000000..7834ef1c3 Binary files /dev/null and b/assets/images/wifi.png differ diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index bf32f6186..7df93400a 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -2,8 +2,9 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; class BitcoinTransactionCredentials { - BitcoinTransactionCredentials(this.outputs, this.priority); + BitcoinTransactionCredentials(this.outputs, {this.priority, this.feeRate}); final List outputs; - BitcoinTransactionPriority priority; + final BitcoinTransactionPriority priority; + final int feeRate; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index f6d3f30f8..a4db681f3 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -208,8 +208,14 @@ abstract class ElectrumWalletBase extends WalletBase minAmount) { @@ -346,45 +364,57 @@ abstract class ElectrumWalletBase extends WalletBase feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); + int feeAmountWithFeeRate(int feeRate, int inputsCount, + int outputsCount) => + feeRate * estimatedTransactionSize(inputsCount, outputsCount); + @override int calculateEstimatedFee(TransactionPriority priority, int amount, {int outputsCount}) { if (priority is BitcoinTransactionPriority) { - 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 feeAmountForPriority( - priority, inputsCount, _outputsCount); + return calculateEstimatedFeeWithFeeRate( + feeRate(priority), + amount, + outputsCount: outputsCount); } 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 Future save() async { final path = await makePath(); @@ -525,10 +555,6 @@ abstract class ElectrumWalletBase extends WalletBase{}; final normalizedHistories = >[]; walletAddresses.addresses.forEach((addressRecord) { - if (addressRecord.isHidden) { - return; - } - final sh = scriptHash(addressRecord.address, networkType: networkType); addressHashes[sh] = addressRecord; }); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 1793db354..b9f754c72 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -24,6 +24,9 @@ class PendingBitcoinTransaction with PendingTransaction { @override String get id => _tx.getId(); + @override + String get hex => _tx.toHex(); + @override String get amountFormatted => bitcoinAmountToString(amount: amount); diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index c7f9b77d5..cc5686fc9 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -2,6 +2,7 @@ mixin PendingTransaction { String get id; String get amountFormatted; String get feeFormatted; + String get hex; Future commit(); } \ No newline at end of file diff --git a/cw_haven/lib/pending_haven_transaction.dart b/cw_haven/lib/pending_haven_transaction.dart index 7a8c6acc5..d56b5096c 100644 --- a/cw_haven/lib/pending_haven_transaction.dart +++ b/cw_haven/lib/pending_haven_transaction.dart @@ -22,6 +22,9 @@ class PendingHavenTransaction with PendingTransaction { @override String get id => pendingTransactionDescription.hash; + @override + String get hex => ''; + @override String get amountFormatted => AmountConverter.amountIntToString( cryptoCurrency, pendingTransactionDescription.amount); diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index 37e6b72b7..f9b6c4890 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -169,13 +169,6 @@ packages: relative: true source: path version: "0.0.1" - cw_monero: - dependency: "direct main" - description: - path: "../cw_monero" - relative: true - source: path - version: "0.0.1" dart_style: dependency: transitive description: diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 4d3f5f56b..6bb251dcf 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -166,6 +166,8 @@ extern "C" uint64_t amount; uint64_t fee; char *hash; + char *hex; + char *txKey; Monero::PendingTransaction *transaction; PendingTransactionRaw(Monero::PendingTransaction *_transaction) @@ -174,6 +176,8 @@ extern "C" amount = _transaction->amount(); fee = _transaction->fee(); 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) { - Monero::WalletManagerFactory::setLogLevel(4); - Monero::NetworkType _networkType = static_cast(networkType); Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); diff --git a/cw_monero/lib/api/structs/pending_transaction.dart b/cw_monero/lib/api/structs/pending_transaction.dart index b492f28a0..edbd2d0ff 100644 --- a/cw_monero/lib/api/structs/pending_transaction.dart +++ b/cw_monero/lib/api/structs/pending_transaction.dart @@ -10,14 +10,30 @@ class PendingTransactionRaw extends Struct { Pointer hash; + Pointer hex; + + Pointer txKey; + String getHash() => Utf8.fromUtf8(hash); + + String getHex() => Utf8.fromUtf8(hex); + + String getKey() => Utf8.fromUtf8(txKey); } 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 fee; final String hash; + final String hex; + final String txKey; final int pointerAddress; } \ No newline at end of file diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index d693e16b9..9546a93d3 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -104,6 +104,8 @@ PendingTransactionDescription createTransactionSync( amount: pendingTransactionRawPointer.ref.amount, fee: pendingTransactionRawPointer.ref.fee, hash: pendingTransactionRawPointer.ref.getHash(), + hex: pendingTransactionRawPointer.ref.getHex(), + txKey: pendingTransactionRawPointer.ref.getKey(), pointerAddress: pendingTransactionRawPointer.address); } @@ -157,6 +159,8 @@ PendingTransactionDescription createTransactionMultDestSync( amount: pendingTransactionRawPointer.ref.amount, fee: pendingTransactionRawPointer.ref.fee, hash: pendingTransactionRawPointer.ref.getHash(), + hex: pendingTransactionRawPointer.ref.getHex(), + txKey: pendingTransactionRawPointer.ref.getKey(), pointerAddress: pendingTransactionRawPointer.address); } diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index d927dd0d7..d32bab2ce 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -22,6 +22,11 @@ class PendingMoneroTransaction with PendingTransaction { @override String get id => pendingTransactionDescription.hash; + @override + String get hex => pendingTransactionDescription.hex; + + String get txKey => pendingTransactionDescription.txKey; + @override String get amountFormatted => AmountConverter.amountIntToString( CryptoCurrency.xmr, pendingTransactionDescription.amount); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d90ef8ca3..d527812b2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,6 +57,8 @@ PODS: - Flutter - cw_shared_external/Sodium (0.0.1): - Flutter + - device_display_brightness (0.0.1): + - Flutter - devicelocale (0.0.1): - Flutter - DKImagePickerController/Core (4.3.2): @@ -134,6 +136,7 @@ DEPENDENCIES: - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_monero (from `.symlinks/plugins/cw_monero/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`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -174,6 +177,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cw_monero/ios" cw_shared_external: :path: ".symlinks/plugins/cw_shared_external/ios" + device_display_brightness: + :path: ".symlinks/plugins/device_display_brightness/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" esys_flutter_share: @@ -211,6 +216,7 @@ SPEC CHECKSUMS: cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_monero: 88c5e7aa596c6848330750f5f8bcf05fb9c66375 cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 + device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 diff --git a/lib/anypay/any_pay_chain.dart b/lib/anypay/any_pay_chain.dart new file mode 100644 index 000000000..3f6d9b135 --- /dev/null +++ b/lib/anypay/any_pay_chain.dart @@ -0,0 +1,5 @@ +class AnyPayChain { + static const xmr = 'XMR'; + static const btc = 'BTC'; + static const ltc = 'LTC'; +} \ No newline at end of file diff --git a/lib/anypay/any_pay_payment.dart b/lib/anypay/any_pay_payment.dart new file mode 100644 index 000000000..295ee2dc4 --- /dev/null +++ b/lib/anypay/any_pay_payment.dart @@ -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 obj) { + final instructions = (obj['instructions'] as List) + .map((dynamic instruction) => AnyPayPaymentInstruction.fromMap(instruction as Map)) + .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 instructions; + + String get totalAmount { + final total = instructions + .fold(0, (int acc, instruction) => acc + instruction.outputs + .fold(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 get outAddresses { + return instructions + .map((instuction) => instuction.outputs.map((out) => out.address)) + .expand((e) => e) + .toList(); + } +} \ No newline at end of file diff --git a/lib/anypay/any_pay_payment_committed_info.dart b/lib/anypay/any_pay_payment_committed_info.dart new file mode 100644 index 000000000..126b3d92e --- /dev/null +++ b/lib/anypay/any_pay_payment_committed_info.dart @@ -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 transactions; + final String memo; +} \ No newline at end of file diff --git a/lib/anypay/any_pay_payment_instruction.dart b/lib/anypay/any_pay_payment_instruction.dart new file mode 100644 index 000000000..41d2ee82d --- /dev/null +++ b/lib/anypay/any_pay_payment_instruction.dart @@ -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 obj) { + final outputs = (obj['outputs'] as List) + .map((dynamic out) => + AnyPayPaymentInstructionOutput.fromMap(out as Map)) + .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 outputs; +} \ No newline at end of file diff --git a/lib/anypay/any_pay_payment_instruction_output.dart b/lib/anypay/any_pay_payment_instruction_output.dart new file mode 100644 index 000000000..7fabea966 --- /dev/null +++ b/lib/anypay/any_pay_payment_instruction_output.dart @@ -0,0 +1,10 @@ +class AnyPayPaymentInstructionOutput { + const AnyPayPaymentInstructionOutput(this.address, this.amount); + + factory AnyPayPaymentInstructionOutput.fromMap(Map obj) { + return AnyPayPaymentInstructionOutput(obj['address'] as String, obj['amount'] as int); + } + + final String address; + final int amount; +} \ No newline at end of file diff --git a/lib/anypay/any_pay_trasnaction.dart b/lib/anypay/any_pay_trasnaction.dart new file mode 100644 index 000000000..29f8a0152 --- /dev/null +++ b/lib/anypay/any_pay_trasnaction.dart @@ -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; +} \ No newline at end of file diff --git a/lib/anypay/anypay_api.dart b/lib/anypay/anypay_api.dart new file mode 100644 index 000000000..c0727bc29 --- /dev/null +++ b/lib/anypay/anypay_api.dart @@ -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 paymentRequest(String uri) async { + final fragments = uri.split(':?r='); + final scheme = fragments.first; + final url = fragments[1]; + final headers = { + 'Content-Type': contentTypePaymentRequest, + 'X-Paypro-Version': xPayproVersion, + 'Accept': '*/*',}; + final body = { + '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; + return AnyPayPayment.fromMap(decodedBody); + } + + Future payment( + String uri, + {@required String chain, + @required String currency, + @required List transactions}) async { + final headers = { + 'Content-Type': contentTypePayment, + 'X-Paypro-Version': xPayproVersion, + 'Accept': '*/*',}; + final body = { + '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; + throw Exception(decodedBody['message'] as String); + } + + if (response.statusCode != 200) { + throw Exception('Unexpected response'); + } + + final decodedBody = json.decode(response.body) as Map; + return AnyPayPaymentCommittedInfo( + uri: uri, + currency: currency, + chain: chain, + transactions: transactions, + memo: decodedBody['memo'] as String); + } +} \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index e73861594..46ef89172 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -55,7 +55,7 @@ class CWBitcoin extends Bitcoin { } @override - Object createBitcoinTransactionCredentials(List outputs, TransactionPriority priority) + Object createBitcoinTransactionCredentials(List outputs, {TransactionPriority priority, int feeRate}) => BitcoinTransactionCredentials( outputs.map((out) => OutputInfo( fiatAmount: out.fiatAmount, @@ -67,7 +67,15 @@ class CWBitcoin extends Bitcoin { isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), - priority as BitcoinTransactionPriority); + priority: priority != null ? priority as BitcoinTransactionPriority : null, + feeRate: feeRate); + + @override + Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority priority, int feeRate}) + => BitcoinTransactionCredentials( + outputs, + priority: priority != null ? priority as BitcoinTransactionPriority : null, + feeRate: feeRate); @override List getAddresses(Object wallet) { diff --git a/lib/core/email_validator.dart b/lib/core/email_validator.dart new file mode 100644 index 000000000..23910db60 --- /dev/null +++ b/lib/core/email_validator.dart @@ -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: + '^[^@]+@[^@]+\.[^@]+', + ); +} diff --git a/lib/di.dart b/lib/di.dart index 2270f1863..bd911335d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,10 +1,27 @@ import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/wake_lock.dart'; +import 'package:cake_wallet/ionia/ionia_anypay.dart'; +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_filter_view_model.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:cake_wallet/ionia/ionia_api.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; +import 'package:cake_wallet/src/screens/ionia/ionia.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -100,6 +117,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; @@ -123,6 +141,12 @@ import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; +import 'package:cake_wallet/anypay/anypay_api.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; @@ -261,7 +285,6 @@ Future setup( fiatConvertationStore: getIt.get())); getIt.registerFactory(() => DashboardViewModel( - balanceViewModel: getIt.get(), appStore: getIt.get(), tradesStore: getIt.get(), @@ -560,10 +583,6 @@ Future setup( getIt.registerFactory(() => BackupPage(getIt.get())); - getIt.registerFactory(() => EditBackupPasswordViewModel( - getIt.get(), getIt.get()) - ..init()); - getIt.registerFactory( () => EditBackupPasswordPage(getIt.get())); @@ -596,10 +615,7 @@ Future setup( final url = args.first as String; final buyViewModel = args[1] as BuyViewModel; - return BuyWebViewPage( - buyViewModel: buyViewModel, - ordersStore: getIt.get(), - url: url); + return BuyWebViewPage(buyViewModel: buyViewModel, ordersStore: getIt.get(), url: url); }); getIt.registerFactoryParam((order, _) { @@ -649,6 +665,102 @@ Future setup( getIt.registerFactoryParam( (String qrData, bool isLight) => FullscreenQRPage(qrData: qrData, isLight: isLight,)); + + getIt.registerFactory(() => IoniaApi()); + + getIt.registerFactory(() => AnyPayApi()); + + getIt.registerFactory( + () => IoniaService(getIt.get(), getIt.get())); + + getIt.registerFactory( + () => IoniaAnyPay( + getIt.get(), + getIt.get(), + getIt.get().wallet)); + + getIt.registerFactory(() => IoniaFilterViewModel()); + + getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get())); + + getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get())); + + getIt.registerFactoryParam((double amount, merchant) { + return IoniaMerchPurchaseViewModel( + ioniaAnyPayService: getIt.get(), + amount: amount, + ioniaMerchant: merchant, + ); + }); + + getIt.registerFactoryParam((IoniaMerchant merchant, _) { + return IoniaBuyCardViewModel(ioniaMerchant: merchant); + }); + + getIt.registerFactory(() => IoniaAccountViewModel(ioniaService: getIt.get())); + + getIt.registerFactory(() => IoniaCreateAccountPage(getIt.get())); + + getIt.registerFactory(() => IoniaLoginPage(getIt.get())); + + getIt.registerFactoryParam((List args, _) { + final email = args.first as String; + final isSignIn = args[1] as bool; + + return IoniaVerifyIoniaOtp(getIt.get(), email, isSignIn); + }); + + getIt.registerFactory(() => IoniaWelcomePage(getIt.get())); + + getIt.registerFactoryParam((List args, _) { + final merchant = args.first as IoniaMerchant; + + return IoniaBuyGiftCardPage(getIt.get(param1: merchant)); + }); + + getIt.registerFactoryParam((List args, _) { + final amount = args.first as double; + final merchant = args.last as IoniaMerchant; + return IoniaBuyGiftCardDetailPage(getIt.get(param1: amount, param2: merchant)); + }); + + getIt.registerFactoryParam((IoniaGiftCard giftCard, _) { + return IoniaGiftCardDetailsViewModel( + ioniaService: getIt.get(), + giftCard: giftCard); + }); + + getIt.registerFactoryParam((IoniaGiftCard giftCard, _) { + return IoniaGiftCardDetailPage(getIt.get(param1: giftCard)); + }); + + getIt.registerFactoryParam((List args, _) { + final amount = args.first as String; + final merchant = args.last as IoniaMerchant; + + return IoniaCustomTipPage(getIt.get(param1: amount, param2: merchant)); + }); + + getIt.registerFactory(() => IoniaManageCardsPage(getIt.get())); + + getIt.registerFactory(() => IoniaDebitCardPage(getIt.get())); + + getIt.registerFactory(() => IoniaActivateDebitCardPage(getIt.get())); + + getIt.registerFactory(() => IoniaAccountPage(getIt.get())); + + getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get())); + + getIt.registerFactoryParam( + (IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo) + => IoniaPaymentStatusViewModel( + getIt.get(), + paymentInfo: paymentInfo, + committedInfo: committedInfo)); + + getIt.registerFactoryParam( + (IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo) + => IoniaPaymentStatusPage(getIt.get(param1: paymentInfo, param2: committedInfo))); _isSetupFinished = true; } diff --git a/lib/ionia/ionia_any_pay_payment_info.dart b/lib/ionia/ionia_any_pay_payment_info.dart new file mode 100644 index 000000000..6146a46fe --- /dev/null +++ b/lib/ionia/ionia_any_pay_payment_info.dart @@ -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; +} diff --git a/lib/ionia/ionia_anypay.dart b/lib/ionia/ionia_anypay.dart new file mode 100644 index 000000000..b9b53498a --- /dev/null +++ b/lib/ionia/ionia_anypay.dart @@ -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 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 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); + } +} \ No newline at end of file diff --git a/lib/ionia/ionia_api.dart b/lib/ionia/ionia_api.dart new file mode 100644 index 000000000..6784e2ba1 --- /dev/null +++ b/lib/ionia/ionia_api.dart @@ -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 createUser(String email, {@required String clientId}) async { + final headers = {'clientId': clientId}; + final query = {'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; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(data['ErrorMessage'] as String); + } + + return data['username'] as String; + } + + // Verify email + + Future verifyEmail({ + @required String username, + @required String email, + @required String code, + @required String clientId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'EmailAddress': email}; + final query = {'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; + final data = bodyJson['Data'] as Map; + 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 signIn(String email, {@required String clientId}) async { + final headers = {'clientId': clientId}; + final query = {'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; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(data['ErrorMessage'] as String); + } + + return data['username'] as String; + } + + // Get virtual card + + Future getCards({ + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + '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; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(data['message'] as String); + } + + final virtualCard = data['VirtualCard'] as Map; + return IoniaVirtualCard.fromMap(virtualCard); + } + + // Create virtual card + + Future createCard({ + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + '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; + final data = bodyJson['Data'] as Map; + final isSuccessful = bodyJson['Successful'] as bool; + + if (!isSuccessful) { + throw Exception(data['message'] as String); + } + + return IoniaVirtualCard.fromMap(data); + } + + // Get Merchants + + Future> getMerchants({ + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + '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; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + return []; + } + + final data = decodedBody['Data'] as List; + return data.map((dynamic e) { + try { + final element = e as Map; + return IoniaMerchant.fromJsonMap(element); + } catch(_) { + return null; + } + }).where((e) => e != null) + .toList(); + } + + // Get Merchants By Filter + + Future> getMerchantsByFilter({ + @required String username, + @required String password, + @required String clientId, + String search, + List categories, + int merchantFilterType = 0}) async { + // MerchantFilterType: {All = 0, Nearby = 1, Popular = 2, Online = 3, MyFaves = 4, Search = 5} + + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = {'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; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + return []; + } + + final data = decodedBody['Data'] as List; + return data.map((dynamic e) { + try { + final element = e['Merchant'] as Map; + return IoniaMerchant.fromJsonMap(element); + } catch(_) { + return null; + } + }).where((e) => e != null) + .toList(); + } + + // Purchase Gift Card + + Future purchaseGiftCard({ + @required String merchId, + @required double amount, + @required String currency, + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = { + '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; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + throw Exception(decodedBody['ErrorMessage'] as String); + } + + final data = decodedBody['Data'] as Map; + return IoniaOrder.fromMap(data); + } + + // Get Current User Gift Card Summaries + + Future> getCurrentUserGiftCardSummaries({ + @required String username, + @required String password, + @required String clientId}) async { + final headers = { + '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; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + return []; + } + + final data = decodedBody['Data'] as List; + return data.map((dynamic e) { + try { + final element = e as Map; + return IoniaGiftCard.fromJsonMap(element); + } catch(e) { + return null; + } + }).where((e) => e != null) + .toList(); + } + + // Charge Gift Card + + Future chargeGiftCard({ + @required String username, + @required String password, + @required String clientId, + @required int giftCardId, + @required double amount}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = { + '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; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + final data = decodedBody['Data'] as Map; + 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 getGiftCard({ + @required String username, + @required String password, + @required String clientId, + @required int id}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = {'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; + 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; + return IoniaGiftCard.fromJsonMap(data); + } + + // Payment Status + + Future getPaymentStatus({ + @required String username, + @required String password, + @required String clientId, + @required String orderId, + @required String paymentId}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = { + '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; + 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; + return data['gift_card_id'] as int; + } +} \ No newline at end of file diff --git a/lib/ionia/ionia_category.dart b/lib/ionia/ionia_category.dart new file mode 100644 index 000000000..4b94d70cf --- /dev/null +++ b/lib/ionia/ionia_category.dart @@ -0,0 +1,18 @@ +class IoniaCategory { + const IoniaCategory({this.index, this.title, this.ids, this.iconPath}); + + static const allCategories = [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 ids; + final String iconPath; +} diff --git a/lib/ionia/ionia_create_state.dart b/lib/ionia/ionia_create_state.dart new file mode 100644 index 000000000..b0277be45 --- /dev/null +++ b/lib/ionia/ionia_create_state.dart @@ -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; +} diff --git a/lib/ionia/ionia_gift_card.dart b/lib/ionia/ionia_gift_card.dart new file mode 100644 index 000000000..ebe8084f8 --- /dev/null +++ b/lib/ionia/ionia_gift_card.dart @@ -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 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 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; +} \ No newline at end of file diff --git a/lib/ionia/ionia_gift_card_instruction.dart b/lib/ionia/ionia_gift_card_instruction.dart new file mode 100644 index 000000000..da1fdae1b --- /dev/null +++ b/lib/ionia/ionia_gift_card_instruction.dart @@ -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 element) { + return IoniaGiftCardInstruction( + toBeginningOfSentenceCase(element['title'] as String ?? ''), + element['description'] as String); + } + + static List parseListOfInstructions(String instructionsJSON) { + List instructions = []; + + if (instructionsJSON.isNotEmpty) { + final decodedInstructions = json.decode(instructionsJSON) as List; + instructions = decodedInstructions + .map((dynamic e) =>IoniaGiftCardInstruction.fromJsonMap(e as Map)) + .toList(); + } + + return instructions; + } + + final String header; + final String body; +} \ No newline at end of file diff --git a/lib/ionia/ionia_merchant.dart b/lib/ionia/ionia_merchant.dart new file mode 100644 index 000000000..16df62da7 --- /dev/null +++ b/lib/ionia/ionia_merchant.dart @@ -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 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 instructions; + final String alderSku; + final String ngcSku; + final String acceptedCurrency; + final String deepLink; + final bool isPayLater; + final double savingsPercentage; + + double get discount => savingsPercentage; + +} diff --git a/lib/ionia/ionia_order.dart b/lib/ionia/ionia_order.dart new file mode 100644 index 000000000..f9c35ea70 --- /dev/null +++ b/lib/ionia/ionia_order.dart @@ -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 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; +} \ No newline at end of file diff --git a/lib/ionia/ionia_service.dart b/lib/ionia/ionia_service.dart new file mode 100644 index 000000000..d1a2052f0 --- /dev/null +++ b/lib/ionia/ionia_service.dart @@ -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 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 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 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 getUserEmail() async { + return secureStorage.read(key: ioniaEmailStorageKey); + } + + // Check is user logined + + Future isLogined() async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey) ?? ''; + final password = await secureStorage.read(key: ioniaPasswordStorageKey) ?? ''; + return username.isNotEmpty && password.isNotEmpty; + } + + // Logout + + Future logout() async { + await secureStorage.delete(key: ioniaUsernameStorageKey); + await secureStorage.delete(key: ioniaPasswordStorageKey); + } + + // Create virtual card + + Future 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 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> 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> getMerchantsByFilter({ + String search, + List 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 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> 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 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 redeem(IoniaGiftCard giftCard) async { + await chargeGiftCard(giftCardId: giftCard.id, amount: giftCard.remainingAmount); + } + + // Get Gift Card + + Future 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 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); + } +} \ No newline at end of file diff --git a/lib/ionia/ionia_tip.dart b/lib/ionia/ionia_tip.dart new file mode 100644 index 000000000..340c6226f --- /dev/null +++ b/lib/ionia/ionia_tip.dart @@ -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) + ]; +} diff --git a/lib/ionia/ionia_token_data.dart b/lib/ionia/ionia_token_data.dart new file mode 100644 index 000000000..1baa4c63d --- /dev/null +++ b/lib/ionia/ionia_token_data.dart @@ -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; + 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({ + 'access_token': accessToken, + 'token_type': tokenType, + 'expired_at': expiredAt.millisecondsSinceEpoch + }); + } +} \ No newline at end of file diff --git a/lib/ionia/ionia_user_credentials.dart b/lib/ionia/ionia_user_credentials.dart new file mode 100644 index 000000000..c398385f5 --- /dev/null +++ b/lib/ionia/ionia_user_credentials.dart @@ -0,0 +1,6 @@ +class IoniaUserCredentials { + const IoniaUserCredentials(this.username, this.password); + + final String username; + final String password; +} \ No newline at end of file diff --git a/lib/ionia/ionia_virtual_card.dart b/lib/ionia/ionia_virtual_card.dart new file mode 100644 index 000000000..43cb07584 --- /dev/null +++ b/lib/ionia/ionia_virtual_card.dart @@ -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 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; +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b9f8074e7..4d0ea6208 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/ionia/ionia_category.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -174,7 +176,8 @@ Future initialSetup( exchangeTemplates: exchangeTemplates, transactionDescriptionBox: transactionDescriptions, ordersSource: ordersSource, - unspentCoinsInfoSource: unspentCoinsInfoSource); + unspentCoinsInfoSource: unspentCoinsInfoSource, + ); await bootstrap(navigatorKey); monero?.onStartup(); } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 98ba26446..dfa308b0d 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -2,7 +2,7 @@ part of 'monero.dart'; class CWMoneroAccountList extends MoneroAccountList { CWMoneroAccountList(this._wallet); - Object _wallet; + final Object _wallet; @override @computed @@ -39,13 +39,13 @@ class CWMoneroAccountList extends MoneroAccountList { @override Future addAccount(Object wallet, {String label}) async { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.accountList.addAccount(label: label); + await moneroWallet.walletAddresses.accountList.addAccount(label: label); } @override Future setLabelAccount(Object wallet, {int accountIndex, String label}) async { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.accountList + await moneroWallet.walletAddresses.accountList .setLabelAccount( accountIndex: accountIndex, label: label); @@ -95,7 +95,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { @override Future addSubaddress(Object wallet, {int accountIndex, String label}) async { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.subaddressList + await moneroWallet.walletAddresses.subaddressList .addSubaddress( accountIndex: accountIndex, label: label); @@ -105,7 +105,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { Future setLabelSubaddress(Object wallet, {int accountIndex, int addressIndex, String label}) async { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.subaddressList + await moneroWallet.walletAddresses.subaddressList .setLabelSubaddress( accountIndex: accountIndex, addressIndex: addressIndex, @@ -140,35 +140,43 @@ class CWMonero extends Monero { return CWMoneroAccountList(wallet); } + @override MoneroSubaddressList getSubaddressList(Object wallet) { return CWMoneroSubaddressList(wallet); } + @override TransactionHistoryBase getTransactionHistory(Object wallet) { final moneroWallet = wallet as MoneroWallet; return moneroWallet.transactionHistory; } + @override MoneroWalletDetails getMoneroWalletDetails(Object wallet) { return CWMoneroWalletDetails(wallet); } + @override int getHeigthByDate({DateTime date}) { return getMoneroHeigthByDate(date: date); } + @override TransactionPriority getDefaultTransactionPriority() { return MoneroTransactionPriority.slow; } + @override TransactionPriority deserializeMoneroTransactionPriority({int raw}) { return MoneroTransactionPriority.deserialize(raw: raw); } + @override List getTransactionPriorities() { return MoneroTransactionPriority.all; } + @override List getMoneroWordList(String language) { switch (language.toLowerCase()) { case 'english': @@ -196,14 +204,15 @@ class CWMonero extends Monero { } } + @override WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ String name, - String spendKey, - String viewKey, - String address, - String password, - String language, - int height}) { + String spendKey, + String viewKey, + String address, + String password, + String language, + int height}) { return MoneroRestoreWalletFromKeysCredentials( name: name, spendKey: spendKey, @@ -214,6 +223,7 @@ class CWMonero extends Monero { height: height); } + @override WalletCredentials createMoneroRestoreWalletFromSeedCredentials({String name, String password, int height, String mnemonic}) { return MoneroRestoreWalletFromSeedCredentials( name: name, @@ -222,6 +232,7 @@ class CWMonero extends Monero { mnemonic: mnemonic); } + @override WalletCredentials createMoneroNewWalletCredentials({String name, String password, String language}) { return MoneroNewWalletCredentials( name: name, @@ -229,6 +240,7 @@ class CWMonero extends Monero { language: language); } + @override Map getKeys(Object wallet) { final moneroWallet = wallet as MoneroWallet; final keys = moneroWallet.keys; @@ -239,6 +251,7 @@ class CWMonero extends Monero { 'publicViewKey': keys.publicViewKey}; } + @override Object createMoneroTransactionCreationCredentials({List outputs, TransactionPriority priority}) { return MoneroTransactionCreationCredentials( outputs: outputs.map((out) => OutputInfo( @@ -254,49 +267,72 @@ class CWMonero extends Monero { priority: priority as MoneroTransactionPriority); } + @override + Object createMoneroTransactionCreationCredentialsRaw({List outputs, TransactionPriority priority}) { + return MoneroTransactionCreationCredentials( + outputs: outputs, + priority: priority as MoneroTransactionPriority); + } + + @override String formatterMoneroAmountToString({int amount}) { return moneroAmountToString(amount: amount); } + @override double formatterMoneroAmountToDouble({int amount}) { return moneroAmountToDouble(amount: amount); } + @override int formatterMoneroParseAmount({String amount}) { return moneroParseAmount(amount: amount); } + @override Account getCurrentAccount(Object wallet) { final moneroWallet = wallet as MoneroWallet; final acc = moneroWallet.walletAddresses.account; return Account(id: acc.id, label: acc.label); } + @override void setCurrentAccount(Object wallet, int id, String label) { final moneroWallet = wallet as MoneroWallet; moneroWallet.walletAddresses.account = monero_account.Account(id: id, label: label); } + @override void onStartup() { monero_wallet_api.onStartup(); } + @override int getTransactionInfoAccountId(TransactionInfo tx) { final moneroTransactionInfo = tx as MoneroTransactionInfo; return moneroTransactionInfo.accountIndex; } + @override WalletService createMoneroWalletService(Box walletInfoSource) { return MoneroWalletService(walletInfoSource); } + @override String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { final moneroWallet = wallet as MoneroWallet; return moneroWallet.getTransactionAddress(accountIndex, addressIndex); } + @override String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex) { final moneroWallet = wallet as MoneroWallet; return moneroWallet.getSubaddressLabel(accountIndex, addressIndex); } + + @override + Map pendingTransactionInfo(Object transaction) { + final ptx = transaction as PendingMoneroTransaction; + return {'id': ptx.id, 'hex': ptx.hex, 'key': ptx.txKey}; + } } diff --git a/lib/router.dart b/lib/router.dart index a3c7573ea..1a9c3c2d5 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -4,6 +4,10 @@ import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; @@ -63,6 +67,10 @@ import 'package:flutter/services.dart'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; +import 'package:cake_wallet/src/screens/ionia/ionia.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; RouteSettings currentRouteSettings; @@ -401,6 +409,58 @@ Route createRoute(RouteSettings settings) { param2: args['isLight'] as bool, )); + case Routes.ioniaWelcomePage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaLoginPage: + return CupertinoPageRoute( builder: (_) => getIt.get()); + + case Routes.ioniaCreateAccountPage: + return CupertinoPageRoute( builder: (_) => getIt.get()); + + case Routes.ioniaManageCardsPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaBuyGiftCardPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.ioniaBuyGiftCardDetailPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.ioniaVerifyIoniaOtpPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) =>getIt.get(param1: args)); + + case Routes.ioniaDebitCardPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaActivateDebitCardPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaAccountPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaAccountCardsPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaCustomTipPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) =>getIt.get(param1: args)); + + case Routes.ioniaGiftCardDetailPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) => getIt.get(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(builder: (_) => getIt.get( + param1: paymentInfo, + param2: commitedInfo)); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 1200d558a..82bd46691 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -60,4 +60,18 @@ class Routes { static const moneroNewWalletFromWelcome = '/monero_new_wallet'; static const addressPage = '/address_page'; static const fullscreenQR = '/fullscreen_qr'; -} \ No newline at end of file + 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'; +} diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index eb8c9ab17..6ed07850f 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/yat/yat_popup.dart'; import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/themes/theme_base.dart'; @@ -14,19 +14,15 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/router.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; class DashboardPage extends BasePage { DashboardPage({ @@ -85,7 +81,7 @@ class DashboardPage extends BasePage { final DashboardViewModel walletViewModel; final WalletAddressListViewModel addressListViewModel; - final controller = PageController(initialPage: 0); + final controller = PageController(initialPage: 1); var pages = []; bool _isEffectsInstalled = false; @@ -221,7 +217,7 @@ class DashboardPage extends BasePage { if (_isEffectsInstalled) { return; } - + pages.add(MarketPlacePage(dashboardViewModel: walletViewModel)); pages.add(balancePage); pages.add(TransactionsPage(dashboardViewModel: walletViewModel)); _isEffectsInstalled = true; diff --git a/lib/src/screens/dashboard/widgets/market_place_page.dart b/lib/src/screens/dashboard/widgets/market_place_page.dart new file mode 100644 index 000000000..2484ae32a --- /dev/null +++ b/lib/src/screens/dashboard/widgets/market_place_page.dart @@ -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: [ + 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( + 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); + } + } + +} diff --git a/lib/src/screens/ionia/auth/ionia_create_account_page.dart b/lib/src/screens/ionia/auth/ionia_create_account_page.dart new file mode 100644 index 000000000..5e2797768 --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_create_account_page.dart @@ -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() { + _emailController.text = _authViewModel.email; + _emailController.addListener(() => _authViewModel.email = _emailController.text); + } + + final IoniaAuthViewModel _authViewModel; + + final GlobalKey _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: [ + 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( + 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], + ); +} diff --git a/lib/src/screens/ionia/auth/ionia_login_page.dart b/lib/src/screens/ionia/auth/ionia_login_page.dart new file mode 100644 index 000000000..bcbc0fee3 --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_login_page.dart @@ -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(), + _emailController = TextEditingController() { + _emailController.text = _authViewModel.email; + _emailController.addListener(() => _authViewModel.email = _emailController.text); + } + + final GlobalKey _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: [ + 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( + 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], + ); +} diff --git a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart new file mode 100644 index 000000000..c219781cc --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart @@ -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: [ + 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( + 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); +} diff --git a/lib/src/screens/ionia/auth/ionia_welcome_page.dart b/lib/src/screens/ionia/auth/ionia_welcome_page.dart new file mode 100644 index 000000000..bd01aed06 --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_welcome_page.dart @@ -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: [ + 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) + ], + ) + ], + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart new file mode 100644 index 000000000..f1d08a75e --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart @@ -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 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, + ), + ); + }, + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_account_page.dart b/lib/src/screens/ionia/cards/ionia_account_page.dart new file mode 100644 index 000000000..28e7e9b42 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_account_page.dart @@ -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, + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart b/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart new file mode 100644 index 000000000..4f688a129 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart @@ -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( + 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( + 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(), + ); + }, + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart new file mode 100644 index 000000000..95202f472 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart @@ -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( + 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( + 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( + 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 purchaseCard(BuildContext context) async { + await ioniaPurchaseViewModel.createInvoice(); + + if (ioniaPurchaseViewModel.invoiceCreationState is ExecutedSuccessfullyState) { + await _presentSuccessfulInvoiceCreationPopup(context); + } + } + + void _showHowToUseCard( + BuildContext context, + IoniaMerchant merchant, + ) { + showPopUp( + 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 _presentSuccessfulInvoiceCreationPopup(BuildContext context) async { + final amount = ioniaPurchaseViewModel.invoice.totalAmount; + final addresses = ioniaPurchaseViewModel.invoice.outAddresses; + + await showPopUp( + 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 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, + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart new file mode 100644 index 000000000..899b1387d --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart @@ -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), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart b/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart new file mode 100644 index 000000000..5dbce02b6 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart @@ -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), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_debit_card_page.dart b/lib/src/screens/ionia/cards/ionia_debit_card_page.dart new file mode 100644 index 000000000..f863f5780 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_debit_card_page.dart @@ -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( + 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), + ), + ], + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart new file mode 100644 index 000000000..9aa094f4e --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart @@ -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( + 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(context, + S.of(context).transaction_details_copied(title)); + }); + } + + void _showHowToUseCard( + BuildContext context, + IoniaGiftCard merchant, + ) { + showPopUp( + 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, + ); + }); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart new file mode 100644 index 000000000..656b8676b --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart @@ -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> showCategoryFilter( + BuildContext context, + IoniaGiftCardsListViewModel viewModel, + ) async { + return await showPopUp>( + context: context, + builder: (BuildContext context) { + return IoniaFilterModal( + filterViewModel: getIt.get(), + 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 { + double get backgroundHeight => MediaQuery.of(context).size.height * 0.75; + double thumbHeight = 72; + bool get isAlwaysShowScrollThumb => merchantsList == null ? false : merchantsList.length > 3; + + List 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, + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_payment_status_page.dart b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart new file mode 100644 index 000000000..d264842e6 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart @@ -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(context, + S.of(context).transaction_details_copied(title)); + }); + } +} \ No newline at end of file diff --git a/lib/src/screens/ionia/ionia.dart b/lib/src/screens/ionia/ionia.dart new file mode 100644 index 000000000..bdc2065a9 --- /dev/null +++ b/lib/src/screens/ionia/ionia.dart @@ -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'; diff --git a/lib/src/screens/ionia/widgets/card_item.dart b/lib/src/screens/ionia/widgets/card_item.dart new file mode 100644 index 000000000..ae3554331 --- /dev/null +++ b/lib/src/screens/ionia/widgets/card_item.dart @@ -0,0 +1,139 @@ +import 'package:cake_wallet/src/widgets/discount_badge.dart'; +import 'package:flutter/material.dart'; + +class CardItem extends StatelessWidget { + CardItem({ + @required this.title, + @required this.subTitle, + @required this.backgroundColor, + @required this.titleColor, + @required this.subtitleColor, + this.discountBackground, + this.onTap, + this.logoUrl, + this.discount, + }); + + final VoidCallback onTap; + final String title; + final String subTitle; + final String logoUrl; + final double discount; + final Color backgroundColor; + final Color titleColor; + final Color subtitleColor; + final AssetImage discountBackground; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Stack( + children: [ + Container( + padding: EdgeInsets.all(12), + width: double.infinity, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.20), + ), + ), + child: Row( + children: [ + if (logoUrl != null) ...[ + ClipOval( + child: Image.network( + logoUrl, + width: 40.0, + height: 40.0, + fit: BoxFit.cover, + loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent loadingProgress) { + if (loadingProgress == null) { + return child; + } else { + return _PlaceholderContainer(text: 'Logo'); + } + }, + errorBuilder: (_, __, ___) => _PlaceholderContainer(text: '!'), + ), + ), + SizedBox(width: 5), + ], + Column( + crossAxisAlignment: (subTitle?.isEmpty ?? false) + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: Text( + title, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontSize: 20, + fontWeight: FontWeight.w900, + ), + ), + ), + if (subTitle?.isNotEmpty ?? false) + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + subTitle, + style: TextStyle( + color: subtitleColor, + fontWeight: FontWeight.w500, + fontFamily: 'Lato')), + ) + ], + ), + ], + ), + ), + if (discount != 0.0) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(top: 20.0), + child: DiscountBadge( + percentage: discount, + discountBackground: discountBackground, + ), + ), + ), + ], + ), + ); + } +} + +class _PlaceholderContainer extends StatelessWidget { + const _PlaceholderContainer({@required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Container( + height: 42, + width: 42, + child: Center( + child: Text( + text, + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontWeight: FontWeight.w900, + ), + ), + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(100), + ), + ); + } +} diff --git a/lib/src/screens/ionia/widgets/card_menu.dart b/lib/src/screens/ionia/widgets/card_menu.dart new file mode 100644 index 000000000..9212c0448 --- /dev/null +++ b/lib/src/screens/ionia/widgets/card_menu.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class CardMenu extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return Container( + + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/ionia/widgets/confirm_modal.dart b/lib/src/screens/ionia/widgets/confirm_modal.dart new file mode 100644 index 000000000..33a89a9b0 --- /dev/null +++ b/lib/src/screens/ionia/widgets/confirm_modal.dart @@ -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: [ + 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, + ), + )), + ), + )); + } +} diff --git a/lib/src/screens/ionia/widgets/ionia_alert_model.dart b/lib/src/screens/ionia/widgets/ionia_alert_model.dart new file mode 100644 index 000000000..abe442d8e --- /dev/null +++ b/lib/src/screens/ionia/widgets/ionia_alert_model.dart @@ -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, + ), + ), + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/ionia/widgets/ionia_filter_modal.dart b/lib/src/screens/ionia/widgets/ionia_filter_modal.dart new file mode 100644 index 000000000..242b19e51 --- /dev/null +++ b/lib/src/screens/ionia/widgets/ionia_filter_modal.dart @@ -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 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, + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/widgets/ionia_tile.dart b/lib/src/screens/ionia/widgets/ionia_tile.dart new file mode 100644 index 000000000..1e066312c --- /dev/null +++ b/lib/src/screens/ionia/widgets/ionia_tile.dart @@ -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, + ), + ), + ], + ) + ], + )); + } +} diff --git a/lib/src/screens/ionia/widgets/rounded_checkbox.dart b/lib/src/screens/ionia/widgets/rounded_checkbox.dart new file mode 100644 index 000000000..095ad08b5 --- /dev/null +++ b/lib/src/screens/ionia/widgets/rounded_checkbox.dart @@ -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(); + } +} diff --git a/lib/src/screens/ionia/widgets/text_icon_button.dart b/lib/src/screens/ionia/widgets/text_icon_button.dart new file mode 100644 index 000000000..16606e65d --- /dev/null +++ b/lib/src/screens/ionia/widgets/text_icon_button.dart @@ -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, + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/alert_with_two_actions.dart b/lib/src/widgets/alert_with_two_actions.dart index c2831675e..f3c66f275 100644 --- a/lib/src/widgets/alert_with_two_actions.dart +++ b/lib/src/widgets/alert_with_two_actions.dart @@ -10,7 +10,10 @@ class AlertWithTwoActions extends BaseAlertDialog { @required this.rightButtonText, @required this.actionLeftButton, @required this.actionRightButton, - this.alertBarrierDismissible = true + this.alertBarrierDismissible = true, + this.isDividerExist = false, + this.leftActionColor, + this.rightActionColor, }); final String alertTitle; @@ -20,6 +23,9 @@ class AlertWithTwoActions extends BaseAlertDialog { final VoidCallback actionLeftButton; final VoidCallback actionRightButton; final bool alertBarrierDismissible; + final Color leftActionColor; + final Color rightActionColor; + final bool isDividerExist; @override String get titleText => alertTitle; @@ -35,4 +41,10 @@ class AlertWithTwoActions extends BaseAlertDialog { VoidCallback get actionRight => actionRightButton; @override bool get barrierDismissible => alertBarrierDismissible; -} \ No newline at end of file + @override + Color get leftButtonColor => leftActionColor; + @override + Color get rightButtonColor => rightActionColor; + @override + bool get isDividerExists => isDividerExist; +} diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 19ff62ce2..d0aaace2f 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -46,30 +46,28 @@ class BaseAlertDialog extends StatelessWidget { children: [ Flexible( child: Container( - height: 52, - padding: EdgeInsets.only(left: 6, right: 6), - color: Theme.of(context).accentTextTheme.body2.decorationColor, - child: ButtonTheme( - minWidth: double.infinity, - child: FlatButton( - onPressed: actionLeft, - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: Text( - leftActionButtonText, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.body2 - .backgroundColor, - decoration: TextDecoration.none, - ), - )), - ), - ) - ), + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: Theme.of(context).accentTextTheme.body2.decorationColor, + child: ButtonTheme( + minWidth: double.infinity, + child: FlatButton( + onPressed: actionLeft, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + leftActionButtonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.body2.backgroundColor, + decoration: TextDecoration.none, + ), + )), + ), + )), Container( width: 1, height: 52, @@ -77,30 +75,28 @@ class BaseAlertDialog extends StatelessWidget { ), Flexible( child: Container( - height: 52, - padding: EdgeInsets.only(left: 6, right: 6), - color: Theme.of(context).accentTextTheme.body1.backgroundColor, - child: ButtonTheme( - minWidth: double.infinity, - child: FlatButton( - onPressed: actionRight, - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: Text( - rightActionButtonText, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.body1 - .backgroundColor, - decoration: TextDecoration.none, - ), - )), - ), - ) - ), + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: Theme.of(context).accentTextTheme.body1.backgroundColor, + child: ButtonTheme( + minWidth: double.infinity, + child: FlatButton( + onPressed: actionRight, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + rightActionButtonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.body1.backgroundColor, + decoration: TextDecoration.none, + ), + )), + ), + )), ], ); } @@ -108,9 +104,7 @@ class BaseAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => barrierDismissible - ? Navigator.of(context).pop() - : null, + onTap: () => barrierDismissible ? Navigator.of(context).pop() : null, child: Container( color: Colors.transparent, child: BackdropFilter( @@ -136,14 +130,14 @@ class BaseAlertDialog extends StatelessWidget { child: title(context), ), isDividerExists - ? Padding( - padding: EdgeInsets.only(top: 16, bottom: 8), - child: Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - ) - : Offstage(), + ? Padding( + padding: EdgeInsets.only(top: 16, bottom: 8), + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ) + : Offstage(), Padding( padding: EdgeInsets.fromLTRB(24, 8, 24, 32), child: content(context), @@ -166,4 +160,4 @@ class BaseAlertDialog extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/cake_scrollbar.dart b/lib/src/widgets/cake_scrollbar.dart index 6ccf391c6..6a0cb2e14 100644 --- a/lib/src/widgets/cake_scrollbar.dart +++ b/lib/src/widgets/cake_scrollbar.dart @@ -5,13 +5,19 @@ class CakeScrollbar extends StatelessWidget { @required this.backgroundHeight, @required this.thumbHeight, @required this.fromTop, - this.rightOffset = 6 + this.rightOffset = 6, + this.backgroundColor, + this.thumbColor, + this.width = 6, }); final double backgroundHeight; final double thumbHeight; final double fromTop; + final double width; final double rightOffset; + final Color backgroundColor; + final Color thumbColor; @override Widget build(BuildContext context) { @@ -19,11 +25,10 @@ class CakeScrollbar extends StatelessWidget { right: rightOffset, child: Container( height: backgroundHeight, - width: 6, + width: width, decoration: BoxDecoration( - color: Theme.of(context).textTheme.body1.decorationColor, - borderRadius: BorderRadius.all(Radius.circular(3)) - ), + color: backgroundColor ?? Theme.of(context).textTheme.body1.decorationColor, + borderRadius: BorderRadius.all(Radius.circular(3))), child: Stack( children: [ AnimatedPositioned( @@ -31,16 +36,14 @@ class CakeScrollbar extends StatelessWidget { top: fromTop, child: Container( height: thumbHeight, - width: 6.0, + width: width, decoration: BoxDecoration( - color: Theme.of(context).textTheme.body1.color, - borderRadius: BorderRadius.all(Radius.circular(3)) - ), + color: thumbColor ?? Theme.of(context).textTheme.body1.color, + borderRadius: BorderRadius.all(Radius.circular(3))), ), ) ], ), - ) - ); + )); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/discount_badge.dart b/lib/src/widgets/discount_badge.dart new file mode 100644 index 000000000..1cbbf3e89 --- /dev/null +++ b/lib/src/widgets/discount_badge.dart @@ -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'), + ), + ), + ); + } +} diff --git a/lib/src/widgets/market_place_item.dart b/lib/src/widgets/market_place_item.dart new file mode 100644 index 000000000..0112ea00d --- /dev/null +++ b/lib/src/widgets/market_place_item.dart @@ -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'), + ) + ], + ), + ), + ], + ), + ); + } +} + diff --git a/lib/typography.dart b/lib/typography.dart new file mode 100644 index 000000000..08491e2cb --- /dev/null +++ b/lib/typography.dart @@ -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, + ); diff --git a/lib/view_model/ionia/ionia_account_view_model.dart b/lib/view_model/ionia/ionia_account_view_model.dart new file mode 100644 index 000000000..a4875ec61 --- /dev/null +++ b/lib/view_model/ionia/ionia_account_view_model.dart @@ -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 giftCards; + + @computed + int get countOfMerch => giftCards.where((giftCard) => !giftCard.isEmpty).length; + + @computed + List get activeMechs => giftCards.where((giftCard) => !giftCard.isEmpty).toList(); + + @computed + List get redeemedMerchs => giftCards.where((giftCard) => giftCard.isEmpty).toList(); + + @action + void logout() { + ioniaService.logout(); + } + + @action + Future updateUserGiftCards() async { + giftCards = await ioniaService.getCurrentUserGiftCardSummaries(); + } +} diff --git a/lib/view_model/ionia/ionia_auth_view_model.dart b/lib/view_model/ionia/ionia_auth_view_model.dart new file mode 100644 index 000000000..5e662b462 --- /dev/null +++ b/lib/view_model/ionia/ionia_auth_view_model.dart @@ -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 verifyEmail(String code) async { + try { + otpState = IoniaOtpValidating(); + await ioniaService.verifyEmail(code); + otpState = IoniaOtpSuccess(); + } catch (_) { + otpState = IoniaOtpFailure(error: 'Invalid OTP. Try again'); + } + } + + @action + Future createUser(String email) async { + try { + createUserState = IoniaCreateStateLoading(); + await ioniaService.createUser(email); + createUserState = IoniaCreateStateSuccess(); + } catch (e) { + createUserState = IoniaCreateStateFailure(error: e.toString()); + } + } + + + @action + Future signIn(String email) async { + try { + signInState = IoniaCreateStateLoading(); + await ioniaService.signIn(email); + signInState = IoniaCreateStateSuccess(); + } catch (e) { + signInState = IoniaCreateStateFailure(error: e.toString()); + } + } + +} \ No newline at end of file diff --git a/lib/view_model/ionia/ionia_buy_card_view_model.dart b/lib/view_model/ionia/ionia_buy_card_view_model.dart new file mode 100644 index 000000000..b6314be80 --- /dev/null +++ b/lib/view_model/ionia/ionia_buy_card_view_model.dart @@ -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; + } +} diff --git a/lib/view_model/ionia/ionia_filter_view_model.dart b/lib/view_model/ionia/ionia_filter_view_model.dart new file mode 100644 index 000000000..43d2790e0 --- /dev/null +++ b/lib/view_model/ionia/ionia_filter_view_model.dart @@ -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(); + ioniaCategories = IoniaCategory.allCategories; + } + + List get selectedCategories => ioniaCategories.where(_isSelected).toList(); + + @observable + ObservableList selectedIndices; + + @observable + List 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 selectedCategories) { + selectedIndices = ObservableList.of(selectedCategories.map((e) => e.index)); + } + + bool _isSelected(IoniaCategory ioniaCategory) { + return selectedIndices.contains(ioniaCategory.index); + } +} diff --git a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart new file mode 100644 index 000000000..e6138bb53 --- /dev/null +++ b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart @@ -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 redeem() async { + try { + redeemState = IsExecutingState(); + await ioniaService.redeem(giftCard); + giftCard = await ioniaService.getGiftCard(id: giftCard.id); + redeemState = ExecutedSuccessfullyState(); + } catch(e) { + redeemState = FailureState(e.toString()); + } + } +} \ No newline at end of file diff --git a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart new file mode 100644 index 000000000..f20000a6d --- /dev/null +++ b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart @@ -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 ioniaMerchantList; + + String searchString; + + List selectedFilters; + + @observable + double scrollOffsetFromTop; + + @observable + IoniaCreateCardState createCardState; + + @observable + IoniaFetchCardState cardState; + + @observable + List ioniaMerchants; + + @observable + bool isLoggedIn; + + Future _getAuthStatus() async { + return await ioniaService.isLogined(); + } + + @action + Future 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 _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 filters) { + selectedFilters = filters; + _getMerchants(); + } + + void setScrollOffsetFromTop(double scrollOffset) { + scrollOffsetFromTop = scrollOffset; + } +} diff --git a/lib/view_model/ionia/ionia_payment_status_view_model.dart b/lib/view_model/ionia/ionia_payment_status_view_model.dart new file mode 100644 index 000000000..187e24856 --- /dev/null +++ b/lib/view_model/ionia/ionia_payment_status_view_model.dart @@ -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 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(); + } + } +} diff --git a/lib/view_model/ionia/ionia_purchase_merch_view_model.dart b/lib/view_model/ionia/ionia_purchase_merch_view_model.dart new file mode 100644 index 000000000..b8c8eaa96 --- /dev/null +++ b/lib/view_model/ionia/ionia_purchase_merch_view_model.dart @@ -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(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 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 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 commitPaymentInvoice() async { + try { + invoiceCommittingState = IsExecutingState(); + committedInfo = await ioniaAnyPayService.commitInvoice(invoice); + invoiceCommittingState = ExecutedSuccessfullyState(payload: committedInfo); + } catch (e) { + invoiceCommittingState = FailureState(e.toString()); + } + } +} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index d82dd979f..b615c00c4 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -222,11 +222,11 @@ abstract class SendViewModelBase with Store { case WalletType.bitcoin: final priority = _settingsStore.priority[_wallet.type]; - return bitcoin.createBitcoinTransactionCredentials(outputs, priority); + return bitcoin.createBitcoinTransactionCredentials(outputs, priority: priority); case WalletType.litecoin: final priority = _settingsStore.priority[_wallet.type]; - return bitcoin.createBitcoinTransactionCredentials(outputs, priority); + return bitcoin.createBitcoinTransactionCredentials(outputs, priority: priority); case WalletType.monero: final priority = _settingsStore.priority[_wallet.type]; diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 6d88ae671..d9687bc36 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -534,5 +534,103 @@ "search_currency": "Währung suchen", "new_template" : "neue Vorlage", "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" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 639c97c97..c66f9a6e6 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -245,7 +245,7 @@ "settings_only_transactions" : "Only transactions", "settings_none" : "None", "settings_support" : "Support", - "settings_terms_and_conditions" : "Terms and conditions", + "settings_terms_and_conditions" : "Terms and Conditions", "pin_is_incorrect" : "PIN is incorrect", @@ -534,5 +534,103 @@ "search_currency": "Search currency", "new_template" : "New Template", "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 you’ve 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 doesn’t 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 doesn’t 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" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index efe78c145..084658081 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -534,5 +534,103 @@ "search_currency": "Moneda de búsqueda", "new_template" : "Nueva plantilla", "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" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 01b12f791..9817e7bef 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -243,7 +243,7 @@ "settings_only_transactions" : "Seulement les transactions", "settings_none" : "Rien", "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", @@ -532,5 +532,103 @@ "search_currency": "Devise de recherche", "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", - "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" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 711e4d92c..03a891169 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -534,5 +534,103 @@ "search_currency": "मुद्रा खोजें", "new_template" : "नया टेम्पलेट", "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": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 209fa38b9..c25b54eb8 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -534,5 +534,103 @@ "search_currency": "Traži valutu", "new_template" : "novi predložak", "electrum_address_disclaimer": "Minden egyes alkalommal új címeket generálunk, de a korábbi címek továbbra is működnek", - "wallet_name_exists": "Novčanik s tim nazivom već postoji" + "wallet_name_exists": "Novčanik s tim nazivom već postoji", + "market_place": "Tržnica", + "cake_pay_title": "Cake Pay poklon kartice", + "cake_pay_subtitle": "Kupite darovne kartice i odmah ih iskoristite", + "about_cake_pay": "Cake Pay vam omogućuje jednostavnu kupnju darovnih kartica s virtualnim sredstvima, koja se trenutno mogu potrošiti kod više od 150 000 trgovaca u Sjedinjenim Državama.", + "cake_pay_account_note": "Napravite račun da vidite dostupne kartice. Neke su čak dostupne uz popust!", + "already_have_account": "Već imate račun?", + "create_account": "Stvori račun", + "privacy_policy": "Pravila privatnosti", + "welcome_to_cakepay": "Dobro došli u Cake Pay!", + "sign_up": "Prijavite se", + "forgot_password": "Zaboravljena lozinka", + "reset_password": "Poništi lozinku", + "gift_cards": "Ajándékkártya", + "setup_your_debit_card": "Postavite svoju debitnu karticu", + "no_id_required": "Nije potreban ID. Nadopunite i potrošite bilo gdje", + "how_to_use_card": "Kako koristiti ovu karticu", + "purchase_gift_card": "Kupnja darovne kartice", + "verification": "Potvrda", + "fill_code": "Molimo vas da ispunite kontrolni kod koji ste dobili na svojoj e-pošti", + "dont_get_code": "Ne dobivate kod?", + "resend_code": "Molimo da ga ponovno pošaljete", + "debit_card": "Debitna kartica", + "cakepay_prepaid_card": "CakePay unaprijed plaćena debitna kartica", + "no_id_needed": "Nije potreban ID!", + "frequently_asked_questions": "Često postavljana pitanja", + "debit_card_terms": "Pohranjivanje i korištenje broja vaše platne kartice (i vjerodajnica koje odgovaraju broju vaše platne kartice) u ovom digitalnom novčaniku podliježu Uvjetima i odredbama važećeg ugovora vlasnika kartice s izdavateljem platne kartice, koji su na snazi ​​od S vremena na vrijeme.", + "please_reference_document": "Molimo pogledajte dokumente ispod za više informacija.", + "cardholder_agreement": "Ugovor s vlasnikom kartice", + "e_sign_consent": "E-Sign pristanak", + "agree_and_continue": "Slažem se i nastavi", + "email_address": "Adresa e-pošte", + "agree_to": "Stvaranjem računa pristajete na ", + "and": "i", + "enter_code": "Unesite kod", + "congratulations": "Čestitamo!", + "you_now_have_debit_card": "Sada imate debitnu karticu", + "min_amount" : "Minimalno: ${value}", + "max_amount" : "Maksimum: ${value}", + "enter_amount": "Unesite iznos", + "billing_address_info": "Ako se od vas zatraži adresa za naplatu, navedite svoju adresu za dostavu", + "order_physical_card": "Naručite fizičku karticu", + "add_value": "Dodaj vrijednost", + "activate": "Aktiviraj", + "get_a": "Nabavite ", + "digital_and_physical_card": "digitalna i fizička unaprijed plaćena debitna kartica", + "get_card_note": " koju možete ponovno napuniti digitalnim valutama. Nisu potrebne dodatne informacije!", + "signup_for_card_accept_terms": "Prijavite se za karticu i prihvatite uvjete.", + "add_fund_to_card": "Dodajte unaprijed uplaćena sredstva na kartice (do ${value})", + "use_card_info_two": "Sredstva se pretvaraju u USD kada se drže na prepaid računu, a ne u digitalnim valutama.", + "use_card_info_three": "Koristite digitalnu karticu online ili s beskontaktnim metodama plaćanja.", + "optionally_order_card": "Opcionalno naručite fizičku karticu.", + "hide_details" : "Sakrij pojedinosti", + "show_details": "Prikaži pojedinosti", + "upto": "do ${value}", + "discount": "Uštedite ${value}%", + "gift_card_amount": "Iznos darovne kartice", + "bill_amount": "Iznos računa", + "you_pay": "Vi plaćate", + "tip": "Savjet:", + "custom": "prilagođeno", + "by_cake_pay": "od Cake Paya", + "expires": "Ističe", + "mm": "MM", + "yy": "GG", + "online": "Na mreži", + "offline": "izvan mreže", + "gift_card_number": "Broj darovne kartice", + "pin_number": "PIN broj", + "total_saving": "Ukupna ušteda", + "last_30_days": "Zadnjih 30 dana", + "avg_savings": "Prosj. ušteda", + "view_all": "Prikaži sve", + "active_cards": "Aktivne kartice", + "delete_account": "Izbriši račun", + "cards": "Kartice", + "active": "Aktivno", + "redeemed": "otkupljeno", + "gift_card_balance_note": "Ovdje će se pojaviti darovne kartice s preostalim saldom", + "gift_card_redeemed_note": "Poklon kartice koje ste iskoristili pojavit će se ovdje", + "logout": "Odjava", + "add_tip": "Dodaj savjet", + "percentageOf": "od ${amount}", + "is_percentage": "je", + "search_category": "Kategorija pretraživanja", + "mark_as_redeemed": "Označi kao otkupljeno", + "more_options": "Više opcija", + "awaiting_payment_confirmation": "Čeka se potvrda plaćanja", + "transaction_sent_notice": "Ako se zaslon ne nastavi nakon 1 minute, provjerite block explorer i svoju e-poštu.", + "agree": "Slažem se", + "in_store": "U trgovini", + "generating_gift_card": "Generiranje darovne kartice", + "payment_was_received": "Vaša uplata je primljena.", + "proceed_after_one_minute": "Ako se zaslon ne nastavi nakon 1 minute, provjerite svoju e-poštu.", + "order_id": "ID narudžbe", + "gift_card_is_generated": "Poklon kartica je generirana", + "open_gift_card": "Otvori darovnu karticu", + "contact_support": "Kontaktirajte podršku", + "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 4b67fdc80..8383c9ad9 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -534,5 +534,103 @@ "search_currency": "Cerca valuta", "new_template" : "Nuovo modello", "electrum_address_disclaimer": "Generiamo nuovi indirizzi ogni volta che ne utilizzi uno, ma gli indirizzi precedenti continuano a funzionare", - "wallet_name_exists": "Il portafoglio con quel nome è già esistito" + "wallet_name_exists": "Il portafoglio con quel nome è già esistito", + "market_place": "Mercato", + "cake_pay_title": "Carte regalo Cake Pay", + "cake_pay_subtitle": "Acquista carte regalo e riscattale all'istante", + "about_cake_pay": "Cake Pay ti consente di acquistare facilmente buoni regalo con asset virtuali, spendibili istantaneamente presso oltre 150.000 commercianti negli Stati Uniti.", + "cake_pay_account_note": "Crea un account per vedere le carte disponibili. Alcune sono anche disponibili con uno sconto!", + "already_have_account": "Hai già un account?", + "create_account": "Crea account", + "privacy_policy": "Informativa sulla privacy", + "welcome_to_cakepay": "Benvenuto in Cake Pay!", + "sign_up": "Registrati", + "forgot_password": "Password dimenticata", + "reset_password": "Reimposta password", + "gift_cards": "Carte regalo", + "setup_your_debit_card": "Configura la tua carta di debito", + "no_id_required": "Nessun ID richiesto. Ricarica e spendi ovunque", + "how_to_use_card": "Come usare questa carta", + "purchase_gift_card": "Acquista carta regalo", + "verification": "Verifica", + "fill_code": "Compila il codice di verifica fornito alla tua email", + "dont_get_code": "Non ricevi il codice?", + "resend_code": "Per favore, invialo nuovamente", + "debit_card": "Carta di debito", + "cakepay_prepaid_card": "Carta di debito prepagata CakePay", + "no_id_needed": "Nessun ID necessario!", + "frequently_asked_questions": "Domande frequenti", + "debit_card_terms": "L'archiviazione e l'utilizzo del numero della carta di pagamento (e delle credenziali corrispondenti al numero della carta di pagamento) in questo portafoglio digitale sono soggetti ai Termini e condizioni del contratto applicabile con il titolare della carta con l'emittente della carta di pagamento, come in vigore da tempo al tempo.", + "please_reference_document": "Si prega di fare riferimento ai documenti di seguito per ulteriori informazioni.", + "cardholder_agreement": "Contratto del titolare della carta", + "e_sign_consent": "Consenso alla firma elettronica", + "agree_and_continue": "Accetta e continua", + "email_address": "Indirizzo e-mail", + "agree_to": "Creando un account accetti il ​​", + "and": "e", + "enter_code": "Inserisci codice", + "congratulation": "Congratulazioni!", + "you_now_have_debit_card": "Ora hai una carta di debito", + "min_amount" : "Min: ${value}", + "max_amount" : "Max: ${value}", + "enter_amount": "Inserisci importo", + "billing_address_info": "Se ti viene richiesto un indirizzo di fatturazione, fornisci il tuo indirizzo di spedizione", + "order_physical_card": "Ordine carta fisica", + "add_value": "Aggiungi valore", + "activate": "Attiva", + "get_a": "Prendi un ", + "digital_and_physical_card": "carta di debito prepagata digitale e fisica", + "get_card_note": "che puoi ricaricare con le valute digitali. Non sono necessarie informazioni aggiuntive!", + "signup_for_card_accept_terms": "Registrati per la carta e accetta i termini.", + "add_fund_to_card": "Aggiungi fondi prepagati alle carte (fino a ${value})", + "use_card_info_two": "I fondi vengono convertiti in USD quando sono detenuti nel conto prepagato, non in valute digitali.", + "use_card_info_three": "Utilizza la carta digitale online o con metodi di pagamento contactless.", + "optional_order_card": "Ordina facoltativamente una carta fisica.", + "hide_details" : "Nascondi dettagli", + "show_details": "Mostra dettagli", + "upto": "fino a ${value}", + "discount": "Risparmia ${value}%", + "gift_card_amount": "Importo del buono regalo", + "bill_amount": "Importo della fattura", + "you_pay": "Tu paghi", + "tip": "Suggerimento:", + "custom": "personalizzato", + "by_cake_pay": "da Cake Pay", + "expires": "Scade", + "mm": "mm", + "yy": "YY", + "online": "in linea", + "offline": "Offline", + "gift_card_number": "Numero del buono regalo", + "pin_number": "Numero PIN", + "total_saving": "Risparmio totale", + "last_30_days": "Ultimi 30 giorni", + "avg_savings": "Risparmio medio", + "view_all": "Visualizza tutto", + "active_cards": "Carte attive", + "delete_account": "Elimina account", + "cards": "Carte", + "active": "Attivo", + "redeemed": "Redento", + "gift_card_balance_note": "Le carte regalo con un saldo residuo appariranno qui", + "gift_card_redeemed_note": "Le carte regalo che hai riscattato appariranno qui", + "logout": "Logout", + "add_tip": "Aggiungi suggerimento", + "percentageOf": "di ${amount}", + "is_percentage": "è", + "search_category": "Categoria di ricerca", + "mark_as_redeemed": "Segna come riscattato", + "more_options": "Altre opzioni", + "waiting_payment_confirmation": "In attesa di conferma del pagamento", + "transaction_sent_notice": "Se lo schermo non procede dopo 1 minuto, controlla un block explorer e la tua email.", + "agree": "d'accordo", + "in_store": "In negozio", + "generating_gift_card": "Generazione carta regalo", + "payment_was_received": "Il tuo pagamento è stato ricevuto.", + "proceed_after_one_minute": "Se lo schermo non procede dopo 1 minuto, controlla la tua email.", + "order_id": "ID ordine", + "gift_card_is_generated": "Il buono regalo è stato generato", + "open_gift_card": "Apri carta regalo", + "contact_support": "Contatta l'assistenza", + "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 9a3f739e5..0ee7b5f29 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -534,5 +534,103 @@ "search_currency": "検索通貨", "new_template" : "新しいテンプレート", "electrum_address_disclaimer": "使用するたびに新しいアドレスが生成されますが、以前のアドレスは引き続き機能します", - "wallet_name_exists": "その名前のウォレットはすでに存在しています" + "wallet_name_exists": "その名前のウォレットはすでに存在しています", + "market_place": "Marketplace", + "cake_pay_title": "ケーキペイギフトカード", + "cake_pay_subtitle": "ギフトカードを購入してすぐに利用できます", + "about_cake_pay": "Cake Payを使用すると、仮想資産を含むギフトカードを簡単に購入でき、米国内の150,000を超える加盟店ですぐに利用できます。", + "cake_pay_account_note": "アカウントを作成して、利用可能なカードを確認してください。割引価格で利用できるカードもあります!", + "already_have_account": "すでにアカウントをお持ちですか?", + "create_account": "アカウントの作成", + "privacy_policy": "プライバシーポリシー", + "welcome_to_cakepay": "Cake Payへようこそ!", + "sign_up": "サインアップ", + "forgot_password": "パスワードを忘れた", + "reset_password": "パスワードのリセット", + "gift_cards": "ギフトカード", + "setup_your_debit_card": "デビットカードを設定してください", + "no_id_required": "IDは必要ありません。どこにでも補充して使用できます", + "how_to_use_card": "このカードの使用方法", + "purchase_gift_card": "ギフトカードを購入", + "verification" : "検証", + "fill_code": "メールアドレスに記載されている確認コードを入力してください", + "dont_get_code": "コードを取得しませんか?", + "resend_code": "再送してください", + "debit_card": "デビットカード", + "cakepay_prepaid_card": "CakePayプリペイドデビットカード", + "no_id_needed": "IDは必要ありません!", + "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": "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": "up up ${value}", + "discount": "${value}%を節約", + "gift_card_amount": "ギフトカードの金額", + "bill_amount": "請求額", + "you_pay": "あなたが支払う", + "tip": "ヒント: ", + "custom": "カスタム", + "by_cake_pay": "by Cake Pay", + "expires": "Expires", + "mm": "んん", + "yy": "YY", + "online": "オンライン", + "offline": "オフライン", + "gift_card_number": "ギフトカード番号", + "pin_number": "PIN番号", + "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": "of ${amount}", + "is_percentage": "is", + "search_category": "検索カテゴリ", + "mark_as_redeemed": "償還済みとしてマーク", + "more_options": "その他のオプション", + "awaiting_payment_confirmation": "支払い確認を待っています", + "transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。", + "agree": "同意する", + "in_store": "インストア", + "generated_gift_card": "ギフトカードの生成", + "payment_was_received": "お支払いを受け取りました。", + "proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。", + "order_id": "注文ID", + "gift_card_is_generated": "ギフトカードが生成されます", + "open_gift_card": "オープンギフトカード", + "contact_support": "サポートに連絡する", + "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index afe78fa3a..d7ee849aa 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -534,5 +534,103 @@ "search_currency": "통화 검색", "new_template" : "새 템플릿", "electrum_address_disclaimer": "사용할 때마다 새 주소가 생성되지만 이전 주소는 계속 작동합니다.", - "wallet_name_exists": "해당 이름의 지갑이 이미 존재합니다." + "wallet_name_exists": "해당 이름의 지갑이 이미 존재합니다.", + "market_place": "마켓플레이스", + "cake_pay_title": "케이크 페이 기프트 카드", + "cake_pay_subtitle": "기프트 카드를 구매하고 즉시 사용", + "about_cake_pay": "Cake Pay를 사용하면 미국 내 150,000개 이상의 가맹점에서 즉시 사용할 수 있는 가상 자산이 포함된 기프트 카드를 쉽게 구입할 수 있습니다.", + "cake_pay_account_note": "사용 가능한 카드를 보려면 계정을 만드십시오. 일부는 할인된 가격으로 사용 가능합니다!", + "already_have_account": "이미 계정이 있습니까?", + "create_account": "계정 만들기", + "privacy_policy": "개인 정보 보호 정책", + "welcome_to_cakepay": "Cake Pay에 오신 것을 환영합니다!", + "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": "CakePay 선불 직불 카드", + "no_id_needed": "ID가 필요하지 않습니다!", + "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": "디지털 화폐가 아닌 선불 계정에 보유하면 자금이 USD로 변환됩니다.", + "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": "Cake Pay로", + "expires": "만료", + "mm": "mm", + "YY": "YY", + "online": "온라인", + "offline": "오프라인", + "gift_card_number": "기프트 카드 번호", + "pin_number": "PIN 번호", + "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": "주문 ID", + "gift_card_is_generated": "기프트 카드가 생성되었습니다", + "open_gift_card": "기프트 카드 열기", + "contact_support": "지원팀에 문의", + "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다." } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 5dfd9c3f2..967cf1e38 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -534,5 +534,103 @@ "search_currency": "Zoek valuta", "new_template" : "Nieuwe sjabloon", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", - "wallet_name_exists": "Portemonnee met die naam bestaat al" + "wallet_name_exists": "Portemonnee met die naam bestaat al", + "market_place": "Marktplaats", + "cake_pay_title": "Cake Pay-cadeaubonnen", + "cake_pay_subtitle": "Koop cadeaubonnen en wissel ze direct in", + "about_cake_pay": "Met Cake Pay kunt u eenvoudig cadeaubonnen kopen met virtuele activa, die direct kunnen worden uitgegeven bij meer dan 150.000 handelaren in de Verenigde Staten.", + "cake_pay_account_note": "Maak een account aan om de beschikbare kaarten te zien. Sommige zijn zelfs met korting verkrijgbaar!", + "already_have_account": "Heb je al een account?", + "create_account": "Account aanmaken", + "privacy_policy": "Privacybeleid", + "welcome_to_cakepay": "Welkom bij Cake Pay!", + "sign_up": "Aanmelden", + "forgot_password": "Wachtwoord vergeten", + "reset_password": "Wachtwoord resetten", + "gift_cards": "Cadeaubonnen", + "setup_your_debit_card": "Stel uw debetkaart in", + "no_id_required": "Geen ID vereist. Opwaarderen en overal uitgeven", + "how_to_use_card": "Hoe deze kaart te gebruiken", + "purchase_gift_card": "Cadeaubon kopen", + "verification": "Verificatie", + "fill_code": "Vul de verificatiecode in die u in uw e-mail hebt ontvangen", + "dont_get_code": "Geen code?", + "resend_code": "Stuur het alstublieft opnieuw", + "debit_card": "Debetkaart", + "cakepay_prepaid_card": "CakePay Prepaid Debetkaart", + "no_id_needed": "Geen ID nodig!", + "frequently_asked_questions": "Veelgestelde vragen", + "debit_card_terms": "De opslag en het gebruik van uw betaalkaartnummer (en inloggegevens die overeenkomen met uw betaalkaartnummer) in deze digitale portemonnee zijn onderworpen aan de Algemene voorwaarden van de toepasselijke kaarthouderovereenkomst met de uitgever van de betaalkaart, zoals van kracht vanaf tijd tot tijd.", + "please_reference_document": "Raadpleeg de onderstaande documenten voor meer informatie.", + "cardholder_agreement": "Kaarthouderovereenkomst", + "e_sign_consent": "Toestemming e-ondertekenen", + "agree_and_continue": "Akkoord & doorgaan", + "email_address": "E-mailadres", + "agree_to": "Door een account aan te maken gaat u akkoord met de ", + "and": "en", + "enter_code": "Voer code in", + "congratulations": "gefeliciteerd!", + "you_now_have_debit_card": "Je hebt nu een debetkaart", + "min_amount" : "Min: ${value}", + "max_amount" : "Max: ${value}", + "enter_amount": "Voer Bedrag in", + "billing_address_info": "Als u om een ​​factuuradres wordt gevraagd, geef dan uw verzendadres op", + "order_physical_card": "Fysieke kaart bestellen", + "add_value": "Waarde toevoegen", + "activate": "Activeren", + "get_a": "Krijg een ", + "digital_and_physical_card": "digitale und physische Prepaid-Debitkarte", + "get_card_note": " die Sie mit digitaler Währung aufladen können. Keine zusätzlichen Informationen erforderlich!", + "signup_for_card_accept_terms": "Melden Sie sich für die Karte an und akzeptieren Sie die Bedingungen.", + "add_fund_to_card": "Prepaid-Guthaben zu den Karten hinzufügen (bis zu ${value})", + "use_card_info_two": "Guthaben werden auf dem Prepaid-Konto in USD umgerechnet, nicht in digitale Währung.", + "use_card_info_three": "Verwenden Sie die digitale Karte online oder mit kontaktlosen Zahlungsmethoden.", + "optional_order_card": "Optional eine physische Karte bestellen.", + "hide_details": "Details ausblenden", + "show_details": "Details anzeigen", + "upto": "bis zu ${value}", + "discount": "${value} % sparen", + "gift_card_amount": "Gutscheinbetrag", + "bill_amount": "Rechnungsbetrag", + "you_pay": "Sie bezahlen", + "tip": "Hinweis:", + "custom": "benutzerdefiniert", + "by_cake_pay": "von Cake Pay", + "expires": "Läuft ab", + "mm": "MM", + "yy": "YY", + "online": "online", + "offline": "offline", + "gift_card_number": "Geschenkkartennummer", + "pin_number": "PIN-Nummer", + "total_saving": "Gesamteinsparungen", + "last_30_days": "Letzte 30 Tage", + "avg_savings": "Durchschn. Einsparungen", + "view_all": "Alle anzeigen", + "active_cards": "Aktive Karten", + "delete_account": "Konto löschen", + "cards": "Karten", + "active": "Aktiv", + "redeemed": "Versilbert", + "gift_card_balance_note": "Geschenkkarten mit Restguthaben erscheinen hier", + "gift_card_redeemed_note": "Gutscheine, die Sie eingelöst haben, werden hier angezeigt", + "abmelden": "Abmelden", + "add_tip": "Tipp hinzufügen", + "percentageOf": "von ${amount}", + "is_percentage": "ist", + "search_category": "Suchkategorie", + "mark_as_redeemed": "Als eingelöst markieren", + "more_options": "Weitere Optionen", + "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", + "transaction_sent_notice": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie einen Block-Explorer und Ihre E-Mail.", + "agree": "stimme zu", + "in_store": "Im Geschäft", + "generating_gift_card": "Geschenkkarte wird erstellt", + "payment_was_received": "Ihre Zahlung ist eingegangen.", + "proceed_after_one_minute": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie bitte Ihre E-Mail.", + "order_id": "Order-ID", + "gift_card_is_generated": "Cadeaukaart is gegenereerd", + "open_gift_card": "Geschenkkaart openen", + "contact_support": "Contact opnemen met ondersteuning", + "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 0fa9ac3a3..be58b6a17 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -534,5 +534,103 @@ "search_currency": "Wyszukaj walutę", "new_template" : "Nowy szablon", "electrum_address_disclaimer": "Za każdym razem, gdy korzystasz z jednego z nich, generujemy nowe adresy, ale poprzednie adresy nadal działają", - "wallet_name_exists": "Portfel o tej nazwie już istnieje" + "wallet_name_exists": "Portfel o tej nazwie już istnieje", + "market_place": "Rynek", + "cake_pay_title": "Karty podarunkowe Cake Pay", + "cake_pay_subtitle": "Kup karty podarunkowe i wykorzystaj je natychmiast", + "about_cake_pay": "Cake Pay umożliwia łatwe kupowanie kart podarunkowych z wirtualnymi aktywami, które można natychmiast wydać u ponad 150 000 sprzedawców w Stanach Zjednoczonych.", + "cake_pay_account_note": "Załóż konto, aby zobaczyć dostępne karty. Niektóre są nawet dostępne ze zniżką!", + "already_have_account": "Masz już konto?", + "create_account": "Utwórz konto", + "privacy_policy": "Polityka prywatności", + "welcome_to_cakepay": "Witamy w Cake Pay!", + "sign_up": "Zarejestruj się", + "forgot_password": "Zapomniałem hasła", + "reset_password": "Zresetuj hasło", + "gift_cards": "Karty podarunkowe", + "setup_your_debit_card": "Skonfiguruj swoją kartę debetową", + "no_id_required": "Nie wymagamy ID. Doładuj i wydawaj gdziekolwiek", + "how_to_use_card": "Jak korzystać z tej karty", + "purchase_gift_card": "Kup kartę podarunkową", + "verification": "Weryfikacja", + "fill_code": "Proszę wpisać kod weryfikacyjny podany w wiadomości e-mail", + "dont_get_code": "Nie odbierasz kodu?", + "resend_code": "Wyślij go ponownie", + "debit_card": "Karta debetowa", + "cakepay_prepaid_card": "Przedpłacona karta debetowa CakePay", + "no_id_needed": "Nie potrzeba ID!", + "frequently_asked_questions": "Często zadawane pytania", + "debit_card_terms": "Przechowywanie i używanie numeru karty płatniczej (oraz danych uwierzytelniających odpowiadających numerowi karty płatniczej) w tym portfelu cyfrowym podlega Warunkom odpowiedniej umowy posiadacza karty z wydawcą karty płatniczej, zgodnie z obowiązującym od od czasu do czasu.", + "please_reference_document": "Proszę odwołać się do poniższych dokumentów, aby uzyskać więcej informacji.", + "cardholder_agreement": "Umowa posiadacza karty", + "e_sign_consent": "Zgoda na podpis elektroniczny", + "agree_and_continue": "Zgadzam się i kontynuuj", + "email_address": "Adres e-mail", + "agree_to": "Tworząc konto wyrażasz zgodę na ", + "and": "i", + "enter_code": "Wprowadź kod", + "congratulations": "gratulacje!", + "you_now_have_debit_card": "Masz teraz kartę debetową", + "min_amount" : "Min: ${value}", + "max_amount" : "Max: ${value}", + "enter_amount": "Wprowadź kwotę", + "billing_address_info": "Jeśli zostaniesz poproszony o podanie adresu rozliczeniowego, podaj swój adres wysyłki", + "order_physical_card": "Zamów kartę fizyczną", + "add_value": "Dodaj wartość", + "activate": "Aktywuj", + "get_a": "Zdobądź ", + "digital_and_physical_card": " cyfrowa i fizyczna przedpłacona karta debetowa", + "get_card_note": " które możesz doładować walutami cyfrowymi. Nie są potrzebne żadne dodatkowe informacje!", + "signup_for_card_accept_terms": "Zarejestruj się, aby otrzymać kartę i zaakceptuj warunki.", + "add_fund_to_card": "Dodaj przedpłacone środki do kart (do ${value})", + "use_card_info_two": "Środki są przeliczane na USD, gdy są przechowywane na koncie przedpłaconym, a nie w walutach cyfrowych.", + "use_card_info_three": "Użyj cyfrowej karty online lub za pomocą zbliżeniowych metod płatności.", + "optionally_order_card": "Opcjonalnie zamów kartę fizyczną.", + "hide_details" : "Ukryj szczegóły", + "show_details" : "Pokaż szczegóły", + "upto": "do ${value}", + "discount": "Zaoszczędź ${value}%", + "gift_card_amount": "Kwota karty podarunkowej", + "bill_amount": "Kwota rachunku", + "you_pay": "Płacisz", + "tip": "wskazówka:", + "custom": "niestandardowy", + "by_cake_pay": "przez Cake Pay", + "expires": "Wygasa", + "mm": "MM", + "yy": "RR", + "online": "online", + "offline": "Offline", + "gift_card_number": "Numer karty podarunkowej", + "pin_number": "Numer PIN", + "total_saving": "Całkowite oszczędności", + "last_30_days": "Ostatnie 30 dni", + "avg_savings": "Śr. oszczędności", + "view_all": "Wyświetl wszystko", + "active_cards": "Aktywne karty", + "delete_account": "Usuń konto", + "cards": "Karty", + "active": "Aktywny", + "redeemed": "wykupione", + "gift_card_balance_note": "Tutaj pojawią się karty podarunkowe z pozostałym saldem", + "gift_card_redeemed_note": "Karty podarunkowe, które wykorzystałeś, pojawią się tutaj", + "logout": "Wyloguj", + "add_tip": "Dodaj wskazówkę", + "percentageOf": "z ${amount}", + "is_percentage": "jest", + "search_category": "Kategoria wyszukiwania", + "mark_as_redeemed": "Oznacz jako wykorzystany", + "more_options": "Więcej opcji", + "awaiting_payment_confirmation": "Oczekiwanie na potwierdzenie płatności", + "transaction_sent_notice": "Jeśli ekran nie pojawi się po 1 minucie, sprawdź eksplorator bloków i swój e-mail.", + "agree": "Zgadzam się", + "in_store": "W Sklepie", + "generating_gift_card": "Generowanie karty podarunkowej", + "payment_was_received": "Twoja płatność została otrzymana.", + "proceed_after_one_minute": "Jeśli ekran nie przejdzie dalej po 1 minucie, sprawdź pocztę.", + "order_id": "Identyfikator zamówienia", + "gift_card_is_generated": "Karta podarunkowa jest generowana", + "open_gift_card": "Otwórz kartę podarunkową", + "contact_support": "Skontaktuj się z pomocą techniczną", + "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 558884ca8..50aa25100 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -534,5 +534,103 @@ "search_currency": "Pesquisar moeda", "new_template" : "Novo modelo", "electrum_address_disclaimer": "Geramos novos endereços cada vez que você usa um, mas os endereços anteriores continuam funcionando", - "wallet_name_exists": "A carteira com esse nome já existe" + "wallet_name_exists": "A carteira com esse nome já existe", + "market_place": "Mercado", + "cake_pay_title": "Cartões de presente de pagamento de bolo", + "cake_pay_subtitle": "Compre vales-presente e resgate instantaneamente", + "about_cake_pay": "O Cake Pay permite que você compre facilmente cartões-presente com ativos virtuais, que podem ser gastos instantaneamente em mais de 150.000 comerciantes nos Estados Unidos.", + "cake_pay_account_note": "Faça uma conta para ver os cartões disponíveis. Alguns estão até com desconto!", + "already_have_account": "Já tem uma conta?", + "create_account": "Criar conta", + "privacy_policy": "Política de privacidade", + "welcome_to_cakepay": "Bem-vindo ao Cake Pay!", + "create_account": "Registar-se", + "forgot_password": "Esqueci a senha", + "reset_password": "Redefinir senha", + "gift_cards": "Cartões de presente", + "setup_your_debit_card": "Configure seu cartão de débito", + "no_id_required": "Não é necessário ID. Recarregue e gaste em qualquer lugar", + "how_to_use_card": "Como usar este cartão", + "purchase_gift_card": "Comprar vale-presente", + "verification": "Verificação", + "fill_code": "Por favor, preencha o código de verificação fornecido ao seu e-mail", + "dont_get_code": "Não recebeu o código?", + "resend_code": "Por favor, reenvie", + "debit_card": "Cartão de débito", + "cakepay_prepaid_card": "Cartão de débito pré-pago CakePay", + "no_id_needed": "Nenhum ID necessário!", + "frequently_asked_questions": "Perguntas frequentes", + "debit_card_terms": "O armazenamento e uso do número do cartão de pagamento (e credenciais correspondentes ao número do cartão de pagamento) nesta carteira digital estão sujeitos aos Termos e Condições do contrato do titular do cartão aplicável com o emissor do cartão de pagamento, em vigor a partir de tempo ao tempo.", + "please_reference_document": "Por favor, consulte os documentos abaixo para mais informações.", + "cardholder_agreement": "Acordo do titular do cartão", + "e_sign_consent": "Consentimento de assinatura eletrônica", + "agree_and_continue": "Concordar e continuar", + "email_address": "Endereço de e-mail", + "agree_to": "Ao criar conta você concorda com ", + "and": "e", + "enter_code": "Digite o código", + "congratulations": "Parabéns!", + "you_now_have_debit_card": "Agora você tem um cartão de débito", + "min_amount" : "Mínimo: ${valor}", + "max_amount" : "Máx.: ${valor}", + "enter_amount": "Digite o valor", + "billing_address_info": "Se for solicitado um endereço de cobrança, forneça seu endereço de entrega", + "order_physical_card": "Pedir Cartão Físico", + "add_value": "Adicionar valor", + "activate": "Ativar", + "get_a": "Obter um ", + "digital_and_physical_card": "cartão de débito pré-pago digital e físico", + "get_card_note": " que você pode recarregar com moedas digitais. Nenhuma informação adicional é necessária!", + "signup_for_card_accept_terms": "Cadastre-se no cartão e aceite os termos.", + "add_fund_to_card": "Adicionar fundos pré-pagos aos cartões (até ${value})", + "use_card_info_two": "Os fundos são convertidos para USD quando mantidos na conta pré-paga, não em moedas digitais.", + "use_card_info_three": "Use o cartão digital online ou com métodos de pagamento sem contato.", + "opcionalmente_order_card": "Opcionalmente, peça um cartão físico.", + "hide_details" : "Ocultar detalhes", + "show_details" : "Mostrar detalhes", + "upto": "até ${value}", + "discount": "Economize ${value}%", + "gift_card_amount": "Valor do Cartão Presente", + "bill_amount": "Valor da conta", + "you_pay": "Você paga", + "tip": "Dica:", + "custom": "personalizado", + "by_cake_pay": "por Cake Pay", + "expires": "Expira", + "mm": "MM", + "yy": "aa", + "online": "Online", + "offline": "offline", + "gift_card_number": "Número do cartão-presente", + "pin_number": "Número PIN", + "total_saving": "Economia total", + "last_30_days": "Últimos 30 dias", + "avg_savings": "Poupança média", + "view_all": "Ver todos", + "active_cards": "Cartões ativos", + "delete_account": "Excluir conta", + "cards": "Cartões", + "active": "Ativo", + "redeemed": "Resgatado", + "gift_card_balance_note": "Os cartões-presente com saldo restante aparecerão aqui", + "gift_card_redeemed_note": "Os cartões-presente que você resgatou aparecerão aqui", + "logout": "Logout", + "add_tip": "Adicionar Dica", + "percentageOf": "de ${amount}", + "is_percentage": "é", + "search_category": "Categoria de pesquisa", + "mark_as_redemed": "Marcar como resgatado", + "more_options": "Mais opções", + "waiting_payment_confirmation": "Aguardando confirmação de pagamento", + "transaction_sent_notice": "Se a tela não prosseguir após 1 minuto, verifique um explorador de blocos e seu e-mail.", + "agree": "Concordo", + "in_store": "Na loja", + "generating_gift_card": "Gerando Cartão Presente", + "payment_was_received": "Seu pagamento foi recebido.", + "proceed_after_one_minute": "Se a tela não prosseguir após 1 minuto, verifique seu e-mail.", + "order_id": "ID do pedido", + "gift_card_is_generated": "Cartão presente é gerado", + "open_gift_card": "Abrir vale-presente", + "contact_support": "Contatar Suporte", + "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1dbc473ab..ce8109512 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -534,5 +534,103 @@ "search_currency": "Валюта поиска", "new_template" : "Новый шаблон", "electrum_address_disclaimer": "Мы генерируем новые адреса каждый раз, когда вы их используете, но предыдущие адреса продолжают работать.", - "wallet_name_exists": "Кошелек с таким именем уже существует" + "wallet_name_exists": "Кошелек с таким именем уже существует", + "market_place": "Торговая площадка", + "cake_pay_title": "Подарочные карты Cake Pay", + "cake_pay_subtitle": "Купите подарочные карты и моментально погасите их", + "about_cake_pay": "Cake Pay позволяет вам легко покупать подарочные карты с виртуальными активами, которые можно мгновенно потратить в более чем 150 000 продавцов в Соединенных Штатах.", + "cake_pay_account_note": "Создайте учетную запись, чтобы увидеть доступные карты. Некоторые даже доступны со скидкой!", + "already_have_account": "У вас уже есть аккаунт?", + "create_account": "Создать аккаунт", + "privacy_policy": "Политика конфиденциальности", + "welcome_to_cakepay": "Добро пожаловать в Cake Pay!", + "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": "Предоплаченная дебетовая карта CakePay", + "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": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.", + "optionly_order_card": "При желании закажите физическую карту.", + "hide_details": "Скрыть детали", + "show_details": "Показать детали", + "upto": "до ${value}", + "discount": "Сэкономьте ${value}%", + "gift_card_amount": "Сумма подарочной карты", + "bill_amount": "Сумма счета", + "you_pay": "Вы платите", + "tip": "Совет:", + "custom": "обычай", + "by_cake_pay": "от 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": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin." } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index a152e35a8..04e270d57 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -533,5 +533,103 @@ "search_currency": "Шукати валюту", "new_template" : "Новий шаблон", "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", - "wallet_name_exists": "Гаманець з такою назвою вже існує" + "wallet_name_exists": "Гаманець з такою назвою вже існує", + "market_place": "Ринок", + "cake_pay_title": "Подарункові картки Cake Pay", + "cake_pay_subtitle": "Купуйте подарункові картки та використовуйте їх миттєво", + "about_cake_pay": "Cake Pay дозволяє вам легко купувати подарункові картки з віртуальними активами, які можна миттєво витратити в понад 150 000 продавців у Сполучених Штатах.", + "cake_pay_account_note": "Створіть обліковий запис, щоб побачити доступні картки. Деякі навіть доступні зі знижкою!", + "already_have_account": "Вже є обліковий запис?", + "create_account": "Створити обліковий запис", + "privacy_policy": "Політика конфіденційності", + "welcome_to_cakepay": "Ласкаво просимо до Cake Pay!", + "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": "Передплачена дебетова картка CakePay", + "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": "від Cake Pay", + "expires": "Закінчується", + "mm": "MM", + "yy": "YY", + "online": "Онлайн", + "offline": "Офлайн", + "gift_card_number": "Номер подарункової картки", + "pin_number": "PIN-код", + "total_saving": "Загальна економія", + "last_30_days": "Останні 30 днів", + "avg_savings": "Середня економія", + "view_all": "Переглянути все", + "active_cards": "Активні картки", + "delete_account": "Видалити обліковий запис", + "cards": "Картки", + "active": "Активний", + "redeeded": "Викуплено", + "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": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 9b9fe7a79..14af48137 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -532,5 +532,103 @@ "search_currency": "搜索货币", "new_template" : "新模板", "electrum_address_disclaimer": "每次您使用一个地址时,我们都会生成新地址,但之前的地址仍然有效", - "wallet_name_exists": "同名的钱包已经存在" + "wallet_name_exists": "同名的钱包已经存在", + "market_place": "市场", + "cake_pay_title": "Cake Pay 礼品卡", + "cake_pay_subtitle": "购买礼品卡并立即兑换", + "about_cake_pay": "Cake Pay 让您可以轻松购买带有虚拟资产的礼品卡,可立即在美国超过 150,000 家商家消费。", + "cake_pay_account_note": "注册一个账户来查看可用的卡片。有些甚至可以打折!", + "already_have_account": "已经有账号了?", + "create_account": "创建账户", + "privacy_policy": "隐私政策", + "welcome_to_cakepay": "欢迎来到 Cake Pay!", + "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": "CakePay 预付借记卡", + "no_id_needed": "不需要 ID!", + "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": "通过 Cake Pay", + "expires": "过期", + "mm": "毫米", + "yy": "YY", + "online": "在线", + "offline": "离线", + "gift_card_number": "礼品卡号", + "pin_number": "PIN 码", + "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": "目前只能通过门罗币、比特币和莱特币购买礼品卡" } diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 9245c9c1f..4345c0230 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -20,8 +20,8 @@ MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.3" -CAKEWALLET_BUILD_NUMBER=105 +CAKEWALLET_VERSION="4.4.4" +CAKEWALLET_BUILD_NUMBER=108 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/android/build_monero.sh b/scripts/android/build_monero.sh index 0a9ce08ff..5634aa20a 100755 --- a/scripts/android/build_monero.sh +++ b/scripts/android/build_monero.sh @@ -1,7 +1,7 @@ #!/bin/sh . ./config.sh -MONERO_BRANCH=v0.17.3.0-android +MONERO_BRANCH=release-v0.17.3.2-android MONERO_SRC_DIR=${WORKDIR}/monero git clone https://github.com/cake-tech/monero.git ${MONERO_SRC_DIR} --branch ${MONERO_BRANCH} diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index de6d2adf8..99f306343 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -18,8 +18,8 @@ MONERO_COM_BUILD_NUMBER=17 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.4.3" -CAKEWALLET_BUILD_NUMBER=104 +CAKEWALLET_VERSION="4.4.4" +CAKEWALLET_BUILD_NUMBER=109 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/ios/build_monero.sh b/scripts/ios/build_monero.sh index 2d9d32fb0..ea29c7131 100755 --- a/scripts/ios/build_monero.sh +++ b/scripts/ios/build_monero.sh @@ -2,9 +2,9 @@ . ./config.sh -MONERO_URL="https://github.com/monero-project/monero.git" +MONERO_URL="https://github.com/cake-tech/monero.git" MONERO_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/monero" -MONERO_VERSION=tags/v0.17.3.0 +MONERO_VERSION=release-v0.17.3.2 BUILD_TYPE=release PREFIX=${EXTERNAL_IOS_DIR} DEST_LIB_DIR=${EXTERNAL_IOS_LIB_DIR}/monero diff --git a/tool/configure.dart b/tool/configure.dart index dcbac59c5..cfa675bd6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -77,7 +77,8 @@ abstract class Bitcoin { TransactionPriority deserializeBitcoinTransactionPriority(int raw); int getFeeRate(Object wallet, TransactionPriority priority); Future generateNewAddress(Object wallet); - Object createBitcoinTransactionCredentials(List outputs, TransactionPriority priority); + Object createBitcoinTransactionCredentials(List outputs, {TransactionPriority priority, int feeRate}); + Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority priority, int feeRate}); List getAddresses(Object wallet); String getAddress(Object wallet); @@ -146,6 +147,7 @@ import 'package:cw_monero/mnemonics/spanish.dart'; import 'package:cw_monero/mnemonics/portuguese.dart'; import 'package:cw_monero/mnemonics/french.dart'; import 'package:cw_monero/mnemonics/italian.dart'; +import 'package:cw_monero/pending_monero_transaction.dart'; """; const moneroCwPart = "part 'cw_monero.dart';"; const moneroContent = """ @@ -229,6 +231,7 @@ abstract class Monero { WalletCredentials createMoneroNewWalletCredentials({String name, String password, String language}); Map getKeys(Object wallet); Object createMoneroTransactionCreationCredentials({List outputs, TransactionPriority priority}); + Object createMoneroTransactionCreationCredentialsRaw({List outputs, TransactionPriority priority}); String formatterMoneroAmountToString({int amount}); double formatterMoneroAmountToDouble({int amount}); int formatterMoneroParseAmount({String amount}); @@ -237,6 +240,7 @@ abstract class Monero { void onStartup(); int getTransactionInfoAccountId(TransactionInfo tx); WalletService createMoneroWalletService(Box walletInfoSource); + Map pendingTransactionInfo(Object transaction); } abstract class MoneroSubaddressList {