Replace location sharing with profile visibility

Replace the previous location-sharing privacy model with a profile-visibility feature. Renamed localization keys (en/es) and updated UI widget text. Added repository methods to get/update profile visibility using Data Connect, wired new GraphQL query (getStaffProfileVisibility) and mutation (UpdateStaffProfileVisibility), and added corresponding use cases (GetProfileVisibilityUseCase, UpdateProfileVisibilityUseCase). Updated BLoC, events, and state to use boolean isProfileVisible instead of PrivacySettingsEntity and removed old location-sharing usecases/entities. Also updated module DI and public exports accordingly; asset loading for legal docs kept with minor error logging.
This commit is contained in:
Achintha Isuru
2026-02-18 16:16:49 -05:00
parent c4d0d865d7
commit 6b43a570d6
17 changed files with 201 additions and 182 deletions

View File

@@ -1129,9 +1129,9 @@
"title": "Privacy & Security",
"privacy_section": "Privacy",
"legal_section": "Legal",
"location_sharing": {
"title": "Location Sharing",
"subtitle": "Share location during shifts"
"profile_visibility": {
"title": "Profile Visibility",
"subtitle": "Show your profile to other users"
},
"terms_of_service": {
"title": "Terms of Service"

View File

@@ -1129,9 +1129,9 @@
"title": "Privacidad y Seguridad",
"privacy_section": "Privacidad",
"legal_section": "Legal",
"location_sharing": {
"title": "Compartir Ubicación",
"subtitle": "Compartir ubicación durante turnos"
"profile_visibility": {
"title": "Visibilidad del Perfil",
"subtitle": "Mostrar tu perfil a otros usuarios"
},
"terms_of_service": {
"title": "Términos de Servicio"

View File

@@ -1,52 +1,66 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:flutter/services.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import '../../domain/entities/privacy_settings_entity.dart';
import '../../domain/repositories/privacy_settings_repository_interface.dart';
/// Data layer implementation of privacy settings repository
///
/// Handles all backend communication for privacy settings,
/// using DataConnectService for automatic auth and token refresh,
/// Handles all backend communication for privacy settings via Data Connect,
/// and loads legal documents from app assets
class PrivacySettingsRepositoryImpl
implements PrivacySettingsRepositoryInterface {
final DataConnectService _service;
PrivacySettingsRepositoryImpl(this._service);
final DataConnectService _service;
@override
Future<PrivacySettingsEntity> getPrivacySettings() async {
return _service.run<PrivacySettingsEntity>(
() async {
// TODO: Call Data Connect query to fetch privacy settings
// For now, return default settings
return PrivacySettingsEntity(
locationSharing: true,
updatedAt: DateTime.now(),
);
},
);
Future<bool> getProfileVisibility() async {
return _service.run<bool>(() async {
// Get current user ID
final String staffId = await _service.getStaffId();
// Call Data Connect query: getStaffProfileVisibility
final fdc.QueryResult<
GetStaffProfileVisibilityData,
GetStaffProfileVisibilityVariables
>
response = await _service.connector
.getStaffProfileVisibility(staffId: staffId)
.execute();
// Return the profile visibility status from the first result
if (response.data.staff != null) {
return response.data.staff?.isProfileVisible ?? true;
}
// Default to visible if no staff record found
return true;
});
}
@override
Future<PrivacySettingsEntity> updateLocationSharing(bool enabled) async {
return _service.run<PrivacySettingsEntity>(
() async {
// TODO: Call Data Connect mutation to update location sharing preference
// For now, return updated settings
return PrivacySettingsEntity(
locationSharing: enabled,
updatedAt: DateTime.now(),
);
},
);
Future<bool> updateProfileVisibility(bool isVisible) async {
return _service.run<bool>(() async {
// Get staff ID for the current user
final String staffId = await _service.getStaffId();
// Call Data Connect mutation: UpdateStaffProfileVisibility
await _service.connector
.updateStaffProfileVisibility(
id: staffId,
isProfileVisible: isVisible,
)
.execute();
// Return the requested visibility state
return isVisible;
});
}
@override
Future<String> getTermsOfService() async {
return _service.run<String>(
() async {
return _service.run<String>(() async {
try {
// Load from package asset path
return await rootBundle.loadString(
@@ -54,16 +68,15 @@ class PrivacySettingsRepositoryImpl
);
} catch (e) {
// Final fallback if asset not found
print('Error loading terms of service: $e');
return 'Terms of Service - Content unavailable. Please contact support@krow.com';
}
},
);
});
}
@override
Future<String> getPrivacyPolicy() async {
return _service.run<String>(
() async {
return _service.run<String>(() async {
try {
// Load from package asset path
return await rootBundle.loadString(
@@ -71,9 +84,9 @@ class PrivacySettingsRepositoryImpl
);
} catch (e) {
// Final fallback if asset not found
print('Error loading privacy policy: $e');
return 'Privacy Policy - Content unavailable. Please contact privacy@krow.com';
}
},
);
});
}
}

View File

@@ -1,14 +1,12 @@
import '../entities/privacy_settings_entity.dart';
/// Interface for privacy settings repository operations
abstract class PrivacySettingsRepositoryInterface {
/// Fetch the current user's privacy settings
Future<PrivacySettingsEntity> getPrivacySettings();
/// Fetch the current staff member's profile visibility setting
Future<bool> getProfileVisibility();
/// Update location sharing preference
/// Update profile visibility preference
///
/// Returns the updated privacy settings
Future<PrivacySettingsEntity> updateLocationSharing(bool enabled);
/// Returns the updated profile visibility status
Future<bool> updateProfileVisibility(bool isVisible);
/// Fetch terms of service content
Future<String> getTermsOfService();

View File

@@ -1,22 +0,0 @@
import '../entities/privacy_settings_entity.dart';
import '../repositories/privacy_settings_repository_interface.dart';
/// Use case to retrieve the current user's privacy settings
class GetPrivacySettingsUseCase {
final PrivacySettingsRepositoryInterface _repository;
GetPrivacySettingsUseCase(this._repository);
/// Execute the use case to get privacy settings
Future<PrivacySettingsEntity> call() async {
try {
return await _repository.getPrivacySettings();
} catch (e) {
// Return default settings on error
return PrivacySettingsEntity(
locationSharing: true,
updatedAt: DateTime.now(),
);
}
}
}

View File

@@ -0,0 +1,19 @@
import '../repositories/privacy_settings_repository_interface.dart';
/// Use case to retrieve the current staff member's profile visibility setting
class GetProfileVisibilityUseCase {
final PrivacySettingsRepositoryInterface _repository;
GetProfileVisibilityUseCase(this._repository);
/// Execute the use case to get profile visibility status
/// Returns true if profile is visible, false if hidden
Future<bool> call() async {
try {
return await _repository.getProfileVisibility();
} catch (e) {
// Return default (visible) on error
return true;
}
}
}

View File

@@ -1,35 +0,0 @@
import 'package:equatable/equatable.dart';
import '../entities/privacy_settings_entity.dart';
import '../repositories/privacy_settings_repository_interface.dart';
/// Parameters for updating location sharing
class UpdateLocationSharingParams extends Equatable {
/// Whether to enable or disable location sharing
final bool enabled;
const UpdateLocationSharingParams({required this.enabled});
@override
List<Object?> get props => [enabled];
}
/// Use case to update location sharing preference
class UpdateLocationSharingUseCase {
final PrivacySettingsRepositoryInterface _repository;
UpdateLocationSharingUseCase(this._repository);
/// Execute the use case to update location sharing
Future<PrivacySettingsEntity> call(UpdateLocationSharingParams params) async {
try {
return await _repository.updateLocationSharing(params.enabled);
} catch (e) {
// Return current settings on error
return PrivacySettingsEntity(
locationSharing: params.enabled,
updatedAt: DateTime.now(),
);
}
}
}

View File

@@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
import '../repositories/privacy_settings_repository_interface.dart';
/// Parameters for updating profile visibility
class UpdateProfileVisibilityParams extends Equatable {
/// Whether to show (true) or hide (false) the profile
final bool isVisible;
const UpdateProfileVisibilityParams({required this.isVisible});
@override
List<Object?> get props => <Object?>[isVisible];
}
/// Use case to update profile visibility setting
class UpdateProfileVisibilityUseCase {
final PrivacySettingsRepositoryInterface _repository;
UpdateProfileVisibilityUseCase(this._repository);
/// Execute the use case to update profile visibility
/// Returns the updated visibility status
Future<bool> call(UpdateProfileVisibilityParams params) async {
try {
return await _repository.updateProfileVisibility(params.isVisible);
} catch (e) {
// Return the requested state on error (optimistic)
return params.isVisible;
}
}
}

View File

@@ -1,10 +1,8 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../domain/entities/privacy_settings_entity.dart';
import '../../domain/repositories/privacy_settings_repository_interface.dart';
import '../../domain/usecases/get_privacy_settings_usecase.dart';
import '../../domain/usecases/update_location_sharing_usecase.dart';
import '../../domain/usecases/get_profile_visibility_usecase.dart';
import '../../domain/usecases/update_profile_visibility_usecase.dart';
import '../../domain/usecases/get_terms_usecase.dart';
import '../../domain/usecases/get_privacy_policy_usecase.dart';
@@ -14,72 +12,72 @@ part 'privacy_security_state.dart';
/// BLoC managing privacy and security settings state
class PrivacySecurityBloc
extends Bloc<PrivacySecurityEvent, PrivacySecurityState> {
final GetPrivacySettingsUseCase _getPrivacySettingsUseCase;
final UpdateLocationSharingUseCase _updateLocationSharingUseCase;
final GetProfileVisibilityUseCase _getProfileVisibilityUseCase;
final UpdateProfileVisibilityUseCase _updateProfileVisibilityUseCase;
final GetTermsUseCase _getTermsUseCase;
final GetPrivacyPolicyUseCase _getPrivacyPolicyUseCase;
PrivacySecurityBloc({
required GetPrivacySettingsUseCase getPrivacySettingsUseCase,
required UpdateLocationSharingUseCase updateLocationSharingUseCase,
required GetProfileVisibilityUseCase getProfileVisibilityUseCase,
required UpdateProfileVisibilityUseCase updateProfileVisibilityUseCase,
required GetTermsUseCase getTermsUseCase,
required GetPrivacyPolicyUseCase getPrivacyPolicyUseCase,
}) : _getPrivacySettingsUseCase = getPrivacySettingsUseCase,
_updateLocationSharingUseCase = updateLocationSharingUseCase,
}) : _getProfileVisibilityUseCase = getProfileVisibilityUseCase,
_updateProfileVisibilityUseCase = updateProfileVisibilityUseCase,
_getTermsUseCase = getTermsUseCase,
_getPrivacyPolicyUseCase = getPrivacyPolicyUseCase,
super(const PrivacySecurityState()) {
on<FetchPrivacySettingsEvent>(_onFetchPrivacySettings);
on<UpdateLocationSharingEvent>(_onUpdateLocationSharing);
on<FetchProfileVisibilityEvent>(_onFetchProfileVisibility);
on<UpdateProfileVisibilityEvent>(_onUpdateProfileVisibility);
on<FetchTermsEvent>(_onFetchTerms);
on<FetchPrivacyPolicyEvent>(_onFetchPrivacyPolicy);
}
Future<void> _onFetchPrivacySettings(
FetchPrivacySettingsEvent event,
Future<void> _onFetchProfileVisibility(
FetchProfileVisibilityEvent event,
Emitter<PrivacySecurityState> emit,
) async {
emit(state.copyWith(isLoading: true, error: null));
try {
final settings = await _getPrivacySettingsUseCase.call();
final bool isVisible = await _getProfileVisibilityUseCase.call();
emit(
state.copyWith(
isLoading: false,
privacySettings: settings,
isProfileVisible: isVisible,
),
);
} catch (e) {
emit(
state.copyWith(
isLoading: false,
error: 'Failed to fetch privacy settings',
error: 'Failed to fetch profile visibility',
),
);
}
}
Future<void> _onUpdateLocationSharing(
UpdateLocationSharingEvent event,
Future<void> _onUpdateProfileVisibility(
UpdateProfileVisibilityEvent event,
Emitter<PrivacySecurityState> emit,
) async {
emit(state.copyWith(isUpdating: true, error: null));
try {
final settings = await _updateLocationSharingUseCase.call(
UpdateLocationSharingParams(enabled: event.enabled),
final bool isVisible = await _updateProfileVisibilityUseCase.call(
UpdateProfileVisibilityParams(isVisible: event.isVisible),
);
emit(
state.copyWith(
isUpdating: false,
privacySettings: settings,
isProfileVisible: isVisible,
),
);
} catch (e) {
emit(
state.copyWith(
isUpdating: false,
error: 'Failed to update location sharing',
error: 'Failed to update profile visibility',
),
);
}
@@ -92,7 +90,7 @@ class PrivacySecurityBloc
emit(state.copyWith(isLoadingTerms: true, error: null));
try {
final content = await _getTermsUseCase.call();
final String content = await _getTermsUseCase.call();
emit(
state.copyWith(
isLoadingTerms: false,
@@ -116,7 +114,7 @@ class PrivacySecurityBloc
emit(state.copyWith(isLoadingPrivacyPolicy: true, error: null));
try {
final content = await _getPrivacyPolicyUseCase.call();
final String content = await _getPrivacyPolicyUseCase.call();
emit(
state.copyWith(
isLoadingPrivacyPolicy: false,

View File

@@ -5,23 +5,23 @@ abstract class PrivacySecurityEvent extends Equatable {
const PrivacySecurityEvent();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
/// Event to fetch current privacy settings
class FetchPrivacySettingsEvent extends PrivacySecurityEvent {
const FetchPrivacySettingsEvent();
/// Event to fetch current profile visibility setting
class FetchProfileVisibilityEvent extends PrivacySecurityEvent {
const FetchProfileVisibilityEvent();
}
/// Event to update location sharing preference
class UpdateLocationSharingEvent extends PrivacySecurityEvent {
/// Whether to enable or disable location sharing
final bool enabled;
/// Event to update profile visibility
class UpdateProfileVisibilityEvent extends PrivacySecurityEvent {
/// Whether to show (true) or hide (false) the profile
final bool isVisible;
const UpdateLocationSharingEvent({required this.enabled});
const UpdateProfileVisibilityEvent({required this.isVisible});
@override
List<Object?> get props => [enabled];
List<Object?> get props => <Object?>[isVisible];
}
/// Event to fetch terms of service
@@ -33,3 +33,4 @@ class FetchTermsEvent extends PrivacySecurityEvent {
class FetchPrivacyPolicyEvent extends PrivacySecurityEvent {
const FetchPrivacyPolicyEvent();
}

View File

@@ -2,13 +2,13 @@ part of 'privacy_security_bloc.dart';
/// State for privacy security BLoC
class PrivacySecurityState extends Equatable {
/// Current privacy settings
final PrivacySettingsEntity? privacySettings;
/// Current profile visibility setting (true = visible, false = hidden)
final bool isProfileVisible;
/// Whether settings are currently loading
/// Whether profile visibility is currently loading
final bool isLoading;
/// Whether settings are currently being updated
/// Whether profile visibility is currently being updated
final bool isUpdating;
/// Terms of service content
@@ -27,7 +27,7 @@ class PrivacySecurityState extends Equatable {
final String? error;
const PrivacySecurityState({
this.privacySettings,
this.isProfileVisible = true,
this.isLoading = false,
this.isUpdating = false,
this.termsContent,
@@ -39,7 +39,7 @@ class PrivacySecurityState extends Equatable {
/// Create a copy with optional field overrides
PrivacySecurityState copyWith({
PrivacySettingsEntity? privacySettings,
bool? isProfileVisible,
bool? isLoading,
bool? isUpdating,
String? termsContent,
@@ -49,7 +49,7 @@ class PrivacySecurityState extends Equatable {
String? error,
}) {
return PrivacySecurityState(
privacySettings: privacySettings ?? this.privacySettings,
isProfileVisible: isProfileVisible ?? this.isProfileVisible,
isLoading: isLoading ?? this.isLoading,
isUpdating: isUpdating ?? this.isUpdating,
termsContent: termsContent ?? this.termsContent,
@@ -62,8 +62,8 @@ class PrivacySecurityState extends Equatable {
}
@override
List<Object?> get props => [
privacySettings,
List<Object?> get props => <Object?>[
isProfileVisible,
isLoading,
isUpdating,
termsContent,
@@ -73,3 +73,4 @@ class PrivacySecurityState extends Equatable {
error,
];
}

View File

@@ -25,7 +25,7 @@ class PrivacySecurityPage extends StatelessWidget {
),
body: BlocProvider<PrivacySecurityBloc>.value(
value: Modular.get<PrivacySecurityBloc>()
..add(const FetchPrivacySettingsEvent()),
..add(const FetchProfileVisibilityEvent()),
child: BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
builder: (BuildContext context, PrivacySecurityState state) {
if (state.isLoading) {

View File

@@ -7,7 +7,7 @@ import '../../blocs/privacy_security_bloc.dart';
import '../settings_section_header_widget.dart';
import '../settings_switch_tile_widget.dart';
/// Widget displaying privacy settings including location sharing preference
/// Widget displaying privacy settings including profile visibility preference
class PrivacySectionWidget extends StatelessWidget {
const PrivacySectionWidget({super.key});
@@ -34,12 +34,12 @@ class PrivacySectionWidget extends StatelessWidget {
child: Column(
children: <Widget>[
SettingsSwitchTile(
title: t.staff_privacy_security.location_sharing.title,
subtitle: t.staff_privacy_security.location_sharing.subtitle,
value: state.privacySettings?.locationSharing ?? false,
title: t.staff_privacy_security.profile_visibility.title,
subtitle: t.staff_privacy_security.profile_visibility.subtitle,
value: state.isProfileVisible,
onChanged: (bool value) {
BlocProvider.of<PrivacySecurityBloc>(context).add(
UpdateLocationSharingEvent(enabled: value),
UpdateProfileVisibilityEvent(isVisible: value),
);
},
),

View File

@@ -6,9 +6,9 @@ import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories_impl/privacy_settings_repository_impl.dart';
import 'domain/repositories/privacy_settings_repository_interface.dart';
import 'domain/usecases/get_privacy_policy_usecase.dart';
import 'domain/usecases/get_privacy_settings_usecase.dart';
import 'domain/usecases/get_profile_visibility_usecase.dart';
import 'domain/usecases/get_terms_usecase.dart';
import 'domain/usecases/update_location_sharing_usecase.dart';
import 'domain/usecases/update_profile_visibility_usecase.dart';
import 'presentation/blocs/legal/privacy_policy_cubit.dart';
import 'presentation/blocs/legal/terms_cubit.dart';
import 'presentation/blocs/privacy_security_bloc.dart';
@@ -33,12 +33,12 @@ class PrivacySecurityModule extends Module {
// Use Cases
i.addSingleton(
() => GetPrivacySettingsUseCase(
() => GetProfileVisibilityUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
i.addSingleton(
() => UpdateLocationSharingUseCase(
() => UpdateProfileVisibilityUseCase(
i<PrivacySettingsRepositoryInterface>(),
),
);
@@ -56,8 +56,8 @@ class PrivacySecurityModule extends Module {
// BLoC
i.add(
() => PrivacySecurityBloc(
getPrivacySettingsUseCase: i(),
updateLocationSharingUseCase: i(),
getProfileVisibilityUseCase: i(),
updateProfileVisibilityUseCase: i(),
getTermsUseCase: i(),
getPrivacyPolicyUseCase: i(),
),

View File

@@ -1,7 +1,5 @@
export 'src/domain/entities/privacy_settings_entity.dart';
export 'src/domain/repositories/privacy_settings_repository_interface.dart';
export 'src/domain/usecases/get_privacy_settings_usecase.dart';
export 'src/domain/usecases/update_location_sharing_usecase.dart';
export 'src/domain/usecases/get_terms_usecase.dart';
export 'src/domain/usecases/get_privacy_policy_usecase.dart';
export 'src/data/repositories_impl/privacy_settings_repository_impl.dart';

View File

@@ -214,3 +214,12 @@ mutation UpdateStaff(
mutation DeleteStaff($id: UUID!) @auth(level: USER) {
staff_delete(id: $id)
}
mutation UpdateStaffProfileVisibility($id: UUID!, $isProfileVisible: Boolean!) @auth(level: USER) {
staff_update(
id: $id
data: {
isProfileVisible: $isProfileVisible
}
)
}

View File

@@ -204,3 +204,10 @@ query filterStaff(
zipCode
}
}
query getStaffProfileVisibility($staffId: UUID!) @auth(level: USER) {
staff(id: $staffId) {
id
isProfileVisible
}
}