parent
59b48f342c
commit
ea55fd4551
|
|
@ -1,5 +1,7 @@
|
|||
app_name: GuruApp
|
||||
|
||||
app_category: app
|
||||
|
||||
flavor: "guru_test"
|
||||
|
||||
# App接入GuruApp的基础信息(下面内容必填)
|
||||
|
|
@ -81,7 +83,6 @@ deployment:
|
|||
# ios 验证服务器的密码
|
||||
ios_validate_receipt_password: aa998877665544332211bb00cc
|
||||
|
||||
|
||||
# 被标注的conversion点,在自打点库中将被以Emergency的优先级进行发送
|
||||
conversion_events:
|
||||
- first_rads_rewarded
|
||||
|
|
@ -166,6 +167,30 @@ deployment:
|
|||
# 因此它的优先级永远不会大于正常的 Banner 广告, 默认值是 false
|
||||
show_internal_ads_when_banner_unavailable: true
|
||||
|
||||
# 由于订阅订单比较重要,而从用户反馈的日志上来看,会存在接口返回异常的问题
|
||||
# 因此针对这种情况,添加订阅的恢复宽限次数,默认为 3 次
|
||||
# 当订阅订单恢复失败次数超过该次数,才会真正删除
|
||||
subscription_restore_grace_count: 3
|
||||
|
||||
# 插屏在展示广告前,为了保证用户的体验,会有一个广告的保护时间,
|
||||
# 即:距上一次全屏广告(插屏广告和激励广告)的结束间隔时间,
|
||||
# 默认的间隔保护时间为 1 分钟(60 秒)单位为秒
|
||||
fullscreen_ads_min_interval: 60
|
||||
|
||||
# 是否打开中台的 AccountProfile 同步机制
|
||||
# 打开后,在登陆后(包括匿名登陆) 会启动向 Firestore 进行同步AccountProfile的机制
|
||||
# Firestore 针对 AccountProfile的存储位置默认放在 users 表中
|
||||
enabled_sync_account_profile: false
|
||||
|
||||
# 根据 BI 的需求,对应的 Purchase事件只能报太极的 001 或 020的其中一个
|
||||
# 因此添加 Purchase Event 的 trigger, 默认值为 1
|
||||
# 1: 表示在发生购买时打 tch_ad_rev_roas_001
|
||||
# 2: 表示在发生购买时打 tch_ad_rev_roas_020
|
||||
# 在广告展示时也会依据该 trigger 的值,在不同的时机打对应的 purchase事件
|
||||
purchase_event_trigger: 1
|
||||
|
||||
# tracking_notification_permission_pass_analytics_type : guru|firebase
|
||||
|
||||
# 广告配置
|
||||
ads_profile:
|
||||
# Banner广告ID(变现提供)
|
||||
|
|
@ -202,7 +227,6 @@ ads_profile:
|
|||
android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
|
||||
|
||||
remote_config:
|
||||
# 保留配置,插屏广告相关配置
|
||||
iads_config: '{"free_s":600,"win_count":4,"scene":"game_start","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}'
|
||||
|
|
@ -215,6 +239,10 @@ remote_config:
|
|||
|
||||
# 保留配置,打点相关配置
|
||||
analytics_config: '{"cap":"firebase|facebook|guru", "init_delay_s": 10}'
|
||||
#
|
||||
# _mapping:
|
||||
# cdn_config: "cdn2_config"
|
||||
|
||||
|
||||
products:
|
||||
# sku
|
||||
|
|
@ -311,6 +339,7 @@ products:
|
|||
manifest:
|
||||
category: "prop"
|
||||
details:
|
||||
sku: "{1}_{2}"
|
||||
type: "prop"
|
||||
amount: 1
|
||||
theme_id: "{1}"
|
||||
|
|
@ -389,15 +418,15 @@ products:
|
|||
details:
|
||||
type: "igc"
|
||||
amount: 16000
|
||||
|
||||
theme_mul:
|
||||
sku: "theme_{category}_{theme_id}"
|
||||
attr: possessive
|
||||
method: igc
|
||||
manifest:
|
||||
category: "{1}"
|
||||
theme_id: "{2}"
|
||||
cate: "{1}"
|
||||
#
|
||||
# theme_mul:
|
||||
# sku: "theme_{category}_{theme_id}"
|
||||
# attr: possessive
|
||||
# method: igc
|
||||
# manifest:
|
||||
# category: "{1}"
|
||||
# theme_id: "{2}"
|
||||
# cate: "{1}"
|
||||
|
||||
# adjust 相关配置
|
||||
adjust_profile:
|
||||
|
|
@ -429,3 +458,46 @@ adjust_profile:
|
|||
android: 95fu7q
|
||||
ios: 1p8z5t
|
||||
|
||||
experiments:
|
||||
|
||||
test:
|
||||
start: 20240129T000000
|
||||
end: 20240129T000000
|
||||
audience:
|
||||
filters:
|
||||
- version:
|
||||
opt: lt
|
||||
mmp: 2.3.0
|
||||
- country:
|
||||
included: ""
|
||||
excluded: "us,cn,en"
|
||||
- platform:
|
||||
android:
|
||||
opt: gte
|
||||
ver: 33
|
||||
ios:
|
||||
opt: gte
|
||||
ver: 14
|
||||
variant: 2
|
||||
test2:
|
||||
start: 20240129T000000
|
||||
end: 20240129T000000
|
||||
audience:
|
||||
filters:
|
||||
- version:
|
||||
opt: lt
|
||||
mmp: 2.3.0
|
||||
- country:
|
||||
included: "cn"
|
||||
excluded: "us"
|
||||
- platform:
|
||||
android:
|
||||
opt: lt
|
||||
ver: 24
|
||||
ios:
|
||||
opt: gte
|
||||
ver: 14
|
||||
- new_user: true
|
||||
variant: 5
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,41 +3,41 @@
|
|||
part of "account_manager.dart";
|
||||
|
||||
extension AccountAuthExtension on AccountManager {
|
||||
Future<AccountAuth> _authenticate(SaasUser saasUser,
|
||||
Future<FirebaseAccountAuth> _loginFirebase(GuruUser guruUser,
|
||||
{bool canRefreshFirebaseToken = true}) async {
|
||||
User? firebaseUser;
|
||||
SaasUser newSaasUser = saasUser;
|
||||
firebaseUser = await _authenticateFirebase(saasUser).catchError((error) {
|
||||
GuruUser newGuruUser = guruUser;
|
||||
firebaseUser = await _authenticateFirebase(guruUser).catchError((error) {
|
||||
Log.e("_authenticateFirebase error! $error", tag: "Account");
|
||||
return null;
|
||||
});
|
||||
if (firebaseUser == null && canRefreshFirebaseToken) {
|
||||
try {
|
||||
newSaasUser = await _refreshFirebaseToken(saasUser);
|
||||
return _authenticate(newSaasUser, canRefreshFirebaseToken: false);
|
||||
newGuruUser = await _refreshFirebaseToken(guruUser);
|
||||
return _loginFirebase(newGuruUser, canRefreshFirebaseToken: false);
|
||||
} catch (error, stacktrace) {
|
||||
return AccountAuth(saasUser, null);
|
||||
return FirebaseAccountAuth(guruUser, firebaseUser: null);
|
||||
}
|
||||
}
|
||||
|
||||
return AccountAuth(newSaasUser, firebaseUser);
|
||||
return FirebaseAccountAuth(newGuruUser, firebaseUser: firebaseUser);
|
||||
}
|
||||
|
||||
Future<SaasUser> _refreshFirebaseToken(SaasUser oldSaasUser) async {
|
||||
Future<GuruUser> _refreshFirebaseToken(GuruUser oldSaasUser) async {
|
||||
return await GuruApi.instance
|
||||
.renewFirebaseToken()
|
||||
.then((tokenData) => oldSaasUser.copyWith(firebaseToken: tokenData.firebaseToken));
|
||||
}
|
||||
|
||||
Future<User?> _authenticateFirebase(SaasUser saasUser) async {
|
||||
Future<User?> _authenticateFirebase(GuruUser guruUser) async {
|
||||
int retry = 0;
|
||||
dynamic lastError;
|
||||
while (retry < 1) {
|
||||
try {
|
||||
Log.i("[$retry] _authenticateFirebase:${saasUser.firebaseToken}", tag: "Account");
|
||||
Log.i("[$retry] _authenticateFirebase:${guruUser.firebaseToken}", tag: "Account");
|
||||
|
||||
return await FirebaseAuth.instance
|
||||
.signInWithCustomToken(saasUser.firebaseToken)
|
||||
.signInWithCustomToken(guruUser.firebaseToken)
|
||||
.then((result) => result.user);
|
||||
} catch (error, stacktrace) {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
|
|
@ -48,4 +48,26 @@ extension AccountAuthExtension on AccountManager {
|
|||
}
|
||||
throw lastError ?? ("_authenticateFirebase error!");
|
||||
}
|
||||
|
||||
Future<bool> authenticateFirebase() async {
|
||||
final guruUser = accountDataStore.user;
|
||||
if (guruUser == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final auth = await _loginFirebase(guruUser);
|
||||
final newGuruUser = auth.user;
|
||||
if (!guruUser.isSame(newGuruUser)) {
|
||||
_updateGuruUser(newGuruUser);
|
||||
}
|
||||
if (auth.firebaseUser != null) {
|
||||
_updateFirebaseUser(auth.firebaseUser!);
|
||||
Log.i("_updateFirebaseUser success!", tag: "Account");
|
||||
}
|
||||
return true;
|
||||
} catch (error, stacktrace) {
|
||||
GuruAnalytics.instance.logException(error, stacktrace: stacktrace);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
part of "account_manager.dart";
|
||||
|
||||
extension AccountAuthInvoker on AccountManager {
|
||||
Future<bool> _invokeLogin(GuruUser loginUser, Credential credential) async {
|
||||
return await GuruApp.instance.protocol.accountAuthDelegate?.onLogin(loginUser, credential) ??
|
||||
true;
|
||||
}
|
||||
|
||||
Future<bool> _invokeLogout(GuruUser logoutUser) async {
|
||||
return await GuruApp.instance.protocol.accountAuthDelegate?.onLogout(logoutUser) ?? true;
|
||||
}
|
||||
|
||||
Future<bool> _invokeAnonymousLogout(GuruUser logoutUser) async {
|
||||
return await GuruApp.instance.protocol.accountAuthDelegate?.onAnonymousLogout(logoutUser) ??
|
||||
true;
|
||||
}
|
||||
|
||||
Future<bool> _invokeAnonymousLogin(GuruUser loginUser, Credential credential) async {
|
||||
return await GuruApp.instance.protocol.accountAuthDelegate?.onAnonymousLogin(loginUser, credential) ??
|
||||
true;
|
||||
}
|
||||
|
||||
Future<bool> _invokeConflict() async {
|
||||
return await GuruApp.instance.protocol.accountAuthDelegate?.onConflict() ?? false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import 'dart:convert';
|
||||
import 'package:firebase_auth/firebase_auth.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/guru_app.dart';
|
||||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||||
import 'package:guru_utils/device/device_info.dart';
|
||||
import 'package:guru_utils/extensions/extensions.dart';
|
||||
|
||||
|
|
@ -19,11 +21,15 @@ class AccountDataStore {
|
|||
static final AccountDataStore instance = AccountDataStore._();
|
||||
|
||||
final BehaviorSubject<DeviceInfo?> _deviceInfoSubject = BehaviorSubject.seeded(null);
|
||||
final BehaviorSubject<SaasUser?> _saasUserSubject = BehaviorSubject.seeded(null);
|
||||
final BehaviorSubject<GuruUser?> _guruUserSubject = BehaviorSubject.seeded(null);
|
||||
final BehaviorSubject<User?> _firebaseUser = BehaviorSubject.seeded(null);
|
||||
final BehaviorSubject<AccountProfile?> _accountProfile = BehaviorSubject.seeded(null);
|
||||
final BehaviorSubject<AccountDataStatus> _accountDataStatus =
|
||||
BehaviorSubject<AccountDataStatus>.seeded(AccountDataStatus.idle);
|
||||
|
||||
final BehaviorSubject<Map<AuthType, Credential>> _credentials =
|
||||
BehaviorSubject.seeded(<AuthType, Credential>{});
|
||||
|
||||
int initRetryCount = 0;
|
||||
|
||||
Stream<AccountProfile?> get observableAccountProfile => _accountProfile.stream;
|
||||
|
|
@ -32,9 +38,12 @@ class AccountDataStore {
|
|||
|
||||
AccountDataStore._();
|
||||
|
||||
String? get saasToken => _saasUserSubject.value?.token;
|
||||
@Deprecated("use guruToken instead")
|
||||
String? get saasToken => _guruUserSubject.value?.token;
|
||||
|
||||
String? get uid => _saasUserSubject.value?.uid;
|
||||
String? get guruToken => _guruUserSubject.value?.token;
|
||||
|
||||
String? get uid => _guruUserSubject.value?.uid;
|
||||
|
||||
AccountProfile? get accountProfile => _accountProfile.value;
|
||||
|
||||
|
|
@ -42,7 +51,7 @@ class AccountDataStore {
|
|||
|
||||
String? get countryCode => _accountProfile.value?.countryCode;
|
||||
|
||||
SaasUser? get user => _saasUserSubject.value;
|
||||
GuruUser? get user => _guruUserSubject.value;
|
||||
|
||||
String? get avatar => _accountProfile.value?.avatar;
|
||||
|
||||
|
|
@ -55,16 +64,42 @@ class AccountDataStore {
|
|||
Stream<bool> get observableInitialized =>
|
||||
_accountDataStatus.stream.map((status) => status == AccountDataStatus.initialized);
|
||||
|
||||
Stream<SaasUser?> get observableSaasUser => _saasUserSubject.stream;
|
||||
bool get hasUid => uid?.isNotEmpty == true;
|
||||
|
||||
bool get isAnonymous =>
|
||||
(uid?.isNotEmpty != true) ||
|
||||
(_credentials.value.containsKey(AuthType.anonymous) && _credentials.value.length == 1);
|
||||
|
||||
Stream<GuruUser?> get observableSaasUser => _guruUserSubject.stream;
|
||||
|
||||
Map<AuthType, Credential> get credentials => _credentials.value;
|
||||
|
||||
Account get account => Account.restore(
|
||||
guruUser: user,
|
||||
device: currentDevice,
|
||||
accountProfile: accountProfile,
|
||||
firebaseUser: _firebaseUser.value,
|
||||
credentials: credentials);
|
||||
|
||||
Stream<Account> get observableAccount => Rx.combineLatestList([
|
||||
_guruUserSubject.stream,
|
||||
_deviceInfoSubject.stream,
|
||||
_accountProfile.stream,
|
||||
_firebaseUser.stream,
|
||||
_credentials.stream
|
||||
]).debounceTime(const Duration(milliseconds: 100)).map((_) => account);
|
||||
|
||||
bool get isSocialLogged => (uid?.isNotEmpty == true) && credentials.isNotEmpty;
|
||||
|
||||
void dispose() {
|
||||
_deviceInfoSubject.close();
|
||||
_saasUserSubject.close();
|
||||
_guruUserSubject.close();
|
||||
_firebaseUser.close();
|
||||
_accountProfile.close();
|
||||
_credentials.close();
|
||||
}
|
||||
|
||||
Future<SaasUser?> signInAnonymousInLocked() async {
|
||||
Future<GuruUser?> signInAnonymousInLocked() async {
|
||||
// 这里需要使用原始的http post请求。否则这里将会死锁DIO所有请求
|
||||
final secret = await AppProperty.getInstance().getAnonymousSecretKey();
|
||||
final headers = {
|
||||
|
|
@ -82,7 +117,7 @@ class AccountDataStore {
|
|||
final data = const Utf8Decoder().convert(response.bodyBytes);
|
||||
if (data.isNotEmpty) {
|
||||
final result = json.decode(data);
|
||||
return SaasUser.fromJson(result["data"]);
|
||||
return GuruUser.fromJson(result["data"]);
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Log.v("signInAnonymousInLocked error:$error", tag: "Account");
|
||||
|
|
@ -91,9 +126,9 @@ class AccountDataStore {
|
|||
}
|
||||
|
||||
Future refreshAuth() async {
|
||||
final saasUser = await signInAnonymousInLocked();
|
||||
if (saasUser != null) {
|
||||
updateSaasUser(saasUser);
|
||||
final guruUser = await signInAnonymousInLocked();
|
||||
if (guruUser != null) {
|
||||
updateGuruUser(guruUser);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,12 +136,16 @@ class AccountDataStore {
|
|||
_deviceInfoSubject.addEx(deviceInfo);
|
||||
}
|
||||
|
||||
void updateSaasUser(SaasUser saasUser) {
|
||||
_saasUserSubject.addEx(saasUser);
|
||||
@Deprecated("use updateGuruUser instead")
|
||||
void updateSaasUser(GuruUser saasUser) {
|
||||
updateGuruUser(saasUser);
|
||||
}
|
||||
|
||||
if (saasUser.createAtTimestamp > 0) {
|
||||
void updateGuruUser(GuruUser guruUser) {
|
||||
_guruUserSubject.addEx(guruUser);
|
||||
if (guruUser.createAtTimestamp > 0) {
|
||||
GuruAnalytics.instance
|
||||
.setUserProperty("user_created_timestamp", saasUser.createAtTimestamp.toString());
|
||||
.setUserProperty("user_created_timestamp", guruUser.createAtTimestamp.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +157,31 @@ class AccountDataStore {
|
|||
_accountProfile.addEx(profile);
|
||||
}
|
||||
|
||||
void bindCredential(Credential credential) {
|
||||
final newCredentials = Map.of(_credentials.value);
|
||||
newCredentials[credential.authType] = credential;
|
||||
_credentials.addEx(newCredentials);
|
||||
}
|
||||
|
||||
void unbindCredential(AuthType authType) {
|
||||
final newCredentials = Map.of(_credentials.value);
|
||||
newCredentials.remove(authType);
|
||||
_credentials.addEx(newCredentials);
|
||||
}
|
||||
|
||||
void updateCredentials(Map<AuthType, Credential> credentials) {
|
||||
_credentials.addEx(Map.of(credentials));
|
||||
}
|
||||
|
||||
bool transitionTo(AccountDataStatus status, {AccountDataStatus? expectStatus}) {
|
||||
return _accountDataStatus.addIfChanged(status);
|
||||
}
|
||||
|
||||
logout() {
|
||||
_guruUserSubject.addEx(null);
|
||||
_firebaseUser.addEx(null);
|
||||
_deviceInfoSubject.addEx(null);
|
||||
_accountProfile.addEx(null);
|
||||
_accountDataStatus.addIfChanged(AccountDataStatus.idle);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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';
|
||||
|
|
@ -9,8 +10,10 @@ 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';
|
||||
|
|
@ -19,6 +22,8 @@ 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
|
||||
///
|
||||
///
|
||||
|
|
@ -26,6 +31,8 @@ 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;
|
||||
|
|
@ -53,11 +60,13 @@ class ModifyLevelException implements Exception {
|
|||
class AccountManager {
|
||||
final AccountDataStore accountDataStore;
|
||||
|
||||
// final FirestoreService firestoreService;
|
||||
|
||||
Timer? retryTimer;
|
||||
|
||||
static AccountManager instance = AccountManager();
|
||||
static final AccountManager instance = AccountManager();
|
||||
|
||||
static const List<AuthCredentialDelegate> defaultSupportedAuthCredentialDelegates = [
|
||||
AnonymousCredentialDelegate()
|
||||
];
|
||||
|
||||
AccountManager() : accountDataStore = AccountDataStore.instance;
|
||||
|
||||
|
|
@ -109,6 +118,117 @@ class AccountManager {
|
|||
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,
|
||||
|
|
@ -134,6 +254,10 @@ class AccountManager {
|
|||
});
|
||||
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) {
|
||||
|
|
@ -149,7 +273,9 @@ class AccountManager {
|
|||
return true;
|
||||
} else {
|
||||
Log.i("[$retryCount] modify profile error!", tag: "Account");
|
||||
await authenticate().timeout(const Duration(seconds: 15)).catchError((error, stackTrace) {
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
/// Created by Haoyi on 6/3/21
|
||||
|
||||
part of "account_manager.dart";
|
||||
|
||||
extension AccountServiceExtension on AccountManager {
|
||||
Future<bool> _restoreAccount(Account account) async {
|
||||
SaasUser? saasUser = account.saasUser;
|
||||
Log.d("restoreAccount $saasUser", tag: "Account");
|
||||
saasUser ??= await signInWithAnonymous().catchError((error, stacktrace) {
|
||||
Log.v("signInWithAnonymous error:$error, $stacktrace");
|
||||
return null;
|
||||
});
|
||||
AccountAuth? anonymousAuth;
|
||||
GuruUser? guruUser = account.guruUser;
|
||||
Log.d("restoreAccount $guruUser", tag: "Account");
|
||||
try {
|
||||
if (guruUser == null) {
|
||||
anonymousAuth = await _retrieveAnonymous();
|
||||
guruUser = anonymousAuth?.user;
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("loginWith Anonymous error:$error, $stacktrace");
|
||||
}
|
||||
|
||||
Log.v("_restoreAccount saasUser:$saasUser", tag: "Account");
|
||||
Log.v("_restoreAccount saasUser:$guruUser", tag: "Account");
|
||||
final device = account.device;
|
||||
if (device != null) {
|
||||
_updateDevice(device);
|
||||
|
|
@ -22,45 +26,116 @@ extension AccountServiceExtension on AccountManager {
|
|||
_updateAccountProfile(accountProfile);
|
||||
}
|
||||
|
||||
if (saasUser != null) {
|
||||
_updateSaasUser(saasUser);
|
||||
await _verifyOrReportAuthDevice(saasUser);
|
||||
final auth = await authenticate();
|
||||
if (auth == null) {
|
||||
return false;
|
||||
final credentials = account.credentials;
|
||||
if (credentials.isNotEmpty) {
|
||||
_restoreCredentials(credentials);
|
||||
}
|
||||
|
||||
if (guruUser != null) {
|
||||
await _updateGuruUser(guruUser);
|
||||
await _verifyOrReportAuthDevice(guruUser);
|
||||
await authenticateFirebase();
|
||||
if (accountProfile != null) {
|
||||
await _checkOrUploadAccountProfile(accountProfile);
|
||||
}
|
||||
if (anonymousAuth != null) {
|
||||
final anonymousCredential = anonymousAuth.credential;
|
||||
if (anonymousCredential != null) {
|
||||
_bindCredential(anonymousCredential);
|
||||
return await _invokeAnonymousLogin(anonymousAuth.user, anonymousCredential);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<AccountAuth?> authenticate() async {
|
||||
final saasUser = accountDataStore.user;
|
||||
if (saasUser == null) {
|
||||
return null;
|
||||
}
|
||||
Future switchUser(GuruUser newUser) async {
|
||||
/// 更新 login 的用户信息
|
||||
_updateGuruUser(newUser);
|
||||
try {
|
||||
final auth = await _authenticate(saasUser);
|
||||
final newSaasUser = auth.user;
|
||||
if (newSaasUser != null && !saasUser.isSame(newSaasUser)) {
|
||||
_updateSaasUser(newSaasUser);
|
||||
}
|
||||
if (auth.firebaseUser != null) {
|
||||
_updateFirebaseUser(auth.firebaseUser!);
|
||||
Log.i("_updateFirebaseUser success!", tag: "Account");
|
||||
}
|
||||
return auth;
|
||||
await _verifyOrReportAuthDevice(newUser);
|
||||
// 登陆 firebase 不需要同步等待
|
||||
authenticateFirebase();
|
||||
} catch (error, stacktrace) {
|
||||
GuruAnalytics.instance.logException(error, stacktrace: stacktrace);
|
||||
Log.w("loginWithCredential error:$error, $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<DeviceTrack> _buildDevice(SaasUser saasUser) async {
|
||||
Future<bool> _switchAccount(Credential credential) async {
|
||||
GuruUser? loginUser;
|
||||
GuruUser? logoutUser;
|
||||
|
||||
/// 这里只调用接口获取对应的新用户信息,还没有做对应的绑定操作
|
||||
try {
|
||||
loginUser = await _loginGuruWithCredential(credential);
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("loginWithCredential[${credential.authType}] error:$error, $stacktrace");
|
||||
return false;
|
||||
}
|
||||
if (loginUser.isSame(accountDataStore.user)) {
|
||||
Log.w("loginWithCredential same user!", tag: "Account");
|
||||
_bindCredential(credential);
|
||||
return false;
|
||||
}
|
||||
bool result = false;
|
||||
|
||||
/// logout 内部进行了拦截,因此这里总是会返回一个 logoutUser
|
||||
/// logout传入 switch参数,表示是一个切换帐号,不需要真正的登出,
|
||||
/// 因为在下面的 SwitchAccount方法中会完成后续的过程
|
||||
logoutUser = await logout(switching: true);
|
||||
|
||||
/// 如果这里没有返回出对应的退出用户,将认为退出失败
|
||||
/// 因为进到 switchAccount 里肯定是非匿名登陆的帐号做登出操作
|
||||
if (logoutUser != null) {
|
||||
result = await GuruApp.instance.switchAccount(loginUser, credential, oldUser: logoutUser);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> _processConflict(Credential credential) async {
|
||||
final historicalSocialAuths = await AppProperty.getInstance().getHistoricalSocialAuths();
|
||||
|
||||
/// 如果是匿名登录,并且在这个设备上同样的用户没有绑定过其它的三方登陆凭证
|
||||
/// 这种情况下,认定为新用户,中台会静默解决冲突,并且对应的数据库不会发生迁移
|
||||
if (accountDataStore.isAnonymous && historicalSocialAuths.isEmpty) {
|
||||
Log.d("associate conflict: _loginGuruWithCredential!");
|
||||
final user = accountDataStore.user;
|
||||
final oldUid = user?.uid ?? "";
|
||||
if (user != null) {
|
||||
await _invokeAnonymousLogout(user);
|
||||
}
|
||||
|
||||
/// 因为这里是匿名登陆,因此在冲突的时候通过静默的方法切换账户
|
||||
final guruUser = await _loginGuruWithCredential(credential);
|
||||
|
||||
/// 由于是冲突处理,此时的匿名帐号已经和当前新登陆的用户不能配对
|
||||
/// 因此在新用户登陆成功后,这里需要将匿名帐户的凭证解绑,并清除匿名的密钥
|
||||
/// 这样做的目的是为了在该帐号退出时,判断匿名帐号是否存在,
|
||||
/// 如果不存在会创建一个新的匿名帐号,确保数据不被污染
|
||||
await _unbindCredential(AuthType.anonymous);
|
||||
|
||||
/// 将新的用户进行关联,此时当前设备上只有一个登陆凭证
|
||||
await processLogin(guruUser, credential);
|
||||
|
||||
GuruAnalytics.instance.logGuruEvent("switch_account", {
|
||||
"auth": getAuthName(credential.authType),
|
||||
"old_uid": oldUid,
|
||||
"new_uid": guruUser.uid,
|
||||
"silent": true
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
final canSwitch = await _invokeConflict();
|
||||
if (canSwitch) {
|
||||
return await _switchAccount(credential);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<DeviceTrack> _buildDevice(GuruUser saasUser) async {
|
||||
final DeviceInfo? deviceInfo = await AppProperty.getInstance().getAccountDevice();
|
||||
final firebasePushToken = await RemoteMessagingManager.instance.getToken();
|
||||
|
||||
|
|
@ -73,13 +148,39 @@ extension AccountServiceExtension on AccountManager {
|
|||
return DeviceTrack(null, deviceInfo);
|
||||
}
|
||||
|
||||
Future<SaasUser?> signInWithAnonymous() async {
|
||||
final anonymousSecretKey = await AppProperty.getInstance().getAnonymousSecretKey();
|
||||
return GuruApi.instance.signInWithAnonymous(secret: anonymousSecretKey);
|
||||
Future<AccountAuth?> _retrieveAnonymous() async {
|
||||
final result = await AuthCredentialManager.instance.loginWith(AuthType.anonymous);
|
||||
final credential = result.credential;
|
||||
if (!result.isSuccess || credential == null) {
|
||||
Log.w("_retrieveAnonymous error!", tag: "Account");
|
||||
return null;
|
||||
}
|
||||
final user = await _requestGuruUser(credential);
|
||||
return AccountAuth(user, credential: credential);
|
||||
}
|
||||
|
||||
Future _verifyOrReportAuthDevice(SaasUser saasUser) async {
|
||||
final deviceTrack = await _buildDevice(saasUser);
|
||||
Future<GuruUser> _loginGuruWithCredential(Credential credential) async {
|
||||
return await GuruApi.instance.loginGuruWithCredential(credential: credential);
|
||||
}
|
||||
|
||||
Future<GuruUser> _associateCredential(Credential credential) async {
|
||||
return await GuruApi.instance.associateCredential(credential: credential);
|
||||
}
|
||||
|
||||
Future<GuruUser> _requestGuruUser(Credential credential) async {
|
||||
//MetaData是匿名请求,或者当前没有任何 GuruUser Id,走signIn接口
|
||||
if (!accountDataStore.hasUid || credential.isAnonymous) {
|
||||
Log.d("_loginGuruWithCredential!", tag: "Account");
|
||||
return await _loginGuruWithCredential(credential);
|
||||
} else {
|
||||
Log.d("_associateCredential!");
|
||||
//当前有 GuruUser id,并且MetaData是三方登录Token,走associate接口(不管已有的SaasUser是不是三方登录)
|
||||
return await _associateCredential(credential);
|
||||
}
|
||||
}
|
||||
|
||||
Future _verifyOrReportAuthDevice(GuruUser guruUser) async {
|
||||
final deviceTrack = await _buildDevice(guruUser);
|
||||
final latestReportDeviceTimestamp =
|
||||
await AppProperty.getInstance().getLatestReportDeviceTimestamp();
|
||||
final elapsedInterval = DateTimeUtils.currentTimeInMillis() - latestReportDeviceTimestamp;
|
||||
|
|
@ -89,7 +190,7 @@ extension AccountServiceExtension on AccountManager {
|
|||
if (deviceId.isNotEmpty) {
|
||||
GuruAnalytics.instance.setDeviceId(deviceId);
|
||||
}
|
||||
if (isChanged && reportDevice?.isValid == true && saasUser.isValid == true) {
|
||||
if (isChanged && reportDevice?.isValid == true && guruUser.isValid == true) {
|
||||
final result = await GuruApi.instance.reportDevice(reportDevice!).then((_) {
|
||||
return true;
|
||||
}).catchError((error) {
|
||||
|
|
@ -135,10 +236,32 @@ extension AccountServiceExtension on AccountManager {
|
|||
accountDataStore.updateDeviceInfo(device);
|
||||
}
|
||||
|
||||
void _updateSaasUser(SaasUser saasUser) {
|
||||
accountDataStore.updateSaasUser(saasUser);
|
||||
AppProperty.getInstance().setAccountSaasUser(saasUser);
|
||||
GuruAnalytics.instance.setUserId(saasUser.uid);
|
||||
Future _bindCredential(Credential credential) async {
|
||||
accountDataStore.bindCredential(credential);
|
||||
|
||||
/// 这里匿名帐号是不会保存凭证的,因为匿名帐号的登陆凭证是自生成的
|
||||
if (credential.authType != AuthType.anonymous) {
|
||||
await AppProperty.getInstance().saveCredential(credential);
|
||||
}
|
||||
}
|
||||
|
||||
Future _unbindCredential(AuthType authType) async {
|
||||
accountDataStore.unbindCredential(authType);
|
||||
if (authType != AuthType.anonymous) {
|
||||
await AppProperty.getInstance().deleteCredential(authType);
|
||||
} else {
|
||||
await AppProperty.getInstance().clearAnonymousSecretKey();
|
||||
}
|
||||
}
|
||||
|
||||
void _restoreCredentials(Map<AuthType, Credential> credentials) {
|
||||
accountDataStore.updateCredentials(credentials);
|
||||
}
|
||||
|
||||
Future _updateGuruUser(GuruUser guruUser) async {
|
||||
accountDataStore.updateGuruUser(guruUser);
|
||||
await AppProperty.getInstance().setAccountGuruUser(guruUser);
|
||||
await GuruAnalytics.instance.setUserId(guruUser.uid);
|
||||
}
|
||||
|
||||
void _updateFirebaseUser(User user) {
|
||||
|
|
|
|||
|
|
@ -1,40 +1,108 @@
|
|||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:guru_app/account/account_manager.dart';
|
||||
import 'package:guru_app/account/model/account_profile.dart';
|
||||
import 'package:guru_app/account/model/user.dart';
|
||||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||||
import 'package:guru_utils/device/device_info.dart';
|
||||
|
||||
import 'package:guru_utils/property/app_property.dart';
|
||||
|
||||
/// Created by Haoyi on 6/3/21
|
||||
|
||||
class Account {
|
||||
final SaasUser? saasUser;
|
||||
final GuruUser? guruUser;
|
||||
final DeviceInfo? device;
|
||||
final AccountProfile? accountProfile;
|
||||
final User? firebaseUser;
|
||||
final Map<AuthType, Credential> credentials; // facebook, google, apple, anonymous
|
||||
|
||||
String? get uid => saasUser?.uid;
|
||||
@Deprecated("use guruUser instead")
|
||||
SaasUser? get saasUser => guruUser;
|
||||
|
||||
String? get uid => guruUser?.uid;
|
||||
|
||||
String? get nickname => accountProfile?.nickname;
|
||||
|
||||
Account.restore({this.saasUser, this.device, this.accountProfile, this.firebaseUser});
|
||||
Account.restore(
|
||||
{this.guruUser,
|
||||
this.device,
|
||||
this.accountProfile,
|
||||
this.firebaseUser,
|
||||
this.credentials = const {}});
|
||||
}
|
||||
|
||||
class AccountAuth {
|
||||
final SaasUser? user;
|
||||
final User? firebaseUser;
|
||||
final GuruUser user;
|
||||
final Credential? credential;
|
||||
|
||||
AccountAuth(this.user, this.firebaseUser);
|
||||
AccountAuth(this.user, {this.credential});
|
||||
|
||||
bool get isValid => uid != null && uid != "";
|
||||
|
||||
String? get saasToken => user?.token;
|
||||
String? get saasToken => user.token;
|
||||
|
||||
String? get uid => user?.uid;
|
||||
String? get uid => user.uid;
|
||||
|
||||
// bool get existsFirebaseUser => firebaseUser != null;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AccountAuth{user: $user}';
|
||||
}
|
||||
}
|
||||
|
||||
class FirebaseAccountAuth {
|
||||
final GuruUser user;
|
||||
final User? firebaseUser;
|
||||
|
||||
FirebaseAccountAuth(this.user, {this.firebaseUser});
|
||||
|
||||
bool get isValid => uid != null && uid != "";
|
||||
|
||||
String? get guruToken => user.token;
|
||||
|
||||
String? get uid => user.uid;
|
||||
|
||||
bool get existsFirebaseUser => firebaseUser != null;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AccountAuth{user: $user, firebaseUser: $firebaseUser}';
|
||||
return 'AccountAuth{user: $user}';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class IAccountAuthDelegate {
|
||||
/// 支持登陆的代理,这块不管返回不返回都会支持匿名登陆
|
||||
List<AuthCredentialDelegate> get supportedAuthCredentialDelegates =>
|
||||
AccountManager.defaultSupportedAuthCredentialDelegates;
|
||||
|
||||
/// 返回设备共享的用户属性,在帐号切换的时候会保证这些 KEY,会保留下来
|
||||
/// 注意,这里尽量不要把用户相关的属性设到这里面,否则会出现不必要的问题
|
||||
Set<PropertyKey> get deviceSharedProperties => {};
|
||||
|
||||
/// 这个方法调用时,中台会确保当前的数据系统是切换后的数据系统
|
||||
/// 因此可以放心使用模板的数据,当 processor 返回后,对应的数据系统将会被切换
|
||||
/// 因此确保 processor 返回后,数据系统已经切换到新用户的数据系统
|
||||
Future<bool> onLogin(GuruUser loginUser, Credential credential);
|
||||
|
||||
/// 这个方法调用时,中台会确保当前的数据系统是切换前的数据系统
|
||||
/// 因此可以放心使用模板的数据,当 processor返回后,对应的数据系统将会被切换
|
||||
/// 因此确保 processor 返回前,数据系统已经完成老用户数据的迁移
|
||||
Future<bool> onLogout(GuruUser logoutUser);
|
||||
|
||||
/// 当出现登陆冲突时,有可能当前是匿名帐号,而这时,中台模板会静默登入到新的帐号中
|
||||
/// 与此同时,中台会调用 onAnonymousLogout
|
||||
Future<bool> onAnonymousLogout(GuruUser logoutUser) async {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 当 APP首次匿名登陆时,或当登陆一个没有绑定匿名Credential的帐号时,会在登出时重新登陆一个新的匿名帐号
|
||||
/// 中台在这种情况下会调用 onAnonymousLogin
|
||||
Future<bool> onAnonymousLogin(GuruUser loginUser, Credential credential) async {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 当登陆时,有可能会出现帐号冲突,因此针对冲突可以选择切换帐号或者忽略
|
||||
/// 如果你选择切换帐号,那么你需要提供 onLogout 方法,用于处理老用户的数据迁移
|
||||
/// 返回值为是否继续
|
||||
Future<bool> onConflict();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||||
|
||||
class AnonymousCredential extends Credential {
|
||||
final String secretKey;
|
||||
|
||||
AnonymousCredential(this.secretKey);
|
||||
|
||||
@override
|
||||
String get token => secretKey;
|
||||
|
||||
@override
|
||||
AuthType get authType => AuthType.anonymous;
|
||||
|
||||
String toJson() => secretKey;
|
||||
}
|
||||
|
||||
class AnonymousCredentialDelegate extends AuthCredentialDelegate {
|
||||
const AnonymousCredentialDelegate();
|
||||
|
||||
@override
|
||||
AuthType get authType => AuthType.anonymous;
|
||||
|
||||
@override
|
||||
Future<AuthResult> login() async {
|
||||
final secretKey = await AppProperty.getInstance().getAnonymousSecretKey();
|
||||
return AuthResult.success(AnonymousCredential(secretKey));
|
||||
}
|
||||
|
||||
@override
|
||||
Future logout() async {}
|
||||
|
||||
@override
|
||||
Credential deserializeCredential(String data) {
|
||||
return AnonymousCredential(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,11 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
|
||||
part 'user.g.dart';
|
||||
|
||||
@Deprecated("Use Guru User instead")
|
||||
typedef SaasUser = GuruUser;
|
||||
|
||||
@JsonSerializable()
|
||||
class SaasUser {
|
||||
class GuruUser {
|
||||
@JsonKey(name: 'uid', defaultValue: "")
|
||||
final String uid;
|
||||
|
||||
|
|
@ -23,22 +26,22 @@ class SaasUser {
|
|||
bool get isValid =>
|
||||
(uid != "") && (token.isNotEmpty == true) && (firebaseToken.isNotEmpty == true);
|
||||
|
||||
SaasUser(
|
||||
GuruUser(
|
||||
{required this.uid,
|
||||
required this.token,
|
||||
required this.firebaseToken,
|
||||
this.createAtTimestamp = 0});
|
||||
|
||||
factory SaasUser.fromJson(Map<String, dynamic> json) => _$SaasUserFromJson(json);
|
||||
factory GuruUser.fromJson(Map<String, dynamic> json) => _$GuruUserFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SaasUserToJson(this);
|
||||
Map<String, dynamic> toJson() => _$GuruUserToJson(this);
|
||||
|
||||
SaasUser copyWith({String? firebaseToken, String? token}) {
|
||||
return SaasUser(
|
||||
GuruUser copyWith({String? firebaseToken, String? token}) {
|
||||
return GuruUser(
|
||||
uid: uid, token: token ?? this.token, firebaseToken: firebaseToken ?? this.firebaseToken);
|
||||
}
|
||||
|
||||
bool isSame(SaasUser? user) {
|
||||
bool isSame(GuruUser? user) {
|
||||
return uid == user?.uid &&
|
||||
token == user?.token &&
|
||||
firebaseToken == user?.firebaseToken &&
|
||||
|
|
@ -70,6 +73,63 @@ class AnonymousLoginReqBody {
|
|||
Map<String, dynamic> toJson() => _$AnonymousLoginReqBodyToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class FacebookLoginReqBody {
|
||||
@JsonKey(name: 'accessToken', defaultValue: "")
|
||||
final String? accessToken;
|
||||
|
||||
FacebookLoginReqBody({this.accessToken});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FacebookLoginReqBody{accessToken: $accessToken}';
|
||||
}
|
||||
|
||||
factory FacebookLoginReqBody.fromJson(Map<String, dynamic> json) =>
|
||||
_$FacebookLoginReqBodyFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FacebookLoginReqBodyToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class GoogleLoginReqBody {
|
||||
@JsonKey(name: 'idToken', defaultValue: "")
|
||||
final String? idToken;
|
||||
|
||||
GoogleLoginReqBody({this.idToken});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GoogleLoginReqBody{idToken: $idToken}';
|
||||
}
|
||||
|
||||
factory GoogleLoginReqBody.fromJson(Map<String, dynamic> json) =>
|
||||
_$GoogleLoginReqBodyFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$GoogleLoginReqBodyToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AppleLoginReqBody {
|
||||
@JsonKey(name: 'token', defaultValue: "")
|
||||
final String? token;
|
||||
|
||||
@JsonKey(name: 'clientType', defaultValue: "ios")
|
||||
final String clientType;
|
||||
|
||||
AppleLoginReqBody({this.token, this.clientType = "ios"});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppleLoginReqBody{token: $token, clientType: $clientType}';
|
||||
}
|
||||
|
||||
factory AppleLoginReqBody.fromJson(Map<String, dynamic> json) =>
|
||||
_$AppleLoginReqBodyFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AppleLoginReqBodyToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class FirebaseTokenData {
|
||||
@JsonKey(name: 'uid', defaultValue: "")
|
||||
|
|
@ -78,7 +138,7 @@ class FirebaseTokenData {
|
|||
@JsonKey(name: 'firebaseToken', defaultValue: "")
|
||||
final String firebaseToken;
|
||||
|
||||
FirebaseTokenData({required this.uid, required this.firebaseToken});
|
||||
FirebaseTokenData({this.uid = "", this.firebaseToken = ""});
|
||||
|
||||
factory FirebaseTokenData.fromJson(Map<String, dynamic> json) =>
|
||||
_$FirebaseTokenDataFromJson(json);
|
||||
|
|
@ -91,6 +151,42 @@ class FirebaseTokenData {
|
|||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class UserAuthInfo {
|
||||
@JsonKey(name: "secret", defaultValue: "")
|
||||
final String secret;
|
||||
|
||||
@JsonKey(name: 'providerList', defaultValue: const <String>[])
|
||||
final List<String> providerList;
|
||||
|
||||
factory UserAuthInfo.fromJson(Map<String, dynamic> json) => _$UserAuthInfoFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserAuthInfoToJson(this);
|
||||
|
||||
UserAuthInfo({this.secret = "", this.providerList = const []});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserAuthList{providerList: $providerList}';
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class UnbindReqBody {
|
||||
@JsonKey(name: 'provider', defaultValue: "")
|
||||
final String provider;
|
||||
|
||||
UnbindReqBody({this.provider = ""});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UnbindReqBody{provider: $provider}';
|
||||
}
|
||||
|
||||
factory UnbindReqBody.fromJson(Map<String, dynamic> json) => _$UnbindReqBodyFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$UnbindReqBodyToJson(this);
|
||||
}
|
||||
|
||||
class UserAttr {
|
||||
static const real = 0;
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ part of 'user.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SaasUser _$SaasUserFromJson(Map<String, dynamic> json) => SaasUser(
|
||||
GuruUser _$GuruUserFromJson(Map<String, dynamic> json) => GuruUser(
|
||||
uid: json['uid'] as String? ?? '',
|
||||
token: json['token'] as String? ?? '',
|
||||
firebaseToken: json['firebaseToken'] as String? ?? '',
|
||||
createAtTimestamp: json['createdAtTimestamp'] as int? ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SaasUserToJson(SaasUser instance) => <String, dynamic>{
|
||||
Map<String, dynamic> _$GuruUserToJson(GuruUser instance) => <String, dynamic>{
|
||||
'uid': instance.uid,
|
||||
'token': instance.token,
|
||||
'firebaseToken': instance.firebaseToken,
|
||||
|
|
@ -32,6 +32,40 @@ Map<String, dynamic> _$AnonymousLoginReqBodyToJson(
|
|||
'secret': instance.secret,
|
||||
};
|
||||
|
||||
FacebookLoginReqBody _$FacebookLoginReqBodyFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
FacebookLoginReqBody(
|
||||
accessToken: json['accessToken'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FacebookLoginReqBodyToJson(
|
||||
FacebookLoginReqBody instance) =>
|
||||
<String, dynamic>{
|
||||
'accessToken': instance.accessToken,
|
||||
};
|
||||
|
||||
GoogleLoginReqBody _$GoogleLoginReqBodyFromJson(Map<String, dynamic> json) =>
|
||||
GoogleLoginReqBody(
|
||||
idToken: json['idToken'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GoogleLoginReqBodyToJson(GoogleLoginReqBody instance) =>
|
||||
<String, dynamic>{
|
||||
'idToken': instance.idToken,
|
||||
};
|
||||
|
||||
AppleLoginReqBody _$AppleLoginReqBodyFromJson(Map<String, dynamic> json) =>
|
||||
AppleLoginReqBody(
|
||||
token: json['token'] as String? ?? '',
|
||||
clientType: json['clientType'] as String? ?? 'ios',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AppleLoginReqBodyToJson(AppleLoginReqBody instance) =>
|
||||
<String, dynamic>{
|
||||
'token': instance.token,
|
||||
'clientType': instance.clientType,
|
||||
};
|
||||
|
||||
FirebaseTokenData _$FirebaseTokenDataFromJson(Map<String, dynamic> json) =>
|
||||
FirebaseTokenData(
|
||||
uid: json['uid'] as String? ?? '',
|
||||
|
|
@ -43,3 +77,27 @@ Map<String, dynamic> _$FirebaseTokenDataToJson(FirebaseTokenData instance) =>
|
|||
'uid': instance.uid,
|
||||
'firebaseToken': instance.firebaseToken,
|
||||
};
|
||||
|
||||
UserAuthInfo _$UserAuthInfoFromJson(Map<String, dynamic> json) => UserAuthInfo(
|
||||
secret: json['secret'] as String? ?? '',
|
||||
providerList: (json['providerList'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserAuthInfoToJson(UserAuthInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'secret': instance.secret,
|
||||
'providerList': instance.providerList,
|
||||
};
|
||||
|
||||
UnbindReqBody _$UnbindReqBodyFromJson(Map<String, dynamic> json) =>
|
||||
UnbindReqBody(
|
||||
provider: json['provider'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UnbindReqBodyToJson(UnbindReqBody instance) =>
|
||||
<String, dynamic>{
|
||||
'provider': instance.provider,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -560,18 +560,26 @@ class AdsManager extends AdsManagerDelegate {
|
|||
return ad;
|
||||
}
|
||||
|
||||
Future<int> requestGdpr({int? debugGeography, String? testDeviceId}) {
|
||||
Future<int> requestGdpr({int? debugGeography, String? testDeviceId}) async {
|
||||
Log.d("requestGdpr! debugGeography:$debugGeography testDeviceId:$testDeviceId", tag: "Ads");
|
||||
// adb logcat -s UserMessagingPlatform
|
||||
// Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("xxxx") to set this as a debug device.
|
||||
return GuruApplovinFlutter.instance
|
||||
final result = await GuruApplovinFlutter.instance
|
||||
.requestGdpr(debugGeography: debugGeography, testDeviceId: testDeviceId);
|
||||
final consentResult = await GuruAnalytics.instance.refreshConsents();
|
||||
Log.d("requestGdpr result:$result consentResult:$consentResult");
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> resetGdpr() {
|
||||
return GuruApplovinFlutter.instance.resetGdpr();
|
||||
}
|
||||
|
||||
Future<bool> updateOrientation(int orientation) async {
|
||||
final result = await GuruApplovinFlutter.instance.updateOrientation(orientation);
|
||||
return result == true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApplovinBannerAds> createBannerAds({String? scene, AdsLifecycleObserver? observer}) async {
|
||||
final _adsProfile = adsProfile;
|
||||
|
|
@ -583,21 +591,14 @@ class AdsManager extends AdsManagerDelegate {
|
|||
if (isPurchasedNoAd) {
|
||||
return AdCause.noAds;
|
||||
}
|
||||
final _adsProfile = adsProfile;
|
||||
Ads? ad = interstitialAds[_adsProfile.interstitialId];
|
||||
int hiddenAt = 0;
|
||||
if (ad is AdsAudit) {
|
||||
hiddenAt = ad.latestHiddenAt;
|
||||
}
|
||||
|
||||
final hiddenAt = AdsManager.instance.latestFullscreenAdsHiddenTimestamps;
|
||||
final now = DateTimeUtils.currentTimeInMillis();
|
||||
final impGapInMillis =
|
||||
AdsManager.instance.adsConfig.interstitialConfig.getSceneImpGapInSeconds(scene) * 1000;
|
||||
Log.d(
|
||||
"canShowInterstitial($scene): now:$now latestFullscreenAdsHiddenTimestamps:$latestFullscreenAdsHiddenTimestamps hiddenAt:$hiddenAt impGapInMillis:$impGapInMillis",
|
||||
tag: "Ads");
|
||||
if ((now - AdsManager.instance.latestFullscreenAdsHiddenTimestamps) < 60000 ||
|
||||
((now - hiddenAt) < impGapInMillis)) {
|
||||
if ((now - hiddenAt) < impGapInMillis) {
|
||||
Log.d("show ads too frequency", syncFirebase: true);
|
||||
return AdCause.tooFrequent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ class AdInterstitialConfig {
|
|||
@joinedStringConvert
|
||||
final List<String> scenes;
|
||||
|
||||
@JsonKey(name: "sp_scene", defaultValue: {"new_block": 120, "reset_scs": 120})
|
||||
@JsonKey(name: "sp_scene", defaultValue: {})
|
||||
@configStringIntMapStringConvert
|
||||
final Map<String, int> specialScenes;
|
||||
|
||||
|
|
@ -329,8 +329,8 @@ class AdInterstitialConfig {
|
|||
@JsonKey(name: "amazon_enable", defaultValue: false)
|
||||
final bool amazonEnable;
|
||||
|
||||
@JsonKey(name: "imp_gap_s", defaultValue: 120)
|
||||
final int impGapInSeconds;
|
||||
@JsonKey(name: "imp_gap_s")
|
||||
final int? impGapInSeconds;
|
||||
|
||||
AdInterstitialConfig(this.freeInSecond, this.validation, this.scenes, this.retryMinTimeInSecond,
|
||||
this.retryMaxTimeInSecond,
|
||||
|
|
@ -346,7 +346,10 @@ class AdInterstitialConfig {
|
|||
}
|
||||
|
||||
int getSceneImpGapInSeconds(String scene) {
|
||||
return specialScenes[scene] ?? impGapInSeconds;
|
||||
return (specialScenes[scene] ??
|
||||
impGapInSeconds ??
|
||||
GuruApp.instance.appSpec.deployment.fullscreenAdsMinInterval)
|
||||
.clamp(5, 600);
|
||||
}
|
||||
|
||||
Future<bool> checkFreeTime() async {
|
||||
|
|
|
|||
|
|
@ -132,10 +132,10 @@ AdInterstitialConfig _$AdInterstitialConfigFromJson(
|
|||
json['retry_max_s'] as int? ?? 600,
|
||||
amazonEnable: json['amazon_enable'] as bool? ?? false,
|
||||
specialScenes: json['sp_scene'] == null
|
||||
? {'new_block': 120, 'reset_scs': 120}
|
||||
? {}
|
||||
: configStringIntMapStringConvert
|
||||
.fromJson(json['sp_scene'] as String),
|
||||
impGapInSeconds: json['imp_gap_s'] as int? ?? 120,
|
||||
impGapInSeconds: json['imp_gap_s'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AdInterstitialConfigToJson(
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import 'dart:io';
|
|||
|
||||
import 'package:guru_app/analytics/guru_analytics.dart';
|
||||
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_app/property/property_keys.dart';
|
||||
import 'package:guru_utils/collection/collectionutils.dart';
|
||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||
import 'package:guru_utils/log/log.dart';
|
||||
import 'package:guru_applovin_flutter/ad_impression.dart';
|
||||
|
||||
|
|
@ -65,26 +67,7 @@ class AdImpressionController {
|
|||
}
|
||||
final payloadMap = json.decode(payload);
|
||||
ImpressionData impressionData = ImpressionData.fromJson(payloadMap);
|
||||
// 判断是不是facebook的network
|
||||
// if (impressionData.networkName == "undisclosed") {
|
||||
// final calibrationCpm = await facebookCalibrator.getCpm(impressionData.unitFormat, impressionData.country);
|
||||
// if (calibrationCpm > 0) {
|
||||
// final newImpressionData = impressionData.derive(newPublisherRevenue: calibrationCpm);
|
||||
// if (facebookCalibrator.config?.fbIrldReport == true) {
|
||||
// AnalyticsUtils.logEventEx("tch_fb_ad_rev", value: calibrationCpm, parameters: {
|
||||
// FirebaseEventsParams.AD_FORMAT: impressionData.unitFormat,
|
||||
// FirebaseEventsParams.AD_UNIT_NAME: impressionData.unitName,
|
||||
// FirebaseEventsParams.CURRENCY: impressionData.currency,
|
||||
// "country": impressionData.country,
|
||||
// "mopub_rev": impressionData.publisherRevenue
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// impressionData = newImpressionData;
|
||||
// }
|
||||
// }
|
||||
await refreshLtv(impressionData);
|
||||
// _reportAdImpression(arguments);
|
||||
|
||||
final jsonPayload = jsonEncode(impressionData.payload);
|
||||
latestImpressionPayload = jsonPayload;
|
||||
|
|
@ -119,12 +102,13 @@ class AdImpressionController {
|
|||
final currency = impressionData.currency;
|
||||
if (revenue != -1) {
|
||||
_logAdRevenue(impressionData);
|
||||
// if ()
|
||||
// _logAdLtv(revenue: revenue, adPlatform: adPlatform, currency: currency);
|
||||
}
|
||||
Log.d("refreshLtv payload:${impressionData.payload}");
|
||||
}
|
||||
|
||||
// _logAdLtv({double revenue = 0.0, String adPlatform = "MAX", String currency = ""}) async {
|
||||
// Future _logAdLtv({double revenue = 0.0, String adPlatform = "MAX", String currency = ""}) async {
|
||||
// final nowDate = DateTimeUtils.yyyyMMddUtcNum;
|
||||
// final appProperty = AppProperty.getInstance();
|
||||
// final totalLtv = await appProperty.getDouble(PropertyKeys.totalLtv, defValue: 0.0);
|
||||
|
|
@ -179,11 +163,25 @@ class AdImpressionController {
|
|||
totalRevenue += data.publisherRevenue;
|
||||
if (totalRevenue >= 0.01) {
|
||||
GuruAnalytics.instance.logAdRevenue(totalRevenue, data.platform, data.currency);
|
||||
if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 1) {
|
||||
GuruAnalytics.instance.logPurchase(totalRevenue,
|
||||
currency: data.currency, contentId: "MAX", adPlatform: "MAX");
|
||||
|
||||
}
|
||||
totalRevenue = .0;
|
||||
}
|
||||
appProperty.setDouble(PropertyKeys.totalRevenue, totalRevenue);
|
||||
|
||||
double totalRevenue020 =
|
||||
await appProperty.getDouble(PropertyKeys.totalRevenue020, defValue: 0.0);
|
||||
totalRevenue020 += data.publisherRevenue;
|
||||
if (totalRevenue020 >= 0.2) {
|
||||
GuruAnalytics.instance.logAdRevenue020(totalRevenue020, data.platform, data.currency);
|
||||
if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 2) {
|
||||
GuruAnalytics.instance.logPurchase(totalRevenue020,
|
||||
currency: data.currency, contentId: "MAX", adPlatform: "MAX");
|
||||
}
|
||||
totalRevenue020 = .0;
|
||||
}
|
||||
appProperty.setDouble(PropertyKeys.totalRevenue020, totalRevenue020);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,388 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:guru_app/account/account_data_store.dart';
|
||||
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
|
||||
import 'package:guru_app/property/settings/guru_settings.dart';
|
||||
import 'package:guru_utils/device/device_utils.dart';
|
||||
import 'package:guru_utils/extensions/extensions.dart';
|
||||
import 'package:guru_utils/log/log.dart';
|
||||
import 'package:guru_utils/random/random_utils.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'abtest_model.g.dart';
|
||||
|
||||
class ConditionOpt {
|
||||
static const equals = "eq";
|
||||
static const greaterThan = "gt";
|
||||
static const greaterThanOrEquals = "gte";
|
||||
static const lessThan = "lt";
|
||||
static const lessThanOrEquals = "lte";
|
||||
static const notEquals = "ne";
|
||||
|
||||
static bool evaluate<T extends Comparable>(T value, T target, String opt) {
|
||||
final result = value.compareTo(target);
|
||||
switch (opt) {
|
||||
case ConditionOpt.equals:
|
||||
return result == 0;
|
||||
case ConditionOpt.greaterThan:
|
||||
return result > 0;
|
||||
case ConditionOpt.greaterThanOrEquals:
|
||||
return result >= 0;
|
||||
case ConditionOpt.lessThan:
|
||||
return result < 0;
|
||||
case ConditionOpt.lessThanOrEquals:
|
||||
return result <= 0;
|
||||
case ConditionOpt.notEquals:
|
||||
return result != 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ABTestFilter {
|
||||
static const platform = 1;
|
||||
static const version = 2;
|
||||
static const country = 3;
|
||||
static const newUser = 4;
|
||||
|
||||
final int type;
|
||||
|
||||
const ABTestFilter(this.type);
|
||||
|
||||
bool filter();
|
||||
|
||||
factory ABTestFilter.fromJson(Map<String, dynamic> json) {
|
||||
final type = json["type"] = json["type"] ?? 0;
|
||||
switch (type) {
|
||||
case ABTestFilter.platform:
|
||||
return PlatformFilter.fromJson(json);
|
||||
case ABTestFilter.country:
|
||||
return CountryFilter.fromJson(json);
|
||||
case ABTestFilter.version:
|
||||
return VersionFilter.fromJson(json);
|
||||
case ABTestFilter.newUser:
|
||||
return NewUserFilter.fromJson(json);
|
||||
default:
|
||||
throw UnimplementedError("Unknown ABTestFilter type: $type");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => toJson()..addAll({"type": type});
|
||||
}
|
||||
|
||||
abstract class ABTestCondition {
|
||||
bool validate();
|
||||
}
|
||||
|
||||
/// 为了后面可以做一些定制,因此这里按平台进行区分
|
||||
@JsonSerializable()
|
||||
class AndroidCondition extends ABTestCondition {
|
||||
@JsonKey(name: "opt")
|
||||
final String? opt;
|
||||
|
||||
@JsonKey(name: "sdk")
|
||||
final int? sdkInt;
|
||||
|
||||
AndroidCondition({this.opt, this.sdkInt});
|
||||
|
||||
factory AndroidCondition.fromJson(Map<String, dynamic> json) => _$AndroidConditionFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AndroidConditionToJson(this);
|
||||
|
||||
@override
|
||||
bool validate() {
|
||||
final versionOpt = opt;
|
||||
final targetVersion = sdkInt;
|
||||
if (versionOpt != null && targetVersion != null) {
|
||||
final versionCode = DeviceUtils.peekOSVersion();
|
||||
// 操作系统版本号获取失败,直接返回false
|
||||
if (versionCode == -1) {
|
||||
return false;
|
||||
}
|
||||
if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 方便后面扩展其它字段
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class IosCondition extends ABTestCondition {
|
||||
@JsonKey(name: "opt")
|
||||
final String? opt;
|
||||
|
||||
@JsonKey(name: "ver")
|
||||
final int? version; // 这里只记录大版本号
|
||||
|
||||
IosCondition({this.opt, this.version});
|
||||
|
||||
factory IosCondition.fromJson(Map<String, dynamic> json) => _$IosConditionFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$IosConditionToJson(this);
|
||||
|
||||
@override
|
||||
bool validate() {
|
||||
final versionOpt = opt;
|
||||
final targetVersion = version;
|
||||
if (versionOpt != null && targetVersion != null) {
|
||||
final versionCode = DeviceUtils.peekOSVersion();
|
||||
// 操作系统版本号获取失败,直接返回false
|
||||
if (versionCode == -1) {
|
||||
return false;
|
||||
}
|
||||
if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 方便后面扩展其它字段
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PlatformFilter extends ABTestFilter {
|
||||
@JsonKey(name: "ac")
|
||||
final AndroidCondition? androidCondition;
|
||||
|
||||
@JsonKey(name: "ic")
|
||||
final IosCondition? iosCondition;
|
||||
|
||||
PlatformFilter({this.androidCondition, this.iosCondition}) : super(ABTestFilter.platform);
|
||||
|
||||
@override
|
||||
bool filter() {
|
||||
// 如果配了 Platform Filter, 如果指定平台没有 condition, 则默认为true
|
||||
if (Platform.isAndroid) {
|
||||
return androidCondition?.validate() != false;
|
||||
} else if (Platform.isIOS) {
|
||||
return iosCondition?.validate() != false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
factory PlatformFilter.fromJson(Map<String, dynamic> json) => _$PlatformFilterFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$PlatformFilterToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(constructor: "_")
|
||||
class VersionFilter extends ABTestFilter {
|
||||
@JsonKey(name: "opt")
|
||||
final String opt;
|
||||
|
||||
@JsonKey(name: "mmp")
|
||||
final String mmp; // major.minor.patch
|
||||
|
||||
VersionFilter._(this.opt, this.mmp) : super(ABTestFilter.version);
|
||||
|
||||
VersionFilter.equals(this.mmp)
|
||||
: opt = ConditionOpt.equals,
|
||||
super(ABTestFilter.version);
|
||||
|
||||
VersionFilter.greaterThan(this.mmp)
|
||||
: opt = ConditionOpt.greaterThan,
|
||||
super(ABTestFilter.version);
|
||||
|
||||
VersionFilter.greaterThanOrEquals(this.mmp)
|
||||
: opt = ConditionOpt.greaterThanOrEquals,
|
||||
super(ABTestFilter.version);
|
||||
|
||||
VersionFilter.lessThan(this.mmp)
|
||||
: opt = ConditionOpt.lessThan,
|
||||
super(ABTestFilter.version);
|
||||
|
||||
VersionFilter.lessThanOrEquals(this.mmp)
|
||||
: opt = ConditionOpt.lessThanOrEquals,
|
||||
super(ABTestFilter.version);
|
||||
|
||||
VersionFilter.notEquals(this.mmp)
|
||||
: opt = ConditionOpt.notEquals,
|
||||
super(ABTestFilter.version);
|
||||
|
||||
@override
|
||||
bool filter() {
|
||||
final version = GuruSettings.instance.version.get();
|
||||
Log.d("[$runtimeType] $version $opt $mmp");
|
||||
return ConditionOpt.evaluate<String>(version, mmp, opt);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VersionValidator{opt: $opt, mmp: $mmp}';
|
||||
}
|
||||
|
||||
factory VersionFilter.fromJson(Map<String, dynamic> json) => _$VersionFilterFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$VersionFilterToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(constructor: "_")
|
||||
class CountryFilter extends ABTestFilter {
|
||||
@JsonKey(name: "included", defaultValue: {})
|
||||
final Set<String> included;
|
||||
|
||||
@JsonKey(name: "excluded", defaultValue: {})
|
||||
final Set<String> excluded;
|
||||
|
||||
CountryFilter._(this.included, this.excluded) : super(ABTestFilter.country);
|
||||
|
||||
CountryFilter.included(this.included)
|
||||
: excluded = {},
|
||||
super(ABTestFilter.country);
|
||||
|
||||
CountryFilter.excluded(this.excluded)
|
||||
: included = {},
|
||||
super(ABTestFilter.country);
|
||||
|
||||
@override
|
||||
bool filter() {
|
||||
final String countryCode = Platform.localeName.split('_').safeLast?.toLowerCase() ?? "";
|
||||
Log.d("[$runtimeType] $countryCode included: $included excluded: $excluded");
|
||||
if (countryCode.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果excluded不为空,证明存在排除选项,该validate将只判断所有excluded中的逻辑,
|
||||
// 将不会在判断included中的逻辑
|
||||
if (excluded.isNotEmpty) {
|
||||
return !excluded.contains(countryCode);
|
||||
}
|
||||
|
||||
if (included.contains(countryCode)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
factory CountryFilter.fromJson(Map<String, dynamic> json) => _$CountryFilterFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$CountryFilterToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class NewUserFilter extends ABTestFilter {
|
||||
NewUserFilter() : super(ABTestFilter.newUser);
|
||||
|
||||
@override
|
||||
bool filter() {
|
||||
final version = GuruSettings.instance.version.get();
|
||||
final fiv = GuruSettings.instance.firstInstallVersion.get();
|
||||
return fiv.startsWith(version);
|
||||
}
|
||||
|
||||
factory NewUserFilter.fromJson(Map<String, dynamic> json) => _$NewUserFilterFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$NewUserFilterToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ABTestAudience {
|
||||
@JsonKey(name: "filters")
|
||||
final List<ABTestFilter> filters;
|
||||
|
||||
@JsonKey(name: "variant", defaultValue: 2)
|
||||
final int variant;
|
||||
|
||||
ABTestAudience({required this.filters, this.variant = 2});
|
||||
|
||||
factory ABTestAudience.fromJson(Map<String, dynamic> json) => _$ABTestAudienceFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ABTestAudienceToJson(this);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ABTestAudience{filters: $filters, variant: $variant}';
|
||||
}
|
||||
|
||||
bool validate() {
|
||||
for (var filter in filters) {
|
||||
if (!filter.filter()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ABTestExperiment {
|
||||
@JsonKey(name: "name")
|
||||
final String name;
|
||||
|
||||
@JsonKey(name: "start_ts", defaultValue: 0)
|
||||
final int startTs;
|
||||
|
||||
@JsonKey(name: "end_ts", defaultValue: 0)
|
||||
final int endTs;
|
||||
|
||||
@JsonKey(name: "audience")
|
||||
final ABTestAudience audience;
|
||||
|
||||
ABTestExperiment(
|
||||
{required String name, required this.startTs, required this.endTs, required this.audience})
|
||||
: name = _validExperimentName(name);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ABTestExperiment{name: $name, startTs: $startTs, endTs: $endTs, audience: $audience}';
|
||||
}
|
||||
|
||||
static String _validExperimentName(String experimentName) {
|
||||
if (experimentName.contains(RemoteConfigManager.invalidABKeyRegExp)) {
|
||||
Log.w("abName($experimentName) use invalid key! $experimentName! replace invalid char to _");
|
||||
experimentName = experimentName.replaceAll(RemoteConfigManager.invalidABKeyRegExp, "_");
|
||||
} else {
|
||||
if (experimentName.length > 20) {
|
||||
experimentName = experimentName.substring(0, 20);
|
||||
}
|
||||
}
|
||||
return experimentName;
|
||||
}
|
||||
|
||||
factory ABTestExperiment.fromJson(Map<String, dynamic> json) => _$ABTestExperimentFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ABTestExperimentToJson(this);
|
||||
|
||||
bool isExpired() {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
return now < startTs || now > endTs;
|
||||
}
|
||||
|
||||
bool isMatchAudience() {
|
||||
return audience.validate();
|
||||
}
|
||||
|
||||
@JsonKey(includeToJson: false)
|
||||
String? _variantName;
|
||||
|
||||
String get variantName =>
|
||||
(_variantName ??= _toVariantName(RandomUtils.nextInt(audience.variant)));
|
||||
|
||||
static const _originalVariant = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
String _toVariantName(int value) {
|
||||
String codes = "";
|
||||
int nv = value;
|
||||
while (true) {
|
||||
final nextNum = nv ~/ _originalVariant.length;
|
||||
if (nextNum <= 0) {
|
||||
break;
|
||||
}
|
||||
codes = "${_originalVariant[nv % _originalVariant.length]}$codes";
|
||||
nv = nextNum;
|
||||
}
|
||||
|
||||
final tailIndex = nv % _originalVariant.length;
|
||||
if (tailIndex >= 0) {
|
||||
codes = "${_originalVariant[tailIndex]}$codes";
|
||||
}
|
||||
return codes.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'abtest_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AndroidCondition _$AndroidConditionFromJson(Map<String, dynamic> json) =>
|
||||
AndroidCondition(
|
||||
opt: json['opt'] as String?,
|
||||
sdkInt: json['sdk'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AndroidConditionToJson(AndroidCondition instance) =>
|
||||
<String, dynamic>{
|
||||
'opt': instance.opt,
|
||||
'sdk': instance.sdkInt,
|
||||
};
|
||||
|
||||
IosCondition _$IosConditionFromJson(Map<String, dynamic> json) => IosCondition(
|
||||
opt: json['opt'] as String?,
|
||||
version: json['ver'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$IosConditionToJson(IosCondition instance) =>
|
||||
<String, dynamic>{
|
||||
'opt': instance.opt,
|
||||
'ver': instance.version,
|
||||
};
|
||||
|
||||
PlatformFilter _$PlatformFilterFromJson(Map<String, dynamic> json) =>
|
||||
PlatformFilter(
|
||||
androidCondition: json['ac'] == null
|
||||
? null
|
||||
: AndroidCondition.fromJson(json['ac'] as Map<String, dynamic>),
|
||||
iosCondition: json['ic'] == null
|
||||
? null
|
||||
: IosCondition.fromJson(json['ic'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlatformFilterToJson(PlatformFilter instance) =>
|
||||
<String, dynamic>{
|
||||
'ac': instance.androidCondition,
|
||||
'ic': instance.iosCondition,
|
||||
};
|
||||
|
||||
VersionFilter _$VersionFilterFromJson(Map<String, dynamic> json) =>
|
||||
VersionFilter._(
|
||||
json['opt'] as String,
|
||||
json['mmp'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$VersionFilterToJson(VersionFilter instance) =>
|
||||
<String, dynamic>{
|
||||
'opt': instance.opt,
|
||||
'mmp': instance.mmp,
|
||||
};
|
||||
|
||||
CountryFilter _$CountryFilterFromJson(Map<String, dynamic> json) =>
|
||||
CountryFilter._(
|
||||
(json['included'] as List<dynamic>?)?.map((e) => e as String).toSet() ??
|
||||
{},
|
||||
(json['excluded'] as List<dynamic>?)?.map((e) => e as String).toSet() ??
|
||||
{},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CountryFilterToJson(CountryFilter instance) =>
|
||||
<String, dynamic>{
|
||||
'included': instance.included.toList(),
|
||||
'excluded': instance.excluded.toList(),
|
||||
};
|
||||
|
||||
NewUserFilter _$NewUserFilterFromJson(Map<String, dynamic> json) =>
|
||||
NewUserFilter();
|
||||
|
||||
Map<String, dynamic> _$NewUserFilterToJson(NewUserFilter instance) =>
|
||||
<String, dynamic>{};
|
||||
|
||||
ABTestAudience _$ABTestAudienceFromJson(Map<String, dynamic> json) =>
|
||||
ABTestAudience(
|
||||
filters: (json['filters'] as List<dynamic>)
|
||||
.map((e) => ABTestFilter.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
variant: json['variant'] as int? ?? 2,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ABTestAudienceToJson(ABTestAudience instance) =>
|
||||
<String, dynamic>{
|
||||
'filters': instance.filters,
|
||||
'variant': instance.variant,
|
||||
};
|
||||
|
||||
ABTestExperiment _$ABTestExperimentFromJson(Map<String, dynamic> json) =>
|
||||
ABTestExperiment(
|
||||
name: json['name'] as String,
|
||||
startTs: json['start_ts'] as int? ?? 0,
|
||||
endTs: json['end_ts'] as int? ?? 0,
|
||||
audience:
|
||||
ABTestAudience.fromJson(json['audience'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ABTestExperimentToJson(ABTestExperiment instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'start_ts': instance.startTs,
|
||||
'end_ts': instance.endTs,
|
||||
'audience': instance.audience,
|
||||
};
|
||||
|
|
@ -8,6 +8,8 @@ part 'analytics_model.g.dart';
|
|||
|
||||
@JsonSerializable()
|
||||
class AnalyticsConfig {
|
||||
static const _defaultGoogleDma = [1, 0, 12, 65];
|
||||
static const _defaultDmaCountry = [];
|
||||
@JsonKey(name: "cap", defaultValue: ["firebase", "facebook", "guru"])
|
||||
@joinedStringConvert
|
||||
final List<String> capabilities;
|
||||
|
|
@ -24,6 +26,13 @@ class AnalyticsConfig {
|
|||
@JsonKey(name: "enabled_strategy", defaultValue: false)
|
||||
final bool enabledStrategy;
|
||||
|
||||
/// ad_storage,analytics_storage,personalization,user_data
|
||||
@JsonKey(name: "google_dma", defaultValue: _defaultGoogleDma)
|
||||
final List<int> googleDmaMask;
|
||||
|
||||
@JsonKey(name: "dma_country", defaultValue: _defaultDmaCountry)
|
||||
final List<String> dmaCountry;
|
||||
|
||||
AppEventCapabilities toAppEventCapabilities() {
|
||||
int capValue = 0;
|
||||
if (capabilities.contains("firebase")) {
|
||||
|
|
@ -38,8 +47,15 @@ class AnalyticsConfig {
|
|||
return AppEventCapabilities(capValue);
|
||||
}
|
||||
|
||||
bool googleDmaGranted(ConsentType type, int flags) {
|
||||
if (type.index < googleDmaMask.length) {
|
||||
return (googleDmaMask[type.index] & flags) == googleDmaMask[type.index];
|
||||
}
|
||||
return _defaultGoogleDma[type.index] & flags == _defaultGoogleDma[type.index];
|
||||
}
|
||||
|
||||
AnalyticsConfig(this.capabilities, this.delayedInSeconds, this.expiredInDays, this.strategy,
|
||||
this.enabledStrategy);
|
||||
this.enabledStrategy, this.googleDmaMask, this.dmaCountry);
|
||||
|
||||
factory AnalyticsConfig.fromJson(Map<String, dynamic> json) => _$AnalyticsConfigFromJson(json);
|
||||
|
||||
|
|
@ -72,3 +88,12 @@ class UserIdentification {
|
|||
return 'UserIdentification{firebaseAppInstanceId: $firebaseAppInstanceId, idfa: $idfa, adId: $adId, gpsAdId: $gpsAdId}';
|
||||
}
|
||||
}
|
||||
|
||||
class ConsentFieldName {
|
||||
static const adStorage = "ad_storage";
|
||||
static const analyticsStorage = "analytics_storage";
|
||||
static const adPersonalization = "ad_personalization";
|
||||
static const adUserData = "ad_user_data";
|
||||
}
|
||||
|
||||
enum ConsentType { adStorage, analyticsStorage, adPersonalization, adUserData }
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ AnalyticsConfig _$AnalyticsConfigFromJson(Map<String, dynamic> json) =>
|
|||
json['expired_d'] as int? ?? 7,
|
||||
json['strategy'] as String? ?? '',
|
||||
json['enabled_strategy'] as bool? ?? false,
|
||||
(json['google_dma'] as List<dynamic>?)?.map((e) => e as int).toList() ??
|
||||
[1, 0, 12, 65],
|
||||
(json['dma_country'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AnalyticsConfigToJson(AnalyticsConfig instance) =>
|
||||
|
|
@ -24,6 +30,8 @@ Map<String, dynamic> _$AnalyticsConfigToJson(AnalyticsConfig instance) =>
|
|||
'expired_d': instance.expiredInDays,
|
||||
'strategy': instance.strategy,
|
||||
'enabled_strategy': instance.enabledStrategy,
|
||||
'google_dma': instance.googleDmaMask,
|
||||
'dma_country': instance.dmaCountry,
|
||||
};
|
||||
|
||||
UserIdentification _$UserIdentificationFromJson(Map<String, dynamic> json) =>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import 'dart:collection';
|
|||
import 'dart:core';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adjust_sdk/adjust_third_party_sharing.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:guru_analytics_flutter/event_logger.dart';
|
||||
import 'package:guru_analytics_flutter/event_logger_common.dart';
|
||||
|
|
@ -15,13 +17,18 @@ import 'package:guru_app/account/account_data_store.dart';
|
|||
import 'package:guru_app/ads/ads_manager.dart';
|
||||
import 'package:guru_app/ads/core/ads_config.dart';
|
||||
import 'package:guru_app/aigc/bi/ai_bi.dart';
|
||||
import 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||||
import 'package:guru_app/analytics/data/analytics_model.dart';
|
||||
import 'package:guru_app/analytics/strategy/guru_analytics_strategy.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_app/property/property_keys.dart';
|
||||
import 'package:guru_app/property/runtime_property.dart';
|
||||
import 'package:guru_app/property/settings/guru_settings.dart';
|
||||
import 'package:guru_platform_data/guru_platform_data.dart';
|
||||
import 'package:guru_utils/collection/collectionutils.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';
|
||||
|
|
@ -38,7 +45,7 @@ part 'modules/ads_analytics.dart';
|
|||
part 'modules/adjust_aware.dart';
|
||||
|
||||
class GuruAnalytics extends Analytics with AdjustAware {
|
||||
bool get release => !_mock && _enabledAnalytics && kReleaseMode;
|
||||
bool get release => !_mock && (_enabledAnalytics || kReleaseMode);
|
||||
|
||||
String appInstanceId = "";
|
||||
|
||||
|
|
@ -55,6 +62,10 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
|
||||
static String currentScreen = "";
|
||||
|
||||
static final RegExp _consentPurposeRegExp = RegExp(r"^[01]+$");
|
||||
|
||||
static String? mockCountryCode;
|
||||
|
||||
static const errorEventCodes = {
|
||||
14, // 上报事件失败
|
||||
22, // 网络状态不可用
|
||||
|
|
@ -71,8 +82,13 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
final BehaviorSubject<GuruStatistic> guruEventStatistic =
|
||||
BehaviorSubject.seeded(GuruStatistic.invalid);
|
||||
|
||||
final BehaviorSubject<Map<String, String>> abTestExperimentVariant = BehaviorSubject.seeded({});
|
||||
|
||||
Stream<GuruStatistic> get observableGuruEventStatistic => guruEventStatistic.stream;
|
||||
|
||||
Stream<Map<String, String>> get observableABTestExperimentVariant =>
|
||||
abTestExperimentVariant.stream;
|
||||
|
||||
final BehaviorSubject<UserIdentification> userIdentificationSubject =
|
||||
BehaviorSubject.seeded(UserIdentification());
|
||||
|
||||
|
|
@ -98,6 +114,19 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
return Analytics.userProperties[key];
|
||||
}
|
||||
|
||||
Future prepare() async {
|
||||
if (GuruApp.instance.appSpec.localABTestExperiments.isNotEmpty) {
|
||||
await initLocalExperiments();
|
||||
}
|
||||
RemoteConfigManager.instance.observeConfig().listen((config) {
|
||||
Log.i(
|
||||
"GuruAnalytics observeConfig changed: ${config.lastFetchStatus} ${config.lastFetchTime}");
|
||||
if (config.lastFetchStatus == RemoteConfigFetchStatus.success) {
|
||||
refreshABProperties();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void init() async {
|
||||
Log.d(
|
||||
"AnalyticsUtil init### Platform.localeName :${Platform.localeName} ${Intl.getCurrentLocale()}");
|
||||
|
|
@ -142,6 +171,7 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
Future.delayed(const Duration(seconds: 1), () {
|
||||
initAdjust();
|
||||
initFbEventMapping();
|
||||
refreshConsents();
|
||||
Log.d("register transmitter");
|
||||
});
|
||||
initialized = true;
|
||||
|
|
@ -151,6 +181,100 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
}
|
||||
}
|
||||
|
||||
Future switchSession(String oldToken, String newToken) async {
|
||||
_initEnvProperties();
|
||||
_logLocale();
|
||||
_logDeviceType();
|
||||
}
|
||||
|
||||
Future initLocalExperiments() async {
|
||||
final runningExperiments = await AppProperty.getInstance().loadRunningExperiments();
|
||||
final experiments = GuruApp.instance.appSpec.localABTestExperiments;
|
||||
final validRunningExperimentKeys =
|
||||
runningExperiments.keys.toSet().intersection(experiments.keys.toSet());
|
||||
for (var experiment in experiments.values) {
|
||||
// 如果在已经开始的实验中,但是不在当前的实验列表中,需要删除
|
||||
final needRemove = runningExperiments.containsKey(experiment.name) &&
|
||||
!validRunningExperimentKeys.contains(experiment.name);
|
||||
if (needRemove) {
|
||||
await removeExperiment(experiment.name);
|
||||
} else {
|
||||
await _applyExperiment(experiment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> refreshConsents({AnalyticsConfig? analyticsConfig}) async {
|
||||
final config = analyticsConfig ?? RemoteConfigManager.instance.getAnalyticsConfig();
|
||||
final purposeConsents = await GuruPlatformData.getPurposeConsents();
|
||||
Log.i("refreshConsents: '$purposeConsents'");
|
||||
if (purposeConsents.isEmpty) {
|
||||
return "";
|
||||
}
|
||||
|
||||
/// 如果他不是完全使用 1,0 组成的字符串
|
||||
if (!_consentPurposeRegExp.hasMatch(purposeConsents)) {
|
||||
Log.i("invalid consents $purposeConsents");
|
||||
return "";
|
||||
}
|
||||
|
||||
/// 获取当前的 countryCode, 判断是否在 dma country的范围内
|
||||
if (config.dmaCountry.isNotEmpty) {
|
||||
final countryCode = getCountryCode();
|
||||
if (!config.dmaCountry.contains(countryCode)) {
|
||||
Log.i("invalid country $countryCode");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
final length = min(purposeConsents.length, 32);
|
||||
int flags = 0;
|
||||
for (var i = 0; i < length; i++) {
|
||||
flags |= (((purposeConsents[i] == "1") ? 1 : 0) << i);
|
||||
}
|
||||
|
||||
final consentsData = {
|
||||
ConsentFieldName.adStorage: config.googleDmaGranted(ConsentType.adStorage, flags),
|
||||
ConsentFieldName.analyticsStorage:
|
||||
config.googleDmaGranted(ConsentType.analyticsStorage, flags),
|
||||
ConsentFieldName.adPersonalization:
|
||||
config.googleDmaGranted(ConsentType.adPersonalization, flags),
|
||||
ConsentFieldName.adUserData: config.googleDmaGranted(ConsentType.adUserData, flags),
|
||||
};
|
||||
|
||||
String _flag(String key) {
|
||||
return consentsData[key] == true ? "1" : "0";
|
||||
}
|
||||
|
||||
Log.d("setConsents consentsData: $consentsData");
|
||||
|
||||
try {
|
||||
final result = await EventLogger.guru.setConsents(consentsData);
|
||||
Log.d("setConsents result: $result");
|
||||
} catch (error, stacktrace) {
|
||||
Log.e("setConsents error! $error, $stacktrace");
|
||||
}
|
||||
|
||||
if (enabledAdjust) {
|
||||
AdjustThirdPartySharing adjustThirdPartySharing = AdjustThirdPartySharing(null);
|
||||
adjustThirdPartySharing.addGranularOption("google_dma", "eea", "1");
|
||||
adjustThirdPartySharing.addGranularOption(
|
||||
"google_dma", "ad_personalization", _flag(ConsentFieldName.adPersonalization));
|
||||
adjustThirdPartySharing.addGranularOption(
|
||||
"google_dma", "ad_user_data", _flag(ConsentFieldName.adUserData));
|
||||
Adjust.trackThirdPartySharing(adjustThirdPartySharing);
|
||||
Log.d("setAdjust complete!");
|
||||
}
|
||||
|
||||
final result =
|
||||
"${_flag(ConsentFieldName.adStorage)}${_flag(ConsentFieldName.analyticsStorage)}${_flag(ConsentFieldName.adPersonalization)}${_flag(ConsentFieldName.adUserData)}";
|
||||
final changed = await AppProperty.getInstance().refreshGoogleDma(result);
|
||||
if (changed || GuruSettings.instance.debugMode.get()) {
|
||||
logEventEx("dma_gg", parameters: {"purpose": purposeConsents, "result": result});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void processAnalyticsCallback(int code, String? errorInfo) {
|
||||
if (!errorEventCodes.contains(code)) {
|
||||
return;
|
||||
|
|
@ -269,7 +393,10 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
if (firebaseId?.isNotEmpty == true) {
|
||||
setFirebaseId(firebaseId!);
|
||||
}
|
||||
refreshABProperties();
|
||||
}
|
||||
|
||||
void refreshABProperties() {
|
||||
final abProperties = RemoteConfigManager.instance.getABProperties();
|
||||
|
||||
final PropertyBundle propertyBundle = PropertyBundle();
|
||||
|
|
@ -293,6 +420,17 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
setUserProperty("first_open_time", firstInstallTime.toString());
|
||||
}
|
||||
|
||||
String getCountryCode() {
|
||||
if (mockCountryCode != null) {
|
||||
return mockCountryCode!;
|
||||
}
|
||||
final currentLocale = Platform.localeName.split('_');
|
||||
if (currentLocale.length > 1) {
|
||||
return currentLocale.last.toLowerCase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void _logLocale() {
|
||||
if (Platform.localeName.isNotEmpty == true) {
|
||||
String lanCode = "";
|
||||
|
|
@ -335,6 +473,60 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
}
|
||||
}
|
||||
|
||||
static String buildVariantKey(String experimentName) {
|
||||
return "ab_$experimentName";
|
||||
}
|
||||
|
||||
String getExperimentVariant(String experimentName) {
|
||||
return abTestExperimentVariant.value[experimentName] ?? "BASELINE";
|
||||
}
|
||||
|
||||
Future<bool> setLocalABTest(ABTestExperiment experiment, {PropertyBundle? bundle}) async {
|
||||
Log.d("setLocalABTest: $experiment");
|
||||
String experimentName = experiment.name;
|
||||
final exp = await AppProperty.getInstance().getExperiment(experimentName, bundle: bundle);
|
||||
if (exp != null) {
|
||||
Log.w("Experiment already exists!");
|
||||
experiment = exp;
|
||||
}
|
||||
|
||||
return await _applyExperiment(experiment);
|
||||
}
|
||||
|
||||
Future removeExperiment(String experimentName) async {
|
||||
await AppProperty.getInstance().removeExperiment(experimentName);
|
||||
final data = Map<String, String>.of(abTestExperimentVariant.value);
|
||||
data.remove(experimentName);
|
||||
abTestExperimentVariant.addIfChanged(data);
|
||||
}
|
||||
|
||||
Future<bool> _applyExperiment(ABTestExperiment experiment) async {
|
||||
final experimentName = experiment.name;
|
||||
if (experiment.isExpired()) {
|
||||
Log.w("Experiment($experimentName) is expired");
|
||||
await removeExperiment(experimentName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!experiment.isMatchAudience()) {
|
||||
Log.i("NOT match audience! $experiment! INTO BASELINE");
|
||||
return false;
|
||||
}
|
||||
|
||||
String variantName = await AppProperty.getInstance().getExperimentVariant(experimentName);
|
||||
if (variantName.isEmpty) {
|
||||
variantName = await AppProperty.getInstance().setExperiment(experiment);
|
||||
}
|
||||
|
||||
await setGuruUserProperty(buildVariantKey(experimentName), variantName);
|
||||
Log.i("==> Setup Local Experiment($experimentName) variantName: $variantName");
|
||||
|
||||
final data = Map<String, String>.of(abTestExperimentVariant.value);
|
||||
data[experimentName] = variantName;
|
||||
abTestExperimentVariant.addIfChanged(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
void setDeviceId(String deviceId) {
|
||||
Log.d("setDeviceId: $deviceId");
|
||||
recordEvents("setDeviceId", {"userId": deviceId});
|
||||
|
|
@ -348,12 +540,12 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
}
|
||||
}
|
||||
|
||||
void setUserId(String userId) {
|
||||
Future setUserId(String userId) async {
|
||||
Log.d("setUserId: $userId");
|
||||
recordEvents("setUserId", {"userId": userId});
|
||||
recordProperty("userId", userId);
|
||||
if (userId.isNotEmpty) {
|
||||
AppProperty.getInstance().setUserId(userId);
|
||||
await AppProperty.getInstance().setUserId(userId);
|
||||
if (release) {
|
||||
EventLogger.setUserId(userId);
|
||||
FirebaseCrashlytics.instance.setUserIdentifier(userId);
|
||||
|
|
@ -528,12 +720,17 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
{String currency = "",
|
||||
String contentId = "",
|
||||
String adPlatform = "",
|
||||
Map<String, dynamic> parameters = const <String, dynamic>{}}) {
|
||||
EventLogger.logFbPurchase(amount,
|
||||
Map<String, dynamic> parameters = const <String, dynamic>{}}) async {
|
||||
Log.i("logPurchase:$amount, $currency, $contentId, $adPlatform, $parameters");
|
||||
try {
|
||||
await EventLogger.logFbPurchase(amount,
|
||||
currency: currency,
|
||||
contentId: contentId,
|
||||
adPlatform: adPlatform,
|
||||
additionParameters: parameters);
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("logFbPurchase error$error, $stacktrace");
|
||||
}
|
||||
}
|
||||
|
||||
void logEventShare({String? itemCategory, String? itemName}) {
|
||||
|
|
@ -547,6 +744,7 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
|
||||
void logSpendCredits(String contentId, String contentType, int price,
|
||||
{required String virtualCurrencyName, required int balance, String scene = ''}) {
|
||||
final levelName = GuruApp.instance.protocol.getLevelName();
|
||||
if (release) {
|
||||
EventLogger.logSpendCredits(contentId, contentType, price,
|
||||
virtualCurrencyName: virtualCurrencyName, balance: balance, scene: scene);
|
||||
|
|
@ -557,7 +755,8 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
"virtual_currency_name": virtualCurrencyName,
|
||||
"value": price,
|
||||
"balance": balance,
|
||||
"scene": scene
|
||||
"scene": scene,
|
||||
"level_name": levelName
|
||||
};
|
||||
Log.d("logEvent: spend_virtual_currency $parameters");
|
||||
EventLogger.transmit("spend_virtual_currency", parameters);
|
||||
|
|
@ -565,22 +764,34 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
|||
AiBi.instance.spendVirtualCurrency(balance, price.toDouble(), contentType);
|
||||
}
|
||||
|
||||
Future<void> logEarnVirtualCurrency({
|
||||
required String virtualCurrencyName,
|
||||
Future<void> logEarnVirtualCurrency(
|
||||
{required String virtualCurrencyName,
|
||||
required String method,
|
||||
required int balance,
|
||||
required int value,
|
||||
}) async {
|
||||
logEvent("earn_virtual_currency", <String, dynamic>{
|
||||
String? specific,
|
||||
String? scene}) async {
|
||||
final levelName = GuruApp.instance.protocol.getLevelName();
|
||||
logEvent(
|
||||
"earn_virtual_currency",
|
||||
filterOutNulls(<String, dynamic>{
|
||||
"virtual_currency_name": virtualCurrencyName,
|
||||
"item_category": method,
|
||||
"item_name": specific,
|
||||
"value": value,
|
||||
"balance": balance
|
||||
});
|
||||
"balance": balance,
|
||||
"level_name": levelName,
|
||||
"scene": scene
|
||||
}));
|
||||
AiBi.instance.earnVirtualCurrency(balance, value.toDouble(), method);
|
||||
}
|
||||
|
||||
String? peekUserProperty(String key) {
|
||||
return Analytics.userProperties[key];
|
||||
}
|
||||
|
||||
Future<void> setGuruUserProperty(String key, String value) async {
|
||||
recordProperty(key, value);
|
||||
return await EventLogger.setGuruUserProperty(key, value);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,44 @@
|
|||
part of "../guru_analytics.dart";
|
||||
|
||||
extension AdsAnalytics on GuruAnalytics {
|
||||
void logAdRevenue(double adRevenue, String adPlatform, String currency) {
|
||||
void logAdRevenue(double adRevenue, String adPlatform, String currency,
|
||||
{String? orderType, String? orderId, String? productId, int? transactionDate}) {
|
||||
// logEventEx(name, itemCategory: scene, itemName: adName);
|
||||
final orderExtras = CollectionUtils.filterOutNulls(<String, dynamic>{
|
||||
"order_type": orderType,
|
||||
"order_id": orderId,
|
||||
"product_id": productId,
|
||||
"trans_ts": transactionDate
|
||||
});
|
||||
if (release) {
|
||||
EventLogger.logAdRevenue(adRevenue, adPlatform, currency);
|
||||
EventLogger.logAdRevenue(adRevenue, adPlatform, currency, extras: orderExtras);
|
||||
} else {
|
||||
Log.d("[firebase] logAdRevenue ${<String, dynamic>{
|
||||
"adRevenue": adRevenue,
|
||||
"adPlatform": adPlatform,
|
||||
"currency": currency
|
||||
"currency": currency,
|
||||
...orderExtras
|
||||
}}");
|
||||
}
|
||||
}
|
||||
|
||||
void logAdRevenue020(double adRevenue, String adPlatform, String currency,
|
||||
{String? orderType, String? orderId, String? productId, int? transactionDate}) {
|
||||
// logEventEx(name, itemCategory: scene, itemName: adName);
|
||||
final orderExtras = CollectionUtils.filterOutNulls(<String, dynamic>{
|
||||
"order_type": orderType,
|
||||
"order_id": orderId,
|
||||
"product_id": productId,
|
||||
"trans_ts": transactionDate
|
||||
});
|
||||
if (release) {
|
||||
EventLogger.logAdRevenue020(adRevenue, adPlatform, currency, extras: orderExtras);
|
||||
} else {
|
||||
Log.d("[firebase] logAdRevenue020 ${<String, dynamic>{
|
||||
"adRevenue": adRevenue,
|
||||
"adPlatform": adPlatform,
|
||||
"currency": currency,
|
||||
...orderExtras
|
||||
}}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:guru_app/analytics/data/analytics_model.dart';
|
||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'orders_model.g.dart';
|
||||
|
|
@ -14,8 +15,7 @@ class OrderUserInfo {
|
|||
|
||||
OrderUserInfo(this.level);
|
||||
|
||||
factory OrderUserInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$OrderUserInfoFromJson(json);
|
||||
factory OrderUserInfo.fromJson(Map<String, dynamic> json) => _$OrderUserInfoFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$OrderUserInfoToJson(this);
|
||||
}
|
||||
|
|
@ -66,6 +66,12 @@ class OrdersReport {
|
|||
@JsonKey(name: "eventConfig")
|
||||
UserIdentification? userIdentification;
|
||||
|
||||
@JsonKey(name: "orderId")
|
||||
String? orderId;
|
||||
|
||||
@JsonKey(name: "transactionDate")
|
||||
int? transactionDate;
|
||||
|
||||
OrdersReport(
|
||||
{this.orderType,
|
||||
this.token,
|
||||
|
|
@ -81,7 +87,9 @@ class OrdersReport {
|
|||
this.orderUserInfo,
|
||||
this.userIdentification,
|
||||
this.offerId,
|
||||
this.basePlanId});
|
||||
this.basePlanId,
|
||||
this.orderId,
|
||||
this.transactionDate});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
|
@ -91,6 +99,7 @@ class OrdersReport {
|
|||
sb.writeln(" price: $price");
|
||||
sb.writeln(" currency: $currency");
|
||||
sb.writeln(" userIdentification: $userIdentification");
|
||||
sb.writeln(" orderId: $orderId");
|
||||
if (Platform.isAndroid) {
|
||||
sb.writeln(" orderType: $orderType");
|
||||
sb.writeln(" packageName: $packageName");
|
||||
|
|
@ -108,8 +117,7 @@ class OrdersReport {
|
|||
.toString(); //'OrdersReport{orderType: $orderType, packageName: $packageName, productId: $productId, subscriptionId: $subscriptionId, token: $token, bundleId: $bundleId, receipt: $receipt, price: $price, currency: $currency, introductoryPrice: $introductoryPrice}';
|
||||
}
|
||||
|
||||
factory OrdersReport.fromJson(Map<String, dynamic> json) =>
|
||||
_$OrdersReportFromJson(json);
|
||||
factory OrdersReport.fromJson(Map<String, dynamic> json) => _$OrdersReportFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$OrdersReportToJson(this);
|
||||
}
|
||||
|
|
@ -126,8 +134,7 @@ class OrdersResponse {
|
|||
|
||||
OrdersResponse(this.usdPrice, this.test);
|
||||
|
||||
factory OrdersResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$OrdersResponseFromJson(json);
|
||||
factory OrdersResponse.fromJson(Map<String, dynamic> json) => _$OrdersResponseFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$OrdersResponseToJson(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ OrdersReport _$OrdersReportFromJson(Map<String, dynamic> json) => OrdersReport(
|
|||
json['eventConfig'] as Map<String, dynamic>),
|
||||
offerId: json['offerId'] as String?,
|
||||
basePlanId: json['basePlanId'] as String?,
|
||||
orderId: json['orderId'] as String?,
|
||||
transactionDate: json['transactionDate'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$OrdersReportToJson(OrdersReport instance) =>
|
||||
|
|
@ -56,6 +58,8 @@ Map<String, dynamic> _$OrdersReportToJson(OrdersReport instance) =>
|
|||
'currency': instance.currency,
|
||||
'userInfo': instance.orderUserInfo,
|
||||
'eventConfig': instance.userIdentification,
|
||||
'orderId': instance.orderId,
|
||||
'transactionDate': instance.transactionDate,
|
||||
};
|
||||
|
||||
OrdersResponse _$OrdersResponseFromJson(Map<String, dynamic> json) =>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:guru_app/account/account_data_store.dart';
|
|||
import 'package:guru_app/account/model/user.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||||
import 'package:guru_utils/device/device_info.dart';
|
||||
import 'package:guru_utils/device/device_utils.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
|
@ -120,10 +121,28 @@ abstract class GuruApiMethods {
|
|||
|
||||
// Auth
|
||||
@POST("/auth/api/v1/tokens/provider/secret")
|
||||
Future<SaasUser> signInWithAnonymous(@Body() AnonymousLoginReqBody body);
|
||||
Future<GuruUser> signInWithAnonymous(@Body() AnonymousLoginReqBody body);
|
||||
|
||||
@POST("/auth/api/v1/tokens/provider/facebook-gaming")
|
||||
Future<GuruUser> signInWithFacebook(@Body() FacebookLoginReqBody body);
|
||||
|
||||
@POST("/auth/api/v1/tokens/provider/google")
|
||||
Future<GuruUser> signInWithGoogle(@Body() GoogleLoginReqBody body);
|
||||
|
||||
@POST("/auth/api/v1/tokens/provider/apple")
|
||||
Future<GuruUser> signInWithApple(@Body() AppleLoginReqBody body);
|
||||
|
||||
@POST("/auth/api/v1/bindings/provider/facebook-gaming")
|
||||
Future<GuruUser> associateWithFacebook(@Body() FacebookLoginReqBody body);
|
||||
|
||||
@POST("/auth/api/v1/bindings/provider/google")
|
||||
Future<GuruUser> associateWithGoogle(@Body() GoogleLoginReqBody body);
|
||||
|
||||
@POST("/auth/api/v1/bindings/provider/apple")
|
||||
Future<GuruUser> associateWithApple(@Body() AppleLoginReqBody body);
|
||||
|
||||
@POST("/auth/api/v1/renewals/token")
|
||||
Future<SaasUser> refreshSaasToken();
|
||||
Future<GuruUser> refreshSaasToken();
|
||||
|
||||
@POST("/auth/api/v1/renewals/firebase")
|
||||
Future<FirebaseTokenData> renewFirebaseToken();
|
||||
|
|
|
|||
|
|
@ -46,14 +46,14 @@ class _GuruApiMethods implements GuruApiMethods {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<SaasUser> signInWithAnonymous(AnonymousLoginReqBody body) async {
|
||||
Future<GuruUser> signInWithAnonymous(AnonymousLoginReqBody body) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
_data.addAll(body.toJson());
|
||||
final _result =
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<SaasUser>(Options(
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||
method: 'POST',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
|
|
@ -69,18 +69,186 @@ class _GuruApiMethods implements GuruApiMethods {
|
|||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
))));
|
||||
final value = SaasUser.fromJson(_result.data!);
|
||||
final value = GuruUser.fromJson(_result.data!);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SaasUser> refreshSaasToken() async {
|
||||
Future<GuruUser> signInWithFacebook(FacebookLoginReqBody body) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
_data.addAll(body.toJson());
|
||||
final _result =
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||
method: 'POST',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
'/auth/api/v1/tokens/provider/facebook-gaming',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
))));
|
||||
final value = GuruUser.fromJson(_result.data!);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GuruUser> signInWithGoogle(GoogleLoginReqBody body) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
_data.addAll(body.toJson());
|
||||
final _result =
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||
method: 'POST',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
'/auth/api/v1/tokens/provider/google',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
))));
|
||||
final value = GuruUser.fromJson(_result.data!);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GuruUser> signInWithApple(AppleLoginReqBody body) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
_data.addAll(body.toJson());
|
||||
final _result =
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||
method: 'POST',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
'/auth/api/v1/tokens/provider/apple',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
))));
|
||||
final value = GuruUser.fromJson(_result.data!);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GuruUser> associateWithFacebook(FacebookLoginReqBody body) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
_data.addAll(body.toJson());
|
||||
final _result =
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||
method: 'POST',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
'/auth/api/v1/bindings/provider/facebook-gaming',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
))));
|
||||
final value = GuruUser.fromJson(_result.data!);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GuruUser> associateWithGoogle(GoogleLoginReqBody body) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
_data.addAll(body.toJson());
|
||||
final _result =
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||
method: 'POST',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
'/auth/api/v1/bindings/provider/google',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
))));
|
||||
final value = GuruUser.fromJson(_result.data!);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GuruUser> associateWithApple(AppleLoginReqBody body) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
_data.addAll(body.toJson());
|
||||
final _result =
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||
method: 'POST',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
'/auth/api/v1/bindings/provider/apple',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
))));
|
||||
final value = GuruUser.fromJson(_result.data!);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GuruUser> refreshSaasToken() async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _data = null;
|
||||
final _result =
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<SaasUser>(Options(
|
||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||
method: 'POST',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
|
|
@ -96,7 +264,7 @@ class _GuruApiMethods implements GuruApiMethods {
|
|||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
))));
|
||||
final value = SaasUser.fromJson(_result.data!);
|
||||
final value = GuruUser.fromJson(_result.data!);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,36 @@
|
|||
part of "../guru_api.dart";
|
||||
|
||||
extension GuruApiExtension on GuruApi {
|
||||
Future<SaasUser> signInWithAnonymous({required String secret}) async {
|
||||
return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: secret));
|
||||
// Future<GuruUser> signInWithAnonymous({required String secret}) async {
|
||||
// return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: secret));
|
||||
// }
|
||||
|
||||
Future<GuruUser> loginGuruWithCredential({required Credential credential}) async {
|
||||
switch (credential.authType) {
|
||||
case AuthType.facebook:
|
||||
return await methods
|
||||
.signInWithFacebook(FacebookLoginReqBody(accessToken: credential.token));
|
||||
case AuthType.google:
|
||||
return await methods.signInWithGoogle(GoogleLoginReqBody(idToken: credential.token));
|
||||
case AuthType.apple:
|
||||
return await methods.signInWithApple(AppleLoginReqBody(token: credential.token));
|
||||
case AuthType.anonymous:
|
||||
return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: credential.token));
|
||||
}
|
||||
}
|
||||
|
||||
Future<GuruUser> associateCredential({required Credential credential}) async {
|
||||
switch (credential.authType) {
|
||||
case AuthType.facebook:
|
||||
return await methods
|
||||
.associateWithFacebook(FacebookLoginReqBody(accessToken: credential.token));
|
||||
case AuthType.google:
|
||||
return await methods.associateWithGoogle(GoogleLoginReqBody(idToken: credential.token));
|
||||
case AuthType.apple:
|
||||
return await methods.associateWithApple(AppleLoginReqBody(token: credential.token));
|
||||
case AuthType.anonymous:
|
||||
return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: credential.token));
|
||||
}
|
||||
}
|
||||
|
||||
Future reportDevice(DeviceInfo deviceInfo) async {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:guru_app/firebase/messaging/remote_messaging_manager.dart';
|
||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
/// Created by Haoyi on 2022/8/29
|
||||
|
|
@ -64,6 +65,9 @@ class Deployment {
|
|||
static const int defaultApiTimeout = 15000; // 15s
|
||||
static const int defaultIosSandboxSubsRenewalSpeed = 2;
|
||||
static const int defaultTrackingNotificationPermissionPassLimitTimes = 10;
|
||||
static const int defaultSubscriptionRestoreGraceCount = 3;
|
||||
static const int defaultFullscreenMinInterval = 60;
|
||||
static const int defaultSubscriptionGraceDays = DateTimeUtils.dayInMillis;
|
||||
|
||||
@JsonKey(name: "property_cache_size", defaultValue: 256)
|
||||
final int propertyCacheSize;
|
||||
|
|
@ -139,6 +143,21 @@ class Deployment {
|
|||
@JsonKey(name: "show_internal_ads_when_banner_unavailable", defaultValue: false)
|
||||
final bool showInternalAdsWhenBannerUnavailable;
|
||||
|
||||
@JsonKey(name: "subscription_restore_grace_count", defaultValue: defaultSubscriptionRestoreGraceCount)
|
||||
final int subscriptionRestoreGraceCount;
|
||||
|
||||
@JsonKey(name: "fullscreen_ads_min_interval", defaultValue: defaultFullscreenMinInterval)
|
||||
final int fullscreenAdsMinInterval;
|
||||
|
||||
@JsonKey(name: "subscription_grace_period", defaultValue: defaultSubscriptionGraceDays)
|
||||
final int subscriptionGraceDays;
|
||||
|
||||
@JsonKey(name: "enabled_sync_account_profile", defaultValue: false)
|
||||
final bool enabledSyncAccountProfile;
|
||||
|
||||
@JsonKey(name: "purchase_event_trigger", defaultValue: 1)
|
||||
final int purchaseEventTrigger;
|
||||
|
||||
Deployment(
|
||||
{this.propertyCacheSize = 256,
|
||||
this.enableDithering = true,
|
||||
|
|
@ -164,7 +183,12 @@ class Deployment {
|
|||
defaultTrackingNotificationPermissionPassLimitTimes,
|
||||
this.enabledGuruAnalyticsStrategy = false,
|
||||
this.allowInterstitialAsAlternativeReward = false,
|
||||
this.showInternalAdsWhenBannerUnavailable = false});
|
||||
this.showInternalAdsWhenBannerUnavailable = false,
|
||||
this.subscriptionRestoreGraceCount = defaultSubscriptionRestoreGraceCount,
|
||||
this.fullscreenAdsMinInterval = defaultFullscreenMinInterval,
|
||||
this.subscriptionGraceDays = defaultSubscriptionGraceDays,
|
||||
this.enabledSyncAccountProfile = false,
|
||||
this.purchaseEventTrigger = 1});
|
||||
|
||||
factory Deployment.fromJson(Map<String, dynamic> json) => _$DeploymentFromJson(json);
|
||||
|
||||
|
|
@ -176,7 +200,11 @@ class RemoteDeployment {
|
|||
@JsonKey(name: "keep_screen_on_duration_m", defaultValue: 0)
|
||||
final int keepScreenOnDuration;
|
||||
|
||||
RemoteDeployment({this.keepScreenOnDuration = 0});
|
||||
@JsonKey(name: "subscriptionGraceDays")
|
||||
final int? subscriptionGraceDays;
|
||||
|
||||
RemoteDeployment(
|
||||
{this.keepScreenOnDuration = 0, this.subscriptionGraceDays});
|
||||
|
||||
factory RemoteDeployment.fromJson(Map<String, dynamic> json) => _$RemoteDeploymentFromJson(json);
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,14 @@ Deployment _$DeploymentFromJson(Map<String, dynamic> json) => Deployment(
|
|||
json['allow_interstitial_as_alternative_reward'] as bool? ?? false,
|
||||
showInternalAdsWhenBannerUnavailable:
|
||||
json['show_internal_ads_when_banner_unavailable'] as bool? ?? false,
|
||||
subscriptionRestoreGraceCount:
|
||||
json['subscription_restore_grace_count'] as int? ?? 3,
|
||||
fullscreenAdsMinInterval:
|
||||
json['fullscreen_ads_min_interval'] as int? ?? 60,
|
||||
subscriptionGraceDays:
|
||||
json['subscription_grace_period'] as int? ?? 86400000,
|
||||
enabledSyncAccountProfile:
|
||||
json['enabled_sync_account_profile'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DeploymentToJson(Deployment instance) =>
|
||||
|
|
@ -113,6 +121,11 @@ Map<String, dynamic> _$DeploymentToJson(Deployment instance) =>
|
|||
instance.allowInterstitialAsAlternativeReward,
|
||||
'show_internal_ads_when_banner_unavailable':
|
||||
instance.showInternalAdsWhenBannerUnavailable,
|
||||
'subscription_restore_grace_count':
|
||||
instance.subscriptionRestoreGraceCount,
|
||||
'fullscreen_ads_min_interval': instance.fullscreenAdsMinInterval,
|
||||
'subscription_grace_period': instance.subscriptionGraceDays,
|
||||
'enabled_sync_account_profile': instance.enabledSyncAccountProfile,
|
||||
};
|
||||
|
||||
const _$PromptTriggerEnumMap = {
|
||||
|
|
@ -123,9 +136,11 @@ const _$PromptTriggerEnumMap = {
|
|||
RemoteDeployment _$RemoteDeploymentFromJson(Map<String, dynamic> json) =>
|
||||
RemoteDeployment(
|
||||
keepScreenOnDuration: json['keep_screen_on_duration_m'] as int? ?? 0,
|
||||
subscriptionGraceDays: json['subscriptionGraceDays'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RemoteDeploymentToJson(RemoteDeployment instance) =>
|
||||
<String, dynamic>{
|
||||
'keep_screen_on_duration_m': instance.keepScreenOnDuration,
|
||||
'subscriptionGraceDays': instance.subscriptionGraceDays,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@ import 'package:guru_app/financial/asset/assets_store.dart';
|
|||
import 'package:guru_app/financial/financial_manager.dart';
|
||||
import 'package:guru_app/financial/iap/iap_manager.dart';
|
||||
import 'package:guru_app/financial/iap/iap_model.dart';
|
||||
import 'package:guru_app/financial/igb/igb_manager.dart';
|
||||
import 'package:guru_app/financial/igb/igb_product.dart';
|
||||
import 'package:guru_app/financial/igc/igc_manager.dart';
|
||||
import 'package:guru_app/financial/igc/igc_model.dart';
|
||||
import 'package:guru_app/financial/product/product_store.dart';
|
||||
import 'package:guru_app/financial/reward/reward_manager.dart';
|
||||
import 'package:guru_app/financial/reward/reward_model.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/inventory/db/inventory_database.dart';
|
||||
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||
import 'package:guru_app/test/test_guru_app_creator.dart';
|
||||
import 'package:guru_utils/collection/collectionutils.dart';
|
||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||
import 'package:guru_utils/extensions/extensions.dart';
|
||||
import 'package:guru_utils/controller/controller.dart';
|
||||
|
|
@ -44,6 +49,9 @@ mixin AssetsAware on LifecycleController {
|
|||
|
||||
Stream<int> get observableIgcBalance => IgcManager.instance.observableCurrentBalance;
|
||||
|
||||
Stream<Map<String, InventoryItem>> get observableInventoryItems =>
|
||||
InventoryManager.instance.observableData;
|
||||
|
||||
Future restorePurchases() async {
|
||||
return await IapManager.instance.restorePurchases();
|
||||
}
|
||||
|
|
@ -73,6 +81,52 @@ mixin AssetsAware on LifecycleController {
|
|||
return RewardManager.instance.buildRewardProduct(intent);
|
||||
}
|
||||
|
||||
Future<IgbProduct> buildIgbProduct(TransactionIntent intent) {
|
||||
return IgbManager.instance.buildIgbProduct(intent);
|
||||
}
|
||||
|
||||
int getInventoryBalance(String sku) {
|
||||
return InventoryManager.instance.getData(sku)?.balance ?? 0;
|
||||
}
|
||||
|
||||
TimeSensitiveData getInventoryTimeSensitiveData(String sku) {
|
||||
return InventoryManager.instance.getData(sku)?.timeSensitive ?? const TimeSensitiveData();
|
||||
}
|
||||
|
||||
/// 使用指定[sku]的道具,[amount]为使用数量,[action]的行为,[scene]为使用场景
|
||||
/// useProp最终会得到一个行为上的收益,因此这里为了方便针对道具使用进行统一的行为分析
|
||||
/// 这里的 [action]和[scene]最终会通过 spend_virtual_currency 事件进行统一的统计
|
||||
/// propCategory可以参照 [PropCategory] 中的定义
|
||||
/// 具体参数对照如下:
|
||||
/// - **`item_name`**: [intent] 如果这里指定了使用道具的意图,那么这里就会使用这个意图,否则使用场景,
|
||||
/// - **`item_category`**: [category],
|
||||
/// - **`virtual_currency_name`**: [propSku],
|
||||
/// - **`value`**: [amount],
|
||||
/// - **`balance`**: balance,
|
||||
/// - **`scene`**: [scene],
|
||||
/// - **`level_name`**: levelName
|
||||
///
|
||||
/// 返回值为是否成功使用道具,false表示道具不足,true表示使用成功
|
||||
///
|
||||
Future<bool> useProp(
|
||||
String propSku,
|
||||
String scene, {
|
||||
int amount = 1,
|
||||
String? intent,
|
||||
String category = PropCategory.boosts,
|
||||
bool timeSensitiveOnly = false,
|
||||
int? transactionTs,
|
||||
}) async {
|
||||
final manifest = Manifest.action(category, scene,
|
||||
extras: CollectionUtils.filterOutNulls({
|
||||
ExtraReservedField.contentId: intent ?? scene, // 如果这里指定了使用道具的意图,那么这里就会使用这个意图,否则使用场景
|
||||
ExtraReservedField.transactionTs: transactionTs // 如果这里指定了交易时间,那么就会使用这个时间,否则使用当前时间
|
||||
}));
|
||||
return await InventoryManager.instance.consume(
|
||||
[StockItem.consumable(propSku, amount)], manifest,
|
||||
timeSensitiveOnly: timeSensitiveOnly);
|
||||
}
|
||||
|
||||
Future<bool> requestProduct(Product product, {String from = ""}) async {
|
||||
if (product is IapProduct) {
|
||||
return await IapManager.instance.buy(product);
|
||||
|
|
@ -80,6 +134,8 @@ mixin AssetsAware on LifecycleController {
|
|||
return await IgcManager.instance.purchase(product);
|
||||
} else if (product is RewardProduct) {
|
||||
return await RewardManager.instance.claim(product);
|
||||
} else if (product is IgbProduct) {
|
||||
return await IgbManager.instance.redeem(product);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:guru_app/financial/data/db/order_database.dart';
|
||||
import 'package:guru_app/inventory/db/inventory_database.dart';
|
||||
import 'package:guru_utils/database/database.dart';
|
||||
import 'package:guru_utils/property/storage/db/property_database.dart';
|
||||
|
||||
|
|
@ -8,6 +9,8 @@ final List<TableCreator> _creatorV1 = [PropertyEntity.createTable];
|
|||
|
||||
final List<TableCreator> _creatorV2 = [OrderEntity.createTable];
|
||||
|
||||
final List<TableCreator> _creatorV4 = [InventoryTable.createTable];
|
||||
|
||||
class Creators {
|
||||
static final List<TableCreator> creators = [..._creatorV1, ..._creatorV2];
|
||||
static final List<TableCreator> creators = [..._creatorV1, ..._creatorV2, ..._creatorV4];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,5 @@ class GuruDB extends _GuruDB with PropertyDatabase {
|
|||
List<TableCreator> get tableCreators => Creators.creators;
|
||||
|
||||
@override
|
||||
int get version => 3;
|
||||
int get version => 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
part of "migrations.dart";
|
||||
|
||||
class _MigrationV3toV4 implements Migration {
|
||||
@override
|
||||
Future<MigrateResult> migrate(Transaction transaction) async {
|
||||
// 由于这里无法保证所在平台是否支持IF NOT EXISTS,所以这里用try catch来处理
|
||||
try {
|
||||
await InventoryTable.createTable(transaction);
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("ignore alter cmd!");
|
||||
}
|
||||
return MigrateResult.success;
|
||||
}
|
||||
}
|
||||
|
||||
final migration3to4 = _MigrationV3toV4();
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import 'package:guru_app/financial/data/db/order_database.dart';
|
||||
import 'package:guru_app/inventory/db/inventory_database.dart';
|
||||
import 'package:guru_utils/database/database.dart';
|
||||
import 'package:guru_utils/log/log.dart';
|
||||
|
||||
part "migration_v1_to_v2.dart";
|
||||
part 'migration_v2_to_v3.dart';
|
||||
part 'migration_v3_to_v4.dart';
|
||||
|
||||
/// Created by @Haoyi on 2020/5/22
|
||||
///
|
||||
|
||||
class Migrations {
|
||||
static final migrations = [migration1to2, migration2to3];
|
||||
static final migrations = [migration1to2, migration2to3, migration3to4];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:guru_app/financial/asset/assets_model.dart';
|
|||
import 'package:guru_app/financial/asset/assets_store.dart';
|
||||
import 'package:guru_app/financial/iap/iap_manager.dart';
|
||||
import 'package:guru_app/financial/iap/iap_model.dart';
|
||||
import 'package:guru_app/financial/igb/igb_manager.dart';
|
||||
import 'package:guru_app/financial/igc/igc_manager.dart';
|
||||
import 'package:guru_app/financial/reward/reward_manager.dart';
|
||||
import 'package:guru_utils/extensions/extensions.dart';
|
||||
|
|
@ -54,6 +55,13 @@ class FinancialManager {
|
|||
void init() {
|
||||
IapManager.instance.init();
|
||||
IgcManager.instance.init();
|
||||
IgbManager.instance.init();
|
||||
RewardManager.instance.init();
|
||||
}
|
||||
|
||||
void switchSession(String fromUid, String toUid) {
|
||||
IapManager.instance.switchSession();
|
||||
IgcManager.instance.switchSession();
|
||||
RewardManager.instance.switchSession();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'package:guru_app/financial/manifest/manifest.dart';
|
|||
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
||||
import 'package:guru_app/financial/product/product_model.dart';
|
||||
import 'package:guru_app/financial/product/product_store.dart';
|
||||
import 'package:guru_app/firebase/firebase.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_app/property/property_keys.dart';
|
||||
|
|
@ -50,17 +51,14 @@ class IapManager {
|
|||
final BehaviorSubject<AssetsStore<Asset>> _iapStoreSubject =
|
||||
BehaviorSubject.seeded(AssetsStore<Asset>.inactive());
|
||||
|
||||
final Map<ProductId, IapRequest> iapRequestMap =
|
||||
HashMap<ProductId, IapRequest>();
|
||||
final Map<ProductId, IapRequest> iapRequestMap = HashMap<ProductId, IapRequest>();
|
||||
|
||||
Stream<Map<ProductId, ProductDetails>> get observableProductDetails =>
|
||||
_productDetailsSubject.stream;
|
||||
|
||||
Stream<AssetsStore<Asset>> get observableAssetStore =>
|
||||
_iapStoreSubject.stream;
|
||||
Stream<AssetsStore<Asset>> get observableAssetStore => _iapStoreSubject.stream;
|
||||
|
||||
Map<ProductId, ProductDetails> get loadedProductDetails =>
|
||||
_productDetailsSubject.value;
|
||||
Map<ProductId, ProductDetails> get loadedProductDetails => _productDetailsSubject.value;
|
||||
|
||||
AssetsStore<Asset> get purchasedStore => _iapStoreSubject.value;
|
||||
|
||||
|
|
@ -83,8 +81,8 @@ class IapManager {
|
|||
bool _restorePurchase = false;
|
||||
|
||||
final iapRevenueAppEventOptions = AppEventOptions(
|
||||
capabilities: const AppEventCapabilities(
|
||||
AppEventCapabilities.firebase | AppEventCapabilities.guru),
|
||||
capabilities:
|
||||
const AppEventCapabilities(AppEventCapabilities.firebase | AppEventCapabilities.guru),
|
||||
firebaseParamsConvertor: _iapRevenueToValue,
|
||||
guruParamsConvertor: _iapRevenueToValue);
|
||||
|
||||
|
|
@ -100,8 +98,7 @@ class IapManager {
|
|||
void init() async {
|
||||
final iapCount = await AppProperty.getInstance().getIapCount();
|
||||
if (iapCount > 0) {
|
||||
GuruAnalytics.instance
|
||||
.setUserProperty("purchase_count", iapCount.toString());
|
||||
GuruAnalytics.instance.setUserProperty("purchase_count", iapCount.toString());
|
||||
GuruAnalytics.instance.setUserProperty("is_iap_user", "true");
|
||||
} else {
|
||||
GuruAnalytics.instance.setUserProperty("is_iap_user", "false");
|
||||
|
|
@ -113,8 +110,7 @@ class IapManager {
|
|||
stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true);
|
||||
}
|
||||
if (subscription == null) {
|
||||
final Stream<List<PurchaseDetails>> purchaseUpdated =
|
||||
_inAppPurchase.purchaseStream;
|
||||
final Stream<List<PurchaseDetails>> purchaseUpdated = _inAppPurchase.purchaseStream;
|
||||
subscription = purchaseUpdated.listen(
|
||||
(List<PurchaseDetails> purchaseDetailsList) {
|
||||
_listenToPurchaseUpdated(purchaseDetailsList);
|
||||
|
|
@ -141,13 +137,27 @@ class IapManager {
|
|||
} finally {}
|
||||
}
|
||||
|
||||
Future switchSession() async {
|
||||
final iapCount = await AppProperty.getInstance().getIapCount();
|
||||
if (iapCount > 0) {
|
||||
GuruAnalytics.instance.setUserProperty("purchase_count", iapCount.toString());
|
||||
GuruAnalytics.instance.setUserProperty("is_iap_user", "true");
|
||||
} else {
|
||||
GuruAnalytics.instance.setUserProperty("is_iap_user", "false");
|
||||
}
|
||||
try {
|
||||
await reloadOrders();
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("reloadOrders error! $error",
|
||||
stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true);
|
||||
}
|
||||
_checkAndLoad();
|
||||
}
|
||||
|
||||
Future reloadOrders() async {
|
||||
final transactions = await GuruDB.instance.selectOrders(
|
||||
method: TransactionMethod.iap,
|
||||
attrs: [
|
||||
TransactionAttributes.asset,
|
||||
TransactionAttributes.subscriptions
|
||||
]);
|
||||
attrs: [TransactionAttributes.asset, TransactionAttributes.subscriptions]);
|
||||
final newAssetStore = AssetsStore<Asset>();
|
||||
Log.d("reloadOrders ${transactions.length}");
|
||||
for (var transaction in transactions) {
|
||||
|
|
@ -165,15 +175,14 @@ class IapManager {
|
|||
do {
|
||||
final seconds = min(MathUtils.fibonacci(retry, offset: 2), 900);
|
||||
await Future.delayed(Duration(seconds: seconds));
|
||||
available =
|
||||
await _inAppPurchase.isAvailable().catchError((error, stacktrace) {
|
||||
available = await _inAppPurchase.isAvailable().catchError((error, stacktrace) {
|
||||
Log.w("isAvailable error:$error", stackTrace: stacktrace);
|
||||
return false;
|
||||
});
|
||||
Log.d("_checkAndLoad:$retry available:$available");
|
||||
retry++;
|
||||
} while (!available);
|
||||
availableSubject.addEx(true);
|
||||
availableSubject.addIfChanged(true);
|
||||
try {
|
||||
await refreshProducts();
|
||||
if (GuruApp.instance.appSpec.deployment.autoRestoreIap ||
|
||||
|
|
@ -196,12 +205,9 @@ class IapManager {
|
|||
iapRequest.response(false);
|
||||
final iapErrorMsg = "_processIapError:${iapRequest.productId}";
|
||||
Log.w(iapErrorMsg,
|
||||
error: PurchaseError(iapErrorMsg),
|
||||
syncFirebase: true,
|
||||
syncCrashlytics: true);
|
||||
error: PurchaseError(iapErrorMsg), syncFirebase: true, syncCrashlytics: true);
|
||||
try {
|
||||
await GuruDB.instance
|
||||
.upsertOrder(order: iapRequest.order.error(iapErrorMsg));
|
||||
await GuruDB.instance.upsertOrder(order: iapRequest.order.error(iapErrorMsg));
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("_processIapError upsert error! $error", syncFirebase: true);
|
||||
}
|
||||
|
|
@ -218,8 +224,7 @@ class IapManager {
|
|||
try {
|
||||
await GuruDB.instance.deleteOrder(order: order);
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("_processIapCancel deleteOrder error! $error",
|
||||
syncFirebase: true);
|
||||
Log.w("_processIapCancel deleteOrder error! $error", syncFirebase: true);
|
||||
}
|
||||
}
|
||||
iapRequestMap.clear();
|
||||
|
|
@ -248,22 +253,18 @@ class IapManager {
|
|||
// });
|
||||
// }
|
||||
|
||||
String dumpProductAndPurchased(
|
||||
ProductDetails details, PurchaseDetails purchaseDetails) {
|
||||
String dumpProductAndPurchased(ProductDetails details, PurchaseDetails purchaseDetails) {
|
||||
final StringBuffer sb = StringBuffer();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
GooglePlayPurchaseDetails googlePlayDetails =
|
||||
purchaseDetails as GooglePlayPurchaseDetails;
|
||||
GooglePlayProductDetails googlePlayProduct =
|
||||
details as GooglePlayProductDetails;
|
||||
GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails;
|
||||
GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails;
|
||||
Log.d(
|
||||
"Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}");
|
||||
} catch (error, stacktrace) {}
|
||||
} else if (Platform.isIOS) {
|
||||
AppStorePurchaseDetails appleDetails =
|
||||
purchaseDetails as AppStorePurchaseDetails;
|
||||
AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails;
|
||||
AppStoreProductDetails appleProduct = details as AppStoreProductDetails;
|
||||
sb.writeln("#### purchase ####");
|
||||
sb.writeln("productID: ${appleDetails.productID}");
|
||||
|
|
@ -274,23 +275,18 @@ class IapManager {
|
|||
sb.writeln("skPaymentTransaction:");
|
||||
sb.writeln(
|
||||
" =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}");
|
||||
sb.writeln(
|
||||
" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
||||
sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
||||
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:");
|
||||
sb.writeln(
|
||||
" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
||||
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
||||
sb.writeln("\n#### product ####");
|
||||
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
||||
sb.writeln("rawPrice: ${appleProduct.rawPrice}");
|
||||
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
||||
sb.writeln("currencyCode skProduct");
|
||||
sb.writeln(
|
||||
" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
||||
sb.writeln(
|
||||
" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
||||
sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
||||
sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
||||
sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}");
|
||||
sb.writeln(
|
||||
" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
||||
sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
||||
sb.writeln(
|
||||
" =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}");
|
||||
sb.writeln(" =>appleProduct.skProduct.priceLocale");
|
||||
|
|
@ -319,9 +315,8 @@ class IapManager {
|
|||
|
||||
int getIOSPeriodInterval(int numberOfUnits, SKSubscriptionPeriodUnit unit) {
|
||||
if (GuruSettings.instance.debugMode.get()) {
|
||||
final renewalSpeed = GuruApp
|
||||
.instance.appSpec.deployment.iosSandboxSubsRenewalSpeed
|
||||
.clamp(1, 5);
|
||||
final renewalSpeed =
|
||||
GuruApp.instance.appSpec.deployment.iosSandboxSubsRenewalSpeed.clamp(1, 5);
|
||||
switch (unit) {
|
||||
case SKSubscriptionPeriodUnit.day:
|
||||
return numberOfUnits * weekRenewalDurations[renewalSpeed - 1] ~/ 7;
|
||||
|
|
@ -346,8 +341,52 @@ class IapManager {
|
|||
}
|
||||
}
|
||||
|
||||
Future processRestoredSubscription(
|
||||
List<PurchaseDetails> subscriptionPurchased) async {
|
||||
bool checkSubscriptionPeriod(PurchaseDetails purchaseDetails, ProductDetails productDetails) {
|
||||
bool validOrder = false;
|
||||
if (Platform.isAndroid) {
|
||||
validOrder = true;
|
||||
} else if (Platform.isIOS) {
|
||||
final appleProduct = productDetails as AppStoreProductDetails;
|
||||
SKProductSubscriptionPeriodWrapper? period = appleProduct.skProduct.subscriptionPeriod;
|
||||
|
||||
final appleDetails = purchaseDetails as AppStorePurchaseDetails;
|
||||
final paymentDiscount = appleDetails.skPaymentTransaction.payment.paymentDiscount;
|
||||
if (paymentDiscount != null) {
|
||||
Log.i("paymentDiscount: ${paymentDiscount.identifier} ${paymentDiscount.timestamp}");
|
||||
for (var discount in appleProduct.skProduct.discounts) {
|
||||
final discountSubPeriod = discount.subscriptionPeriod;
|
||||
Log.i(
|
||||
"check discount(${paymentDiscount.identifier}) product [ ${discount.identifier} ${discountSubPeriod.unit} ${discountSubPeriod.numberOfUnits} ]");
|
||||
if (discount.identifier == paymentDiscount.identifier) {
|
||||
period = discount.subscriptionPeriod;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("checkSubscriptionPeriod: ${appleProduct.skProduct.productIdentifier} ${period?.unit} ${period?.numberOfUnits} ${appleDetails.transactionDate}");
|
||||
|
||||
if (period != null) {
|
||||
final numberOfUnits = period.numberOfUnits;
|
||||
final unit = period.unit;
|
||||
final int validInterval = getIOSPeriodInterval(numberOfUnits, unit);
|
||||
final transactionTs = int.tryParse(purchaseDetails.transactionDate ?? "") ?? 0;
|
||||
final now = DateTimeUtils.currentTimeInMillis();
|
||||
final gracePeriod = GuruApp.instance.remoteDeployment.subscriptionGraceDays ??
|
||||
GuruApp.instance.appSpec.deployment.subscriptionGraceDays;
|
||||
/// 过期时间 = 订单的最后一次刷新时间 + 订阅周期(优惠周期) + 宽限期
|
||||
final expiredTs = transactionTs + validInterval + gracePeriod;
|
||||
/// 如果当前的时间小于过期时间,那么这个订单是有效的
|
||||
validOrder = now < expiredTs;
|
||||
Log.d(
|
||||
"productID: ${purchaseDetails.productID}) purchaseID: ${purchaseDetails.purchaseID}[$numberOfUnits][$unit] [$now < $transactionTs + $validInterval]($validOrder) ",
|
||||
tag: PropertyTags.iap);
|
||||
}
|
||||
}
|
||||
return validOrder;
|
||||
}
|
||||
|
||||
Future processRestoredSubscription(List<PurchaseDetails> subscriptionPurchased) async {
|
||||
List<PurchaseDetails> purchasedDetails = subscriptionPurchased;
|
||||
if (Platform.isIOS) {
|
||||
purchasedDetails = buildLatestPurchasedPlanForIos(subscriptionPurchased);
|
||||
|
|
@ -359,8 +398,7 @@ class IapManager {
|
|||
if (Platform.isAndroid) {
|
||||
final purchasedSkus = purchasedDetails.map((e) => e.productID).toSet();
|
||||
newPurchasedStore.removeWhere((productId, asset) {
|
||||
final expired =
|
||||
productId.isSubscription && !purchasedSkus.contains(productId.sku);
|
||||
final expired = productId.isSubscription && !purchasedSkus.contains(productId.sku);
|
||||
Log.i("remove expired subscription[$productId] expired:$expired");
|
||||
if (expired) {
|
||||
expiredSkus.add(asset.productId.sku);
|
||||
|
|
@ -370,8 +408,7 @@ class IapManager {
|
|||
}
|
||||
|
||||
for (var purchased in purchasedDetails) {
|
||||
final productId =
|
||||
GuruApp.instance.findProductId(sku: purchased.productID);
|
||||
final productId = GuruApp.instance.findProductId(sku: purchased.productID);
|
||||
if (productId == null) {
|
||||
Log.w("productId is null! ${purchased.productID}");
|
||||
continue;
|
||||
|
|
@ -381,26 +418,7 @@ class IapManager {
|
|||
Log.w("product is null! ${purchased.productID}");
|
||||
continue;
|
||||
}
|
||||
purchased.transactionDate;
|
||||
bool validPurchase = false;
|
||||
if (Platform.isAndroid) {
|
||||
validPurchase = true;
|
||||
} else if (Platform.isIOS) {
|
||||
final appleProduct = productDetails as AppStoreProductDetails;
|
||||
final period = appleProduct.skProduct.subscriptionPeriod;
|
||||
if (period != null) {
|
||||
final numberOfUnits = period.numberOfUnits;
|
||||
final unit = period.unit;
|
||||
final int validInterval = getIOSPeriodInterval(numberOfUnits, unit);
|
||||
final transactionTs =
|
||||
int.tryParse(purchased.transactionDate ?? "") ?? 0;
|
||||
final now = DateTimeUtils.currentTimeInMillis();
|
||||
validPurchase = transactionTs + validInterval < now;
|
||||
Log.d(
|
||||
"productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}[$numberOfUnits][$unit] $transactionTs + $validInterval < $now ($validPurchase)",
|
||||
tag: PropertyTags.iap);
|
||||
}
|
||||
}
|
||||
final bool validPurchase = checkSubscriptionPeriod(purchased, productDetails);
|
||||
if (validPurchase) {
|
||||
Log.d(
|
||||
"[Restored Subscription] productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}",
|
||||
|
|
@ -408,8 +426,8 @@ class IapManager {
|
|||
final asset = newPurchasedStore.getAsset(productId);
|
||||
late OrderEntity newOrder;
|
||||
if (asset == null) {
|
||||
final product = await _createProduct(
|
||||
productId.createIntent(scene: "restore"), productDetails);
|
||||
final product =
|
||||
await _createProduct(productId.createIntent(scene: "restore"), productDetails);
|
||||
newOrder = product.createOrder().success();
|
||||
} else {
|
||||
newOrder = asset.order.success();
|
||||
|
|
@ -417,8 +435,7 @@ class IapManager {
|
|||
try {
|
||||
await GuruDB.instance.replaceOrderBySku(order: newOrder);
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("Failed to upsert order: $error $stacktrace",
|
||||
tag: PropertyTags.iap);
|
||||
Log.w("Failed to upsert order: $error $stacktrace", tag: PropertyTags.iap);
|
||||
}
|
||||
final newAsset = Asset(productId, newOrder);
|
||||
newPurchasedStore.addAsset(newAsset);
|
||||
|
|
@ -431,21 +448,30 @@ class IapManager {
|
|||
}
|
||||
|
||||
if (expiredSkus.isNotEmpty) {
|
||||
Log.i("expired orders:${expiredSkus.length}}");
|
||||
final graceCount = await AppProperty.getInstance().increaseGraceCount();
|
||||
Log.i("expired orders:${expiredSkus.length}} grace count: $graceCount");
|
||||
if (graceCount > GuruApp.instance.appSpec.deployment.subscriptionRestoreGraceCount) {
|
||||
try {
|
||||
await GuruDB.instance.deleteOrdersBySkus(expiredSkus);
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("Failed to upsert order: $error $stacktrace",
|
||||
tag: PropertyTags.iap);
|
||||
Log.w("Failed to upsert order: $error $stacktrace", tag: PropertyTags.iap);
|
||||
}
|
||||
await AppProperty.getInstance().resetGraceCount();
|
||||
GuruAnalytics.instance.logGuruEvent('dev_iap_audit', {
|
||||
"item_category": "expired",
|
||||
"item_name": "sub",
|
||||
"platform": Platform.isAndroid ? "Android" : "iOS",
|
||||
"value": graceCount
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await AppProperty.getInstance().resetGraceCount();
|
||||
}
|
||||
_iapStoreSubject.addEx(newPurchasedStore);
|
||||
Log.d(
|
||||
"[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}");
|
||||
Log.d("[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}");
|
||||
}
|
||||
|
||||
List<PurchaseDetails> buildLatestPurchasedPlanForIos(
|
||||
List<PurchaseDetails> purchaseDetails) {
|
||||
List<PurchaseDetails> buildLatestPurchasedPlanForIos(List<PurchaseDetails> purchaseDetails) {
|
||||
if (purchaseDetails.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -459,15 +485,14 @@ class IapManager {
|
|||
.toSet();
|
||||
Log.d("rawTransactionIds:$rawTransactionIds");
|
||||
final sortedPurchaseDetails = purchaseDetails.toList();
|
||||
sortedPurchaseDetails.sort((a, b) =>
|
||||
(int.tryParse(b.transactionDate ?? '') ?? 0)
|
||||
sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0)
|
||||
.compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
|
||||
sortedPurchaseDetails.retainWhere((details) {
|
||||
var detail = details as AppStorePurchaseDetails;
|
||||
Log.d(
|
||||
"checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}");
|
||||
return rawTransactionIds.remove(detail
|
||||
.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
||||
return rawTransactionIds
|
||||
.remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
||||
});
|
||||
|
||||
return sortedPurchaseDetails;
|
||||
|
|
@ -487,15 +512,14 @@ class IapManager {
|
|||
.toSet();
|
||||
Log.d("rawTransactionIds:$rawTransactionIds");
|
||||
final sortedPurchaseDetails = purchaseDetails.toList();
|
||||
sortedPurchaseDetails.sort((a, b) =>
|
||||
(int.tryParse(b.transactionDate ?? '') ?? 0)
|
||||
sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0)
|
||||
.compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
|
||||
sortedPurchaseDetails.retainWhere((details) {
|
||||
var detail = details as AppStorePurchaseDetails;
|
||||
Log.d(
|
||||
"checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}");
|
||||
return rawTransactionIds.remove(detail
|
||||
.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
||||
return rawTransactionIds
|
||||
.remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
||||
});
|
||||
|
||||
for (var details in sortedPurchaseDetails) {
|
||||
|
|
@ -509,8 +533,7 @@ class IapManager {
|
|||
}
|
||||
}
|
||||
|
||||
void _listenToPurchaseUpdated(
|
||||
List<PurchaseDetails> purchaseDetailsList) async {
|
||||
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) async {
|
||||
final List<Tuple2<ProductId, PurchaseDetails>> restoredIapPurchases = [];
|
||||
final List<Tuple2<ProductId, PurchaseDetails>> pendingCompletePurchase = [];
|
||||
final List<PurchaseDetails> subscriptionPurchases = [];
|
||||
|
|
@ -532,9 +555,7 @@ class IapManager {
|
|||
return;
|
||||
}
|
||||
for (var details in purchaseDetailsList) {
|
||||
final productId =
|
||||
GuruApp.instance.findProductId(sku: details.productID) ??
|
||||
ProductId.invalid;
|
||||
final productId = GuruApp.instance.findProductId(sku: details.productID) ?? ProductId.invalid;
|
||||
Log.d(
|
||||
"[details]: $productId [${details.productID}] ${details.purchaseID} ${details.status} ${details.pendingCompletePurchase}");
|
||||
GuruAnalytics.instance.logGuruEvent('dev_iap_update', {
|
||||
|
|
@ -557,8 +578,14 @@ class IapManager {
|
|||
|
||||
final productDetails = loadedProductDetails[productId];
|
||||
if (productDetails != null) {
|
||||
/// 如果是 IOS的 purchased订单,并且是订阅的订单,他又没在当前请求的列表中,证明他是一个恢复的订单
|
||||
if (Platform.isIOS && productId.isSubscription && !iapRequestMap.containsKey(productId)) {
|
||||
subscriptionPurchases.add(details);
|
||||
existsRestored = true;
|
||||
} else {
|
||||
await _completePurchase(productId, productDetails, details);
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("completePurchase ${details.productID} ${details.purchaseID}");
|
||||
break;
|
||||
|
|
@ -574,9 +601,8 @@ class IapManager {
|
|||
}
|
||||
// 如果是未完成的商品或是恢复出了消耗品,都需要手动完成
|
||||
if (Platform.isAndroid) {
|
||||
final originPurchaseState = (details as GooglePlayPurchaseDetails)
|
||||
.billingClientPurchase
|
||||
.purchaseState;
|
||||
final originPurchaseState =
|
||||
(details as GooglePlayPurchaseDetails).billingClientPurchase.purchaseState;
|
||||
Log.d(
|
||||
"restore android ${details.pendingCompletePurchase} $productId $originPurchaseState");
|
||||
if (originPurchaseState == PurchaseStateWrapper.purchased) {
|
||||
|
|
@ -614,8 +640,7 @@ class IapManager {
|
|||
if (existsRestored) {
|
||||
if (pendingCompletePurchase.isNotEmpty) {
|
||||
await completeAllPurchases(pendingCompletePurchase);
|
||||
Log.d("manual complete/consume all purchases!",
|
||||
syncFirebase: true, syncCrashlytics: true);
|
||||
Log.d("manual complete/consume all purchases!", syncFirebase: true, syncCrashlytics: true);
|
||||
}
|
||||
|
||||
if (restoredIapPurchases.isNotEmpty) {
|
||||
|
|
@ -659,8 +684,8 @@ class IapManager {
|
|||
upsertOrders.add(newOrder);
|
||||
}
|
||||
} else if (productDetails != null) {
|
||||
final product = await _createProduct(
|
||||
productId.createIntent(scene: "restore"), productDetails);
|
||||
final product =
|
||||
await _createProduct(productId.createIntent(scene: "restore"), productDetails);
|
||||
final newOrder = product.createOrder().success();
|
||||
upsertOrders.add(newOrder);
|
||||
}
|
||||
|
|
@ -671,20 +696,17 @@ class IapManager {
|
|||
await GuruDB.instance.upsertOrders(upsertOrders);
|
||||
updatedOrder.addAll(upsertOrders);
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("upsertOrders error:$error $stacktrace",
|
||||
syncCrashlytics: true, syncFirebase: true);
|
||||
Log.w("upsertOrders error:$error $stacktrace", syncCrashlytics: true, syncFirebase: true);
|
||||
for (var order in upsertOrders) {
|
||||
try {
|
||||
await GuruDB.instance.upsertOrder(order: order);
|
||||
updatedOrder.add(order);
|
||||
} catch (error1, stacktrace1) {
|
||||
Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1",
|
||||
syncFirebase: true);
|
||||
Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1", syncFirebase: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
final assets =
|
||||
updatedOrder.map((order) => Asset(order.productId, order)).toList();
|
||||
final assets = updatedOrder.map((order) => Asset(order.productId, order)).toList();
|
||||
newPurchased.addAllAssets(assets);
|
||||
}
|
||||
_iapStoreSubject.addEx(newPurchased);
|
||||
|
|
@ -692,16 +714,13 @@ class IapManager {
|
|||
}
|
||||
|
||||
Future reportFailedOrders() async {
|
||||
final failedIapOrders =
|
||||
await AppProperty.getInstance().loadAllFailedIapOrders();
|
||||
final failedIapOrders = await AppProperty.getInstance().loadAllFailedIapOrders();
|
||||
failedIapOrders.forEach((key, value) async {
|
||||
try {
|
||||
final order = OrdersReport.fromJson(json.decode(value));
|
||||
final result = await GuruApi.instance.reportOrders(order);
|
||||
if (result.usdPrice > 0) {
|
||||
logRevenue(
|
||||
result.usdPrice, order.productId ?? order.subscriptionId);
|
||||
}
|
||||
// 这里不管返回什么值,都认为是成功的, 只有崩溃才会返回失败
|
||||
await logRevenue(order, result);
|
||||
AppProperty.getInstance().removeReportSuccessOrder(key);
|
||||
} catch (error, stacktrace) {}
|
||||
});
|
||||
|
|
@ -709,8 +728,7 @@ class IapManager {
|
|||
}
|
||||
|
||||
String buildGooglePlayDetailsString(
|
||||
GooglePlayProductDetails googlePlayProduct,
|
||||
GooglePlayPurchaseDetails googlePlayDetails) {
|
||||
GooglePlayProductDetails googlePlayProduct, GooglePlayPurchaseDetails googlePlayDetails) {
|
||||
final StringBuffer sb = StringBuffer();
|
||||
sb.writeln("#### purchase ####");
|
||||
|
||||
|
|
@ -742,15 +760,12 @@ class IapManager {
|
|||
if (oneTimeDetails != null) {
|
||||
sb.writeln(" => oneTimeDetails:");
|
||||
sb.writeln(" - formattedPrice: ${oneTimeDetails.formattedPrice}");
|
||||
sb.writeln(
|
||||
" - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}");
|
||||
sb.writeln(
|
||||
" - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}");
|
||||
sb.writeln(" - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}");
|
||||
sb.writeln(" - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}");
|
||||
}
|
||||
|
||||
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
||||
if (subscriptionOfferDetails != null &&
|
||||
subscriptionOfferDetails.isNotEmpty) {
|
||||
if (subscriptionOfferDetails != null && subscriptionOfferDetails.isNotEmpty) {
|
||||
for (var offer in subscriptionOfferDetails) {
|
||||
sb.writeln(" => sub offer: ${offer.offerId}");
|
||||
sb.writeln(" - basePlanId: ${offer.basePlanId}");
|
||||
|
|
@ -773,13 +788,12 @@ class IapManager {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
Future reportOrders(ProductId productId, ProductDetails details,
|
||||
PurchaseDetails purchaseDetails, OrderEntity? order) async {
|
||||
Future reportOrders(ProductId productId, ProductDetails details, PurchaseDetails purchaseDetails,
|
||||
OrderEntity? order) async {
|
||||
final OrdersReport ordersReport = OrdersReport();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
ordersReport.token =
|
||||
purchaseDetails.verificationData.serverVerificationData;
|
||||
ordersReport.token = purchaseDetails.verificationData.serverVerificationData;
|
||||
ordersReport.packageName = GuruApp.instance.details.packageName;
|
||||
final manifest = order?.manifest;
|
||||
final basePlanId = manifest?.basePlanId;
|
||||
|
|
@ -789,16 +803,13 @@ class IapManager {
|
|||
ordersReport.offerId = offerId;
|
||||
}
|
||||
try {
|
||||
GooglePlayPurchaseDetails googlePlayDetails =
|
||||
purchaseDetails as GooglePlayPurchaseDetails;
|
||||
GooglePlayProductDetails googlePlayProduct =
|
||||
details as GooglePlayProductDetails;
|
||||
GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails;
|
||||
GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails;
|
||||
Log.d(
|
||||
"Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}");
|
||||
} catch (error, stacktrace) {}
|
||||
} else if (Platform.isIOS) {
|
||||
AppStorePurchaseDetails appleDetails =
|
||||
purchaseDetails as AppStorePurchaseDetails;
|
||||
AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails;
|
||||
AppStoreProductDetails appleProduct = details as AppStoreProductDetails;
|
||||
final StringBuffer sb = StringBuffer();
|
||||
sb.writeln("#### purchase ####");
|
||||
|
|
@ -810,31 +821,25 @@ class IapManager {
|
|||
sb.writeln("skPaymentTransaction:");
|
||||
sb.writeln(
|
||||
" =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}");
|
||||
sb.writeln(
|
||||
" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
||||
sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
||||
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:");
|
||||
sb.writeln(
|
||||
" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
||||
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
||||
sb.writeln("\n#### product ####");
|
||||
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
||||
sb.writeln("rawPrice: ${appleProduct.rawPrice}");
|
||||
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
||||
sb.writeln("currencyCode skProduct");
|
||||
sb.writeln(
|
||||
" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
||||
sb.writeln(
|
||||
" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
||||
sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
||||
sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
||||
sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}");
|
||||
sb.writeln(
|
||||
" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
||||
sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
||||
sb.writeln(
|
||||
" =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}");
|
||||
sb.writeln(" =>appleProduct.skProduct.priceLocale");
|
||||
sb.writeln(" ->{appleProduct.skProduct.priceLocale}");
|
||||
|
||||
ordersReport.bundleId = GuruApp.instance.appSpec.details.bundleId;
|
||||
ordersReport.receipt =
|
||||
purchaseDetails.verificationData.serverVerificationData;
|
||||
ordersReport.receipt = purchaseDetails.verificationData.serverVerificationData;
|
||||
ordersReport.sku = appleDetails.productID;
|
||||
ordersReport.countryCode = appleProduct.skProduct.priceLocale.countryCode;
|
||||
Log.d("IOS Product/Purchase ${sb.toString()}");
|
||||
|
|
@ -847,40 +852,66 @@ class IapManager {
|
|||
ordersReport.orderType = OrderType.inapp;
|
||||
ordersReport.productId = details.id;
|
||||
}
|
||||
ordersReport.orderId = purchaseDetails.purchaseID;
|
||||
ordersReport.price = details.rawPrice.toString();
|
||||
ordersReport.currency = details.currencyCode;
|
||||
|
||||
ordersReport.orderUserInfo =
|
||||
OrderUserInfo(GuruSettings.instance.bestLevel.get().toString());
|
||||
ordersReport.orderUserInfo = OrderUserInfo(GuruSettings.instance.bestLevel.get().toString());
|
||||
ordersReport.userIdentification = GuruAnalytics.instance.userIdentification;
|
||||
|
||||
final transactionDate = purchaseDetails.transactionDate;
|
||||
if (transactionDate != null && transactionDate.isNotEmpty) {
|
||||
try {
|
||||
final ts = int.tryParse(transactionDate) ?? DateTimeUtils.currentTimeInMillis();
|
||||
ordersReport.transactionDate = ts;
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("parse transactionDate error! $error", stackTrace: stacktrace);
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("orderReport:$ordersReport", tag: "Iap");
|
||||
try {
|
||||
final result = await GuruApi.instance.reportOrders(ordersReport);
|
||||
if ((result.usdPrice > 0) ||
|
||||
(result.usdPrice == 0 && result.isTestOrder)) {
|
||||
logRevenue(result.usdPrice, purchaseDetails.productID);
|
||||
Log.i("reportOrders success! $result");
|
||||
// 这里不管返回什么值,都认为是成功的
|
||||
await logRevenue(ordersReport, result);
|
||||
return;
|
||||
}
|
||||
Log.i("ignoreInvalidResult $result", tag: "Iap");
|
||||
} catch (error, stacktrace) {
|
||||
Log.i("reportOrders error!", error: error, stackTrace: stacktrace);
|
||||
}
|
||||
AppProperty.getInstance().saveFailedIapOrders(ordersReport);
|
||||
}
|
||||
|
||||
Future logRevenue(double usdPrice, String? sku) async {
|
||||
Future<bool> logRevenue(OrdersReport order, OrdersResponse result) async {
|
||||
final isSubscription = order.orderType == OrderType.subs;
|
||||
final sku = (isSubscription ? order.subscriptionId : order.productId) ??
|
||||
(order.productId ?? order.subscriptionId ?? order.sku);
|
||||
final usdPrice = result.usdPrice;
|
||||
if (sku == null || sku.isEmpty) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!result.isTestOrder && usdPrice <= 0) {
|
||||
Log.i("ignoreInvalidResult $result", tag: "Iap");
|
||||
return false;
|
||||
}
|
||||
Log.i("prepare logRevenue! $result $sku");
|
||||
|
||||
final platform = Platform.isIOS ? "appstore" : "google_play";
|
||||
GuruAnalytics.instance.logAdRevenue(usdPrice, platform, "USD");
|
||||
final productId =
|
||||
GuruApp.instance.findProductId(sku: sku) ?? ProductId.invalid;
|
||||
GuruAnalytics.instance.logPurchase(usdPrice,
|
||||
currency: 'USD', contentId: sku, adPlatform: platform);
|
||||
if (productId.isSubscription) {
|
||||
if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 2) {
|
||||
GuruAnalytics.instance.logAdRevenue020(usdPrice, platform, "USD",
|
||||
orderId: order.orderId,
|
||||
orderType: isSubscription ? "SUB" : "IAP",
|
||||
productId: sku,
|
||||
transactionDate: order.transactionDate);
|
||||
} else {
|
||||
GuruAnalytics.instance.logAdRevenue(usdPrice, platform, "USD",
|
||||
orderId: order.orderId,
|
||||
orderType: isSubscription ? "SUB" : "IAP",
|
||||
productId: sku,
|
||||
transactionDate: order.transactionDate);
|
||||
}
|
||||
GuruAnalytics.instance
|
||||
.logPurchase(usdPrice, currency: 'USD', contentId: sku, adPlatform: platform);
|
||||
if (isSubscription) {
|
||||
GuruAnalytics.instance.logEvent(
|
||||
"sub_purchase",
|
||||
{
|
||||
|
|
@ -888,6 +919,9 @@ class IapManager {
|
|||
"currency": "USD",
|
||||
"revenue": usdPrice,
|
||||
"product_id": sku,
|
||||
"order_type": "SUB",
|
||||
"order_id": order.orderId,
|
||||
"trans_ts": order.transactionDate
|
||||
},
|
||||
options: iapRevenueAppEventOptions);
|
||||
} else {
|
||||
|
|
@ -898,11 +932,16 @@ class IapManager {
|
|||
"currency": "USD",
|
||||
"revenue": usdPrice,
|
||||
"product_id": sku,
|
||||
"order_type": "IAP",
|
||||
"order_id": order.orderId,
|
||||
"trans_ts": order.transactionDate
|
||||
},
|
||||
options: iapRevenueAppEventOptions);
|
||||
}
|
||||
GuruAnalytics.instance.logGuruEvent("dev_iap_action",
|
||||
{"item_category": "reported", "item_name": sku, "result": "true"});
|
||||
GuruAnalytics.instance.logGuruEvent(
|
||||
"dev_iap_action", {"item_category": "reported", "item_name": sku, "result": "true"});
|
||||
Log.i("reportOrders completed! logRevenue success! $result $sku");
|
||||
return true;
|
||||
}
|
||||
|
||||
Future _deliverManifest(ProductId productId, Manifest manifest) async {
|
||||
|
|
@ -912,8 +951,7 @@ class IapManager {
|
|||
result = await ManifestManager.instance
|
||||
.deliver(manifest, TransactionMethod.iap)
|
||||
.catchError((error) {
|
||||
Log.w("applyManifest error:$error",
|
||||
syncCrashlytics: true, syncFirebase: true);
|
||||
Log.w("applyManifest error:$error", syncCrashlytics: true, syncFirebase: true);
|
||||
});
|
||||
} catch (error, stacktrace) {
|
||||
cause = error.toString();
|
||||
|
|
@ -991,8 +1029,7 @@ class IapManager {
|
|||
await _inAppPurchase.completePurchase(details);
|
||||
final count = await AppProperty.getInstance().increaseAndGetIapCount();
|
||||
GuruAnalytics.instance.setUserProperty("purchase_count", count.toString());
|
||||
Log.d("_completePurchase $productId ${details.pendingCompletePurchase}",
|
||||
tag: "Iap");
|
||||
Log.d("_completePurchase $productId ${details.pendingCompletePurchase}", tag: "Iap");
|
||||
OrderEntity? resultOrder;
|
||||
|
||||
IapRequest? iapRequest = iapRequestMap.remove(productId);
|
||||
|
|
@ -1075,27 +1112,24 @@ class IapManager {
|
|||
|
||||
await appProperty.getAndIncrease(PropertyKeys.subscriptionCount);
|
||||
if (group != null) {
|
||||
await appProperty
|
||||
.getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group));
|
||||
await appProperty.getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group));
|
||||
}
|
||||
await appProperty
|
||||
.getAndIncrease(PropertyKeys.buildSubscriptionCount(productId));
|
||||
await appProperty.getAndIncrease(PropertyKeys.buildSubscriptionCount(productId));
|
||||
}
|
||||
|
||||
Future<Manifest> createPurchaseManifest(TransactionIntent intent) {
|
||||
return ManifestManager.instance.createManifest(intent);
|
||||
}
|
||||
|
||||
Future<ProductDetails?> checkAndDistributeOfferDetails(ProductId productId,
|
||||
ProductDetails? details, EligibilityCriteria eligibilityCriteria) async {
|
||||
Future<ProductDetails?> checkAndDistributeOfferDetails(
|
||||
ProductId productId, ProductDetails? details, EligibilityCriteria eligibilityCriteria) async {
|
||||
Log.d("checkAndDistributeOfferDetails $productId $eligibilityCriteria");
|
||||
switch (eligibilityCriteria) {
|
||||
case EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup:
|
||||
final group = GuruApp.instance.appSpec.productProfile.group(productId);
|
||||
if (group != null) {
|
||||
final key = PropertyKeys.buildGroupSubscriptionCount(group);
|
||||
final count =
|
||||
await AppProperty.getInstance().getInt(key, defValue: 0);
|
||||
final count = await AppProperty.getInstance().getInt(key, defValue: 0);
|
||||
Log.d(" ==> $key $count");
|
||||
return count > 0 ? null : details;
|
||||
}
|
||||
|
|
@ -1107,8 +1141,8 @@ class IapManager {
|
|||
Log.d(" ==> $key $count");
|
||||
return count > 0 ? null : details;
|
||||
case EligibilityCriteria.newCustomerNeverHadAnySubscription:
|
||||
final count = await AppProperty.getInstance()
|
||||
.getInt(PropertyKeys.subscriptionCount, defValue: 0);
|
||||
final count =
|
||||
await AppProperty.getInstance().getInt(PropertyKeys.subscriptionCount, defValue: 0);
|
||||
Log.d(" ==> subscriptionCount $count");
|
||||
return count > 0 ? null : details;
|
||||
default:
|
||||
|
|
@ -1117,21 +1151,17 @@ class IapManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<IapProduct> _createProduct(
|
||||
TransactionIntent intent, ProductDetails details) async {
|
||||
Future<IapProduct> _createProduct(TransactionIntent intent, ProductDetails details) async {
|
||||
final productId = intent.productId;
|
||||
Manifest manifest = await ManifestManager.instance.createManifest(intent);
|
||||
Log.d("createProduct ${productId.sku} ${productId.hasBasePlan}");
|
||||
ProductDetails baseDetails = details;
|
||||
ProductDetails? offerDetails;
|
||||
if (Platform.isAndroid &&
|
||||
productId.isSubscription &&
|
||||
productId.hasBasePlan) {
|
||||
if (Platform.isAndroid && productId.isSubscription && productId.hasBasePlan) {
|
||||
final googlePlayProductDetails = details as GooglePlayProductDetails;
|
||||
final productDetails = googlePlayProductDetails.productDetails;
|
||||
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
||||
final offerProductDetails =
|
||||
GooglePlayProductDetails.fromProductDetails(productDetails);
|
||||
final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails);
|
||||
final expectBasePlan = productId.basePlan;
|
||||
final expectOfferId = productId.offerId;
|
||||
Log.d(
|
||||
|
|
@ -1161,12 +1191,10 @@ class IapManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
return Product.iap(productId, baseDetails, manifest,
|
||||
offerDetails: offerDetails) as IapProduct;
|
||||
return Product.iap(productId, baseDetails, manifest, offerDetails: offerDetails) as IapProduct;
|
||||
}
|
||||
|
||||
Future<ProductStore<IapProduct>> buildProducts(
|
||||
Set<TransactionIntent> intents) async {
|
||||
Future<ProductStore<IapProduct>> buildProducts(Set<TransactionIntent> intents) async {
|
||||
ProductStore<IapProduct> iapStore = ProductStore();
|
||||
final _productDetails = loadedProductDetails;
|
||||
for (var intent in intents) {
|
||||
|
|
@ -1180,8 +1208,8 @@ class IapManager {
|
|||
final product = await _createProduct(intent, details);
|
||||
iapStore.putProduct(product);
|
||||
if (intent.productId.hasOffer && !iapStore.existsProduct(productId)) {
|
||||
final originProduct = await _createProduct(
|
||||
productId.createIntent(scene: intent.scene), details);
|
||||
final originProduct =
|
||||
await _createProduct(productId.createIntent(scene: intent.scene), details);
|
||||
iapStore.putProduct(originProduct);
|
||||
}
|
||||
}
|
||||
|
|
@ -1222,8 +1250,7 @@ class IapManager {
|
|||
result = await _inAppPurchase.buyNonConsumable(purchaseParam: param);
|
||||
}
|
||||
if (!result) {
|
||||
Log.d(
|
||||
"_requestPurchases error! ${product.productId} ${product.details.price}",
|
||||
Log.d("_requestPurchases error! ${product.productId} ${product.details.price}",
|
||||
syncFirebase: true);
|
||||
GuruAnalytics.instance.logGuruEvent("dev_iap_action", {
|
||||
"item_category": "request",
|
||||
|
|
@ -1252,13 +1279,12 @@ class IapManager {
|
|||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase
|
||||
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
||||
_inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||
final response = await androidAddition.queryPastPurchases();
|
||||
for (var purchase in response.pastPurchases) {
|
||||
androidAddition.consumePurchase(purchase);
|
||||
Log.w(
|
||||
"[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}");
|
||||
Log.w("[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}");
|
||||
_inAppPurchase.completePurchase(purchase);
|
||||
}
|
||||
|
||||
|
|
@ -1270,30 +1296,26 @@ class IapManager {
|
|||
Future manualConsumePurchase(PurchaseDetails purchase) async {
|
||||
if (Platform.isAndroid) {
|
||||
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
||||
_inAppPurchase
|
||||
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||
_inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||
await androidAddition.consumePurchase(purchase);
|
||||
_inAppPurchase.completePurchase(purchase);
|
||||
await GuruDB.instance.deletePendingOrderBySku(sku: purchase.productID);
|
||||
}
|
||||
}
|
||||
|
||||
Future manualConsumeAllPurchases(
|
||||
List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
||||
Future manualConsumeAllPurchases(List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
||||
for (var tuple in tuples) {
|
||||
try {
|
||||
final productId = tuple.item1;
|
||||
final purchase = tuple.item2;
|
||||
await manualConsumePurchase(purchase);
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("consumePurchase error! $error",
|
||||
stackTrace: stacktrace, syncFirebase: true);
|
||||
Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future completeAllPurchases(
|
||||
List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
||||
Future completeAllPurchases(List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
||||
for (var tuple in tuples) {
|
||||
try {
|
||||
final productId = tuple.item1;
|
||||
|
|
@ -1306,8 +1328,7 @@ class IapManager {
|
|||
"item_name": productId.sku,
|
||||
"result": "true",
|
||||
});
|
||||
final order =
|
||||
await _completePurchase(productId, productDetails, details);
|
||||
final order = await _completePurchase(productId, productDetails, details);
|
||||
} else {
|
||||
GuruAnalytics.instance.logGuruEvent("dev_iap_action", {
|
||||
"item_category": "pending_consume",
|
||||
|
|
@ -1325,8 +1346,7 @@ class IapManager {
|
|||
}
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("consumePurchase error! $error",
|
||||
stackTrace: stacktrace, syncFirebase: true);
|
||||
Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1339,13 +1359,10 @@ class IapManager {
|
|||
// }
|
||||
|
||||
Map<String, ProductId> _filterProductSkus(
|
||||
{required Set<ProductId> ids,
|
||||
required Set<int> attrs,
|
||||
Set<ProductId>? validIds}) {
|
||||
{required Set<ProductId> ids, required Set<int> attrs, Set<ProductId>? validIds}) {
|
||||
final List<MapEntry<String, ProductId>> entries = ids
|
||||
.where((productId) =>
|
||||
(validIds?.contains(productId) != false) &&
|
||||
attrs.contains(productId.attr))
|
||||
(validIds?.contains(productId) != false) && attrs.contains(productId.attr))
|
||||
.map((productId) => MapEntry(productId.sku, productId))
|
||||
.toList();
|
||||
return Map.fromEntries(entries);
|
||||
|
|
@ -1393,16 +1410,13 @@ class IapManager {
|
|||
}
|
||||
|
||||
final queryProductIds = queryOneOffChargeSkuMap.keys.toSet();
|
||||
queryProductIds.addAll(
|
||||
GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku));
|
||||
queryProductIds.addAll(GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku));
|
||||
Log.d("refresh product:", tag: "IAP");
|
||||
for (var productId in queryProductIds) {
|
||||
Log.d(" => $productId", tag: "IAP");
|
||||
}
|
||||
final response =
|
||||
await _queryProducts(queryProductIds).catchError((error, stacktrace) {
|
||||
Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace",
|
||||
tag: "IAP");
|
||||
final response = await _queryProducts(queryProductIds).catchError((error, stacktrace) {
|
||||
Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace", tag: "IAP");
|
||||
return _emptyResponse;
|
||||
});
|
||||
Log.i("refreshProduct COMPLETED:", tag: "IAP");
|
||||
|
|
@ -1419,8 +1433,8 @@ class IapManager {
|
|||
detailsMap.addAll(extractProducts(details));
|
||||
}
|
||||
|
||||
GuruAnalytics.instance.logGuruEvent(
|
||||
"dev_iap_action", {"item_category": "load", "result": "true"});
|
||||
GuruAnalytics.instance
|
||||
.logGuruEvent("dev_iap_action", {"item_category": "load", "result": "true"});
|
||||
final newProductDetails = Map.of(loadedProductDetails);
|
||||
newProductDetails.addAll(detailsMap);
|
||||
_productDetailsSubject.addEx(newProductDetails);
|
||||
|
|
@ -1440,8 +1454,7 @@ class IapManager {
|
|||
final googlePlayProductDetails = details as GooglePlayProductDetails;
|
||||
final productDetails = googlePlayProductDetails.productDetails;
|
||||
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
||||
final offerProductDetails =
|
||||
GooglePlayProductDetails.fromProductDetails(productDetails);
|
||||
final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails);
|
||||
for (var id in ids) {
|
||||
final expectBasePlan = id.basePlan;
|
||||
final expectOfferId = id.offerId;
|
||||
|
|
@ -1452,8 +1465,7 @@ class IapManager {
|
|||
final offer = subscriptionOfferDetails[i];
|
||||
Log.d(
|
||||
"$i expectOfferId:$expectOfferId offerId:${offer.offerId} expectBasePlan:$expectBasePlan basePlanId:${offer.basePlanId}");
|
||||
if (expectBasePlan != offer.basePlanId ||
|
||||
expectOfferId != offer.offerId) {
|
||||
if (expectBasePlan != offer.basePlanId || expectOfferId != offer.offerId) {
|
||||
continue;
|
||||
}
|
||||
detailsMap[id] = offerProductDetails[i];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import 'package:guru_app/analytics/guru_analytics.dart';
|
||||
import 'package:guru_app/database/guru_db.dart';
|
||||
import 'package:guru_app/financial/asset/assets_model.dart';
|
||||
import 'package:guru_app/financial/asset/assets_store.dart';
|
||||
import 'package:guru_app/financial/data/db/order_database.dart';
|
||||
import 'package:guru_app/financial/igb/igb_product.dart';
|
||||
import 'package:guru_app/financial/igc/igc_model.dart';
|
||||
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
||||
import 'package:guru_app/financial/product/product_model.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_utils/extensions/extensions.dart';
|
||||
import 'package:guru_utils/log/log.dart';
|
||||
import 'package:guru_utils/property/app_property.dart';
|
||||
|
||||
/// Created by Haoyi on 2023/2/18
|
||||
|
||||
class IgbManager {
|
||||
static final IgbManager _instance = IgbManager._();
|
||||
|
||||
static IgbManager get instance => _instance;
|
||||
|
||||
IgbManager._();
|
||||
|
||||
Future init() async {
|
||||
await InventoryManager.instance.init();
|
||||
}
|
||||
|
||||
Future reloadAssets() async {}
|
||||
|
||||
Future<bool> accumulate(int igc, TransactionMethod method, {String? scene}) async {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future clear() async {}
|
||||
|
||||
Future<IgbProduct> buildIgbProduct(TransactionIntent intent) async {
|
||||
final manifest = await ManifestManager.instance.createManifest(intent);
|
||||
return IgbProduct(intent.productId, manifest, intent.igbCost);
|
||||
}
|
||||
|
||||
Future<bool> redeem(IgbProduct product) async {
|
||||
Log.v("Igb buy");
|
||||
final result = await InventoryManager.instance.consume(product.cost, product.manifest);
|
||||
if (result) {
|
||||
await ManifestManager.instance.deliver(product.manifest, TransactionMethod.igb);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:guru_app/financial/data/db/order_database.dart';
|
||||
import 'package:guru_app/financial/product/product_model.dart';
|
||||
import 'package:guru_app/inventory/db/inventory_database.dart';
|
||||
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||
import 'package:guru_utils/id/id_utils.dart';
|
||||
import 'package:guru_utils/manifest/manifest.dart';
|
||||
|
||||
// In-game barter Product
|
||||
class IgbProduct implements Product {
|
||||
@override
|
||||
final ProductId productId;
|
||||
|
||||
@override
|
||||
final Manifest manifest;
|
||||
|
||||
final List<StockItem> cost;
|
||||
|
||||
String get sku => productId.sku;
|
||||
|
||||
IgbProduct(this.productId, this.manifest, this.cost);
|
||||
|
||||
bool isConsumable() {
|
||||
return productId.isConsumable;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StockProduct{productId: $productId}';
|
||||
}
|
||||
|
||||
@override
|
||||
OrderEntity createOrder() {
|
||||
return OrderEntity(
|
||||
orderId: IdUtils.uuidV4(),
|
||||
sku: productId.sku,
|
||||
state: TransactionState.success,
|
||||
attr: productId.attr,
|
||||
method: TransactionMethod.igc.index,
|
||||
currency: TransactionCurrency.igc,
|
||||
cost: 1,
|
||||
category: manifest.category,
|
||||
timestamp: DateTimeUtils.currentTimeInMillis(),
|
||||
manifest: manifest);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,10 @@ class IgcManager {
|
|||
await reloadAssets();
|
||||
}
|
||||
|
||||
Future switchSession() async {
|
||||
init();
|
||||
}
|
||||
|
||||
Future reloadAssets() async {
|
||||
final orders = await GuruDB.instance
|
||||
.selectOrders(method: TransactionMethod.igc, attrs: [TransactionAttributes.asset]);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ import 'dart:async';
|
|||
|
||||
import 'package:guru_app/financial/igc/igc_manager.dart';
|
||||
import 'package:guru_app/financial/product/product_model.dart';
|
||||
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||
import 'manifest.dart';
|
||||
|
||||
/// Created by Haoyi on 2022/8/21
|
||||
|
||||
typedef DetailsDistributor = Future<bool> Function(Details, TransactionMethod, String scene);
|
||||
// typedef DetailsDistributor = Future<bool> Function(Details, TransactionMethod, String scene);
|
||||
|
||||
typedef DetailsDistributor = Future<List<StockItem>> Function(
|
||||
Details, TransactionMethod, String scene);
|
||||
|
||||
typedef ManifestBuilder = Future<Manifest?> Function(TransactionIntent);
|
||||
|
||||
|
|
@ -27,15 +31,31 @@ class ManifestManager {
|
|||
|
||||
final List<ManifestBuilder> builders = [];
|
||||
|
||||
static Future<bool> _deliverIgcDetails(
|
||||
static Future<List<StockItem>> _deliverIgcDetails(
|
||||
Details details, TransactionMethod method, String scene) async {
|
||||
if (details.amount > 0) {
|
||||
IgcManager.instance.accumulate(details.amount, method, scene: scene);
|
||||
return true;
|
||||
await IgcManager.instance.accumulate(details.amount, method, scene: scene);
|
||||
}
|
||||
return false;
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<List<StockItem>> _deliverDefaultDetails(
|
||||
Details details, TransactionMethod method, String scene) async {
|
||||
if (details.amount > 0) {
|
||||
return [StockItem.fromDetails(details)];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// static Future<bool> _deliverDetails(
|
||||
// Details details, TransactionMethod method, String scene) async {
|
||||
// final stock = StockItem.fromDetails(details);
|
||||
// if (stock.amount > 0) {
|
||||
// await InventoryManager.instance.acquire([stock], method, scene);
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
|
||||
void addDistributor(String type, DetailsDistributor distributor) {
|
||||
distributors[type] = distributor;
|
||||
}
|
||||
|
|
@ -48,11 +68,55 @@ class ManifestManager {
|
|||
this.builders.addAll(builders);
|
||||
}
|
||||
|
||||
Future _acquire(List<StockItem> items, TransactionMethod method, Manifest manifest) async {
|
||||
if (items.isNotEmpty) {
|
||||
String specific = "";
|
||||
switch (method) {
|
||||
case TransactionMethod.iap:
|
||||
specific = manifest.contentId;
|
||||
break;
|
||||
case TransactionMethod.igc:
|
||||
specific = "coin";
|
||||
break;
|
||||
case TransactionMethod.igb:
|
||||
specific = manifest.barterId;
|
||||
break;
|
||||
default:
|
||||
specific = manifest.scene;
|
||||
break;
|
||||
}
|
||||
await InventoryManager.instance.acquire(items, method, specific, scene: manifest.scene);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<StockItem>> deliverStockItems(
|
||||
List<StockItem> items, Manifest manifest, TransactionMethod method) async {
|
||||
final List<StockItem> unsold = [];
|
||||
for (var item in items) {
|
||||
if (item.sku == DetailsReservedType.igc) {
|
||||
await distributors[DetailsReservedType.igc]
|
||||
?.call(Details.define(DetailsReservedType.igc, item.amount), method, manifest.scene);
|
||||
continue;
|
||||
}
|
||||
unsold.add(item);
|
||||
}
|
||||
return unsold;
|
||||
}
|
||||
|
||||
Future<bool> deliver(Manifest manifest, TransactionMethod method) async {
|
||||
bool result = false;
|
||||
final List<StockItem> unsold = [];
|
||||
for (var details in manifest.details) {
|
||||
result |= await distributors[details.type]?.call(details, method, manifest.scene) ?? false;
|
||||
final items = await distributors[details.type]?.call(details, method, manifest.scene) ??
|
||||
await _deliverDefaultDetails(details, method, manifest.scene);
|
||||
final unsoldItems = await deliverStockItems(items, manifest, method);
|
||||
unsold.addAll(unsoldItems);
|
||||
result |= unsoldItems.isEmpty;
|
||||
}
|
||||
if (unsold.isNotEmpty) {
|
||||
await _acquire(unsold, method, manifest);
|
||||
}
|
||||
|
||||
deliveredManifestStream.add(manifest);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -74,4 +138,12 @@ class ManifestManager {
|
|||
final extras = <String, dynamic>{ExtraReservedField.scene: scene};
|
||||
return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details);
|
||||
}
|
||||
|
||||
// Manifest createIgbManifest(int igc, {String? category, String scene = ""}) {
|
||||
// final details = <Details>[];
|
||||
// details.add(Details.define(DetailsReservedType.igc, igc));
|
||||
//
|
||||
// final extras = <String, dynamic>{ExtraReservedField.scene: scene};
|
||||
// return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details);
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:guru_app/financial/data/db/order_database.dart';
|
||||
import 'package:guru_app/financial/igb/igb_product.dart';
|
||||
import 'package:guru_app/financial/manifest/manifest.dart';
|
||||
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||
import 'package:guru_utils/hash/hash.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:guru_app/financial/iap/iap_model.dart';
|
||||
|
|
@ -51,9 +53,9 @@ class TransactionAttributes {
|
|||
|
||||
// Offer products for sale in your app for a one-off charge
|
||||
@deprecated
|
||||
static const possessive = 1;
|
||||
static const asset = 1;
|
||||
static const consumable = 2;
|
||||
static const possessive = DetailsAttr.permanent;
|
||||
static const asset = DetailsAttr.permanent;
|
||||
static const consumable = DetailsAttr.consumable;
|
||||
|
||||
static const Set<int> oneOffChargeAttributes = <int>{asset, consumable};
|
||||
|
||||
|
|
@ -174,6 +176,7 @@ class ProductId {
|
|||
TransactionIntent createIntent(
|
||||
{required String scene,
|
||||
int igcCost = 0,
|
||||
List<StockItem> igbCost = const <StockItem>[],
|
||||
bool sales = false,
|
||||
double rate = 1.0,
|
||||
EligibilityCriteria eligibilityCriteria =
|
||||
|
|
@ -193,6 +196,13 @@ class ProductId {
|
|||
final manifest = await ManifestManager.instance.createManifest(intent);
|
||||
return IgcProduct(this, manifest, igcCost);
|
||||
}
|
||||
|
||||
Future<IgbProduct> createIgbProduct(List<StockItem> igbCost, String scene,
|
||||
{Manifest? specified}) async {
|
||||
final manifest = specified ??
|
||||
await ManifestManager.instance.createManifest(createIntent(scene: scene, igbCost: igbCost));
|
||||
return IgbProduct(this, manifest, igbCost);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Product {
|
||||
|
|
@ -207,12 +217,14 @@ abstract class Product {
|
|||
|
||||
factory Product.reward(ProductId productId, Manifest manifest) = RewardProduct;
|
||||
|
||||
factory Product.igb(ProductId productId, Manifest manifest, List<StockItem> cost) = IgbProduct;
|
||||
|
||||
//
|
||||
// factory Product.gems(ProductId productId, int price, Manifest manifest) = GemProduct;
|
||||
//
|
||||
// factory Product.reward(Reward reward) = RewardProduct;
|
||||
//
|
||||
OrderEntity createOrder();
|
||||
// OrderEntity createOrder();
|
||||
}
|
||||
|
||||
class TransactionState {
|
||||
|
|
@ -223,11 +235,17 @@ class TransactionState {
|
|||
static const expired = -3;
|
||||
}
|
||||
|
||||
// 交易方式
|
||||
enum TransactionMethod {
|
||||
iap, // IAP购买
|
||||
igc, // In-game currency 购买
|
||||
igc, // In-game currency 购买(coin/gems..)
|
||||
reward, // 奖励获得
|
||||
none
|
||||
|
||||
bonus, // 优惠
|
||||
igb, // In-game barter
|
||||
free,
|
||||
migrate,
|
||||
unknown
|
||||
}
|
||||
|
||||
String convertTransactionMethodName(TransactionMethod method) {
|
||||
|
|
@ -238,14 +256,23 @@ String convertTransactionMethodName(TransactionMethod method) {
|
|||
return "igc";
|
||||
case TransactionMethod.reward:
|
||||
return "reward";
|
||||
case TransactionMethod.bonus:
|
||||
return "bonus";
|
||||
case TransactionMethod.igb:
|
||||
return "igb";
|
||||
case TransactionMethod.free:
|
||||
return "prop";
|
||||
case TransactionMethod.migrate:
|
||||
return "migrate";
|
||||
default:
|
||||
return "none";
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
class TransactionIntent {
|
||||
final ProductId productId;
|
||||
final int igcCost;
|
||||
final int igcCost; // In-game currency cost
|
||||
final List<StockItem> igbCost; // In-game barter cost
|
||||
final String scene; // 购买场景(最终会用到logEarnVirtualCurrency的item_category上)
|
||||
final bool sales; // 是否为促销商品
|
||||
final double rate; // 默认1.0 指收益率,如:打折促销时设成1.2,最终的收益将是1.2倍
|
||||
|
|
@ -253,6 +280,7 @@ class TransactionIntent {
|
|||
|
||||
TransactionIntent(this.productId, this.scene,
|
||||
{this.igcCost = 0,
|
||||
this.igbCost = const <StockItem>[],
|
||||
this.sales = false,
|
||||
this.rate = 1.0,
|
||||
this.eligibilityCriteria = EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class ProductProfile {
|
|||
final Set<ProductId> iapIds = {};
|
||||
final Set<ProductId> igcIds = {};
|
||||
final Set<ProductId> rewardIds = {};
|
||||
final Set<ProductId> igbIds = {};
|
||||
|
||||
final Map<String, String> groupMap;
|
||||
|
||||
|
|
@ -21,7 +22,8 @@ class ProductProfile {
|
|||
final List<Map<String, ProductId>> _idsMap =
|
||||
List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
|
||||
|
||||
ProductProfile({required Set<ProductId> oneOffChargeIapIds,
|
||||
ProductProfile(
|
||||
{required Set<ProductId> oneOffChargeIapIds,
|
||||
required Set<ProductId> subscriptionsIapIds,
|
||||
Set<ProductId> pointsIapIds = const <ProductId>{},
|
||||
Set<ProductId> igcIds = const <ProductId>{},
|
||||
|
|
@ -70,7 +72,16 @@ class ProductProfile {
|
|||
case TransactionMethod.reward:
|
||||
rewardIds.add(definedProductId);
|
||||
break;
|
||||
case TransactionMethod.none:
|
||||
case TransactionMethod.bonus:
|
||||
break;
|
||||
case TransactionMethod.igb:
|
||||
igbIds.add(definedProductId);
|
||||
break;
|
||||
case TransactionMethod.free:
|
||||
break;
|
||||
case TransactionMethod.migrate:
|
||||
break;
|
||||
case TransactionMethod.unknown:
|
||||
break;
|
||||
}
|
||||
_idsMap[productId.attr][productId.sku] = definedProductId;
|
||||
|
|
@ -116,7 +127,8 @@ class IapProfile {
|
|||
final List<Map<String, ProductId>> _idsMap =
|
||||
List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
|
||||
|
||||
IapProfile({required List<ProductId> oneOffChargeIapIds,
|
||||
IapProfile(
|
||||
{required List<ProductId> oneOffChargeIapIds,
|
||||
required List<ProductId> subscriptionsIapIds,
|
||||
this.noAdsCapIds = const <ProductId>[]}) {
|
||||
for (var productId in oneOffChargeIapIds) {
|
||||
|
|
|
|||
|
|
@ -28,9 +28,13 @@ class RewardManager {
|
|||
await reloadAssets();
|
||||
}
|
||||
|
||||
Future switchSession() async {
|
||||
reloadAssets();
|
||||
}
|
||||
|
||||
Future reloadAssets() async {
|
||||
final transactions = await GuruDB.instance.selectOrders(
|
||||
method: TransactionMethod.reward, attrs: [TransactionAttributes.asset]);
|
||||
final transactions = await GuruDB.instance
|
||||
.selectOrders(method: TransactionMethod.reward, attrs: [TransactionAttributes.asset]);
|
||||
final newAssetsStore = AssetsStore<Asset>();
|
||||
for (var transaction in transactions) {
|
||||
final productId = transaction.productId;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||
import 'package:guru_app/ads/core/ads_config.dart';
|
||||
import 'package:guru_app/analytics/data/analytics_model.dart';
|
||||
import 'package:guru_app/firebase/firebase.dart';
|
||||
import 'package:guru_app/firebase/remoteconfig/reserved_remote_config_models.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_utils/http/http_model.dart';
|
||||
|
|
@ -18,8 +19,7 @@ part "remote_config_interface.dart";
|
|||
part "remote_config_reserved_constants.dart";
|
||||
|
||||
class RemoteConfigManager extends IRemoteConfig {
|
||||
final BehaviorSubject<FirebaseRemoteConfig?> _subject =
|
||||
BehaviorSubject.seeded(null);
|
||||
final BehaviorSubject<FirebaseRemoteConfigWrapper?> _subject = BehaviorSubject.seeded(null);
|
||||
static RemoteConfigManager? _instance;
|
||||
|
||||
static RemoteConfigManager _getInstance() {
|
||||
|
|
@ -31,7 +31,7 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
|
||||
static RemoteConfigManager get instance => _getInstance();
|
||||
|
||||
static final RegExp _invalidABKey = RegExp('[^a-zA-Z0-9_-]');
|
||||
static final RegExp invalidABKeyRegExp = RegExp('[^a-zA-Z0-9_-]');
|
||||
|
||||
RemoteConfigManager._internal();
|
||||
|
||||
|
|
@ -42,17 +42,16 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
minimumFetchInterval: const Duration(hours: 2),
|
||||
));
|
||||
|
||||
_subject.add(remoteConfig);
|
||||
_subject.add(FirebaseRemoteConfigWrapper._(remoteConfig));
|
||||
|
||||
try {
|
||||
await remoteConfig.setDefaults(defaultConfigs);
|
||||
await remoteConfig.activate();
|
||||
} catch (exception) {
|
||||
Log.d(
|
||||
"Unable to fetch remote config. Cached or default values will be used!",
|
||||
Log.d("Unable to fetch remote config. Cached or default values will be used!",
|
||||
error: exception);
|
||||
} finally {
|
||||
_subject.add(remoteConfig);
|
||||
_subject.add(FirebaseRemoteConfigWrapper._(remoteConfig));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,11 +60,10 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
try {
|
||||
await remoteConfig.fetchAndActivate();
|
||||
} catch (exception) {
|
||||
Log.d(
|
||||
"Unable to fetch remote config. Cached or default values will be used!",
|
||||
Log.d("Unable to fetch remote config. Cached or default values will be used!",
|
||||
error: exception);
|
||||
} finally {
|
||||
_subject.add(remoteConfig);
|
||||
_subject.add(FirebaseRemoteConfigWrapper._(remoteConfig));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,12 +93,10 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
final data = config.getAll();
|
||||
final result = {
|
||||
for (var entry in data.entries)
|
||||
"${entry.key} [${valueSourceToString(entry.value.source)}]":
|
||||
entry.value.asString()
|
||||
"${entry.key} [${valueSourceToString(entry.value.source)}]": entry.value.asString()
|
||||
};
|
||||
result["last_fetch_remote_config_time"] = config.lastFetchTime.toString();
|
||||
result["last_fetch_remote_config_status"] =
|
||||
config.lastFetchStatus.toString();
|
||||
result["last_fetch_remote_config_status"] = config.lastFetchStatus.toString();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -113,14 +109,13 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
));
|
||||
await remoteConfig.fetchAndActivate();
|
||||
} catch (exception) {
|
||||
Log.d(
|
||||
"Unable to fetch remote config. Cached or default values will be used $exception",
|
||||
Log.d("Unable to fetch remote config. Cached or default values will be used $exception",
|
||||
error: exception);
|
||||
if (debug) {
|
||||
rethrow;
|
||||
}
|
||||
} finally {
|
||||
_subject.add(remoteConfig);
|
||||
_subject.add(FirebaseRemoteConfigWrapper._(remoteConfig));
|
||||
await remoteConfig.setConfigSettings(RemoteConfigSettings(
|
||||
fetchTimeout: const Duration(seconds: 15),
|
||||
minimumFetchInterval: const Duration(hours: 2),
|
||||
|
|
@ -143,7 +138,7 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
for (var jsonEntry in jsonValue.entries) {
|
||||
if (jsonEntry.key.contains("guru_ab_")) {
|
||||
String abName = jsonEntry.key.replaceFirst("guru_ab_", "");
|
||||
if (abName.contains(_invalidABKey)) {
|
||||
if (abName.contains(invalidABKeyRegExp)) {
|
||||
Log.w("abName($abName) length is invalid! $abName");
|
||||
invalidABKeys.add(abName);
|
||||
} else {
|
||||
|
|
@ -151,7 +146,7 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
invalidABKeys.add(abName);
|
||||
abName = abName.substring(0, 20);
|
||||
}
|
||||
result["ab_$abName"] = jsonEntry.value.toString();
|
||||
result[GuruAnalytics.buildVariantKey(abName)] = jsonEntry.value.toString();
|
||||
Log.i("abName:ab_$abName value:${jsonEntry.value}");
|
||||
}
|
||||
}
|
||||
|
|
@ -164,15 +159,14 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
}
|
||||
}
|
||||
if (invalidABKeys.isNotEmpty) {
|
||||
GuruAnalytics.instance.logException(
|
||||
InvalidABPropertyKeysException(invalidABKeys, cause: cause));
|
||||
GuruAnalytics.instance
|
||||
.logException(InvalidABPropertyKeysException(invalidABKeys, cause: cause));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
bool? getBool(String name, {bool? defaultValue}) =>
|
||||
_subject.value?.getBool(name) ?? defaultValue;
|
||||
bool? getBool(String name, {bool? defaultValue}) => _subject.value?.getBool(name) ?? defaultValue;
|
||||
|
||||
@override
|
||||
String? getString(String name, {String? defaultValue}) =>
|
||||
|
|
@ -185,11 +179,10 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
_subject.value?.getDouble(name) ?? defaultValue;
|
||||
|
||||
@override
|
||||
int? getInt(String name, {int? defaultValue}) =>
|
||||
_subject.value?.getInt(name) ?? defaultValue;
|
||||
int? getInt(String name, {int? defaultValue}) => _subject.value?.getInt(name) ?? defaultValue;
|
||||
|
||||
Stream<FirebaseRemoteConfig> observeConfig() =>
|
||||
_subject.stream.map((config) => config ?? FirebaseRemoteConfig.instance);
|
||||
Stream<FirebaseRemoteConfigWrapper> observeConfig() => _subject.stream
|
||||
.map((config) => config ?? FirebaseRemoteConfigWrapper._(FirebaseRemoteConfig.instance));
|
||||
|
||||
@override
|
||||
Stream<bool?> observeBool(String name, {bool? defaultValue}) =>
|
||||
|
|
@ -207,3 +200,23 @@ class RemoteConfigManager extends IRemoteConfig {
|
|||
Stream<int?> observeInt(String name, {int? defaultValue}) =>
|
||||
observeConfig().map((config) => config.getInt(name));
|
||||
}
|
||||
|
||||
class FirebaseRemoteConfigWrapper {
|
||||
final FirebaseRemoteConfig config;
|
||||
|
||||
const FirebaseRemoteConfigWrapper._(this.config);
|
||||
|
||||
DateTime get lastFetchTime => config.lastFetchTime;
|
||||
|
||||
RemoteConfigFetchStatus get lastFetchStatus => config.lastFetchStatus;
|
||||
|
||||
static String _key(String key) => GuruApp.instance.getRemoteConfigKey(key);
|
||||
|
||||
String getString(String key) => config.getString(_key(key));
|
||||
|
||||
bool getBool(String key) => config.getBool(_key(key));
|
||||
|
||||
double getDouble(String key) => config.getDouble(_key(key));
|
||||
|
||||
int getInt(String key) => config.getInt(_key(key));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,6 @@ extension RemoteConfigReservedConstants on RemoteConfigManager {
|
|||
};
|
||||
|
||||
static String? getDefaultConfigString(String key) {
|
||||
return GuruApp.instance.defaultRemoteConfig[key];
|
||||
return GuruApp.instance.defaultRemoteConfig[GuruApp.instance.getRemoteConfigKey(key)];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:io';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adjust_sdk/adjust_event.dart';
|
||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
|
|
@ -7,7 +6,11 @@ import 'package:firebase_core/firebase_core.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:guru_app/account/account_data_store.dart';
|
||||
import 'package:guru_app/account/account_manager.dart';
|
||||
import 'package:guru_app/account/model/account.dart';
|
||||
import 'package:guru_app/account/model/credential.dart';
|
||||
import 'package:guru_app/account/model/user.dart';
|
||||
import 'package:guru_app/ads/ads_manager.dart';
|
||||
import 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||||
import 'package:guru_app/analytics/guru_analytics.dart';
|
||||
import 'package:guru_app/app/app_models.dart';
|
||||
import 'package:guru_app/database/guru_db.dart';
|
||||
|
|
@ -18,10 +21,15 @@ import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
|||
import 'package:guru_app/financial/product/product_model.dart';
|
||||
import 'package:guru_app/financial/reward/reward_manager.dart';
|
||||
import 'package:guru_app/firebase/dxlinks/dxlink_manager.dart';
|
||||
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
|
||||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_app/property/property_keys.dart';
|
||||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||||
import 'package:guru_utils/collection/collectionutils.dart';
|
||||
import 'package:guru_utils/controller/aware/ads/overlay/ads_overlay.dart';
|
||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||
import 'package:guru_utils/device/device_utils.dart';
|
||||
import 'package:guru_utils/lifecycle/lifecycle_manager.dart';
|
||||
import 'package:guru_utils/network/network_utils.dart';
|
||||
import 'package:guru_utils/property/app_property.dart';
|
||||
|
|
@ -32,10 +40,13 @@ import 'package:guru_utils/log/log.dart';
|
|||
import 'package:guru_utils/packages/guru_package.dart';
|
||||
import 'package:guru_utils/ads/ads.dart';
|
||||
import 'package:guru_utils/guru_utils.dart';
|
||||
import 'package:guru_utils/property/property_model.dart';
|
||||
import 'package:logger/logger.dart' as Logger;
|
||||
import 'package:guru_utils/aigc/bi/ai_bi.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:guru_popup/guru_popup.dart';
|
||||
import 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||||
|
||||
export 'package:firebase_core/firebase_core.dart';
|
||||
export 'package:guru_app/app/app_models.dart';
|
||||
export 'package:guru_utils/log/log.dart';
|
||||
|
|
@ -49,13 +60,15 @@ export 'dart:io';
|
|||
export 'dart:math';
|
||||
export 'package:guru_app/financial/manifest/manifest.dart';
|
||||
export 'package:guru_app/firebase/messaging/remote_messaging_manager.dart';
|
||||
|
||||
export 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||||
|
||||
/// Created by Haoyi on 2022/8/25
|
||||
|
||||
abstract class AppSpec {
|
||||
String get appName;
|
||||
|
||||
AppCategory get appCategory;
|
||||
|
||||
String get flavor;
|
||||
|
||||
AppDetails get details;
|
||||
|
|
@ -69,6 +82,10 @@ abstract class AppSpec {
|
|||
Deployment get deployment;
|
||||
|
||||
Map<String, dynamic> get defaultRemoteConfig;
|
||||
|
||||
Map<String, ABTestExperiment> get localABTestExperiments;
|
||||
|
||||
String getRemoteConfigKey(String key);
|
||||
}
|
||||
|
||||
class NotImplementationAppSpecCreatorException implements Exception {
|
||||
|
|
@ -83,14 +100,12 @@ class NotImplementationAppSpecCreatorException implements Exception {
|
|||
class AppEnv {
|
||||
final AppSpec spec;
|
||||
final RootPackage package;
|
||||
final BackgroundMessageHandler? backgroundMessageHandler;
|
||||
final ToastDelegate? toastDelegate;
|
||||
final IGuruSdkProtocol protocol;
|
||||
|
||||
AppEnv(
|
||||
{required this.spec,
|
||||
required this.package,
|
||||
this.backgroundMessageHandler,
|
||||
this.toastDelegate});
|
||||
required this.protocol});
|
||||
}
|
||||
|
||||
extension _GuruPackageExtension on GuruPackage {
|
||||
|
|
@ -130,8 +145,80 @@ extension _GuruPackageExtension on GuruPackage {
|
|||
child._dispatchInitializeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
Future _dispatchSwitchSession(String oldToken, String newToken) async {
|
||||
await switchSession(oldToken, newToken);
|
||||
children.sort((p1, p2) {
|
||||
return p2.priority.compareTo(p1.priority);
|
||||
});
|
||||
for (var child in children) {
|
||||
if (flattenChildrenAsyncInit) {
|
||||
child._dispatchSwitchSession(oldToken, newToken);
|
||||
} else {
|
||||
await child._dispatchSwitchSession(oldToken, newToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AppCategory { game, app }
|
||||
|
||||
abstract class IGuruSdkProtocol {
|
||||
static void _unimplementedError(String name) {
|
||||
Log.e(
|
||||
"[$name] It is critically important that the GuruSDK protocol be implemented with precision. \n"
|
||||
"Failure to adhere to its standards will result in inaccuracies within our analytics data,\n"
|
||||
"thereby severely compromising our marketing strategies and the effectiveness of our user acquisition efforts.\n"
|
||||
"It is essential to understand that non-compliance is not an option,\n"
|
||||
"as it poses significant risks to our operational success and strategic objectives.");
|
||||
throw UnimplementedError("Please fully implement the IGuruSdkProtocol!");
|
||||
}
|
||||
|
||||
InventoryDelegate? get inventoryDelegate => null;
|
||||
|
||||
BackgroundMessageHandler? get backgroundMessageHandler => null;
|
||||
|
||||
ToastDelegate? get toastDelegate => null;
|
||||
|
||||
IAccountAuthDelegate? get accountAuthDelegate => null;
|
||||
|
||||
String getLevelName() {
|
||||
if (GuruApp.instance.appSpec.appCategory == AppCategory.game) {
|
||||
_unimplementedError("getLevelName");
|
||||
}
|
||||
return "app";
|
||||
}
|
||||
|
||||
int getCurrentLevel() {
|
||||
if (GuruApp.instance.appSpec.appCategory == AppCategory.game) {
|
||||
_unimplementedError("getCurrentLevel");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
final Set<PropertyKey> _deviceSharedProperties = {
|
||||
PropertyKeys.deviceId,
|
||||
PropertyKeys.version,
|
||||
PropertyKeys.buildNumber,
|
||||
PropertyKeys.firstInstallTime,
|
||||
PropertyKeys.firstInstallVersion,
|
||||
PropertyKeys.prevInstallVersion,
|
||||
PropertyKeys.latestInstallVersion,
|
||||
PropertyKeys.previousInstalledVersion,
|
||||
PropertyKeys.latestLtDate,
|
||||
PropertyKeys.ltDays,
|
||||
PropertyKeys.appInstanceId,
|
||||
PropertyKeys.debugMode,
|
||||
PropertyKeys.keepOnScreenDuration,
|
||||
PropertyKeys.analyticsAdId,
|
||||
PropertyKeys.analyticsAdjustId,
|
||||
PropertyKeys.analyticsDeviceId,
|
||||
PropertyKeys.analyticsIdfa,
|
||||
PropertyKeys.analyticsFirebaseId,
|
||||
PropertyKeys.latestAnalyticsStrategy
|
||||
};
|
||||
|
||||
class GuruApp {
|
||||
static late GuruApp _instance;
|
||||
|
||||
|
|
@ -141,6 +228,12 @@ class GuruApp {
|
|||
|
||||
final AppSpec appSpec;
|
||||
|
||||
final IGuruSdkProtocol protocol;
|
||||
RemoteDeployment? _remoteDeployment;
|
||||
|
||||
RemoteDeployment get remoteDeployment =>
|
||||
_remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment();
|
||||
|
||||
String get appName => appSpec.appName;
|
||||
|
||||
String get flavor => appSpec.flavor;
|
||||
|
|
@ -157,8 +250,12 @@ class GuruApp {
|
|||
|
||||
Set<String> get conversionEvents => appSpec.deployment.conversionEvents;
|
||||
|
||||
GuruApp._({required this.appSpec, required this.rootPackage, ToastDelegate? toastDelegate}) {
|
||||
GuruUtils.toastDelegate = toastDelegate;
|
||||
GuruApp._(
|
||||
{required this.appSpec,
|
||||
required this.rootPackage,
|
||||
required this.protocol,
|
||||
}) {
|
||||
GuruUtils.toastDelegate = protocol.toastDelegate;
|
||||
AdsOverlay.bind(showBanner: GuruPopup.instance.showAdsBanner);
|
||||
}
|
||||
|
||||
|
|
@ -167,8 +264,15 @@ class GuruApp {
|
|||
Iterable<LocalizationsDelegate<dynamic>> get localizationsDelegates =>
|
||||
rootPackage._mergeLocalizationsDelegates();
|
||||
|
||||
String getRemoteConfigKey(String key) => appSpec.getRemoteConfigKey(key);
|
||||
|
||||
bool? _check;
|
||||
|
||||
@visibleForTesting
|
||||
static void setMockInstance(GuruApp app) {
|
||||
_instance = app;
|
||||
}
|
||||
|
||||
Future _initialize() async {
|
||||
try {
|
||||
await GuruDB.instance.initDatabase();
|
||||
|
|
@ -182,6 +286,57 @@ class GuruApp {
|
|||
}
|
||||
}
|
||||
|
||||
Future _migrateDeviceSharedData(PropertyBundle latestData) async {
|
||||
final PropertyBundle bundle = PropertyBundle();
|
||||
final keys = {
|
||||
..._deviceSharedProperties,
|
||||
...(protocol.accountAuthDelegate?.deviceSharedProperties ?? {})
|
||||
};
|
||||
for (var key in keys) {
|
||||
final value = latestData.getString(key);
|
||||
if (value != null) {
|
||||
bundle.setString(key, value);
|
||||
}
|
||||
}
|
||||
await AppProperty.getInstance().setProperties(bundle);
|
||||
}
|
||||
|
||||
RemoteDeployment refreshRemoteDeployment() {
|
||||
try {
|
||||
return _remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment();
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("refreshRemoteDeployment error:$error, $stacktrace");
|
||||
}
|
||||
return RemoteDeployment.fromJson({});
|
||||
}
|
||||
|
||||
Future<bool> switchAccount(GuruUser user, Credential credential, {GuruUser? oldUser}) async {
|
||||
final String oldToken = oldUser?.uid ?? "";
|
||||
final String newToken = user.uid;
|
||||
try {
|
||||
final previousUserProperties =
|
||||
PropertyBundle(map: await AppProperty.getInstance().loadAllValues());
|
||||
|
||||
final result = await GuruDB.instance.switchSession(oldToken, newToken);
|
||||
if (!result) {
|
||||
Log.w("switchSession failed");
|
||||
return false;
|
||||
}
|
||||
AppProperty.reload(cacheSize: appSpec.deployment.propertyCacheSize);
|
||||
await _migrateDeviceSharedData(previousUserProperties);
|
||||
await GuruSettings.instance.refresh();
|
||||
await DeviceUtils.reload();
|
||||
FinancialManager.instance.switchSession(oldToken, newToken);
|
||||
GuruAnalytics.instance.switchSession(oldToken, newToken);
|
||||
await AccountManager.instance.processLogin(user, credential);
|
||||
await rootPackage._dispatchSwitchSession(oldToken, newToken);
|
||||
return true;
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("switchSession error:$error, $stacktrace");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _checkApp() async {
|
||||
try {
|
||||
final pkgName = (await PackageInfo.fromPlatform()).appName;
|
||||
|
|
@ -210,6 +365,9 @@ class GuruApp {
|
|||
|
||||
Future _dispatchInitializeSync() async {
|
||||
await RemoteConfigManager.instance.init(appSpec.defaultRemoteConfig);
|
||||
refreshRemoteDeployment();
|
||||
await DeviceUtils.initialize();
|
||||
await GuruAnalytics.instance.prepare();
|
||||
await rootPackage._dispatchInitialize();
|
||||
try {
|
||||
GuruUtils.isTablet = (await GuruApplovinFlutter.instance.isTablet()) ?? false;
|
||||
|
|
@ -237,7 +395,7 @@ class GuruApp {
|
|||
}
|
||||
|
||||
static Future initialize({required AppEnv appEnv}) async {
|
||||
final backgroundMessageHandler = appEnv.backgroundMessageHandler;
|
||||
final backgroundMessageHandler = appEnv.protocol.backgroundMessageHandler;
|
||||
if (backgroundMessageHandler != null) {
|
||||
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
||||
}
|
||||
|
|
@ -248,9 +406,18 @@ class GuruApp {
|
|||
Log.e("Firebase.initializeApp() error!", error: error, stackTrace: stacktrace);
|
||||
}
|
||||
GuruUtils.flavor = appEnv.spec.flavor;
|
||||
|
||||
/// 这里不用担心重复初始化,因为initialize会把对应的 AuthType 重新赋值
|
||||
/// 如果传入的 AuthType 有重复,会覆盖之前的 AuthType
|
||||
AuthCredentialManager.initialize([
|
||||
...AccountManager.defaultSupportedAuthCredentialDelegates,
|
||||
...appEnv.protocol.accountAuthDelegate?.supportedAuthCredentialDelegates ?? []
|
||||
]);
|
||||
try {
|
||||
_instance = GuruApp._(
|
||||
appSpec: appEnv.spec, rootPackage: appEnv.package, toastDelegate: appEnv.toastDelegate);
|
||||
appSpec: appEnv.spec,
|
||||
rootPackage: appEnv.package,
|
||||
protocol: appEnv.protocol);
|
||||
Log.init(_instance.appName,
|
||||
persistentLogFileSize: appEnv.spec.deployment.logFileSizeLimit,
|
||||
persistentLogCount: appEnv.spec.deployment.logFileCount,
|
||||
|
|
@ -279,8 +446,7 @@ extension GuruAppInitializerExt on GuruApp {
|
|||
await RemoteConfigManager.instance.fetchAndActivate();
|
||||
final cdnConfig = RemoteConfigManager.instance.getCdnConfig();
|
||||
HttpEx.init(cdnConfig, GuruApp.instance.appSpec.details.storagePrefix);
|
||||
|
||||
final remoteDeployment = RemoteConfigManager.instance.getRemoteDeployment();
|
||||
refreshRemoteDeployment();
|
||||
Settings.get()
|
||||
.keepOnScreenDuration
|
||||
.set(remoteDeployment.keepScreenOnDuration * DateTimeUtils.minuteInMillis);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,408 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:guru_app/database/guru_db.dart';
|
||||
import 'package:guru_app/financial/product/product_model.dart';
|
||||
import 'package:guru_utils/database/batch/batch_data.dart';
|
||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||
import 'package:guru_utils/id/identifiable.dart';
|
||||
import 'package:guru_utils/log/log.dart';
|
||||
import 'package:guru_utils/manifest/manifest.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'inventory_database.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class LimitedBalance {
|
||||
@JsonKey(name: "a", defaultValue: 0)
|
||||
final int amount;
|
||||
|
||||
@JsonKey(name: "e", defaultValue: -1)
|
||||
final int expireAt;
|
||||
|
||||
@JsonKey(name: "r", defaultValue: false)
|
||||
final bool recycle;
|
||||
|
||||
LimitedBalance(this.amount, this.expireAt, {this.recycle = false});
|
||||
|
||||
factory LimitedBalance.fromJson(Map<String, dynamic> json) => _$LimitedBalanceFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$LimitedBalanceToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class TimeSensitiveData {
|
||||
@JsonKey(name: "v", defaultValue: [])
|
||||
final List<LimitedBalance> valid;
|
||||
|
||||
@JsonKey(name: "e", defaultValue: [])
|
||||
final List<LimitedBalance> expired;
|
||||
|
||||
int get validBalance =>
|
||||
valid.isEmpty ? 0 : valid.map((e) => e.amount).reduce((total, amount) => total + amount);
|
||||
|
||||
int get expiredBalance =>
|
||||
expired.isEmpty ? 0 : expired.map((e) => e.amount).reduce((total, amount) => total + amount);
|
||||
|
||||
const TimeSensitiveData(
|
||||
{this.valid = const <LimitedBalance>[], this.expired = const <LimitedBalance>[]});
|
||||
|
||||
TimeSensitiveData.create({LimitedBalance? balance})
|
||||
: valid = ((balance?.expireAt ?? 0) > DateTimeUtils.currentTimeInMillis()) ? [balance!] : [],
|
||||
expired = [];
|
||||
|
||||
factory TimeSensitiveData.fromJson(Map<String, dynamic> json) =>
|
||||
_$TimeSensitiveDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$TimeSensitiveDataToJson(this);
|
||||
|
||||
TimeSensitiveData attachLimitedBalance(LimitedBalance balance) {
|
||||
if (balance.expireAt > DateTimeUtils.currentTimeInMillis()) {
|
||||
return TimeSensitiveData(valid: List.of(valid)..add(balance), expired: List.of(expired));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
///
|
||||
TimeSensitiveData consume(int balance) {
|
||||
final now = DateTimeUtils.currentTimeInMillis();
|
||||
final newValid = <LimitedBalance>[];
|
||||
for (var item in valid) {
|
||||
int amount = item.amount;
|
||||
if (now < item.expireAt) {
|
||||
if (balance >= item.amount) {
|
||||
balance -= item.amount;
|
||||
continue;
|
||||
} else {
|
||||
amount -= balance;
|
||||
balance = 0;
|
||||
}
|
||||
}
|
||||
// 这里只针对未过期的道具进行处理
|
||||
newValid.add(LimitedBalance(amount, item.expireAt));
|
||||
}
|
||||
newValid.sort((a, b) => b.expireAt.compareTo(a.expireAt));
|
||||
return TimeSensitiveData(valid: newValid, expired: expired);
|
||||
}
|
||||
|
||||
RecycleResult recycleExpiredBalance({int? transactionTs}) {
|
||||
final now = transactionTs ?? DateTimeUtils.currentTimeInMillis();
|
||||
final newValid = <LimitedBalance>[];
|
||||
final newExpired = <LimitedBalance>[];
|
||||
int expiredBalance = 0;
|
||||
for (var item in valid) {
|
||||
if (now >= item.expireAt) {
|
||||
expiredBalance += item.amount;
|
||||
if (item.recycle) {
|
||||
newExpired.add(item);
|
||||
}
|
||||
} else {
|
||||
newValid.add(item);
|
||||
}
|
||||
}
|
||||
newValid.sort((a, b) => b.expireAt.compareTo(a.expireAt));
|
||||
return RecycleResult(expiredBalance, TimeSensitiveData(valid: newValid, expired: newExpired));
|
||||
}
|
||||
}
|
||||
|
||||
class RecycleResult {
|
||||
final int expiredBalance;
|
||||
final TimeSensitiveData timeSensitiveData;
|
||||
|
||||
RecycleResult(this.expiredBalance, this.timeSensitiveData);
|
||||
}
|
||||
|
||||
class ConsumeResult {
|
||||
final bool consumed;
|
||||
final InventoryItem item;
|
||||
|
||||
ConsumeResult.success(this.item) : consumed = true;
|
||||
|
||||
ConsumeResult.error(this.item) : consumed = false;
|
||||
}
|
||||
|
||||
@JsonSerializable(constructor: "_")
|
||||
class InventoryDetails {
|
||||
@JsonKey(name: "a", defaultValue: {})
|
||||
final Map<TransactionMethod, int> acquired;
|
||||
|
||||
@JsonKey(name: "c", defaultValue: {})
|
||||
final Map<String, int> consumed;
|
||||
|
||||
@JsonKey(name: "d")
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
const InventoryDetails._(this.acquired, this.consumed, this.data);
|
||||
|
||||
InventoryDetails.empty()
|
||||
: acquired = const <TransactionMethod, int>{},
|
||||
consumed = const <String, int>{},
|
||||
data = const <String, dynamic>{};
|
||||
|
||||
InventoryDetails.create(int amount)
|
||||
: acquired = (amount == 0)
|
||||
? const <TransactionMethod, int>{}
|
||||
: <TransactionMethod, int>{TransactionMethod.unknown: amount},
|
||||
consumed = const <String, int>{},
|
||||
data = <String, dynamic>{};
|
||||
|
||||
// 通过什么方式获得了多少
|
||||
InventoryDetails acquire(TransactionMethod method, int amount) {
|
||||
return InventoryDetails._(
|
||||
Map<TransactionMethod, int>.from(acquired)..[method] = (acquired[method] ?? 0) + amount,
|
||||
Map<String, int>.from(consumed),
|
||||
Map<String, dynamic>.from(data));
|
||||
}
|
||||
|
||||
// 在什么场景消耗了多少
|
||||
InventoryDetails consume(String scene, int amount) {
|
||||
return InventoryDetails._(
|
||||
Map<TransactionMethod, int>.from(acquired),
|
||||
Map<String, int>.from(consumed)..[scene] = (consumed[scene] ?? 0) + amount,
|
||||
Map<String, dynamic>.from(data));
|
||||
}
|
||||
|
||||
factory InventoryDetails.fromJson(Map<String, dynamic> json) => _$InventoryDetailsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$InventoryDetailsToJson(this);
|
||||
|
||||
void setInt(String key, int value) {
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
void setDouble(String key, double value) {
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
void setString(String key, String value) {
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
void setBool(String key, bool value) {
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
int? getInt(String key) {
|
||||
return data[key];
|
||||
}
|
||||
|
||||
double? getDouble(String key) {
|
||||
return data[key];
|
||||
}
|
||||
|
||||
String? getString(String key) {
|
||||
return data[key];
|
||||
}
|
||||
|
||||
bool? getBool(String key) {
|
||||
return data[key];
|
||||
}
|
||||
}
|
||||
|
||||
class InventoryDetailsStringConvert implements JsonConverter<InventoryDetails, String> {
|
||||
const InventoryDetailsStringConvert();
|
||||
|
||||
@override
|
||||
InventoryDetails fromJson(String json) {
|
||||
if (json.isEmpty) {
|
||||
return InventoryDetails.empty();
|
||||
}
|
||||
final result = jsonDecode(json);
|
||||
return InventoryDetails.fromJson(result);
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson(InventoryDetails transaction) {
|
||||
return jsonEncode(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeSensitiveDataStringConvert implements JsonConverter<TimeSensitiveData, String> {
|
||||
const TimeSensitiveDataStringConvert();
|
||||
|
||||
@override
|
||||
TimeSensitiveData fromJson(String json) {
|
||||
if (json.isEmpty) {
|
||||
return const TimeSensitiveData();
|
||||
}
|
||||
final result = jsonDecode(json);
|
||||
return TimeSensitiveData.fromJson(result);
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson(TimeSensitiveData transaction) {
|
||||
return jsonEncode(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
const InventoryDetailsStringConvert inventoryDetailsStringConvert = InventoryDetailsStringConvert();
|
||||
const TimeSensitiveDataStringConvert timeSensitiveDataStringConvert =
|
||||
TimeSensitiveDataStringConvert();
|
||||
|
||||
class InventoryTable {
|
||||
static const tbName = "inventory"; // Product Transaction Table
|
||||
static const dbSku = "sku";
|
||||
static const dbBalance = "balance";
|
||||
static const dbCategory = "cat";
|
||||
static const dbAttr = "attr";
|
||||
static const dbDetails = "details";
|
||||
static const dbTimeSensitive = "tsv";
|
||||
static const dbUpdateAt = "update_at";
|
||||
static const dbCreateAt = "create_at";
|
||||
|
||||
static Future createTable(Transaction delegate) async {
|
||||
const v1Fields = "${InventoryTable.dbSku} TEXT PRIMARY KEY,"
|
||||
"${InventoryTable.dbBalance} INTEGER NOT NULL DEFAULT 0,"
|
||||
"${InventoryTable.dbCategory} TEXT NOT NULL DEFAULT '',"
|
||||
"${InventoryTable.dbAttr} INTEGER NOT NULL DEFAULT ${DetailsAttr.consumable},"
|
||||
"${InventoryTable.dbDetails} TEXT NOT NULL DEFAULT '',"
|
||||
"${InventoryTable.dbTimeSensitive} TEXT NOT NULL DEFAULT '',"
|
||||
"${InventoryTable.dbCreateAt} INTEGER NOT NULL DEFAULT 0,"
|
||||
"${InventoryTable.dbUpdateAt} INTEGER NOT NULL DEFAULT 0";
|
||||
|
||||
const cmd = "CREATE TABLE ${InventoryTable.tbName} ("
|
||||
"$v1Fields"
|
||||
");";
|
||||
|
||||
Log.v("#### cmd: $cmd");
|
||||
|
||||
await delegate.execute(cmd);
|
||||
await delegate.execute(
|
||||
"CREATE INDEX inventory_item_idx ON ${InventoryTable.tbName} (${InventoryTable.dbSku});");
|
||||
await delegate.execute(
|
||||
"CREATE INDEX inventory_item_category_idx ON ${InventoryTable.tbName} (${InventoryTable.dbCategory});");
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable(constructor: "_")
|
||||
class InventoryItem implements Identifiable {
|
||||
@JsonKey(name: InventoryTable.dbSku)
|
||||
final String sku;
|
||||
|
||||
@JsonKey(name: InventoryTable.dbBalance)
|
||||
final int balance; // 永久的+时效
|
||||
|
||||
@JsonKey(name: InventoryTable.dbCategory)
|
||||
final String category;
|
||||
|
||||
@JsonKey(name: InventoryTable.dbAttr)
|
||||
final int attr;
|
||||
|
||||
@JsonKey(name: InventoryTable.dbTimeSensitive)
|
||||
@timeSensitiveDataStringConvert
|
||||
final TimeSensitiveData timeSensitive;
|
||||
|
||||
@JsonKey(name: InventoryTable.dbUpdateAt)
|
||||
final int updateAt;
|
||||
|
||||
@JsonKey(name: InventoryTable.dbCreateAt)
|
||||
final int createAt;
|
||||
|
||||
@JsonKey(name: InventoryTable.dbDetails)
|
||||
@inventoryDetailsStringConvert
|
||||
final InventoryDetails details;
|
||||
|
||||
InventoryItem._(this.sku, this.balance, this.category, this.attr, this.details,
|
||||
this.timeSensitive, this.createAt, this.updateAt);
|
||||
|
||||
InventoryItem.create(this.sku, this.category, this.attr,
|
||||
{int expireAt = -1, this.balance = 0, TransactionMethod method = TransactionMethod.unknown})
|
||||
: updateAt = DateTimeUtils.currentTimeInMillis(),
|
||||
createAt = DateTimeUtils.currentTimeInMillis(),
|
||||
details = InventoryDetails.create(balance),
|
||||
timeSensitive = TimeSensitiveData.create(balance: LimitedBalance(balance, expireAt));
|
||||
|
||||
InventoryItem acquire(TransactionMethod method, int amount, {int? expiredAt}) {
|
||||
final now = DateTimeUtils.currentTimeInMillis();
|
||||
if (expiredAt != null && expiredAt > now) {
|
||||
timeSensitive.valid.add(LimitedBalance(amount, expiredAt));
|
||||
}
|
||||
final recycled = timeSensitive.recycleExpiredBalance();
|
||||
final target = balance + amount;
|
||||
return InventoryItem._(sku, (target - recycled.expiredBalance).clamp(0, target), category, attr,
|
||||
details.acquire(method, amount), recycled.timeSensitiveData, createAt, now);
|
||||
}
|
||||
|
||||
ConsumeResult consume(int amount, {String scene = ""}) {
|
||||
final now = DateTimeUtils.currentTimeInMillis();
|
||||
final recycled = timeSensitive.recycleExpiredBalance();
|
||||
int remainBalance = (balance - recycled.expiredBalance).clamp(0, balance);
|
||||
if (remainBalance >= amount && attr == DetailsAttr.consumable) {
|
||||
remainBalance -= amount;
|
||||
return ConsumeResult.success(InventoryItem._(
|
||||
sku,
|
||||
remainBalance,
|
||||
category,
|
||||
attr,
|
||||
details.consume(scene, amount),
|
||||
recycled.timeSensitiveData.consume(amount),
|
||||
createAt,
|
||||
now));
|
||||
}
|
||||
return ConsumeResult.error(InventoryItem._(
|
||||
sku, remainBalance, category, attr, details, recycled.timeSensitiveData, createAt, now));
|
||||
}
|
||||
|
||||
ConsumeResult consumeTimeSensitiveOnly(int amount, {String scene = "", int? transactionTs}) {
|
||||
final now = transactionTs ?? DateTimeUtils.currentTimeInMillis();
|
||||
final recycled = timeSensitive.recycleExpiredBalance(transactionTs: transactionTs);
|
||||
final validBalance = recycled.timeSensitiveData.validBalance;
|
||||
int remainBalance = (balance - recycled.expiredBalance).clamp(0, balance);
|
||||
|
||||
/// 这里只是针对有时效性的道具进行消耗,如果时效性道具不足,将返回错误
|
||||
if (validBalance >= amount && attr == DetailsAttr.consumable) {
|
||||
/// 虽然这里只是针对时效性道具进行消耗,但是这里的balance还是会进行更新
|
||||
remainBalance = (remainBalance - amount).clamp(0, remainBalance);
|
||||
return ConsumeResult.success(InventoryItem._(
|
||||
sku,
|
||||
remainBalance,
|
||||
category,
|
||||
attr,
|
||||
details.consume(scene, amount),
|
||||
recycled.timeSensitiveData.consume(amount),
|
||||
createAt,
|
||||
now));
|
||||
}
|
||||
return ConsumeResult.error(InventoryItem._(
|
||||
sku, remainBalance, category, attr, details, recycled.timeSensitiveData, createAt, now));
|
||||
}
|
||||
|
||||
factory InventoryItem.fromJson(Map<String, dynamic> json) => _$InventoryItemFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$InventoryItemToJson(this);
|
||||
|
||||
@override
|
||||
String get id => sku;
|
||||
}
|
||||
|
||||
extension InventoryDatabase on GuruDB {
|
||||
Future<BatchData<InventoryItem>> loadInventoryItems() async {
|
||||
final db = getDb();
|
||||
final result = await db.rawQuery("SELECT * FROM ${InventoryTable.tbName}");
|
||||
final batchData = BatchData<InventoryItem>.empty();
|
||||
if (result.isNotEmpty) {
|
||||
batchData.queryAll([for (var item in result) InventoryItem.fromJson(item)]);
|
||||
}
|
||||
return batchData;
|
||||
}
|
||||
|
||||
Future<BatchData<InventoryItem>> updateInventoryItem(InventoryItem item) async {
|
||||
final db = getDb();
|
||||
await db.insert(InventoryTable.tbName, item.toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return BatchData<InventoryItem>.singleSuccess(BatchMethod.update, item);
|
||||
}
|
||||
|
||||
Future<BatchData<InventoryItem>> updateInventoryItems(List<InventoryItem> items) async {
|
||||
final BatchData<InventoryItem> batchData = BatchData<InventoryItem>.empty();
|
||||
return runInTransaction((txn) async {
|
||||
for (var item in items) {
|
||||
await txn.insert(InventoryTable.tbName, item.toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
batchData.update(item);
|
||||
}
|
||||
return batchData;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'inventory_database.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LimitedBalance _$LimitedBalanceFromJson(Map<String, dynamic> json) =>
|
||||
LimitedBalance(
|
||||
json['a'] as int? ?? 0,
|
||||
json['e'] as int? ?? -1,
|
||||
recycle: json['r'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LimitedBalanceToJson(LimitedBalance instance) =>
|
||||
<String, dynamic>{
|
||||
'a': instance.amount,
|
||||
'e': instance.expireAt,
|
||||
'r': instance.recycle,
|
||||
};
|
||||
|
||||
TimeSensitiveData _$TimeSensitiveDataFromJson(Map<String, dynamic> json) =>
|
||||
TimeSensitiveData(
|
||||
valid: (json['v'] as List<dynamic>?)
|
||||
?.map((e) => LimitedBalance.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
expired: (json['e'] as List<dynamic>?)
|
||||
?.map((e) => LimitedBalance.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TimeSensitiveDataToJson(TimeSensitiveData instance) =>
|
||||
<String, dynamic>{
|
||||
'v': instance.valid,
|
||||
'e': instance.expired,
|
||||
};
|
||||
|
||||
InventoryDetails _$InventoryDetailsFromJson(Map<String, dynamic> json) =>
|
||||
InventoryDetails._(
|
||||
(json['a'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) =>
|
||||
MapEntry($enumDecode(_$TransactionMethodEnumMap, k), e as int),
|
||||
) ??
|
||||
{},
|
||||
(json['c'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as int),
|
||||
) ??
|
||||
{},
|
||||
json['d'] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InventoryDetailsToJson(InventoryDetails instance) =>
|
||||
<String, dynamic>{
|
||||
'a': instance.acquired
|
||||
.map((k, e) => MapEntry(_$TransactionMethodEnumMap[k]!, e)),
|
||||
'c': instance.consumed,
|
||||
'd': instance.data,
|
||||
};
|
||||
|
||||
const _$TransactionMethodEnumMap = {
|
||||
TransactionMethod.iap: 'iap',
|
||||
TransactionMethod.igc: 'igc',
|
||||
TransactionMethod.reward: 'reward',
|
||||
TransactionMethod.bonus: 'bonus',
|
||||
TransactionMethod.igb: 'igb',
|
||||
TransactionMethod.free: 'free',
|
||||
TransactionMethod.migrate: 'migrate',
|
||||
TransactionMethod.unknown: 'unknown',
|
||||
};
|
||||
|
||||
InventoryItem _$InventoryItemFromJson(Map<String, dynamic> json) =>
|
||||
InventoryItem._(
|
||||
json['sku'] as String,
|
||||
json['balance'] as int,
|
||||
json['cat'] as String,
|
||||
json['attr'] as int,
|
||||
inventoryDetailsStringConvert.fromJson(json['details'] as String),
|
||||
timeSensitiveDataStringConvert.fromJson(json['tsv'] as String),
|
||||
json['create_at'] as int,
|
||||
json['update_at'] as int,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InventoryItemToJson(InventoryItem instance) =>
|
||||
<String, dynamic>{
|
||||
'sku': instance.sku,
|
||||
'balance': instance.balance,
|
||||
'cat': instance.category,
|
||||
'attr': instance.attr,
|
||||
'tsv': timeSensitiveDataStringConvert.toJson(instance.timeSensitive),
|
||||
'update_at': instance.updateAt,
|
||||
'create_at': instance.createAt,
|
||||
'details': inventoryDetailsStringConvert.toJson(instance.details),
|
||||
};
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import 'package:guru_app/analytics/guru_analytics.dart';
|
||||
import 'package:guru_app/database/guru_db.dart';
|
||||
import 'package:guru_app/financial/product/product_model.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/inventory/db/inventory_database.dart';
|
||||
import 'package:guru_utils/database/batch/batch_aware.dart';
|
||||
import 'package:guru_utils/log/log.dart';
|
||||
import 'package:guru_utils/manifest/manifest.dart';
|
||||
|
||||
class InventoryCategory {
|
||||
static const String prop = "prop"; // 道具类
|
||||
}
|
||||
|
||||
class PropCategory {
|
||||
// 增益类:提供临时或永久的能力提升,如速度增加、力量提升、生命值恢复等。
|
||||
static const String boosts = 'Boosts';
|
||||
|
||||
// 减益类:施加在对手或玩家身上,造成能力降低、速度减慢、伤害增加等负面效果。
|
||||
static const String debuffs = 'Debuffs';
|
||||
|
||||
// 治疗类:恢复生命值或状态,解除负面效果。
|
||||
static const String healing = 'Healing';
|
||||
|
||||
// 防御类:提供防护,减少受到的伤害或完全避免某些类型的伤害。
|
||||
static const String defensive = 'Defensive';
|
||||
|
||||
// 攻击类:用于对敌人造成伤害或施加减益效果,如武器、陷阱、魔法等。
|
||||
static const String offensive = 'Offensive';
|
||||
|
||||
// 辅助类:提供团队增益、增强队友能力、提供战术优势等非直接攻击手段的道具。
|
||||
static const String supportive = 'Supportive';
|
||||
|
||||
// 探索类:用于揭示地图、发现隐藏物品、解锁新区域或提供关键信息的道具。
|
||||
static const String exploratory = 'Exploratory';
|
||||
|
||||
// 交互类:与游戏世界中的其他元素或玩家进行交互的道具,如钥匙、开关、通信设备等。
|
||||
static const String interactive = 'Interactive';
|
||||
|
||||
// 资源类:用于制造、升级或交易的材料和货币类型道具。
|
||||
static const String resources = 'Resources';
|
||||
|
||||
// 限定类:只能使用一定次数的道具,使用后消失或需要充能。
|
||||
static const String limitedUse = 'LimitedUse';
|
||||
}
|
||||
|
||||
mixin InventoryDelegate {
|
||||
// 返回指定id的库存类别
|
||||
// 如 hint,zoom这些道具统一返回
|
||||
String getInventoryCategory(String id) {
|
||||
return InventoryCategory.prop;
|
||||
}
|
||||
|
||||
Future<List<StockItem>> getMigrateStockItems() async {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class StockItem {
|
||||
final String sku;
|
||||
final int amount;
|
||||
final int attr;
|
||||
final DateTime? expired;
|
||||
|
||||
const StockItem.consumable(this.sku, this.amount, {this.expired}) : attr = DetailsAttr.consumable;
|
||||
|
||||
const StockItem.permanent(this.sku, this.amount, {this.expired}) : attr = DetailsAttr.permanent;
|
||||
|
||||
const StockItem.igc(this.amount, {this.sku = "igc"})
|
||||
: attr = DetailsAttr.consumable,
|
||||
expired = null;
|
||||
|
||||
StockItem.fromDetails(Details details)
|
||||
: sku = details.sku,
|
||||
attr = details.attr,
|
||||
amount = details.amount,
|
||||
expired = null;
|
||||
}
|
||||
|
||||
class InventoryManager with BatchAware<InventoryItem> {
|
||||
static final InventoryManager instance = InventoryManager._();
|
||||
|
||||
InventoryManager._();
|
||||
|
||||
String getInventoryCategory(String id) {
|
||||
return GuruApp.instance.protocol.inventoryDelegate?.getInventoryCategory(id) ??
|
||||
InventoryCategory.prop;
|
||||
}
|
||||
|
||||
Future init() async {
|
||||
final batch = await GuruDB.instance.loadInventoryItems();
|
||||
processBatchData(batch);
|
||||
await _migrate();
|
||||
}
|
||||
|
||||
Future _migrate() async {
|
||||
final migrateStockItems =
|
||||
await GuruApp.instance.protocol.inventoryDelegate?.getMigrateStockItems() ?? [];
|
||||
|
||||
if (migrateStockItems.isNotEmpty) {
|
||||
final needMigrateItems = migrateStockItems.where((item) => !exists(item.sku)).toList();
|
||||
await acquire(needMigrateItems, TransactionMethod.migrate, "migrate");
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// 通过[method]中的的特定[specific]方式 获得了指定的 [items]
|
||||
/// * method: iap -> specific: sku
|
||||
/// * method: igc -> specific: coin/gems...
|
||||
/// * method: reward -> specific: ads/lottery/daily/...
|
||||
/// * method: bonus -> specific: ads/other/...
|
||||
/// * method: igb -> specific: hint/hammer/swap/magic/..
|
||||
///
|
||||
/// method 最终会在 earnVirtualCurrency 中成为 item_category
|
||||
/// specific 最终会在 earnVirtualCurrency 中成为 item_name
|
||||
///
|
||||
Future acquire(List<StockItem> items, TransactionMethod method, String specific,
|
||||
{String? scene}) async {
|
||||
final acquired = <InventoryItem>[];
|
||||
for (var item in items) {
|
||||
final category = getInventoryCategory(item.sku);
|
||||
final invItem = getData(item.sku)
|
||||
?.acquire(method, item.amount, expiredAt: item.expired?.millisecondsSinceEpoch) ??
|
||||
InventoryItem.create(item.sku, category, item.attr,
|
||||
balance: item.amount,
|
||||
method: method,
|
||||
expireAt: item.expired?.millisecondsSinceEpoch ?? -1);
|
||||
acquired.add(invItem);
|
||||
|
||||
GuruAnalytics.instance.logEarnVirtualCurrency(
|
||||
virtualCurrencyName: item.sku,
|
||||
method: convertTransactionMethodName(method),
|
||||
specific: specific,
|
||||
balance: invItem.balance,
|
||||
value: item.amount,
|
||||
scene: scene);
|
||||
}
|
||||
final batchData = await GuruDB.instance.updateInventoryItems(acquired);
|
||||
processBatchData(batchData);
|
||||
}
|
||||
|
||||
ConsumeResult? _consumeItem(StockItem item, Manifest redeemed, {bool timeSensitiveOnly = false}) {
|
||||
final inventoryItem = getData(item.sku);
|
||||
return timeSensitiveOnly
|
||||
? inventoryItem?.consumeTimeSensitiveOnly(item.amount,
|
||||
scene: redeemed.scene, transactionTs: redeemed.transactionTs)
|
||||
: inventoryItem?.consume(item.amount, scene: redeemed.scene);
|
||||
}
|
||||
|
||||
/// 消耗指定的道具[items],如果可以成功消费,便可得到相应的[redeemed]清单
|
||||
Future<bool> consume(List<StockItem> items, Manifest redeemed,
|
||||
{bool timeSensitiveOnly = false}) async {
|
||||
final consumed = <InventoryItem>[];
|
||||
bool isConsumed = true;
|
||||
for (var item in items) {
|
||||
/// 这里只是针对对应的 SKU 商品进行 consume标记,真正的消耗在下面的updateInventoryItems中
|
||||
final result = _consumeItem(item, redeemed, timeSensitiveOnly: timeSensitiveOnly);
|
||||
if (result == null) {
|
||||
Log.e("consume failed! Not Found inventory item: ${item.sku}");
|
||||
return false;
|
||||
}
|
||||
consumed.add(result.item);
|
||||
// 这里如果没有消耗掉,将跳出
|
||||
if (!result.consumed) {
|
||||
Log.w("consume failed: ${item.sku} ${item.amount}");
|
||||
isConsumed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (consumed.isEmpty) {
|
||||
return false;
|
||||
} else {
|
||||
if (isConsumed && consumed.length == items.length) {
|
||||
for (int i = 0; i < items.length; ++i) {
|
||||
GuruAnalytics.instance.logSpendCredits(
|
||||
redeemed.contentId, redeemed.category, items[i].amount,
|
||||
virtualCurrencyName: consumed[i].sku,
|
||||
balance: consumed[i].balance,
|
||||
scene: redeemed.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 这里会更新消耗掉的道具
|
||||
final batchData = await GuruDB.instance.updateInventoryItems(consumed);
|
||||
processBatchData(batchData);
|
||||
return isConsumed;
|
||||
}
|
||||
|
||||
bool canAfford(String id, int amount) {
|
||||
final item = getData(id);
|
||||
return item != null && item.balance >= amount;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,13 @@ import 'dart:convert';
|
|||
|
||||
import 'package:guru_app/account/model/account.dart';
|
||||
import 'package:guru_app/account/model/account_profile.dart';
|
||||
import 'package:guru_app/account/model/credential.dart';
|
||||
import 'package:guru_app/account/model/user.dart';
|
||||
import 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||||
import 'package:guru_app/api/data/orders/orders_model.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/property/property_keys.dart';
|
||||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||||
import 'package:guru_utils/property/property_model.dart';
|
||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||
import 'package:guru_utils/device/device_info.dart';
|
||||
|
|
|
|||
|
|
@ -2,25 +2,25 @@
|
|||
part of "../app_property.dart";
|
||||
|
||||
extension AccountPropertyExtension on AppProperty {
|
||||
void setAccountSaasUser(SaasUser saasUser) {
|
||||
final data = jsonEncode(saasUser);
|
||||
setString(PropertyKeys.accountSaasUser, data);
|
||||
Future setAccountGuruUser(GuruUser guruUser) async {
|
||||
final data = jsonEncode(guruUser);
|
||||
await setString(PropertyKeys.accountGuruUser, data);
|
||||
}
|
||||
|
||||
void setAccountDevice(DeviceInfo deviceInfo) {
|
||||
Future setAccountDevice(DeviceInfo deviceInfo) async {
|
||||
final data = jsonEncode(deviceInfo);
|
||||
setString(PropertyKeys.accountDevice, data);
|
||||
await setString(PropertyKeys.accountDevice, data);
|
||||
}
|
||||
|
||||
void setAccountProfile(AccountProfile accountProfile) {
|
||||
Future setAccountProfile(AccountProfile accountProfile) async {
|
||||
final data = jsonEncode(accountProfile);
|
||||
setString(PropertyKeys.accountProfile, data);
|
||||
await setString(PropertyKeys.accountProfile, data);
|
||||
}
|
||||
|
||||
// refer updateLocalProfile
|
||||
void setDirtyAccountProfile(AccountProfile accountProfile) {
|
||||
Future setDirtyAccountProfile(AccountProfile accountProfile) async {
|
||||
final data = jsonEncode(accountProfile.copyWith(dirty: true));
|
||||
setString(PropertyKeys.accountProfile, data);
|
||||
await setString(PropertyKeys.accountProfile, data);
|
||||
}
|
||||
|
||||
Future<DeviceInfo?> getAccountDevice() async {
|
||||
|
|
@ -37,14 +37,14 @@ extension AccountPropertyExtension on AppProperty {
|
|||
Log.v("loadValuesByTag is empty, $error");
|
||||
return PropertyBundle.empty();
|
||||
});
|
||||
SaasUser? saasUser;
|
||||
GuruUser? guruUser;
|
||||
DeviceInfo? device;
|
||||
AccountProfile? accountProfile;
|
||||
|
||||
final saasUserString = accountBundle.getString(PropertyKeys.accountSaasUser);
|
||||
final saasUserString = accountBundle.getString(PropertyKeys.accountGuruUser);
|
||||
if (DartExt.isNotBlank(saasUserString)) {
|
||||
final map = jsonDecode(saasUserString!);
|
||||
saasUser = SaasUser.fromJson(map);
|
||||
guruUser = GuruUser.fromJson(map);
|
||||
}
|
||||
|
||||
final accountDeviceString = accountBundle.getString(PropertyKeys.accountDevice);
|
||||
|
|
@ -59,7 +59,28 @@ extension AccountPropertyExtension on AppProperty {
|
|||
accountProfile = AccountProfile.fromJson(map);
|
||||
}
|
||||
|
||||
return Account.restore(saasUser: saasUser, device: device, accountProfile: accountProfile);
|
||||
final Map<AuthType, Credential> credentials = {};
|
||||
for (final authType in AuthCredentialManager.instance.supportedAuthType) {
|
||||
final credentialString =
|
||||
accountBundle.getString(PropertyKeys.buildAuthCredentialKey(authType));
|
||||
if (DartExt.isNotBlank(credentialString)) {
|
||||
final credential =
|
||||
AuthCredentialManager.instance.deserializeCredential(authType, credentialString!);
|
||||
if (credential != null) {
|
||||
credentials[authType] = credential;
|
||||
}
|
||||
}
|
||||
}
|
||||
final anonymousSecretKey = accountBundle.getString(PropertyKeys.anonymousSecretKey);
|
||||
if (DartExt.isNotBlank(anonymousSecretKey)) {
|
||||
credentials[AuthType.anonymous] = AnonymousCredential(anonymousSecretKey!);
|
||||
}
|
||||
|
||||
return Account.restore(
|
||||
guruUser: guruUser,
|
||||
device: device,
|
||||
accountProfile: accountProfile,
|
||||
credentials: credentials);
|
||||
}
|
||||
|
||||
Future<int> getLatestReportDeviceTimestamp() async {
|
||||
|
|
@ -78,4 +99,34 @@ extension AccountPropertyExtension on AppProperty {
|
|||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
Future clearAnonymousSecretKey() async {
|
||||
await remove(PropertyKeys.anonymousSecretKey);
|
||||
}
|
||||
|
||||
Future saveCredential(Credential credential) async {
|
||||
final data = jsonEncode(credential);
|
||||
await setString(PropertyKeys.buildAuthCredentialKey(credential.authType), data);
|
||||
final historicalSocialAuths = await getHistoricalSocialAuths();
|
||||
final authName = getAuthName(credential.authType);
|
||||
if (!historicalSocialAuths.contains(authName)) {
|
||||
historicalSocialAuths.add(authName);
|
||||
setHistoricalSocialAuths(historicalSocialAuths);
|
||||
}
|
||||
}
|
||||
|
||||
Future deleteCredential(AuthType authType) async {
|
||||
await remove(PropertyKeys.buildAuthCredentialKey(authType));
|
||||
}
|
||||
|
||||
Future<Set<String>> getHistoricalSocialAuths() async {
|
||||
final data = await getString(PropertyKeys.historicalSocialAuths, defValue: "");
|
||||
return data.isNotEmpty ? (data.split("|").toSet()..remove("")) : {};
|
||||
}
|
||||
|
||||
Future<bool> setHistoricalSocialAuths(Set<String> historicalSocialAuths) async {
|
||||
historicalSocialAuths.remove("");
|
||||
await setString(PropertyKeys.historicalSocialAuths, historicalSocialAuths.join("|"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,4 +38,73 @@ extension AnalyticsPropertyExtension on AppProperty {
|
|||
await setString(PropertyKeys.analyticsIdfa, idfa);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, ABTestExperiment>> loadRunningExperiments() async {
|
||||
final bundle = await AppProperty.getInstance().loadValuesByTag(PropertyTags.guruExperiment);
|
||||
final result = <String, ABTestExperiment>{};
|
||||
bundle.forEach((key, value) {
|
||||
try {
|
||||
if (value.isNotEmpty) {
|
||||
final map = json.decode(value);
|
||||
final experiment = ABTestExperiment.fromJson(map);
|
||||
result[key.name] = experiment;
|
||||
Log.d("loadRunningExperiments: ${key.name} => $experiment");
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("getExperiment error! $error");
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<ABTestExperiment?> getExperiment(String experimentName, {PropertyBundle? bundle}) async {
|
||||
final experimentKey = PropertyKeys.buildExperimentProperty(experimentName);
|
||||
final result = bundle?.getString(experimentKey) ?? await getString(experimentKey, defValue: "");
|
||||
try {
|
||||
if (result.isNotEmpty) {
|
||||
final map = json.decode(result);
|
||||
return ABTestExperiment.fromJson(map);
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Log.w("getExperiment error! $error");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> getExperimentVariant(String experimentName) async {
|
||||
final variantKey =
|
||||
PropertyKeys.buildABTestProperty(GuruAnalytics.buildVariantKey(experimentName));
|
||||
return await getString(variantKey, defValue: "");
|
||||
}
|
||||
|
||||
Future<String> setExperiment(ABTestExperiment experiment) async {
|
||||
PropertyBundle propertyBundle = PropertyBundle();
|
||||
|
||||
final variantKey =
|
||||
PropertyKeys.buildABTestProperty(GuruAnalytics.buildVariantKey(experiment.name));
|
||||
final variantName = experiment.variantName;
|
||||
propertyBundle.setString(variantKey, variantName);
|
||||
|
||||
final experimentKey = PropertyKeys.buildExperimentProperty(experiment.name);
|
||||
propertyBundle.setString(experimentKey, json.encode(experiment));
|
||||
await setProperties(propertyBundle);
|
||||
return variantName;
|
||||
}
|
||||
|
||||
removeExperiment(String experimentName) async {
|
||||
final experimentKey = PropertyKeys.buildExperimentProperty(experimentName);
|
||||
await remove(experimentKey);
|
||||
final variantKey = PropertyKeys.buildABTestProperty(experimentName);
|
||||
await remove(variantKey);
|
||||
}
|
||||
|
||||
Future<bool> refreshGoogleDma(String googleDma) async {
|
||||
final oldGoogleDma =
|
||||
await AppProperty.getInstance().getString(PropertyKeys.googleDma, defValue: "");
|
||||
if (googleDma != oldGoogleDma) {
|
||||
await AppProperty.getInstance().setString(PropertyKeys.googleDma, googleDma);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ part of "../app_property.dart";
|
|||
/// Created by @Haoyi on 5/14/21
|
||||
|
||||
extension DefaultPropertyExtension on AppProperty {
|
||||
Future<String> getDeviceId() async {
|
||||
return getOrCreateString(PropertyKeys.deviceId, IdUtils.uuidV4());
|
||||
Future<String> getDeviceId({String? forceDeviceId}) async {
|
||||
return getOrCreateString(PropertyKeys.deviceId, forceDeviceId ?? IdUtils.uuidV4());
|
||||
}
|
||||
|
||||
Future<int> getFirstInstallTime() async {
|
||||
|
|
|
|||
|
|
@ -18,4 +18,14 @@ extension IapPropertyExtension on AppProperty {
|
|||
Future<void> removeReportSuccessOrder(PropertyKey key) async {
|
||||
remove(key);
|
||||
}
|
||||
|
||||
Future<int> increaseGraceCount() async {
|
||||
final count = await getInt(PropertyKeys.subscriptionGraceCount, defValue: 0);
|
||||
await setInt(PropertyKeys.subscriptionGraceCount, count + 1);
|
||||
return count + 1;
|
||||
}
|
||||
|
||||
Future<void> resetGraceCount() async {
|
||||
await setInt(PropertyKeys.subscriptionGraceCount, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:guru_app/guru_app.dart';
|
||||
import 'package:guru_app/property/app_property.dart';
|
||||
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||||
import 'package:guru_utils/id/id_utils.dart';
|
||||
import 'package:guru_utils/property/property_model.dart';
|
||||
import 'package:guru_utils/settings/settings.dart';
|
||||
|
|
@ -21,9 +22,12 @@ class PropertyKeys {
|
|||
static const PropertyKey debugMode = UtilsSettingsKeys.debugMode;
|
||||
static const PropertyKey latestLtDate = UtilsSettingsKeys.latestLtDate;
|
||||
static const PropertyKey ltDays = UtilsSettingsKeys.ltDays;
|
||||
static const PropertyKey keepOnScreenDuration = UtilsSettingsKeys.keepOnScreenDuration;
|
||||
|
||||
static const PropertyKey accountSaasUser =
|
||||
static const PropertyKey accountGuruUser =
|
||||
PropertyKey.general("account_saas_user", tag: PropertyTags.account);
|
||||
@Deprecated("use accountGuruUser instead")
|
||||
static const PropertyKey accountSaasUser = accountGuruUser;
|
||||
static const PropertyKey accountDevice =
|
||||
PropertyKey.general("account_device", tag: PropertyTags.account);
|
||||
static const PropertyKey accountProfile =
|
||||
|
|
@ -33,11 +37,18 @@ class PropertyKeys {
|
|||
static const PropertyKey anonymousSecretKey =
|
||||
PropertyKey.general("anonymous_secret_key", tag: PropertyTags.account);
|
||||
|
||||
static const PropertyKey historicalSocialAuths =
|
||||
PropertyKey.general("historical_social_auths", tag: PropertyTags.account);
|
||||
|
||||
static const PropertyKey isNoAds = PropertyKey.setting("no_ads", tag: PropertyTags.ads);
|
||||
|
||||
static const PropertyKey totalRevenue =
|
||||
PropertyKey.general("total_revenue", tag: PropertyTags.financial);
|
||||
|
||||
/// 020的 revenue
|
||||
static const PropertyKey totalRevenue020 =
|
||||
PropertyKey.general("total_revenue_020", tag: PropertyTags.financial);
|
||||
|
||||
static const PropertyKey userRewardedCount =
|
||||
PropertyKey.general("user_rewarded_count", tag: PropertyTags.ads);
|
||||
|
||||
|
|
@ -49,6 +60,8 @@ class PropertyKeys {
|
|||
static const PropertyKey iapIgc = PropertyKey.general("iap_igc", tag: PropertyTags.iap);
|
||||
static const PropertyKey noIapIgc = PropertyKey.general("no_iap_igc", tag: PropertyTags.iap);
|
||||
|
||||
static const PropertyKey subscriptionGraceCount =
|
||||
PropertyKey.general("subscription_grace_count", tag: PropertyTags.iap);
|
||||
static const PropertyKey admobConsentTestDeviceId =
|
||||
PropertyKey.general("admob_consent_test_device_id", tag: PropertyTags.ads);
|
||||
static const PropertyKey admobConsentDebugGeography =
|
||||
|
|
@ -97,6 +110,9 @@ class PropertyKeys {
|
|||
static const PropertyKey analyticsIdfa =
|
||||
PropertyKey.general("analytics_idfa", tag: PropertyTags.analytics);
|
||||
|
||||
static const PropertyKey googleDma =
|
||||
PropertyKey.general("google_dma_result", tag: PropertyTags.analytics);
|
||||
|
||||
static const PropertyKey currentIgcBalance =
|
||||
PropertyKey.general("current_balance", tag: PropertyTags.igc);
|
||||
static const PropertyKey currentIgcBalanceValidation =
|
||||
|
|
@ -106,6 +122,10 @@ class PropertyKeys {
|
|||
return PropertyKey.general("abtest_$key", tag: PropertyTags.guruAB);
|
||||
}
|
||||
|
||||
static PropertyKey buildExperimentProperty(String key) {
|
||||
return PropertyKey.general("exp_$key", tag: PropertyTags.guruExperiment);
|
||||
}
|
||||
|
||||
static PropertyKey requestNotificationPermissionTimes =
|
||||
const PropertyKey.general("request_notification_permission_times");
|
||||
|
||||
|
|
@ -114,4 +134,9 @@ class PropertyKeys {
|
|||
|
||||
static PropertyKey deniedNotificationPermissionTimes =
|
||||
const PropertyKey.general("denied_notification_permission_times");
|
||||
|
||||
static PropertyKey buildAuthCredentialKey(AuthType authType) {
|
||||
return PropertyKey.general("${getAuthName(authType)}_auth_credential",
|
||||
tag: PropertyTags.account);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class PropertyTags {
|
|||
static const String failedOrders = "failed_orders";
|
||||
static const String strategyAds = "StrategyAds";
|
||||
static const String guruAB = "GuruAB";
|
||||
static const String guruExperiment = "guru_experiment";
|
||||
|
||||
static const String iap = UtilsPropertyTags.iap;
|
||||
static const String ads = UtilsPropertyTags.ads;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:guru_app/analytics/abtest/abtest_model.dart';
|
||||
import 'package:guru_app/financial/data/db/order_database.dart';
|
||||
import 'package:guru_app/financial/manifest/manifest.dart';
|
||||
import 'package:guru_app/guru_app.dart';
|
||||
|
|
@ -12,5 +13,3 @@ part 'test_guru_app_creator.g.dart';
|
|||
AppSpec createSampleAppSpec(String flavor) {
|
||||
return _GuruSpecFactory.create(flavor);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class _Guru_testRemoteConfigConstants {
|
|||
};
|
||||
|
||||
static String getDefaultConfigString(String key) => _defaultConfigs[key];
|
||||
|
||||
static String getKey(String key) => key;
|
||||
}
|
||||
|
||||
class _Guru_testAppSpec extends AppSpec {
|
||||
|
|
@ -34,6 +36,9 @@ class _Guru_testAppSpec extends AppSpec {
|
|||
@override
|
||||
final appName = 'GuruApp';
|
||||
|
||||
@override
|
||||
final appCategory = AppCategory.app;
|
||||
|
||||
@override
|
||||
final flavor = 'guru_test';
|
||||
|
||||
|
|
@ -95,6 +100,9 @@ class _Guru_testAppSpec extends AppSpec {
|
|||
trackingNotificationPermissionPassLimitTimes: 10,
|
||||
allowInterstitialAsAlternativeReward: false,
|
||||
showInternalAdsWhenBannerUnavailable: true,
|
||||
subscriptionRestoreGraceCount: 3,
|
||||
fullscreenAdsMinInterval: 60,
|
||||
enabledSyncAccountProfile: false,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
@ -158,6 +166,13 @@ class _Guru_testAppSpec extends AppSpec {
|
|||
|
||||
@override
|
||||
final defaultRemoteConfig = _Guru_testRemoteConfigConstants._defaultConfigs;
|
||||
|
||||
@override
|
||||
final localABTestExperiments = _GuruTestABTestExperiments.experiments;
|
||||
|
||||
@override
|
||||
String getRemoteConfigKey(String key) =>
|
||||
_Guru_testRemoteConfigConstants.getKey(key);
|
||||
}
|
||||
|
||||
class _Guru_testProducts {
|
||||
|
|
@ -165,8 +180,6 @@ class _Guru_testProducts {
|
|||
|
||||
static final propRegExp = RegExp(r"^theme_(.*)_(.*)$");
|
||||
|
||||
static final themeMulRegExp = RegExp(r"^theme_(.*)_(.*)$");
|
||||
|
||||
static const noAds = ProductId(
|
||||
android: 'so.a.iap.noads.699',
|
||||
ios: 'so.i.iap.noads.699',
|
||||
|
|
@ -314,8 +327,7 @@ class _Guru_testProducts {
|
|||
buildCoin200Manifest,
|
||||
buildStagePackManifest,
|
||||
buildPremiumWeekManifest,
|
||||
buildPremiumYearManifest,
|
||||
buildThemeMulManifest
|
||||
buildPremiumYearManifest
|
||||
];
|
||||
|
||||
static Future<Manifest?> buildNoAdsManifest(TransactionIntent intent) async {
|
||||
|
|
@ -323,6 +335,7 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
|
|
@ -344,17 +357,21 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
};
|
||||
final details = <Details>[];
|
||||
details.add(Details.define(
|
||||
'igc', intent.sales ? max(500, (intent.rate * 500).toInt()) : 500));
|
||||
'igc', intent.sales ? max(500, (intent.rate * 500).toInt()) : 500,
|
||||
sku: 'igc'));
|
||||
details.add(Details.define(
|
||||
'cup', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1));
|
||||
'cup', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1,
|
||||
sku: 'cup'));
|
||||
details.add(Details.define(
|
||||
'frag', intent.sales ? max(20, (intent.rate * 20).toInt()) : 20));
|
||||
'frag', intent.sales ? max(20, (intent.rate * 20).toInt()) : 20,
|
||||
sku: 'frag'));
|
||||
return Manifest('no_ads', extras: extras, details: details);
|
||||
}
|
||||
|
||||
|
|
@ -379,6 +396,7 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
|
|
@ -413,17 +431,20 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
};
|
||||
final details = <Details>[];
|
||||
details.add(Details.define(
|
||||
'prop', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)
|
||||
..setString('prop_id', matches.first.group(1)!));
|
||||
'prop', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1,
|
||||
sku: '${matches.first.group(1)!}_${matches.first.group(2)!}')
|
||||
..setString('theme_id', matches.first.group(1)!));
|
||||
details.add(Details.define(
|
||||
'pc', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)
|
||||
..setString('prop_id', matches.first.group(2)!));
|
||||
'pc', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1,
|
||||
sku: 'pc')
|
||||
..setString('theme_id', matches.first.group(2)!));
|
||||
return Manifest('prop', extras: extras, details: details);
|
||||
}
|
||||
|
||||
|
|
@ -444,12 +465,13 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
};
|
||||
final details = <Details>[];
|
||||
details.add(Details.define('no_ads', 1));
|
||||
details.add(Details.define('no_ads', 1, sku: 'no_ads'));
|
||||
return Manifest('no_ads', extras: extras, details: details);
|
||||
}
|
||||
|
||||
|
|
@ -467,13 +489,15 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
};
|
||||
final details = <Details>[];
|
||||
details.add(Details.define(
|
||||
'coin', intent.sales ? max(200, (intent.rate * 200).toInt()) : 200));
|
||||
'coin', intent.sales ? max(200, (intent.rate * 200).toInt()) : 200,
|
||||
sku: 'coin'));
|
||||
return Manifest('coin', extras: extras, details: details);
|
||||
}
|
||||
|
||||
|
|
@ -483,13 +507,15 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
};
|
||||
final details = <Details>[];
|
||||
details.add(Details.define(
|
||||
'stage', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)
|
||||
'stage', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1,
|
||||
sku: 'stage')
|
||||
..setInt('stage', 1));
|
||||
return Manifest('stage_1', extras: extras, details: details);
|
||||
}
|
||||
|
|
@ -500,6 +526,7 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
|
|
@ -510,7 +537,8 @@ class _Guru_testProducts {
|
|||
}
|
||||
final details = <Details>[];
|
||||
details.add(Details.define(
|
||||
'igc', intent.sales ? max(8000, (intent.rate * 8000).toInt()) : 8000));
|
||||
'igc', intent.sales ? max(8000, (intent.rate * 8000).toInt()) : 8000,
|
||||
sku: 'igc'));
|
||||
return Manifest('sub', extras: extras, details: details);
|
||||
}
|
||||
|
||||
|
|
@ -520,6 +548,7 @@ class _Guru_testProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
|
|
@ -529,51 +558,54 @@ class _Guru_testProducts {
|
|||
extras[ExtraReservedField.offerId] = intent.productId.offerId;
|
||||
}
|
||||
final details = <Details>[];
|
||||
details.add(Details.define('igc',
|
||||
intent.sales ? max(16000, (intent.rate * 16000).toInt()) : 16000));
|
||||
details.add(Details.define(
|
||||
'igc', intent.sales ? max(16000, (intent.rate * 16000).toInt()) : 16000,
|
||||
sku: 'igc'));
|
||||
return Manifest('sub', extras: extras, details: details);
|
||||
}
|
||||
|
||||
static ProductId themeMul(
|
||||
String category,
|
||||
String themeId,
|
||||
) =>
|
||||
GuruApp.instance.defineProductId('theme_${category}_${themeId}',
|
||||
TransactionAttributes.possessive, TransactionMethod.igc);
|
||||
|
||||
static Future<Manifest?> buildThemeMulManifest(
|
||||
TransactionIntent intent) async {
|
||||
final matches = themeMulRegExp.allMatches(intent.productId.sku);
|
||||
if (matches.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
'theme_id': matches.first.group(2)!,
|
||||
'cate': matches.first.group(1)!,
|
||||
};
|
||||
return Manifest('${matches.first.group(1)!}', extras: extras);
|
||||
}
|
||||
|
||||
static bool isOwnThemeMul(
|
||||
OrderEntity entity,
|
||||
String category,
|
||||
String themeId,
|
||||
) {
|
||||
if (entity.state == TransactionState.success &&
|
||||
entity.category == 'theme_mul') {
|
||||
final match = themeMulRegExp.firstMatch(entity.sku);
|
||||
return match?.group(1) == category && match?.group(2) == themeId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Set<ProductId> get iapIds =>
|
||||
{...oneOffChargeIapIds, ...subscriptionsIapIds};
|
||||
}
|
||||
|
||||
class _GuruTestABTestExperiments {
|
||||
static final test = ABTestExperiment(
|
||||
name: 'test',
|
||||
startTs: 1706457600000, // 2024-01-29 00:00:00.000
|
||||
endTs: 1706457600000, // 2024-01-29 00:00:00.000
|
||||
audience: ABTestAudience(filters: [
|
||||
VersionFilter.lessThan('2.3.0'),
|
||||
CountryFilter.excluded({'us', 'cn', 'en'}),
|
||||
PlatformFilter(
|
||||
androidCondition: AndroidCondition(
|
||||
opt: ConditionOpt.greaterThanOrEquals, sdkInt: 33),
|
||||
iosCondition:
|
||||
IosCondition(opt: ConditionOpt.greaterThanOrEquals, version: 14),
|
||||
),
|
||||
], variant: 2));
|
||||
|
||||
static final test2 = ABTestExperiment(
|
||||
name: 'test2',
|
||||
startTs: 1706457600000, // 2024-01-29 00:00:00.000
|
||||
endTs: 1706457600000, // 2024-01-29 00:00:00.000
|
||||
audience: ABTestAudience(filters: [
|
||||
VersionFilter.lessThan('2.3.0'),
|
||||
CountryFilter.included({'cn'}),
|
||||
PlatformFilter(
|
||||
androidCondition:
|
||||
AndroidCondition(opt: ConditionOpt.lessThan, sdkInt: 24),
|
||||
iosCondition:
|
||||
IosCondition(opt: ConditionOpt.greaterThanOrEquals, version: 14),
|
||||
),
|
||||
NewUserFilter(),
|
||||
], variant: 5));
|
||||
|
||||
static final experiments = <String, ABTestExperiment>{
|
||||
'test': test,
|
||||
'test2': test2,
|
||||
};
|
||||
}
|
||||
|
||||
class _SpiderRemoteConfigConstants {
|
||||
static const iadsConfig = 'iads_config';
|
||||
|
||||
|
|
@ -589,6 +621,8 @@ class _SpiderRemoteConfigConstants {
|
|||
};
|
||||
|
||||
static String getDefaultConfigString(String key) => _defaultConfigs[key];
|
||||
|
||||
static String getKey(String key) => key;
|
||||
}
|
||||
|
||||
class _SpiderAppSpec extends AppSpec {
|
||||
|
|
@ -599,6 +633,9 @@ class _SpiderAppSpec extends AppSpec {
|
|||
@override
|
||||
final appName = 'Spider';
|
||||
|
||||
@override
|
||||
final appCategory = AppCategory.game;
|
||||
|
||||
@override
|
||||
final flavor = 'Spider';
|
||||
|
||||
|
|
@ -680,6 +717,13 @@ class _SpiderAppSpec extends AppSpec {
|
|||
|
||||
@override
|
||||
final defaultRemoteConfig = _SpiderRemoteConfigConstants._defaultConfigs;
|
||||
|
||||
@override
|
||||
final localABTestExperiments = _SpiderABTestExperiments.experiments;
|
||||
|
||||
@override
|
||||
String getRemoteConfigKey(String key) =>
|
||||
_SpiderRemoteConfigConstants.getKey(key);
|
||||
}
|
||||
|
||||
class _SpiderProducts {
|
||||
|
|
@ -726,6 +770,7 @@ class _SpiderProducts {
|
|||
return null;
|
||||
}
|
||||
final extras = <String, dynamic>{
|
||||
ExtraReservedField.contentId: intent.productId.sku,
|
||||
ExtraReservedField.scene: intent.scene,
|
||||
ExtraReservedField.rate: intent.rate,
|
||||
ExtraReservedField.sales: intent.sales,
|
||||
|
|
@ -733,7 +778,8 @@ class _SpiderProducts {
|
|||
};
|
||||
final details = <Details>[];
|
||||
details.add(Details.define(
|
||||
'theme', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1));
|
||||
'theme', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1,
|
||||
sku: 'theme'));
|
||||
return Manifest('${matches.first.group(1)!}',
|
||||
extras: extras, details: details);
|
||||
}
|
||||
|
|
@ -754,6 +800,10 @@ class _SpiderProducts {
|
|||
{...oneOffChargeIapIds, ...subscriptionsIapIds};
|
||||
}
|
||||
|
||||
class _SpiderABTestExperiments {
|
||||
static final experiments = <String, ABTestExperiment>{};
|
||||
}
|
||||
|
||||
class RemoteConfigConstants {
|
||||
static const iadsConfig = 'iads_config';
|
||||
|
||||
|
|
@ -764,6 +814,18 @@ class RemoteConfigConstants {
|
|||
static const analyticsConfig = 'analytics_config';
|
||||
}
|
||||
|
||||
class ABTestExperimentConstants {
|
||||
static Map<String, ABTestExperiment> get experiments {
|
||||
if (GuruApp.instance.flavor == 'guru_test') {
|
||||
return _GuruTestABTestExperiments.experiments;
|
||||
}
|
||||
if (GuruApp.instance.flavor == 'Spider') {
|
||||
return _SpiderABTestExperiments.experiments;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
class ProductIds {
|
||||
static ProductId get noAds {
|
||||
if (GuruApp.instance.flavor == 'guru_test') {
|
||||
|
|
@ -872,16 +934,6 @@ class ProductIds {
|
|||
return ProductId.invalid;
|
||||
}
|
||||
|
||||
static ProductId themeMul(
|
||||
String category,
|
||||
String themeId,
|
||||
) {
|
||||
if (GuruApp.instance.flavor == 'guru_test') {
|
||||
return _Guru_testProducts.themeMul(category, themeId);
|
||||
}
|
||||
return ProductId.invalid;
|
||||
}
|
||||
|
||||
static Set<ProductId> get noAdsCapIds {
|
||||
if (GuruApp.instance.flavor == 'guru_test') {
|
||||
return _Guru_testProducts.noAdsCapIds;
|
||||
|
|
@ -921,10 +973,6 @@ class ProductCategory {
|
|||
static theme(String themeId) {
|
||||
"theme_${themeId}";
|
||||
}
|
||||
|
||||
static themeMul(String category) {
|
||||
"${category}";
|
||||
}
|
||||
}
|
||||
|
||||
class _GuruSpecFactory {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
||||
|
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||
-->
|
||||
|
||||
TODO: Put a short description of the package here that helps potential users
|
||||
know whether this package might be useful for them.
|
||||
|
||||
## Features
|
||||
|
||||
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO: List prerequisites and provide or point to information on how to
|
||||
start using the package.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Include short and useful examples for package users. Add longer examples
|
||||
to `/example` folder.
|
||||
|
||||
```dart
|
||||
const like = 'sample';
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
TODO: Tell users more about the package: where to find more information, how to
|
||||
contribute to the package, how to file issues, what response they can expect
|
||||
from the package authors, and more.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
library guru_fiam;
|
||||
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
name: guru_fiam
|
||||
description: "A new Flutter project."
|
||||
version: 3.0.0
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.3 <4.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_in_app_messaging: 0.7.4+8
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
#
|
||||
# For details regarding assets in packages, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
#
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# To add custom fonts to your package, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts in packages, see
|
||||
# https://flutter.dev/custom-fonts/#from-packages
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:guru_fiam/guru_fiam.dart';
|
||||
|
||||
void main() {
|
||||
test('adds one to input values', () {
|
||||
final calculator = Calculator();
|
||||
expect(calculator.addOne(2), 3);
|
||||
expect(calculator.addOne(-7), -6);
|
||||
expect(calculator.addOne(0), 1);
|
||||
});
|
||||
}
|
||||
|
|
@ -42,7 +42,6 @@ class _RootPackage extends RootPackage {
|
|||
]);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
// TODO: implement localizationsDelegates
|
||||
Iterable<LocalizationsDelegate> get localizationsDelegates => const [
|
||||
|
|
@ -59,6 +58,18 @@ class _RootPackage extends RootPackage {
|
|||
Iterable<Locale> get supportedLocales => throw UnimplementedError();
|
||||
}
|
||||
|
||||
class ComplianceProtocol implements IGuruSdkComplianceProtocol {
|
||||
@override
|
||||
int getCurrentLevel() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@override
|
||||
String getLevelName() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@singleton
|
||||
class Initializer {
|
||||
// final TransactionService transactionService;
|
||||
|
|
@ -79,7 +90,10 @@ class Initializer {
|
|||
static AppEnv _buildAppEnv({String flavor = ""}) {
|
||||
final rootPackage = _RootPackage();
|
||||
|
||||
return AppEnv(spec: createAppSpec(flavor), package: rootPackage);
|
||||
return AppEnv(
|
||||
spec: createAppSpec(flavor),
|
||||
package: rootPackage,
|
||||
complianceProtocol: ComplianceProtocol());
|
||||
}
|
||||
|
||||
static Future ensureInitialized() async {
|
||||
|
|
|
|||
|
|
@ -30,4 +30,6 @@ class _GuruSpecFactory {
|
|||
}
|
||||
}
|
||||
|
||||
class Flavors {}
|
||||
class Flavors {
|
||||
static const String classic = "classic";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ packages:
|
|||
name: _flutterfire_internals
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.12"
|
||||
version: "1.3.16"
|
||||
adjust_sdk:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: adjust_sdk
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.33.0"
|
||||
version: "4.36.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -175,21 +175,21 @@ packages:
|
|||
name: cloud_firestore
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.3.1"
|
||||
version: "4.13.6"
|
||||
cloud_firestore_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cloud_firestore_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.10.1"
|
||||
version: "6.0.10"
|
||||
cloud_firestore_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cloud_firestore_web
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.8.10"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -265,7 +265,7 @@ packages:
|
|||
description:
|
||||
path: "packages/design"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
||||
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||
url: "git@github.com:castbox/guru_ui.git"
|
||||
source: git
|
||||
version: "2.0.2"
|
||||
|
|
@ -274,7 +274,7 @@ packages:
|
|||
description:
|
||||
path: "packages/design_generator"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
||||
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||
url: "git@github.com:castbox/guru_ui.git"
|
||||
source: git
|
||||
version: "2.0.2"
|
||||
|
|
@ -283,7 +283,7 @@ packages:
|
|||
description:
|
||||
path: "packages/design_spec"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
||||
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||
url: "git@github.com:castbox/guru_ui.git"
|
||||
source: git
|
||||
version: "2.0.2"
|
||||
|
|
@ -300,7 +300,7 @@ packages:
|
|||
name: device_info_plus
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.2.2"
|
||||
version: "9.1.1"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -356,147 +356,147 @@ packages:
|
|||
name: firebase_analytics
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.1.0"
|
||||
version: "10.7.4"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.3.17"
|
||||
version: "3.9.0"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.1+8"
|
||||
version: "0.5.5+12"
|
||||
firebase_auth:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
version: "4.15.3"
|
||||
firebase_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.11.7"
|
||||
version: "7.0.9"
|
||||
firebase_auth_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth_web
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.2.4"
|
||||
version: "5.8.13"
|
||||
firebase_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.24.2"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
version: "5.0.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
version: "2.10.0"
|
||||
firebase_crashlytics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.4.8"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.3.11"
|
||||
version: "3.6.16"
|
||||
firebase_dynamic_links:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_dynamic_links
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.11"
|
||||
version: "5.4.8"
|
||||
firebase_dynamic_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_dynamic_links_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.3+26"
|
||||
version: "0.2.6+16"
|
||||
firebase_in_app_messaging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_in_app_messaging
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.0+10"
|
||||
version: "0.7.4+8"
|
||||
firebase_in_app_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_in_app_messaging_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.1+29"
|
||||
version: "0.2.4+16"
|
||||
firebase_messaging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "14.2.1"
|
||||
version: "14.7.9"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.2.10"
|
||||
version: "4.5.18"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.11"
|
||||
version: "3.5.18"
|
||||
firebase_remote_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_remote_config
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "4.3.8"
|
||||
firebase_remote_config_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_remote_config_platform_interface
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.29"
|
||||
version: "1.4.16"
|
||||
firebase_remote_config_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_remote_config_web
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.18"
|
||||
version: "1.4.16"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -504,11 +504,25 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
flame:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flame
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_animate:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_animate
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
flutter_blurhash:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -605,19 +619,17 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: "v2.3.1"
|
||||
resolved-ref: e4438b7ece793a85da477b685e60c79981be281a
|
||||
ref: "v2.3.4"
|
||||
resolved-ref: "804fd22ddc1fc31acecdf72e936dabc0193379c5"
|
||||
url: "git@github.com:castbox/guru_analytics_flutter.git"
|
||||
source: git
|
||||
version: "2.0.0"
|
||||
guru_app:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
path: "."
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
||||
url: "git@github.com:castbox/guru_app.git"
|
||||
source: git
|
||||
path: "../../.."
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.1.0"
|
||||
guru_applifecycle_flutter:
|
||||
dependency: transitive
|
||||
|
|
@ -633,7 +645,7 @@ packages:
|
|||
description:
|
||||
path: "."
|
||||
ref: "v2.3.8"
|
||||
resolved-ref: "6fb5191d4cffc6a5728df7ee8af793fcc27fd081"
|
||||
resolved-ref: "4cb520a2f9bea14300b0d2b452e183bcc42779f9"
|
||||
url: "git@github.com:castbox/guru_applovin_flutter.git"
|
||||
source: git
|
||||
version: "2.3.0"
|
||||
|
|
@ -649,7 +661,7 @@ packages:
|
|||
description:
|
||||
path: "plugins/guru_navigator"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
||||
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||
url: "git@github.com:castbox/guru_app.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
|
@ -658,7 +670,7 @@ packages:
|
|||
description:
|
||||
path: "plugins/guru_platform_data"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
||||
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||
url: "git@github.com:castbox/guru_app.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
|
@ -667,34 +679,30 @@ packages:
|
|||
description:
|
||||
path: "packages/guru_popup"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
||||
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||
url: "git@github.com:castbox/guru_ui.git"
|
||||
source: git
|
||||
version: "2.3.0"
|
||||
guru_spec:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
path: "packages/guru_spec"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
||||
url: "git@github.com:castbox/guru_app.git"
|
||||
source: git
|
||||
path: "../../guru_spec"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.0"
|
||||
guru_utils:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
path: "packages/guru_utils"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
||||
url: "git@github.com:castbox/guru_app.git"
|
||||
source: git
|
||||
path: "../../guru_utils"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.1.0"
|
||||
guru_widgets:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
path: "packages/guru_widgets"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
||||
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||
url: "git@github.com:castbox/guru_ui.git"
|
||||
source: git
|
||||
version: "2.2.0"
|
||||
|
|
@ -866,6 +874,13 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
ordered_set:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ordered_set
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -997,7 +1012,7 @@ packages:
|
|||
description:
|
||||
path: "plugins/persistent"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
||||
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||
url: "git@github.com:castbox/guru_app.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
|
@ -1109,7 +1124,7 @@ packages:
|
|||
description:
|
||||
path: "plugins/soundpool"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
||||
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||
url: "git@github.com:castbox/guru_app.git"
|
||||
source: git
|
||||
version: "2.3.0"
|
||||
|
|
@ -1321,7 +1336,7 @@ packages:
|
|||
description:
|
||||
path: "plugins/vibration"
|
||||
ref: "v2.3.0"
|
||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
||||
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||
url: "git@github.com:castbox/guru_app.git"
|
||||
source: git
|
||||
version: "1.7.5"
|
||||
|
|
@ -1388,6 +1403,13 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.4"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -100,6 +100,16 @@ dev_dependencies:
|
|||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
|
||||
dependency_overrides:
|
||||
guru_app:
|
||||
path: ../../../
|
||||
|
||||
guru_utils:
|
||||
path: ../../guru_utils
|
||||
|
||||
guru_spec:
|
||||
path: ../../guru_spec
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ packages:
|
|||
sha256: "73ff438fe46028f0e19f55da18b6ddc6906ab750562cd7d9ffab77ff8c0c4307"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "6.0.10"
|
||||
cloud_firestore_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -216,7 +216,7 @@ packages:
|
|||
sha256: "232e45e95970d3a6baab8f50f9c3a6e2838d145d9d91ec9a7392837c44296397"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.9.0"
|
||||
version: "3.8.10"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -578,6 +578,13 @@ packages:
|
|||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_animate:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_animate
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
flutter_blurhash:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_animate
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
||||
|
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||
-->
|
||||
|
||||
TODO: Put a short description of the package here that helps potential users
|
||||
know whether this package might be useful for them.
|
||||
|
||||
## Features
|
||||
|
||||
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO: List prerequisites and provide or point to information on how to
|
||||
start using the package.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Include short and useful examples for package users. Add longer examples
|
||||
to `/example` folder.
|
||||
|
||||
```dart
|
||||
const like = 'sample';
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
TODO: Tell users more about the package: where to find more information, how to
|
||||
contribute to the package, how to file issues, what response they can expect
|
||||
from the package authors, and more.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# example
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'You have pushed the button this many times:',
|
||||
),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
sdks:
|
||||
dart: ">=3.2.3 <4.0.0"
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
name: example
|
||||
description: "A new Flutter project."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.3 <4.0.0'
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:example/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
let globalLogger;
|
||||
|
||||
class EventLogger {
|
||||
appId = '';
|
||||
deviceInfo = {};
|
||||
version = 10
|
||||
deviceStr = '';
|
||||
events = [];
|
||||
info = {}; //用户信息
|
||||
|
||||
|
||||
constructor({ appId, deviceInfo, info }) {
|
||||
this.appId = appId;
|
||||
let u = navigator.userAgent;
|
||||
let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;
|
||||
let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
|
||||
let tzOffset = new Date().valueOf();
|
||||
this.deviceInfo = {
|
||||
platform: isAndroid ? 'ANDROID' : 'IOS',
|
||||
country: navigator.languages[1],
|
||||
tzOffset,
|
||||
deviceType: 'other',
|
||||
brand: '',
|
||||
model: '',
|
||||
screenH: document.body.clientHeight,
|
||||
screenW: document.body.clientWidth,
|
||||
osVersion: '',
|
||||
language: navigator.language,
|
||||
...deviceInfo
|
||||
};
|
||||
for (let key in this.deviceInfo) this.deviceStr += `${key}=${this.deviceInfo[key]};`;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
|
||||
getHeaders() {
|
||||
let headers = new Headers();
|
||||
headers.append('X-APP-ID', this.appId);
|
||||
headers.append('X-DEVICE-INFO', this.deviceStr)
|
||||
headers.append('content-type', 'application/json');
|
||||
headers.append('Content-Encoding', 'gzip');
|
||||
return headers;
|
||||
}
|
||||
|
||||
zip(str) {
|
||||
const binaryString = pako.gzip(str)
|
||||
return binaryString;
|
||||
}
|
||||
|
||||
log(event) {
|
||||
event = {
|
||||
...event,
|
||||
timestamp: Date.now(),
|
||||
info: this.info
|
||||
};
|
||||
this.events.push(event);
|
||||
if (this.events.length > 0) this.logEvent();
|
||||
}
|
||||
|
||||
|
||||
async logEvent() {
|
||||
const requestBody = {
|
||||
version: this.version,
|
||||
deviceInfo: this.deviceInfo,
|
||||
events: [...this.events]
|
||||
};
|
||||
const bodyStr = JSON.stringify(requestBody);
|
||||
const gzippedStr = this.zip(bodyStr);
|
||||
const config = {
|
||||
headers: this.getHeaders(),
|
||||
body: gzippedStr,
|
||||
method: 'POST',
|
||||
};
|
||||
await fetch('https://collect.saas.castbox.fm/event', config);
|
||||
this.events = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* appInfo {
|
||||
* appId
|
||||
* deviceInfo
|
||||
* version,
|
||||
* info
|
||||
* }
|
||||
*/
|
||||
function initEventLogger(appInfo) {
|
||||
globalLogger = new EventLogger(JSON.parse(appInfo));
|
||||
}
|
||||
|
||||
function castboxLogEvent(eventName, param, properties) {
|
||||
if (globalLogger) {
|
||||
globalLogger.log({
|
||||
event: eventName,
|
||||
param: JSON.parse(param),
|
||||
properties: JSON.parse(properties)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 917 B |
|
|
@ -0,0 +1,331 @@
|
|||
/**
|
||||
* 获取facebook 用户基本信息
|
||||
* @returns
|
||||
*/
|
||||
function getFBProfile() {
|
||||
const photo = FBInstant.player.getPhoto()
|
||||
const name = FBInstant.player.getName()
|
||||
const id = FBInstant.player.getID()
|
||||
const contextType = FBInstant.context.getType()
|
||||
return JSON.stringify({ photo, name, id, contextType })
|
||||
}
|
||||
|
||||
function getFBProfileWithUid(success, error) {
|
||||
const photo = FBInstant.player.getPhoto()
|
||||
const name = FBInstant.player.getName()
|
||||
const id = FBInstant.player.getID()
|
||||
const contextType = FBInstant.context.getType()
|
||||
FBInstant.player.getASIDAsync().then((userId) => {
|
||||
success(JSON.stringify({ photo, name, id, contextType, userId }))
|
||||
}).catch(err => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
function getEntryPointData() {
|
||||
const data = FBInstant.getEntryPointData();
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
function getLocale() {
|
||||
return FBInstant.getLocale();
|
||||
}
|
||||
|
||||
//IOS, Android, WEB
|
||||
function getPlatform() {
|
||||
return FBInstant.getPlatform()
|
||||
}
|
||||
|
||||
// 分享游戏
|
||||
function shareGame(data, success, error) {
|
||||
FBInstant.shareAsync(JSON.parse(data))
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
function logEvent(eventName, valueToSum, parameters) {
|
||||
FBInstant.logEvent(eventName, valueToSum, JSON.parse(parameters))
|
||||
}
|
||||
|
||||
function inviteUser(data, success, error) {
|
||||
FBInstant.inviteAsync(JSON.parse(data))
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 检测当前支持广告类型 ["getInterstitialAdAsync", "getRewardedVideoAsync"]
|
||||
function checkSupportedAds() {
|
||||
let supportedAPIs = FBInstant.getSupportedAPIs()
|
||||
return JSON.stringify(supportedAPIs);
|
||||
}
|
||||
|
||||
// 加载,展示插屏广告
|
||||
let preloadedInterstitial = null
|
||||
function loadInterstitial(id, success, error) {
|
||||
//插屏预载
|
||||
FBInstant.getInterstitialAdAsync(id)
|
||||
.then((res) => {
|
||||
preloadedInterstitial = res
|
||||
return preloadedInterstitial.loadAsync
|
||||
}).then(res => {
|
||||
success()
|
||||
}).catch((err) => {
|
||||
error(err.message);
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
function showInterstitial(success, error) {
|
||||
if (preloadedInterstitial == null) {
|
||||
return error("showInterstitial preloadedInterstitial is null")
|
||||
}
|
||||
//插屏播放
|
||||
preloadedInterstitial
|
||||
.showAsync()
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
// 加载,展示激励视频
|
||||
let preloadedRewardedVideo = null
|
||||
function loadRewarded(id, success, error) {
|
||||
//激励视频预载
|
||||
FBInstant.getRewardedVideoAsync(id)
|
||||
.then((res) => {
|
||||
preloadedRewardedVideo = res
|
||||
return preloadedRewardedVideo.loadAsync
|
||||
}).then(res => {
|
||||
success()
|
||||
}).catch((err) => {
|
||||
error(err.message);
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
function showRewarded(success, error) {
|
||||
if (preloadedRewardedVideo == null) {
|
||||
return error("showInterstitial preloadedRewardedVideo is null")
|
||||
}
|
||||
//插屏播放
|
||||
preloadedRewardedVideo
|
||||
.showAsync()
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
// 展示,隐藏banner广告
|
||||
function showBannerAds(id, success, error) {
|
||||
FBInstant.loadBannerAdAsync(id)
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
function hideBannerAds(id, success, error) {
|
||||
FBInstant.hideBannerAdAsync(id)
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
// tournament 相关
|
||||
|
||||
function createTournament(initialScore, config, data, success, error) {
|
||||
/**
|
||||
* initialScore: number
|
||||
* config: {
|
||||
* title?: string
|
||||
* image?: base64,
|
||||
* sortOrder?: "HIGHER_IS_BETTER"| "LOWER_IS_BETTER"
|
||||
* scoreFormat?: "NUMERIC" | "TIME"
|
||||
* endTime?: unix timestamp (seconds)
|
||||
* }
|
||||
* data: {} customized
|
||||
*/
|
||||
FBInstant.tournament
|
||||
.createAsync({
|
||||
initialScore,
|
||||
config: JSON.parse(config),
|
||||
data: JSON.parse(data),
|
||||
})
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
function postTournamentScore(id, score, success, error) {
|
||||
FBInstant.tournament
|
||||
.joinAsync(id)
|
||||
.then(function () {
|
||||
return FBInstant.tournament.postScoreAsync(score);
|
||||
})
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
function shareTournament(score, data, success, error) {
|
||||
//data: customized {}
|
||||
FBInstant.tournament
|
||||
.shareAsync({
|
||||
score,
|
||||
data: JSON.parse(data),
|
||||
})
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
function getCurrentTournament(success, error) {
|
||||
FBInstant.getTournamentAsync()
|
||||
.then(function (tournament) {
|
||||
success(
|
||||
JSON.stringify({
|
||||
id: tournament.getContextID(),
|
||||
endTime: tournament.getEndTime(),
|
||||
title: tournament.getTitle(),
|
||||
payLoad: tournament.getPayload(),
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
// player 相关
|
||||
|
||||
function fbSetData(data) {
|
||||
FBInstant.player.setDataAsync(JSON.parse(data))
|
||||
}
|
||||
|
||||
function fbGetData(keys, success, error) {
|
||||
FBInstant.player
|
||||
.getDataAsync(JSON.parse(keys))
|
||||
.then((res) => {
|
||||
success(JSON.stringify(res))
|
||||
})
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
function subscribeBot(success, error) {
|
||||
FBInstant.player
|
||||
.canSubscribeBotAsync()
|
||||
.then((can_subscribe) => {
|
||||
if (can_subscribe) {
|
||||
return FBInstant.player.subscribeBotAsync()
|
||||
}
|
||||
})
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
function getConnectedPlayersAsync(success, error) {
|
||||
//Fetches an array of ConnectedPlayer objects containing information about active players (people who played the game in the last 90 days) that are connected to the current player.
|
||||
FBInstant.player.getConnectedPlayersAsync()
|
||||
.then(function (res) {
|
||||
var players = res.map(function (player) {
|
||||
return {
|
||||
player_id: player.getID(),
|
||||
name: player.getName(),
|
||||
picUrl: player.getPhoto()
|
||||
}
|
||||
})
|
||||
success(JSON.stringify(players))
|
||||
})
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
// context 相关
|
||||
|
||||
function getContextInfo() {
|
||||
return JSON.stringify({
|
||||
id: FBInstant.context.getID(),
|
||||
type: FBInstant.context.getType(),
|
||||
})
|
||||
}
|
||||
|
||||
function chooseContext(success, error) {
|
||||
FBInstant.context.chooseAsync({
|
||||
filters: ['INCLUDE_EXISTING_CHALLENGES'],
|
||||
minSize: 2,
|
||||
maxSize: 2,
|
||||
}).then(function () {
|
||||
var contextInfo = {
|
||||
id: FBInstant.context.getID(),
|
||||
type: FBInstant.context.getType(),
|
||||
}
|
||||
console.log("chooseContext after", contextInfo)
|
||||
success(JSON.stringify(contextInfo))
|
||||
})
|
||||
.catch((err) => {
|
||||
error(JSON.stringify(err))
|
||||
console.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
function createAsync(playerId, success, error) {
|
||||
FBInstant.context
|
||||
.createAsync(playerId)
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
console.log("createAsync error", JSON.stringify(err))
|
||||
error(JSON.stringify(err))
|
||||
})
|
||||
}
|
||||
|
||||
function sendPlayWith(data, success, error) {
|
||||
FBInstant.context
|
||||
.chooseAsync(JSON.parse(data))
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
console.log(err.message)
|
||||
error(JSON.stringify(err))
|
||||
})
|
||||
}
|
||||
|
||||
function updateAsync(img) {
|
||||
FBInstant.updateAsync({
|
||||
action: 'CUSTOM',
|
||||
cta: 'Join The Fight',
|
||||
image: img,
|
||||
text: {
|
||||
default: 'X just invaded Y\'s village!',
|
||||
},
|
||||
template: 'VILLAGE_INVASION',
|
||||
data: { myReplayData: 'test', date: new Date().getMilliseconds() },
|
||||
strategy: 'IMMEDIATE',
|
||||
notification: 'NO_PUSH',
|
||||
}).then(function () {
|
||||
// closes the game after the update is posted.
|
||||
|
||||
});
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,187 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=GT-K5856LV"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
|
||||
gtag("config", "G-K57CGD10ST", {
|
||||
cookie_flags: "max-age=7200;secure;samesite=none",
|
||||
cookieDomain: 'none'
|
||||
});
|
||||
function gaEventLogger(eventName, eventParameters) {
|
||||
gtag("event", eventName, JSON.parse(eventParameters));
|
||||
}
|
||||
</script>
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="example">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>example</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script>
|
||||
// The value below is injected by flutter build, do not touch.
|
||||
const serviceWorkerVersion = null;
|
||||
</script>
|
||||
<!-- This script adds the flutter initialization JS code -->
|
||||
<script src="flutter.js" defer></script>
|
||||
<!-- Event logger-->
|
||||
<script src="pako.min.js"></script>
|
||||
<script src="event-logger.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
flutter-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#launch_view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#launch_view .logo {
|
||||
position: absolute;
|
||||
top: 48%;
|
||||
max-width: 75vw;
|
||||
width: 50vw;
|
||||
top: 30vh;
|
||||
}
|
||||
|
||||
#launch_view .cover {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#launch_view .loading {
|
||||
position: absolute;
|
||||
top: 54%;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 4px solid #fff;
|
||||
border-radius: 50%;
|
||||
width: 7vw;
|
||||
height: 7vw;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
#launch_view .guru_text {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 85%;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#debug_dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
#debug_dialog .content {
|
||||
width: 80%;
|
||||
background-color: #fff;
|
||||
padding: 12px;
|
||||
height: 80%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://connect.facebook.net/en_US/fbinstant.7.0.js"></script>
|
||||
<script src="index.js"></script>
|
||||
<script>
|
||||
showLaunchView()
|
||||
window.flutterConfiguration = {
|
||||
canvasKitBaseUrl: "./canvaskit/",
|
||||
};
|
||||
async function updateProgress() {
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await new Promise((res) => setTimeout(res, 200));
|
||||
FBInstant.setLoadingProgress((i / 10) * 100);
|
||||
}
|
||||
}
|
||||
function onStart() {
|
||||
// Download main.dart.js
|
||||
window.gtag && gtag("event", "app_open", { source: "" });
|
||||
_flutter.loader.loadEntrypoint({
|
||||
serviceWorker: {
|
||||
serviceWorkerVersion: serviceWorkerVersion,
|
||||
},
|
||||
onEntrypointLoaded: function(engineInitializer) {
|
||||
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||
appRunner.runApp();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
window.onload = function () {
|
||||
updateProgress();
|
||||
//onStart();
|
||||
FBInstant.initializeAsync().then(async () => {
|
||||
return FBInstant.startGameAsync().then(onStart);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<div id="debug_dialog" onclick="closeDebugeDialog(event)">
|
||||
<div class="content"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
/**
|
||||
* showLaunchView 展示首页
|
||||
* @param {Object} options
|
||||
* @param {string} options.cover 顶部图片url,有则显示,没有不显示
|
||||
* @param {string} options.logo logo url,有则显示,没有不显示
|
||||
* @param {boolean} options.loading loading,有则显示,没有不显示
|
||||
* @param {string} options.guruLimited guru limited 文案,默认"@ Guru network limited"
|
||||
*/
|
||||
function showLaunchView(options = {
|
||||
guruLimited: '@ Guru network limited'
|
||||
}) {
|
||||
const view = document.createElement('div')
|
||||
view.id = 'launch_view'
|
||||
|
||||
if (options.cover) {
|
||||
const img = document.createElement('img')
|
||||
img.src = options.cover
|
||||
img.className = 'cover'
|
||||
view.appendChild(img)
|
||||
}
|
||||
|
||||
if (options.logo) {
|
||||
const img = document.createElement('img')
|
||||
img.src = options.logo
|
||||
img.className = 'logo'
|
||||
view.appendChild(img)
|
||||
}
|
||||
|
||||
if (options.loading) {
|
||||
const loading = document.createElement('div')
|
||||
loading.className = 'loading'
|
||||
view.appendChild(loading)
|
||||
}
|
||||
|
||||
const text = document.createElement('p')
|
||||
text.className = 'guru_text'
|
||||
view.appendChild(text)
|
||||
|
||||
document.body.append(view)
|
||||
}
|
||||
|
||||
function hideLaunchView() {
|
||||
const view = document.querySelector('#launch_view')
|
||||
if (view) {
|
||||
view.style.display = 'none'
|
||||
}
|
||||
showFlutterView()
|
||||
}
|
||||
|
||||
function showFlutterView() {
|
||||
document.querySelector("flutter-view").style.display = "block"
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开debug dialog
|
||||
* @param {string} message 展示内容
|
||||
*/
|
||||
function showDebugDialog(message) {
|
||||
const view = document.querySelector("#debug_dialog")
|
||||
if (view) view.style.display = "flex"
|
||||
const content = document.querySelector(".content")
|
||||
content.innerHTML = message
|
||||
}
|
||||
|
||||
function closeDebugeDialog(e) {
|
||||
e.target.style.display = "none"
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "example",
|
||||
"short_name": "example",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,59 @@
|
|||
library guru_fb_game;
|
||||
import 'package:guru_fb_game/model/model.dart';
|
||||
import 'package:guru_fb_game/utils.dart';
|
||||
|
||||
class AppConfig {
|
||||
final String appVersion;
|
||||
final String appBuildNumber;
|
||||
final String guruLogAppId;
|
||||
final String bundleId;
|
||||
|
||||
final String? adUnitIdBanner;
|
||||
final String? adUnitIdInter;
|
||||
final String? adUnitIdRewardVideo;
|
||||
|
||||
const AppConfig({
|
||||
required this.appVersion,
|
||||
this.appBuildNumber = "1",
|
||||
required this.guruLogAppId,
|
||||
required this.bundleId,
|
||||
this.adUnitIdBanner,
|
||||
this.adUnitIdInter,
|
||||
this.adUnitIdRewardVideo
|
||||
});
|
||||
}
|
||||
|
||||
class GuruFbGame {
|
||||
static final GuruFbGame _instance = GuruFbGame._();
|
||||
|
||||
static GuruFbGame get instance => _instance;
|
||||
|
||||
GuruFbGame._();
|
||||
|
||||
FbProfile? fbProfile;
|
||||
|
||||
Map<String, String>? fbtournament;
|
||||
|
||||
Future _initialize(AppConfig config) async {
|
||||
// setUrlStrategy(null);
|
||||
fbProfile = await FbGameGlobalUtils.getFBProfile();
|
||||
final fbPlatform = FbGameGlobalUtils.getPlatform();
|
||||
final deviceInfo = GuruLogDeviceData(appId: config.bundleId, version: config.appVersion, fbPlatform: fbPlatform);
|
||||
final logInfoData = GuruLogInfoData(deveiceId: '', uid: fbProfile?.userId ?? '');
|
||||
final initData = InitGuruLogEventData(appId: config.guruLogAppId, deviceInfo: deviceInfo, info: logInfoData);
|
||||
FbGameLogUtils.initGuruLogEvent(initData);
|
||||
fbtournament = await FbGameTournamentUtils.getCurrentTournament();
|
||||
await initAds(config);
|
||||
}
|
||||
|
||||
Future initAds(AppConfig config) async{
|
||||
List<String> list = FbGameAdUtils.checkSupportedAds();
|
||||
if (list.contains('getInterstitialAdAsync') && config.adUnitIdInter != null) {
|
||||
FbGameAdUtils.loadInterstitial(config.adUnitIdInter!);
|
||||
}
|
||||
|
||||
if (list.contains('getRewardedVideoAsync') && config.adUnitIdRewardVideo != null) {
|
||||
FbGameAdUtils.loadRewarded(config.adUnitIdRewardVideo!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
part 'model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class FbProfile {
|
||||
final String photo;
|
||||
final String name;
|
||||
final String id;
|
||||
final String contextType;
|
||||
final String? userId;
|
||||
|
||||
const FbProfile({
|
||||
required this.photo,
|
||||
required this.name,
|
||||
required this.id,
|
||||
required this.contextType,
|
||||
this.userId
|
||||
});
|
||||
|
||||
factory FbProfile.fromJson(Map<String, dynamic> json) => _$FbProfileFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FbProfileToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class FbShareData {
|
||||
final String image;
|
||||
final String text;
|
||||
final Map<String, String> data;
|
||||
final List<String> shareDestination;
|
||||
final bool switchContext;
|
||||
|
||||
const FbShareData({
|
||||
this.image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAMAAADQmBKKAAAAAXNSR0IArs4c6QAAAkxQTFRFAAAAJCQkGhoaDAwMFxcXFhYWFBQUDAwMDAwMEBAQEBAQDw8PDg4ODg4ODQ0NDQ0NDQ0NDAwMDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDAwMDAwMDAwMBwcHCAgICQkJCgoKCwsLDAwMDg4ODw8PEBAQEhISFxcXGhoaGxsbHR0dHh4eHx8fICAgISEhIyMjJCQkJSUlJiYmLCwsLS0tMDAwMjIyMzMzNjY2Nzc3ODg4QEBAQUFBRERERkZGTk5OT09PU1NTV1dXWFhYW1tbXFxcXV1dX19fYGBgYWFhZmZmaGhoaWlpbGxsbm5ucXFxcnJydXV1dnZ2eHh4enp6e3t7fHx8fX19fn5+f39/gICAgoKCg4ODhISEhYWFiYmJi4uLjIyMjY2Njo6Oj4+PkJCQkZGRk5OTlZWVl5eXmZmZmpqam5ubnZ2dnp6eoKCgoaGho6OjpKSkp6enqKioqampqqqqrKysra2trq6ur6+vsLCwsrKys7OztLS0tbW1tra2t7e3uLi4ubm5urq6u7u7vLy8vr6+v7+/wMDAwcHBwsLCxMTExcXFxsbGx8fHyMjIycnJysrKy8vLzc3Nzs7Oz8/P0NDQ0dHR0tLS09PT1NTU1dXV1tbW2NjY2dnZ2tra29vb3t7e39/f4ODg4eHh4uLi4+Pj5OTk5eXl5ubm5+fn6Ojo6enp6urq6+vr7Ozs7e3t7u7u7+/v8PDw8fHx8vLy8/Pz9PT09fX19vb29/f3+Pj4+fn5+vr6+/v7/Pz8/f39/v7+////CfwD0AAAAB10Uk5TAAcKFRYXGT4/QEFGSXBzeaCms8LDxsff7PD5+/2lKaEhAAAGb0lEQVR42s3cCV8UZRwH8BWNI0CWQ46FnZ0ZyJJIs+ygsrSsqLD7wizKCsvMsuzGsEM6sBDIEkKJQwoRxJVjYVnYnXljLbvM7szu8zzzzDPPs/P8XsDO9/N/ZmafeS6XC5is/CJ3eUWlR2YQT2VFubsoP8uFnU2FpUwkKa7Swk1YnJySajlDqS7JMeXcWCZnNGV5SE52sZzxFGfDPQVVsgOpKoB5NtfIjqRmM5CzwS07FveGdM/GLbKD2bIxrT6OeqKi1Bq5ZYfjTrmfZcdjuLMLCJ8viSSwZ0339GdXZciCJFUl35DFGcQgRMWaJy+zGniraf9rZbY5IkYwRGVxTy45R7QYsxLlxkAlhBzdhXzmMYogVyiJ9Q+rSTj4EiMKDape60MWWucYMQJeDCLYZQqjoFKrnqRGsBadCHad0uj3hceaJ1XjxY0mQoI8Wa58Io5FjEYyBcn5riILHj0nfo1b9rR82jsy6V8Mq1gxBRWZ9zvSPBpn29MnJxTVWrSbCNELKbfiSXLqW4cjqvVobQa9XLmrAtMTL886Z3/vqkoUU1CFqxLbEy+P1yu+ekUljSmo0uWx5IlyWmdUlR3I48L3xMrzyGVVZQmSXZY8O39VVA5A6x6v99klVXUQlOqp/VZVnQSleu4dV7kAaZ69iyoNkEgMSvE8FVIpgSQyUIqnJazyAop5Dih0PKpICjIWqJlSfWyD1j17Qyo9kEQEMjRY45JKDyTZAa0VSBBq/6XnUSUykKFA36u8gOKe53Cuo0RWljESCpGB9AW6I2hiiUy0t+yuozP6gQbFC3QGrRl+5SaKwzEYoH2oN6LSu4vq8BAMpPNIiN6zMrybPscM9DbcM/+YhatYCAwUewdJ16Ge0XoWGiQoWqDXoZ5OkQ0HBNK12FXYw3VIZMRBgoRnYPVpY+eBgWIF+gvi+UVkxkGCGiC9oFFZZOdBgIQ2sGeugaUnHZR86C+BQfvNO1ksQNEWA/9rjIpMC4QAPQ/+w3iIbYEQoE4g6E/GHgQI+FZU7nMMVA+8hcbMv/RYgR4GttibrAsEB7UCW2w76wKBQWuvoXYQaMrnFMjnGwD2OhwEAQc32xwEXQOBHncQtAAC3e1z7qZeBoFudQi0ViLQ1IoicAty4MUIHKSq9zn31xEAge7y4ZdoYhoQGyA/CPSoBRCwxDZAk6Dfe0swn3pnBboA+r3vkvPKpiLKILED9HsTAn6JaIE00SHgN3QDfolog/YBO2gHBYwlJWxA4K+gIUFfIilzoKgI+NxH7jGKpAyCuoBtdkYwNBpCRA2ktdnL4A/FB1NFUqZAt4E/pQe9aSIpE6CoaBI82NAEEIFM9EFHwKDpbcK6SESO/gYjgBANC2uiHZDlL/0+4/ItSAIrxqxGEyYaOE+UaAgypPd1fLGUT0RWaXbJkGAwuBxasTO1IIkvwgY9DyRXlMFN07P6zM3NLwQWl8hAiRLNQUCrL2kiH3xV5/gVXaampq/O+GfnbU7gvQMdOP8ssewOahoa1Wfs0vh/k9PXJHtTnLUBqOjczXGSAF3WeW5Al78HL1wcHhmftAeSxA/gky+X96wvToShurp1OdvT98f5wX/GSCeBEyWaRUxPnW30Jk3p6fhRl9OdP3f93td/kXSaPFGiJ1ETeOGO23XLOFNz4ktdvvrm5KnOrp7zou2lFv3IKc6V7mYJtq7zw491Of7JiS/af+jqsw+602x9YGjgaHOjCAAdfl+XI0ePHf+8/fRv5ItREqKDWNPkoXl/amZBL0YboISom+ZCAhsLmhJLvrb6aYJoLIq7f4UTUELUFOYMJL2g8AFKit5V+ADRF9kF6VotwgcoKXpilQ9QUvTAdT5ASdHWHmff1Oki6Y1VCiDJNkgn2jWo8ADSk5oXWIM8FkV1x4I2QKYdNI/JNgqASKo7HGC39LTSbKMJkCS1jBC+J02/OipMt+JASDs/miG5v02/y8qtbZI2fLzveK1nQaENcmNs54KTJGl703s/jfgDy2GFEqgIZ8MbkkR5KWM+3pZA2ib4U5+FuWmSLgr+q6XY20opqlA/WGhh420mEtt4i781mX1K4ofF8APKsb69nWnKtANseAHlkR6RwCjFtg6RoB/dIRLEx2zQTE0BzweR8HdUC3+H2XB33A9/ByLxd2QUh4dq8Xfs2NrBNnwdzBY7SiaTR9flYp2mdwNfh/s5fPzh/zcuYJd5bFPWAAAAAElFTkSuQmCC',
|
||||
required this.text,
|
||||
this.data = const {},
|
||||
this.shareDestination = const [
|
||||
'NEWSFEED',
|
||||
'GROUP',
|
||||
'COPY_LINK',
|
||||
'MESSENGER'
|
||||
],
|
||||
this.switchContext = false,
|
||||
});
|
||||
|
||||
factory FbShareData.fromJson(Map<String, dynamic> json) => _$FbShareDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FbShareDataToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class FbInviteTextData {
|
||||
final String text;
|
||||
// localizations: {
|
||||
// ar_AR: 'X \u0641\u0642\u0637 \u063A\u0632\u062A ' +
|
||||
// '\u0642\u0631\u064A\u0629 Y!',
|
||||
// en_US: 'X just invaded Y\'s village!',
|
||||
// es_LA: '\u00A1X acaba de invadir el pueblo de Y!'
|
||||
// }
|
||||
final Map<String, String> localizations;
|
||||
|
||||
const FbInviteTextData({required this.text, required this.localizations});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'default': text,
|
||||
'localizations': localizations,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
factory FbInviteTextData.fromJson(Map<String, dynamic> json) => _$FbInviteTextDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FbInviteTextDataToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class FbInviteData {
|
||||
final String image;
|
||||
final FbInviteTextData text;
|
||||
final FbInviteTextData? cta;
|
||||
final FbInviteTextData? dialogTitle;
|
||||
final List<String>? filters; // ['NEW_CONTEXT_ONLY', 'EXISTING_PLAYERS_ONLY']
|
||||
// sections: [
|
||||
// {sectionType: 'GROUPS', maxResults: 2},
|
||||
// {sectionType: 'USERS'}
|
||||
// ],
|
||||
final List<Map<String, dynamic>>? sections;
|
||||
|
||||
const FbInviteData({
|
||||
required this.image,
|
||||
required this.text,
|
||||
this.cta,
|
||||
this.dialogTitle,
|
||||
this.filters,
|
||||
this.sections,
|
||||
});
|
||||
|
||||
factory FbInviteData.fromJson(Map<String, dynamic> json) => _$FbInviteDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FbInviteDataToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class GuruLogDeviceData{
|
||||
final String appId;
|
||||
final String version;
|
||||
final String fbPlatform;
|
||||
|
||||
GuruLogDeviceData({
|
||||
required this.appId,
|
||||
this.version = '1.0.0',
|
||||
required this.fbPlatform
|
||||
});
|
||||
|
||||
factory GuruLogDeviceData.fromJson(Map<String, dynamic> json) => _$GuruLogDeviceDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$GuruLogDeviceDataToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class GuruLogInfoData {
|
||||
final String? deveiceId;
|
||||
final String? uid;
|
||||
final String? adjustId;
|
||||
final String? adId;
|
||||
final String? firebaseId;
|
||||
|
||||
const GuruLogInfoData({
|
||||
this.deveiceId,
|
||||
this.uid,
|
||||
this.adjustId,
|
||||
this.adId,
|
||||
this.firebaseId
|
||||
});
|
||||
|
||||
factory GuruLogInfoData.fromJson(Map<String, dynamic> json) => _$GuruLogInfoDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$GuruLogInfoDataToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class InitGuruLogEventData {
|
||||
final String appId;
|
||||
final GuruLogDeviceData deviceInfo;
|
||||
final GuruLogInfoData info;
|
||||
|
||||
const InitGuruLogEventData({
|
||||
required this.appId,
|
||||
required this.deviceInfo,
|
||||
required this.info
|
||||
});
|
||||
|
||||
factory InitGuruLogEventData.fromJson(Map<String, dynamic> json) => _$InitGuruLogEventDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$InitGuruLogEventDataToJson(this);
|
||||
}
|
||||
|
||||
enum FBTournamentSortOrder{
|
||||
HIGHER_IS_BETTER, LOWER_IS_BETTER;
|
||||
}
|
||||
|
||||
enum FBTournamentSortFormat{
|
||||
NUMERIC, TIME;
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class FbTournamentConfig{
|
||||
final String? title;
|
||||
final String? image;
|
||||
// "HIGHER_IS_BETTER"| "LOWER_IS_BETTER"
|
||||
final FBTournamentSortOrder? sortOrder;
|
||||
// "NUMERIC" | "TIME"
|
||||
final FBTournamentSortFormat? scoreFormat;
|
||||
// timestamp
|
||||
final num? endTime;
|
||||
|
||||
const FbTournamentConfig({
|
||||
this.title,
|
||||
this.image,
|
||||
this.sortOrder,
|
||||
this.scoreFormat,
|
||||
this.endTime
|
||||
});
|
||||
|
||||
factory FbTournamentConfig.fromJson(Map<String, dynamic> json) => _$FbTournamentConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FbTournamentConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class FbUserInfo {
|
||||
@JsonKey(name: 'player_id', defaultValue: "")
|
||||
final String id;
|
||||
|
||||
@JsonKey(name: 'user_id', defaultValue: "")
|
||||
final String userId;
|
||||
|
||||
@JsonKey(name: 'name', defaultValue: "")
|
||||
final String name;
|
||||
|
||||
@JsonKey(name: 'picUrl', defaultValue: "")
|
||||
final String picUrl;
|
||||
|
||||
FbUserInfo(this.id, this.userId, this.name, this.picUrl);
|
||||
|
||||
factory FbUserInfo.fromJson(Map<String, dynamic> json) => _$FbUserInfoFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FbUserInfoToJson(this);
|
||||
}
|
||||
|
||||
enum FBContextType {
|
||||
@JsonValue("POST") POST,
|
||||
@JsonValue("THREAD") THREAD,
|
||||
@JsonValue("GROUP") GROUP,
|
||||
@JsonValue("SOLO") SOLO
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class FBContextInfo {
|
||||
final String id;
|
||||
final FBContextType type;
|
||||
|
||||
const FBContextInfo({
|
||||
required this.id,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
factory FBContextInfo.fromJson(Map<String, dynamic> json) => _$FBContextInfoFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FBContextInfoToJson(this);
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
FbProfile _$FbProfileFromJson(Map<String, dynamic> json) => FbProfile(
|
||||
photo: json['photo'] as String,
|
||||
name: json['name'] as String,
|
||||
id: json['id'] as String,
|
||||
contextType: json['contextType'] as String,
|
||||
userId: json['userId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FbProfileToJson(FbProfile instance) => <String, dynamic>{
|
||||
'photo': instance.photo,
|
||||
'name': instance.name,
|
||||
'id': instance.id,
|
||||
'contextType': instance.contextType,
|
||||
'userId': instance.userId,
|
||||
};
|
||||
|
||||
FbShareData _$FbShareDataFromJson(Map<String, dynamic> json) => FbShareData(
|
||||
image: json['image'] as String? ??
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAMAAADQmBKKAAAAAXNSR0IArs4c6QAAAkxQTFRFAAAAJCQkGhoaDAwMFxcXFhYWFBQUDAwMDAwMEBAQEBAQDw8PDg4ODg4ODQ0NDQ0NDQ0NDAwMDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDAwMDAwMDAwMBwcHCAgICQkJCgoKCwsLDAwMDg4ODw8PEBAQEhISFxcXGhoaGxsbHR0dHh4eHx8fICAgISEhIyMjJCQkJSUlJiYmLCwsLS0tMDAwMjIyMzMzNjY2Nzc3ODg4QEBAQUFBRERERkZGTk5OT09PU1NTV1dXWFhYW1tbXFxcXV1dX19fYGBgYWFhZmZmaGhoaWlpbGxsbm5ucXFxcnJydXV1dnZ2eHh4enp6e3t7fHx8fX19fn5+f39/gICAgoKCg4ODhISEhYWFiYmJi4uLjIyMjY2Njo6Oj4+PkJCQkZGRk5OTlZWVl5eXmZmZmpqam5ubnZ2dnp6eoKCgoaGho6OjpKSkp6enqKioqampqqqqrKysra2trq6ur6+vsLCwsrKys7OztLS0tbW1tra2t7e3uLi4ubm5urq6u7u7vLy8vr6+v7+/wMDAwcHBwsLCxMTExcXFxsbGx8fHyMjIycnJysrKy8vLzc3Nzs7Oz8/P0NDQ0dHR0tLS09PT1NTU1dXV1tbW2NjY2dnZ2tra29vb3t7e39/f4ODg4eHh4uLi4+Pj5OTk5eXl5ubm5+fn6Ojo6enp6urq6+vr7Ozs7e3t7u7u7+/v8PDw8fHx8vLy8/Pz9PT09fX19vb29/f3+Pj4+fn5+vr6+/v7/Pz8/f39/v7+////CfwD0AAAAB10Uk5TAAcKFRYXGT4/QEFGSXBzeaCms8LDxsff7PD5+/2lKaEhAAAGb0lEQVR42s3cCV8UZRwH8BWNI0CWQ46FnZ0ZyJJIs+ygsrSsqLD7wizKCsvMsuzGsEM6sBDIEkKJQwoRxJVjYVnYnXljLbvM7szu8zzzzDPPs/P8XsDO9/N/ZmafeS6XC5is/CJ3eUWlR2YQT2VFubsoP8uFnU2FpUwkKa7Swk1YnJySajlDqS7JMeXcWCZnNGV5SE52sZzxFGfDPQVVsgOpKoB5NtfIjqRmM5CzwS07FveGdM/GLbKD2bIxrT6OeqKi1Bq5ZYfjTrmfZcdjuLMLCJ8viSSwZ0339GdXZciCJFUl35DFGcQgRMWaJy+zGniraf9rZbY5IkYwRGVxTy45R7QYsxLlxkAlhBzdhXzmMYogVyiJ9Q+rSTj4EiMKDape60MWWucYMQJeDCLYZQqjoFKrnqRGsBadCHad0uj3hceaJ1XjxY0mQoI8Wa58Io5FjEYyBcn5riILHj0nfo1b9rR82jsy6V8Mq1gxBRWZ9zvSPBpn29MnJxTVWrSbCNELKbfiSXLqW4cjqvVobQa9XLmrAtMTL886Z3/vqkoUU1CFqxLbEy+P1yu+ekUljSmo0uWx5IlyWmdUlR3I48L3xMrzyGVVZQmSXZY8O39VVA5A6x6v99klVXUQlOqp/VZVnQSleu4dV7kAaZ69iyoNkEgMSvE8FVIpgSQyUIqnJazyAop5Dih0PKpICjIWqJlSfWyD1j17Qyo9kEQEMjRY45JKDyTZAa0VSBBq/6XnUSUykKFA36u8gOKe53Cuo0RWljESCpGB9AW6I2hiiUy0t+yuozP6gQbFC3QGrRl+5SaKwzEYoH2oN6LSu4vq8BAMpPNIiN6zMrybPscM9DbcM/+YhatYCAwUewdJ16Ge0XoWGiQoWqDXoZ5OkQ0HBNK12FXYw3VIZMRBgoRnYPVpY+eBgWIF+gvi+UVkxkGCGiC9oFFZZOdBgIQ2sGeugaUnHZR86C+BQfvNO1ksQNEWA/9rjIpMC4QAPQ/+w3iIbYEQoE4g6E/GHgQI+FZU7nMMVA+8hcbMv/RYgR4GttibrAsEB7UCW2w76wKBQWuvoXYQaMrnFMjnGwD2OhwEAQc32xwEXQOBHncQtAAC3e1z7qZeBoFudQi0ViLQ1IoicAty4MUIHKSq9zn31xEAge7y4ZdoYhoQGyA/CPSoBRCwxDZAk6Dfe0swn3pnBboA+r3vkvPKpiLKILED9HsTAn6JaIE00SHgN3QDfolog/YBO2gHBYwlJWxA4K+gIUFfIilzoKgI+NxH7jGKpAyCuoBtdkYwNBpCRA2ktdnL4A/FB1NFUqZAt4E/pQe9aSIpE6CoaBI82NAEEIFM9EFHwKDpbcK6SESO/gYjgBANC2uiHZDlL/0+4/ItSAIrxqxGEyYaOE+UaAgypPd1fLGUT0RWaXbJkGAwuBxasTO1IIkvwgY9DyRXlMFN07P6zM3NLwQWl8hAiRLNQUCrL2kiH3xV5/gVXaampq/O+GfnbU7gvQMdOP8ssewOahoa1Wfs0vh/k9PXJHtTnLUBqOjczXGSAF3WeW5Al78HL1wcHhmftAeSxA/gky+X96wvToShurp1OdvT98f5wX/GSCeBEyWaRUxPnW30Jk3p6fhRl9OdP3f93td/kXSaPFGiJ1ETeOGO23XLOFNz4ktdvvrm5KnOrp7zou2lFv3IKc6V7mYJtq7zw491Of7JiS/af+jqsw+602x9YGjgaHOjCAAdfl+XI0ePHf+8/fRv5ItREqKDWNPkoXl/amZBL0YboISom+ZCAhsLmhJLvrb6aYJoLIq7f4UTUELUFOYMJL2g8AFKit5V+ADRF9kF6VotwgcoKXpilQ9QUvTAdT5ASdHWHmff1Oki6Y1VCiDJNkgn2jWo8ADSk5oXWIM8FkV1x4I2QKYdNI/JNgqASKo7HGC39LTSbKMJkCS1jBC+J02/OipMt+JASDs/miG5v02/y8qtbZI2fLzveK1nQaENcmNs54KTJGl703s/jfgDy2GFEqgIZ8MbkkR5KWM+3pZA2ib4U5+FuWmSLgr+q6XY20opqlA/WGhh420mEtt4i781mX1K4ofF8APKsb69nWnKtANseAHlkR6RwCjFtg6RoB/dIRLEx2zQTE0BzweR8HdUC3+H2XB33A9/ByLxd2QUh4dq8Xfs2NrBNnwdzBY7SiaTR9flYp2mdwNfh/s5fPzh/zcuYJd5bFPWAAAAAElFTkSuQmCC',
|
||||
text: json['text'] as String,
|
||||
data: (json['data'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
) ??
|
||||
const {},
|
||||
shareDestination: (json['shareDestination'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const ['NEWSFEED', 'GROUP', 'COPY_LINK', 'MESSENGER'],
|
||||
switchContext: json['switchContext'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FbShareDataToJson(FbShareData instance) =>
|
||||
<String, dynamic>{
|
||||
'image': instance.image,
|
||||
'text': instance.text,
|
||||
'data': instance.data,
|
||||
'shareDestination': instance.shareDestination,
|
||||
'switchContext': instance.switchContext,
|
||||
};
|
||||
|
||||
FbInviteTextData _$FbInviteTextDataFromJson(Map<String, dynamic> json) =>
|
||||
FbInviteTextData(
|
||||
text: json['text'] as String,
|
||||
localizations: Map<String, String>.from(json['localizations'] as Map),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FbInviteTextDataToJson(FbInviteTextData instance) =>
|
||||
<String, dynamic>{
|
||||
'text': instance.text,
|
||||
'localizations': instance.localizations,
|
||||
};
|
||||
|
||||
FbInviteData _$FbInviteDataFromJson(Map<String, dynamic> json) => FbInviteData(
|
||||
image: json['image'] as String,
|
||||
text: FbInviteTextData.fromJson(json['text'] as Map<String, dynamic>),
|
||||
cta: json['cta'] == null
|
||||
? null
|
||||
: FbInviteTextData.fromJson(json['cta'] as Map<String, dynamic>),
|
||||
dialogTitle: json['dialogTitle'] == null
|
||||
? null
|
||||
: FbInviteTextData.fromJson(
|
||||
json['dialogTitle'] as Map<String, dynamic>),
|
||||
filters:
|
||||
(json['filters'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
sections: (json['sections'] as List<dynamic>?)
|
||||
?.map((e) => e as Map<String, dynamic>)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FbInviteDataToJson(FbInviteData instance) =>
|
||||
<String, dynamic>{
|
||||
'image': instance.image,
|
||||
'text': instance.text,
|
||||
'cta': instance.cta,
|
||||
'dialogTitle': instance.dialogTitle,
|
||||
'filters': instance.filters,
|
||||
'sections': instance.sections,
|
||||
};
|
||||
|
||||
GuruLogDeviceData _$GuruLogDeviceDataFromJson(Map<String, dynamic> json) =>
|
||||
GuruLogDeviceData(
|
||||
appId: json['appId'] as String,
|
||||
version: json['version'] as String? ?? '1.0.0',
|
||||
fbPlatform: json['fbPlatform'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GuruLogDeviceDataToJson(GuruLogDeviceData instance) =>
|
||||
<String, dynamic>{
|
||||
'appId': instance.appId,
|
||||
'version': instance.version,
|
||||
'fbPlatform': instance.fbPlatform,
|
||||
};
|
||||
|
||||
GuruLogInfoData _$GuruLogInfoDataFromJson(Map<String, dynamic> json) =>
|
||||
GuruLogInfoData(
|
||||
deveiceId: json['deveiceId'] as String?,
|
||||
uid: json['uid'] as String?,
|
||||
adjustId: json['adjustId'] as String?,
|
||||
adId: json['adId'] as String?,
|
||||
firebaseId: json['firebaseId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GuruLogInfoDataToJson(GuruLogInfoData instance) =>
|
||||
<String, dynamic>{
|
||||
'deveiceId': instance.deveiceId,
|
||||
'uid': instance.uid,
|
||||
'adjustId': instance.adjustId,
|
||||
'adId': instance.adId,
|
||||
'firebaseId': instance.firebaseId,
|
||||
};
|
||||
|
||||
InitGuruLogEventData _$InitGuruLogEventDataFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
InitGuruLogEventData(
|
||||
appId: json['appId'] as String,
|
||||
deviceInfo: GuruLogDeviceData.fromJson(
|
||||
json['deviceInfo'] as Map<String, dynamic>),
|
||||
info: GuruLogInfoData.fromJson(json['info'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InitGuruLogEventDataToJson(
|
||||
InitGuruLogEventData instance) =>
|
||||
<String, dynamic>{
|
||||
'appId': instance.appId,
|
||||
'deviceInfo': instance.deviceInfo,
|
||||
'info': instance.info,
|
||||
};
|
||||
|
||||
FbTournamentConfig _$FbTournamentConfigFromJson(Map<String, dynamic> json) =>
|
||||
FbTournamentConfig(
|
||||
title: json['title'] as String?,
|
||||
image: json['image'] as String?,
|
||||
sortOrder: $enumDecodeNullable(
|
||||
_$FBTournamentSortOrderEnumMap, json['sortOrder']),
|
||||
scoreFormat: $enumDecodeNullable(
|
||||
_$FBTournamentSortFormatEnumMap, json['scoreFormat']),
|
||||
endTime: json['endTime'] as num?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FbTournamentConfigToJson(FbTournamentConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'title': instance.title,
|
||||
'image': instance.image,
|
||||
'sortOrder': _$FBTournamentSortOrderEnumMap[instance.sortOrder],
|
||||
'scoreFormat': _$FBTournamentSortFormatEnumMap[instance.scoreFormat],
|
||||
'endTime': instance.endTime,
|
||||
};
|
||||
|
||||
const _$FBTournamentSortOrderEnumMap = {
|
||||
FBTournamentSortOrder.HIGHER_IS_BETTER: 'HIGHER_IS_BETTER',
|
||||
FBTournamentSortOrder.LOWER_IS_BETTER: 'LOWER_IS_BETTER',
|
||||
};
|
||||
|
||||
const _$FBTournamentSortFormatEnumMap = {
|
||||
FBTournamentSortFormat.NUMERIC: 'NUMERIC',
|
||||
FBTournamentSortFormat.TIME: 'TIME',
|
||||
};
|
||||
|
||||
FbUserInfo _$FbUserInfoFromJson(Map<String, dynamic> json) => FbUserInfo(
|
||||
json['player_id'] as String? ?? '',
|
||||
json['user_id'] as String? ?? '',
|
||||
json['name'] as String? ?? '',
|
||||
json['picUrl'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FbUserInfoToJson(FbUserInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'player_id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'name': instance.name,
|
||||
'picUrl': instance.picUrl,
|
||||
};
|
||||
|
||||
FBContextInfo _$FBContextInfoFromJson(Map<String, dynamic> json) =>
|
||||
FBContextInfo(
|
||||
id: json['id'] as String,
|
||||
type: $enumDecode(_$FBContextTypeEnumMap, json['type']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FBContextInfoToJson(FBContextInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'type': _$FBContextTypeEnumMap[instance.type]!,
|
||||
};
|
||||
|
||||
const _$FBContextTypeEnumMap = {
|
||||
FBContextType.POST: 'POST',
|
||||
FBContextType.THREAD: 'THREAD',
|
||||
FBContextType.GROUP: 'GROUP',
|
||||
FBContextType.SOLO: 'SOLO',
|
||||
};
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:js' as js;
|
||||
|
||||
import 'package:guru_fb_game/model/model.dart';
|
||||
|
||||
class FbGameGlobalUtils {
|
||||
static Future<dynamic> getFBProfile() {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('getFBProfileWithUid', [
|
||||
(success) => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> shareGame(FbShareData data) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('shareGame', [
|
||||
json.encode(data.toJson()),
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> inviteUser(FbInviteData data) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('inviteUser', [
|
||||
json.encode(data.toJson()),
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Map<String, dynamic> getEntryPointData() {
|
||||
final result = js.context.callMethod('getEntryPointData');
|
||||
return json.decode(result);
|
||||
}
|
||||
|
||||
static String getLocale() {
|
||||
final result = js.context.callMethod('getLocale');
|
||||
return result ?? "";
|
||||
}
|
||||
|
||||
static String getPlatform() {
|
||||
String result = "web";
|
||||
result = js.context.callMethod('getPlatform');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class FbGamePlayerUtils {
|
||||
static void saveData(Map<String, dynamic> data) {
|
||||
js.context.callMethod('fbSetData', [json.encode(data)]);
|
||||
}
|
||||
|
||||
static Future<dynamic> getData(List<String> keys) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('fbGetData', [
|
||||
json.encode(keys),
|
||||
(success) => completer.complete(json.decode(success)),
|
||||
(error) => completer.completeError(json.decode(error)),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
//获取当前环境下的用户信息,包括用户id,用户名称,用户头像
|
||||
Future<dynamic> getPlayersAsync() async {
|
||||
final completer = Completer<dynamic>();
|
||||
js.context.callMethod('getPlayersAsync', [
|
||||
(value) {
|
||||
List<Map<String, dynamic>> players = json.decode(value);
|
||||
final users = players.map((e) => FbUserInfo.fromJson(e)).toList();
|
||||
completer.complete(users);
|
||||
},
|
||||
(error) => completer.completeError(json.decode(error)),
|
||||
]);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家同玩好友的信息
|
||||
* 返回的值是数组
|
||||
*/
|
||||
Future<dynamic> getConnectedPlayersAsync() {
|
||||
final completer = Completer<dynamic>();
|
||||
js.context.callMethod('getConnectedPlayersAsync', [
|
||||
(value) {
|
||||
List<Map<String, dynamic>> players = json.decode(value);
|
||||
final users = players.map((e) => FbUserInfo.fromJson(e)).toList();
|
||||
completer.complete(users);
|
||||
},
|
||||
(error) => completer.completeError(json.decode(error)),
|
||||
]);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> subscribeBot() {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('subscribeBot', [
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(json.decode(error)),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
class FbGameTournamentUtils {
|
||||
static Future<dynamic> createTournament(num initialScore, FbTournamentConfig config, Map<String, String> data) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('createTournament', [
|
||||
initialScore,
|
||||
json.encode(config.toJson()),
|
||||
json.encode(data),
|
||||
(value) => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> postTournamentScore(String id, num score) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('postTournamentScore', [
|
||||
id,
|
||||
score,
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> shareTournament(num score, Map<String, dynamic> data) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('shareTournament', [
|
||||
score,
|
||||
json.encode(data),
|
||||
(value) => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> getCurrentTournament() {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('getCurrentTournament', [
|
||||
(success) => completer.complete(json.decode(success)),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
// success 返回 map
|
||||
// {
|
||||
// id: tournament.getContextID(),
|
||||
// endTime: tournament.getEndTime(),
|
||||
// title: tournament.getTitle(),
|
||||
// payLoad: tournament.getPayload()
|
||||
// }
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
class FbGameContextUtils {
|
||||
static Future<dynamic> createAsync(String playerId) {
|
||||
final completer = Completer<dynamic>();
|
||||
js.context.callMethod('createAsync', [
|
||||
playerId,
|
||||
(value) => completer.complete(),
|
||||
(error) => completer.completeError(json.decode(error)),
|
||||
]);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static void updateAsync(String img) {
|
||||
js.context.callMethod('updateAsync', [img]);
|
||||
}
|
||||
|
||||
static FBContextInfo getContext() {
|
||||
final result = js.context.callMethod('getContextInfo');
|
||||
return FBContextInfo.fromJson(json.decode(result));
|
||||
}
|
||||
|
||||
static Future<dynamic> contextSwitchAsync(String contextId) {
|
||||
final completer = Completer<dynamic>();
|
||||
js.context.callMethod('contextSwitchAsync', [
|
||||
contextId,
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(json.decode(error)),
|
||||
]);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
//切换当前环境,如果选择朋友,则可以获取到朋友的信息
|
||||
static Future<dynamic> chooseContext() async {
|
||||
final completer = Completer<dynamic>();
|
||||
js.context.callMethod('chooseContext', [
|
||||
(value) => completer.complete(FBContextInfo.fromJson(json.decode(value))),
|
||||
(error) => completer.completeError(json.decode(error)),
|
||||
]);
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
class FbGameLogUtils {
|
||||
static void fbLogEvent(String eventName, int valueToSum, Map<String, dynamic> params) {
|
||||
js.context.callMethod('logEvent', [eventName, valueToSum, json.encode(params)]);
|
||||
}
|
||||
|
||||
static void initGuruLogEvent(InitGuruLogEventData data) {
|
||||
js.context.callMethod('initEventLogger', [json.encode(data.toJson())]);
|
||||
}
|
||||
|
||||
static void guruLogEvent(String eventName, Map<String, dynamic> params, Map<String, dynamic> properties) {
|
||||
js.context.callMethod('castboxLogEvent', [eventName, json.encode(params), json.encode(properties)]);
|
||||
}
|
||||
}
|
||||
|
||||
class FbGameAdUtils {
|
||||
static List<String> checkSupportedAds() {
|
||||
final String listStr = js.context.callMethod("checkSupportedAds");
|
||||
List<String> list = json.decode(listStr);
|
||||
return list;
|
||||
}
|
||||
|
||||
static Future<dynamic> showBanner(String id) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('showBannerAds', [
|
||||
id,
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> hideBanner(String id) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('hideBannerAds', [
|
||||
id,
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> loadInterstitial(String id) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('loadInterstitial', [
|
||||
id,
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> showInterstitial() {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('showInterstitial', [
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> loadRewarded(String id) {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('loadRewarded', [
|
||||
id,
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<dynamic> showRewarded() {
|
||||
final completer = Completer<dynamic>();
|
||||
|
||||
js.context.callMethod('showRewarded', [
|
||||
() => completer.complete(),
|
||||
(error) => completer.completeError(error),
|
||||
]);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
name: guru_fb_game
|
||||
description: "A new Flutter package project."
|
||||
version: 0.0.1
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.3 <4.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: 2.4.7
|
||||
json_serializable: 6.7.1
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
#
|
||||
# For details regarding assets in packages, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
#
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# To add custom fonts to your package, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts in packages, see
|
||||
# https://flutter.dev/custom-fonts/#from-packages
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:guru_fb_game/guru_fb_game.dart';
|
||||
|
||||
void main() {
|
||||
test('adds one to input values', () {
|
||||
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue