glp-companion/lib/features/settings/presentation/controllers/integrations_controller.dart

152 lines
5.4 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/platform/health_gateway.dart';
import '../../../health_sync/presentation/controllers/biometric_controller.dart';
/// Immutable snapshot of the integrations toggles shown on the Settings
/// screen.
///
/// Native health toggles are gated by the platform actually backing the
/// gateway (HealthKit on iOS, Health Connect on Android); the HealthGateway
/// connector (cloud partner REST sync) is available everywhere and reports
/// manual-entry fallback on Web/Windows/macOS.
class IntegrationsState {
const IntegrationsState({
this.appleHealthEnabled = false,
this.healthConnectEnabled = false,
this.healthGatewayEnabled = false,
this.isRequestingPermissions = false,
this.errorMessage,
});
/// Sentinel allowing `copyWith` to explicitly reset a field to `null`.
static const Object _keep = Object();
/// Apple HealthKit sync armed (iOS only).
final bool appleHealthEnabled;
/// Google Health Connect sync armed (Android only).
final bool healthConnectEnabled;
/// HealthGateway (cloud partner / manual-entry fallback) sync armed.
final bool healthGatewayEnabled;
/// `true` while a native permission round-trip is in flight.
final bool isRequestingPermissions;
/// Last permission/persistence failure, surfaced inline.
final String? errorMessage;
IntegrationsState copyWith({
bool? appleHealthEnabled,
bool? healthConnectEnabled,
bool? healthGatewayEnabled,
bool? isRequestingPermissions,
Object? errorMessage = _keep,
}) =>
IntegrationsState(
appleHealthEnabled: appleHealthEnabled ?? this.appleHealthEnabled,
healthConnectEnabled: healthConnectEnabled ?? this.healthConnectEnabled,
healthGatewayEnabled: healthGatewayEnabled ?? this.healthGatewayEnabled,
isRequestingPermissions:
isRequestingPermissions ?? this.isRequestingPermissions,
errorMessage: identical(errorMessage, _keep)
? this.errorMessage
: errorMessage as String?,
);
}
/// Orchestrates the integrations toggles: requests native health
/// permissions through the cross-platform [IHealthGateway] before arming a
/// toggle, and degrades gracefully on platforms without native support.
class IntegrationsController extends Notifier<IntegrationsState> {
@override
IntegrationsState build() {
final gateway = ref.read(healthGatewayProvider);
return IntegrationsState(
// Toggles start disarmed; enabling requests fresh permissions.
appleHealthEnabled: false,
healthConnectEnabled: false,
healthGatewayEnabled: false,
errorMessage:
gateway.supportsNativeHealth ? null : _fallbackNotice(gateway),
);
}
/// Human-readable capability notice for platforms without native health
/// SDKs (Web / Windows / macOS fallback driver).
static String? _fallbackNotice(IHealthGateway gateway) {
if (gateway.supportsNativeHealth) return null;
return 'Native health SDKs are unavailable on this platform — '
'HealthGateway runs in manual-entry mode.';
}
/// Arms/disarms the Apple Health toggle. Enabling requests native
/// permission; denial reverts the toggle and records a message.
Future<void> setAppleHealthEnabled(bool enabled) =>
_setNative(enabled: enabled, isApple: true);
/// Arms/disarms the Health Connect toggle (same permission contract as
/// Apple Health; the gateway routes to the right SDK per platform).
Future<void> setHealthConnectEnabled(bool enabled) =>
_setNative(enabled: enabled, isApple: false);
/// Arms/disarms the HealthGateway connector (no native permission needed).
void setHealthGatewayEnabled(bool enabled) {
state = state.copyWith(healthGatewayEnabled: enabled, errorMessage: null);
}
/// Clears the transient error banner.
void clearError() {
if (state.errorMessage != null) {
state = state.copyWith(errorMessage: null);
}
}
Future<void> _setNative({required bool enabled, required bool isApple}) async {
if (!enabled) {
state = state.copyWith(
appleHealthEnabled: isApple ? false : state.appleHealthEnabled,
healthConnectEnabled: isApple ? state.healthConnectEnabled : false,
errorMessage: null,
);
return;
}
state = state.copyWith(isRequestingPermissions: true, errorMessage: null);
try {
final granted = await ref.read(healthGatewayProvider).requestPermissions();
if (granted) {
state = state.copyWith(
appleHealthEnabled: isApple ? true : state.appleHealthEnabled,
healthConnectEnabled: isApple ? state.healthConnectEnabled : true,
isRequestingPermissions: false,
);
} else {
state = state.copyWith(
isRequestingPermissions: false,
errorMessage:
'Health permissions were denied. Enable them in your device '
'settings to sync biometrics.',
);
}
} catch (error, stackTrace) {
debugPrintStack(
stackTrace: stackTrace,
label: 'health permission request failed',
);
state = state.copyWith(
isRequestingPermissions: false,
errorMessage: 'Could not request health permissions: $error',
);
}
}
}
/// Exposes the [IntegrationsController] to the widget tree.
final integrationsControllerProvider =
NotifierProvider<IntegrationsController, IntegrationsState>(
IntegrationsController.new,
);