login_main_cubit.dart 14.7 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
import 'dart:async';
import 'dart:io';

import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/data/repositories/user_auth_repository.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:fluwx/fluwx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

class LoginMainState extends Equatable {
  final bool agreed;
  final bool showAgreed;
  final bool showNeedWechatForApple;
  final int loginType; // 1=Wechat,2=Apple
  final String appleUserIdentifier;
  final bool loading;
  final bool wechatInstalled;
  final bool showPrivacyFirstTime;

  const LoginMainState({
    this.agreed = false,
    this.showAgreed = false,
    this.showNeedWechatForApple = false,
    this.loginType = 0,
    this.appleUserIdentifier = '',
    this.loading = false,
    this.wechatInstalled = false,
    this.showPrivacyFirstTime = false,
  });

  LoginMainState copyWith({
    bool? agreed,
    bool? showAgreed,
    bool? showNeedWechatForApple,
    int? loginType,
    String? appleUserIdentifier,
    bool? loading,
    bool? wechatInstalled,
    bool? showPrivacyFirstTime,
  }) {
    return LoginMainState(
      agreed: agreed ?? this.agreed,
      showAgreed: showAgreed ?? this.showAgreed,
      showNeedWechatForApple: showNeedWechatForApple ?? this.showNeedWechatForApple,
      loginType: loginType ?? this.loginType,
      appleUserIdentifier: appleUserIdentifier ?? this.appleUserIdentifier,
      loading: loading ?? this.loading,
      wechatInstalled: wechatInstalled ?? this.wechatInstalled,
      showPrivacyFirstTime: showPrivacyFirstTime ?? this.showPrivacyFirstTime,
    );
  }

  @override
  List<Object?> get props => [
        agreed,
        showAgreed,
        showNeedWechatForApple,
        loginType,
        appleUserIdentifier,
        loading,
        wechatInstalled,
        showPrivacyFirstTime,
      ];
}

class LoginMainCubit extends Cubit<LoginMainState> with WidgetsBindingObserver {
  late final Fluwx _fluwx;
  late final FluwxCancelable _fluwxCancelable;
  late final WechatAuthRepository _wechatAuthRepository;
  late final UserAuthRepository _userAuthRepository;

  // 是否正在等待微信授权回调;用于多微信选择器取消、用户中途返回等异常场景的兜底
  bool _waitingWechatAuth = false;
  // 总超时(请求已投递但长时间无任何回调时强制复位)
  Timer? _wechatAuthTimeoutTimer;
  // App 回到前台后的短延时(用于判定系统层取消)
  Timer? _wechatAuthResumeTimer;

  LoginMainCubit(super.initialState) {
    WidgetsBinding.instance.addObserver(this);
    _fluwx = getIt.get<Fluwx>();

    // 处理微信安装检测
    _fluwx.isWeChatInstalled.then((value) {
      emit(state.copyWith(wechatInstalled: value));
    });

    _fluwxCancelable = _fluwx.addSubscriber(_responseListener);
    _wechatAuthRepository = getIt.get<WechatAuthRepository>();
    _userAuthRepository = getIt.get<UserAuthRepository>();

    // 检查是否首次打开登录页面,显示个人信息收集提示
    if(Platform.isIOS) {
      _checkFirstTimePrivacy();
    }
  }

  // 检查是否首次打开,显示个人信息收集提示
  void _checkFirstTimePrivacy() async {
    var sharedPreferences = getIt.get<SharedPreferences>();
    var hasShownPrivacy = sharedPreferences.getBool(Constant.hasShownPrivacyFirstTimeKey) ?? false;
    if (!hasShownPrivacy) {
      emit(state.copyWith(showPrivacyFirstTime: true));
    }
  }

  // 确认已显示个人信息收集提示
  void confirmPrivacyFirstTime() async {
    var sharedPreferences = getIt.get<SharedPreferences>();
    await sharedPreferences.setBool(Constant.hasShownPrivacyFirstTimeKey, true);
    emit(state.copyWith(showPrivacyFirstTime: false));
  }

