This commit is contained in:
Hanh 2021-07-07 23:40:05 +08:00
parent b2b5b0072f
commit 2eb76f8232
20 changed files with 581 additions and 253 deletions

2
.gitignore vendored
View File

@ -45,6 +45,8 @@ app.*.map.json
/android/app/profile
/android/app/release
.gradle/
target/
jniLibs/
assets/

115
Cargo.lock generated
View File

@ -45,9 +45,9 @@ checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
[[package]]
name = "aho-corasick"
version = "0.7.15"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
@ -198,9 +198,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bitvec"
version = "0.19.5"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
dependencies = [
"funty",
"radium 0.5.3",
@ -291,9 +291,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
dependencies = [
"lazy_static",
"memchr",
@ -321,11 +321,11 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
[[package]]
name = "cast"
version = "0.2.6"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57cdfa5d50aad6cb4d44dcab6101a7f79925bd59d82ca42f38a9856a28865374"
checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a"
dependencies = [
"rustc_version 0.3.3",
"rustc_version 0.4.0",
]
[[package]]
@ -992,13 +992,19 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "hashlink"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8"
dependencies = [
"hashbrown",
"hashbrown 0.9.1",
]
[[package]]
@ -1101,12 +1107,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "1.6.2"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.11.2",
]
[[package]]
@ -1227,9 +1233,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "memchr"
version = "2.3.4"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "memoffset"
@ -1289,12 +1295,11 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "nom"
version = "6.2.1"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
checksum = "3d521ee2250f619dd5e06515ba405858d249edc8fae9ddee2dba0695e57db01b"
dependencies = [
"bitvec 0.19.5",
"funty",
"bitvec 0.19.4",
"lexical-core",
"memchr",
"version_check",
@ -1426,15 +1431,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]]
name = "petgraph"
version = "0.5.1"
@ -1467,9 +1463,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.6"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]]
name = "pin-utils"
@ -1498,15 +1494,15 @@ dependencies = [
[[package]]
name = "plotters-backend"
version = "0.3.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07fffcddc1cb3a1de753caa4e4df03b79922ba43cf882acc1bdd7e8df9f4590"
checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c"
[[package]]
name = "plotters-svg"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b38a02e23bd9604b842a812063aec4ef702b57989c37b655254bb61c471ad211"
checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9"
dependencies = [
"plotters-backend",
]
@ -1762,9 +1758,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.4.6"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr",
@ -1839,11 +1835,11 @@ dependencies = [
[[package]]
name = "rustc_version"
version = "0.3.3"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 0.11.0",
"semver 1.0.3",
]
[[package]]
@ -1941,17 +1937,14 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser 0.7.0",
"semver-parser",
]
[[package]]
name = "semver"
version = "0.11.0"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser 0.10.2",
]
checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe"
[[package]]
name = "semver-parser"
@ -1959,15 +1952,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]]
name = "serde"
version = "1.0.126"
@ -2150,7 +2134,6 @@ dependencies = [
"anyhow",
"bls12_381",
"byteorder",
"bytes",
"criterion",
"dotenv",
"env_logger",
@ -2231,18 +2214,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.25"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.25"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
dependencies = [
"proc-macro2",
"quote",
@ -2342,9 +2325,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.7.1"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2"
checksum = "570c2eb13b3ab38208130eccd41be92520388791207fde783bda7c1e8ace28d4"
dependencies = [
"autocfg",
"bytes",
@ -2543,12 +2526,6 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
@ -2560,9 +2537,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-width"

View File

@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx4608m
android.useAndroidX=true
android.enableJetifier=true

View File

@ -2,20 +2,30 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:package_info_plus/package_info_plus.dart';
Future<void> showAbout(BuildContext context) async {
final content = await rootBundle.loadString('assets/about.md');
showDialog(context: context, barrierDismissible: false,
var content = await rootBundle.loadString('assets/about.md');
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
String code = packageInfo.buildNumber;
content += "`Version: $version+$code`";
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('About ZWallet'),
content: Container(width: double.maxFinite, child: Markdown(data: content)),
actions: [
ElevatedButton(onPressed: () { Navigator.of(context).pop(); }, child: Text('OK'))
]
));
title: Text('About ZWallet'),
content: Container(
width: double.maxFinite,
child: Markdown(data: content),
),
actions: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'))
]));
}
Future<void> showAboutOnce(BuildContext context) async {
@ -25,4 +35,4 @@ Future<void> showAboutOnce(BuildContext context) async {
await showAbout(context);
prefs.setBool('about', true);
}
}
}

View File

