update guru_app/guru_ui

Signed-off-by: Haoyi <haoyi.zhang@castbox.fm>
sdk/v3.0.0
Haoyi 2024-03-07 11:46:50 +08:00
parent 59b48f342c
commit ea55fd4551
221 changed files with 12047 additions and 1478 deletions

View File

@ -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

View File

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

View File

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

View File

@ -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 postDIO
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);
}
}

View File

@ -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));

View File

@ -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 IdsignIn
if (!accountDataStore.hasUid || credential.isAnonymous) {
Log.d("_loginGuruWithCredential!", tag: "Account");
return await _loginGuruWithCredential(credential);
} else {
Log.d("_associateCredential!");
// GuruUser idMetaDataTokenassociateSaasUser
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) {

View File

@ -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;
}
/// APPCredential
/// onAnonymousLogin
Future<bool> onAnonymousLogin(GuruUser loginUser, Credential credential) async {
return true;
}
///
/// onLogout
///
Future<bool> onConflict();
}

View File

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

View File

@ -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,9 +151,45 @@ 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;
static const tester = 10;
static const machine = 100;
}
}

View File

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

View File

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

View File

@ -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 {

View File

@ -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(

View File

@ -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);
// facebooknetwork
// 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);
GuruAnalytics.instance.logPurchase(totalRevenue,
currency: data.currency, contentId: "MAX", adPlatform: "MAX");
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);
}
}

View File

@ -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;
}
// excludedvalidateexcluded
// 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();
}
}

View File

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

View File

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

View File

@ -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) =>

View File

@ -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 "";
}
/// 使 10
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,
currency: currency,
contentId: contentId,
adPlatform: adPlatform,
additionParameters: parameters);
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,
required String method,
required int balance,
required int value,
}) async {
logEvent("earn_virtual_currency", <String, dynamic>{
"virtual_currency_name": virtualCurrencyName,
"item_category": method,
"value": value,
"balance": balance
});
Future<void> logEarnVirtualCurrency(
{required String virtualCurrencyName,
required String method,
required int balance,
required int value,
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,
"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);
}

View File

@ -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
}}");
}
}

View File

@ -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);

View File

@ -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) =>

View File

@ -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();

View File

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

View File

@ -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 {

View File

@ -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);

View File

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

View File

@ -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';
@ -18,7 +23,7 @@ import 'package:guru_utils/controller/controller.dart';
mixin AssetsAware on LifecycleController {
final BehaviorSubject<ProductStore<IapProduct>> _productStoreSubject =
BehaviorSubject.seeded(ProductStore());
BehaviorSubject.seeded(ProductStore());
ProductStore<IapProduct> get currentProductStore => _productStoreSubject.value;
@ -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;
}

View File

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

View File

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

View File

@ -0,0 +1,16 @@
part of "migrations.dart";
class _MigrationV3toV4 implements Migration {
@override
Future<MigrateResult> migrate(Transaction transaction) async {
// IF NOT EXISTStry catch
try {
await InventoryTable.createTable(transaction);
} catch (error, stacktrace) {
Log.w("ignore alter cmd!");
}
return MigrateResult.success;
}
}
final migration3to4 = _MigrationV3toV4();

View File

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

View File

@ -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();
}
}

View File

@ -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}}");
try {
await GuruDB.instance.deleteOrdersBySkus(expiredSkus);
} catch (error, stacktrace) {
Log.w("Failed to upsert order: $error $stacktrace",
tag: PropertyTags.iap);
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);
}
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)
.compareTo(int.tryParse(a.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)
.compareTo(int.tryParse(a.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,7 +578,13 @@ class IapManager {
final productDetails = loadedProductDetails[productId];
if (productDetails != null) {
await _completePurchase(productId, productDetails, details);
/// 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}");
@ -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");
return;
}
Log.i("ignoreInvalidResult $result", tag: "Iap");
//
await logRevenue(ordersReport, result);
return;
} 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];

View File

@ -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() {}
}

View File

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

View File

@ -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]);

View File

@ -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);
// }
}

View File

@ -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; // (logEarnVirtualCurrencyitem_category)
final bool sales; //
final double rate; // 1.0 :1.21.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});

View File

