Payment URI

This commit is contained in:
Hanh 2021-09-25 17:09:41 +08:00
parent 48dfe10e14
commit 33bcca6447
26 changed files with 342 additions and 18 deletions

View File

@ -13,6 +13,7 @@ import 'package:local_auth/local_auth.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:warp/payment_uri.dart';
import 'package:warp/store.dart';
import 'package:warp_api/warp_api.dart';
@ -387,8 +388,9 @@ class _AccountPageState extends State<AccountPage>
);
}
_onReceive() {
showQR(context, _address());
_onReceive() async {
await showDialog(context: context, builder: (context) =>
AlertDialog(title: Text(S.of(context).receivePayment), content: PaymentURIPage(_address())));
}
_unconfirmedStyle() {

View File

@ -92,7 +92,6 @@ class LineChartTimeSeriesState extends State<LineChartTimeSeries> {
color: theme.primaryColor,
fontWeight: FontWeight.bold,
fontSize: 12),
// checkToShowTitle: (min, max, _, interval, v) => v != max,
getTitles: (v) {
final dt = DateTime.fromMillisecondsSinceEpoch(v.toInt() * DAY_MS);
return DateFormat.Md().format(dt);

View File

@ -73,6 +73,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Amount must be a number"),
"amountMustBePositive":
MessageLookupByLibrary.simpleMessage("Amount must be positive"),
"amountTooHigh":
MessageLookupByLibrary.simpleMessage("Amount too high"),
"approve": MessageLookupByLibrary.simpleMessage("APPROVE"),
"areYouSureYouWantToDeleteThisContact":
MessageLookupByLibrary.simpleMessage(
@ -176,6 +178,8 @@ class MessageLookup extends MessageLookupByLibrary {
"purple": MessageLookupByLibrary.simpleMessage("Purple"),
"qty": MessageLookupByLibrary.simpleMessage("Qty"),
"realized": MessageLookupByLibrary.simpleMessage("Realized"),
"receivePayment":
MessageLookupByLibrary.simpleMessage("Receive a payment"),
"rescan": MessageLookupByLibrary.simpleMessage("Rescan"),
"rescanRequested":
MessageLookupByLibrary.simpleMessage("Rescan Requested..."),

View File

@ -73,6 +73,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Cantidad debe ser un número"),
"amountMustBePositive": MessageLookupByLibrary.simpleMessage(
"Cantidad debe ser un positivo"),
"amountTooHigh":
MessageLookupByLibrary.simpleMessage("Amount too high"),
"approve": MessageLookupByLibrary.simpleMessage("APROBAR"),
"areYouSureYouWantToDeleteThisContact":
MessageLookupByLibrary.simpleMessage(
@ -176,6 +178,8 @@ class MessageLookup extends MessageLookupByLibrary {
"purple": MessageLookupByLibrary.simpleMessage("Morada"),
"qty": MessageLookupByLibrary.simpleMessage("Cantidad"),
"realized": MessageLookupByLibrary.simpleMessage("Dio Cuenta"),
"receivePayment":
MessageLookupByLibrary.simpleMessage("Receive a payment"),
"rescan": MessageLookupByLibrary.simpleMessage("Escanear"),
"rescanRequested":
MessageLookupByLibrary.simpleMessage("Escaneo solicitado…"),

View File

@ -74,6 +74,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Le montant doit être un nombre"),
"amountMustBePositive": MessageLookupByLibrary.simpleMessage(
"Le montant doit être positif"),
"amountTooHigh":
MessageLookupByLibrary.simpleMessage("Amount too high"),
"approve": MessageLookupByLibrary.simpleMessage("APPROUVER"),
"areYouSureYouWantToDeleteThisContact":
MessageLookupByLibrary.simpleMessage(
@ -176,6 +178,8 @@ class MessageLookup extends MessageLookupByLibrary {
"purple": MessageLookupByLibrary.simpleMessage("Purple"),
"qty": MessageLookupByLibrary.simpleMessage("Quantité"),
"realized": MessageLookupByLibrary.simpleMessage("Réalisé"),
"receivePayment":
MessageLookupByLibrary.simpleMessage("Recevoir un payment"),
"rescan": MessageLookupByLibrary.simpleMessage("Parcourir à nouveau"),
"rescanRequested":
MessageLookupByLibrary.simpleMessage("Parcours demandé..."),

View File

@ -64,6 +64,8 @@ class MessageLookup extends MessageLookupByLibrary {
"amountMustBeANumber":
MessageLookupByLibrary.simpleMessage("請輸入數字(數量)"),
"amountMustBePositive": MessageLookupByLibrary.simpleMessage("數量必須為正數"),
"amountTooHigh":
MessageLookupByLibrary.simpleMessage("Amount too high"),
"approve": MessageLookupByLibrary.simpleMessage("同意"),
"areYouSureYouWantToDeleteThisContact":
MessageLookupByLibrary.simpleMessage(
@ -153,6 +155,8 @@ class MessageLookup extends MessageLookupByLibrary {
"purple": MessageLookupByLibrary.simpleMessage("Purple"),
"qty": MessageLookupByLibrary.simpleMessage("數量"),
"realized": MessageLookupByLibrary.simpleMessage("已獲利"),
"receivePayment":
MessageLookupByLibrary.simpleMessage("Receive a payment"),
"rescan": MessageLookupByLibrary.simpleMessage("重新掃描"),
"rescanRequested": MessageLookupByLibrary.simpleMessage("已經收到重新掃描要求"),
"rescanWalletFromTheFirstBlock":

View File

@ -1431,6 +1431,26 @@ class S {
args: [],
);
}
/// `Receive a payment`
String get receivePayment {
return Intl.message(
'Receive a payment',
name: 'receivePayment',
desc: '',
args: [],
);
}
/// `Amount too high`
String get amountTooHigh {
return Intl.message(
'Amount too high',
name: 'amountTooHigh',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@ -136,5 +136,7 @@
"gold": "Gold",
"purple": "Purple",
"noRecipient": "No Recipient",
"addARecipientAndItWillShowHere": "Add a recipient and it will show here"
"addARecipientAndItWillShowHere": "Add a recipient and it will show here",
"receivePayment": "Receive a payment",
"amountTooHigh": "Amount too high"
}

View File

@ -136,5 +136,7 @@
"gold": "Oro",
"purple": "Morada",
"noRecipient": "Sin Destinatario",
"addARecipientAndItWillShowHere": "Agregar un destinatario y se mostrará aquí"
"addARecipientAndItWillShowHere": "Agregar un destinatario y se mostrará aquí",
"receivePayment": "Receive a payment",
"amountTooHigh": "Amount too high"
}

View File

@ -136,5 +136,7 @@
"gold": "Gold",
"purple": "Purple",
"noRecipient": "Pas de receveur",
"addARecipientAndItWillShowHere": "Ajoutez un receveur et il sera ici"
"addARecipientAndItWillShowHere": "Ajoutez un receveur et il sera ici",
"receivePayment": "Recevoir un payment",
"amountTooHigh": "Amount too high"
}

View File

@ -132,5 +132,7 @@
"gold": "Gold",
"purple": "Purple",
"noRecipient": "No Recipient",
"addARecipientAndItWillShowHere": "Add a recipient and it will show here"
"addARecipientAndItWillShowHere": "Add a recipient and it will show here",
"receivePayment": "Receive a payment",
"amountTooHigh": "Amount too high"
}

View File

@ -9,7 +9,9 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:rate_my_app/rate_my_app.dart';
import 'package:sqflite/sqflite.dart';
import 'package:warp/payment_uri.dart';
import 'package:warp_api/warp_api.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
@ -32,6 +34,7 @@ const ZECUNIT = 100000000.0;
var ZECUNIT_DECIMAL = Decimal.parse('100000000');
const mZECUNIT = 100000;
const DEFAULT_FEE = 1000;
const MAXMONEY = 21000000;
var accountManager = AccountManager();
var priceStore = PriceStore();
@ -101,6 +104,23 @@ class ZWalletApp extends StatefulWidget {
class ZWalletAppState extends State<ZWalletApp> {
bool initialized = false;
RateMyApp rateMyApp = RateMyApp(
preferencesPrefix: 'rateMyApp_',
minDays: 0,
minLaunches: 5,
);
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) async {
await rateMyApp.init();
if (mounted && rateMyApp.shouldOpenDialog) {
rateMyApp.showRateDialog(this.context);
}
});
}
Future<bool> _init() async {
if (!initialized) {
initialized = true;
@ -272,6 +292,14 @@ double parseNumber(String? s) {
return NumberFormat.currency().parse(s).toDouble();
}
bool checkNumber(String s) {
try {
NumberFormat.currency().parse(s);
}
on FormatException { return false; }
return true;
}
int precision(bool mZEC) => mZEC ? 3 : 8;
Future<String?> scanCode(BuildContext context) async {

View File

@ -207,8 +207,9 @@ class PayRecipientState extends State<PayRecipient> {
}
String? _checkAmount(String? vs) {
if (vs == null) return S.of(context).amountMustBeANumber;
if (checkNumber(vs)) return S.of(context).amountMustBeANumber;
final v = parseNumber(vs);
if (v == null) return S.of(context).amountMustBeANumber;
if (v <= 0.0) return S.of(context).amountMustBePositive;
return null;
}

99
lib/payment_uri.dart Normal file
View File

@ -0,0 +1,99 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:warp_api/warp_api.dart';
import 'main.dart';
import 'generated/l10n.dart';
class PaymentURIPage extends StatefulWidget {
final String address;
PaymentURIPage(this.address);
@override
PaymentURIState createState() => PaymentURIState();
}
class PaymentURIState extends State<PaymentURIPage> {
var _amountController = TextEditingController(text: '');
var _memoController = TextEditingController(text: '');
var qrText = "";
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
qrText = widget.address;
}
@override
Widget build(BuildContext context) {
final qrSize = getScreenSize(context) / 1.5;
return Container(
width: double.maxFinite,
child: Form(
key: _formKey,
child:
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
QrImage(
data: qrText, size: qrSize, backgroundColor: Colors.white),
Padding(padding: EdgeInsets.all(8)),
TextFormField(
decoration: InputDecoration(labelText: 'Amount Requested'),
controller: _amountController,
keyboardType: TextInputType.number,
inputFormatters: [makeInputFormatter(true)],
validator: _checkAmount,
),
TextFormField(
decoration: InputDecoration(labelText: S.of(context).memo),
minLines: 4,
maxLines: null,
keyboardType: TextInputType.multiline,
controller: _memoController,
),
Padding(padding: EdgeInsets.all(8)),
ButtonBar(children: [
ElevatedButton.icon(
icon: Icon(Icons.build),
label: Text('MAKE QR'),
onPressed: _ok,
),
]),
])));
}
String? _checkAmount(String? vs) {
if (vs == null || vs.isEmpty) return null;
if (!checkNumber(vs)) return S.of(context).amountMustBeANumber;
final a = parseNumber(vs);
if (a >= MAXMONEY) return S.of(context).amountTooHigh;
return null;
}
void _updateQR() {
final amount = _amountController.text;
final memo = _memoController.text;
final String _qrText;
if (amount.isNotEmpty) {
final a = (Decimal.parse(amount) * ZECUNIT_DECIMAL).toInt();
_qrText = WarpApi.makePaymentURI(widget.address, a, memo);
} else
_qrText = widget.address;
setState(() {
qrText = _qrText;
});
}
void _ok() {
final form = _formKey.currentState!;
if (form.validate()) {
form.save();
_updateQR();
}
}
}

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
@ -210,6 +211,7 @@ class SendState extends State<SendPage> {
String? _checkAmount(String? vs, { bool isFiat: false }) {
if (vs == null) return S.of(context).amountMustBeANumber;
if (!checkNumber(vs)) return S.of(context).amountMustBeANumber;
final v = parseNumber(vs);
if (v < 0.0) return S.of(context).amountMustBePositive;
if (!isFiat && v == 0.0) return S.of(context).amountMustBePositive;
@ -220,6 +222,7 @@ class SendState extends State<SendPage> {
String? _checkMaxAmountPerNote(String? vs) {
if (vs == null) return S.of(context).amountMustBeANumber;
if (!checkNumber(vs)) return S.of(context).amountMustBeANumber;
final v = parseNumber(vs);
if (v < 0.0) return S.of(context).amountMustBePositive;
return null;
@ -259,11 +262,25 @@ class SendState extends State<SendPage> {
void _onScan() async {
final code = await scanCode(context);
if (code != null)
setState(() {
_address = code;
_addressController.text = _address;
});
if (code != null) {
if (_checkAddress(code) != null) {
final json = WarpApi.parsePaymentURI(code);
final payment = DecodedPaymentURI.fromJson(jsonDecode(json));
setState(() {
_address = payment.address;
_addressController.text = _address;
_memoController.text = payment.memo;
_amount = payment.amount;
_zecAmountController.text = amountFromZAT(_amount);
});
}
else {
setState(() {
_address = code;
_addressController.text = _address;
});
}
}
}
void _onAmount(String? vs) {
@ -368,6 +385,7 @@ class SendState extends State<SendPage> {
}
int amountInZAT(Decimal v) => (v * ZECUNIT_DECIMAL).toInt();
String amountFromZAT(int v) => (Decimal.fromInt(v) / ZECUNIT_DECIMAL).toString();
double _fx() {
return priceStore.zecPrice;

View File

@ -172,8 +172,8 @@ class SettingsState extends State<SettingsPage> {
String? _checkAmount(String? vs) {
if (vs == null) return S.of(context).amountMustBeANumber;
if (!checkNumber(vs)) return S.of(context).amountMustBeANumber;
final v = parseNumber(vs);
if (v == null) return S.of(context).amountMustBeANumber;
if (v < 0.0) return S.of(context).amountMustBePositive;
return null;
}

View File

@ -1168,3 +1168,17 @@ class SortConfig {
return '';
}
}
@JsonSerializable()
class DecodedPaymentURI {
String address;
int amount;
String memo;
DecodedPaymentURI(this.address, this.amount, this.memo);
factory DecodedPaymentURI.fromJson(Map<String, dynamic> json) =>
_$DecodedPaymentURIFromJson(json);
Map<String, dynamic> toJson() => _$DecodedPaymentURIToJson(this);
}

View File

@ -83,4 +83,8 @@ void delete_account(uint32_t account);
void truncate_data(void);
char *make_payment_uri(char *address, uint64_t amount, char *memo);
char *parse_payment_uri(char *uri);
void dummy_export(void);

View File

@ -425,3 +425,19 @@ pub fn delete_account(account: u32) {
};
log_result(res())
}
pub fn make_payment_uri(address: &str, amount: u64, memo: &str) -> String {
let res = || {
let uri = Wallet::make_payment_uri(address, amount, memo)?;
Ok(uri)
};
log_result(res())
}
pub fn parse_payment_uri(uri: &str) -> String {
let res = || {
let payment_json = Wallet::parse_payment_uri(uri)?;
Ok(payment_json)
};
log_result(res())
}

View File

@ -227,5 +227,24 @@ pub unsafe extern "C" fn truncate_data() {
api::truncate_data();
}
#[no_mangle]
pub unsafe extern "C" fn make_payment_uri(
address: *mut c_char,
amount: u64,
memo: *mut c_char,
) -> *mut c_char {
let address = CStr::from_ptr(address).to_string_lossy();
let memo = CStr::from_ptr(memo).to_string_lossy();
let uri = api::make_payment_uri(&address, amount, &memo);
CString::new(uri).unwrap().into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn parse_payment_uri(uri: *mut c_char) -> *mut c_char {
let uri = CStr::from_ptr(uri).to_string_lossy();
let payment_json = api::parse_payment_uri(&uri);
CString::new(payment_json).unwrap().into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn dummy_export() {}

@ -1 +1 @@
Subproject commit b8c41fe9b3227624ebf0e86c364be9c456ad3b5c
Subproject commit 7d37177bec06450682ffb975767416433ba1a9a6

View File

@ -227,6 +227,20 @@ class WarpApi {
static void deleteAccount(int account) {
warp_api_lib.delete_account(account);
}
static String makePaymentURI(String address, int amount, String memo) {
final uri = warp_api_lib.make_payment_uri(
address.toNativeUtf8().cast<Int8>(),
amount,
memo.toNativeUtf8().cast<Int8>());
return uri.cast<Utf8>().toDartString();
}
static String parsePaymentURI(String uri) {
final json = warp_api_lib.parse_payment_uri(
uri.toNativeUtf8().cast<Int8>());
return json.cast<Utf8>().toDartString();
}
}
String sendPaymentIsolateFn(PaymentParams params) {

View File

@ -434,6 +434,36 @@ class NativeLibrary {
late final _dart_truncate_data _truncate_data =
_truncate_data_ptr.asFunction<_dart_truncate_data>();
ffi.Pointer<ffi.Int8> make_payment_uri(
ffi.Pointer<ffi.Int8> address,
int amount,
ffi.Pointer<ffi.Int8> memo,
) {
return _make_payment_uri(
address,
amount,
memo,
);
}
late final _make_payment_uri_ptr =
_lookup<ffi.NativeFunction<_c_make_payment_uri>>('make_payment_uri');
late final _dart_make_payment_uri _make_payment_uri =
_make_payment_uri_ptr.asFunction<_dart_make_payment_uri>();
ffi.Pointer<ffi.Int8> parse_payment_uri(
ffi.Pointer<ffi.Int8> uri,
) {
return _parse_payment_uri(
uri,
);
}
late final _parse_payment_uri_ptr =
_lookup<ffi.NativeFunction<_c_parse_payment_uri>>('parse_payment_uri');
late final _dart_parse_payment_uri _parse_payment_uri =
_parse_payment_uri_ptr.asFunction<_dart_parse_payment_uri>();
void dummy_export() {
return _dummy_export();
}
@ -712,6 +742,26 @@ typedef _c_truncate_data = ffi.Void Function();
typedef _dart_truncate_data = void Function();
typedef _c_make_payment_uri = ffi.Pointer<ffi.Int8> Function(
ffi.Pointer<ffi.Int8> address,
ffi.Uint64 amount,
ffi.Pointer<ffi.Int8> memo,
);
typedef _dart_make_payment_uri = ffi.Pointer<ffi.Int8> Function(
ffi.Pointer<ffi.Int8> address,
int amount,
ffi.Pointer<ffi.Int8> memo,
);
typedef _c_parse_payment_uri = ffi.Pointer<ffi.Int8> Function(
ffi.Pointer<ffi.Int8> uri,
);
typedef _dart_parse_payment_uri = ffi.Pointer<ffi.Int8> Function(
ffi.Pointer<ffi.Int8> uri,
);
typedef _c_dummy_export = ffi.Void Function();
typedef _dart_dummy_export = void Function();

View File

@ -419,6 +419,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
flutter_rating_bar:
dependency: transitive
description:
name: flutter_rating_bar
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
flutter_svg:
dependency: "direct main"
description:
@ -814,6 +821,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
rate_my_app:
dependency: "direct main"
description:
name: rate_my_app
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
rational:
dependency: transitive
description:

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.10+148
version: 1.0.10+150
environment:
sdk: ">=2.12.0 <3.0.0"
@ -50,6 +50,7 @@ dependencies:
path_provider: ^2.0.3
file_picker: ^4.0.2
mustache_template: ^2.0.0
rate_my_app: ^1.1.1
flutter_palette: ^1.1.0+1
flutter_svg: ^0.22.0
flutter_typeahead: ^3.2.0
@ -79,7 +80,7 @@ dev_dependencies:
flutter_native_splash: ^1.2.3
flutter_app_name:
name: "WarpWallet"
name: "YWallet"
flutter_icons:
android: true

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.10+148
version: 1.0.10+150
environment:
sdk: ">=2.12.0 <3.0.0"
@ -50,6 +50,7 @@ dependencies:
path_provider: ^2.0.3
file_picker: ^4.0.2
mustache_template: ^2.0.0
rate_my_app: ^1.1.1
flutter_palette: ^1.1.0+1
flutter_svg: ^0.22.0
flutter_typeahead: ^3.2.0