guru_sdk/guru_app/lib/account/account_manager.dart

288 lines
11 KiB
Dart
Raw Normal View History

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;
}
}