diff --git a/lib/pages/paynym/add_new_paynym_follow_view.dart b/lib/pages/paynym/add_new_paynym_follow_view.dart new file mode 100644 index 000000000..f6c3003f4 --- /dev/null +++ b/lib/pages/paynym/add_new_paynym_follow_view.dart @@ -0,0 +1,306 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/paynym/paynym_account.dart'; +import 'package:stackwallet/pages/paynym/subwidgets/featured_paynyms_widget.dart'; +import 'package:stackwallet/pages/paynym/subwidgets/paynym_card.dart'; +import 'package:stackwallet/providers/global/paynym_api_provider.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class AddNewPaynymFollowView extends ConsumerStatefulWidget { + const AddNewPaynymFollowView({ + Key? key, + required this.walletId, + required this.nymAccount, + }) : super(key: key); + + final String walletId; + final PaynymAccount nymAccount; + + static const String routeName = "/addNewPaynymFollow"; + + @override + ConsumerState createState() => + _AddNewPaynymFollowViewState(); +} + +class _AddNewPaynymFollowViewState + extends ConsumerState { + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchString = ""; + + bool _didSearch = false; + PaynymAccount? _searchResult; + + Future _search() async { + _didSearch = true; + bool didPopLoading = false; + unawaited( + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => const LoadingIndicator( + width: 200, + ), + ).then((_) => didPopLoading = true), + ); + + final paynymAccount = + await ref.read(paynymAPIProvider).nym(_searchString, true); + + if (mounted) { + if (!didPopLoading) { + Navigator.of(context).pop(); + } + + setState(() { + _searchResult = paynymAccount; + }); + } + } + + @override + void initState() { + _searchController = TextEditingController(); + searchFieldFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + + return MasterScaffold( + isDesktop: isDesktop, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Add new", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: ConditionalParent( + condition: !isDesktop, + builder: (child) => SafeArea( + child: LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 10, + ), + Text( + "Featured PayNyms", + style: STextStyles.sectionLabelMedium12(context), + ), + const SizedBox( + height: 12, + ), + const FeaturedPaynymsWidget(), + const SizedBox( + height: 24, + ), + Text( + "Add new", + style: STextStyles.sectionLabelMedium12(context), + ), + const SizedBox( + height: 12, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Paste payment code", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 8), + child: UnconstrainedBox( + child: Row( + children: [ + _searchController.text.isNotEmpty + ? TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _searchString = ""; + setState(() { + _searchController.text = ""; + }); + }, + ) + : TextFieldIconButton( + key: const Key( + "paynymPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf( + "\n", + ), + ); + } + + _searchString = content; + setState(() { + _searchController.text = content; + _searchController.selection = + TextSelection.collapsed( + offset: content.length, + ); + }); + } + }, + child: const ClipboardIcon(), + ), + TextFieldIconButton( + key: const Key("paynymScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = + await const BarcodeScannerWrapper() + .scan(); + + final pCodeString = qrResult.rawContent; + + _searchString = pCodeString; + + setState(() { + _searchController.text = pCodeString; + _searchController.selection = + TextSelection.collapsed( + offset: pCodeString.length, + ); + }); + } catch (_) { + // scan failed + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 12, + ), + SecondaryButton( + label: "Search", + onPressed: _search, + ), + if (_didSearch) + const SizedBox( + height: 20, + ), + if (_didSearch && _searchResult == null) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Nothing found. Please check the payment code.", + style: STextStyles.label(context), + ), + ], + ), + ), + if (_didSearch && _searchResult != null) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: PaynymCard( + label: _searchResult!.nymName, + paymentCodeString: _searchResult!.codes.first.code, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/paynym/paynym_claim_view.dart b/lib/pages/paynym/paynym_claim_view.dart index b9b5ebd95..aa79e57b9 100644 --- a/lib/pages/paynym/paynym_claim_view.dart +++ b/lib/pages/paynym/paynym_claim_view.dart @@ -87,6 +87,7 @@ class _PaynymClaimViewState extends ConsumerState { unawaited( showDialog( context: context, + barrierDismissible: false, builder: (context) => const ClaimingPaynymDialog(), ).then((value) => shouldCancel = value == true), ); diff --git a/lib/pages/paynym/paynym_home_view.dart b/lib/pages/paynym/paynym_home_view.dart index d3c3771ea..6d4713227 100644 --- a/lib/pages/paynym/paynym_home_view.dart +++ b/lib/pages/paynym/paynym_home_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/paynym/paynym_account.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart'; import 'package:stackwallet/pages/paynym/dialogs/paynym_qr_popup.dart'; import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -21,16 +22,17 @@ import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/share_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/toggle.dart'; +import 'package:tuple/tuple.dart'; class PaynymHomeView extends StatefulWidget { const PaynymHomeView({ Key? key, required this.walletId, - required this.nymAccount, + required this.paynymAccount, }) : super(key: key); final String walletId; - final PaynymAccount nymAccount; + final PaynymAccount paynymAccount; static const String routeName = "/paynymHome"; @@ -73,7 +75,13 @@ class _PaynymHomeViewState extends State { color: Theme.of(context).extension()!.textDark, ), onPressed: () { - // todo add ? + Navigator.of(context).pushNamed( + AddNewPaynymFollowView.routeName, + arguments: Tuple2( + widget.walletId, + widget.paynymAccount, + ), + ); }, ), ), @@ -90,7 +98,7 @@ class _PaynymHomeViewState extends State { color: Theme.of(context).extension()!.textDark, ), onPressed: () { - // todo add ? + // todo info ? }, ), ), @@ -107,20 +115,20 @@ class _PaynymHomeViewState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ PayNymBot( - paymentCodeString: widget.nymAccount.codes.first.code, + paymentCodeString: widget.paynymAccount.codes.first.code, ), const SizedBox( height: 10, ), Text( - widget.nymAccount.nymName, + widget.paynymAccount.nymName, style: STextStyles.desktopMenuItemSelected(context), ), const SizedBox( height: 4, ), Text( - Format.shorten(widget.nymAccount.codes.first.code, 12, 5), + Format.shorten(widget.paynymAccount.codes.first.code, 12, 5), style: STextStyles.label(context), ), const SizedBox( @@ -143,7 +151,7 @@ class _PaynymHomeViewState extends State { onPressed: () async { await Clipboard.setData( ClipboardData( - text: widget.nymAccount.codes.first.code, + text: widget.paynymAccount.codes.first.code, ), ); unawaited( @@ -196,7 +204,7 @@ class _PaynymHomeViewState extends State { showDialog( context: context, builder: (context) => PaynymQrPopup( - paynymAccount: widget.nymAccount, + paynymAccount: widget.paynymAccount, ), ); }, diff --git a/lib/pages/paynym/subwidgets/featured_paynyms_widget.dart b/lib/pages/paynym/subwidgets/featured_paynyms_widget.dart index a9dd34309..ab63f8411 100644 --- a/lib/pages/paynym/subwidgets/featured_paynyms_widget.dart +++ b/lib/pages/paynym/subwidgets/featured_paynyms_widget.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart'; +import 'package:stackwallet/pages/paynym/subwidgets/paynym_card.dart'; import 'package:stackwallet/utilities/featured_paynyms.dart'; -import 'package:stackwallet/utilities/format.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class FeaturedPaynymsWidget extends StatelessWidget { @@ -28,49 +25,9 @@ class FeaturedPaynymsWidget extends StatelessWidget { .backgroundAppBar, height: 1, ), - Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - PayNymBot( - size: 32, - paymentCodeString: entries[i].value, - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - entries[i].key, - style: STextStyles.w500_12(context), - ), - const SizedBox( - height: 2, - ), - Text( - Format.shorten(entries[i].value, 12, 5), - style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ], - ), - ), - PrimaryButton( - width: 84, - buttonHeight: ButtonHeight.l, - label: "Follow", - onPressed: () { - // todo : follow - }, - ) - ], - ), + PaynymCard( + label: entries[i].key, + paymentCodeString: entries[i].value, ), ], ), diff --git a/lib/pages/paynym/subwidgets/paynym_bot.dart b/lib/pages/paynym/subwidgets/paynym_bot.dart index bdb909f9c..6c6c4ae97 100644 --- a/lib/pages/paynym/subwidgets/paynym_bot.dart +++ b/lib/pages/paynym/subwidgets/paynym_bot.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; class PayNymBot extends StatelessWidget { const PayNymBot({ @@ -15,21 +14,13 @@ class PayNymBot extends StatelessWidget { Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(size / 2), - child: Stack( - children: [ - SizedBox( - width: size, - height: size, - child: const LoadingIndicator(), - ), - SizedBox( - width: size, - height: size, - child: Image.network( - "https://paynym.is/$paymentCodeString/avatar", - ), - ), - ], + child: SizedBox( + width: size, + height: size, + child: Image.network( + "https://paynym.is/$paymentCodeString/avatar", + // todo: loading indicator that doesn't lag + ), ), ); } diff --git a/lib/pages/paynym/subwidgets/paynym_card.dart b/lib/pages/paynym/subwidgets/paynym_card.dart new file mode 100644 index 000000000..cc3e60524 --- /dev/null +++ b/lib/pages/paynym/subwidgets/paynym_card.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; + +class PaynymCard extends StatefulWidget { + const PaynymCard({ + Key? key, + required this.label, + required this.paymentCodeString, + }) : super(key: key); + + final String label; + final String paymentCodeString; + + @override + State createState() => _PaynymCardState(); +} + +class _PaynymCardState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + PayNymBot( + size: 32, + paymentCodeString: widget.paymentCodeString, + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.label, + style: STextStyles.w500_12(context), + ), + const SizedBox( + height: 2, + ), + Text( + Format.shorten(widget.paymentCodeString, 12, 5), + style: STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ), + ), + PrimaryButton( + width: 84, + buttonHeight: ButtonHeight.l, + label: "Follow", + onPressed: () { + // todo : follow + }, + ) + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 3b4c4e103..30a7313b6 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -37,6 +37,7 @@ import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; +import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; @@ -209,6 +210,21 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => PaynymHomeView( + walletId: args.item1, + paynymAccount: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case AddNewPaynymFollowView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => AddNewPaynymFollowView( walletId: args.item1, nymAccount: args.item2, ), diff --git a/lib/utilities/paynym_api.dart b/lib/utilities/paynym_api.dart index 65de910a6..c8db9f4f4 100644 --- a/lib/utilities/paynym_api.dart +++ b/lib/utilities/paynym_api.dart @@ -173,10 +173,13 @@ class PaynymAPI { // | 200 | Nym found and returned | // | 404 | Nym not found | // | 400 | Bad request | - - Future nym(String code) async { - final map = await _post("/nym", {"nym": code}); + Future nym(String code, [bool compact = false]) async { + final Map requestBody = {"nym": code}; + if (compact) { + requestBody["compact"] = true; + } try { + final map = await _post("/nym", requestBody); return PaynymAccount.fromMap(map); } catch (_) { return null; diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 8afbf77dd..51ade8153 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -7,6 +7,29 @@ class STextStyles { static StackColors _theme(BuildContext context) => Theme.of(context).extension()!; + static TextStyle sectionLabelMedium12(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + } + } + static TextStyle pageTitleH1(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart index 9441168e7..de8351122 100644 --- a/lib/widgets/desktop/primary_button.dart +++ b/lib/widgets/desktop/primary_button.dart @@ -63,6 +63,16 @@ class PrimaryButton extends StatelessWidget { : STextStyles.desktopButtonDisabled(context); } } else { + if (buttonHeight == ButtonHeight.l) { + return STextStyles.button(context).copyWith( + fontSize: 10, + color: enabled + ? Theme.of(context).extension()!.buttonTextPrimary + : Theme.of(context) + .extension()! + .buttonTextPrimaryDisabled, + ); + } return STextStyles.button(context).copyWith( color: enabled ? Theme.of(context).extension()!.buttonTextPrimary