  void toggleAgreed(bool value) {
    emit(state.copyWith(agreed: value));
  }

  void confirmAgreed() {
    emit(state.copyWith(agreed: true, showAgreed: false));
    if (state.loginType == 1) {
      wechatAuth();
    } else if (state.loginType == 2) {
      appleAuth();
    }
  }

  void cancelAgreed() {
    emit(state.copyWith(showAgreed: false));
  }

  ///
  /// 通过 Apple 登录
  ///
  void appleAuth() async {
    emit(state.copyWith(loginType: 2));

    if (!state.agreed) {
      emit(state.copyWith(showAgreed: true));
      return;
    }

    AuthorizationCredentialAppleID credential = await SignInWithApple.getAppleIDCredential(
      scopes: [
        AppleIDAuthorizationScopes.email,
        AppleIDAuthorizationScopes.fullName,
      ],
    );

    debugPrint('用户唯一标识: ${credential.userIdentifier}');
    debugPrint('用户邮箱: ${credential.email}');
    debugPrint('用户姓名: ${credential.givenName} ${credential.familyName}');
    // 应将 credential.authorizationCode 发送给后端服务器,由后端与 Apple 服务器验证该码的有效性
    debugPrint('授权码 (authorizationCode): ${credential.authorizationCode}');
    debugPrint('身份令牌 (identityToken): ${credential.identityToken}');

    if (credential.userIdentifier == null) {
      Fluttertoast.showToast(msg: '授权失败', gravity: ToastGravity.TOP, backgroundColor: Colors.red);
      return;
    }

    ///
    /// 处理 credential,发送到服务器获取用户信息。有关联用户信息,则登录成功;没有关联用户信息,则弹出提示框提示用户授权微信认证
    ///
    var resultData = await _userAuthRepository.appleLogin(
        credential.userIdentifier!, credential.authorizationCode, credential.identityToken!) as Map<String, dynamic>?;
    if (resultData == null) {
      Fluttertoast.showToast(msg: '登录请求处理失败', gravity: ToastGravity.TOP, backgroundColor: Colors.red);
      return;
    }
    if (resultData['code'] != 0) {
      Fluttertoast.showToast(msg: resultData['error'], gravity: ToastGravity.TOP, backgroundColor: Colors.red);
      return;
    }

    var data = resultData['data'] as Map<String, dynamic>;
    int binding = resultData['binding'];
    if (binding == 1) {
      _handleLoginSuccess(data);
    } else {
      // 未绑定时,也会返回 sessionCode 和 userCode
      if (state.wechatInstalled) {
        // 已安装微信APP,直接拉起微信授权,不使用 sessionCode 和 userCode
        // 设置 appleUserIdentifier 状态,通知用户需要授权微信认证
        emit(state.copyWith(appleUserIdentifier: data['appleUid']!, showNeedWechatForApple: true));
      } else {
        // 未安装微信APP,使用 sessionCode 和 userCode
        _handleLoginSuccess(data);
      }
    }
  }

  Future<void> wechatAuthForApple() async {
    emit(state.copyWith(showNeedWechatForApple: false, loginType: 2));

    var authResult = await _fluwx.authBy(
      which: NormalAuth(scope: 'snsapi_userinfo', state: 'wechat_sdk_test'),
    );

    if (!authResult) {
      Fluttertoast.showToast(msg: '微信授权处理失败', gravity: ToastGravity.TOP, backgroundColor: Colors.red);
      return;
    }

    // 控制显示加载框,并启动等待微信授权回调的兜底机制
    emit(state.copyWith(loading: true));
    _startWechatAuthWaiting();
  }

