user_onboarding_controller.dart 9 KB
import 'dart:io';

import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';

import '../../../routes/app_pages.dart';

class UserOnboardingController extends GetxController {
  int totalPages = 9;

  final currentPageIndex = 0.obs;
  final selections = <int, Set<int>>{}.obs;
  final selectionVersion = 0.obs;

  UserOnboardingController(this._userApi);
  final UserApi _userApi;
  final AppHealthKitHostApi _hostApi = AppHealthKitHostApi();
  final AppPlatformHostApi _platformHostApi = AppPlatformHostApi();

  bool hasSelection(int pageIndex) =>
      selections[pageIndex]?.isNotEmpty ?? false;

  bool isSelected(int pageIndex, int optionIndex) =>
      selections[pageIndex]?.contains(optionIndex) ?? false;

  bool canContinue(int pageIndex) {
    return switch (pageIndex) {
      1 || 2 || 3 => hasSelection(pageIndex),
      _ => true,
    };
  }

  @override
  void onInit() {
    final argument = Get.arguments?['resumeStage'];
    if (argument != null && argument is int) {
      currentPageIndex.value = argument.clamp(0, totalPages - 1);
    }
    totalPages = 9;
    super.onInit();
  }

  void toggleOption({
    required int pageIndex,
    required int optionIndex,
    required bool multiple,
    bool exclusive = false,
    Set<int> exclusiveOptionIndexes = const {},
  }) {
    final next = Set<int>.of(selections[pageIndex] ?? const <int>{});

    if (multiple) {
      if (next.contains(optionIndex)) {
        next.remove(optionIndex);
      } else if (exclusive) {
        next
          ..clear()
          ..add(optionIndex);
      } else {
        next.removeAll(exclusiveOptionIndexes);
        next.add(optionIndex);
      }
    } else {
      next
        ..clear()
        ..add(optionIndex);
    }

    if (next.isEmpty) {
      selections.remove(pageIndex);
    } else {
      selections[pageIndex] = next;
    }
    selectionVersion.value++;
    selections.refresh();
  }

  Future<void> goBack() async {
    if (currentPageIndex.value > 0) {
      currentPageIndex.value--;
      return;
    }
    if (Get.key.currentState?.canPop() ?? false) {
      Get.back();
    } else {
      // SystemNavigator.pop();

      await _userApi.updateOwnerUserInfo(isPassNoviceGuide: 1);
      final userId =
          Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
              0;
      if (userId > 0) {
        Get.find<UserAccountStorage>().markOnboardingCompleted(userId);
      }

      Get.offAllNamed(AppRoutes.home);
    }
  }

  Future<void> goNext() async {
    final pageIndex = currentPageIndex.value;
    if (!await prepareContinue(pageIndex)) {
      return;
    }

    // 保存当前页进度(中途退出后可断点续做)
    final userId =
        Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
            0;
    if (userId > 0) {
      await Get.find<UserAccountStorage>()
          .saveOnboardingStage(userId, pageIndex);
    }

    if (pageIndex >= totalPages - 1) {
      finishOnboarding();
      return;
    }
    try {
      report();
    } on Exception catch (error, stackTrace) {
      AppLogger.e('Onboarding report failed', error, stackTrace);
    }
    currentPageIndex.value++;
    if (currentPageIndex.value == 7) {
      ta.track('enter_doublefeel_health_grant_page');
    } else if (currentPageIndex.value == 8) {
      ta.track('enter_doublefeel_notification_grant_page');
    }
  }

