login_controller.dart 10.7 KB
import 'dart:async';
import 'dart:convert';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/constants/app_const.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/local_storage.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/data/models/user/user_models.dart';
import 'package:doublefeel_flutter/data/models/vip/vip_info.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/services/loading_service.dart';

class LoginController extends GetxController {
  final UserApi _userApi = Get.find<UserApi>();
  final VipApi _vipApi = Get.find<VipApi>();
  final UserPreferencesStorage _userPrefs = Get.find<UserPreferencesStorage>();
  final UserAccountStorage _userAccount = Get.find<UserAccountStorage>();
  final UserStateService _userStateService = Get.find<UserStateService>();
  final environmentConfig = Get.find<AppEnvironmentConfig>();

  final phoneController = TextEditingController();
  final codeController = TextEditingController();
  final codeFocusNode = FocusNode();

  final phoneInput = ''.obs;
  final codeInput = ''.obs;

  final isSendingCode = false.obs;
  final countdownSeconds = 0.obs;
  Timer? _countdownTimer;

  final isLoggingIn = false.obs;
  final hasSentCode = false.obs;

  final termsChecked = false.obs;

  AppleSignInModel? appleSignInModel;

  Future<void> toggleTermsChecked() async {
    final newValue = !termsChecked.value;
    termsChecked.value = newValue;
    try {
      final local = Get.find<LocalStorage>();
      await local.setTermsAgreed(newValue);
      if (newValue && !_userStateService.sdksInitialized) {
        await _userStateService.initializeSdks();
      }
      if (needTrackWelcomepage) {
        ta.track('enter_doublefeel_welcome_page');
        needTrackWelcomepage = false;
      }
    } catch (e) {
      AppLogger.e(e.toString());
    }
  }

  void onPhoneLoginPressed() {
    if (!termsChecked.value) {
      AppToast.show(AppLocalizations.of(Get.context!)!.loginAgreeToTermsToast);
      return;
    }
    ta.track('enter_doublefeel_telephone_login_page');
    Get.toNamed(AppRoutes.phoneLogin);
  }

  void onAppleLoginPressed() async {
    if (!termsChecked.value) {
      AppToast.show(AppLocalizations.of(Get.context!)!.loginAgreeToTermsToast);
      return;
    }
    try {
      final result = await PlatformHostApi().requestAppleSignIn();
      // result 为 null 或 identityToken 为空,说明用户取消或苹果授权失败
      if (result == null ||
          result.identityToken?.isNotEmpty != true ||
          result.userId.isNotEmpty != true) {
        appleSignInModel = null;
        return;
      }
      appleSignInModel = result;
      await _loginWithApple(result);
    } catch (e) {
      appleSignInModel = null;
      AppToast.show(e.toString());
    }
  }

  void onDebugPressed() {
    Get.toNamed(AppRoutes.debugEnvironment);
  }

  void openUserTerms() {
    Get.toNamed(
      AppRoutes.webview,
      parameters: {'url': AppConst.userTerms},
    );
  }

  void openPrivacyPolicy() {
    Get.toNamed(
      AppRoutes.webview,
      parameters: {'url': AppConst.privacyPolicy},
    );
  }

  @override
  void onInit() {
    super.onInit();
    LocalStorage.open().then((local) {
      termsChecked.value = local.termsAgreed;
    });

    phoneController.addListener(() {
      phoneInput.value = phoneController.text;
    });
    codeController.addListener(() {
      codeInput.value = codeController.text;
    });
  }

  @override
  void onClose() {
    _countdownTimer?.cancel();
    phoneController.dispose();
    codeController.dispose();
    codeFocusNode.dispose();
    super.onClose();
  }

  @override
  void onReady() {
    super.onReady();
    if (_userStateService.sdksInitialized) {
      ta.track('enter_doublefeel_welcome_page');
    } else {
      needTrackWelcomepage = true;
    }
  }

  bool needTrackWelcomepage = false;

  String get cleanPhone => phoneInput.value.replaceAll(' ', '');

  bool get canRequestCode =>
      cleanPhone.length == 11 &&
      !isSendingCode.value &&
      countdownSeconds.value == 0;

  bool get canLogin =>
      cleanPhone.length == 11 &&
      codeInput.value.isNotEmpty &&
      !isLoggingIn.value;

  Future<void> requestVerifyCode() async {
    if (!canRequestCode) return;

    if (cleanPhone.length < 11) {
      AppToast.show(AppLocalizations.of(Get.context!)!.phoneLoginInvalidPhone);
      return;
    }

    isSendingCode.value = true;
    final request = VerifyCodeRequest.login(cleanPhone);
    final result = await _userApi.requestVerifyCode(request);

    isSendingCode.value = false;
    if (result is AppSuccess<void>) {
      AppToast.show(
          AppLocalizations.of(Get.context!)!.phoneLoginCodeSentSuccess);
      _startCountdown();
      codeFocusNode.requestFocus();
    }
  }

