user_preferences_storage.dart 5.13 KB
import 'dart:async';
import 'dart:convert';

import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../models/local/user_preferences.dart';
import '../models/user/user_models.dart';
import '../models/vip/vip_info.dart';

/// Persists [UserPreferences] as JSON.
class UserPreferencesStorage {
  UserPreferencesStorage(this._prefs);

  final SharedPreferences _prefs;

  static const _storageKey = 'double_feel_user_preferences_json';

  final Rx<UserPreferences> preferences = UserPreferences.empty.obs;

  Future<UserPreferencesStorage> init() async {
    await _load();
    return this;
  }

  // bool get isBound => (preferences.value.partnerUserInfo?.id ?? 0) > 0;

  String get accessToken => preferences.value.accessToken;

  bool get isLoggedIn => accessToken.isNotEmpty;

  String get rongcloudToken => preferences.value.rongcloudToken;

  Future<void> _load() async {
    final raw = _prefs.getString(_storageKey);
    if (raw == null || raw.isEmpty) {
      preferences.value = UserPreferences.empty;
      return;
    }
    try {
      final map = jsonDecode(raw) as Map<String, dynamic>;
      preferences.value = UserPreferences.fromJson(map);
    } catch (error, stackTrace) {
      AppLogger.e('UserPreferences JSON parse failed', error, stackTrace);
      AppDiagnosticLogStore.instance.log(
        category: 'UserPreferences',
        action: 'load_deserialization_failed',
        details: {
          'error': error.toString(),
          'stackTrace': stackTrace.toString(),
          'rawPreview': raw.length > 200 ? '${raw.substring(0, 200)}...' : raw,
        },
      );
      preferences.value = UserPreferences.empty;
    }
  }

  Future<void> syncLoginInfoToNative() =>
      _syncLoginInfoToNative(userInfo: preferences.value);

  Future<void> _syncLoginInfoToNative(
      {required UserPreferences userInfo}) async {
    if (userInfo.accessToken.isEmpty) {
      try {
        if (Get.isRegistered<AppEnvironmentConfig>()) {
          await AppPlatformHostApi().logout();
        }
      } on Exception {
        return;
      }
      return;
    }
    try {
      if (Get.isRegistered<AppEnvironmentConfig>()) {
        var env = Get.find<AppEnvironmentConfig>();
        await AppPlatformHostApi()
            .updateLoginInfo(jsonEncode(userInfo), env.serverBaseUrl);
        await _syncHealthRawDatabaseToNative();
      }
      AppLogger.d('Native login info synced for cold start');
    } catch (error, stackTrace) {
      AppLogger.e('Native login info sync failed', error, stackTrace);
    }
  }

  Future<void> _syncHealthRawDatabaseToNative() async {
    if (!Get.isRegistered<HealthRawDataCoreService>()) return;
    await Get.find<HealthRawDataCoreService>().syncDatabaseToNativeIfNeeded();
  }

  Future<void> _persist(UserPreferences value) async {
    preferences.value = value;
    await _prefs.setString(_storageKey, jsonEncode(value.toJson()));
  }

  Future<void> update(UserPreferences value) => _persist(value);

  Future<void> updateAccessToken(String token) async {
    await _persist(preferences.value.copyWith(accessToken: token));
  }

  Future<void> updateRongcloudToken(String token) async {
    await _persist(preferences.value.copyWith(rongcloudToken: token));
  }

  Future<void> updateMeUserInfo(UserInfoResponse info) async {
    await _persist(preferences.value.copyWith(meUserInfo: info));
  }

  Future<void> updatePartnerUserInfo(UserInfoResponse info) async {
    await _persist(preferences.value.copyWith(partnerUserInfo: info));
  }

  Future<void> updateBothUserInfo({
    UserInfoResponse? me,
    UserInfoResponse? partner,
  }) async {
    await _persist(
      preferences.value.copyWith(
        meUserInfo: me ?? preferences.value.meUserInfo,
        partnerUserInfo: partner ?? preferences.value.partnerUserInfo,
      ),
    );
  }

  Future<void> updateVipInfo(UserPreferencesVipInfo vipInfo) async {
    await _persist(preferences.value.copyWith(vipInfo: vipInfo));
  }

  Future<void> updateFromLogin({
    required String accessToken,
    UserInfoResponse? me,
    UserInfoResponse? partner,
    VipInfo? vip,
  }) async {
    final userInfo = UserPreferences(
      accessToken: accessToken,
      meUserInfo: me,
      partnerUserInfo: partner,
      vipInfo: vip != null ? UserPreferencesVipInfo.fromVipInfo(vip) : null,
    );
    await _persist(userInfo);
    // Native login/health database synchronization is best effort and can
    // trigger expensive platform work. Do not hold the login transition for it.
    unawaited(_syncLoginInfoToNative(userInfo: userInfo));
  }

  Future<void> clearPartnerUserInfo() async {
    await _persist(preferences.value.copyWith(clearPartner: true));
  }

  Future<void> clear() async {
    await _prefs.remove(_storageKey);
    preferences.value = UserPreferences.empty;
  }
}