  void report() {
    const page1Labels = [
      'stressAnxiety', // index 0
      'tired', // index 1
      'poorRest', // index 2
      'needStimulants', // index 3
      'none', // index 4
    ];
    // page 2: 压力目标(单选)
    const page2Labels = [
      'findSource', // index 0
      'reminder', // index 1
      'lovedOnes', // index 2
      'relax', // index 3
      'bodyTalk', // index 4
    ];
    // page 3: 缓解方式(多选)
    const page3Labels = [
      'sleep', // index 0
      'care', // index 1
      'exercise', // index 2
      'sun', // index 3
      'water', // index 4
      'meditation', // index 5
    ];

    late List<String> labels;
    switch (currentPageIndex.value) {
      case 1:
        labels = page1Labels;
        break;
      case 2:
        labels = page2Labels;
        break;
      case 3:
        labels = page3Labels;
        break;
    }

    final selected = (selections[currentPageIndex.value] ?? {})
        .map((i) => labels[i])
        .toList();
    var key = '';
    switch (currentPageIndex.value) {
      case 1:
        key = 'what_happened';
        break;
      case 2:
        key = 'want_messages';
        break;
      case 3:
        key = 'vent_ways';
        break;
    }

    if (selected.isNotEmpty && key.isNotEmpty) {
      _userApi.updateUserOnboarding({key: selected});
    }
  }

  Future<bool> prepareContinue(int pageIndex) async {
    if (!canContinue(pageIndex)) {
      return false;
    }

    if (pageIndex == 7) {
      return await requestHealthAuthorization();
    } else if (pageIndex == 8) {
      await requestNotificationAuthorization();
    }

    return true;
  }

  void finishOnboarding() {
    // 标记当前账号引导已全部完成
    _userApi.updateOwnerUserInfo(isPassNoviceGuide: 1);
    final userId =
        Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
            0;
    if (userId > 0) {
      Get.find<UserAccountStorage>().markOnboardingCompleted(userId);
    }

    final userStateService = Get.find<UserStateService>();
    if (userStateService.isBound) {
      Get.offAllNamed(AppRoutes.home);
    } else {
      Get.offAllNamed(AppRoutes.bindPartner, arguments: {'from': 'onboarding'});
    }
  }

  /// 计算 Apple Health 新数据。
  Future<void> _performDataUpload() async {
    try {
      final service = Get.find<HealthRawDataCoreService>();
      await service.performHealthDataUpload();
      await service.startCoreCaculate();
    } catch (error, stackTrace) {
      AppLogger.e('Apple Health calculate failed', error, stackTrace);
    }
  }

  Future<bool> requestHealthAuthorization() async {
    if (Platform.isAndroid) {
      return true;
    } else if (Platform.isIOS) {
      try {
        final result = await _hostApi.checkHealthAppAuthorization();
        AppLogger.d("checkHealthAppAuthorization : $result");
        if (result.status == HealthAuthorizationStatus.notDetermined) {
          final authorization =
              await _hostApi.requestHealthClientAuthorization();
          AppLogger.d("requestHealthClientAuthorization : $authorization");
          if (authorization == HealthAuthorizationStatus.authorized) {
            _performDataUpload();
            return true;
          } else {
            // Get.to(NoHealthDataPage(
            //   onRefresh: _performDataUpload,
            // ));
            return false;
          }
        } else {
          // status == 1: 已授权;status == 2: 已拒绝
          // 两种情况都放行,已授权时额外上传数据
          if (result.status == HealthAuthorizationStatus.authorized) {
            _performDataUpload();
          }
          return true;
        }
      } catch (error, stackTrace) {
        AppLogger.e('Health authorization failed', error, stackTrace);
        return true;
      }
    } else if (isOhos()) {
      try {
        final result = await _hostApi.checkHealthAppAuthorization();
        AppLogger.d("checkHealthAppAuthorization : $result");
        if (result.status != HealthAuthorizationStatus.authorized) {
          final authorization =
              await _hostApi.requestHealthClientAuthorization();
          AppLogger.d("requestHealthClientAuthorization : $authorization");
          return true;
        } else {
          return true;
        }
      } catch (error, stackTrace) {
        AppLogger.e('Health authorization failed', error, stackTrace);
        return true;
      }
    } else {
      return true;
    }
  }

  Future<void> requestNotificationAuthorization() async {
    try {
      final status = await _platformHostApi.getApnsAuthStatus();

      if (status == 1) {
        // 已授权,无需再请求
        return;
      }

      if (status == -1) {
        // 用户永久拒绝(Android),引导去设置
        await openAppSettings();
        return;
      }

      // 未决定 → 弹出系统授权弹窗
      await _platformHostApi.requestNotificationAuth();
    } catch (error) {
      AppLogger.e('Notification authorization failed: $error');
    }
  }
}