152 lines
5.4 KiB
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,
|
|
);
|