@ -1,12 +1,12 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:warp/store.dart';
import 'package:warp_api/warp_api.dart';
import 'about.dart';
@ -20,29 +20,33 @@ class AccountPage extends StatefulWidget {
class _AccountPageState extends State<AccountPage>
with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
Timer _timerSync;
int _progress = 0;
StreamSubscription _sub;
@override
bool get wantKeepAlive => true;
@override
initState() {
super.initState();
print("INITSTATE");
Future.microtask(() async {
if (await accountManager.isEmpty())
Navigator.of(this.context).pushReplacementNamed('/accounts');
else {
await accountManager.updateUnconfirmedBalance();
await accountManager.fetchNotesAndHistory();
_sync();
_setupTimer();
}
await accountManager.updateUnconfirmedBalance();
await accountManager.fetchNotesAndHistory();
_setupTimer();
await showAboutOnce(this.context);
});
WidgetsBinding.instance.addObserver(this);
super.initState();
progressStream.listen((percent) {
setState(() {
_progress = percent;
});
});
}
@override
void dispose() {
print("DISPOSE");
_timerSync?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
@ -73,25 +77,31 @@ class _AccountPageState extends State<AccountPage>
child: Scaffold(
appBar: AppBar(
title: Observer(
builder: (context) =>
Text("\u24E9 Wallet - ${accountManager.active.name}")),
builder: (context) => Text(
"\u24E9 Wallet - ${accountManager.active.name}")),
bottom: TabBar(tabs: [
Tab(text: "Account"),
Tab(text: "Notes"),
Tab(text: "History"),
]),
actions: [
PopupMenuButton<String>(
itemBuilder: (context) => [
PopupMenuItem(child: Text("Accounts"), value: "Accounts"),
PopupMenuItem(child: Text("Backup"), value: "Backup"),
PopupMenuItem(child: Text("Rescan"), value: "Rescan"),
PopupMenuItem(
child: Text(settings.nextMode()), value: "Theme"),
PopupMenuItem(child: Text("About"), value: "About"),
],
onSelected: _onMenu,
)
Observer(builder: (context) {
accountManager.canPay;
return PopupMenuButton<String>(
itemBuilder: (context) => [
PopupMenuItem(child: Text("Accounts"), value: "Accounts"),
PopupMenuItem(child: Text("Backup"), value: "Backup"),
PopupMenuItem(child: Text("Rescan"), value: "Rescan"),
if (accountManager.canPay)
PopupMenuItem(
child: Text("Cold Storage"), value: "Cold"),
PopupMenuItem(
child: Text(settings.nextMode()), value: "Theme"),
PopupMenuItem(child: Text("About"), value: "About"),
],
onSelected: _onMenu,
);
})
],
),
body: TabBarView(children: [
@ -100,10 +110,15 @@ class _AccountPageState extends State<AccountPage>
child: Center(
child: Column(children: [
Observer(
builder: (context) => syncStatus.isSynced()
? Text('${syncStatus.syncedHeight}', style: Theme.of(this.context).textTheme.caption)
: Text(
'${syncStatus.syncedHeight} / ${syncStatus.latestHeight}')),
builder: (context) => syncStatus.syncedHeight <= 0
? Text('Synching')
: syncStatus.isSynced()
? Text('${syncStatus.syncedHeight}',
style: Theme.of(this.context)
.textTheme
.caption)
: Text(
'${syncStatus.syncedHeight} / ${syncStatus.latestHeight}')),
Padding(padding: EdgeInsets.symmetric(vertical: 8)),
Observer(builder: (context) {
return Column(children: [
@ -115,16 +130,20 @@ class _AccountPageState extends State<AccountPage>
SelectableText('${accountManager.active.address}'),
]);
}),
Observer(builder: (context) => Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: <Widget>[
Text(
'\u24E9 ${_getBalance_hi(accountManager.balance)}',
style: Theme.of(context).textTheme.headline2),
Text('${_getBalance_lo(accountManager.balance)}'),
])),
Observer(
builder: (context) => Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: <Widget>[
Text(
'\u24E9 ${_getBalance_hi(accountManager.balance)}',
style: Theme.of(context)
.textTheme
.headline2),
Text(
'${_getBalance_lo(accountManager.balance)}'),
])),
Observer(builder: (context) {
final zecPrice = priceStore.zecPrice;
final balanceUSD =
@ -139,23 +158,28 @@ class _AccountPageState extends State<AccountPage>
]);
}),
Padding(padding: EdgeInsets.symmetric(vertical: 8)),
Observer(builder: (context) =>
(accountManager.unconfirmedBalance != 0) ?
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: <Widget>[
Text(
'${_sign(accountManager.unconfirmedBalance)} ${_getBalance_hi(accountManager.unconfirmedBalance)}',
style: Theme.of(context)
.textTheme
.headline4
?.merge(_unconfirmedStyle())),
Text(
'${_getBalance_lo(accountManager.unconfirmedBalance)}',
style: _unconfirmedStyle()),
]) : Container()),
Observer(
builder: (context) =>
(accountManager.unconfirmedBalance != 0)
? Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: <Widget>[
Text(
'${_sign(accountManager.unconfirmedBalance)} ${_getBalance_hi(accountManager.unconfirmedBalance)}',
style: Theme.of(context)
.textTheme
.headline4
?.merge(_unconfirmedStyle())),
Text(
'${_getBalance_lo(accountManager.unconfirmedBalance)}',
style: _unconfirmedStyle()),
])
: Container()),
if (_progress > 0)
LinearProgressIndicator(value: _progress / 100.0),
]))),
NoteWidget(),
HistoryWidget(),
@ -190,6 +214,7 @@ class _AccountPageState extends State<AccountPage>
}
_setupTimer() {
_sync();
_timerSync = Timer.periodic(Duration(seconds: 15), (Timer t) {
_trySync();
});
@ -199,6 +224,7 @@ class _AccountPageState extends State<AccountPage>
WarpApi.warpSync((int height) async {
setState(() {
syncStatus.setSyncHeight(height);
if (syncStatus.isSynced()) accountManager.fetchNotesAndHistory();
});
});
}
@ -218,8 +244,9 @@ class _AccountPageState extends State<AccountPage>
} else if (res == 0) {
syncStatus.setSyncHeight(syncStatus.latestHeight);
}
await accountManager.fetchNotesAndHistory();
}
await accountManager.fetchNotesAndHistory();
await accountManager.updateBalance();
await accountManager.updateUnconfirmedBalance();
}
@ -238,6 +265,9 @@ class _AccountPageState extends State<AccountPage>
case "Rescan":
_rescan();
break;
case "Cold":
_cold();
break;
case "Theme":
settings.toggle();
break;
@ -250,8 +280,7 @@ class _AccountPageState extends State<AccountPage>
_backup() async {
final localAuth = LocalAuthentication();
final didAuthenticate = await localAuth.authenticate(
localizedReason: "Please authenticate to show account seed",
biometricOnly: true);
localizedReason: "Please authenticate to show account seed");
if (didAuthenticate) {
final seed = await accountManager.getBackup();
showDialog(
@ -300,6 +329,31 @@ class _AccountPageState extends State<AccountPage>
]),
);
}
_cold() {
showDialog(
context: this.context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('Cold Storage'),
content: Text(
'Do you want to DELETE the secret key and convert this account to a watch-only account? '
'You will not be able to spend from this device anymore. This operation is NOT reversible.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Cancel')),
TextButton(
onPressed: _convertToWatchOnly, child: Text('DELETE'))
]));
}
_convertToWatchOnly() {
accountManager.convertToWatchOnly();
Navigator.of(context).pop();
}
}
class NoteWidget extends StatefulWidget {
@ -309,7 +363,6 @@ class NoteWidget extends StatefulWidget {
class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
final DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");
final latestHeight = syncStatus.latestHeight;
@override
bool get wantKeepAlive => true; //Set to true
@ -330,7 +383,7 @@ class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
.toList();
bool _confirmed(int height) {
return latestHeight - height >= 10;
return syncStatus.latestHeight - height >= 10;
}
@override
@ -338,6 +391,7 @@ class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
super.build(context);
return SingleChildScrollView(
scrollDirection: Axis.vertical,
padding: EdgeInsets.only(bottom: 32),
child: Observer(
builder: (context) => DataTable(
columns: [
@ -376,6 +430,7 @@ class _HistoryState extends State<HistoryWidget>
super.build(context);
return SingleChildScrollView(
scrollDirection: Axis.vertical,
padding: EdgeInsets.only(bottom: 64),
child: Observer(
builder: (context) => DataTable(
columns: [

View File

@ -54,12 +54,13 @@ class AccountManagerState extends State<AccountManagerPage> {
}
Future<bool> _onAccountDelete(DismissDirection _direction) async {
if (accountManager.accounts.length == 1) return false;
final confirm = showDialog<bool>(
context: this.context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('Seed'),
content: Text('Are you SURE you want to DELETE this account?'),
content: Text('Are you SURE you want to DELETE this account? You MUST have a BACKUP to recover it. This operation is NOT reversible.'),
actions: [
TextButton(
child: Text('Cancel'),
@ -68,7 +69,7 @@ class AccountManagerState extends State<AccountManagerPage> {
},
),
TextButton(
child: Text('OK'),
child: Text('DELETE'),
onPressed: () {
Navigator.of(this.context).pop(true);
},

View File

@ -1,3 +1,4 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:path/path.dart';
@ -12,6 +13,9 @@ import 'send.dart';
import 'store.dart';
const ZECUNIT = 100000000.0;
var ZECUNIT_DECIMAL = Decimal.parse('100000000');
const mZECUNIT = 100000;
const DEFAULT_FEE = 1000;
var accountManager = AccountManager();
var priceStore = PriceStore();
@ -58,7 +62,8 @@ class _ZWalletState extends State<ZWalletApp> {
await accountManager.init();
await syncStatus.init();
await settings.restore();
return Future.value(AccountPage());
return Future.value(
accountManager.accounts.isNotEmpty ? AccountPage() : AccountManagerPage());
}
@override

View File

@ -66,8 +66,11 @@ class _RestorePageState extends State<RestorePage> {
if (accountManager.accounts.length == 1) {
if (_keyController.text == "")
WarpApi.skipToLastHeight(); // single new account -> quick sync
else
else {
final snackBar = SnackBar(content: Text("Scan starting momentarily"));
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar);
WarpApi.rewindToHeight(0);
}
}
Navigator.of(context).pop();
Navigator.of(context).pushReplacementNamed('/account');

View File

@ -1,11 +1,16 @@
import 'dart:isolate';
import 'package:barcode_scan/barcode_scan.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_masked_text/flutter_masked_text.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:warp_api/warp_api.dart';
import 'package:decimal/decimal.dart';
import 'dart:math' as math;
import 'main.dart';
import 'store.dart';
class SendPage extends StatefulWidget {
SendPage();
@ -17,77 +22,112 @@ class SendPage extends StatefulWidget {
class SendState extends State<SendPage> {
final _formKey = GlobalKey<FormState>();
var _address = "";
var _amount = 0.0;
var _amount = Decimal.zero;
var _maxAmountPerNote = Decimal.zero;
var _balance = 0;
final _addressController = TextEditingController();
final _currencyController = MoneyMaskedTextController(decimalSeparator: '.', thousandSeparator: ',', precision: 3);
var _mZEC = true;
var _currencyController = _makeMoneyMaskedTextController(true);
var _maxAmountPerNoteController = _makeMoneyMaskedTextController(true);
var _includeFee = false;
var _isExpanded = false;
@override
initState() {
Future.microtask(() async {
final balance = await accountManager.getBalanceSpendable(syncStatus.latestHeight - 10);
final balance = await accountManager
.getBalanceSpendable(syncStatus.latestHeight - 10);
setState(() {
_balance = balance;
_balance = math.max(balance - DEFAULT_FEE, 0);
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint(_address);
return Scaffold(
appBar: AppBar(title: Text('Send ZEC')),
body: Form(
key: _formKey,
child: Container(
padding: EdgeInsets.all(20),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(child: TextFormField(
decoration: InputDecoration(labelText: 'Send ZEC to...'),
minLines: 4, maxLines: null,
keyboardType: TextInputType.multiline,
controller: _addressController,
onSaved: onAddress,
validator: checkAddress,
appBar: AppBar(title: Text('Send ZEC')),
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Column(children: <Widget>[
Row(children: <Widget>[
Expanded(
child: TextFormField(
decoration:
InputDecoration(labelText: 'Send ZEC to...'),
minLines: 4,
maxLines: null,
keyboardType: TextInputType.multiline,
controller: _addressController,
onSaved: _onAddress,
validator: _checkAddress,
),
),
),
IconButton(icon: new Icon(MdiIcons.qrcodeScan), onPressed: onScan)
]
),
TextFormField(
decoration: InputDecoration(labelText: 'Amount'),
keyboardType: TextInputType.number,
controller: _currencyController,
validator: checkAmount,
onSaved: onAmount
),
Padding(padding: EdgeInsets.all(8)),
Text("Spendable: ${_balance / ZECUNIT } ZEC"),
ButtonBar(
children: [
IconButton(icon: new Icon(MdiIcons.send), onPressed: onSend),
IconButton(icon: new Icon(MdiIcons.cancel), onPressed: onCancel)
]
)
]
)
)
)
);
IconButton(
icon: new Icon(MdiIcons.qrcodeScan), onPressed: _onScan)
]),
Row(children: [
Expanded(
child: TextFormField(
decoration: InputDecoration(labelText: 'Amount'),
keyboardType: TextInputType.number,
controller: _currencyController,
validator: _checkAmount,
onSaved: _onAmount)),
TextButton(child: Text('MAX'), onPressed: _onMax),
]),
ExpansionPanelList(
expansionCallback: (_, isExpanded) {
setState(() {
_isExpanded = !isExpanded;
});
},
children: [
ExpansionPanel(
headerBuilder: (_, __) =>
ListTile(title: Text('Advanced Options')),
body: Column(children: [
CheckboxListTile(
title: Text('Round to mZEC'),
value: _mZEC,
onChanged: _onChangedmZEC),
CheckboxListTile(
title: Text('Include Fee in Amount'),
value: _includeFee,
onChanged: _onChangedIncludeFee),
ListTile(
title: TextFormField(
decoration: InputDecoration(
labelText: 'Max Amount per Note'),
keyboardType: TextInputType.number,
controller: _maxAmountPerNoteController,
validator: _checkMaxAmountPerNote,
onSaved: _onSavedMaxAmountPerNote,
))
]),
isExpanded: _isExpanded)
]),
Padding(padding: EdgeInsets.all(8)),
Text("Spendable: ${_balance / ZECUNIT} ZEC"),
ButtonBar(children: [
IconButton(
icon: new Icon(MdiIcons.cancel), onPressed: _onCancel),
IconButton(
icon: new Icon(MdiIcons.send), onPressed: _onSend),
])
]))));
}
String checkAddress(String v) {
String _checkAddress(String v) {
if (v.isEmpty) return 'Address is empty';
if (!WarpApi.validAddress(v)) return 'Invalid Address';
return null;
}
String checkAmount(String vs) {
String _checkAmount(String vs) {
final v = double.tryParse(vs);
if (v == null) return 'Amount must be a number';
if (v <= 0.0) return 'Amount must be positive';
@ -95,11 +135,44 @@ class SendState extends State<SendPage> {
return null;
}
void onCancel() {
String _checkMaxAmountPerNote(String vs) {
final v = double.tryParse(vs);
if (v == null) return 'Amount must be a number';
if (v < 0.0) return 'Amount must be positive';
return null;
}
void _onMax() {
setState(() {
_mZEC = false;
_currencyController = _makeMoneyMaskedTextController(false);
_includeFee = false;
_currencyController.updateValue(
(Decimal.fromInt(_balance) / ZECUNIT_DECIMAL).toDouble());
});
}
void _onChangedIncludeFee(bool v) {
setState(() {
_includeFee = v;
});
}
void _onChangedmZEC(bool v) {
setState(() {
_mZEC = v;
final amount = _currencyController.numberValue;
_currencyController = _makeMoneyMaskedTextController(v);
_currencyController.updateValue(amount);
_maxAmountPerNoteController = _makeMoneyMaskedTextController(v);
});
}
void _onCancel() {
Navigator.of(context).pop();
}
void onScan() async {
void _onScan() async {
var code = await BarcodeScanner.scan();
setState(() {
_address = code.rawContent;
@ -107,58 +180,90 @@ class SendState extends State<SendPage> {
});
}
void onAmount(v) {
_amount = double.parse(v);
void _onAmount(v) {
_amount = Decimal.parse(v);
}
void onAddress(v) {
void _onAddress(v) {
_address = v;
}
void onSend() async {
void _onSavedMaxAmountPerNote(v) {
_maxAmountPerNote = Decimal.parse(v);
}
void _onSend() async {
final form = _formKey.currentState;
if (form == null) return;
if (form.validate()) {
form.save();
final approved = await showDialog(context: context, barrierDismissible: false,
builder: (BuildContext context) =>
AlertDialog(title: Text('Please Confirm'),
content: SingleChildScrollView(
child: Text("Sending $_amount ZEC to $_address")
),
actions: <Widget>[
TextButton(child: Text('Cancel'), onPressed: () {Navigator.of(context).pop(false);}),
TextButton(child: Text('Approve'), onPressed: () {Navigator.of(context).pop(true);}),
],
)
);
final approved = await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => AlertDialog(
title: Text('Please Confirm'),
content: SingleChildScrollView(
child: Text("Sending $_amount ZEC to $_address")),
actions: <Widget>[
TextButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop(false);
}),
TextButton(
child: Text('Approve'),
onPressed: () {
Navigator.of(context).pop(true);
}),
],
));
if (approved) {
Navigator.of(context).pop();
final snackBar1 = SnackBar(
content: Text("Preparing transaction..."));
final snackBar1 = SnackBar(content: Text("Preparing transaction..."));
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar1);
final amount = (_amount * ZECUNIT).toInt();
final tx = await compute(sendPayment, SendPaymentParam(accountManager.active.id, _address, amount));
int amount = (_amount * ZECUNIT_DECIMAL).toInt();
if (_includeFee) amount -= DEFAULT_FEE;
int maxAmountPerNote = (_maxAmountPerNote * ZECUNIT_DECIMAL).toInt();
final tx = await compute(
sendPayment,
SendPaymentParam(
accountManager.active.id, _address, amount, maxAmountPerNote,
progressPort.sendPort
));
final snackBar2 = SnackBar(
content: Text("TX ID: $tx"));
final snackBar2 = SnackBar(content: Text("TX ID: $tx"));
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar2);
}
}
}
static MoneyMaskedTextController _makeMoneyMaskedTextController(bool mZEC) =>
MoneyMaskedTextController(
decimalSeparator: '.',
thousandSeparator: ',',
precision: mZEC ? 3 : 8);
}
class SendPaymentParam {
int account;
String address;
int amount;
int maxAmountPerNote;
SendPort sendPort;
SendPaymentParam(this.account, this.address, this.amount);
SendPaymentParam(
this.account, this.address, this.amount, this.maxAmountPerNote, this.sendPort);
}
sendPayment(SendPaymentParam param) {
final tx = WarpApi.sendPayment(param.account, param.address, param.amount);
sendPayment(SendPaymentParam param) async {
param.sendPort.send(0);
final tx = await WarpApi.sendPayment(
param.account, param.address, param.amount, param.maxAmountPerNote,
(percent) {
param.sendPort.send(percent);
});
param.sendPort.send(0);
return tx;
}

View File

@ -1,3 +1,5 @@
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
@ -26,7 +28,7 @@ abstract class _Settings with Store {
@action
Future<void> restore() async {
final prefs = await SharedPreferences.getInstance();
final prefMode = await prefs.getString('theme') ?? "light";
final prefMode = prefs.getString('theme') ?? "light";
if (prefMode == "light")
mode = ThemeMode.light;
else
@ -65,7 +67,7 @@ abstract class _AccountManager with Store {
List<Tx> txs = [];
@observable
List<Account> accounts;
List<Account> accounts = [];
Future<void> init() async {
db = await getDatabase();
@ -87,11 +89,13 @@ abstract class _AccountManager with Store {
@action
Future<void> setActiveAccount(Account account) async {
if (account == null) return;
final prefs = await SharedPreferences.getInstance();
prefs.setInt('account', account.id);
this.active = account;
WarpApi.setMempoolAccount(account.id);
final List<Map> res = await db.rawQuery("SELECT sk FROM accounts WHERE id_account = ?1", [active.id]);
final List<Map> res = await db
.rawQuery("SELECT sk FROM accounts WHERE id_account = ?1", [active.id]);
canPay = res.isNotEmpty && res[0]['sk'] != null;
await fetchNotesAndHistory();
}
@ -99,12 +103,14 @@ abstract class _AccountManager with Store {
@action
setActiveAccountId(int idAccount) {
final account = accounts.firstWhere((account) => account.id == idAccount,
orElse: () => accounts[0]);
orElse: () => accounts.isNotEmpty ? accounts[0] : null);
setActiveAccount(account);
}
Future<String> getBackup() async {
final List<Map> res = await db.rawQuery("SELECT seed, sk, ivk FROM accounts WHERE id_account = ?1", [active.id]);
final List<Map> res = await db.rawQuery(
"SELECT seed, sk, ivk FROM accounts WHERE id_account = ?1",
[active.id]);
if (res.isEmpty) return null;
final row = res[0];
final backup = row['seed'] ?? row['sk'] ?? row['ivk'];
@ -112,13 +118,17 @@ abstract class _AccountManager with Store {
}
Future<int> _getBalance() async {
final List<Map> res = await db.rawQuery("SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND (spent IS NULL OR spent = 0)", [active.id]);
final List<Map> res = await db.rawQuery(
"SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND (spent IS NULL OR spent = 0)",
[active.id]);
if (res.isEmpty) return 0;
return res[0]['value'] ?? 0;
}
Future<int> getBalanceSpendable(int height) async {
final List<Map> res = await db.rawQuery("SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND (spent IS NULL OR spent = 0) AND height <= ?2", [active.id, height]);
final List<Map> res = await db.rawQuery(
"SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND (spent IS NULL OR spent = 0) AND height <= ?2",
[active.id, height]);
if (res.isEmpty) return 0;
return res[0]['value'] ?? 0;
}
@ -129,16 +139,19 @@ abstract class _AccountManager with Store {
}
isEmpty() async {
final List<Map> res = await db.rawQuery("SELECT 1 FROM accounts", []);
final List<Map> res = await db.rawQuery("SELECT name FROM accounts", []);
return res.isEmpty;
}
Future<List<Account>> _list() async {
final List<Map> res = await db.rawQuery(
"SELECT a.id_account AS account, a.name, a.address, COALESCE(SUM(n.value), 0) AS balance FROM accounts a LEFT JOIN received_notes n ON n.account = a.id_account WHERE "
"n.spent IS NULL OR n.spent = 0 "
"GROUP BY name", []);
return res.map((r) => Account(r['account'], r['name'], r['address'], r['balance'])).toList();
"WITH notes AS (SELECT a.id_account, a.name, a.address, CASE WHEN r.spent IS NULL THEN r.value ELSE 0 END AS nv FROM accounts a LEFT JOIN received_notes r ON a.id_account = r.account) "
"SELECT id_account, name, address, COALESCE(sum(nv), 0) AS balance FROM notes GROUP by id_account",
[]);
return res
.map((r) =>
Account(r['id_account'], r['name'], r['address'], r['balance']))
.toList();
}
@action
@ -146,17 +159,25 @@ abstract class _AccountManager with Store {
await db.rawDelete("DELETE FROM accounts WHERE id_account = ?1", [account]);
}
@action
Future<void> updateBalance() async {
if (active == null) return;
balance = await _getBalance();
}
final DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");
@action Future<void> fetchNotesAndHistory() async {
balance = await _getBalance();
@action
Future<void> fetchNotesAndHistory() async {
if (active == null) return;
await updateBalance();
final List<Map> res = await db.rawQuery(
"SELECT n.height, n.value, t.timestamp FROM received_notes n, transactions t WHERE n.account = ?1 AND (n.spent IS NULL OR n.spent = 0) AND n.tx = t.id_tx",
[active.id]);
notes = res.map((row) {
final height = row['height'];
final timestamp = dateFormat.format(
DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
final timestamp = dateFormat
.format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
return Note(height, timestamp, row['value'] / ZECUNIT);
}).toList();
@ -165,11 +186,17 @@ abstract class _AccountManager with Store {
[active.id]);
txs = res2.map((row) {
final txid = hex.encode(row['txid']).substring(0, 8);
final timestamp = dateFormat.format(
DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
final timestamp = dateFormat
.format(DateTime.fromMillisecondsSinceEpoch(row['timestamp'] * 1000));
return Tx(row['height'], timestamp, txid, row['value'] / ZECUNIT);
}).toList();
}
@action
Future<void> convertToWatchOnly() async {
await db.rawUpdate("UPDATE accounts SET seed = NULL, sk = NULL WHERE id_account = ?1", [active.id]);
canPay = false;
}
}
class Account {
@ -186,11 +213,11 @@ class PriceStore = _PriceStore with _$PriceStore;
abstract class _PriceStore with Store {
@observable
double zecPrice = 0.0;
@action
Future<void> fetchZecPrice() async {
final base = "api.binance.com";
final uri = Uri.https(base, '/api/v3/avgPrice', { 'symbol': 'ZECUSDT' });
final uri = Uri.https(base, '/api/v3/avgPrice', {'symbol': 'ZECUSDT'});
final rep = await http.get(uri);
if (rep.statusCode == 200) {
final json = convert.jsonDecode(rep.body) as Map<String, dynamic>;
@ -224,13 +251,13 @@ abstract class _SyncStatus with Store {
@action
setSyncHeight(int height) {
syncedHeight = height;
syncedHeight = height;
}
@action
Future<bool> update() async {
final _syncedHeight = Sqflite.firstIntValue(
await _db.rawQuery("SELECT MAX(height) FROM blocks")) ??
await _db.rawQuery("SELECT MAX(height) FROM blocks")) ??
0;
if (_syncedHeight > 0) syncedHeight = _syncedHeight;
latestHeight = await WarpApi.getLatestHeight();
@ -239,6 +266,9 @@ abstract class _SyncStatus with Store {
}
}
var progressPort = ReceivePort();
var progressStream = progressPort.asBroadcastStream();
class Note {
int height;
String timestamp;

View File

@ -24,7 +24,7 @@ uint32_t new_account(char *name, char *data);
int64_t get_mempool_balance(void);
const char *send_payment(uint32_t account, char *address, uint64_t amount);
const char *send_payment(uint32_t account, char *address, uint64_t amount, uint64_t max_amount_per_note, int64_t port);
int8_t try_warp_sync(void);

View File

@ -122,15 +122,42 @@ pub fn get_latest_height() -> u32 {
})
}
pub fn send_payment(account: u32, address: &str, amount: u64) -> String {
pub fn send_payment(
account: u32,
address: &str,
amount: u64,
max_amount_per_note: u64,
port: i64,
) -> String {
let r = Runtime::new().unwrap();
r.block_on(async {
let wallet = WALLET.get().unwrap().lock().unwrap();
let res = wallet.send_payment(account, address, amount).await;
let res = wallet
.send_payment(
account,
address,
amount,
max_amount_per_note,
move |progress| {
if port != 0 {
let progress = match progress.end() {
Some(end) => (progress.cur() * 100 / end) as i32,
None => -(progress.cur() as i32),
};
let mut progress = progress.into_dart();
unsafe {
POST_COBJ.map(|p| {
p(port, &mut progress);
});
}
}
},
)
.await;
match res {
Err(err) => {
log::error!("{}", err);
"".to_string()
err.to_string()
}
Ok(tx_id) => tx_id,
}

View File

@ -61,9 +61,11 @@ pub unsafe extern "C" fn send_payment(
account: u32,
address: *mut c_char,
amount: u64,
max_amount_per_note: u64,
port: i64,
) -> *const c_char {
let address = CStr::from_ptr(address).to_string_lossy();
let tx_id = api::send_payment(account, &address, amount);
let tx_id = api::send_payment(account, &address, amount, max_amount_per_note, port);
CString::new(tx_id).unwrap().into_raw()
}

@ -1 +1 @@
Subproject commit cb44cb2438f1239bb494ea7e82801e4135598420
Subproject commit 4548e888a1e5c67d98a31f16be7a0323ef3e318c

2
native/zcash_params/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.params
.idea/

View File

@ -0,0 +1,2 @@
This package just bundles the Zcash sapling circuit parameters
as a crate. To build, copy the `*.params` files into `src`.

View File

@ -17,6 +17,16 @@ class SyncParams {
SyncParams(this.port);
}
class PaymentParams {
int account;
String address;
int amount;
int maxAmountPerNote;
SendPort port;
PaymentParams(this.account, this.address, this.amount, this.maxAmountPerNote, this.port);
}
const DEFAULT_ACCOUNT = 1;
final warp_api_lib = init();
@ -98,12 +108,21 @@ class WarpApi {
return warp_api_lib.get_mempool_balance();
}
static String sendPayment(int account, String address, int amount) {
final txId = warp_api_lib.send_payment(account, address.toNativeUtf8().cast<Int8>(), amount);
return txId.cast<Utf8>().toDartString();
static Future<String> sendPayment(int account, String address, int amount, int maxAmountPerNote, void Function(int) f) async {
var receivePort = ReceivePort();
receivePort.listen((progress) {
f(progress);
});
return await compute(sendPaymentIsolateFn, PaymentParams(account, address, amount, maxAmountPerNote, receivePort.sendPort));
}
}
String sendPaymentIsolateFn(PaymentParams params) {
final txId = warp_api_lib.send_payment(params.account, params.address.toNativeUtf8().cast<Int8>(), params.amount, params.maxAmountPerNote, params.port.nativePort);
return txId.cast<Utf8>().toDartString();
}
void warpSyncIsolateFn(SyncParams params) {
warp_api_lib.warp_sync(params.port.nativePort);
}

View File

@ -135,11 +135,15 @@ class NativeLibrary {
int account,
ffi.Pointer<ffi.Int8> address,
int amount,
int max_amount_per_note,
int port,
) {
return _send_payment(
account,
address,
amount,
max_amount_per_note,
port,
);
}
@ -260,12 +264,16 @@ typedef _c_send_payment = ffi.Pointer<ffi.Int8> Function(
ffi.Uint32 account,
ffi.Pointer<ffi.Int8> address,
ffi.Uint64 amount,
ffi.Uint64 max_amount_per_note,
ffi.Int64 port,
);
typedef _dart_send_payment = ffi.Pointer<ffi.Int8> Function(
int account,
ffi.Pointer<ffi.Int8> address,
int amount,
int max_amount_per_note,
int port,
);
typedef _c_try_warp_sync = ffi.Int8 Function();

View File

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.1"
auto_size_text_pk:
dependency: transitive
description:
name: auto_size_text_pk
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
barcode_scan:
dependency: "direct main"
description:
@ -133,7 +140,7 @@ packages:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
version: "0.3.2"
clock:
dependency: transitive
description:
@ -183,6 +190,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.12"
decimal:
dependency: "direct main"
description:
name: decimal
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
fake_async:
dependency: transitive
description:
@ -408,6 +422,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
package_info_plus_linux:
dependency: transitive
description:
name: package_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_info_plus_macos:
dependency: transitive
description:
name: package_info_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
package_info_plus_web:
dependency: transitive
description:
name: package_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_info_plus_windows:
dependency: transitive
description:
name: package_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
path:
dependency: "direct main"
description:
@ -520,6 +576,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
rational:
dependency: transitive
description:
name: rational
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
rflutter_alert:
dependency: "direct main"
description:
@ -700,6 +763,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
velocity_x:
dependency: "direct main"
description:
name: velocity_x
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.0"
vxstate:
dependency: transitive
description:
name: vxstate
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
warp_api:
dependency: "direct main"
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.0+5
version: 1.0.1+14
environment:
sdk: ">=2.9.0 <3.0.0"
@ -40,6 +40,9 @@ dependencies:
shared_preferences: any
splashscreen: any
flutter_markdown: any
package_info_plus: any
velocity_x: ^3.3.0
decimal: any
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.