  void _startCountdown() {
    hasSentCode.value = true;
    _countdownTimer?.cancel();
    countdownSeconds.value = 60;
    _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (countdownSeconds.value > 1) {
        countdownSeconds.value--;
      } else {
        countdownSeconds.value = 0;
        timer.cancel();
      }
    });
  }

  Future<void> login() async {
    if (!canLogin) return;

    if (cleanPhone.length < 11) {
      AppToast.show(AppLocalizations.of(Get.context!)!.phoneLoginInvalidPhone);
      return;
    }
    if (codeInput.value.isEmpty) {
      AppToast.show(AppLocalizations.of(Get.context!)!.phoneLoginInvalidCode);
      return;
    }

    isLoggingIn.value = true;
    try {
      await LoadingService.instance.run(() async {
        final loginRes = await _userApi.login(
          telephone: cleanPhone,
          verifyCode: codeInput.value,
        );

        if (loginRes is AppSuccess<LoginResponse>) {
          await _handleLoginSuccess(loginRes.data);
          await Get.find<LocalStorage>().setLastLoginMethod('phone');
        }
      });
    } finally {
      isLoggingIn.value = false;
    }
  }

  Future<void> _loginWithApple(AppleSignInModel appleModel) async {
    isLoggingIn.value = true;
    try {
      await LoadingService.instance.run(() async {
        final loginRes = await _userApi.loginWithApple(appleModel);

        if (loginRes is AppSuccess<LoginResponse>) {
          await _handleLoginSuccess(loginRes.data, appleModel: appleModel);
          await Get.find<LocalStorage>().setLastLoginMethod('apple');
        }
      });
    } finally {
      isLoggingIn.value = false;
    }
  }

  /// 登录/注册成功后的公共处理逻辑(手机号登录与苹果登录共用)
  Future<void> _handleLoginSuccess(LoginResponse loginData,
      {AppleSignInModel? appleModel}) async {
    bool isApple = appleModel != null;
    final isRegister = loginData.isNewUser == true;
    String accessToken = loginData.accessToken;

    if (isRegister) {
      final regRes = await _userApi.register(isApple
          ? {
              'login_type': 'apple',
              'apple_user_id': appleModel.userId,
              'identity_token': appleModel.identityToken,
            }
          : {
              'login_type': 'verification_code',
              'telephone': cleanPhone,
              'verification_code': codeInput.value,
            });
      if (regRes is AppSuccess<RegisterResponse>) {
        accessToken = regRes.data.accessToken;
      } else {
        isLoggingIn.value = false;
        return;
      }
    }

    // Fetch user info and VIP info
    final userInfoResFuture = _userApi.getUserInfo(accessToken: accessToken);
    final vipInfoResFuture = isRegister
        ? Future.value(null)
        : _vipApi.getVipInfo(accessToken: accessToken);

    final results = await Future.wait([userInfoResFuture, vipInfoResFuture]);
    final userInfoRes = results[0];
    final vipInfoRes = results[1];

    if (userInfoRes is AppSuccess<UserInfoResponse>) {
      final me = userInfoRes.data;
      UserInfoResponse? partner;

      if ((me.pairId ?? 0) > 0) {
        final partnerRes =
            await _userApi.getPartnerUserInfo(accessToken: accessToken);
        if (partnerRes is AppSuccess<BoundUserInfoResponse>) {
          partner = partnerRes.data.partnerUserInfo;
        }
      }

      VipInfo? vip;
      if (vipInfoRes is AppSuccess<VipInfo>) {
        vip = vipInfoRes.data;
      }

      await _userPrefs.updateFromLogin(
        accessToken: accessToken,
        me: me,
        partner: partner,
        vip: vip,
      );

      try {
        if (Get.isRegistered<AppEnvironmentConfig>()) {
          var env = Get.find<AppEnvironmentConfig>();
          await PlatformHostApi().updateLoginInfo(
              jsonEncode(UserPreferences(
                accessToken: accessToken,
                meUserInfo: me,
                partnerUserInfo: partner,
                vipInfo: vip != null
                    ? UserPreferencesVipInfo.fromVipInfo(vip)
                    : null,
              )),
              env.serverBaseUrl);
        }
      } on Exception catch (e) {
        AppLogger.e(e);
      }

      await _userStateService.onLogin();
      if (isRegister) {
        ta.track('success_regist');
      }

      // 用户登录成功即代表同意了协议,持久化到本地供冷启动时 SDK 初始化判断使用
      await Get.find<LocalStorage>().setTermsAgreed(true);

      // 根据引导完成状态决定跳转目标
      final userId = me.id ?? 0;
      if (_userAccount.hasCompletedOnboarding(userId) ||
          me.isPassNoviceGuide == 1) {
        // 该账号已完成引导,直接进主页
        Get.offAllNamed(AppRoutes.home);
      } else {
        // 新用户或未完成引导,进入引导页(支持断点续做)
        final resumeStage = _userAccount.onboardingResumeStage(userId);
        Get.offAllNamed(
          AppRoutes.userOnboarding,
          arguments: resumeStage != null ? {'resumeStage': resumeStage} : null,
        );
      }
    } else {
      // userInfoRes 失败,外层 finally 会重置 isLoggingIn
    }
  }
}