diff --git a/lib/core/di/repositories_module.dart b/lib/core/di/repositories_module.dart index a52ca0de..96eb65b4 100644 --- a/lib/core/di/repositories_module.dart +++ b/lib/core/di/repositories_module.dart @@ -23,5 +23,5 @@ void _registerRepositoriesModule() { _registerLazySingleton(() => TransactionHistoryRepository(service: _getIt())); - _registerLazySingleton(() => ProposalRepository(_getIt(),_getIt(),_getIt())); + _registerLazySingleton(() => ProposalRepository(_getIt(),_getIt(),_getIt(),_getIt(),_getIt())); } \ No newline at end of file diff --git a/lib/core/network/api/actions/vote_action_factory.dart b/lib/core/network/api/actions/vote_action_factory.dart index 4c471a90..2f634f4e 100644 --- a/lib/core/network/api/actions/vote_action_factory.dart +++ b/lib/core/network/api/actions/vote_action_factory.dart @@ -1,22 +1,17 @@ import 'package:hypha_wallet/core/crypto/seeds_esr/eos_action.dart'; +import 'package:hypha_wallet/core/network/models/vote_model.dart'; -class Voteactionfactory { +class VoteActionFactory { // Vote action - // Note: Don't hard-code the dao contract since it's different on different networks - // get daoContract by calling - // final daoContract = remoteConfigService.daoContract(network: network); - // - static EOSAction voteAction(String daoContract, String voter, int proposalId, String vote) { - if (vote != 'pass' && vote != 'fail') { - throw 'vote needs to be one of pass or fail'; - } + static EOSAction voteAction(String daoContract, String voter, String proposalId, VoteStatus vote) { return EOSAction() ..account = daoContract ..name = 'vote' ..data = { 'voter': voter, 'proposal_id': proposalId, - 'vote': vote, + 'vote': vote.name, + 'notes': '' }; } } diff --git a/lib/core/network/repository/proposal_repository.dart b/lib/core/network/repository/proposal_repository.dart index 8a62c983..fcc4356c 100644 --- a/lib/core/network/repository/proposal_repository.dart +++ b/lib/core/network/repository/proposal_repository.dart @@ -1,28 +1,38 @@ import 'package:hypha_wallet/core/error_handler/model/hypha_error.dart'; import 'package:hypha_wallet/core/extension/base_proposal_model_extension.dart'; import 'package:hypha_wallet/core/logging/log_helper.dart'; +import 'package:hypha_wallet/core/network/api/actions/vote_action_factory.dart'; import 'package:hypha_wallet/core/network/api/services/dao_service.dart'; import 'package:hypha_wallet/core/network/api/services/proposal_service.dart'; +import 'package:hypha_wallet/core/network/api/services/remote_config_service.dart'; import 'package:hypha_wallet/core/network/models/dao_data_model.dart'; import 'package:hypha_wallet/core/network/models/dao_proposals_model.dart'; import 'package:hypha_wallet/core/network/models/network.dart'; import 'package:hypha_wallet/core/network/models/proposal_details_model.dart'; import 'package:hypha_wallet/core/network/models/proposal_model.dart'; import 'package:hypha_wallet/core/network/models/user_profile_data.dart'; +import 'package:hypha_wallet/core/network/models/vote_model.dart'; import 'package:hypha_wallet/core/network/repository/profile_repository.dart'; import 'package:hypha_wallet/ui/architecture/result/result.dart'; import 'package:hypha_wallet/ui/profile/interactor/profile_data.dart'; import 'package:hypha_wallet/ui/proposals/filter/interactor/filter_status.dart'; import 'package:hypha_wallet/ui/proposals/list/interactor/get_proposals_use_case_input.dart'; +import '../../crypto/seeds_esr/eos_action.dart'; +import '../api/eos_service.dart'; + class ProposalRepository { final ProposalService _proposalService; final ProfileService _profileService; final DaoService _daoService; + final EOSService _eosService; + final RemoteConfigService _remoteConfigService; ProposalRepository( + this._remoteConfigService, + this._eosService, this._daoService, this._proposalService, this._profileService); @@ -213,4 +223,24 @@ class ProposalRepository { return Result.error(result.asError!.error); } } + + Future> castVote( + String proposalId, + VoteStatus vote, + UserProfileData user + ) async { + // Get the DAO contract for the user's network + final String daoContract = _remoteConfigService.daoContract(network: user.network); + // Create the EOS action for casting the vote + final EOSAction eosAction = VoteActionFactory.voteAction(daoContract, user.accountName, proposalId, vote); + + try { + // Execute the action using EOS service and get the result + final castVoteResult = await _eosService.runAction(signer: user, action: eosAction); + return Result.value(castVoteResult.asValue!.value); + } catch (e, stackTrace) { + LogHelper.e('Error casting vote', error: e, stacktrace: stackTrace); + return Result.error(HyphaError.generic('Failed to cast vote')); + } + } } diff --git a/lib/ui/profile/usecases/fetch_profile_use_case.dart b/lib/ui/profile/usecases/fetch_profile_use_case.dart index 830bd697..9df694dc 100644 --- a/lib/ui/profile/usecases/fetch_profile_use_case.dart +++ b/lib/ui/profile/usecases/fetch_profile_use_case.dart @@ -31,7 +31,6 @@ class FetchProfileUseCase { final Result, HyphaError> daosResult = futureResults.first as Result, HyphaError>; final Result profileResult = futureResults.last as Result; - if (profileResult.isValue && daosResult.isValue) { var profile = profileResult.asValue!.value; profile = profile.updateDaos(daosResult.asValue!.value); diff --git a/lib/ui/proposals/details/components/proposal_details_view.dart b/lib/ui/proposals/details/components/proposal_details_view.dart index c9ed7ec4..3d90f016 100644 --- a/lib/ui/proposals/details/components/proposal_details_view.dart +++ b/lib/ui/proposals/details/components/proposal_details_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:get/get_utils/src/extensions/context_extensions.dart'; +import 'package:get/get.dart'; import 'package:hypha_wallet/core/extension/base_proposal_model_extension.dart'; import 'package:hypha_wallet/core/extension/proposal_details_model_extension.dart'; import 'package:hypha_wallet/core/network/models/proposal_details_model.dart'; @@ -12,6 +12,7 @@ import 'package:hypha_wallet/design/dao_image.dart'; import 'package:hypha_wallet/design/dividers/hypha_divider.dart'; import 'package:hypha_wallet/design/hypha_colors.dart'; import 'package:hypha_wallet/design/themes/extensions/theme_extension_provider.dart'; +import 'package:hypha_wallet/ui/blocs/authentication/authentication_bloc.dart'; import 'package:hypha_wallet/ui/proposals/components/proposal_button.dart'; import 'package:hypha_wallet/ui/proposals/components/proposal_creator.dart'; import 'package:hypha_wallet/ui/proposals/components/proposal_expiration_timer.dart'; @@ -33,6 +34,7 @@ class _ProposalDetailsViewState extends State { final ValueNotifier _isOverflowingNotifier = ValueNotifier(false); final ValueNotifier _isExpandedNotifier = ValueNotifier(false); final ValueNotifier _detailsNotifier = ValueNotifier(null); + final ValueNotifier _changeVoteNotifier = ValueNotifier(false); void _checkIfTextIsOverflowing() { final TextPainter textPainter = TextPainter( @@ -46,6 +48,15 @@ class _ProposalDetailsViewState extends State { } } + VoteModel? _userVote(BuildContext context, List? voters) { + final userProfileData = + context.read().state.userProfileData; + final myVoteIndex = voters?.indexWhere( + (element) => element.voter == userProfileData?.accountName); + if (myVoteIndex == null || myVoteIndex == -1 || voters == null) return null; + return voters[myVoteIndex]; + } + @override Widget build(BuildContext context) { return HyphaPageBackground( @@ -61,6 +72,8 @@ class _ProposalDetailsViewState extends State { return HyphaBodyWidget( pageState: state.pageState, success: (context) { + final VoteModel? userVote = + _userVote(context, state.proposalDetailsModel!.votes); final ProposalDetailsModel _proposalDetailsModel = state.proposalDetailsModel!; final List passVoters = _proposalDetailsModel @@ -362,36 +375,50 @@ class _ProposalDetailsViewState extends State { _proposalDetailsModel.formatExpiration(), ), ), - const HyphaDivider(), /// Vote Section - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Text( - 'Cast your Vote', - style: context.hyphaTextTheme.smallTitles, - ), - ), - ...List.generate( - 3, - (index) => Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: HyphaAppButton( - title: index == 0 - ? 'Yes' - : index == 1 - ? 'Abstain' - : 'No', - onPressed: () async {}, - buttonType: ButtonType.danger, - buttonColor: index == 0 - ? HyphaColors.success - : index == 1 - ? HyphaColors.lightBlack - : HyphaColors.error, - ), + if (_proposalDetailsModel.formatExpiration() != + 'Expired') + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const HyphaDivider(), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 20), + child: Text( + 'Cast your Vote', + style: context.hyphaTextTheme.smallTitles, + ), + ), + ValueListenableBuilder( + valueListenable: _changeVoteNotifier, + builder: (context, isChangingVote, child) => + userVote != null && isChangingVote == false + ? HyphaAppButton( + onPressed: () { + _changeVoteNotifier.value = true; + }, + buttonType: ButtonType.danger, + buttonColor: { + VoteStatus.pass: + HyphaColors.success, + VoteStatus.fail: + HyphaColors.error, + }[userVote.voteStatus] ?? + HyphaColors.lightBlack, + title: { + VoteStatus.pass: + 'You Voted Yes', + VoteStatus.fail: + 'You Voted No', + }[userVote.voteStatus] ?? + 'You chose to abstain', + ) + : _buildVoteWidget(context), + ), + ], ), - ), const SizedBox(height: 20), ], ), @@ -404,6 +431,49 @@ class _ProposalDetailsViewState extends State { } } +Widget _buildVoteWidget(BuildContext context) => Column( + children: List.generate( + 3, + (index) => Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: HyphaAppButton( + title: index == 0 + ? 'Yes' + : index == 1 + ? 'Abstain' + : 'No', + onPressed: () async { + late final VoteStatus voteStatus; + + // Determine the vote status based on the index + switch (index) { + case 0: + voteStatus = VoteStatus.pass; + break; + case 1: + voteStatus = VoteStatus.abstain; + break; + default: + voteStatus = VoteStatus.fail; + break; + } + + // Get the bloc instances + final proposalDetailBloc = context.read(); + // Dispatch the castVote event + proposalDetailBloc.add(ProposalDetailEvent.castVote(voteStatus)); + }, + buttonType: ButtonType.danger, + buttonColor: index == 0 + ? HyphaColors.success + : index == 1 + ? HyphaColors.lightBlack + : HyphaColors.error, + ), + ), + ), + ); + Widget _buildTokenRow( BuildContext context, ProposalDetailsModel proposalDetailsModel, diff --git a/lib/ui/proposals/details/interactor/proposal_detail_bloc.dart b/lib/ui/proposals/details/interactor/proposal_detail_bloc.dart index aeafff43..41a0b148 100644 --- a/lib/ui/proposals/details/interactor/proposal_detail_bloc.dart +++ b/lib/ui/proposals/details/interactor/proposal_detail_bloc.dart @@ -1,32 +1,61 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:get/get.dart' as Get; +import 'package:get_it/get_it.dart'; import 'package:hypha_wallet/core/error_handler/error_handler_manager.dart'; import 'package:hypha_wallet/core/error_handler/model/hypha_error.dart'; import 'package:hypha_wallet/core/network/models/proposal_details_model.dart'; +import 'package:hypha_wallet/core/network/models/vote_model.dart'; import 'package:hypha_wallet/ui/architecture/interactor/page_states.dart'; import 'package:hypha_wallet/ui/architecture/result/result.dart'; import 'package:hypha_wallet/ui/proposals/details/usecases/get_proposal_details_use_case.dart'; +import 'package:hypha_wallet/ui/proposals/list/interactor/proposals_bloc.dart'; part 'proposal_detail_bloc.freezed.dart'; + part 'proposal_detail_event.dart'; + part 'proposal_detail_state.dart'; -class ProposalDetailBloc extends Bloc { +class ProposalDetailBloc + extends Bloc { final GetProposalDetailsUseCase _getProposalDetailsUseCase; final ErrorHandlerManager _errorHandlerManager; final String _proposalId; - ProposalDetailBloc(this._proposalId,this._getProposalDetailsUseCase, this._errorHandlerManager) : super(const ProposalDetailState()) { + ProposalDetailBloc(this._proposalId, this._getProposalDetailsUseCase, + this._errorHandlerManager) + : super(const ProposalDetailState()) { on<_Initial>(_initial); + on<_CastVote>(_castVote); } - Future _initial(_Initial event, Emitter emit) async { + Future _initial( + _Initial event, Emitter emit) async { emit(state.copyWith(pageState: PageState.loading)); - final Result result = await _getProposalDetailsUseCase.run(_proposalId); + final Result result = + await _getProposalDetailsUseCase.run(_proposalId); + if (result.isValue) { + emit(state.copyWith( + pageState: PageState.success, + proposalDetailsModel: result.asValue!.value)); + } else { + await _errorHandlerManager.handlerError(result.asError!.error); + emit(state.copyWith(pageState: PageState.failure)); + } + } + + Future _castVote( + _CastVote event, Emitter emit) async { + emit(state.copyWith(pageState: PageState.loading)); + final Result result = + await _getProposalDetailsUseCase.castVote(_proposalId, event.vote); if (result.isValue) { - emit(state.copyWith(pageState: PageState.success, proposalDetailsModel: result.asValue!.value)); + GetIt.I.get().add(const ProposalsEvent.initial()); + Get.Get.back(); + emit(state.copyWith(pageState: PageState.success)); } else { await _errorHandlerManager.handlerError(result.asError!.error); emit(state.copyWith(pageState: PageState.failure)); diff --git a/lib/ui/proposals/details/interactor/proposal_detail_bloc.freezed.dart b/lib/ui/proposals/details/interactor/proposal_detail_bloc.freezed.dart index 713b2be4..b2a093ba 100644 --- a/lib/ui/proposals/details/interactor/proposal_detail_bloc.freezed.dart +++ b/lib/ui/proposals/details/interactor/proposal_detail_bloc.freezed.dart @@ -19,32 +19,38 @@ mixin _$ProposalDetailEvent { @optionalTypeArgs TResult when({ required TResult Function() initial, + required TResult Function(VoteStatus vote) castVote, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? initial, + TResult? Function(VoteStatus vote)? castVote, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function()? initial, + TResult Function(VoteStatus vote)? castVote, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult map({ required TResult Function(_Initial value) initial, + required TResult Function(_CastVote value) castVote, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_Initial value)? initial, + TResult? Function(_CastVote value)? castVote, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ TResult Function(_Initial value)? initial, + TResult Function(_CastVote value)? castVote, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -113,6 +119,7 @@ class _$InitialImpl implements _Initial { @optionalTypeArgs TResult when({ required TResult Function() initial, + required TResult Function(VoteStatus vote) castVote, }) { return initial(); } @@ -121,6 +128,7 @@ class _$InitialImpl implements _Initial { @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? initial, + TResult? Function(VoteStatus vote)? castVote, }) { return initial?.call(); } @@ -129,6 +137,7 @@ class _$InitialImpl implements _Initial { @optionalTypeArgs TResult maybeWhen({ TResult Function()? initial, + TResult Function(VoteStatus vote)? castVote, required TResult orElse(), }) { if (initial != null) { @@ -141,6 +150,7 @@ class _$InitialImpl implements _Initial { @optionalTypeArgs TResult map({ required TResult Function(_Initial value) initial, + required TResult Function(_CastVote value) castVote, }) { return initial(this); } @@ -149,6 +159,7 @@ class _$InitialImpl implements _Initial { @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_Initial value)? initial, + TResult? Function(_CastVote value)? castVote, }) { return initial?.call(this); } @@ -157,6 +168,7 @@ class _$InitialImpl implements _Initial { @optionalTypeArgs TResult maybeMap({ TResult Function(_Initial value)? initial, + TResult Function(_CastVote value)? castVote, required TResult orElse(), }) { if (initial != null) { @@ -170,6 +182,146 @@ abstract class _Initial implements ProposalDetailEvent { const factory _Initial() = _$InitialImpl; } +/// @nodoc +abstract class _$$CastVoteImplCopyWith<$Res> { + factory _$$CastVoteImplCopyWith( + _$CastVoteImpl value, $Res Function(_$CastVoteImpl) then) = + __$$CastVoteImplCopyWithImpl<$Res>; + @useResult + $Res call({VoteStatus vote}); +} + +/// @nodoc +class __$$CastVoteImplCopyWithImpl<$Res> + extends _$ProposalDetailEventCopyWithImpl<$Res, _$CastVoteImpl> + implements _$$CastVoteImplCopyWith<$Res> { + __$$CastVoteImplCopyWithImpl( + _$CastVoteImpl _value, $Res Function(_$CastVoteImpl) _then) + : super(_value, _then); + + /// Create a copy of ProposalDetailEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? vote = null, + }) { + return _then(_$CastVoteImpl( + null == vote + ? _value.vote + : vote // ignore: cast_nullable_to_non_nullable + as VoteStatus, + )); + } +} + +/// @nodoc + +class _$CastVoteImpl implements _CastVote { + const _$CastVoteImpl(this.vote); + + @override + final VoteStatus vote; + + @override + String toString() { + return 'ProposalDetailEvent.castVote(vote: $vote)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CastVoteImpl && + (identical(other.vote, vote) || other.vote == vote)); + } + + @override + int get hashCode => Object.hash(runtimeType, vote); + + /// Create a copy of ProposalDetailEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CastVoteImplCopyWith<_$CastVoteImpl> get copyWith => + __$$CastVoteImplCopyWithImpl<_$CastVoteImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(VoteStatus vote) castVote, + }) { + return castVote(vote); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(VoteStatus vote)? castVote, + }) { + return castVote?.call(vote); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(VoteStatus vote)? castVote, + required TResult orElse(), + }) { + if (castVote != null) { + return castVote(vote); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_CastVote value) castVote, + }) { + return castVote(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_CastVote value)? castVote, + }) { + return castVote?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_CastVote value)? castVote, + required TResult orElse(), + }) { + if (castVote != null) { + return castVote(this); + } + return orElse(); + } +} + +abstract class _CastVote implements ProposalDetailEvent { + const factory _CastVote(final VoteStatus vote) = _$CastVoteImpl; + + VoteStatus get vote; + + /// Create a copy of ProposalDetailEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CastVoteImplCopyWith<_$CastVoteImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$ProposalDetailState { PageState get pageState => throw _privateConstructorUsedError; diff --git a/lib/ui/proposals/details/interactor/proposal_detail_event.dart b/lib/ui/proposals/details/interactor/proposal_detail_event.dart index 1219aa8e..f0b20531 100644 --- a/lib/ui/proposals/details/interactor/proposal_detail_event.dart +++ b/lib/ui/proposals/details/interactor/proposal_detail_event.dart @@ -3,4 +3,5 @@ part of 'proposal_detail_bloc.dart'; @freezed class ProposalDetailEvent with _$ProposalDetailEvent { const factory ProposalDetailEvent.initial() = _Initial; + const factory ProposalDetailEvent.castVote(VoteStatus vote) = _CastVote; } diff --git a/lib/ui/proposals/details/usecases/get_proposal_details_use_case.dart b/lib/ui/proposals/details/usecases/get_proposal_details_use_case.dart index 650fe8e8..58fc24f7 100644 --- a/lib/ui/proposals/details/usecases/get_proposal_details_use_case.dart +++ b/lib/ui/proposals/details/usecases/get_proposal_details_use_case.dart @@ -1,5 +1,6 @@ import 'package:hypha_wallet/core/error_handler/model/hypha_error.dart'; import 'package:hypha_wallet/core/network/models/proposal_details_model.dart'; +import 'package:hypha_wallet/core/network/models/vote_model.dart'; import 'package:hypha_wallet/core/network/repository/auth_repository.dart'; import 'package:hypha_wallet/core/network/repository/proposal_repository.dart'; import 'package:hypha_wallet/ui/architecture/result/result.dart'; @@ -10,4 +11,6 @@ class GetProposalDetailsUseCase { GetProposalDetailsUseCase(this._authRepository, this._proposalRepository); Future> run(String proposalId) async => _proposalRepository.getProposalDetails(proposalId,_authRepository.authDataOrCrash.userProfileData); + Future> castVote(String proposalId,VoteStatus vote) async => _proposalRepository.castVote(proposalId,vote,_authRepository.authDataOrCrash.userProfileData); + } \ No newline at end of file diff --git a/lib/ui/proposals/list/components/hypha_proposals_action_card.dart b/lib/ui/proposals/list/components/hypha_proposals_action_card.dart index bcf1ea78..b5ac4b23 100644 --- a/lib/ui/proposals/list/components/hypha_proposals_action_card.dart +++ b/lib/ui/proposals/list/components/hypha_proposals_action_card.dart @@ -60,9 +60,7 @@ class HyphaProposalsActionCard extends StatelessWidget { ? HyphaColors.success : HyphaColors.error), const SizedBox(height: 20), - ProposalExpirationTimer( - _proposalModel.formatExpiration(), - ), + ProposalExpirationTimer(_proposalModel.formatExpiration(),), const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: HyphaDivider(), @@ -83,7 +81,7 @@ class HyphaProposalsActionCard extends StatelessWidget { return BlocBuilder( builder: (context, state) { final myVoteIndex = _proposalModel.votes?.indexWhere((element) => - element.voter == state.userProfileData?.userNameOrAccount); + element.voter == state.userProfileData?.accountName); if (myVoteIndex == null || myVoteIndex == -1) return const SizedBox(); final voteStatus = _proposalModel.votes![myVoteIndex].voteStatus; final color = voteStatus == VoteStatus.pass @@ -105,7 +103,7 @@ class HyphaProposalsActionCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), ), width: double.infinity, - height: 460, + height: 475, child: Text( statusText, style: context.hyphaTextTheme.smallTitles