  Future<void> wechatAuth() async {
    emit(state.copyWith(loginType: 1));

    if (!state.agreed) {
      emit(state.copyWith(showAgreed: true));
      return;
    }

    var authResult = await _fluwx.authBy(
      which: NormalAuth(scope: 'snsapi_userinfo', state: 'wechat_sdk_test'),
    );

    if (!authResult) {
      Fluttertoast.showToast(msg: '微信授权处理失败', gravity: ToastGravity.TOP, backgroundColor: Colors.red);
      return;
    }

    // 控制显示加载框,并启动等待微信授权回调的兜底机制
    emit(state.copyWith(loading: true));
    _startWechatAuthWaiting();
  }

  void goLoginPhone() {
    router.go('/loginPhone');
  }

  void goLoginQr() {
    router.go('/loginQr');
  }

  void _responseListener(WeChatResponse response) async {
    if (response is WeChatAuthResponse) {
      // 收到正式回调,结束等待状态(不弹提示)
      _finishWechatAuthWaiting();
      if (response.code == null || response.code == '') {
        emit(state.copyWith(loading: false));
        return;
      }

      var resultData = await _wechatAuthRepository.codeToSk(response.code!) as Map<String, dynamic>?;

      // 请求接口异常
      if (resultData == null) {
        Fluttertoast.showToast(msg: '登录请求处理失败', gravity: ToastGravity.TOP, backgroundColor: Colors.red);
        return;
      }

      // 状态码错误
      if (resultData['resultCode'] != '001') {
        Fluttertoast.showToast(msg: '登录请求状态失败', gravity: ToastGravity.TOP, backgroundColor: Colors.red);
        return;
      }

      var data = resultData['data'] as Map<String, dynamic>;
      _handleLoginSuccess(data);
    }
  }

  void _handleLoginSuccess(Map<String, dynamic> data) {
    var roles = data['roles'];
    // 过滤出家长角色的数据
    if (roles?.isNotEmpty ?? false) {
      roles.removeWhere((element) => element['userType'] != 2);
    } else {
      roles = [];
    }

    var sessionCode = data['sessionCode'];
    var userCode = data['userCode'];
    var classCode = '';
    var userType = 0;
    var stuId = '';
    var className = '';
    var stuName = '';
    var relation = '';

    var sharedPreferences = getIt.get<SharedPreferences>();

    if (roles.isNotEmpty) {
      var role = roles[0];
      classCode = role['classCode'];
      userType = role['userType'];
      stuId = role['stuId'];
      className = role['className'];
      stuName = role['stuName'];
      relation = role['relation'] ?? '';

      List<String> classIdList = [];
      for (var role in roles) {
        classIdList.add(role['classCode'] as String);
      }
      debugPrint('classCodeIds:-------------- $classIdList');
      sharedPreferences.setStringList(Constant.classIdSetKey, classIdList);
    } else {
      sharedPreferences.setStringList(Constant.classIdSetKey, []);
    }

    var preUserCode = sharedPreferences.getString('pre_userCode') ?? '';
    if (userCode != preUserCode) {
      sharedPreferences.setString('pre_userCode', userCode);
      sharedPreferences.setString('pre_classCode', classCode);
      sharedPreferences.setInt('pre_userType', userType);
      sharedPreferences.setString('pre_stuId', stuId);
    } else {
      var preClassCode = sharedPreferences.getString('pre_classCode') ?? '';
      var preUserType = sharedPreferences.getInt('pre_userType') ?? 0;
      var preStuId = sharedPreferences.getString('pre_stuId') ?? '';

      if (preClassCode != '' &&
          roles.any((element) =>
              element['classCode'] == preClassCode &&
              element['userType'] == preUserType &&
              element['stuId'] == preStuId)) {
        classCode = preClassCode;
        userType = preUserType;
        stuId = preStuId;
      } else {
        sharedPreferences.setString('pre_userCode', userCode);
        sharedPreferences.setString('pre_classCode', classCode);
        sharedPreferences.setInt('pre_userType', userType);
        sharedPreferences.setString('pre_stuId', stuId);
      }
    }

    sharedPreferences.setString('auth_sessionCode', sessionCode);
    sharedPreferences.setString('auth_userCode', userCode);
    sharedPreferences.setString('auth_classCode', classCode);
    sharedPreferences.setInt('auth_userType', userType);
    sharedPreferences.setString('auth_stuId', stuId);
    sharedPreferences.setString('auth_className', className);
    sharedPreferences.setString('auth_stuName', stuName);
    sharedPreferences.setString('auth_relation', relation);

    debugPrint('loginType: ${state.loginType} appleUid: ${state.appleUserIdentifier}');
    // 针对 Apple 登录
    if (state.loginType == 2 && state.appleUserIdentifier.isNotEmpty) {
      // appleUserIdentifier未绑定,则进行绑定
      _userAuthRepository.newBinding(state.appleUserIdentifier, userCode);
    }

    router.go(
      '/web',
      extra: {
        'sessionCode': sessionCode,
        'userCode': userCode,
        'classCode': classCode,
        'userType': userType,
        'stuId': stuId,
      },
    );
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState appState) {
    // App 回到前台时,如果仍在等待微信授权回调,给真实回调一个短窗口;
    // 窗口内仍未收到 WeChatAuthResponse,则认定为系统层取消(典型场景:
    // 设备装有多个微信,系统弹出选择器后用户点击取消,微信 SDK 根本不会回调)。
    if (appState == AppLifecycleState.resumed && _waitingWechatAuth) {
      _wechatAuthResumeTimer?.cancel();
      _wechatAuthResumeTimer = Timer(const Duration(milliseconds: 1500), () {
        if (_waitingWechatAuth) {
          _finishWechatAuthWaiting(reason: _WechatAuthFinishReason.cancel);
        }
      });
    }
  }

  // 标记进入"等待微信授权回调"状态,并启动总超时
  void _startWechatAuthWaiting() {
    _waitingWechatAuth = true;
    _wechatAuthTimeoutTimer?.cancel();
    _wechatAuthTimeoutTimer = Timer(const Duration(seconds: 30), () {
      if (_waitingWechatAuth) {
        _finishWechatAuthWaiting(reason: _WechatAuthFinishReason.timeout);
      }
    });
  }

  // 结束等待,统一复位 loading 状态并按场景给出提示
  void _finishWechatAuthWaiting({
    _WechatAuthFinishReason reason = _WechatAuthFinishReason.response,
  }) {
    if (!_waitingWechatAuth) return;
    _waitingWechatAuth = false;
    _wechatAuthTimeoutTimer?.cancel();
    _wechatAuthResumeTimer?.cancel();
    _wechatAuthTimeoutTimer = null;
    _wechatAuthResumeTimer = null;
    if (state.loading) {
      emit(state.copyWith(loading: false));
    }
    switch (reason) {
      case _WechatAuthFinishReason.cancel:
        Fluttertoast.showToast(msg: '已取消微信授权', gravity: ToastGravity.TOP);
        break;
      case _WechatAuthFinishReason.timeout:
        Fluttertoast.showToast(
          msg: '微信授权超时,请重试',
          gravity: ToastGravity.TOP,
          backgroundColor: Colors.red,
        );
        break;
      case _WechatAuthFinishReason.response:
      // 正常收到回调时不弹提示
        break;
    }
  }

  @override
  Future<void> close() {
    WidgetsBinding.instance.removeObserver(this);
    _wechatAuthTimeoutTimer?.cancel();
    _wechatAuthResumeTimer?.cancel();
    _fluwxCancelable.cancel();
    return super.close();
  }


}

enum _WechatAuthFinishReason {
  // 收到了正式的 WeChatAuthResponse
  response,
  // App 已回到前台但仍无回调,判定为系统层/微信侧取消
  cancel,
  // 总超时
  timeout,
}