diff --git a/assets/images/yat_crypto.png b/assets/images/yat_crypto.png new file mode 100644 index 00000000..fbd5d248 Binary files /dev/null and b/assets/images/yat_crypto.png differ diff --git a/assets/images/yat_logo.png b/assets/images/yat_logo.png new file mode 100644 index 00000000..73732566 Binary files /dev/null and b/assets/images/yat_logo.png differ diff --git a/lib/di.dart b/lib/di.dart index ffee7cbf..af5068e5 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -45,6 +45,7 @@ import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.da import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; +import 'package:cake_wallet/src/screens/yat/yat_webview_page.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; import 'package:cake_wallet/store/node_list_store.dart'; import 'package:cake_wallet/store/secret_store.dart'; @@ -62,6 +63,7 @@ import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/store/yat_store.dart'; import 'package:cake_wallet/view_model/backup_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; @@ -97,6 +99,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:cake_wallet/view_model/yat_view_model.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; @@ -192,6 +195,7 @@ Future setup( SendTemplateStore(templateSource: _templates)); getIt.registerSingleton( ExchangeTemplateStore(templateSource: _exchangeTemplates)); + getIt.registerSingleton(YatStore()); final secretStore = await SecretStoreBase.load(getIt.get()); @@ -235,7 +239,10 @@ Future setup( }); getIt.registerFactory( - () => WalletAddressListViewModel(appStore: getIt.get())); + () => WalletAddressListViewModel( + appStore: getIt.get(), + yatStore: getIt.get() + )); getIt.registerFactory(() => BalanceViewModel( appStore: getIt.get(), @@ -629,5 +636,15 @@ Future setup( param2: unspentCoinsListViewModel)); }); + getIt.registerFactory(() => YatViewModel( + yatStore: getIt.get() + )); + + getIt.registerFactoryParam((YatMode mode, _) => + YatWebViewPage( + mode: mode, + yatViewModel: getIt.get(), + )); + _isSetupFinished = true; } diff --git a/lib/palette.dart b/lib/palette.dart index 5f1655bd..6f4526e6 100644 --- a/lib/palette.dart +++ b/lib/palette.dart @@ -46,6 +46,9 @@ class Palette { static const Color protectiveBlue = Color.fromRGBO(33, 148, 255, 1.0); static const Color darkBlue = Color.fromRGBO(109, 128, 178, 1.0); static const Color paleCornflowerBlue = Color.fromRGBO(185, 196, 237, 1.0); + static const Color manatee = Color.fromRGBO(153, 161, 176, 1.0); + static const Color stateGray = Color.fromRGBO(68, 74, 89, 1.0); + static const Color frostySky = Color.fromRGBO(0, 184, 250, 1.0); } class PaletteDark { diff --git a/lib/router.dart b/lib/router.dart index acf81043..74e36d23 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -13,6 +13,7 @@ import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; +import 'package:cake_wallet/src/screens/yat/yat_webview_page.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; @@ -379,6 +380,11 @@ Route createRoute(RouteSettings settings) { getIt.get( param1: args)); + case Routes.yat: + return MaterialPageRoute( + builder: (_) => + getIt.get(param1: settings.arguments as YatMode)); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 19269337..af7459cc 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -57,4 +57,5 @@ class Routes { static const buyWebView = '/buy_web_view'; static const unspentCoinsList = '/unspent_coins_list'; static const unspentCoinsDetails = '/unspent_coins_details'; + static const yat = '/yat'; } \ No newline at end of file diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 169a2712..360a9cea 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -66,7 +66,7 @@ class QRWidget extends StatelessWidget { ]), if (isAmountFieldShow) Padding( - padding: EdgeInsets.only(top: 20), + padding: EdgeInsets.only(top: 10), child: Row( children: [ Expanded( @@ -101,7 +101,7 @@ class QRWidget extends StatelessWidget { ), ), Padding( - padding: EdgeInsets.only(top: 20, bottom: 20), + padding: EdgeInsets.only(top: 16, bottom: 16), child: Builder( builder: (context) => Observer( builder: (context) => GestureDetector( @@ -133,7 +133,47 @@ class QRWidget extends StatelessWidget { ], ), ))), - ) + ), + Observer(builder: (_) { + return addressListViewModel.yatAddress.isNotEmpty + ? Padding( + padding: EdgeInsets.only(bottom: 10), + child: Builder( + builder: (context) => GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData( + text: addressListViewModel.yatAddress)); + showBar( + context, S.of(context).copied_to_clipboard); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Yat Address', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.normal, + color: Theme.of(context).accentTextTheme. + display3.backgroundColor), + ), + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + addressListViewModel.yatAddress, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + ), + ) + ) + ] + ) + )), + ) + : Container(); + }) ], ); } diff --git a/lib/src/screens/yat/widgets/yat_bar.dart b/lib/src/screens/yat/widgets/yat_bar.dart new file mode 100644 index 00000000..03b3556b --- /dev/null +++ b/lib/src/screens/yat/widgets/yat_bar.dart @@ -0,0 +1,27 @@ +import 'package:cake_wallet/src/screens/yat/widgets/yat_close_button.dart'; +import 'package:flutter/material.dart'; + +class YatBar extends StatelessWidget { + YatBar({this.onClose}); + + final VoidCallback onClose; + final image = Image.asset('assets/images/yat_logo.png'); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned( + top: 0, + right: 0, + child: YatCloseButton(onClose: onClose) + ), + Positioned( + top: 16, + child: image + ) + ] + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/yat/widgets/yat_close_button.dart b/lib/src/screens/yat/widgets/yat_close_button.dart new file mode 100644 index 00000000..692f416d --- /dev/null +++ b/lib/src/screens/yat/widgets/yat_close_button.dart @@ -0,0 +1,28 @@ +import 'package:cake_wallet/palette.dart'; +import 'package:flutter/material.dart'; + +class YatCloseButton extends StatelessWidget { + YatCloseButton({this.onClose}); + + final VoidCallback onClose; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onClose, + child: Container( + height: 28, + width: 28, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Palette.manatee, + shape: BoxShape.circle + ), + child: Icon( + Icons.clear, + color: Colors.white, + size: 20) + ) + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/yat/yat_alert.dart b/lib/src/screens/yat/yat_alert.dart new file mode 100644 index 00000000..71b558a2 --- /dev/null +++ b/lib/src/screens/yat/yat_alert.dart @@ -0,0 +1,106 @@ +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/yat/widgets/yat_bar.dart'; +import 'package:cake_wallet/src/screens/yat/yat_webview_page.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/palette.dart'; + +class YatAlert extends StatelessWidget { + static const aspectRatioImage = 1.133; + final image = Image.asset('assets/images/yat_crypto.png'); + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final screenWidth = MediaQuery.of(context).size.width; + + return Container( + height: screenHeight, + width: screenWidth, + color: Colors.white, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(top: 40, bottom: 40), + content: Column( + children: [ + Container( + height: 90, + padding: EdgeInsets.only(left: 24, right: 24), + child: YatBar(onClose: () => Navigator.of(context).pop()) + ), + AspectRatio( + aspectRatio: aspectRatioImage, + child: FittedBox(child: image, fit: BoxFit.fill) + ), + Container( + padding: EdgeInsets.only(left: 30, right: 30), + child: Column( + children: [ + Text( + 'Send and receive crypto more easily with Yat', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: Colors.black, + decoration: TextDecoration.none, + ) + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: Text( + 'Cake Wallet users can now send and receive all their favorite currencies with a one-of-a-kind emoji-based username.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + fontFamily: 'Lato', + color: Colors.black, + decoration: TextDecoration.none, + ) + ) + ) + ] + ) + ) + ] + ), + bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 40), + bottomSection: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + PrimaryIconButton( + text: 'Get your Yat', + textColor: Colors.white, + color: Palette.protectiveBlue, + borderColor: Palette.protectiveBlue, + iconColor: Colors.white, + iconBackgroundColor: Colors.transparent, + iconData: CupertinoIcons + .arrow_up_right_square, + mainAxisAlignment: MainAxisAlignment.end, + onPressed: () => Navigator.of(context) + .popAndPushNamed(Routes.yat, arguments: YatMode.create)), + Padding( + padding: EdgeInsets.only(top: 24), + child: PrimaryIconButton( + text: 'Connect an existing Yat', + textColor: Colors.black, + color: Palette.blueAlice, + borderColor: Palette.blueAlice, + iconColor: Colors.black, + iconBackgroundColor: Colors.transparent, + iconData: CupertinoIcons + .arrow_up_right_square, + mainAxisAlignment: MainAxisAlignment.end, + onPressed: () => Navigator.of(context) + .popAndPushNamed(Routes.yat, arguments: YatMode.connect)) + ) + ] + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/yat/yat_webview_page.dart b/lib/src/screens/yat/yat_webview_page.dart new file mode 100644 index 00000000..dbfb8b14 --- /dev/null +++ b/lib/src/screens/yat/yat_webview_page.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/view_model/yat_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +enum YatMode {create, connect} + +class YatWebViewPage extends BasePage { + YatWebViewPage({this.yatViewModel, this.mode}) { + switch (mode) { + case YatMode.create: + url = _baseUrl + _createSuffix; + break; + case YatMode.connect: + url = _baseUrl + _signInSuffix; + break; + default: + url = _baseUrl + _createSuffix; + } + } + + static const _baseUrl = 'https://y.at'; + static const _signInSuffix = '/sign-in'; + static const _createSuffix = '/create'; + + final YatMode mode; + final YatViewModel yatViewModel; + + String url; + + @override + String get title => 'Yat'; + + @override + Color get backgroundDarkColor => Colors.white; + + @override + Color get titleColor => Palette.darkBlueCraiola; + + @override + Widget body(BuildContext context) => WebView( + initialUrl: url, + javascriptMode: JavascriptMode.unrestricted); +} \ No newline at end of file diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index 1b12d5d3..36788325 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -110,6 +110,9 @@ class PrimaryIconButton extends StatelessWidget { @required this.borderColor, @required this.iconColor, @required this.iconBackgroundColor, + @required this.textColor, + this.mainAxisAlignment = MainAxisAlignment.start, + this.radius = 26 }); final VoidCallback onPressed; @@ -119,40 +122,44 @@ class PrimaryIconButton extends StatelessWidget { final Color iconColor; final Color iconBackgroundColor; final String text; + final MainAxisAlignment mainAxisAlignment; + final Color textColor; + final double radius; @override Widget build(BuildContext context) { return ButtonTheme( minWidth: double.infinity, - height: 56.0, + height: 52.0, child: FlatButton( onPressed: onPressed, color: color, shape: RoundedRectangleBorder( side: BorderSide(color: borderColor), - borderRadius: BorderRadius.circular(10.0)), + borderRadius: BorderRadius.circular(radius)), child: Stack( children: [ Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: mainAxisAlignment, children: [ Container( - width: 28.0, - height: 56.0, + width: 26.0, + height: 52.0, decoration: BoxDecoration( shape: BoxShape.circle, color: iconBackgroundColor), - child: Icon(iconData, color: iconColor, size: 22.0), + child: Center( + child: Icon(iconData, color: iconColor, size: 22.0) + ), ), ], ), Container( - height: 56.0, + height: 52.0, child: Center( child: Text(text, style: TextStyle( fontSize: 16.0, - color: - Theme.of(context).primaryTextTheme.button.color)), + color: textColor)), ), ) ], diff --git a/lib/store/yat_store.dart b/lib/store/yat_store.dart new file mode 100644 index 00000000..d57f6bfe --- /dev/null +++ b/lib/store/yat_store.dart @@ -0,0 +1,12 @@ +import 'package:mobx/mobx.dart'; + +part 'yat_store.g.dart'; + +class YatStore = YatStoreBase with _$YatStore; + +abstract class YatStoreBase with Store { + YatStoreBase() : yatAddress = ''; + + @observable + String yatAddress; +} \ No newline at end of file diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index 57de6d7d..bc482bee 100644 --- a/lib/view_model/settings/settings_view_model.dart +++ b/lib/view_model/settings/settings_view_model.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/src/screens/yat/yat_alert.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; import 'package:package_info/package_info.dart'; @@ -154,6 +156,16 @@ abstract class SettingsViewModelBase with Store { _settingsStore.currentTheme = theme) ], [ + RegularListItem( + title: 'Manage Yats', + handler: (BuildContext context) async { + await showPopUp( + context: context, + builder: (BuildContext context) { + return YatAlert(); + }); + }, + ), RegularListItem( title: S.current.settings_terms_and_conditions, handler: (BuildContext context) => diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index a4d6eff6..9cc57aeb 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/store/yat_store.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; @@ -59,7 +60,10 @@ class BitcoinURI extends PaymentURI { } abstract class WalletAddressListViewModelBase with Store { - WalletAddressListViewModelBase({@required AppStore appStore}) { + WalletAddressListViewModelBase({ + @required AppStore appStore, + @required this.yatStore + }) { _appStore = appStore; _wallet = _appStore.wallet; hasAccounts = _wallet?.type == WalletType.monero; @@ -150,6 +154,9 @@ abstract class WalletAddressListViewModelBase with Store { @computed bool get hasAddressList => _wallet.type == WalletType.monero; + @computed + String get yatAddress => yatStore.yatAddress; + @observable WalletBase, TransactionInfo> _wallet; @@ -158,6 +165,8 @@ abstract class WalletAddressListViewModelBase with Store { AppStore _appStore; + final YatStore yatStore; + ReactionDisposer _onWalletChangeReaction; @action diff --git a/lib/view_model/yat_view_model.dart b/lib/view_model/yat_view_model.dart new file mode 100644 index 00000000..68b08469 --- /dev/null +++ b/lib/view_model/yat_view_model.dart @@ -0,0 +1,37 @@ +import 'package:cake_wallet/store/yat_store.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'dart:convert'; +import 'package:cake_wallet/yat/yat_exception.dart'; +import 'package:http/http.dart'; + +part 'yat_view_model.g.dart'; + +class YatViewModel = YatViewModelBase with _$YatViewModel; + +abstract class YatViewModelBase with Store { + YatViewModelBase({@required this.yatStore}); + + final YatStore yatStore; + + Future fetchCartInfo() async { + const _apiKey = ''; // FIXME + + final url = 'https://api.y.at/cart'; + + final response = await get( + url, + headers: { + 'Accept': '*/*', + 'Authorization,X-Api-Key': _apiKey + } + ); + + if (response.statusCode != 200) { + throw YatException(text: response.body.toString()); + } + + final responseJSON = json.decode(response.body) as Map; + yatStore.yatAddress = responseJSON[''] as String; // FIXME + } +} \ No newline at end of file diff --git a/lib/yat/yat_exception.dart b/lib/yat/yat_exception.dart new file mode 100644 index 00000000..0e81ef9c --- /dev/null +++ b/lib/yat/yat_exception.dart @@ -0,0 +1,10 @@ +import 'package:flutter/foundation.dart'; + +class YatException implements Exception { + YatException({@required this.text}); + + final String text; + + @override + String toString() => 'Yat exception: $text'; +} \ No newline at end of file diff --git a/lib/yat/yat_record.dart b/lib/yat/yat_record.dart new file mode 100644 index 00000000..aa5275e9 --- /dev/null +++ b/lib/yat/yat_record.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; +import 'package:cake_wallet/yat/yat_exception.dart'; +import 'package:http/http.dart'; + +Future fetchYatAddress(String emojiId) async { + const _apiKey = ''; // FIXME + const _requestURL = 'https://api.y.at/emoji_id/'; + + final url = _requestURL + emojiId; + + final response = await get( + url, + headers: { + 'Accept': '*/*', + 'Authorization': 'Bearer $_apiKey' + } + ); + + if (response.statusCode != 200) { + throw YatException(text: response.body.toString()); + } + + final responseJSON = json.decode(response.body) as Map; + final yatAddress = responseJSON[''] as String; // FIXME + return yatAddress; +} \ No newline at end of file