MotorSignal-App/lib/features/social/clubs/club_membership_controller....

243 lines
8.0 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../services/auth/auth_providers.dart';
import '../../../services/clubs/club_providers.dart';
import '../../../services/clubs/club_service.dart';
/// State of active membership mutations (join, request, withdraw, accept, decline, leave).
@immutable
class ClubMembershipActionState {
final bool isSubmitting;
final String? error;
final String? success;
final String? activeClubId;
const ClubMembershipActionState({
this.isSubmitting = false,
this.error,
this.success,
this.activeClubId,
});
ClubMembershipActionState copyWith({
bool? isSubmitting,
String? error,
String? success,
String? activeClubId,
bool clearError = false,
bool clearSuccess = false,
}) {
return ClubMembershipActionState(
isSubmitting: isSubmitting ?? this.isSubmitting,
error: clearError ? null : (error ?? this.error),
success: clearSuccess ? null : (success ?? this.success),
activeClubId: activeClubId ?? this.activeClubId,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ClubMembershipActionState &&
runtimeType == other.runtimeType &&
isSubmitting == other.isSubmitting &&
error == other.error &&
success == other.success &&
activeClubId == other.activeClubId;
@override
int get hashCode =>
isSubmitting.hashCode ^
error.hashCode ^
success.hashCode ^
activeClubId.hashCode;
}
/// Controller coordinating membership lifecycle operations.
/// Interacts with authoritative ClubService and synchronizes Riverpod streams.
class ClubMembershipController extends StateNotifier<ClubMembershipActionState> {
final Ref _ref;
final String clubId;
ClubMembershipController(this._ref, this.clubId)
: super(ClubMembershipActionState(activeClubId: clubId));
void clearMessages() {
state = state.copyWith(clearError: true, clearSuccess: true);
}
/// Direct self-join for open admission clubs.
Future<bool> joinOpenClub([String? targetClubId]) async {
final effectiveClubId = targetClubId ?? clubId;
return _performMutation(
clubId: effectiveClubId,
mutation: (service, uid) => service.joinOpenClub(effectiveClubId, callerUid: uid),
successMessage: 'Successfully joined club!',
onSuccessInvalidate: () {
_ref.invalidate(currentMemberProvider(effectiveClubId));
_ref.invalidate(clubDetailProvider(effectiveClubId));
_ref.invalidate(myClubsProvider);
},
);
}
/// Request admission for request-policy clubs.
Future<bool> requestToJoinClub([String? targetClubId]) async {
final effectiveClubId = targetClubId ?? clubId;
return _performMutation(
clubId: effectiveClubId,
mutation: (service, uid) => service.requestToJoinClub(effectiveClubId, callerUid: uid),
successMessage: 'Join request submitted to club leadership.',
onSuccessInvalidate: () {
_ref.invalidate(currentMemberProvider(effectiveClubId));
},
);
}
/// Withdraw an existing pending request.
Future<bool> withdrawJoinRequest([String? targetClubId]) async {
final effectiveClubId = targetClubId ?? clubId;
return _performMutation(
clubId: effectiveClubId,
mutation: (service, uid) => service.withdrawJoinRequest(effectiveClubId, callerUid: uid),
successMessage: 'Join request withdrawn.',
onSuccessInvalidate: () {
_ref.invalidate(currentMemberProvider(effectiveClubId));
},
);
}
/// Accept an incoming leadership invitation.
Future<bool> acceptInvitation([String? targetClubId]) async {
final effectiveClubId = targetClubId ?? clubId;
return _performMutation(
clubId: effectiveClubId,
mutation: (service, uid) =>
service.respondToInvitation(effectiveClubId, accept: true, callerUid: uid),
successMessage: 'Invitation accepted! Welcome to the club.',
onSuccessInvalidate: () {
_ref.invalidate(currentMemberProvider(effectiveClubId));
_ref.invalidate(clubDetailProvider(effectiveClubId));
_ref.invalidate(myClubsProvider);
_ref.invalidate(myClubInvitesProvider);
},
);
}
/// Decline an incoming leadership invitation.
Future<bool> declineInvitation([String? targetClubId]) async {
final effectiveClubId = targetClubId ?? clubId;
return _performMutation(
clubId: effectiveClubId,
mutation: (service, uid) =>
service.respondToInvitation(effectiveClubId, accept: false, callerUid: uid),
successMessage: 'Invitation declined.',
onSuccessInvalidate: () {
_ref.invalidate(currentMemberProvider(effectiveClubId));
_ref.invalidate(myClubInvitesProvider);
},
);
}
/// Voluntarily leave an active club membership.
/// Enforces invariant: Owner cannot leave.
Future<bool> leaveClub([String? targetClubId]) async {
final effectiveClubId = targetClubId ?? clubId;
return _performMutation(
clubId: effectiveClubId,
mutation: (service, uid) => service.leaveClub(effectiveClubId, callerUid: uid),
successMessage: 'You have left the club.',
onSuccessInvalidate: () {
_ref.invalidate(currentMemberProvider(effectiveClubId));
_ref.invalidate(clubDetailProvider(effectiveClubId));
_ref.invalidate(myClubsProvider);
},
);
}
Future<bool> _performMutation({
required String clubId,
required Future<void> Function(ClubService service, String uid) mutation,
required String successMessage,
required VoidCallback onSuccessInvalidate,
}) async {
// Concurrency guard: reject duplicate or overlapping mutations in-flight
if (state.isSubmitting) {
return false;
}
final user = _ref.read(currentUserProvider);
if (user == null || user.isAnonymous) {
state = state.copyWith(
error: 'You must be signed in with a registered account to perform this action.',
clearSuccess: true,
);
return false;
}
state = ClubMembershipActionState(
isSubmitting: true,
activeClubId: clubId,
);
try {
final service = _ref.read(clubServiceProvider);
await mutation(service, user.uid);
// Perform minimal, surgical invalidations for this exact lifecycle transition
onSuccessInvalidate();
state = ClubMembershipActionState(
isSubmitting: false,
success: successMessage,
activeClubId: clubId,
);
return true;
} on ClubOperationException catch (e) {
String message;
switch (e.code) {
case 'banned':
message = 'Membership is unavailable for this club.';
break;
case 'owner-cannot-leave':
message = 'Club owner cannot leave without transferring ownership first.';
break;
case 'policy-mismatch':
message = 'This club does not allow open joining. Please submit a request.';
break;
case 'not-found':
message = 'Club or membership record was not found.';
break;
case 'unavailable':
message = 'Club services are temporarily unavailable. Please try again later.';
break;
default:
message = e.message.isNotEmpty
? e.message
: 'Unable to update club membership right now. Please try again.';
break;
}
state = ClubMembershipActionState(
isSubmitting: false,
error: message,
activeClubId: clubId,
);
return false;
} catch (e) {
state = ClubMembershipActionState(
isSubmitting: false,
error: 'Unable to update club membership right now. Please try again.',
activeClubId: clubId,
);
return false;
}
}
}
/// Family provider for ClubMembershipController scoped per club.
final clubMembershipControllerProvider = StateNotifierProvider.autoDispose
.family<ClubMembershipController, ClubMembershipActionState, String>((ref, clubId) {
return ClubMembershipController(ref, clubId);
});