WIP follow/unfollow

This commit is contained in:
julian 2022-12-21 17:02:14 -06:00
parent 0711bd03cf
commit 7631d3f3c6
11 changed files with 377 additions and 77 deletions

View file

@ -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<String, dynamic> 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?;

View file

@ -26,19 +26,27 @@ class PaynymAccount {
codes = (map["codes"] as List<dynamic>)
.map((e) => PaynymCode.fromMap(Map<String, dynamic>.from(e as Map)))
.toList(),
followers = (map["followers"] as List<dynamic>)
.map((e) => e["nymId"] as String)
.toList(),
following = (map["following"] as List<dynamic>)
.map((e) => e["nymId"] as String)
.toList();
followers = [],
following = [] {
final f1 = map["followers"] as List<dynamic>;
for (final item in f1) {
followers.add(Map<String, dynamic>.from(item as Map)["nymId"] as String);
}
final f2 = map["following"] as List<dynamic>;
for (final item in f2) {
final nymId = Map<String, dynamic>.from(item as Map)["nymId"] as String;
print(nymId + "DDDDDDDDDDDDD");
following.add(nymId);
}
}
Map<String, dynamic> 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

View file

@ -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,
),
),
],

View file

@ -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<PaynymClaimView> {
.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();
}
},

View file

@ -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<PaynymAccount?>((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<PaynymHomeView> createState() => _PaynymHomeViewState();
ConsumerState<PaynymHomeView> createState() => _PaynymHomeViewState();
}
class _PaynymHomeViewState extends State<PaynymHomeView> {
class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
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<PaynymHomeView> {
onPressed: () {
Navigator.of(context).pushNamed(
AddNewPaynymFollowView.routeName,
arguments: Tuple2(
widget.walletId,
widget.paynymAccount,
),
arguments: widget.walletId,
);
},
),
@ -114,21 +121,54 @@ class _PaynymHomeViewState extends State<PaynymHomeView> {
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<PaynymHomeView> {
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<PaynymHomeView> {
showDialog<void>(
context: context,
builder: (context) => PaynymQrPopup(
paynymAccount: widget.paynymAccount,
paynymAccount: ref
.read(myPaynymAccountStateProvider.state)
.state!,
),
);
},

View file

@ -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,
),

View file

@ -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<PaynymCard> {
],
),
),
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<PaynymFollowToggleButton> createState() =>
_PaynymFollowToggleButtonState();
}
class _PaynymFollowToggleButtonState
extends ConsumerState<PaynymFollowToggleButton> {
Future<bool> follow() async {
bool loadingPopped = false;
unawaited(
showDialog<void>(
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<bool> unfollow() async {
bool loadingPopped = false;
unawaited(
showDialog<void>(
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;
}
},
);
}
}

View file

@ -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<WalletNavigationBar> {
.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(

View file

@ -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<String, PaynymAccount>) {
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<String, PaynymAccount>) {
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => AddNewPaynymFollowView(
walletId: args.item1,
nymAccount: args.item2,
walletId: args,
),
settings: RouteSettings(
name: settings.name,

View file

@ -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<String> 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<Map<String, dynamic>> prepareNotificationTransaction(
// String targetPaymentCode) async {}
}

View file

@ -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(