diff --git a/lib/models/paynym/created_paynym.dart b/lib/models/paynym/created_paynym.dart index 4efdcb85b..8716d7e10 100644 --- a/lib/models/paynym/created_paynym.dart +++ b/lib/models/paynym/created_paynym.dart @@ -1,6 +1,6 @@ class CreatedPaynym { final bool claimed; - final String nymAvatar; + final String? nymAvatar; final String? nymId; final String? nymName; final String? token; @@ -15,8 +15,8 @@ class CreatedPaynym { CreatedPaynym.fromMap(Map map) : claimed = map["claimed"] as bool, - nymAvatar = map["nymAvatar"] as String, - nymId = map["nymId"] as String?, + nymAvatar = map["nymAvatar"] as String?, + nymId = map["nymID"] as String?, nymName = map["nymName"] as String?, token = map["token"] as String?; diff --git a/lib/models/paynym/paynym_account.dart b/lib/models/paynym/paynym_account.dart index 08438a6b7..b2c989014 100644 --- a/lib/models/paynym/paynym_account.dart +++ b/lib/models/paynym/paynym_account.dart @@ -26,19 +26,27 @@ class PaynymAccount { codes = (map["codes"] as List) .map((e) => PaynymCode.fromMap(Map.from(e as Map))) .toList(), - followers = (map["followers"] as List) - .map((e) => e["nymId"] as String) - .toList(), - following = (map["following"] as List) - .map((e) => e["nymId"] as String) - .toList(); + followers = [], + following = [] { + final f1 = map["followers"] as List; + for (final item in f1) { + followers.add(Map.from(item as Map)["nymId"] as String); + } + + final f2 = map["following"] as List; + for (final item in f2) { + final nymId = Map.from(item as Map)["nymId"] as String; + print(nymId + "DDDDDDDDDDDDD"); + following.add(nymId); + } + } Map toMap() => { "nymID": nymID, "nymName": nymName, "codes": codes.map((e) => e.toMap()), - "followers": followers.map((e) => {"nymId": e}), - "following": followers.map((e) => {"nymId": e}), + "followers": followers.map((e) => {"nymId": e}).toList(), + "following": followers.map((e) => {"nymId": e}).toList(), }; @override diff --git a/lib/pages/paynym/add_new_paynym_follow_view.dart b/lib/pages/paynym/add_new_paynym_follow_view.dart index f6c3003f4..4382c9f7f 100644 --- a/lib/pages/paynym/add_new_paynym_follow_view.dart +++ b/lib/pages/paynym/add_new_paynym_follow_view.dart @@ -28,11 +28,9 @@ 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"; @@ -64,8 +62,7 @@ class _AddNewPaynymFollowViewState ).then((_) => didPopLoading = true), ); - final paynymAccount = - await ref.read(paynymAPIProvider).nym(_searchString, true); + final paynymAccount = await ref.read(paynymAPIProvider).nym(_searchString); if (mounted) { if (!didPopLoading) { @@ -143,7 +140,9 @@ class _AddNewPaynymFollowViewState const SizedBox( height: 12, ), - const FeaturedPaynymsWidget(), + FeaturedPaynymsWidget( + walletId: widget.walletId, + ), const SizedBox( height: 24, ), @@ -295,6 +294,7 @@ class _AddNewPaynymFollowViewState child: PaynymCard( label: _searchResult!.nymName, paymentCodeString: _searchResult!.codes.first.code, + walletId: widget.walletId, ), ), ], diff --git a/lib/pages/paynym/paynym_claim_view.dart b/lib/pages/paynym/paynym_claim_view.dart index aa79e57b9..c49992576 100644 --- a/lib/pages/paynym/paynym_claim_view.dart +++ b/lib/pages/paynym/paynym_claim_view.dart @@ -1,15 +1,15 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/paynym/dialogs/claiming_paynym_dialog.dart'; +import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/global/paynym_api_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/coin_paynym_extension.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/utilities/assets.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/utilities/util.dart'; @@ -106,39 +106,50 @@ class _PaynymClaimViewState extends ConsumerState { .read(paynymAPIProvider) .create(pCode.toString()); + debugPrint("created:$created"); + if (created.claimed) { // payment code already claimed debugPrint("pcode already claimed!!"); + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + } return; } - String token; - - if (created.token == null) { - // payment code already in db - // so we need to fetch a token - - token = await ref - .read(paynymAPIProvider) - .token(pCode.toString()); - } else { - token = created.token!; - } + final token = + await ref.read(paynymAPIProvider).token(pCode.toString()); // sign token with notification private key - final signatureBytes = await wallet.signWithNotificationKey( - Uint8List.fromList(token.codeUnits)); - final signature = Format.uint8listToString(signatureBytes); + final signature = + await wallet.signStringWithNotificationKey(token); // claim paynym account final claim = await ref.read(paynymAPIProvider).claim(token, signature); if (claim["claimed"] == pCode.toString()) { - // mark claim successful - } + final account = + await ref.read(paynymAPIProvider).nym(pCode.toString()); - if (mounted && !shouldCancel) { + ref.read(myPaynymAccountStateProvider.state).state = + account!; + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + await Navigator.of(context).pushNamed( + PaynymHomeView.routeName, + arguments: widget.walletId, + ); + } + } else if (mounted && !shouldCancel) { Navigator.of(context).pop(); } }, diff --git a/lib/pages/paynym/paynym_home_view.dart b/lib/pages/paynym/paynym_home_view.dart index 6d4713227..c633c0aa6 100644 --- a/lib/pages/paynym/paynym_home_view.dart +++ b/lib/pages/paynym/paynym_home_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/paynym/paynym_account.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; @@ -22,26 +23,35 @@ 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 { +final myPaynymAccountStateProvider = + StateProvider((ref) => null); + +class PaynymHomeView extends ConsumerStatefulWidget { const PaynymHomeView({ Key? key, required this.walletId, - required this.paynymAccount, }) : super(key: key); final String walletId; - final PaynymAccount paynymAccount; static const String routeName = "/paynymHome"; @override - State createState() => _PaynymHomeViewState(); + ConsumerState createState() => _PaynymHomeViewState(); } -class _PaynymHomeViewState extends State { +class _PaynymHomeViewState extends ConsumerState { bool showFollowing = false; + int secretCount = 0; + Timer? timer; + + @override + void dispose() { + timer?.cancel(); + timer = null; + super.dispose(); + } @override Widget build(BuildContext context) { @@ -77,10 +87,7 @@ class _PaynymHomeViewState extends State { onPressed: () { Navigator.of(context).pushNamed( AddNewPaynymFollowView.routeName, - arguments: Tuple2( - widget.walletId, - widget.paynymAccount, - ), + arguments: widget.walletId, ); }, ), @@ -114,21 +121,54 @@ class _PaynymHomeViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - PayNymBot( - paymentCodeString: widget.paynymAccount.codes.first.code, + GestureDetector( + onTap: () { + secretCount++; + if (secretCount > 5) { + debugPrint( + "My Account: ${ref.read(myPaynymAccountStateProvider.state).state}"); + debugPrint( + "My Account: ${ref.read(myPaynymAccountStateProvider.state).state!.following}"); + secretCount = 0; + } + + timer ??= Timer( + const Duration(milliseconds: 1500), + () { + secretCount = 0; + timer = null; + }, + ); + }, + child: PayNymBot( + paymentCodeString: ref + .watch(myPaynymAccountStateProvider.state) + .state! + .codes + .first + .code, + ), ), const SizedBox( height: 10, ), Text( - widget.paynymAccount.nymName, + ref.watch(myPaynymAccountStateProvider.state).state!.nymName, style: STextStyles.desktopMenuItemSelected(context), ), const SizedBox( height: 4, ), Text( - Format.shorten(widget.paynymAccount.codes.first.code, 12, 5), + Format.shorten( + ref + .watch(myPaynymAccountStateProvider.state) + .state! + .codes + .first + .code, + 12, + 5), style: STextStyles.label(context), ), const SizedBox( @@ -151,7 +191,12 @@ class _PaynymHomeViewState extends State { onPressed: () async { await Clipboard.setData( ClipboardData( - text: widget.paynymAccount.codes.first.code, + text: ref + .read(myPaynymAccountStateProvider.state) + .state! + .codes + .first + .code, ), ); unawaited( @@ -204,7 +249,9 @@ class _PaynymHomeViewState extends State { showDialog( context: context, builder: (context) => PaynymQrPopup( - paynymAccount: widget.paynymAccount, + paynymAccount: ref + .read(myPaynymAccountStateProvider.state) + .state!, ), ); }, diff --git a/lib/pages/paynym/subwidgets/featured_paynyms_widget.dart b/lib/pages/paynym/subwidgets/featured_paynyms_widget.dart index ab63f8411..f9457374a 100644 --- a/lib/pages/paynym/subwidgets/featured_paynyms_widget.dart +++ b/lib/pages/paynym/subwidgets/featured_paynyms_widget.dart @@ -5,7 +5,12 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class FeaturedPaynymsWidget extends StatelessWidget { - const FeaturedPaynymsWidget({Key? key}) : super(key: key); + const FeaturedPaynymsWidget({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; @override Widget build(BuildContext context) { @@ -26,6 +31,7 @@ class FeaturedPaynymsWidget extends StatelessWidget { height: 1, ), PaynymCard( + walletId: walletId, label: entries[i].key, paymentCodeString: entries[i].value, ), diff --git a/lib/pages/paynym/subwidgets/paynym_card.dart b/lib/pages/paynym/subwidgets/paynym_card.dart index cc3e60524..6dd00c0ed 100644 --- a/lib/pages/paynym/subwidgets/paynym_card.dart +++ b/lib/pages/paynym/subwidgets/paynym_card.dart @@ -1,17 +1,29 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart'; +import 'package:stackwallet/providers/global/paynym_api_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/coin_paynym_extension.dart'; +import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.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/loading_indicator.dart'; class PaynymCard extends StatefulWidget { const PaynymCard({ Key? key, + required this.walletId, required this.label, required this.paymentCodeString, }) : super(key: key); + final String walletId; final String label; final String paymentCodeString; @@ -55,16 +67,225 @@ class _PaynymCardState extends State { ], ), ), - PrimaryButton( - width: 84, - buttonHeight: ButtonHeight.l, - label: "Follow", - onPressed: () { - // todo : follow - }, - ) + PaynymFollowToggleButton( + walletId: widget.walletId, + paymentCodeStringToFollow: widget.paymentCodeString, + ), + // PrimaryButton( + // width: 84, + // buttonHeight: ButtonHeight.l, + // label: "Follow", + // onPressed: () { + // // todo : follow + // }, + // ) ], ), ); } } + +class PaynymFollowToggleButton extends ConsumerStatefulWidget { + const PaynymFollowToggleButton({ + Key? key, + required this.walletId, + required this.paymentCodeStringToFollow, + }) : super(key: key); + + final String walletId; + final String paymentCodeStringToFollow; + + @override + ConsumerState createState() => + _PaynymFollowToggleButtonState(); +} + +class _PaynymFollowToggleButtonState + extends ConsumerState { + Future follow() async { + bool loadingPopped = false; + unawaited( + showDialog( + context: context, + builder: (context) => const LoadingIndicator( + width: 200, + ), + ).then( + (_) => loadingPopped = true, + ), + ); + + final wallet = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as DogecoinWallet; + + final followedAccount = await ref + .read(paynymAPIProvider) + .nym(widget.paymentCodeStringToFollow, true); + await Future.delayed(const Duration(milliseconds: 100)); + + final myPCode = await wallet.getPaymentCode(); + await Future.delayed(const Duration(milliseconds: 100)); + final token = await ref.read(paynymAPIProvider).token(myPCode.toString()); + + await Future.delayed(const Duration(milliseconds: 100)); + + // sign token with notification private key + final signature = await wallet.signStringWithNotificationKey(token); + await Future.delayed(const Duration(milliseconds: 100)); + + final result = await ref + .read(paynymAPIProvider) + .follow(token, signature, followedAccount!.codes.first.code); + await Future.delayed(const Duration(milliseconds: 100)); + + print("Follow result: $result"); + + if (result["following"] == followedAccount.nymID) { + if (!loadingPopped && mounted) { + Navigator.of(context).pop(); + } + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "You are following ${followedAccount.nymName}", + context: context, + ), + ); + ref + .read(myPaynymAccountStateProvider.state) + .state! + .following + .add(followedAccount.codes.first.code); + + setState(() { + isFollowing = true; + }); + + return true; + } else { + if (!loadingPopped && mounted) { + Navigator.of(context).pop(); + } + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to follow ${followedAccount.nymName}", + context: context, + ), + ); + + return false; + } + } + + Future unfollow() async { + bool loadingPopped = false; + unawaited( + showDialog( + context: context, + builder: (context) => const LoadingIndicator( + width: 200, + ), + ).then( + (_) => loadingPopped = true, + ), + ); + + final wallet = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as DogecoinWallet; + + final followedAccount = await ref + .read(paynymAPIProvider) + .nym(widget.paymentCodeStringToFollow, true); + + final myPCode = await wallet.getPaymentCode(); + final token = await ref.read(paynymAPIProvider).token(myPCode.toString()); + + // sign token with notification private key + final signature = await wallet.signStringWithNotificationKey(token); + + final result = await ref + .read(paynymAPIProvider) + .unfollow(token, signature, followedAccount!.codes.first.code); + + print("Unfollow result: $result"); + + if (result["unfollowing"] == followedAccount.nymID) { + if (!loadingPopped && mounted) { + Navigator.of(context).pop(); + } + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "You have unfollowed ${followedAccount.nymName}", + context: context, + ), + ); + ref + .read(myPaynymAccountStateProvider.state) + .state! + .following + .remove(followedAccount.codes.first.code); + + setState(() { + isFollowing = false; + }); + + return true; + } else { + if (!loadingPopped && mounted) { + Navigator.of(context).pop(); + } + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to unfollow ${followedAccount.nymName}", + context: context, + ), + ); + + return false; + } + } + + bool _lock = false; + late bool isFollowing; + + @override + void initState() { + isFollowing = ref + .read(myPaynymAccountStateProvider.state) + .state! + .following + .contains(widget.paymentCodeStringToFollow); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return PrimaryButton( + width: 84, + buttonHeight: ButtonHeight.l, + label: isFollowing ? "Unfollow" : "Follow", + onPressed: () async { + if (!_lock) { + _lock = true; + if (isFollowing) { + await unfollow(); + } else { + await follow(); + } + _lock = false; + } + }, + ); + } +} diff --git a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart index af5b19f14..596968c68 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -13,9 +13,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:tuple/tuple.dart'; - -import '../../../widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; class WalletNavigationBar extends StatefulWidget { const WalletNavigationBar({ @@ -135,12 +133,12 @@ class _WalletNavigationBarState extends State { .where((e) => e.code == code.toString() && e.claimed) .isNotEmpty) { + ref.read(myPaynymAccountStateProvider.state).state = + account; + await Navigator.of(context).pushNamed( PaynymHomeView.routeName, - arguments: Tuple2( - widget.walletId, - account, - ), + arguments: widget.walletId, ); } else { await Navigator.of(context).pushNamed( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 30a7313b6..203edef4a 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -6,7 +6,6 @@ import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; -import 'package:stackwallet/models/paynym/paynym_account.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; @@ -206,12 +205,11 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case PaynymHomeView.routeName: - if (args is Tuple2) { + if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => PaynymHomeView( - walletId: args.item1, - paynymAccount: args.item2, + walletId: args, ), settings: RouteSettings( name: settings.name, @@ -221,12 +219,11 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case AddNewPaynymFollowView.routeName: - if (args is Tuple2) { + if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => AddNewPaynymFollowView( - walletId: args.item1, - nymAccount: args.item2, + walletId: args, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/coins/coin_paynym_extension.dart b/lib/services/coins/coin_paynym_extension.dart index 366b2cf78..5b6082d75 100644 --- a/lib/services/coins/coin_paynym_extension.dart +++ b/lib/services/coins/coin_paynym_extension.dart @@ -5,6 +5,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:pointycastle/digests/sha256.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; +import 'package:stackwallet/utilities/format.dart'; extension PayNym on DogecoinWallet { // fetch or generate this wallet's bip47 payment code @@ -35,6 +36,15 @@ extension PayNym on DogecoinWallet { return signed; } + Future signStringWithNotificationKey(String data) async { + final bytes = + await signWithNotificationKey(Uint8List.fromList(data.codeUnits)); + return Format.uint8listToString(bytes); + // final bytes = + // await signWithNotificationKey(Uint8List.fromList(utf8.encode(data))); + // return Format.uint8listToString(bytes); + } + // Future> prepareNotificationTransaction( // String targetPaymentCode) async {} } diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart index de8351122..95c28696e 100644 --- a/lib/widgets/desktop/primary_button.dart +++ b/lib/widgets/desktop/primary_button.dart @@ -16,6 +16,7 @@ class PrimaryButton extends StatelessWidget { this.onPressed, this.enabled = true, this.buttonHeight, + this.iconSpacing = 10, }) : super(key: key); final double? width; @@ -25,6 +26,7 @@ class PrimaryButton extends StatelessWidget { final bool enabled; final Widget? icon; final ButtonHeight? buttonHeight; + final double? iconSpacing; TextStyle getStyle(bool isDesktop, BuildContext context) { if (isDesktop) { @@ -143,8 +145,8 @@ class PrimaryButton extends StatelessWidget { children: [ if (icon != null) icon!, if (icon != null && label != null) - const SizedBox( - width: 10, + SizedBox( + width: iconSpacing, ), if (label != null) Text(