@ -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;
@ -19,16 +20,17 @@ class ProductProfile {
final Map<String, Set<ProductId>> _offerIds = {};
final List<Map<String, ProductId>> _idsMap =
List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
ProductProfile({required Set<ProductId> oneOffChargeIapIds,
required Set<ProductId> subscriptionsIapIds,
Set<ProductId> pointsIapIds = const <ProductId>{},
Set<ProductId> igcIds = const <ProductId>{},
Set<ProductId> rewardIds = const <ProductId>{},
this.groupMap = const <String, String>{},
List<ManifestBuilder> manifestBuilders = const <ManifestBuilder>[],
this.noAdsCapIds = const <ProductId>{}}) {
ProductProfile(
{required Set<ProductId> oneOffChargeIapIds,
required Set<ProductId> subscriptionsIapIds,
Set<ProductId> pointsIapIds = const <ProductId>{},
Set<ProductId> igcIds = const <ProductId>{},
Set<ProductId> rewardIds = const <ProductId>{},
this.groupMap = const <String, String>{},
List<ManifestBuilder> manifestBuilders = const <ManifestBuilder>[],
this.noAdsCapIds = const <ProductId>{}}) {
for (var productId in oneOffChargeIapIds) {
_define(productId, TransactionMethod.iap);
}
@ -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;
@ -114,11 +125,12 @@ class IapProfile {
final List<ProductId> subscriptionsIapIds = [];
final List<ProductId> noAdsCapIds;
final List<Map<String, ProductId>> _idsMap =
List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
IapProfile({required List<ProductId> oneOffChargeIapIds,
required List<ProductId> subscriptionsIapIds,
this.noAdsCapIds = const <ProductId>[]}) {
IapProfile(
{required List<ProductId> oneOffChargeIapIds,
required List<ProductId> subscriptionsIapIds,
this.noAdsCapIds = const <ProductId>[]}) {
for (var productId in oneOffChargeIapIds) {
_define(productId);
}
@ -130,7 +142,7 @@ class IapProfile {
bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty;
static final IapProfile invalid =
IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []);
IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []);
ProductId _define(ProductId productId) {
if (productId.isOneOffCharge) {

View File

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

View File

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

View File

@ -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)];
}
}

View File

@ -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);

View File

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

View File

@ -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),
};

View File

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

View File

@ -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';

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -6,4 +6,4 @@ mixin GlobalSettings {
final SettingBoolData isNoAds = SettingBoolData(PropertyKeys.isNoAds, defaultValue: false);
final SettingIntData bestLevel = SettingIntData(PropertyKeys.bestLevel, defaultValue: 1);
}
}

View File

@ -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';
@ -11,6 +12,4 @@ part 'test_guru_app_creator.g.dart';
@guruSpecCreator
AppSpec createSampleAppSpec(String flavor) {
return _GuruSpecFactory.create(flavor);
}
}

View File

@ -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 {

View File

@ -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/

View File

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View File

@ -0,0 +1 @@
TODO: Add your license here.

View File

@ -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.

View File

@ -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

View File

@ -0,0 +1,7 @@
library guru_fiam;
/// A Calculator.
class Calculator {
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}

View File

@ -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

View File

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

View File

@ -27,7 +27,7 @@ class _RootPackage extends RootPackage {
Log.isDebug = kDebugMode;
await DebugSettings.instance.refresh();
Initializer.initialPath = AppPages.initialPath;
RouteCenter.initialize(routeMatchers: [
@ -42,23 +42,34 @@ class _RootPackage extends RootPackage {
]);
}
@override
// TODO: implement localizationsDelegates
Iterable<LocalizationsDelegate> get localizationsDelegates => const [
// Built-in localization of basic text for Material widgets
GlobalMaterialLocalizations.delegate,
// Built-in localization for text direction LTR/RTL
GlobalWidgetsLocalizations.delegate,
// Built-in localization of basic text for Cupertino widgets
GlobalCupertinoLocalizations.delegate,
];
// Built-in localization of basic text for Material widgets
GlobalMaterialLocalizations.delegate,
// Built-in localization for text direction LTR/RTL
GlobalWidgetsLocalizations.delegate,
// Built-in localization of basic text for Cupertino widgets
GlobalCupertinoLocalizations.delegate,
];
@override
// TODO: implement supportedLocales
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 {

View File

@ -30,4 +30,6 @@ class _GuruSpecFactory {
}
}
class Flavors {}
class Flavors {
static const String classic = "classic";
}

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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/

View File

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View File

@ -0,0 +1 @@
TODO: Add your license here.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.
);
}
}

View File

@ -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"

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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"
}

View File

@ -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

View File

@ -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!);
}
}
}

View File

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

View File

@ -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',
};

View File

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

View File

@ -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

View File

@ -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', () {
});
}

30
guru_app/packages/guru_login/.gitignore vendored Normal file
View File

@ -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/

Some files were not shown because too many files have changed in this diff Show More