288 lines
11 KiB
Dart
288 lines
11 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:dio/dio.dart';
|
||
import 'package:firebase_auth/firebase_auth.dart';
|
||
import 'package:guru_app/account/account_data_store.dart';
|
||
import 'package:guru_app/account/model/account.dart';
|
||
import 'package:guru_app/account/model/account_profile.dart';
|
||
import 'package:guru_app/account/model/user.dart';
|
||
import 'package:guru_app/analytics/guru_analytics.dart';
|
||
import 'package:guru_app/api/guru_api.dart';
|
||
import 'package:guru_app/firebase/firebase.dart';
|
||
import 'package:guru_app/firebase/firestore/firestore_manager.dart';
|
||
import 'package:guru_app/guru_app.dart';
|
||
import 'package:guru_app/property/app_property.dart';
|
||
import 'package:guru_app/property/settings/guru_settings.dart';
|
||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||
import 'package:guru_utils/collection/collectionutils.dart';
|
||
import 'package:guru_utils/core/ext.dart';
|
||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||
import 'package:guru_utils/device/device_info.dart';
|
||
import 'package:guru_utils/device/device_utils.dart';
|
||
import 'package:guru_utils/log/log.dart';
|
||
import 'package:guru_utils/network/network_utils.dart';
|
||
|
||
import 'model/credential.dart';
|
||
|
||
/// Created by Haoyi on 6/3/21
|
||
///
|
||
///
|
||
part "account_service_extension.dart";
|
||
|
||
part "account_auth_extension.dart";
|
||
|
||
part "account_auth_invoker.dart";
|
||
|
||
class ModifyNicknameException implements Exception {
|
||
final String? message;
|
||
final dynamic cause;
|
||
|
||
ModifyNicknameException(this.message, {this.cause});
|
||
|
||
@override
|
||
String toString() {
|
||
return "ModifyNicknameException: $message cause:$cause";
|
||
}
|
||
}
|
||
|
||
class ModifyLevelException implements Exception {
|
||
final String? message;
|
||
final dynamic cause;
|
||
|
||
ModifyLevelException(this.message, {this.cause});
|
||
|
||
@override
|
||
String toString() {
|
||
return "ModifyLevelException: $message cause:$cause";
|
||
}
|
||
}
|
||
|
||
class AccountManager {
|
||
final AccountDataStore accountDataStore;
|
||
|
||
Timer? retryTimer;
|
||
|
||
static final AccountManager instance = AccountManager();
|
||
|
||
static const List<AuthCredentialDelegate> defaultSupportedAuthCredentialDelegates = [
|
||
AnonymousCredentialDelegate()
|
||
];
|
||
|
||
AccountManager() : accountDataStore = AccountDataStore.instance;
|
||
|
||
Future init({Completer<dynamic>? completer}) async {
|
||
try {
|
||
final result = accountDataStore.transitionTo(AccountDataStatus.initializing);
|
||
if (!result) {
|
||
Log.w(
|
||
"init account error, current initializing! please wait result! retry[${accountDataStore.initRetryCount}]",
|
||
tag: "Account");
|
||
return;
|
||
}
|
||
retryTimer?.cancel();
|
||
final account = await AppProperty.getInstance().loadAccount();
|
||
final restoreResult = await _restoreAccount(account);
|
||
if (!restoreResult) {
|
||
Log.v("init account error: restoreAccount error! retry[${accountDataStore.initRetryCount}]",
|
||
tag: "Account");
|
||
_retry();
|
||
} else {
|
||
accountDataStore.initRetryCount = 0;
|
||
accountDataStore.transitionTo(AccountDataStatus.initialized);
|
||
Log.v("init account success!", tag: "Account");
|
||
}
|
||
} catch (error, stacktrace) {
|
||
completer?.complete(error);
|
||
Log.v("init account error retry[${accountDataStore.initRetryCount}]:$error $stacktrace",
|
||
tag: "Account");
|
||
_retry();
|
||
}
|
||
completer?.complete(true);
|
||
}
|
||
|
||
void _retry() {
|
||
final intervalSeconds = (accountDataStore.initRetryCount * 2 + 8).clamp(8, 30);
|
||
retryTimer?.cancel();
|
||
accountDataStore.transitionTo(AccountDataStatus.waiting);
|
||
retryTimer = Timer(Duration(seconds: intervalSeconds), () {
|
||
init();
|
||
accountDataStore.initRetryCount++;
|
||
});
|
||
}
|
||
|
||
Future updateLocalProfile(Map<String, dynamic> modifiedJson) async {
|
||
modifiedJson[AccountProfile.dirtyField] = true;
|
||
final dirtyAccountProfile = accountDataStore.accountProfile?.merge(modifiedJson) ??
|
||
AccountProfile.fromJson(modifiedJson);
|
||
AppProperty.getInstance().setAccountProfile(dirtyAccountProfile);
|
||
accountDataStore.updateAccountProfile(dirtyAccountProfile);
|
||
}
|
||
|
||
/// 登录
|
||
///
|
||
/// [authType] 登录类型
|
||
/// [onConflict] 登录冲突处理
|
||
/// [onLogin] 登录成功处理
|
||
///
|
||
Future<bool> loginWith(AuthType authType) async {
|
||
late final Credential? credential;
|
||
try {
|
||
final result = await AuthCredentialManager.instance.loginWith(authType);
|
||
credential = result.credential;
|
||
if (!result.isSuccess || credential == null) {
|
||
Log.w("loginWith $authType error! credential: [$credential]", tag: "Account");
|
||
return false;
|
||
}
|
||
} catch (error, stacktrace) {
|
||
Log.e("loginWith $authType error:$error, $stacktrace");
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
/// 如果冲突将会报 409 的错
|
||
final guruUser = await _requestGuruUser(credential);
|
||
await processLogin(guruUser, credential);
|
||
return true;
|
||
} catch (error, stacktrace) {
|
||
Log.w("loginWith $authType error:$error, $stacktrace");
|
||
if (error is DioError && error.response?.statusCode == 409) {
|
||
return await _processConflict(credential);
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<bool> processLogin(GuruUser user, Credential credential) async {
|
||
await _updateGuruUser(user);
|
||
await _bindCredential(credential);
|
||
try {
|
||
await _verifyOrReportAuthDevice(user);
|
||
authenticateFirebase();
|
||
} catch (error, stacktrace) {
|
||
Log.e("_verifyOrReportAuthDevice error!$error $stacktrace");
|
||
}
|
||
if (credential.isAnonymous) {
|
||
return await _invokeAnonymousLogin(user, credential);
|
||
} else {
|
||
return await _invokeLogin(user, credential);
|
||
}
|
||
}
|
||
|
||
///
|
||
/// 登出操作,会把所有的第三方登陆都登出,如果当前有对应的匿名登陆的 credential 那么保留到匿名登陆的状态
|
||
/// 如果当前没有匿名登录的 credential 那么这里将会重新创建一个匿名登陆,并且这里不会进行数据迁移
|
||
///
|
||
/// 如果在调用 logout 时,明确指定了要登出哪些 AuthType,
|
||
/// 那么 logout 方法将尝试用 unbind 的方法去处理用户信息。
|
||
/// 当尝试解绑掉指定的 authTypes 时,只要满足下面两种情况的其中一种,
|
||
/// 都不能以 unbind 形式进行处理,都会认定为是真正的 logout
|
||
/// 1. 如果解绑掉所有指定的凭证后,当前凭证信息只保留了一个匿名凭证
|
||
/// 2. 如果解绑掉所有指定的凭证后,当前没有任何凭证信息
|
||
/// 如果上面两个条件都不满足,那么将以 unbind方法进行 logout
|
||
/// 以 unbind 形式进行 logout时,将不会通知应用 onLogout方法
|
||
///
|
||
/// 因此这里需要注意,就算明确指定了登出的 AuthType,依然存在调用 onLogout 的情况
|
||
///
|
||
Future<GuruUser?> logout({bool switching = false, Set<AuthType>? authTypes}) async {
|
||
bool isUnbind = false;
|
||
|
||
if (authTypes != null && authTypes.isNotEmpty) {
|
||
final currentCredentials = accountDataStore.credentials.keys.toSet();
|
||
currentCredentials.removeAll(authTypes);
|
||
currentCredentials.remove(AuthType.anonymous);
|
||
isUnbind = currentCredentials.isNotEmpty;
|
||
}
|
||
|
||
final logoutUser = accountDataStore.user?.copyWith();
|
||
try {
|
||
if (!isUnbind && logoutUser != null) {
|
||
final result = await _invokeLogout(logoutUser);
|
||
if (!result) {
|
||
Log.w("logout error! ignore!");
|
||
return null;
|
||
}
|
||
}
|
||
} catch (error, stacktrace) {
|
||
Log.w("invokeLogout error! $error!");
|
||
return null;
|
||
}
|
||
for (var authType in accountDataStore.credentials.keys) {
|
||
/// 默认的登出只是 unbind 掉三方的 credentials ,不会真正的登出
|
||
/// 如果当前没有匿名登陆,那么就会真正的登出,并会重新登陆匿名,但是数据不会清除
|
||
/// 如果 authTypes传的是 null,这里会返回空,依然满足不等于 False,
|
||
/// 这里只要是空或是真正的包含才会进行真正的解绑
|
||
if (authTypes?.contains(authType) != false && authType != AuthType.anonymous) {
|
||
await AuthCredentialManager.instance.logout(authType);
|
||
_unbindCredential(authType);
|
||
}
|
||
}
|
||
|
||
/// 如果当前连匿名登陆也没有了,那么就会重新登陆匿名帐号
|
||
/// 如果是正在切换帐号的话,这里不需要登录一个新的匿名帐号
|
||
if (!switching && accountDataStore.credentials.isEmpty) {
|
||
final auth = await _retrieveAnonymous();
|
||
if (auth != null) {
|
||
await processLogin(auth.user, auth.credential!);
|
||
}
|
||
}
|
||
return logoutUser;
|
||
}
|
||
|
||
Future<bool> modifyProfile(
|
||
{String? nickname,
|
||
String? avatar,
|
||
String? countryCode,
|
||
Map<String, dynamic> userData = const <String, dynamic>{}}) async {
|
||
int retryCount = 2;
|
||
Log.i("modifyProfile $nickname $avatar $countryCode", syncFirebase: true, tag: "Account");
|
||
if (nickname == null && avatar == null && countryCode == null && userData.isEmpty) {
|
||
return false;
|
||
}
|
||
final now = DateTimeUtils.currentTimeInMillis();
|
||
final modifiedJson = CollectionUtils.filterOutNulls(<String, dynamic>{
|
||
AccountProfile.uidField: accountDataStore.uid,
|
||
AccountProfile.nicknameField: nickname,
|
||
AccountProfile.countryField: countryCode?.toLowerCase(),
|
||
AccountProfile.avatarField: avatar,
|
||
AccountProfile.updateAtField: now,
|
||
AccountProfile.versionField: GuruSettings.instance.version.get(),
|
||
AccountProfile.roleField:
|
||
GuruSettings.instance.debugMode.get() == true ? UserAttr.tester : UserAttr.real,
|
||
AccountProfile.dirtyField: true,
|
||
...userData
|
||
});
|
||
await updateLocalProfile(modifiedJson);
|
||
|
||
/// 如果本地部署没有打开同步 AccountProfile 机制,这里直接返回 true
|
||
if (!GuruApp.instance.appSpec.deployment.enabledSyncAccountProfile) {
|
||
return true;
|
||
}
|
||
while ((await NetworkUtils.isNetworkConnected()) && retryCount-- > 0) {
|
||
final accountProfile =
|
||
await FirestoreManager.instance.modifyProfile(modifiedJson).onError((error, stackTrace) {
|
||
Log.i("modifyProfile error!:$error");
|
||
GuruAnalytics.instance.logException(ModifyLevelException("modifyProfile error!:$error"),
|
||
stacktrace: stackTrace);
|
||
return null;
|
||
});
|
||
if (accountProfile != null) {
|
||
Log.i("modifyProfile success! $accountProfile", tag: "Account");
|
||
AppProperty.getInstance().setAccountProfile(accountProfile);
|
||
accountDataStore.updateAccountProfile(accountProfile);
|
||
return true;
|
||
} else {
|
||
Log.i("[$retryCount] modify profile error!", tag: "Account");
|
||
await authenticateFirebase()
|
||
.timeout(const Duration(seconds: 15))
|
||
.catchError((error, stackTrace) {
|
||
Log.i("re-authenticate error:$error", stackTrace: stackTrace, tag: "Account");
|
||
});
|
||
await Future.delayed(const Duration(seconds: 1));
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|