parent
59b48f342c
commit
ea55fd4551
|
|
@ -1,5 +1,7 @@
|
||||||
app_name: GuruApp
|
app_name: GuruApp
|
||||||
|
|
||||||
|
app_category: app
|
||||||
|
|
||||||
flavor: "guru_test"
|
flavor: "guru_test"
|
||||||
|
|
||||||
# App接入GuruApp的基础信息(下面内容必填)
|
# App接入GuruApp的基础信息(下面内容必填)
|
||||||
|
|
@ -81,7 +83,6 @@ deployment:
|
||||||
# ios 验证服务器的密码
|
# ios 验证服务器的密码
|
||||||
ios_validate_receipt_password: aa998877665544332211bb00cc
|
ios_validate_receipt_password: aa998877665544332211bb00cc
|
||||||
|
|
||||||
|
|
||||||
# 被标注的conversion点,在自打点库中将被以Emergency的优先级进行发送
|
# 被标注的conversion点,在自打点库中将被以Emergency的优先级进行发送
|
||||||
conversion_events:
|
conversion_events:
|
||||||
- first_rads_rewarded
|
- first_rads_rewarded
|
||||||
|
|
@ -166,6 +167,30 @@ deployment:
|
||||||
# 因此它的优先级永远不会大于正常的 Banner 广告, 默认值是 false
|
# 因此它的优先级永远不会大于正常的 Banner 广告, 默认值是 false
|
||||||
show_internal_ads_when_banner_unavailable: true
|
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:
|
ads_profile:
|
||||||
# Banner广告ID(变现提供)
|
# Banner广告ID(变现提供)
|
||||||
|
|
@ -202,7 +227,6 @@ ads_profile:
|
||||||
android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
|
||||||
|
|
||||||
remote_config:
|
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}'
|
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}'
|
analytics_config: '{"cap":"firebase|facebook|guru", "init_delay_s": 10}'
|
||||||
|
#
|
||||||
|
# _mapping:
|
||||||
|
# cdn_config: "cdn2_config"
|
||||||
|
|
||||||
|
|
||||||
products:
|
products:
|
||||||
# sku
|
# sku
|
||||||
|
|
@ -311,6 +339,7 @@ products:
|
||||||
manifest:
|
manifest:
|
||||||
category: "prop"
|
category: "prop"
|
||||||
details:
|
details:
|
||||||
|
sku: "{1}_{2}"
|
||||||
type: "prop"
|
type: "prop"
|
||||||
amount: 1
|
amount: 1
|
||||||
theme_id: "{1}"
|
theme_id: "{1}"
|
||||||
|
|
@ -389,15 +418,15 @@ products:
|
||||||
details:
|
details:
|
||||||
type: "igc"
|
type: "igc"
|
||||||
amount: 16000
|
amount: 16000
|
||||||
|
#
|
||||||
theme_mul:
|
# theme_mul:
|
||||||
sku: "theme_{category}_{theme_id}"
|
# sku: "theme_{category}_{theme_id}"
|
||||||
attr: possessive
|
# attr: possessive
|
||||||
method: igc
|
# method: igc
|
||||||
manifest:
|
# manifest:
|
||||||
category: "{1}"
|
# category: "{1}"
|
||||||
theme_id: "{2}"
|
# theme_id: "{2}"
|
||||||
cate: "{1}"
|
# cate: "{1}"
|
||||||
|
|
||||||
# adjust 相关配置
|
# adjust 相关配置
|
||||||
adjust_profile:
|
adjust_profile:
|
||||||
|
|
@ -429,3 +458,46 @@ adjust_profile:
|
||||||
android: 95fu7q
|
android: 95fu7q
|
||||||
ios: 1p8z5t
|
ios: 1p8z5t
|
||||||
|
|
||||||
|
experiments:
|
||||||
|
|
||||||
|
test:
|
||||||
|
start: 20240129T000000
|
||||||
|
end: 20240129T000000
|
||||||
|
audience:
|
||||||
|
filters:
|
||||||
|
- version:
|
||||||
|
opt: lt
|
||||||
|
mmp: 2.3.0
|
||||||
|
- country:
|
||||||
|
included: ""
|
||||||
|
excluded: "us,cn,en"
|
||||||
|
- platform:
|
||||||
|
android:
|
||||||
|
opt: gte
|
||||||
|
ver: 33
|
||||||
|
ios:
|
||||||
|
opt: gte
|
||||||
|
ver: 14
|
||||||
|
variant: 2
|
||||||
|
test2:
|
||||||
|
start: 20240129T000000
|
||||||
|
end: 20240129T000000
|
||||||
|
audience:
|
||||||
|
filters:
|
||||||
|
- version:
|
||||||
|
opt: lt
|
||||||
|
mmp: 2.3.0
|
||||||
|
- country:
|
||||||
|
included: "cn"
|
||||||
|
excluded: "us"
|
||||||
|
- platform:
|
||||||
|
android:
|
||||||
|
opt: lt
|
||||||
|
ver: 24
|
||||||
|
ios:
|
||||||
|
opt: gte
|
||||||
|
ver: 14
|
||||||
|
- new_user: true
|
||||||
|
variant: 5
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,41 @@
|
||||||
part of "account_manager.dart";
|
part of "account_manager.dart";
|
||||||
|
|
||||||
extension AccountAuthExtension on AccountManager {
|
extension AccountAuthExtension on AccountManager {
|
||||||
Future<AccountAuth> _authenticate(SaasUser saasUser,
|
Future<FirebaseAccountAuth> _loginFirebase(GuruUser guruUser,
|
||||||
{bool canRefreshFirebaseToken = true}) async {
|
{bool canRefreshFirebaseToken = true}) async {
|
||||||
User? firebaseUser;
|
User? firebaseUser;
|
||||||
SaasUser newSaasUser = saasUser;
|
GuruUser newGuruUser = guruUser;
|
||||||
firebaseUser = await _authenticateFirebase(saasUser).catchError((error) {
|
firebaseUser = await _authenticateFirebase(guruUser).catchError((error) {
|
||||||
Log.e("_authenticateFirebase error! $error", tag: "Account");
|
Log.e("_authenticateFirebase error! $error", tag: "Account");
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (firebaseUser == null && canRefreshFirebaseToken) {
|
if (firebaseUser == null && canRefreshFirebaseToken) {
|
||||||
try {
|
try {
|
||||||
newSaasUser = await _refreshFirebaseToken(saasUser);
|
newGuruUser = await _refreshFirebaseToken(guruUser);
|
||||||
return _authenticate(newSaasUser, canRefreshFirebaseToken: false);
|
return _loginFirebase(newGuruUser, canRefreshFirebaseToken: false);
|
||||||
} catch (error, stacktrace) {
|
} 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
|
return await GuruApi.instance
|
||||||
.renewFirebaseToken()
|
.renewFirebaseToken()
|
||||||
.then((tokenData) => oldSaasUser.copyWith(firebaseToken: tokenData.firebaseToken));
|
.then((tokenData) => oldSaasUser.copyWith(firebaseToken: tokenData.firebaseToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<User?> _authenticateFirebase(SaasUser saasUser) async {
|
Future<User?> _authenticateFirebase(GuruUser guruUser) async {
|
||||||
int retry = 0;
|
int retry = 0;
|
||||||
dynamic lastError;
|
dynamic lastError;
|
||||||
while (retry < 1) {
|
while (retry < 1) {
|
||||||
try {
|
try {
|
||||||
Log.i("[$retry] _authenticateFirebase:${saasUser.firebaseToken}", tag: "Account");
|
Log.i("[$retry] _authenticateFirebase:${guruUser.firebaseToken}", tag: "Account");
|
||||||
|
|
||||||
return await FirebaseAuth.instance
|
return await FirebaseAuth.instance
|
||||||
.signInWithCustomToken(saasUser.firebaseToken)
|
.signInWithCustomToken(guruUser.firebaseToken)
|
||||||
.then((result) => result.user);
|
.then((result) => result.user);
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
await Future.delayed(const Duration(milliseconds: 600));
|
await Future.delayed(const Duration(milliseconds: 600));
|
||||||
|
|
@ -48,4 +48,26 @@ extension AccountAuthExtension on AccountManager {
|
||||||
}
|
}
|
||||||
throw lastError ?? ("_authenticateFirebase error!");
|
throw lastError ?? ("_authenticateFirebase error!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> authenticateFirebase() async {
|
||||||
|
final guruUser = accountDataStore.user;
|
||||||
|
if (guruUser == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final auth = await _loginFirebase(guruUser);
|
||||||
|
final newGuruUser = auth.user;
|
||||||
|
if (!guruUser.isSame(newGuruUser)) {
|
||||||
|
_updateGuruUser(newGuruUser);
|
||||||
|
}
|
||||||
|
if (auth.firebaseUser != null) {
|
||||||
|
_updateFirebaseUser(auth.firebaseUser!);
|
||||||
|
Log.i("_updateFirebaseUser success!", tag: "Account");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
GuruAnalytics.instance.logException(error, stacktrace: stacktrace);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
part of "account_manager.dart";
|
||||||
|
|
||||||
|
extension AccountAuthInvoker on AccountManager {
|
||||||
|
Future<bool> _invokeLogin(GuruUser loginUser, Credential credential) async {
|
||||||
|
return await GuruApp.instance.protocol.accountAuthDelegate?.onLogin(loginUser, credential) ??
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _invokeLogout(GuruUser logoutUser) async {
|
||||||
|
return await GuruApp.instance.protocol.accountAuthDelegate?.onLogout(logoutUser) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _invokeAnonymousLogout(GuruUser logoutUser) async {
|
||||||
|
return await GuruApp.instance.protocol.accountAuthDelegate?.onAnonymousLogout(logoutUser) ??
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _invokeAnonymousLogin(GuruUser loginUser, Credential credential) async {
|
||||||
|
return await GuruApp.instance.protocol.accountAuthDelegate?.onAnonymousLogin(loginUser, credential) ??
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _invokeConflict() async {
|
||||||
|
return await GuruApp.instance.protocol.accountAuthDelegate?.onConflict() ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
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/account_profile.dart';
|
||||||
import 'package:guru_app/account/model/user.dart';
|
import 'package:guru_app/account/model/user.dart';
|
||||||
import 'package:guru_app/analytics/guru_analytics.dart';
|
import 'package:guru_app/analytics/guru_analytics.dart';
|
||||||
import 'package:guru_app/api/guru_api.dart';
|
import 'package:guru_app/api/guru_api.dart';
|
||||||
import 'package:guru_app/guru_app.dart';
|
import 'package:guru_app/guru_app.dart';
|
||||||
import 'package:guru_app/property/app_property.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_info.dart';
|
||||||
import 'package:guru_utils/extensions/extensions.dart';
|
import 'package:guru_utils/extensions/extensions.dart';
|
||||||
|
|
||||||
|
|
@ -19,11 +21,15 @@ class AccountDataStore {
|
||||||
static final AccountDataStore instance = AccountDataStore._();
|
static final AccountDataStore instance = AccountDataStore._();
|
||||||
|
|
||||||
final BehaviorSubject<DeviceInfo?> _deviceInfoSubject = BehaviorSubject.seeded(null);
|
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<User?> _firebaseUser = BehaviorSubject.seeded(null);
|
||||||
final BehaviorSubject<AccountProfile?> _accountProfile = BehaviorSubject.seeded(null);
|
final BehaviorSubject<AccountProfile?> _accountProfile = BehaviorSubject.seeded(null);
|
||||||
final BehaviorSubject<AccountDataStatus> _accountDataStatus =
|
final BehaviorSubject<AccountDataStatus> _accountDataStatus =
|
||||||
BehaviorSubject<AccountDataStatus>.seeded(AccountDataStatus.idle);
|
BehaviorSubject<AccountDataStatus>.seeded(AccountDataStatus.idle);
|
||||||
|
|
||||||
|
final BehaviorSubject<Map<AuthType, Credential>> _credentials =
|
||||||
|
BehaviorSubject.seeded(<AuthType, Credential>{});
|
||||||
|
|
||||||
int initRetryCount = 0;
|
int initRetryCount = 0;
|
||||||
|
|
||||||
Stream<AccountProfile?> get observableAccountProfile => _accountProfile.stream;
|
Stream<AccountProfile?> get observableAccountProfile => _accountProfile.stream;
|
||||||
|
|
@ -32,9 +38,12 @@ class AccountDataStore {
|
||||||
|
|
||||||
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;
|
AccountProfile? get accountProfile => _accountProfile.value;
|
||||||
|
|
||||||
|
|
@ -42,7 +51,7 @@ class AccountDataStore {
|
||||||
|
|
||||||
String? get countryCode => _accountProfile.value?.countryCode;
|
String? get countryCode => _accountProfile.value?.countryCode;
|
||||||
|
|
||||||
SaasUser? get user => _saasUserSubject.value;
|
GuruUser? get user => _guruUserSubject.value;
|
||||||
|
|
||||||
String? get avatar => _accountProfile.value?.avatar;
|
String? get avatar => _accountProfile.value?.avatar;
|
||||||
|
|
||||||
|
|
@ -55,16 +64,42 @@ class AccountDataStore {
|
||||||
Stream<bool> get observableInitialized =>
|
Stream<bool> get observableInitialized =>
|
||||||
_accountDataStatus.stream.map((status) => status == AccountDataStatus.initialized);
|
_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() {
|
void dispose() {
|
||||||
_deviceInfoSubject.close();
|
_deviceInfoSubject.close();
|
||||||
_saasUserSubject.close();
|
_guruUserSubject.close();
|
||||||
_firebaseUser.close();
|
_firebaseUser.close();
|
||||||
_accountProfile.close();
|
_accountProfile.close();
|
||||||
|
_credentials.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SaasUser?> signInAnonymousInLocked() async {
|
Future<GuruUser?> signInAnonymousInLocked() async {
|
||||||
// 这里需要使用原始的http post请求。否则这里将会死锁DIO所有请求
|
// 这里需要使用原始的http post请求。否则这里将会死锁DIO所有请求
|
||||||
final secret = await AppProperty.getInstance().getAnonymousSecretKey();
|
final secret = await AppProperty.getInstance().getAnonymousSecretKey();
|
||||||
final headers = {
|
final headers = {
|
||||||
|
|
@ -82,7 +117,7 @@ class AccountDataStore {
|
||||||
final data = const Utf8Decoder().convert(response.bodyBytes);
|
final data = const Utf8Decoder().convert(response.bodyBytes);
|
||||||
if (data.isNotEmpty) {
|
if (data.isNotEmpty) {
|
||||||
final result = json.decode(data);
|
final result = json.decode(data);
|
||||||
return SaasUser.fromJson(result["data"]);
|
return GuruUser.fromJson(result["data"]);
|
||||||
}
|
}
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Log.v("signInAnonymousInLocked error:$error", tag: "Account");
|
Log.v("signInAnonymousInLocked error:$error", tag: "Account");
|
||||||
|
|
@ -91,9 +126,9 @@ class AccountDataStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future refreshAuth() async {
|
Future refreshAuth() async {
|
||||||
final saasUser = await signInAnonymousInLocked();
|
final guruUser = await signInAnonymousInLocked();
|
||||||
if (saasUser != null) {
|
if (guruUser != null) {
|
||||||
updateSaasUser(saasUser);
|
updateGuruUser(guruUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,12 +136,16 @@ class AccountDataStore {
|
||||||
_deviceInfoSubject.addEx(deviceInfo);
|
_deviceInfoSubject.addEx(deviceInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSaasUser(SaasUser saasUser) {
|
@Deprecated("use updateGuruUser instead")
|
||||||
_saasUserSubject.addEx(saasUser);
|
void updateSaasUser(GuruUser saasUser) {
|
||||||
|
updateGuruUser(saasUser);
|
||||||
|
}
|
||||||
|
|
||||||
if (saasUser.createAtTimestamp > 0) {
|
void updateGuruUser(GuruUser guruUser) {
|
||||||
|
_guruUserSubject.addEx(guruUser);
|
||||||
|
if (guruUser.createAtTimestamp > 0) {
|
||||||
GuruAnalytics.instance
|
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);
|
_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}) {
|
bool transitionTo(AccountDataStatus status, {AccountDataStatus? expectStatus}) {
|
||||||
return _accountDataStatus.addIfChanged(status);
|
return _accountDataStatus.addIfChanged(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
_guruUserSubject.addEx(null);
|
||||||
|
_firebaseUser.addEx(null);
|
||||||
|
_deviceInfoSubject.addEx(null);
|
||||||
|
_accountProfile.addEx(null);
|
||||||
|
_accountDataStatus.addIfChanged(AccountDataStatus.idle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:guru_app/account/account_data_store.dart';
|
import 'package:guru_app/account/account_data_store.dart';
|
||||||
import 'package:guru_app/account/model/account.dart';
|
import 'package:guru_app/account/model/account.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/api/guru_api.dart';
|
||||||
import 'package:guru_app/firebase/firebase.dart';
|
import 'package:guru_app/firebase/firebase.dart';
|
||||||
import 'package:guru_app/firebase/firestore/firestore_manager.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/app_property.dart';
|
||||||
import 'package:guru_app/property/settings/guru_settings.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/collection/collectionutils.dart';
|
||||||
import 'package:guru_utils/core/ext.dart';
|
import 'package:guru_utils/core/ext.dart';
|
||||||
import 'package:guru_utils/datetime/datetime_utils.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/log/log.dart';
|
||||||
import 'package:guru_utils/network/network_utils.dart';
|
import 'package:guru_utils/network/network_utils.dart';
|
||||||
|
|
||||||
|
import 'model/credential.dart';
|
||||||
|
|
||||||
/// Created by Haoyi on 6/3/21
|
/// Created by Haoyi on 6/3/21
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
|
|
@ -26,6 +31,8 @@ part "account_service_extension.dart";
|
||||||
|
|
||||||
part "account_auth_extension.dart";
|
part "account_auth_extension.dart";
|
||||||
|
|
||||||
|
part "account_auth_invoker.dart";
|
||||||
|
|
||||||
class ModifyNicknameException implements Exception {
|
class ModifyNicknameException implements Exception {
|
||||||
final String? message;
|
final String? message;
|
||||||
final dynamic cause;
|
final dynamic cause;
|
||||||
|
|
@ -53,11 +60,13 @@ class ModifyLevelException implements Exception {
|
||||||
class AccountManager {
|
class AccountManager {
|
||||||
final AccountDataStore accountDataStore;
|
final AccountDataStore accountDataStore;
|
||||||
|
|
||||||
// final FirestoreService firestoreService;
|
|
||||||
|
|
||||||
Timer? retryTimer;
|
Timer? retryTimer;
|
||||||
|
|
||||||
static AccountManager instance = AccountManager();
|
static final AccountManager instance = AccountManager();
|
||||||
|
|
||||||
|
static const List<AuthCredentialDelegate> defaultSupportedAuthCredentialDelegates = [
|
||||||
|
AnonymousCredentialDelegate()
|
||||||
|
];
|
||||||
|
|
||||||
AccountManager() : accountDataStore = AccountDataStore.instance;
|
AccountManager() : accountDataStore = AccountDataStore.instance;
|
||||||
|
|
||||||
|
|
@ -109,6 +118,117 @@ class AccountManager {
|
||||||
accountDataStore.updateAccountProfile(dirtyAccountProfile);
|
accountDataStore.updateAccountProfile(dirtyAccountProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 登录
|
||||||
|
///
|
||||||
|
/// [authType] 登录类型
|
||||||
|
/// [onConflict] 登录冲突处理
|
||||||
|
/// [onLogin] 登录成功处理
|
||||||
|
///
|
||||||
|
Future<bool> loginWith(AuthType authType) async {
|
||||||
|
late final Credential? credential;
|
||||||
|
try {
|
||||||
|
final result = await AuthCredentialManager.instance.loginWith(authType);
|
||||||
|
credential = result.credential;
|
||||||
|
if (!result.isSuccess || credential == null) {
|
||||||
|
Log.w("loginWith $authType error! credential: [$credential]", tag: "Account");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.e("loginWith $authType error:$error, $stacktrace");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
/// 如果冲突将会报 409 的错
|
||||||
|
final guruUser = await _requestGuruUser(credential);
|
||||||
|
await processLogin(guruUser, credential);
|
||||||
|
return true;
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.w("loginWith $authType error:$error, $stacktrace");
|
||||||
|
if (error is DioError && error.response?.statusCode == 409) {
|
||||||
|
return await _processConflict(credential);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> processLogin(GuruUser user, Credential credential) async {
|
||||||
|
await _updateGuruUser(user);
|
||||||
|
await _bindCredential(credential);
|
||||||
|
try {
|
||||||
|
await _verifyOrReportAuthDevice(user);
|
||||||
|
authenticateFirebase();
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.e("_verifyOrReportAuthDevice error!$error $stacktrace");
|
||||||
|
}
|
||||||
|
if (credential.isAnonymous) {
|
||||||
|
return await _invokeAnonymousLogin(user, credential);
|
||||||
|
} else {
|
||||||
|
return await _invokeLogin(user, credential);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 登出操作,会把所有的第三方登陆都登出,如果当前有对应的匿名登陆的 credential 那么保留到匿名登陆的状态
|
||||||
|
/// 如果当前没有匿名登录的 credential 那么这里将会重新创建一个匿名登陆,并且这里不会进行数据迁移
|
||||||
|
///
|
||||||
|
/// 如果在调用 logout 时,明确指定了要登出哪些 AuthType,
|
||||||
|
/// 那么 logout 方法将尝试用 unbind 的方法去处理用户信息。
|
||||||
|
/// 当尝试解绑掉指定的 authTypes 时,只要满足下面两种情况的其中一种,
|
||||||
|
/// 都不能以 unbind 形式进行处理,都会认定为是真正的 logout
|
||||||
|
/// 1. 如果解绑掉所有指定的凭证后,当前凭证信息只保留了一个匿名凭证
|
||||||
|
/// 2. 如果解绑掉所有指定的凭证后,当前没有任何凭证信息
|
||||||
|
/// 如果上面两个条件都不满足,那么将以 unbind方法进行 logout
|
||||||
|
/// 以 unbind 形式进行 logout时,将不会通知应用 onLogout方法
|
||||||
|
///
|
||||||
|
/// 因此这里需要注意,就算明确指定了登出的 AuthType,依然存在调用 onLogout 的情况
|
||||||
|
///
|
||||||
|
Future<GuruUser?> logout({bool switching = false, Set<AuthType>? authTypes}) async {
|
||||||
|
bool isUnbind = false;
|
||||||
|
|
||||||
|
if (authTypes != null && authTypes.isNotEmpty) {
|
||||||
|
final currentCredentials = accountDataStore.credentials.keys.toSet();
|
||||||
|
currentCredentials.removeAll(authTypes);
|
||||||
|
currentCredentials.remove(AuthType.anonymous);
|
||||||
|
isUnbind = currentCredentials.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
final logoutUser = accountDataStore.user?.copyWith();
|
||||||
|
try {
|
||||||
|
if (!isUnbind && logoutUser != null) {
|
||||||
|
final result = await _invokeLogout(logoutUser);
|
||||||
|
if (!result) {
|
||||||
|
Log.w("logout error! ignore!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.w("invokeLogout error! $error!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (var authType in accountDataStore.credentials.keys) {
|
||||||
|
/// 默认的登出只是 unbind 掉三方的 credentials ,不会真正的登出
|
||||||
|
/// 如果当前没有匿名登陆,那么就会真正的登出,并会重新登陆匿名,但是数据不会清除
|
||||||
|
/// 如果 authTypes传的是 null,这里会返回空,依然满足不等于 False,
|
||||||
|
/// 这里只要是空或是真正的包含才会进行真正的解绑
|
||||||
|
if (authTypes?.contains(authType) != false && authType != AuthType.anonymous) {
|
||||||
|
await AuthCredentialManager.instance.logout(authType);
|
||||||
|
_unbindCredential(authType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 如果当前连匿名登陆也没有了,那么就会重新登陆匿名帐号
|
||||||
|
/// 如果是正在切换帐号的话,这里不需要登录一个新的匿名帐号
|
||||||
|
if (!switching && accountDataStore.credentials.isEmpty) {
|
||||||
|
final auth = await _retrieveAnonymous();
|
||||||
|
if (auth != null) {
|
||||||
|
await processLogin(auth.user, auth.credential!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logoutUser;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> modifyProfile(
|
Future<bool> modifyProfile(
|
||||||
{String? nickname,
|
{String? nickname,
|
||||||
String? avatar,
|
String? avatar,
|
||||||
|
|
@ -134,6 +254,10 @@ class AccountManager {
|
||||||
});
|
});
|
||||||
await updateLocalProfile(modifiedJson);
|
await updateLocalProfile(modifiedJson);
|
||||||
|
|
||||||
|
/// 如果本地部署没有打开同步 AccountProfile 机制,这里直接返回 true
|
||||||
|
if (!GuruApp.instance.appSpec.deployment.enabledSyncAccountProfile) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
while ((await NetworkUtils.isNetworkConnected()) && retryCount-- > 0) {
|
while ((await NetworkUtils.isNetworkConnected()) && retryCount-- > 0) {
|
||||||
final accountProfile =
|
final accountProfile =
|
||||||
await FirestoreManager.instance.modifyProfile(modifiedJson).onError((error, stackTrace) {
|
await FirestoreManager.instance.modifyProfile(modifiedJson).onError((error, stackTrace) {
|
||||||
|
|
@ -149,7 +273,9 @@ class AccountManager {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
Log.i("[$retryCount] modify profile error!", tag: "Account");
|
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");
|
Log.i("re-authenticate error:$error", stackTrace: stackTrace, tag: "Account");
|
||||||
});
|
});
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
/// Created by Haoyi on 6/3/21
|
/// Created by Haoyi on 6/3/21
|
||||||
|
|
||||||
part of "account_manager.dart";
|
part of "account_manager.dart";
|
||||||
|
|
||||||
extension AccountServiceExtension on AccountManager {
|
extension AccountServiceExtension on AccountManager {
|
||||||
Future<bool> _restoreAccount(Account account) async {
|
Future<bool> _restoreAccount(Account account) async {
|
||||||
SaasUser? saasUser = account.saasUser;
|
AccountAuth? anonymousAuth;
|
||||||
Log.d("restoreAccount $saasUser", tag: "Account");
|
GuruUser? guruUser = account.guruUser;
|
||||||
saasUser ??= await signInWithAnonymous().catchError((error, stacktrace) {
|
Log.d("restoreAccount $guruUser", tag: "Account");
|
||||||
Log.v("signInWithAnonymous error:$error, $stacktrace");
|
try {
|
||||||
return null;
|
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;
|
final device = account.device;
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
_updateDevice(device);
|
_updateDevice(device);
|
||||||
|
|
@ -22,45 +26,116 @@ extension AccountServiceExtension on AccountManager {
|
||||||
_updateAccountProfile(accountProfile);
|
_updateAccountProfile(accountProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saasUser != null) {
|
final credentials = account.credentials;
|
||||||
_updateSaasUser(saasUser);
|
if (credentials.isNotEmpty) {
|
||||||
await _verifyOrReportAuthDevice(saasUser);
|
_restoreCredentials(credentials);
|
||||||
final auth = await authenticate();
|
}
|
||||||
if (auth == null) {
|
|
||||||
return false;
|
if (guruUser != null) {
|
||||||
}
|
await _updateGuruUser(guruUser);
|
||||||
|
await _verifyOrReportAuthDevice(guruUser);
|
||||||
|
await authenticateFirebase();
|
||||||
if (accountProfile != null) {
|
if (accountProfile != null) {
|
||||||
await _checkOrUploadAccountProfile(accountProfile);
|
await _checkOrUploadAccountProfile(accountProfile);
|
||||||
}
|
}
|
||||||
|
if (anonymousAuth != null) {
|
||||||
|
final anonymousCredential = anonymousAuth.credential;
|
||||||
|
if (anonymousCredential != null) {
|
||||||
|
_bindCredential(anonymousCredential);
|
||||||
|
return await _invokeAnonymousLogin(anonymousAuth.user, anonymousCredential);
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AccountAuth?> authenticate() async {
|
Future switchUser(GuruUser newUser) async {
|
||||||
final saasUser = accountDataStore.user;
|
/// 更新 login 的用户信息
|
||||||
if (saasUser == null) {
|
_updateGuruUser(newUser);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
final auth = await _authenticate(saasUser);
|
await _verifyOrReportAuthDevice(newUser);
|
||||||
final newSaasUser = auth.user;
|
// 登陆 firebase 不需要同步等待
|
||||||
if (newSaasUser != null && !saasUser.isSame(newSaasUser)) {
|
authenticateFirebase();
|
||||||
_updateSaasUser(newSaasUser);
|
|
||||||
}
|
|
||||||
if (auth.firebaseUser != null) {
|
|
||||||
_updateFirebaseUser(auth.firebaseUser!);
|
|
||||||
Log.i("_updateFirebaseUser success!", tag: "Account");
|
|
||||||
}
|
|
||||||
return auth;
|
|
||||||
} catch (error, stacktrace) {
|
} 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 DeviceInfo? deviceInfo = await AppProperty.getInstance().getAccountDevice();
|
||||||
final firebasePushToken = await RemoteMessagingManager.instance.getToken();
|
final firebasePushToken = await RemoteMessagingManager.instance.getToken();
|
||||||
|
|
||||||
|
|
@ -73,13 +148,39 @@ extension AccountServiceExtension on AccountManager {
|
||||||
return DeviceTrack(null, deviceInfo);
|
return DeviceTrack(null, deviceInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SaasUser?> signInWithAnonymous() async {
|
Future<AccountAuth?> _retrieveAnonymous() async {
|
||||||
final anonymousSecretKey = await AppProperty.getInstance().getAnonymousSecretKey();
|
final result = await AuthCredentialManager.instance.loginWith(AuthType.anonymous);
|
||||||
return GuruApi.instance.signInWithAnonymous(secret: anonymousSecretKey);
|
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 {
|
Future<GuruUser> _loginGuruWithCredential(Credential credential) async {
|
||||||
final deviceTrack = await _buildDevice(saasUser);
|
return await GuruApi.instance.loginGuruWithCredential(credential: credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<GuruUser> _associateCredential(Credential credential) async {
|
||||||
|
return await GuruApi.instance.associateCredential(credential: credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<GuruUser> _requestGuruUser(Credential credential) async {
|
||||||
|
//MetaData是匿名请求,或者当前没有任何 GuruUser Id,走signIn接口
|
||||||
|
if (!accountDataStore.hasUid || credential.isAnonymous) {
|
||||||
|
Log.d("_loginGuruWithCredential!", tag: "Account");
|
||||||
|
return await _loginGuruWithCredential(credential);
|
||||||
|
} else {
|
||||||
|
Log.d("_associateCredential!");
|
||||||
|
//当前有 GuruUser id,并且MetaData是三方登录Token,走associate接口(不管已有的SaasUser是不是三方登录)
|
||||||
|
return await _associateCredential(credential);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _verifyOrReportAuthDevice(GuruUser guruUser) async {
|
||||||
|
final deviceTrack = await _buildDevice(guruUser);
|
||||||
final latestReportDeviceTimestamp =
|
final latestReportDeviceTimestamp =
|
||||||
await AppProperty.getInstance().getLatestReportDeviceTimestamp();
|
await AppProperty.getInstance().getLatestReportDeviceTimestamp();
|
||||||
final elapsedInterval = DateTimeUtils.currentTimeInMillis() - latestReportDeviceTimestamp;
|
final elapsedInterval = DateTimeUtils.currentTimeInMillis() - latestReportDeviceTimestamp;
|
||||||
|
|
@ -89,7 +190,7 @@ extension AccountServiceExtension on AccountManager {
|
||||||
if (deviceId.isNotEmpty) {
|
if (deviceId.isNotEmpty) {
|
||||||
GuruAnalytics.instance.setDeviceId(deviceId);
|
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((_) {
|
final result = await GuruApi.instance.reportDevice(reportDevice!).then((_) {
|
||||||
return true;
|
return true;
|
||||||
}).catchError((error) {
|
}).catchError((error) {
|
||||||
|
|
@ -135,10 +236,32 @@ extension AccountServiceExtension on AccountManager {
|
||||||
accountDataStore.updateDeviceInfo(device);
|
accountDataStore.updateDeviceInfo(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateSaasUser(SaasUser saasUser) {
|
Future _bindCredential(Credential credential) async {
|
||||||
accountDataStore.updateSaasUser(saasUser);
|
accountDataStore.bindCredential(credential);
|
||||||
AppProperty.getInstance().setAccountSaasUser(saasUser);
|
|
||||||
GuruAnalytics.instance.setUserId(saasUser.uid);
|
/// 这里匿名帐号是不会保存凭证的,因为匿名帐号的登陆凭证是自生成的
|
||||||
|
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) {
|
void _updateFirebaseUser(User user) {
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,108 @@
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
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/account_profile.dart';
|
||||||
import 'package:guru_app/account/model/user.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/device/device_info.dart';
|
||||||
|
import 'package:guru_utils/property/app_property.dart';
|
||||||
|
|
||||||
/// Created by Haoyi on 6/3/21
|
/// Created by Haoyi on 6/3/21
|
||||||
|
|
||||||
class Account {
|
class Account {
|
||||||
final SaasUser? saasUser;
|
final GuruUser? guruUser;
|
||||||
final DeviceInfo? device;
|
final DeviceInfo? device;
|
||||||
final AccountProfile? accountProfile;
|
final AccountProfile? accountProfile;
|
||||||
final User? firebaseUser;
|
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;
|
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 {
|
class AccountAuth {
|
||||||
final SaasUser? user;
|
final GuruUser user;
|
||||||
final User? firebaseUser;
|
final Credential? credential;
|
||||||
|
|
||||||
AccountAuth(this.user, this.firebaseUser);
|
AccountAuth(this.user, {this.credential});
|
||||||
|
|
||||||
bool get isValid => uid != null && uid != "";
|
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;
|
bool get existsFirebaseUser => firebaseUser != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AccountAuth{user: $user, firebaseUser: $firebaseUser}';
|
return 'AccountAuth{user: $user}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class IAccountAuthDelegate {
|
||||||
|
/// 支持登陆的代理,这块不管返回不返回都会支持匿名登陆
|
||||||
|
List<AuthCredentialDelegate> get supportedAuthCredentialDelegates =>
|
||||||
|
AccountManager.defaultSupportedAuthCredentialDelegates;
|
||||||
|
|
||||||
|
/// 返回设备共享的用户属性,在帐号切换的时候会保证这些 KEY,会保留下来
|
||||||
|
/// 注意,这里尽量不要把用户相关的属性设到这里面,否则会出现不必要的问题
|
||||||
|
Set<PropertyKey> get deviceSharedProperties => {};
|
||||||
|
|
||||||
|
/// 这个方法调用时,中台会确保当前的数据系统是切换后的数据系统
|
||||||
|
/// 因此可以放心使用模板的数据,当 processor 返回后,对应的数据系统将会被切换
|
||||||
|
/// 因此确保 processor 返回后,数据系统已经切换到新用户的数据系统
|
||||||
|
Future<bool> onLogin(GuruUser loginUser, Credential credential);
|
||||||
|
|
||||||
|
/// 这个方法调用时,中台会确保当前的数据系统是切换前的数据系统
|
||||||
|
/// 因此可以放心使用模板的数据,当 processor返回后,对应的数据系统将会被切换
|
||||||
|
/// 因此确保 processor 返回前,数据系统已经完成老用户数据的迁移
|
||||||
|
Future<bool> onLogout(GuruUser logoutUser);
|
||||||
|
|
||||||
|
/// 当出现登陆冲突时,有可能当前是匿名帐号,而这时,中台模板会静默登入到新的帐号中
|
||||||
|
/// 与此同时,中台会调用 onAnonymousLogout
|
||||||
|
Future<bool> onAnonymousLogout(GuruUser logoutUser) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当 APP首次匿名登陆时,或当登陆一个没有绑定匿名Credential的帐号时,会在登出时重新登陆一个新的匿名帐号
|
||||||
|
/// 中台在这种情况下会调用 onAnonymousLogin
|
||||||
|
Future<bool> onAnonymousLogin(GuruUser loginUser, Credential credential) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当登陆时,有可能会出现帐号冲突,因此针对冲突可以选择切换帐号或者忽略
|
||||||
|
/// 如果你选择切换帐号,那么你需要提供 onLogout 方法,用于处理老用户的数据迁移
|
||||||
|
/// 返回值为是否继续
|
||||||
|
Future<bool> onConflict();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:guru_app/property/app_property.dart';
|
||||||
|
import 'package:guru_utils/auth/auth_credential_manager.dart';
|
||||||
|
|
||||||
|
class AnonymousCredential extends Credential {
|
||||||
|
final String secretKey;
|
||||||
|
|
||||||
|
AnonymousCredential(this.secretKey);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get token => secretKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AuthType get authType => AuthType.anonymous;
|
||||||
|
|
||||||
|
String toJson() => secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnonymousCredentialDelegate extends AuthCredentialDelegate {
|
||||||
|
const AnonymousCredentialDelegate();
|
||||||
|
|
||||||
|
@override
|
||||||
|
AuthType get authType => AuthType.anonymous;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AuthResult> login() async {
|
||||||
|
final secretKey = await AppProperty.getInstance().getAnonymousSecretKey();
|
||||||
|
return AuthResult.success(AnonymousCredential(secretKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future logout() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Credential deserializeCredential(String data) {
|
||||||
|
return AnonymousCredential(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,11 @@ import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
part 'user.g.dart';
|
part 'user.g.dart';
|
||||||
|
|
||||||
|
@Deprecated("Use Guru User instead")
|
||||||
|
typedef SaasUser = GuruUser;
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class SaasUser {
|
class GuruUser {
|
||||||
@JsonKey(name: 'uid', defaultValue: "")
|
@JsonKey(name: 'uid', defaultValue: "")
|
||||||
final String uid;
|
final String uid;
|
||||||
|
|
||||||
|
|
@ -23,22 +26,22 @@ class SaasUser {
|
||||||
bool get isValid =>
|
bool get isValid =>
|
||||||
(uid != "") && (token.isNotEmpty == true) && (firebaseToken.isNotEmpty == true);
|
(uid != "") && (token.isNotEmpty == true) && (firebaseToken.isNotEmpty == true);
|
||||||
|
|
||||||
SaasUser(
|
GuruUser(
|
||||||
{required this.uid,
|
{required this.uid,
|
||||||
required this.token,
|
required this.token,
|
||||||
required this.firebaseToken,
|
required this.firebaseToken,
|
||||||
this.createAtTimestamp = 0});
|
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}) {
|
GuruUser copyWith({String? firebaseToken, String? token}) {
|
||||||
return SaasUser(
|
return GuruUser(
|
||||||
uid: uid, token: token ?? this.token, firebaseToken: firebaseToken ?? this.firebaseToken);
|
uid: uid, token: token ?? this.token, firebaseToken: firebaseToken ?? this.firebaseToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSame(SaasUser? user) {
|
bool isSame(GuruUser? user) {
|
||||||
return uid == user?.uid &&
|
return uid == user?.uid &&
|
||||||
token == user?.token &&
|
token == user?.token &&
|
||||||
firebaseToken == user?.firebaseToken &&
|
firebaseToken == user?.firebaseToken &&
|
||||||
|
|
@ -70,6 +73,63 @@ class AnonymousLoginReqBody {
|
||||||
Map<String, dynamic> toJson() => _$AnonymousLoginReqBodyToJson(this);
|
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()
|
@JsonSerializable()
|
||||||
class FirebaseTokenData {
|
class FirebaseTokenData {
|
||||||
@JsonKey(name: 'uid', defaultValue: "")
|
@JsonKey(name: 'uid', defaultValue: "")
|
||||||
|
|
@ -78,7 +138,7 @@ class FirebaseTokenData {
|
||||||
@JsonKey(name: 'firebaseToken', defaultValue: "")
|
@JsonKey(name: 'firebaseToken', defaultValue: "")
|
||||||
final String firebaseToken;
|
final String firebaseToken;
|
||||||
|
|
||||||
FirebaseTokenData({required this.uid, required this.firebaseToken});
|
FirebaseTokenData({this.uid = "", this.firebaseToken = ""});
|
||||||
|
|
||||||
factory FirebaseTokenData.fromJson(Map<String, dynamic> json) =>
|
factory FirebaseTokenData.fromJson(Map<String, dynamic> json) =>
|
||||||
_$FirebaseTokenDataFromJson(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 {
|
class UserAttr {
|
||||||
static const real = 0;
|
static const real = 0;
|
||||||
static const tester = 10;
|
static const tester = 10;
|
||||||
static const machine = 100;
|
static const machine = 100;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ part of 'user.dart';
|
||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
SaasUser _$SaasUserFromJson(Map<String, dynamic> json) => SaasUser(
|
GuruUser _$GuruUserFromJson(Map<String, dynamic> json) => GuruUser(
|
||||||
uid: json['uid'] as String? ?? '',
|
uid: json['uid'] as String? ?? '',
|
||||||
token: json['token'] as String? ?? '',
|
token: json['token'] as String? ?? '',
|
||||||
firebaseToken: json['firebaseToken'] as String? ?? '',
|
firebaseToken: json['firebaseToken'] as String? ?? '',
|
||||||
createAtTimestamp: json['createdAtTimestamp'] as int? ?? 0,
|
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,
|
'uid': instance.uid,
|
||||||
'token': instance.token,
|
'token': instance.token,
|
||||||
'firebaseToken': instance.firebaseToken,
|
'firebaseToken': instance.firebaseToken,
|
||||||
|
|
@ -32,6 +32,40 @@ Map<String, dynamic> _$AnonymousLoginReqBodyToJson(
|
||||||
'secret': instance.secret,
|
'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 _$FirebaseTokenDataFromJson(Map<String, dynamic> json) =>
|
||||||
FirebaseTokenData(
|
FirebaseTokenData(
|
||||||
uid: json['uid'] as String? ?? '',
|
uid: json['uid'] as String? ?? '',
|
||||||
|
|
@ -43,3 +77,27 @@ Map<String, dynamic> _$FirebaseTokenDataToJson(FirebaseTokenData instance) =>
|
||||||
'uid': instance.uid,
|
'uid': instance.uid,
|
||||||
'firebaseToken': instance.firebaseToken,
|
'firebaseToken': instance.firebaseToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UserAuthInfo _$UserAuthInfoFromJson(Map<String, dynamic> json) => UserAuthInfo(
|
||||||
|
secret: json['secret'] as String? ?? '',
|
||||||
|
providerList: (json['providerList'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UserAuthInfoToJson(UserAuthInfo instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'secret': instance.secret,
|
||||||
|
'providerList': instance.providerList,
|
||||||
|
};
|
||||||
|
|
||||||
|
UnbindReqBody _$UnbindReqBodyFromJson(Map<String, dynamic> json) =>
|
||||||
|
UnbindReqBody(
|
||||||
|
provider: json['provider'] as String? ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UnbindReqBodyToJson(UnbindReqBody instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'provider': instance.provider,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -560,18 +560,26 @@ class AdsManager extends AdsManagerDelegate {
|
||||||
return ad;
|
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");
|
Log.d("requestGdpr! debugGeography:$debugGeography testDeviceId:$testDeviceId", tag: "Ads");
|
||||||
// adb logcat -s UserMessagingPlatform
|
// adb logcat -s UserMessagingPlatform
|
||||||
// Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("xxxx") to set this as a debug device.
|
// 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);
|
.requestGdpr(debugGeography: debugGeography, testDeviceId: testDeviceId);
|
||||||
|
final consentResult = await GuruAnalytics.instance.refreshConsents();
|
||||||
|
Log.d("requestGdpr result:$result consentResult:$consentResult");
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> resetGdpr() {
|
Future<bool> resetGdpr() {
|
||||||
return GuruApplovinFlutter.instance.resetGdpr();
|
return GuruApplovinFlutter.instance.resetGdpr();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> updateOrientation(int orientation) async {
|
||||||
|
final result = await GuruApplovinFlutter.instance.updateOrientation(orientation);
|
||||||
|
return result == true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ApplovinBannerAds> createBannerAds({String? scene, AdsLifecycleObserver? observer}) async {
|
Future<ApplovinBannerAds> createBannerAds({String? scene, AdsLifecycleObserver? observer}) async {
|
||||||
final _adsProfile = adsProfile;
|
final _adsProfile = adsProfile;
|
||||||
|
|
@ -583,21 +591,14 @@ class AdsManager extends AdsManagerDelegate {
|
||||||
if (isPurchasedNoAd) {
|
if (isPurchasedNoAd) {
|
||||||
return AdCause.noAds;
|
return AdCause.noAds;
|
||||||
}
|
}
|
||||||
final _adsProfile = adsProfile;
|
final hiddenAt = AdsManager.instance.latestFullscreenAdsHiddenTimestamps;
|
||||||
Ads? ad = interstitialAds[_adsProfile.interstitialId];
|
|
||||||
int hiddenAt = 0;
|
|
||||||
if (ad is AdsAudit) {
|
|
||||||
hiddenAt = ad.latestHiddenAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
final now = DateTimeUtils.currentTimeInMillis();
|
final now = DateTimeUtils.currentTimeInMillis();
|
||||||
final impGapInMillis =
|
final impGapInMillis =
|
||||||
AdsManager.instance.adsConfig.interstitialConfig.getSceneImpGapInSeconds(scene) * 1000;
|
AdsManager.instance.adsConfig.interstitialConfig.getSceneImpGapInSeconds(scene) * 1000;
|
||||||
Log.d(
|
Log.d(
|
||||||
"canShowInterstitial($scene): now:$now latestFullscreenAdsHiddenTimestamps:$latestFullscreenAdsHiddenTimestamps hiddenAt:$hiddenAt impGapInMillis:$impGapInMillis",
|
"canShowInterstitial($scene): now:$now latestFullscreenAdsHiddenTimestamps:$latestFullscreenAdsHiddenTimestamps hiddenAt:$hiddenAt impGapInMillis:$impGapInMillis",
|
||||||
tag: "Ads");
|
tag: "Ads");
|
||||||
if ((now - AdsManager.instance.latestFullscreenAdsHiddenTimestamps) < 60000 ||
|
if ((now - hiddenAt) < impGapInMillis) {
|
||||||
((now - hiddenAt) < impGapInMillis)) {
|
|
||||||
Log.d("show ads too frequency", syncFirebase: true);
|
Log.d("show ads too frequency", syncFirebase: true);
|
||||||
return AdCause.tooFrequent;
|
return AdCause.tooFrequent;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ class AdInterstitialConfig {
|
||||||
@joinedStringConvert
|
@joinedStringConvert
|
||||||
final List<String> scenes;
|
final List<String> scenes;
|
||||||
|
|
||||||
@JsonKey(name: "sp_scene", defaultValue: {"new_block": 120, "reset_scs": 120})
|
@JsonKey(name: "sp_scene", defaultValue: {})
|
||||||
@configStringIntMapStringConvert
|
@configStringIntMapStringConvert
|
||||||
final Map<String, int> specialScenes;
|
final Map<String, int> specialScenes;
|
||||||
|
|
||||||
|
|
@ -329,8 +329,8 @@ class AdInterstitialConfig {
|
||||||
@JsonKey(name: "amazon_enable", defaultValue: false)
|
@JsonKey(name: "amazon_enable", defaultValue: false)
|
||||||
final bool amazonEnable;
|
final bool amazonEnable;
|
||||||
|
|
||||||
@JsonKey(name: "imp_gap_s", defaultValue: 120)
|
@JsonKey(name: "imp_gap_s")
|
||||||
final int impGapInSeconds;
|
final int? impGapInSeconds;
|
||||||
|
|
||||||
AdInterstitialConfig(this.freeInSecond, this.validation, this.scenes, this.retryMinTimeInSecond,
|
AdInterstitialConfig(this.freeInSecond, this.validation, this.scenes, this.retryMinTimeInSecond,
|
||||||
this.retryMaxTimeInSecond,
|
this.retryMaxTimeInSecond,
|
||||||
|
|
@ -346,7 +346,10 @@ class AdInterstitialConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
int getSceneImpGapInSeconds(String scene) {
|
int getSceneImpGapInSeconds(String scene) {
|
||||||
return specialScenes[scene] ?? impGapInSeconds;
|
return (specialScenes[scene] ??
|
||||||
|
impGapInSeconds ??
|
||||||
|
GuruApp.instance.appSpec.deployment.fullscreenAdsMinInterval)
|
||||||
|
.clamp(5, 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> checkFreeTime() async {
|
Future<bool> checkFreeTime() async {
|
||||||
|
|
|
||||||
|
|
@ -132,10 +132,10 @@ AdInterstitialConfig _$AdInterstitialConfigFromJson(
|
||||||
json['retry_max_s'] as int? ?? 600,
|
json['retry_max_s'] as int? ?? 600,
|
||||||
amazonEnable: json['amazon_enable'] as bool? ?? false,
|
amazonEnable: json['amazon_enable'] as bool? ?? false,
|
||||||
specialScenes: json['sp_scene'] == null
|
specialScenes: json['sp_scene'] == null
|
||||||
? {'new_block': 120, 'reset_scs': 120}
|
? {}
|
||||||
: configStringIntMapStringConvert
|
: configStringIntMapStringConvert
|
||||||
.fromJson(json['sp_scene'] as String),
|
.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(
|
Map<String, dynamic> _$AdInterstitialConfigToJson(
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:guru_app/analytics/guru_analytics.dart';
|
import 'package:guru_app/analytics/guru_analytics.dart';
|
||||||
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.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/app_property.dart';
|
||||||
import 'package:guru_app/property/property_keys.dart';
|
import 'package:guru_app/property/property_keys.dart';
|
||||||
import 'package:guru_utils/collection/collectionutils.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_utils/log/log.dart';
|
||||||
import 'package:guru_applovin_flutter/ad_impression.dart';
|
import 'package:guru_applovin_flutter/ad_impression.dart';
|
||||||
|
|
||||||
|
|
@ -65,26 +67,7 @@ class AdImpressionController {
|
||||||
}
|
}
|
||||||
final payloadMap = json.decode(payload);
|
final payloadMap = json.decode(payload);
|
||||||
ImpressionData impressionData = ImpressionData.fromJson(payloadMap);
|
ImpressionData impressionData = ImpressionData.fromJson(payloadMap);
|
||||||
// 判断是不是facebook的network
|
|
||||||
// if (impressionData.networkName == "undisclosed") {
|
|
||||||
// final calibrationCpm = await facebookCalibrator.getCpm(impressionData.unitFormat, impressionData.country);
|
|
||||||
// if (calibrationCpm > 0) {
|
|
||||||
// final newImpressionData = impressionData.derive(newPublisherRevenue: calibrationCpm);
|
|
||||||
// if (facebookCalibrator.config?.fbIrldReport == true) {
|
|
||||||
// AnalyticsUtils.logEventEx("tch_fb_ad_rev", value: calibrationCpm, parameters: {
|
|
||||||
// FirebaseEventsParams.AD_FORMAT: impressionData.unitFormat,
|
|
||||||
// FirebaseEventsParams.AD_UNIT_NAME: impressionData.unitName,
|
|
||||||
// FirebaseEventsParams.CURRENCY: impressionData.currency,
|
|
||||||
// "country": impressionData.country,
|
|
||||||
// "mopub_rev": impressionData.publisherRevenue
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// impressionData = newImpressionData;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
await refreshLtv(impressionData);
|
await refreshLtv(impressionData);
|
||||||
// _reportAdImpression(arguments);
|
|
||||||
|
|
||||||
final jsonPayload = jsonEncode(impressionData.payload);
|
final jsonPayload = jsonEncode(impressionData.payload);
|
||||||
latestImpressionPayload = jsonPayload;
|
latestImpressionPayload = jsonPayload;
|
||||||
|
|
@ -119,12 +102,13 @@ class AdImpressionController {
|
||||||
final currency = impressionData.currency;
|
final currency = impressionData.currency;
|
||||||
if (revenue != -1) {
|
if (revenue != -1) {
|
||||||
_logAdRevenue(impressionData);
|
_logAdRevenue(impressionData);
|
||||||
|
// if ()
|
||||||
// _logAdLtv(revenue: revenue, adPlatform: adPlatform, currency: currency);
|
// _logAdLtv(revenue: revenue, adPlatform: adPlatform, currency: currency);
|
||||||
}
|
}
|
||||||
Log.d("refreshLtv payload:${impressionData.payload}");
|
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 nowDate = DateTimeUtils.yyyyMMddUtcNum;
|
||||||
// final appProperty = AppProperty.getInstance();
|
// final appProperty = AppProperty.getInstance();
|
||||||
// final totalLtv = await appProperty.getDouble(PropertyKeys.totalLtv, defValue: 0.0);
|
// final totalLtv = await appProperty.getDouble(PropertyKeys.totalLtv, defValue: 0.0);
|
||||||
|
|
@ -179,11 +163,25 @@ class AdImpressionController {
|
||||||
totalRevenue += data.publisherRevenue;
|
totalRevenue += data.publisherRevenue;
|
||||||
if (totalRevenue >= 0.01) {
|
if (totalRevenue >= 0.01) {
|
||||||
GuruAnalytics.instance.logAdRevenue(totalRevenue, data.platform, data.currency);
|
GuruAnalytics.instance.logAdRevenue(totalRevenue, data.platform, data.currency);
|
||||||
GuruAnalytics.instance.logPurchase(totalRevenue,
|
if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 1) {
|
||||||
currency: data.currency, contentId: "MAX", adPlatform: "MAX");
|
GuruAnalytics.instance.logPurchase(totalRevenue,
|
||||||
|
currency: data.currency, contentId: "MAX", adPlatform: "MAX");
|
||||||
|
}
|
||||||
totalRevenue = .0;
|
totalRevenue = .0;
|
||||||
}
|
}
|
||||||
appProperty.setDouble(PropertyKeys.totalRevenue, totalRevenue);
|
appProperty.setDouble(PropertyKeys.totalRevenue, totalRevenue);
|
||||||
|
|
||||||
|
double totalRevenue020 =
|
||||||
|
await appProperty.getDouble(PropertyKeys.totalRevenue020, defValue: 0.0);
|
||||||
|
totalRevenue020 += data.publisherRevenue;
|
||||||
|
if (totalRevenue020 >= 0.2) {
|
||||||
|
GuruAnalytics.instance.logAdRevenue020(totalRevenue020, data.platform, data.currency);
|
||||||
|
if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 2) {
|
||||||
|
GuruAnalytics.instance.logPurchase(totalRevenue020,
|
||||||
|
currency: data.currency, contentId: "MAX", adPlatform: "MAX");
|
||||||
|
}
|
||||||
|
totalRevenue020 = .0;
|
||||||
|
}
|
||||||
|
appProperty.setDouble(PropertyKeys.totalRevenue020, totalRevenue020);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,388 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:guru_app/account/account_data_store.dart';
|
||||||
|
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
|
||||||
|
import 'package:guru_app/property/settings/guru_settings.dart';
|
||||||
|
import 'package:guru_utils/device/device_utils.dart';
|
||||||
|
import 'package:guru_utils/extensions/extensions.dart';
|
||||||
|
import 'package:guru_utils/log/log.dart';
|
||||||
|
import 'package:guru_utils/random/random_utils.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'abtest_model.g.dart';
|
||||||
|
|
||||||
|
class ConditionOpt {
|
||||||
|
static const equals = "eq";
|
||||||
|
static const greaterThan = "gt";
|
||||||
|
static const greaterThanOrEquals = "gte";
|
||||||
|
static const lessThan = "lt";
|
||||||
|
static const lessThanOrEquals = "lte";
|
||||||
|
static const notEquals = "ne";
|
||||||
|
|
||||||
|
static bool evaluate<T extends Comparable>(T value, T target, String opt) {
|
||||||
|
final result = value.compareTo(target);
|
||||||
|
switch (opt) {
|
||||||
|
case ConditionOpt.equals:
|
||||||
|
return result == 0;
|
||||||
|
case ConditionOpt.greaterThan:
|
||||||
|
return result > 0;
|
||||||
|
case ConditionOpt.greaterThanOrEquals:
|
||||||
|
return result >= 0;
|
||||||
|
case ConditionOpt.lessThan:
|
||||||
|
return result < 0;
|
||||||
|
case ConditionOpt.lessThanOrEquals:
|
||||||
|
return result <= 0;
|
||||||
|
case ConditionOpt.notEquals:
|
||||||
|
return result != 0;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ABTestFilter {
|
||||||
|
static const platform = 1;
|
||||||
|
static const version = 2;
|
||||||
|
static const country = 3;
|
||||||
|
static const newUser = 4;
|
||||||
|
|
||||||
|
final int type;
|
||||||
|
|
||||||
|
const ABTestFilter(this.type);
|
||||||
|
|
||||||
|
bool filter();
|
||||||
|
|
||||||
|
factory ABTestFilter.fromJson(Map<String, dynamic> json) {
|
||||||
|
final type = json["type"] = json["type"] ?? 0;
|
||||||
|
switch (type) {
|
||||||
|
case ABTestFilter.platform:
|
||||||
|
return PlatformFilter.fromJson(json);
|
||||||
|
case ABTestFilter.country:
|
||||||
|
return CountryFilter.fromJson(json);
|
||||||
|
case ABTestFilter.version:
|
||||||
|
return VersionFilter.fromJson(json);
|
||||||
|
case ABTestFilter.newUser:
|
||||||
|
return NewUserFilter.fromJson(json);
|
||||||
|
default:
|
||||||
|
throw UnimplementedError("Unknown ABTestFilter type: $type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => toJson()..addAll({"type": type});
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ABTestCondition {
|
||||||
|
bool validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 为了后面可以做一些定制,因此这里按平台进行区分
|
||||||
|
@JsonSerializable()
|
||||||
|
class AndroidCondition extends ABTestCondition {
|
||||||
|
@JsonKey(name: "opt")
|
||||||
|
final String? opt;
|
||||||
|
|
||||||
|
@JsonKey(name: "sdk")
|
||||||
|
final int? sdkInt;
|
||||||
|
|
||||||
|
AndroidCondition({this.opt, this.sdkInt});
|
||||||
|
|
||||||
|
factory AndroidCondition.fromJson(Map<String, dynamic> json) => _$AndroidConditionFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$AndroidConditionToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool validate() {
|
||||||
|
final versionOpt = opt;
|
||||||
|
final targetVersion = sdkInt;
|
||||||
|
if (versionOpt != null && targetVersion != null) {
|
||||||
|
final versionCode = DeviceUtils.peekOSVersion();
|
||||||
|
// 操作系统版本号获取失败,直接返回false
|
||||||
|
if (versionCode == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 方便后面扩展其它字段
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class IosCondition extends ABTestCondition {
|
||||||
|
@JsonKey(name: "opt")
|
||||||
|
final String? opt;
|
||||||
|
|
||||||
|
@JsonKey(name: "ver")
|
||||||
|
final int? version; // 这里只记录大版本号
|
||||||
|
|
||||||
|
IosCondition({this.opt, this.version});
|
||||||
|
|
||||||
|
factory IosCondition.fromJson(Map<String, dynamic> json) => _$IosConditionFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$IosConditionToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool validate() {
|
||||||
|
final versionOpt = opt;
|
||||||
|
final targetVersion = version;
|
||||||
|
if (versionOpt != null && targetVersion != null) {
|
||||||
|
final versionCode = DeviceUtils.peekOSVersion();
|
||||||
|
// 操作系统版本号获取失败,直接返回false
|
||||||
|
if (versionCode == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 方便后面扩展其它字段
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class PlatformFilter extends ABTestFilter {
|
||||||
|
@JsonKey(name: "ac")
|
||||||
|
final AndroidCondition? androidCondition;
|
||||||
|
|
||||||
|
@JsonKey(name: "ic")
|
||||||
|
final IosCondition? iosCondition;
|
||||||
|
|
||||||
|
PlatformFilter({this.androidCondition, this.iosCondition}) : super(ABTestFilter.platform);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter() {
|
||||||
|
// 如果配了 Platform Filter, 如果指定平台没有 condition, 则默认为true
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
return androidCondition?.validate() != false;
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
return iosCondition?.validate() != false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PlatformFilter.fromJson(Map<String, dynamic> json) => _$PlatformFilterFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$PlatformFilterToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable(constructor: "_")
|
||||||
|
class VersionFilter extends ABTestFilter {
|
||||||
|
@JsonKey(name: "opt")
|
||||||
|
final String opt;
|
||||||
|
|
||||||
|
@JsonKey(name: "mmp")
|
||||||
|
final String mmp; // major.minor.patch
|
||||||
|
|
||||||
|
VersionFilter._(this.opt, this.mmp) : super(ABTestFilter.version);
|
||||||
|
|
||||||
|
VersionFilter.equals(this.mmp)
|
||||||
|
: opt = ConditionOpt.equals,
|
||||||
|
super(ABTestFilter.version);
|
||||||
|
|
||||||
|
VersionFilter.greaterThan(this.mmp)
|
||||||
|
: opt = ConditionOpt.greaterThan,
|
||||||
|
super(ABTestFilter.version);
|
||||||
|
|
||||||
|
VersionFilter.greaterThanOrEquals(this.mmp)
|
||||||
|
: opt = ConditionOpt.greaterThanOrEquals,
|
||||||
|
super(ABTestFilter.version);
|
||||||
|
|
||||||
|
VersionFilter.lessThan(this.mmp)
|
||||||
|
: opt = ConditionOpt.lessThan,
|
||||||
|
super(ABTestFilter.version);
|
||||||
|
|
||||||
|
VersionFilter.lessThanOrEquals(this.mmp)
|
||||||
|
: opt = ConditionOpt.lessThanOrEquals,
|
||||||
|
super(ABTestFilter.version);
|
||||||
|
|
||||||
|
VersionFilter.notEquals(this.mmp)
|
||||||
|
: opt = ConditionOpt.notEquals,
|
||||||
|
super(ABTestFilter.version);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter() {
|
||||||
|
final version = GuruSettings.instance.version.get();
|
||||||
|
Log.d("[$runtimeType] $version $opt $mmp");
|
||||||
|
return ConditionOpt.evaluate<String>(version, mmp, opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'VersionValidator{opt: $opt, mmp: $mmp}';
|
||||||
|
}
|
||||||
|
|
||||||
|
factory VersionFilter.fromJson(Map<String, dynamic> json) => _$VersionFilterFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$VersionFilterToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable(constructor: "_")
|
||||||
|
class CountryFilter extends ABTestFilter {
|
||||||
|
@JsonKey(name: "included", defaultValue: {})
|
||||||
|
final Set<String> included;
|
||||||
|
|
||||||
|
@JsonKey(name: "excluded", defaultValue: {})
|
||||||
|
final Set<String> excluded;
|
||||||
|
|
||||||
|
CountryFilter._(this.included, this.excluded) : super(ABTestFilter.country);
|
||||||
|
|
||||||
|
CountryFilter.included(this.included)
|
||||||
|
: excluded = {},
|
||||||
|
super(ABTestFilter.country);
|
||||||
|
|
||||||
|
CountryFilter.excluded(this.excluded)
|
||||||
|
: included = {},
|
||||||
|
super(ABTestFilter.country);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter() {
|
||||||
|
final String countryCode = Platform.localeName.split('_').safeLast?.toLowerCase() ?? "";
|
||||||
|
Log.d("[$runtimeType] $countryCode included: $included excluded: $excluded");
|
||||||
|
if (countryCode.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果excluded不为空,证明存在排除选项,该validate将只判断所有excluded中的逻辑,
|
||||||
|
// 将不会在判断included中的逻辑
|
||||||
|
if (excluded.isNotEmpty) {
|
||||||
|
return !excluded.contains(countryCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (included.contains(countryCode)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CountryFilter.fromJson(Map<String, dynamic> json) => _$CountryFilterFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$CountryFilterToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class NewUserFilter extends ABTestFilter {
|
||||||
|
NewUserFilter() : super(ABTestFilter.newUser);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter() {
|
||||||
|
final version = GuruSettings.instance.version.get();
|
||||||
|
final fiv = GuruSettings.instance.firstInstallVersion.get();
|
||||||
|
return fiv.startsWith(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory NewUserFilter.fromJson(Map<String, dynamic> json) => _$NewUserFilterFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$NewUserFilterToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ABTestAudience {
|
||||||
|
@JsonKey(name: "filters")
|
||||||
|
final List<ABTestFilter> filters;
|
||||||
|
|
||||||
|
@JsonKey(name: "variant", defaultValue: 2)
|
||||||
|
final int variant;
|
||||||
|
|
||||||
|
ABTestAudience({required this.filters, this.variant = 2});
|
||||||
|
|
||||||
|
factory ABTestAudience.fromJson(Map<String, dynamic> json) => _$ABTestAudienceFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ABTestAudienceToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ABTestAudience{filters: $filters, variant: $variant}';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validate() {
|
||||||
|
for (var filter in filters) {
|
||||||
|
if (!filter.filter()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ABTestExperiment {
|
||||||
|
@JsonKey(name: "name")
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
@JsonKey(name: "start_ts", defaultValue: 0)
|
||||||
|
final int startTs;
|
||||||
|
|
||||||
|
@JsonKey(name: "end_ts", defaultValue: 0)
|
||||||
|
final int endTs;
|
||||||
|
|
||||||
|
@JsonKey(name: "audience")
|
||||||
|
final ABTestAudience audience;
|
||||||
|
|
||||||
|
ABTestExperiment(
|
||||||
|
{required String name, required this.startTs, required this.endTs, required this.audience})
|
||||||
|
: name = _validExperimentName(name);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ABTestExperiment{name: $name, startTs: $startTs, endTs: $endTs, audience: $audience}';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _validExperimentName(String experimentName) {
|
||||||
|
if (experimentName.contains(RemoteConfigManager.invalidABKeyRegExp)) {
|
||||||
|
Log.w("abName($experimentName) use invalid key! $experimentName! replace invalid char to _");
|
||||||
|
experimentName = experimentName.replaceAll(RemoteConfigManager.invalidABKeyRegExp, "_");
|
||||||
|
} else {
|
||||||
|
if (experimentName.length > 20) {
|
||||||
|
experimentName = experimentName.substring(0, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return experimentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ABTestExperiment.fromJson(Map<String, dynamic> json) => _$ABTestExperimentFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ABTestExperimentToJson(this);
|
||||||
|
|
||||||
|
bool isExpired() {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
return now < startTs || now > endTs;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isMatchAudience() {
|
||||||
|
return audience.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeToJson: false)
|
||||||
|
String? _variantName;
|
||||||
|
|
||||||
|
String get variantName =>
|
||||||
|
(_variantName ??= _toVariantName(RandomUtils.nextInt(audience.variant)));
|
||||||
|
|
||||||
|
static const _originalVariant = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
|
||||||
|
String _toVariantName(int value) {
|
||||||
|
String codes = "";
|
||||||
|
int nv = value;
|
||||||
|
while (true) {
|
||||||
|
final nextNum = nv ~/ _originalVariant.length;
|
||||||
|
if (nextNum <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
codes = "${_originalVariant[nv % _originalVariant.length]}$codes";
|
||||||
|
nv = nextNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tailIndex = nv % _originalVariant.length;
|
||||||
|
if (tailIndex >= 0) {
|
||||||
|
codes = "${_originalVariant[tailIndex]}$codes";
|
||||||
|
}
|
||||||
|
return codes.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'abtest_model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
AndroidCondition _$AndroidConditionFromJson(Map<String, dynamic> json) =>
|
||||||
|
AndroidCondition(
|
||||||
|
opt: json['opt'] as String?,
|
||||||
|
sdkInt: json['sdk'] as int?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AndroidConditionToJson(AndroidCondition instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'opt': instance.opt,
|
||||||
|
'sdk': instance.sdkInt,
|
||||||
|
};
|
||||||
|
|
||||||
|
IosCondition _$IosConditionFromJson(Map<String, dynamic> json) => IosCondition(
|
||||||
|
opt: json['opt'] as String?,
|
||||||
|
version: json['ver'] as int?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$IosConditionToJson(IosCondition instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'opt': instance.opt,
|
||||||
|
'ver': instance.version,
|
||||||
|
};
|
||||||
|
|
||||||
|
PlatformFilter _$PlatformFilterFromJson(Map<String, dynamic> json) =>
|
||||||
|
PlatformFilter(
|
||||||
|
androidCondition: json['ac'] == null
|
||||||
|
? null
|
||||||
|
: AndroidCondition.fromJson(json['ac'] as Map<String, dynamic>),
|
||||||
|
iosCondition: json['ic'] == null
|
||||||
|
? null
|
||||||
|
: IosCondition.fromJson(json['ic'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$PlatformFilterToJson(PlatformFilter instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'ac': instance.androidCondition,
|
||||||
|
'ic': instance.iosCondition,
|
||||||
|
};
|
||||||
|
|
||||||
|
VersionFilter _$VersionFilterFromJson(Map<String, dynamic> json) =>
|
||||||
|
VersionFilter._(
|
||||||
|
json['opt'] as String,
|
||||||
|
json['mmp'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$VersionFilterToJson(VersionFilter instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'opt': instance.opt,
|
||||||
|
'mmp': instance.mmp,
|
||||||
|
};
|
||||||
|
|
||||||
|
CountryFilter _$CountryFilterFromJson(Map<String, dynamic> json) =>
|
||||||
|
CountryFilter._(
|
||||||
|
(json['included'] as List<dynamic>?)?.map((e) => e as String).toSet() ??
|
||||||
|
{},
|
||||||
|
(json['excluded'] as List<dynamic>?)?.map((e) => e as String).toSet() ??
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CountryFilterToJson(CountryFilter instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'included': instance.included.toList(),
|
||||||
|
'excluded': instance.excluded.toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
NewUserFilter _$NewUserFilterFromJson(Map<String, dynamic> json) =>
|
||||||
|
NewUserFilter();
|
||||||
|
|
||||||
|
Map<String, dynamic> _$NewUserFilterToJson(NewUserFilter instance) =>
|
||||||
|
<String, dynamic>{};
|
||||||
|
|
||||||
|
ABTestAudience _$ABTestAudienceFromJson(Map<String, dynamic> json) =>
|
||||||
|
ABTestAudience(
|
||||||
|
filters: (json['filters'] as List<dynamic>)
|
||||||
|
.map((e) => ABTestFilter.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
variant: json['variant'] as int? ?? 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ABTestAudienceToJson(ABTestAudience instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'filters': instance.filters,
|
||||||
|
'variant': instance.variant,
|
||||||
|
};
|
||||||
|
|
||||||
|
ABTestExperiment _$ABTestExperimentFromJson(Map<String, dynamic> json) =>
|
||||||
|
ABTestExperiment(
|
||||||
|
name: json['name'] as String,
|
||||||
|
startTs: json['start_ts'] as int? ?? 0,
|
||||||
|
endTs: json['end_ts'] as int? ?? 0,
|
||||||
|
audience:
|
||||||
|
ABTestAudience.fromJson(json['audience'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ABTestExperimentToJson(ABTestExperiment instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'start_ts': instance.startTs,
|
||||||
|
'end_ts': instance.endTs,
|
||||||
|
'audience': instance.audience,
|
||||||
|
};
|
||||||
|
|
@ -8,6 +8,8 @@ part 'analytics_model.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class AnalyticsConfig {
|
class AnalyticsConfig {
|
||||||
|
static const _defaultGoogleDma = [1, 0, 12, 65];
|
||||||
|
static const _defaultDmaCountry = [];
|
||||||
@JsonKey(name: "cap", defaultValue: ["firebase", "facebook", "guru"])
|
@JsonKey(name: "cap", defaultValue: ["firebase", "facebook", "guru"])
|
||||||
@joinedStringConvert
|
@joinedStringConvert
|
||||||
final List<String> capabilities;
|
final List<String> capabilities;
|
||||||
|
|
@ -24,6 +26,13 @@ class AnalyticsConfig {
|
||||||
@JsonKey(name: "enabled_strategy", defaultValue: false)
|
@JsonKey(name: "enabled_strategy", defaultValue: false)
|
||||||
final bool enabledStrategy;
|
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() {
|
AppEventCapabilities toAppEventCapabilities() {
|
||||||
int capValue = 0;
|
int capValue = 0;
|
||||||
if (capabilities.contains("firebase")) {
|
if (capabilities.contains("firebase")) {
|
||||||
|
|
@ -38,8 +47,15 @@ class AnalyticsConfig {
|
||||||
return AppEventCapabilities(capValue);
|
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,
|
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);
|
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}';
|
return 'UserIdentification{firebaseAppInstanceId: $firebaseAppInstanceId, idfa: $idfa, adId: $adId, gpsAdId: $gpsAdId}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ConsentFieldName {
|
||||||
|
static const adStorage = "ad_storage";
|
||||||
|
static const analyticsStorage = "analytics_storage";
|
||||||
|
static const adPersonalization = "ad_personalization";
|
||||||
|
static const adUserData = "ad_user_data";
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConsentType { adStorage, analyticsStorage, adPersonalization, adUserData }
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,12 @@ AnalyticsConfig _$AnalyticsConfigFromJson(Map<String, dynamic> json) =>
|
||||||
json['expired_d'] as int? ?? 7,
|
json['expired_d'] as int? ?? 7,
|
||||||
json['strategy'] as String? ?? '',
|
json['strategy'] as String? ?? '',
|
||||||
json['enabled_strategy'] as bool? ?? false,
|
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) =>
|
Map<String, dynamic> _$AnalyticsConfigToJson(AnalyticsConfig instance) =>
|
||||||
|
|
@ -24,6 +30,8 @@ Map<String, dynamic> _$AnalyticsConfigToJson(AnalyticsConfig instance) =>
|
||||||
'expired_d': instance.expiredInDays,
|
'expired_d': instance.expiredInDays,
|
||||||
'strategy': instance.strategy,
|
'strategy': instance.strategy,
|
||||||
'enabled_strategy': instance.enabledStrategy,
|
'enabled_strategy': instance.enabledStrategy,
|
||||||
|
'google_dma': instance.googleDmaMask,
|
||||||
|
'dma_country': instance.dmaCountry,
|
||||||
};
|
};
|
||||||
|
|
||||||
UserIdentification _$UserIdentificationFromJson(Map<String, dynamic> json) =>
|
UserIdentification _$UserIdentificationFromJson(Map<String, dynamic> json) =>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import 'dart:collection';
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:adjust_sdk/adjust_third_party_sharing.dart';
|
||||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
|
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:guru_analytics_flutter/event_logger.dart';
|
import 'package:guru_analytics_flutter/event_logger.dart';
|
||||||
import 'package:guru_analytics_flutter/event_logger_common.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/ads_manager.dart';
|
||||||
import 'package:guru_app/ads/core/ads_config.dart';
|
import 'package:guru_app/ads/core/ads_config.dart';
|
||||||
import 'package:guru_app/aigc/bi/ai_bi.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/data/analytics_model.dart';
|
||||||
import 'package:guru_app/analytics/strategy/guru_analytics_strategy.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/firebase/remoteconfig/remote_config_manager.dart';
|
||||||
import 'package:guru_app/guru_app.dart';
|
import 'package:guru_app/guru_app.dart';
|
||||||
import 'package:guru_app/property/app_property.dart';
|
import 'package:guru_app/property/app_property.dart';
|
||||||
import 'package:guru_app/property/property_keys.dart';
|
import 'package:guru_app/property/property_keys.dart';
|
||||||
import 'package:guru_app/property/runtime_property.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/datetime/datetime_utils.dart';
|
||||||
import 'package:guru_utils/device/device_info.dart';
|
import 'package:guru_utils/device/device_info.dart';
|
||||||
import 'package:guru_utils/device/device_utils.dart';
|
import 'package:guru_utils/device/device_utils.dart';
|
||||||
|
|
@ -38,7 +45,7 @@ part 'modules/ads_analytics.dart';
|
||||||
part 'modules/adjust_aware.dart';
|
part 'modules/adjust_aware.dart';
|
||||||
|
|
||||||
class GuruAnalytics extends Analytics with AdjustAware {
|
class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
bool get release => !_mock && _enabledAnalytics && kReleaseMode;
|
bool get release => !_mock && (_enabledAnalytics || kReleaseMode);
|
||||||
|
|
||||||
String appInstanceId = "";
|
String appInstanceId = "";
|
||||||
|
|
||||||
|
|
@ -55,6 +62,10 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
|
|
||||||
static String currentScreen = "";
|
static String currentScreen = "";
|
||||||
|
|
||||||
|
static final RegExp _consentPurposeRegExp = RegExp(r"^[01]+$");
|
||||||
|
|
||||||
|
static String? mockCountryCode;
|
||||||
|
|
||||||
static const errorEventCodes = {
|
static const errorEventCodes = {
|
||||||
14, // 上报事件失败
|
14, // 上报事件失败
|
||||||
22, // 网络状态不可用
|
22, // 网络状态不可用
|
||||||
|
|
@ -71,8 +82,13 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
final BehaviorSubject<GuruStatistic> guruEventStatistic =
|
final BehaviorSubject<GuruStatistic> guruEventStatistic =
|
||||||
BehaviorSubject.seeded(GuruStatistic.invalid);
|
BehaviorSubject.seeded(GuruStatistic.invalid);
|
||||||
|
|
||||||
|
final BehaviorSubject<Map<String, String>> abTestExperimentVariant = BehaviorSubject.seeded({});
|
||||||
|
|
||||||
Stream<GuruStatistic> get observableGuruEventStatistic => guruEventStatistic.stream;
|
Stream<GuruStatistic> get observableGuruEventStatistic => guruEventStatistic.stream;
|
||||||
|
|
||||||
|
Stream<Map<String, String>> get observableABTestExperimentVariant =>
|
||||||
|
abTestExperimentVariant.stream;
|
||||||
|
|
||||||
final BehaviorSubject<UserIdentification> userIdentificationSubject =
|
final BehaviorSubject<UserIdentification> userIdentificationSubject =
|
||||||
BehaviorSubject.seeded(UserIdentification());
|
BehaviorSubject.seeded(UserIdentification());
|
||||||
|
|
||||||
|
|
@ -98,6 +114,19 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
return Analytics.userProperties[key];
|
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 {
|
void init() async {
|
||||||
Log.d(
|
Log.d(
|
||||||
"AnalyticsUtil init### Platform.localeName :${Platform.localeName} ${Intl.getCurrentLocale()}");
|
"AnalyticsUtil init### Platform.localeName :${Platform.localeName} ${Intl.getCurrentLocale()}");
|
||||||
|
|
@ -142,6 +171,7 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
Future.delayed(const Duration(seconds: 1), () {
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
initAdjust();
|
initAdjust();
|
||||||
initFbEventMapping();
|
initFbEventMapping();
|
||||||
|
refreshConsents();
|
||||||
Log.d("register transmitter");
|
Log.d("register transmitter");
|
||||||
});
|
});
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
@ -151,6 +181,100 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future switchSession(String oldToken, String newToken) async {
|
||||||
|
_initEnvProperties();
|
||||||
|
_logLocale();
|
||||||
|
_logDeviceType();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future initLocalExperiments() async {
|
||||||
|
final runningExperiments = await AppProperty.getInstance().loadRunningExperiments();
|
||||||
|
final experiments = GuruApp.instance.appSpec.localABTestExperiments;
|
||||||
|
final validRunningExperimentKeys =
|
||||||
|
runningExperiments.keys.toSet().intersection(experiments.keys.toSet());
|
||||||
|
for (var experiment in experiments.values) {
|
||||||
|
// 如果在已经开始的实验中,但是不在当前的实验列表中,需要删除
|
||||||
|
final needRemove = runningExperiments.containsKey(experiment.name) &&
|
||||||
|
!validRunningExperimentKeys.contains(experiment.name);
|
||||||
|
if (needRemove) {
|
||||||
|
await removeExperiment(experiment.name);
|
||||||
|
} else {
|
||||||
|
await _applyExperiment(experiment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> refreshConsents({AnalyticsConfig? analyticsConfig}) async {
|
||||||
|
final config = analyticsConfig ?? RemoteConfigManager.instance.getAnalyticsConfig();
|
||||||
|
final purposeConsents = await GuruPlatformData.getPurposeConsents();
|
||||||
|
Log.i("refreshConsents: '$purposeConsents'");
|
||||||
|
if (purposeConsents.isEmpty) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 如果他不是完全使用 1,0 组成的字符串
|
||||||
|
if (!_consentPurposeRegExp.hasMatch(purposeConsents)) {
|
||||||
|
Log.i("invalid consents $purposeConsents");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前的 countryCode, 判断是否在 dma country的范围内
|
||||||
|
if (config.dmaCountry.isNotEmpty) {
|
||||||
|
final countryCode = getCountryCode();
|
||||||
|
if (!config.dmaCountry.contains(countryCode)) {
|
||||||
|
Log.i("invalid country $countryCode");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final length = min(purposeConsents.length, 32);
|
||||||
|
int flags = 0;
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
flags |= (((purposeConsents[i] == "1") ? 1 : 0) << i);
|
||||||
|
}
|
||||||
|
|
||||||
|
final consentsData = {
|
||||||
|
ConsentFieldName.adStorage: config.googleDmaGranted(ConsentType.adStorage, flags),
|
||||||
|
ConsentFieldName.analyticsStorage:
|
||||||
|
config.googleDmaGranted(ConsentType.analyticsStorage, flags),
|
||||||
|
ConsentFieldName.adPersonalization:
|
||||||
|
config.googleDmaGranted(ConsentType.adPersonalization, flags),
|
||||||
|
ConsentFieldName.adUserData: config.googleDmaGranted(ConsentType.adUserData, flags),
|
||||||
|
};
|
||||||
|
|
||||||
|
String _flag(String key) {
|
||||||
|
return consentsData[key] == true ? "1" : "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("setConsents consentsData: $consentsData");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await EventLogger.guru.setConsents(consentsData);
|
||||||
|
Log.d("setConsents result: $result");
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.e("setConsents error! $error, $stacktrace");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabledAdjust) {
|
||||||
|
AdjustThirdPartySharing adjustThirdPartySharing = AdjustThirdPartySharing(null);
|
||||||
|
adjustThirdPartySharing.addGranularOption("google_dma", "eea", "1");
|
||||||
|
adjustThirdPartySharing.addGranularOption(
|
||||||
|
"google_dma", "ad_personalization", _flag(ConsentFieldName.adPersonalization));
|
||||||
|
adjustThirdPartySharing.addGranularOption(
|
||||||
|
"google_dma", "ad_user_data", _flag(ConsentFieldName.adUserData));
|
||||||
|
Adjust.trackThirdPartySharing(adjustThirdPartySharing);
|
||||||
|
Log.d("setAdjust complete!");
|
||||||
|
}
|
||||||
|
|
||||||
|
final result =
|
||||||
|
"${_flag(ConsentFieldName.adStorage)}${_flag(ConsentFieldName.analyticsStorage)}${_flag(ConsentFieldName.adPersonalization)}${_flag(ConsentFieldName.adUserData)}";
|
||||||
|
final changed = await AppProperty.getInstance().refreshGoogleDma(result);
|
||||||
|
if (changed || GuruSettings.instance.debugMode.get()) {
|
||||||
|
logEventEx("dma_gg", parameters: {"purpose": purposeConsents, "result": result});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void processAnalyticsCallback(int code, String? errorInfo) {
|
void processAnalyticsCallback(int code, String? errorInfo) {
|
||||||
if (!errorEventCodes.contains(code)) {
|
if (!errorEventCodes.contains(code)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -269,7 +393,10 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
if (firebaseId?.isNotEmpty == true) {
|
if (firebaseId?.isNotEmpty == true) {
|
||||||
setFirebaseId(firebaseId!);
|
setFirebaseId(firebaseId!);
|
||||||
}
|
}
|
||||||
|
refreshABProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshABProperties() {
|
||||||
final abProperties = RemoteConfigManager.instance.getABProperties();
|
final abProperties = RemoteConfigManager.instance.getABProperties();
|
||||||
|
|
||||||
final PropertyBundle propertyBundle = PropertyBundle();
|
final PropertyBundle propertyBundle = PropertyBundle();
|
||||||
|
|
@ -293,6 +420,17 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
setUserProperty("first_open_time", firstInstallTime.toString());
|
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() {
|
void _logLocale() {
|
||||||
if (Platform.localeName.isNotEmpty == true) {
|
if (Platform.localeName.isNotEmpty == true) {
|
||||||
String lanCode = "";
|
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) {
|
void setDeviceId(String deviceId) {
|
||||||
Log.d("setDeviceId: $deviceId");
|
Log.d("setDeviceId: $deviceId");
|
||||||
recordEvents("setDeviceId", {"userId": 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");
|
Log.d("setUserId: $userId");
|
||||||
recordEvents("setUserId", {"userId": userId});
|
recordEvents("setUserId", {"userId": userId});
|
||||||
recordProperty("userId", userId);
|
recordProperty("userId", userId);
|
||||||
if (userId.isNotEmpty) {
|
if (userId.isNotEmpty) {
|
||||||
AppProperty.getInstance().setUserId(userId);
|
await AppProperty.getInstance().setUserId(userId);
|
||||||
if (release) {
|
if (release) {
|
||||||
EventLogger.setUserId(userId);
|
EventLogger.setUserId(userId);
|
||||||
FirebaseCrashlytics.instance.setUserIdentifier(userId);
|
FirebaseCrashlytics.instance.setUserIdentifier(userId);
|
||||||
|
|
@ -528,12 +720,17 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
{String currency = "",
|
{String currency = "",
|
||||||
String contentId = "",
|
String contentId = "",
|
||||||
String adPlatform = "",
|
String adPlatform = "",
|
||||||
Map<String, dynamic> parameters = const <String, dynamic>{}}) {
|
Map<String, dynamic> parameters = const <String, dynamic>{}}) async {
|
||||||
EventLogger.logFbPurchase(amount,
|
Log.i("logPurchase:$amount, $currency, $contentId, $adPlatform, $parameters");
|
||||||
currency: currency,
|
try {
|
||||||
contentId: contentId,
|
await EventLogger.logFbPurchase(amount,
|
||||||
adPlatform: adPlatform,
|
currency: currency,
|
||||||
additionParameters: parameters);
|
contentId: contentId,
|
||||||
|
adPlatform: adPlatform,
|
||||||
|
additionParameters: parameters);
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.w("logFbPurchase error$error, $stacktrace");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void logEventShare({String? itemCategory, String? itemName}) {
|
void logEventShare({String? itemCategory, String? itemName}) {
|
||||||
|
|
@ -547,6 +744,7 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
|
|
||||||
void logSpendCredits(String contentId, String contentType, int price,
|
void logSpendCredits(String contentId, String contentType, int price,
|
||||||
{required String virtualCurrencyName, required int balance, String scene = ''}) {
|
{required String virtualCurrencyName, required int balance, String scene = ''}) {
|
||||||
|
final levelName = GuruApp.instance.protocol.getLevelName();
|
||||||
if (release) {
|
if (release) {
|
||||||
EventLogger.logSpendCredits(contentId, contentType, price,
|
EventLogger.logSpendCredits(contentId, contentType, price,
|
||||||
virtualCurrencyName: virtualCurrencyName, balance: balance, scene: scene);
|
virtualCurrencyName: virtualCurrencyName, balance: balance, scene: scene);
|
||||||
|
|
@ -557,7 +755,8 @@ class GuruAnalytics extends Analytics with AdjustAware {
|
||||||
"virtual_currency_name": virtualCurrencyName,
|
"virtual_currency_name": virtualCurrencyName,
|
||||||
"value": price,
|
"value": price,
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
"scene": scene
|
"scene": scene,
|
||||||
|
"level_name": levelName
|
||||||
};
|
};
|
||||||
Log.d("logEvent: spend_virtual_currency $parameters");
|
Log.d("logEvent: spend_virtual_currency $parameters");
|
||||||
EventLogger.transmit("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);
|
AiBi.instance.spendVirtualCurrency(balance, price.toDouble(), contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logEarnVirtualCurrency({
|
Future<void> logEarnVirtualCurrency(
|
||||||
required String virtualCurrencyName,
|
{required String virtualCurrencyName,
|
||||||
required String method,
|
required String method,
|
||||||
required int balance,
|
required int balance,
|
||||||
required int value,
|
required int value,
|
||||||
}) async {
|
String? specific,
|
||||||
logEvent("earn_virtual_currency", <String, dynamic>{
|
String? scene}) async {
|
||||||
"virtual_currency_name": virtualCurrencyName,
|
final levelName = GuruApp.instance.protocol.getLevelName();
|
||||||
"item_category": method,
|
logEvent(
|
||||||
"value": value,
|
"earn_virtual_currency",
|
||||||
"balance": balance
|
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);
|
AiBi.instance.earnVirtualCurrency(balance, value.toDouble(), method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? peekUserProperty(String key) {
|
||||||
|
return Analytics.userProperties[key];
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setGuruUserProperty(String key, String value) async {
|
Future<void> setGuruUserProperty(String key, String value) async {
|
||||||
|
recordProperty(key, value);
|
||||||
return await EventLogger.setGuruUserProperty(key, value);
|
return await EventLogger.setGuruUserProperty(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,44 @@
|
||||||
part of "../guru_analytics.dart";
|
part of "../guru_analytics.dart";
|
||||||
|
|
||||||
extension AdsAnalytics on GuruAnalytics {
|
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);
|
// 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) {
|
if (release) {
|
||||||
EventLogger.logAdRevenue(adRevenue, adPlatform, currency);
|
EventLogger.logAdRevenue(adRevenue, adPlatform, currency, extras: orderExtras);
|
||||||
} else {
|
} else {
|
||||||
Log.d("[firebase] logAdRevenue ${<String, dynamic>{
|
Log.d("[firebase] logAdRevenue ${<String, dynamic>{
|
||||||
"adRevenue": adRevenue,
|
"adRevenue": adRevenue,
|
||||||
"adPlatform": adPlatform,
|
"adPlatform": adPlatform,
|
||||||
"currency": currency
|
"currency": currency,
|
||||||
|
...orderExtras
|
||||||
|
}}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logAdRevenue020(double adRevenue, String adPlatform, String currency,
|
||||||
|
{String? orderType, String? orderId, String? productId, int? transactionDate}) {
|
||||||
|
// logEventEx(name, itemCategory: scene, itemName: adName);
|
||||||
|
final orderExtras = CollectionUtils.filterOutNulls(<String, dynamic>{
|
||||||
|
"order_type": orderType,
|
||||||
|
"order_id": orderId,
|
||||||
|
"product_id": productId,
|
||||||
|
"trans_ts": transactionDate
|
||||||
|
});
|
||||||
|
if (release) {
|
||||||
|
EventLogger.logAdRevenue020(adRevenue, adPlatform, currency, extras: orderExtras);
|
||||||
|
} else {
|
||||||
|
Log.d("[firebase] logAdRevenue020 ${<String, dynamic>{
|
||||||
|
"adRevenue": adRevenue,
|
||||||
|
"adPlatform": adPlatform,
|
||||||
|
"currency": currency,
|
||||||
|
...orderExtras
|
||||||
}}");
|
}}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:guru_app/analytics/data/analytics_model.dart';
|
import 'package:guru_app/analytics/data/analytics_model.dart';
|
||||||
|
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
part 'orders_model.g.dart';
|
part 'orders_model.g.dart';
|
||||||
|
|
@ -14,8 +15,7 @@ class OrderUserInfo {
|
||||||
|
|
||||||
OrderUserInfo(this.level);
|
OrderUserInfo(this.level);
|
||||||
|
|
||||||
factory OrderUserInfo.fromJson(Map<String, dynamic> json) =>
|
factory OrderUserInfo.fromJson(Map<String, dynamic> json) => _$OrderUserInfoFromJson(json);
|
||||||
_$OrderUserInfoFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$OrderUserInfoToJson(this);
|
Map<String, dynamic> toJson() => _$OrderUserInfoToJson(this);
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +66,12 @@ class OrdersReport {
|
||||||
@JsonKey(name: "eventConfig")
|
@JsonKey(name: "eventConfig")
|
||||||
UserIdentification? userIdentification;
|
UserIdentification? userIdentification;
|
||||||
|
|
||||||
|
@JsonKey(name: "orderId")
|
||||||
|
String? orderId;
|
||||||
|
|
||||||
|
@JsonKey(name: "transactionDate")
|
||||||
|
int? transactionDate;
|
||||||
|
|
||||||
OrdersReport(
|
OrdersReport(
|
||||||
{this.orderType,
|
{this.orderType,
|
||||||
this.token,
|
this.token,
|
||||||
|
|
@ -81,7 +87,9 @@ class OrdersReport {
|
||||||
this.orderUserInfo,
|
this.orderUserInfo,
|
||||||
this.userIdentification,
|
this.userIdentification,
|
||||||
this.offerId,
|
this.offerId,
|
||||||
this.basePlanId});
|
this.basePlanId,
|
||||||
|
this.orderId,
|
||||||
|
this.transactionDate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|
@ -91,6 +99,7 @@ class OrdersReport {
|
||||||
sb.writeln(" price: $price");
|
sb.writeln(" price: $price");
|
||||||
sb.writeln(" currency: $currency");
|
sb.writeln(" currency: $currency");
|
||||||
sb.writeln(" userIdentification: $userIdentification");
|
sb.writeln(" userIdentification: $userIdentification");
|
||||||
|
sb.writeln(" orderId: $orderId");
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
sb.writeln(" orderType: $orderType");
|
sb.writeln(" orderType: $orderType");
|
||||||
sb.writeln(" packageName: $packageName");
|
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}';
|
.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) =>
|
factory OrdersReport.fromJson(Map<String, dynamic> json) => _$OrdersReportFromJson(json);
|
||||||
_$OrdersReportFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$OrdersReportToJson(this);
|
Map<String, dynamic> toJson() => _$OrdersReportToJson(this);
|
||||||
}
|
}
|
||||||
|
|
@ -126,8 +134,7 @@ class OrdersResponse {
|
||||||
|
|
||||||
OrdersResponse(this.usdPrice, this.test);
|
OrdersResponse(this.usdPrice, this.test);
|
||||||
|
|
||||||
factory OrdersResponse.fromJson(Map<String, dynamic> json) =>
|
factory OrdersResponse.fromJson(Map<String, dynamic> json) => _$OrdersResponseFromJson(json);
|
||||||
_$OrdersResponseFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$OrdersResponseToJson(this);
|
Map<String, dynamic> toJson() => _$OrdersResponseToJson(this);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ OrdersReport _$OrdersReportFromJson(Map<String, dynamic> json) => OrdersReport(
|
||||||
json['eventConfig'] as Map<String, dynamic>),
|
json['eventConfig'] as Map<String, dynamic>),
|
||||||
offerId: json['offerId'] as String?,
|
offerId: json['offerId'] as String?,
|
||||||
basePlanId: json['basePlanId'] as String?,
|
basePlanId: json['basePlanId'] as String?,
|
||||||
|
orderId: json['orderId'] as String?,
|
||||||
|
transactionDate: json['transactionDate'] as int?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$OrdersReportToJson(OrdersReport instance) =>
|
Map<String, dynamic> _$OrdersReportToJson(OrdersReport instance) =>
|
||||||
|
|
@ -56,6 +58,8 @@ Map<String, dynamic> _$OrdersReportToJson(OrdersReport instance) =>
|
||||||
'currency': instance.currency,
|
'currency': instance.currency,
|
||||||
'userInfo': instance.orderUserInfo,
|
'userInfo': instance.orderUserInfo,
|
||||||
'eventConfig': instance.userIdentification,
|
'eventConfig': instance.userIdentification,
|
||||||
|
'orderId': instance.orderId,
|
||||||
|
'transactionDate': instance.transactionDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
OrdersResponse _$OrdersResponseFromJson(Map<String, dynamic> json) =>
|
OrdersResponse _$OrdersResponseFromJson(Map<String, dynamic> json) =>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:guru_app/account/account_data_store.dart';
|
||||||
import 'package:guru_app/account/model/user.dart';
|
import 'package:guru_app/account/model/user.dart';
|
||||||
import 'package:guru_app/guru_app.dart';
|
import 'package:guru_app/guru_app.dart';
|
||||||
import 'package:guru_app/property/app_property.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_info.dart';
|
||||||
import 'package:guru_utils/device/device_utils.dart';
|
import 'package:guru_utils/device/device_utils.dart';
|
||||||
import 'package:retrofit/retrofit.dart';
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
|
@ -120,10 +121,28 @@ abstract class GuruApiMethods {
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
@POST("/auth/api/v1/tokens/provider/secret")
|
@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")
|
@POST("/auth/api/v1/renewals/token")
|
||||||
Future<SaasUser> refreshSaasToken();
|
Future<GuruUser> refreshSaasToken();
|
||||||
|
|
||||||
@POST("/auth/api/v1/renewals/firebase")
|
@POST("/auth/api/v1/renewals/firebase")
|
||||||
Future<FirebaseTokenData> renewFirebaseToken();
|
Future<FirebaseTokenData> renewFirebaseToken();
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,14 @@ class _GuruApiMethods implements GuruApiMethods {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SaasUser> signInWithAnonymous(AnonymousLoginReqBody body) async {
|
Future<GuruUser> signInWithAnonymous(AnonymousLoginReqBody body) async {
|
||||||
const _extra = <String, dynamic>{};
|
const _extra = <String, dynamic>{};
|
||||||
final queryParameters = <String, dynamic>{};
|
final queryParameters = <String, dynamic>{};
|
||||||
final _headers = <String, dynamic>{};
|
final _headers = <String, dynamic>{};
|
||||||
final _data = <String, dynamic>{};
|
final _data = <String, dynamic>{};
|
||||||
_data.addAll(body.toJson());
|
_data.addAll(body.toJson());
|
||||||
final _result =
|
final _result =
|
||||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<SaasUser>(Options(
|
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
extra: _extra,
|
extra: _extra,
|
||||||
|
|
@ -69,18 +69,186 @@ class _GuruApiMethods implements GuruApiMethods {
|
||||||
_dio.options.baseUrl,
|
_dio.options.baseUrl,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
))));
|
))));
|
||||||
final value = SaasUser.fromJson(_result.data!);
|
final value = GuruUser.fromJson(_result.data!);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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>{};
|
const _extra = <String, dynamic>{};
|
||||||
final queryParameters = <String, dynamic>{};
|
final queryParameters = <String, dynamic>{};
|
||||||
final _headers = <String, dynamic>{};
|
final _headers = <String, dynamic>{};
|
||||||
final Map<String, dynamic>? _data = null;
|
final Map<String, dynamic>? _data = null;
|
||||||
final _result =
|
final _result =
|
||||||
await _dio.fetch<Map<String, dynamic>>(_setStreamType<SaasUser>(Options(
|
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
extra: _extra,
|
extra: _extra,
|
||||||
|
|
@ -96,7 +264,7 @@ class _GuruApiMethods implements GuruApiMethods {
|
||||||
_dio.options.baseUrl,
|
_dio.options.baseUrl,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
))));
|
))));
|
||||||
final value = SaasUser.fromJson(_result.data!);
|
final value = GuruUser.fromJson(_result.data!);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,36 @@
|
||||||
part of "../guru_api.dart";
|
part of "../guru_api.dart";
|
||||||
|
|
||||||
extension GuruApiExtension on GuruApi {
|
extension GuruApiExtension on GuruApi {
|
||||||
Future<SaasUser> signInWithAnonymous({required String secret}) async {
|
// Future<GuruUser> signInWithAnonymous({required String secret}) async {
|
||||||
return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: secret));
|
// 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 {
|
Future reportDevice(DeviceInfo deviceInfo) async {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:guru_app/firebase/messaging/remote_messaging_manager.dart';
|
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';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
/// Created by Haoyi on 2022/8/29
|
/// Created by Haoyi on 2022/8/29
|
||||||
|
|
@ -64,6 +65,9 @@ class Deployment {
|
||||||
static const int defaultApiTimeout = 15000; // 15s
|
static const int defaultApiTimeout = 15000; // 15s
|
||||||
static const int defaultIosSandboxSubsRenewalSpeed = 2;
|
static const int defaultIosSandboxSubsRenewalSpeed = 2;
|
||||||
static const int defaultTrackingNotificationPermissionPassLimitTimes = 10;
|
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)
|
@JsonKey(name: "property_cache_size", defaultValue: 256)
|
||||||
final int propertyCacheSize;
|
final int propertyCacheSize;
|
||||||
|
|
@ -139,6 +143,21 @@ class Deployment {
|
||||||
@JsonKey(name: "show_internal_ads_when_banner_unavailable", defaultValue: false)
|
@JsonKey(name: "show_internal_ads_when_banner_unavailable", defaultValue: false)
|
||||||
final bool showInternalAdsWhenBannerUnavailable;
|
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(
|
Deployment(
|
||||||
{this.propertyCacheSize = 256,
|
{this.propertyCacheSize = 256,
|
||||||
this.enableDithering = true,
|
this.enableDithering = true,
|
||||||
|
|
@ -164,7 +183,12 @@ class Deployment {
|
||||||
defaultTrackingNotificationPermissionPassLimitTimes,
|
defaultTrackingNotificationPermissionPassLimitTimes,
|
||||||
this.enabledGuruAnalyticsStrategy = false,
|
this.enabledGuruAnalyticsStrategy = false,
|
||||||
this.allowInterstitialAsAlternativeReward = 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);
|
factory Deployment.fromJson(Map<String, dynamic> json) => _$DeploymentFromJson(json);
|
||||||
|
|
||||||
|
|
@ -176,7 +200,11 @@ class RemoteDeployment {
|
||||||
@JsonKey(name: "keep_screen_on_duration_m", defaultValue: 0)
|
@JsonKey(name: "keep_screen_on_duration_m", defaultValue: 0)
|
||||||
final int keepScreenOnDuration;
|
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);
|
factory RemoteDeployment.fromJson(Map<String, dynamic> json) => _$RemoteDeploymentFromJson(json);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,14 @@ Deployment _$DeploymentFromJson(Map<String, dynamic> json) => Deployment(
|
||||||
json['allow_interstitial_as_alternative_reward'] as bool? ?? false,
|
json['allow_interstitial_as_alternative_reward'] as bool? ?? false,
|
||||||
showInternalAdsWhenBannerUnavailable:
|
showInternalAdsWhenBannerUnavailable:
|
||||||
json['show_internal_ads_when_banner_unavailable'] as bool? ?? false,
|
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) =>
|
Map<String, dynamic> _$DeploymentToJson(Deployment instance) =>
|
||||||
|
|
@ -113,6 +121,11 @@ Map<String, dynamic> _$DeploymentToJson(Deployment instance) =>
|
||||||
instance.allowInterstitialAsAlternativeReward,
|
instance.allowInterstitialAsAlternativeReward,
|
||||||
'show_internal_ads_when_banner_unavailable':
|
'show_internal_ads_when_banner_unavailable':
|
||||||
instance.showInternalAdsWhenBannerUnavailable,
|
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 = {
|
const _$PromptTriggerEnumMap = {
|
||||||
|
|
@ -123,9 +136,11 @@ const _$PromptTriggerEnumMap = {
|
||||||
RemoteDeployment _$RemoteDeploymentFromJson(Map<String, dynamic> json) =>
|
RemoteDeployment _$RemoteDeploymentFromJson(Map<String, dynamic> json) =>
|
||||||
RemoteDeployment(
|
RemoteDeployment(
|
||||||
keepScreenOnDuration: json['keep_screen_on_duration_m'] as int? ?? 0,
|
keepScreenOnDuration: json['keep_screen_on_duration_m'] as int? ?? 0,
|
||||||
|
subscriptionGraceDays: json['subscriptionGraceDays'] as int?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RemoteDeploymentToJson(RemoteDeployment instance) =>
|
Map<String, dynamic> _$RemoteDeploymentToJson(RemoteDeployment instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'keep_screen_on_duration_m': instance.keepScreenOnDuration,
|
'keep_screen_on_duration_m': instance.keepScreenOnDuration,
|
||||||
|
'subscriptionGraceDays': instance.subscriptionGraceDays,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,18 @@ import 'package:guru_app/financial/asset/assets_store.dart';
|
||||||
import 'package:guru_app/financial/financial_manager.dart';
|
import 'package:guru_app/financial/financial_manager.dart';
|
||||||
import 'package:guru_app/financial/iap/iap_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/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_manager.dart';
|
||||||
import 'package:guru_app/financial/igc/igc_model.dart';
|
import 'package:guru_app/financial/igc/igc_model.dart';
|
||||||
import 'package:guru_app/financial/product/product_store.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_manager.dart';
|
||||||
import 'package:guru_app/financial/reward/reward_model.dart';
|
import 'package:guru_app/financial/reward/reward_model.dart';
|
||||||
import 'package:guru_app/guru_app.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_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/datetime/datetime_utils.dart';
|
||||||
import 'package:guru_utils/extensions/extensions.dart';
|
import 'package:guru_utils/extensions/extensions.dart';
|
||||||
import 'package:guru_utils/controller/controller.dart';
|
import 'package:guru_utils/controller/controller.dart';
|
||||||
|
|
@ -18,7 +23,7 @@ import 'package:guru_utils/controller/controller.dart';
|
||||||
|
|
||||||
mixin AssetsAware on LifecycleController {
|
mixin AssetsAware on LifecycleController {
|
||||||
final BehaviorSubject<ProductStore<IapProduct>> _productStoreSubject =
|
final BehaviorSubject<ProductStore<IapProduct>> _productStoreSubject =
|
||||||
BehaviorSubject.seeded(ProductStore());
|
BehaviorSubject.seeded(ProductStore());
|
||||||
|
|
||||||
ProductStore<IapProduct> get currentProductStore => _productStoreSubject.value;
|
ProductStore<IapProduct> get currentProductStore => _productStoreSubject.value;
|
||||||
|
|
||||||
|
|
@ -44,6 +49,9 @@ mixin AssetsAware on LifecycleController {
|
||||||
|
|
||||||
Stream<int> get observableIgcBalance => IgcManager.instance.observableCurrentBalance;
|
Stream<int> get observableIgcBalance => IgcManager.instance.observableCurrentBalance;
|
||||||
|
|
||||||
|
Stream<Map<String, InventoryItem>> get observableInventoryItems =>
|
||||||
|
InventoryManager.instance.observableData;
|
||||||
|
|
||||||
Future restorePurchases() async {
|
Future restorePurchases() async {
|
||||||
return await IapManager.instance.restorePurchases();
|
return await IapManager.instance.restorePurchases();
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +81,52 @@ mixin AssetsAware on LifecycleController {
|
||||||
return RewardManager.instance.buildRewardProduct(intent);
|
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 {
|
Future<bool> requestProduct(Product product, {String from = ""}) async {
|
||||||
if (product is IapProduct) {
|
if (product is IapProduct) {
|
||||||
return await IapManager.instance.buy(product);
|
return await IapManager.instance.buy(product);
|
||||||
|
|
@ -80,6 +134,8 @@ mixin AssetsAware on LifecycleController {
|
||||||
return await IgcManager.instance.purchase(product);
|
return await IgcManager.instance.purchase(product);
|
||||||
} else if (product is RewardProduct) {
|
} else if (product is RewardProduct) {
|
||||||
return await RewardManager.instance.claim(product);
|
return await RewardManager.instance.claim(product);
|
||||||
|
} else if (product is IgbProduct) {
|
||||||
|
return await IgbManager.instance.redeem(product);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:guru_app/financial/data/db/order_database.dart';
|
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/database/database.dart';
|
||||||
import 'package:guru_utils/property/storage/db/property_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> _creatorV2 = [OrderEntity.createTable];
|
||||||
|
|
||||||
|
final List<TableCreator> _creatorV4 = [InventoryTable.createTable];
|
||||||
|
|
||||||
class Creators {
|
class Creators {
|
||||||
static final List<TableCreator> creators = [..._creatorV1, ..._creatorV2];
|
static final List<TableCreator> creators = [..._creatorV1, ..._creatorV2, ..._creatorV4];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,5 @@ class GuruDB extends _GuruDB with PropertyDatabase {
|
||||||
List<TableCreator> get tableCreators => Creators.creators;
|
List<TableCreator> get tableCreators => Creators.creators;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get version => 3;
|
int get version => 4;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
part of "migrations.dart";
|
||||||
|
|
||||||
|
class _MigrationV3toV4 implements Migration {
|
||||||
|
@override
|
||||||
|
Future<MigrateResult> migrate(Transaction transaction) async {
|
||||||
|
// 由于这里无法保证所在平台是否支持IF NOT EXISTS,所以这里用try catch来处理
|
||||||
|
try {
|
||||||
|
await InventoryTable.createTable(transaction);
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.w("ignore alter cmd!");
|
||||||
|
}
|
||||||
|
return MigrateResult.success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final migration3to4 = _MigrationV3toV4();
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import 'package:guru_app/financial/data/db/order_database.dart';
|
import 'package:guru_app/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/database/database.dart';
|
||||||
import 'package:guru_utils/log/log.dart';
|
import 'package:guru_utils/log/log.dart';
|
||||||
|
|
||||||
part "migration_v1_to_v2.dart";
|
part "migration_v1_to_v2.dart";
|
||||||
part 'migration_v2_to_v3.dart';
|
part 'migration_v2_to_v3.dart';
|
||||||
|
part 'migration_v3_to_v4.dart';
|
||||||
|
|
||||||
/// Created by @Haoyi on 2020/5/22
|
/// Created by @Haoyi on 2020/5/22
|
||||||
///
|
///
|
||||||
|
|
||||||
class Migrations {
|
class Migrations {
|
||||||
static final migrations = [migration1to2, migration2to3];
|
static final migrations = [migration1to2, migration2to3, migration3to4];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:guru_app/financial/asset/assets_model.dart';
|
||||||
import 'package:guru_app/financial/asset/assets_store.dart';
|
import 'package:guru_app/financial/asset/assets_store.dart';
|
||||||
import 'package:guru_app/financial/iap/iap_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/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/igc/igc_manager.dart';
|
||||||
import 'package:guru_app/financial/reward/reward_manager.dart';
|
import 'package:guru_app/financial/reward/reward_manager.dart';
|
||||||
import 'package:guru_utils/extensions/extensions.dart';
|
import 'package:guru_utils/extensions/extensions.dart';
|
||||||
|
|
@ -54,6 +55,13 @@ class FinancialManager {
|
||||||
void init() {
|
void init() {
|
||||||
IapManager.instance.init();
|
IapManager.instance.init();
|
||||||
IgcManager.instance.init();
|
IgcManager.instance.init();
|
||||||
|
IgbManager.instance.init();
|
||||||
RewardManager.instance.init();
|
RewardManager.instance.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void switchSession(String fromUid, String toUid) {
|
||||||
|
IapManager.instance.switchSession();
|
||||||
|
IgcManager.instance.switchSession();
|
||||||
|
RewardManager.instance.switchSession();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:guru_app/financial/manifest/manifest.dart';
|
||||||
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
||||||
import 'package:guru_app/financial/product/product_model.dart';
|
import 'package:guru_app/financial/product/product_model.dart';
|
||||||
import 'package:guru_app/financial/product/product_store.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/guru_app.dart';
|
||||||
import 'package:guru_app/property/app_property.dart';
|
import 'package:guru_app/property/app_property.dart';
|
||||||
import 'package:guru_app/property/property_keys.dart';
|
import 'package:guru_app/property/property_keys.dart';
|
||||||
|
|
@ -50,17 +51,14 @@ class IapManager {
|
||||||
final BehaviorSubject<AssetsStore<Asset>> _iapStoreSubject =
|
final BehaviorSubject<AssetsStore<Asset>> _iapStoreSubject =
|
||||||
BehaviorSubject.seeded(AssetsStore<Asset>.inactive());
|
BehaviorSubject.seeded(AssetsStore<Asset>.inactive());
|
||||||
|
|
||||||
final Map<ProductId, IapRequest> iapRequestMap =
|
final Map<ProductId, IapRequest> iapRequestMap = HashMap<ProductId, IapRequest>();
|
||||||
HashMap<ProductId, IapRequest>();
|
|
||||||
|
|
||||||
Stream<Map<ProductId, ProductDetails>> get observableProductDetails =>
|
Stream<Map<ProductId, ProductDetails>> get observableProductDetails =>
|
||||||
_productDetailsSubject.stream;
|
_productDetailsSubject.stream;
|
||||||
|
|
||||||
Stream<AssetsStore<Asset>> get observableAssetStore =>
|
Stream<AssetsStore<Asset>> get observableAssetStore => _iapStoreSubject.stream;
|
||||||
_iapStoreSubject.stream;
|
|
||||||
|
|
||||||
Map<ProductId, ProductDetails> get loadedProductDetails =>
|
Map<ProductId, ProductDetails> get loadedProductDetails => _productDetailsSubject.value;
|
||||||
_productDetailsSubject.value;
|
|
||||||
|
|
||||||
AssetsStore<Asset> get purchasedStore => _iapStoreSubject.value;
|
AssetsStore<Asset> get purchasedStore => _iapStoreSubject.value;
|
||||||
|
|
||||||
|
|
@ -83,8 +81,8 @@ class IapManager {
|
||||||
bool _restorePurchase = false;
|
bool _restorePurchase = false;
|
||||||
|
|
||||||
final iapRevenueAppEventOptions = AppEventOptions(
|
final iapRevenueAppEventOptions = AppEventOptions(
|
||||||
capabilities: const AppEventCapabilities(
|
capabilities:
|
||||||
AppEventCapabilities.firebase | AppEventCapabilities.guru),
|
const AppEventCapabilities(AppEventCapabilities.firebase | AppEventCapabilities.guru),
|
||||||
firebaseParamsConvertor: _iapRevenueToValue,
|
firebaseParamsConvertor: _iapRevenueToValue,
|
||||||
guruParamsConvertor: _iapRevenueToValue);
|
guruParamsConvertor: _iapRevenueToValue);
|
||||||
|
|
||||||
|
|
@ -100,8 +98,7 @@ class IapManager {
|
||||||
void init() async {
|
void init() async {
|
||||||
final iapCount = await AppProperty.getInstance().getIapCount();
|
final iapCount = await AppProperty.getInstance().getIapCount();
|
||||||
if (iapCount > 0) {
|
if (iapCount > 0) {
|
||||||
GuruAnalytics.instance
|
GuruAnalytics.instance.setUserProperty("purchase_count", iapCount.toString());
|
||||||
.setUserProperty("purchase_count", iapCount.toString());
|
|
||||||
GuruAnalytics.instance.setUserProperty("is_iap_user", "true");
|
GuruAnalytics.instance.setUserProperty("is_iap_user", "true");
|
||||||
} else {
|
} else {
|
||||||
GuruAnalytics.instance.setUserProperty("is_iap_user", "false");
|
GuruAnalytics.instance.setUserProperty("is_iap_user", "false");
|
||||||
|
|
@ -113,8 +110,7 @@ class IapManager {
|
||||||
stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true);
|
stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true);
|
||||||
}
|
}
|
||||||
if (subscription == null) {
|
if (subscription == null) {
|
||||||
final Stream<List<PurchaseDetails>> purchaseUpdated =
|
final Stream<List<PurchaseDetails>> purchaseUpdated = _inAppPurchase.purchaseStream;
|
||||||
_inAppPurchase.purchaseStream;
|
|
||||||
subscription = purchaseUpdated.listen(
|
subscription = purchaseUpdated.listen(
|
||||||
(List<PurchaseDetails> purchaseDetailsList) {
|
(List<PurchaseDetails> purchaseDetailsList) {
|
||||||
_listenToPurchaseUpdated(purchaseDetailsList);
|
_listenToPurchaseUpdated(purchaseDetailsList);
|
||||||
|
|
@ -141,13 +137,27 @@ class IapManager {
|
||||||
} finally {}
|
} 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 {
|
Future reloadOrders() async {
|
||||||
final transactions = await GuruDB.instance.selectOrders(
|
final transactions = await GuruDB.instance.selectOrders(
|
||||||
method: TransactionMethod.iap,
|
method: TransactionMethod.iap,
|
||||||
attrs: [
|
attrs: [TransactionAttributes.asset, TransactionAttributes.subscriptions]);
|
||||||
TransactionAttributes.asset,
|
|
||||||
TransactionAttributes.subscriptions
|
|
||||||
]);
|
|
||||||
final newAssetStore = AssetsStore<Asset>();
|
final newAssetStore = AssetsStore<Asset>();
|
||||||
Log.d("reloadOrders ${transactions.length}");
|
Log.d("reloadOrders ${transactions.length}");
|
||||||
for (var transaction in transactions) {
|
for (var transaction in transactions) {
|
||||||
|
|
@ -165,15 +175,14 @@ class IapManager {
|
||||||
do {
|
do {
|
||||||
final seconds = min(MathUtils.fibonacci(retry, offset: 2), 900);
|
final seconds = min(MathUtils.fibonacci(retry, offset: 2), 900);
|
||||||
await Future.delayed(Duration(seconds: seconds));
|
await Future.delayed(Duration(seconds: seconds));
|
||||||
available =
|
available = await _inAppPurchase.isAvailable().catchError((error, stacktrace) {
|
||||||
await _inAppPurchase.isAvailable().catchError((error, stacktrace) {
|
|
||||||
Log.w("isAvailable error:$error", stackTrace: stacktrace);
|
Log.w("isAvailable error:$error", stackTrace: stacktrace);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
Log.d("_checkAndLoad:$retry available:$available");
|
Log.d("_checkAndLoad:$retry available:$available");
|
||||||
retry++;
|
retry++;
|
||||||
} while (!available);
|
} while (!available);
|
||||||
availableSubject.addEx(true);
|
availableSubject.addIfChanged(true);
|
||||||
try {
|
try {
|
||||||
await refreshProducts();
|
await refreshProducts();
|
||||||
if (GuruApp.instance.appSpec.deployment.autoRestoreIap ||
|
if (GuruApp.instance.appSpec.deployment.autoRestoreIap ||
|
||||||
|
|
@ -196,12 +205,9 @@ class IapManager {
|
||||||
iapRequest.response(false);
|
iapRequest.response(false);
|
||||||
final iapErrorMsg = "_processIapError:${iapRequest.productId}";
|
final iapErrorMsg = "_processIapError:${iapRequest.productId}";
|
||||||
Log.w(iapErrorMsg,
|
Log.w(iapErrorMsg,
|
||||||
error: PurchaseError(iapErrorMsg),
|
error: PurchaseError(iapErrorMsg), syncFirebase: true, syncCrashlytics: true);
|
||||||
syncFirebase: true,
|
|
||||||
syncCrashlytics: true);
|
|
||||||
try {
|
try {
|
||||||
await GuruDB.instance
|
await GuruDB.instance.upsertOrder(order: iapRequest.order.error(iapErrorMsg));
|
||||||
.upsertOrder(order: iapRequest.order.error(iapErrorMsg));
|
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Log.w("_processIapError upsert error! $error", syncFirebase: true);
|
Log.w("_processIapError upsert error! $error", syncFirebase: true);
|
||||||
}
|
}
|
||||||
|
|
@ -218,8 +224,7 @@ class IapManager {
|
||||||
try {
|
try {
|
||||||
await GuruDB.instance.deleteOrder(order: order);
|
await GuruDB.instance.deleteOrder(order: order);
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Log.w("_processIapCancel deleteOrder error! $error",
|
Log.w("_processIapCancel deleteOrder error! $error", syncFirebase: true);
|
||||||
syncFirebase: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iapRequestMap.clear();
|
iapRequestMap.clear();
|
||||||
|
|
@ -248,22 +253,18 @@ class IapManager {
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
String dumpProductAndPurchased(
|
String dumpProductAndPurchased(ProductDetails details, PurchaseDetails purchaseDetails) {
|
||||||
ProductDetails details, PurchaseDetails purchaseDetails) {
|
|
||||||
final StringBuffer sb = StringBuffer();
|
final StringBuffer sb = StringBuffer();
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
try {
|
try {
|
||||||
GooglePlayPurchaseDetails googlePlayDetails =
|
GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails;
|
||||||
purchaseDetails as GooglePlayPurchaseDetails;
|
GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails;
|
||||||
GooglePlayProductDetails googlePlayProduct =
|
|
||||||
details as GooglePlayProductDetails;
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}");
|
"Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}");
|
||||||
} catch (error, stacktrace) {}
|
} catch (error, stacktrace) {}
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
AppStorePurchaseDetails appleDetails =
|
AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails;
|
||||||
purchaseDetails as AppStorePurchaseDetails;
|
|
||||||
AppStoreProductDetails appleProduct = details as AppStoreProductDetails;
|
AppStoreProductDetails appleProduct = details as AppStoreProductDetails;
|
||||||
sb.writeln("#### purchase ####");
|
sb.writeln("#### purchase ####");
|
||||||
sb.writeln("productID: ${appleDetails.productID}");
|
sb.writeln("productID: ${appleDetails.productID}");
|
||||||
|
|
@ -274,23 +275,18 @@ class IapManager {
|
||||||
sb.writeln("skPaymentTransaction:");
|
sb.writeln("skPaymentTransaction:");
|
||||||
sb.writeln(
|
sb.writeln(
|
||||||
" =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}");
|
" =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}");
|
||||||
sb.writeln(
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
||||||
" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
|
||||||
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:");
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:");
|
||||||
sb.writeln(
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
||||||
" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
|
||||||
sb.writeln("\n#### product ####");
|
sb.writeln("\n#### product ####");
|
||||||
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
||||||
sb.writeln("rawPrice: ${appleProduct.rawPrice}");
|
sb.writeln("rawPrice: ${appleProduct.rawPrice}");
|
||||||
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
||||||
sb.writeln("currencyCode skProduct");
|
sb.writeln("currencyCode skProduct");
|
||||||
sb.writeln(
|
sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
||||||
" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
||||||
sb.writeln(
|
|
||||||
" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
|
||||||
sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}");
|
sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}");
|
||||||
sb.writeln(
|
sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
||||||
" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
|
||||||
sb.writeln(
|
sb.writeln(
|
||||||
" =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}");
|
" =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}");
|
||||||
sb.writeln(" =>appleProduct.skProduct.priceLocale");
|
sb.writeln(" =>appleProduct.skProduct.priceLocale");
|
||||||
|
|
@ -319,9 +315,8 @@ class IapManager {
|
||||||
|
|
||||||
int getIOSPeriodInterval(int numberOfUnits, SKSubscriptionPeriodUnit unit) {
|
int getIOSPeriodInterval(int numberOfUnits, SKSubscriptionPeriodUnit unit) {
|
||||||
if (GuruSettings.instance.debugMode.get()) {
|
if (GuruSettings.instance.debugMode.get()) {
|
||||||
final renewalSpeed = GuruApp
|
final renewalSpeed =
|
||||||
.instance.appSpec.deployment.iosSandboxSubsRenewalSpeed
|
GuruApp.instance.appSpec.deployment.iosSandboxSubsRenewalSpeed.clamp(1, 5);
|
||||||
.clamp(1, 5);
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case SKSubscriptionPeriodUnit.day:
|
case SKSubscriptionPeriodUnit.day:
|
||||||
return numberOfUnits * weekRenewalDurations[renewalSpeed - 1] ~/ 7;
|
return numberOfUnits * weekRenewalDurations[renewalSpeed - 1] ~/ 7;
|
||||||
|
|
@ -346,8 +341,52 @@ class IapManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future processRestoredSubscription(
|
bool checkSubscriptionPeriod(PurchaseDetails purchaseDetails, ProductDetails productDetails) {
|
||||||
List<PurchaseDetails> subscriptionPurchased) async {
|
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;
|
List<PurchaseDetails> purchasedDetails = subscriptionPurchased;
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
purchasedDetails = buildLatestPurchasedPlanForIos(subscriptionPurchased);
|
purchasedDetails = buildLatestPurchasedPlanForIos(subscriptionPurchased);
|
||||||
|
|
@ -359,8 +398,7 @@ class IapManager {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final purchasedSkus = purchasedDetails.map((e) => e.productID).toSet();
|
final purchasedSkus = purchasedDetails.map((e) => e.productID).toSet();
|
||||||
newPurchasedStore.removeWhere((productId, asset) {
|
newPurchasedStore.removeWhere((productId, asset) {
|
||||||
final expired =
|
final expired = productId.isSubscription && !purchasedSkus.contains(productId.sku);
|
||||||
productId.isSubscription && !purchasedSkus.contains(productId.sku);
|
|
||||||
Log.i("remove expired subscription[$productId] expired:$expired");
|
Log.i("remove expired subscription[$productId] expired:$expired");
|
||||||
if (expired) {
|
if (expired) {
|
||||||
expiredSkus.add(asset.productId.sku);
|
expiredSkus.add(asset.productId.sku);
|
||||||
|
|
@ -370,8 +408,7 @@ class IapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var purchased in purchasedDetails) {
|
for (var purchased in purchasedDetails) {
|
||||||
final productId =
|
final productId = GuruApp.instance.findProductId(sku: purchased.productID);
|
||||||
GuruApp.instance.findProductId(sku: purchased.productID);
|
|
||||||
if (productId == null) {
|
if (productId == null) {
|
||||||
Log.w("productId is null! ${purchased.productID}");
|
Log.w("productId is null! ${purchased.productID}");
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -381,26 +418,7 @@ class IapManager {
|
||||||
Log.w("product is null! ${purchased.productID}");
|
Log.w("product is null! ${purchased.productID}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
purchased.transactionDate;
|
final bool validPurchase = checkSubscriptionPeriod(purchased, productDetails);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (validPurchase) {
|
if (validPurchase) {
|
||||||
Log.d(
|
Log.d(
|
||||||
"[Restored Subscription] productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}",
|
"[Restored Subscription] productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}",
|
||||||
|
|
@ -408,8 +426,8 @@ class IapManager {
|
||||||
final asset = newPurchasedStore.getAsset(productId);
|
final asset = newPurchasedStore.getAsset(productId);
|
||||||
late OrderEntity newOrder;
|
late OrderEntity newOrder;
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
final product = await _createProduct(
|
final product =
|
||||||
productId.createIntent(scene: "restore"), productDetails);
|
await _createProduct(productId.createIntent(scene: "restore"), productDetails);
|
||||||
newOrder = product.createOrder().success();
|
newOrder = product.createOrder().success();
|
||||||
} else {
|
} else {
|
||||||
newOrder = asset.order.success();
|
newOrder = asset.order.success();
|
||||||
|
|
@ -417,8 +435,7 @@ class IapManager {
|
||||||
try {
|
try {
|
||||||
await GuruDB.instance.replaceOrderBySku(order: newOrder);
|
await GuruDB.instance.replaceOrderBySku(order: newOrder);
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Log.w("Failed to upsert order: $error $stacktrace",
|
Log.w("Failed to upsert order: $error $stacktrace", tag: PropertyTags.iap);
|
||||||
tag: PropertyTags.iap);
|
|
||||||
}
|
}
|
||||||
final newAsset = Asset(productId, newOrder);
|
final newAsset = Asset(productId, newOrder);
|
||||||
newPurchasedStore.addAsset(newAsset);
|
newPurchasedStore.addAsset(newAsset);
|
||||||
|
|
@ -431,21 +448,30 @@ class IapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expiredSkus.isNotEmpty) {
|
if (expiredSkus.isNotEmpty) {
|
||||||
Log.i("expired orders:${expiredSkus.length}}");
|
final graceCount = await AppProperty.getInstance().increaseGraceCount();
|
||||||
try {
|
Log.i("expired orders:${expiredSkus.length}} grace count: $graceCount");
|
||||||
await GuruDB.instance.deleteOrdersBySkus(expiredSkus);
|
if (graceCount > GuruApp.instance.appSpec.deployment.subscriptionRestoreGraceCount) {
|
||||||
} catch (error, stacktrace) {
|
try {
|
||||||
Log.w("Failed to upsert order: $error $stacktrace",
|
await GuruDB.instance.deleteOrdersBySkus(expiredSkus);
|
||||||
tag: PropertyTags.iap);
|
} 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);
|
_iapStoreSubject.addEx(newPurchasedStore);
|
||||||
Log.d(
|
Log.d("[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}");
|
||||||
"[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PurchaseDetails> buildLatestPurchasedPlanForIos(
|
List<PurchaseDetails> buildLatestPurchasedPlanForIos(List<PurchaseDetails> purchaseDetails) {
|
||||||
List<PurchaseDetails> purchaseDetails) {
|
|
||||||
if (purchaseDetails.isEmpty) {
|
if (purchaseDetails.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -459,15 +485,14 @@ class IapManager {
|
||||||
.toSet();
|
.toSet();
|
||||||
Log.d("rawTransactionIds:$rawTransactionIds");
|
Log.d("rawTransactionIds:$rawTransactionIds");
|
||||||
final sortedPurchaseDetails = purchaseDetails.toList();
|
final sortedPurchaseDetails = purchaseDetails.toList();
|
||||||
sortedPurchaseDetails.sort((a, b) =>
|
sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0)
|
||||||
(int.tryParse(b.transactionDate ?? '') ?? 0)
|
.compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
|
||||||
.compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
|
|
||||||
sortedPurchaseDetails.retainWhere((details) {
|
sortedPurchaseDetails.retainWhere((details) {
|
||||||
var detail = details as AppStorePurchaseDetails;
|
var detail = details as AppStorePurchaseDetails;
|
||||||
Log.d(
|
Log.d(
|
||||||
"checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}");
|
"checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}");
|
||||||
return rawTransactionIds.remove(detail
|
return rawTransactionIds
|
||||||
.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
.remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
||||||
});
|
});
|
||||||
|
|
||||||
return sortedPurchaseDetails;
|
return sortedPurchaseDetails;
|
||||||
|
|
@ -487,15 +512,14 @@ class IapManager {
|
||||||
.toSet();
|
.toSet();
|
||||||
Log.d("rawTransactionIds:$rawTransactionIds");
|
Log.d("rawTransactionIds:$rawTransactionIds");
|
||||||
final sortedPurchaseDetails = purchaseDetails.toList();
|
final sortedPurchaseDetails = purchaseDetails.toList();
|
||||||
sortedPurchaseDetails.sort((a, b) =>
|
sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0)
|
||||||
(int.tryParse(b.transactionDate ?? '') ?? 0)
|
.compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
|
||||||
.compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
|
|
||||||
sortedPurchaseDetails.retainWhere((details) {
|
sortedPurchaseDetails.retainWhere((details) {
|
||||||
var detail = details as AppStorePurchaseDetails;
|
var detail = details as AppStorePurchaseDetails;
|
||||||
Log.d(
|
Log.d(
|
||||||
"checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}");
|
"checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}");
|
||||||
return rawTransactionIds.remove(detail
|
return rawTransactionIds
|
||||||
.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
.remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (var details in sortedPurchaseDetails) {
|
for (var details in sortedPurchaseDetails) {
|
||||||
|
|
@ -509,8 +533,7 @@ class IapManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenToPurchaseUpdated(
|
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) async {
|
||||||
List<PurchaseDetails> purchaseDetailsList) async {
|
|
||||||
final List<Tuple2<ProductId, PurchaseDetails>> restoredIapPurchases = [];
|
final List<Tuple2<ProductId, PurchaseDetails>> restoredIapPurchases = [];
|
||||||
final List<Tuple2<ProductId, PurchaseDetails>> pendingCompletePurchase = [];
|
final List<Tuple2<ProductId, PurchaseDetails>> pendingCompletePurchase = [];
|
||||||
final List<PurchaseDetails> subscriptionPurchases = [];
|
final List<PurchaseDetails> subscriptionPurchases = [];
|
||||||
|
|
@ -532,9 +555,7 @@ class IapManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (var details in purchaseDetailsList) {
|
for (var details in purchaseDetailsList) {
|
||||||
final productId =
|
final productId = GuruApp.instance.findProductId(sku: details.productID) ?? ProductId.invalid;
|
||||||
GuruApp.instance.findProductId(sku: details.productID) ??
|
|
||||||
ProductId.invalid;
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"[details]: $productId [${details.productID}] ${details.purchaseID} ${details.status} ${details.pendingCompletePurchase}");
|
"[details]: $productId [${details.productID}] ${details.purchaseID} ${details.status} ${details.pendingCompletePurchase}");
|
||||||
GuruAnalytics.instance.logGuruEvent('dev_iap_update', {
|
GuruAnalytics.instance.logGuruEvent('dev_iap_update', {
|
||||||
|
|
@ -557,7 +578,13 @@ class IapManager {
|
||||||
|
|
||||||
final productDetails = loadedProductDetails[productId];
|
final productDetails = loadedProductDetails[productId];
|
||||||
if (productDetails != null) {
|
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}");
|
Log.d("completePurchase ${details.productID} ${details.purchaseID}");
|
||||||
|
|
@ -574,9 +601,8 @@ class IapManager {
|
||||||
}
|
}
|
||||||
// 如果是未完成的商品或是恢复出了消耗品,都需要手动完成
|
// 如果是未完成的商品或是恢复出了消耗品,都需要手动完成
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final originPurchaseState = (details as GooglePlayPurchaseDetails)
|
final originPurchaseState =
|
||||||
.billingClientPurchase
|
(details as GooglePlayPurchaseDetails).billingClientPurchase.purchaseState;
|
||||||
.purchaseState;
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"restore android ${details.pendingCompletePurchase} $productId $originPurchaseState");
|
"restore android ${details.pendingCompletePurchase} $productId $originPurchaseState");
|
||||||
if (originPurchaseState == PurchaseStateWrapper.purchased) {
|
if (originPurchaseState == PurchaseStateWrapper.purchased) {
|
||||||
|
|
@ -614,8 +640,7 @@ class IapManager {
|
||||||
if (existsRestored) {
|
if (existsRestored) {
|
||||||
if (pendingCompletePurchase.isNotEmpty) {
|
if (pendingCompletePurchase.isNotEmpty) {
|
||||||
await completeAllPurchases(pendingCompletePurchase);
|
await completeAllPurchases(pendingCompletePurchase);
|
||||||
Log.d("manual complete/consume all purchases!",
|
Log.d("manual complete/consume all purchases!", syncFirebase: true, syncCrashlytics: true);
|
||||||
syncFirebase: true, syncCrashlytics: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restoredIapPurchases.isNotEmpty) {
|
if (restoredIapPurchases.isNotEmpty) {
|
||||||
|
|
@ -659,8 +684,8 @@ class IapManager {
|
||||||
upsertOrders.add(newOrder);
|
upsertOrders.add(newOrder);
|
||||||
}
|
}
|
||||||
} else if (productDetails != null) {
|
} else if (productDetails != null) {
|
||||||
final product = await _createProduct(
|
final product =
|
||||||
productId.createIntent(scene: "restore"), productDetails);
|
await _createProduct(productId.createIntent(scene: "restore"), productDetails);
|
||||||
final newOrder = product.createOrder().success();
|
final newOrder = product.createOrder().success();
|
||||||
upsertOrders.add(newOrder);
|
upsertOrders.add(newOrder);
|
||||||
}
|
}
|
||||||
|
|
@ -671,20 +696,17 @@ class IapManager {
|
||||||
await GuruDB.instance.upsertOrders(upsertOrders);
|
await GuruDB.instance.upsertOrders(upsertOrders);
|
||||||
updatedOrder.addAll(upsertOrders);
|
updatedOrder.addAll(upsertOrders);
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Log.w("upsertOrders error:$error $stacktrace",
|
Log.w("upsertOrders error:$error $stacktrace", syncCrashlytics: true, syncFirebase: true);
|
||||||
syncCrashlytics: true, syncFirebase: true);
|
|
||||||
for (var order in upsertOrders) {
|
for (var order in upsertOrders) {
|
||||||
try {
|
try {
|
||||||
await GuruDB.instance.upsertOrder(order: order);
|
await GuruDB.instance.upsertOrder(order: order);
|
||||||
updatedOrder.add(order);
|
updatedOrder.add(order);
|
||||||
} catch (error1, stacktrace1) {
|
} catch (error1, stacktrace1) {
|
||||||
Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1",
|
Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1", syncFirebase: true);
|
||||||
syncFirebase: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final assets =
|
final assets = updatedOrder.map((order) => Asset(order.productId, order)).toList();
|
||||||
updatedOrder.map((order) => Asset(order.productId, order)).toList();
|
|
||||||
newPurchased.addAllAssets(assets);
|
newPurchased.addAllAssets(assets);
|
||||||
}
|
}
|
||||||
_iapStoreSubject.addEx(newPurchased);
|
_iapStoreSubject.addEx(newPurchased);
|
||||||
|
|
@ -692,16 +714,13 @@ class IapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future reportFailedOrders() async {
|
Future reportFailedOrders() async {
|
||||||
final failedIapOrders =
|
final failedIapOrders = await AppProperty.getInstance().loadAllFailedIapOrders();
|
||||||
await AppProperty.getInstance().loadAllFailedIapOrders();
|
|
||||||
failedIapOrders.forEach((key, value) async {
|
failedIapOrders.forEach((key, value) async {
|
||||||
try {
|
try {
|
||||||
final order = OrdersReport.fromJson(json.decode(value));
|
final order = OrdersReport.fromJson(json.decode(value));
|
||||||
final result = await GuruApi.instance.reportOrders(order);
|
final result = await GuruApi.instance.reportOrders(order);
|
||||||
if (result.usdPrice > 0) {
|
// 这里不管返回什么值,都认为是成功的, 只有崩溃才会返回失败
|
||||||
logRevenue(
|
await logRevenue(order, result);
|
||||||
result.usdPrice, order.productId ?? order.subscriptionId);
|
|
||||||
}
|
|
||||||
AppProperty.getInstance().removeReportSuccessOrder(key);
|
AppProperty.getInstance().removeReportSuccessOrder(key);
|
||||||
} catch (error, stacktrace) {}
|
} catch (error, stacktrace) {}
|
||||||
});
|
});
|
||||||
|
|
@ -709,8 +728,7 @@ class IapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
String buildGooglePlayDetailsString(
|
String buildGooglePlayDetailsString(
|
||||||
GooglePlayProductDetails googlePlayProduct,
|
GooglePlayProductDetails googlePlayProduct, GooglePlayPurchaseDetails googlePlayDetails) {
|
||||||
GooglePlayPurchaseDetails googlePlayDetails) {
|
|
||||||
final StringBuffer sb = StringBuffer();
|
final StringBuffer sb = StringBuffer();
|
||||||
sb.writeln("#### purchase ####");
|
sb.writeln("#### purchase ####");
|
||||||
|
|
||||||
|
|
@ -742,15 +760,12 @@ class IapManager {
|
||||||
if (oneTimeDetails != null) {
|
if (oneTimeDetails != null) {
|
||||||
sb.writeln(" => oneTimeDetails:");
|
sb.writeln(" => oneTimeDetails:");
|
||||||
sb.writeln(" - formattedPrice: ${oneTimeDetails.formattedPrice}");
|
sb.writeln(" - formattedPrice: ${oneTimeDetails.formattedPrice}");
|
||||||
sb.writeln(
|
sb.writeln(" - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}");
|
||||||
" - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}");
|
sb.writeln(" - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}");
|
||||||
sb.writeln(
|
|
||||||
" - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
||||||
if (subscriptionOfferDetails != null &&
|
if (subscriptionOfferDetails != null && subscriptionOfferDetails.isNotEmpty) {
|
||||||
subscriptionOfferDetails.isNotEmpty) {
|
|
||||||
for (var offer in subscriptionOfferDetails) {
|
for (var offer in subscriptionOfferDetails) {
|
||||||
sb.writeln(" => sub offer: ${offer.offerId}");
|
sb.writeln(" => sub offer: ${offer.offerId}");
|
||||||
sb.writeln(" - basePlanId: ${offer.basePlanId}");
|
sb.writeln(" - basePlanId: ${offer.basePlanId}");
|
||||||
|
|
@ -773,13 +788,12 @@ class IapManager {
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future reportOrders(ProductId productId, ProductDetails details,
|
Future reportOrders(ProductId productId, ProductDetails details, PurchaseDetails purchaseDetails,
|
||||||
PurchaseDetails purchaseDetails, OrderEntity? order) async {
|
OrderEntity? order) async {
|
||||||
final OrdersReport ordersReport = OrdersReport();
|
final OrdersReport ordersReport = OrdersReport();
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
ordersReport.token =
|
ordersReport.token = purchaseDetails.verificationData.serverVerificationData;
|
||||||
purchaseDetails.verificationData.serverVerificationData;
|
|
||||||
ordersReport.packageName = GuruApp.instance.details.packageName;
|
ordersReport.packageName = GuruApp.instance.details.packageName;
|
||||||
final manifest = order?.manifest;
|
final manifest = order?.manifest;
|
||||||
final basePlanId = manifest?.basePlanId;
|
final basePlanId = manifest?.basePlanId;
|
||||||
|
|
@ -789,16 +803,13 @@ class IapManager {
|
||||||
ordersReport.offerId = offerId;
|
ordersReport.offerId = offerId;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
GooglePlayPurchaseDetails googlePlayDetails =
|
GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails;
|
||||||
purchaseDetails as GooglePlayPurchaseDetails;
|
GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails;
|
||||||
GooglePlayProductDetails googlePlayProduct =
|
|
||||||
details as GooglePlayProductDetails;
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}");
|
"Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}");
|
||||||
} catch (error, stacktrace) {}
|
} catch (error, stacktrace) {}
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
AppStorePurchaseDetails appleDetails =
|
AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails;
|
||||||
purchaseDetails as AppStorePurchaseDetails;
|
|
||||||
AppStoreProductDetails appleProduct = details as AppStoreProductDetails;
|
AppStoreProductDetails appleProduct = details as AppStoreProductDetails;
|
||||||
final StringBuffer sb = StringBuffer();
|
final StringBuffer sb = StringBuffer();
|
||||||
sb.writeln("#### purchase ####");
|
sb.writeln("#### purchase ####");
|
||||||
|
|
@ -810,31 +821,25 @@ class IapManager {
|
||||||
sb.writeln("skPaymentTransaction:");
|
sb.writeln("skPaymentTransaction:");
|
||||||
sb.writeln(
|
sb.writeln(
|
||||||
" =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}");
|
" =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}");
|
||||||
sb.writeln(
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
||||||
" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
|
||||||
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:");
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:");
|
||||||
sb.writeln(
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
||||||
" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
|
||||||
sb.writeln("\n#### product ####");
|
sb.writeln("\n#### product ####");
|
||||||
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
||||||
sb.writeln("rawPrice: ${appleProduct.rawPrice}");
|
sb.writeln("rawPrice: ${appleProduct.rawPrice}");
|
||||||
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
||||||
sb.writeln("currencyCode skProduct");
|
sb.writeln("currencyCode skProduct");
|
||||||
sb.writeln(
|
sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
||||||
" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
||||||
sb.writeln(
|
|
||||||
" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
|
||||||
sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}");
|
sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}");
|
||||||
sb.writeln(
|
sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
||||||
" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
|
||||||
sb.writeln(
|
sb.writeln(
|
||||||
" =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}");
|
" =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}");
|
||||||
sb.writeln(" =>appleProduct.skProduct.priceLocale");
|
sb.writeln(" =>appleProduct.skProduct.priceLocale");
|
||||||
sb.writeln(" ->{appleProduct.skProduct.priceLocale}");
|
sb.writeln(" ->{appleProduct.skProduct.priceLocale}");
|
||||||
|
|
||||||
ordersReport.bundleId = GuruApp.instance.appSpec.details.bundleId;
|
ordersReport.bundleId = GuruApp.instance.appSpec.details.bundleId;
|
||||||
ordersReport.receipt =
|
ordersReport.receipt = purchaseDetails.verificationData.serverVerificationData;
|
||||||
purchaseDetails.verificationData.serverVerificationData;
|
|
||||||
ordersReport.sku = appleDetails.productID;
|
ordersReport.sku = appleDetails.productID;
|
||||||
ordersReport.countryCode = appleProduct.skProduct.priceLocale.countryCode;
|
ordersReport.countryCode = appleProduct.skProduct.priceLocale.countryCode;
|
||||||
Log.d("IOS Product/Purchase ${sb.toString()}");
|
Log.d("IOS Product/Purchase ${sb.toString()}");
|
||||||
|
|
@ -847,40 +852,66 @@ class IapManager {
|
||||||
ordersReport.orderType = OrderType.inapp;
|
ordersReport.orderType = OrderType.inapp;
|
||||||
ordersReport.productId = details.id;
|
ordersReport.productId = details.id;
|
||||||
}
|
}
|
||||||
|
ordersReport.orderId = purchaseDetails.purchaseID;
|
||||||
ordersReport.price = details.rawPrice.toString();
|
ordersReport.price = details.rawPrice.toString();
|
||||||
ordersReport.currency = details.currencyCode;
|
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;
|
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");
|
Log.d("orderReport:$ordersReport", tag: "Iap");
|
||||||
try {
|
try {
|
||||||
final result = await GuruApi.instance.reportOrders(ordersReport);
|
final result = await GuruApi.instance.reportOrders(ordersReport);
|
||||||
if ((result.usdPrice > 0) ||
|
// 这里不管返回什么值,都认为是成功的
|
||||||
(result.usdPrice == 0 && result.isTestOrder)) {
|
await logRevenue(ordersReport, result);
|
||||||
logRevenue(result.usdPrice, purchaseDetails.productID);
|
return;
|
||||||
Log.i("reportOrders success! $result");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Log.i("ignoreInvalidResult $result", tag: "Iap");
|
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Log.i("reportOrders error!", error: error, stackTrace: stacktrace);
|
Log.i("reportOrders error!", error: error, stackTrace: stacktrace);
|
||||||
}
|
}
|
||||||
AppProperty.getInstance().saveFailedIapOrders(ordersReport);
|
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) {
|
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";
|
final platform = Platform.isIOS ? "appstore" : "google_play";
|
||||||
GuruAnalytics.instance.logAdRevenue(usdPrice, platform, "USD");
|
if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 2) {
|
||||||
final productId =
|
GuruAnalytics.instance.logAdRevenue020(usdPrice, platform, "USD",
|
||||||
GuruApp.instance.findProductId(sku: sku) ?? ProductId.invalid;
|
orderId: order.orderId,
|
||||||
GuruAnalytics.instance.logPurchase(usdPrice,
|
orderType: isSubscription ? "SUB" : "IAP",
|
||||||
currency: 'USD', contentId: sku, adPlatform: platform);
|
productId: sku,
|
||||||
if (productId.isSubscription) {
|
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(
|
GuruAnalytics.instance.logEvent(
|
||||||
"sub_purchase",
|
"sub_purchase",
|
||||||
{
|
{
|
||||||
|
|
@ -888,6 +919,9 @@ class IapManager {
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"revenue": usdPrice,
|
"revenue": usdPrice,
|
||||||
"product_id": sku,
|
"product_id": sku,
|
||||||
|
"order_type": "SUB",
|
||||||
|
"order_id": order.orderId,
|
||||||
|
"trans_ts": order.transactionDate
|
||||||
},
|
},
|
||||||
options: iapRevenueAppEventOptions);
|
options: iapRevenueAppEventOptions);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -898,11 +932,16 @@ class IapManager {
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"revenue": usdPrice,
|
"revenue": usdPrice,
|
||||||
"product_id": sku,
|
"product_id": sku,
|
||||||
|
"order_type": "IAP",
|
||||||
|
"order_id": order.orderId,
|
||||||
|
"trans_ts": order.transactionDate
|
||||||
},
|
},
|
||||||
options: iapRevenueAppEventOptions);
|
options: iapRevenueAppEventOptions);
|
||||||
}
|
}
|
||||||
GuruAnalytics.instance.logGuruEvent("dev_iap_action",
|
GuruAnalytics.instance.logGuruEvent(
|
||||||
{"item_category": "reported", "item_name": sku, "result": "true"});
|
"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 {
|
Future _deliverManifest(ProductId productId, Manifest manifest) async {
|
||||||
|
|
@ -912,8 +951,7 @@ class IapManager {
|
||||||
result = await ManifestManager.instance
|
result = await ManifestManager.instance
|
||||||
.deliver(manifest, TransactionMethod.iap)
|
.deliver(manifest, TransactionMethod.iap)
|
||||||
.catchError((error) {
|
.catchError((error) {
|
||||||
Log.w("applyManifest error:$error",
|
Log.w("applyManifest error:$error", syncCrashlytics: true, syncFirebase: true);
|
||||||
syncCrashlytics: true, syncFirebase: true);
|
|
||||||
});
|
});
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
cause = error.toString();
|
cause = error.toString();
|
||||||
|
|
@ -991,8 +1029,7 @@ class IapManager {
|
||||||
await _inAppPurchase.completePurchase(details);
|
await _inAppPurchase.completePurchase(details);
|
||||||
final count = await AppProperty.getInstance().increaseAndGetIapCount();
|
final count = await AppProperty.getInstance().increaseAndGetIapCount();
|
||||||
GuruAnalytics.instance.setUserProperty("purchase_count", count.toString());
|
GuruAnalytics.instance.setUserProperty("purchase_count", count.toString());
|
||||||
Log.d("_completePurchase $productId ${details.pendingCompletePurchase}",
|
Log.d("_completePurchase $productId ${details.pendingCompletePurchase}", tag: "Iap");
|
||||||
tag: "Iap");
|
|
||||||
OrderEntity? resultOrder;
|
OrderEntity? resultOrder;
|
||||||
|
|
||||||
IapRequest? iapRequest = iapRequestMap.remove(productId);
|
IapRequest? iapRequest = iapRequestMap.remove(productId);
|
||||||
|
|
@ -1075,27 +1112,24 @@ class IapManager {
|
||||||
|
|
||||||
await appProperty.getAndIncrease(PropertyKeys.subscriptionCount);
|
await appProperty.getAndIncrease(PropertyKeys.subscriptionCount);
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
await appProperty
|
await appProperty.getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group));
|
||||||
.getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group));
|
|
||||||
}
|
}
|
||||||
await appProperty
|
await appProperty.getAndIncrease(PropertyKeys.buildSubscriptionCount(productId));
|
||||||
.getAndIncrease(PropertyKeys.buildSubscriptionCount(productId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Manifest> createPurchaseManifest(TransactionIntent intent) {
|
Future<Manifest> createPurchaseManifest(TransactionIntent intent) {
|
||||||
return ManifestManager.instance.createManifest(intent);
|
return ManifestManager.instance.createManifest(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ProductDetails?> checkAndDistributeOfferDetails(ProductId productId,
|
Future<ProductDetails?> checkAndDistributeOfferDetails(
|
||||||
ProductDetails? details, EligibilityCriteria eligibilityCriteria) async {
|
ProductId productId, ProductDetails? details, EligibilityCriteria eligibilityCriteria) async {
|
||||||
Log.d("checkAndDistributeOfferDetails $productId $eligibilityCriteria");
|
Log.d("checkAndDistributeOfferDetails $productId $eligibilityCriteria");
|
||||||
switch (eligibilityCriteria) {
|
switch (eligibilityCriteria) {
|
||||||
case EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup:
|
case EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup:
|
||||||
final group = GuruApp.instance.appSpec.productProfile.group(productId);
|
final group = GuruApp.instance.appSpec.productProfile.group(productId);
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
final key = PropertyKeys.buildGroupSubscriptionCount(group);
|
final key = PropertyKeys.buildGroupSubscriptionCount(group);
|
||||||
final count =
|
final count = await AppProperty.getInstance().getInt(key, defValue: 0);
|
||||||
await AppProperty.getInstance().getInt(key, defValue: 0);
|
|
||||||
Log.d(" ==> $key $count");
|
Log.d(" ==> $key $count");
|
||||||
return count > 0 ? null : details;
|
return count > 0 ? null : details;
|
||||||
}
|
}
|
||||||
|
|
@ -1107,8 +1141,8 @@ class IapManager {
|
||||||
Log.d(" ==> $key $count");
|
Log.d(" ==> $key $count");
|
||||||
return count > 0 ? null : details;
|
return count > 0 ? null : details;
|
||||||
case EligibilityCriteria.newCustomerNeverHadAnySubscription:
|
case EligibilityCriteria.newCustomerNeverHadAnySubscription:
|
||||||
final count = await AppProperty.getInstance()
|
final count =
|
||||||
.getInt(PropertyKeys.subscriptionCount, defValue: 0);
|
await AppProperty.getInstance().getInt(PropertyKeys.subscriptionCount, defValue: 0);
|
||||||
Log.d(" ==> subscriptionCount $count");
|
Log.d(" ==> subscriptionCount $count");
|
||||||
return count > 0 ? null : details;
|
return count > 0 ? null : details;
|
||||||
default:
|
default:
|
||||||
|
|
@ -1117,21 +1151,17 @@ class IapManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<IapProduct> _createProduct(
|
Future<IapProduct> _createProduct(TransactionIntent intent, ProductDetails details) async {
|
||||||
TransactionIntent intent, ProductDetails details) async {
|
|
||||||
final productId = intent.productId;
|
final productId = intent.productId;
|
||||||
Manifest manifest = await ManifestManager.instance.createManifest(intent);
|
Manifest manifest = await ManifestManager.instance.createManifest(intent);
|
||||||
Log.d("createProduct ${productId.sku} ${productId.hasBasePlan}");
|
Log.d("createProduct ${productId.sku} ${productId.hasBasePlan}");
|
||||||
ProductDetails baseDetails = details;
|
ProductDetails baseDetails = details;
|
||||||
ProductDetails? offerDetails;
|
ProductDetails? offerDetails;
|
||||||
if (Platform.isAndroid &&
|
if (Platform.isAndroid && productId.isSubscription && productId.hasBasePlan) {
|
||||||
productId.isSubscription &&
|
|
||||||
productId.hasBasePlan) {
|
|
||||||
final googlePlayProductDetails = details as GooglePlayProductDetails;
|
final googlePlayProductDetails = details as GooglePlayProductDetails;
|
||||||
final productDetails = googlePlayProductDetails.productDetails;
|
final productDetails = googlePlayProductDetails.productDetails;
|
||||||
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
||||||
final offerProductDetails =
|
final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails);
|
||||||
GooglePlayProductDetails.fromProductDetails(productDetails);
|
|
||||||
final expectBasePlan = productId.basePlan;
|
final expectBasePlan = productId.basePlan;
|
||||||
final expectOfferId = productId.offerId;
|
final expectOfferId = productId.offerId;
|
||||||
Log.d(
|
Log.d(
|
||||||
|
|
@ -1161,12 +1191,10 @@ class IapManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Product.iap(productId, baseDetails, manifest,
|
return Product.iap(productId, baseDetails, manifest, offerDetails: offerDetails) as IapProduct;
|
||||||
offerDetails: offerDetails) as IapProduct;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ProductStore<IapProduct>> buildProducts(
|
Future<ProductStore<IapProduct>> buildProducts(Set<TransactionIntent> intents) async {
|
||||||
Set<TransactionIntent> intents) async {
|
|
||||||
ProductStore<IapProduct> iapStore = ProductStore();
|
ProductStore<IapProduct> iapStore = ProductStore();
|
||||||
final _productDetails = loadedProductDetails;
|
final _productDetails = loadedProductDetails;
|
||||||
for (var intent in intents) {
|
for (var intent in intents) {
|
||||||
|
|
@ -1180,8 +1208,8 @@ class IapManager {
|
||||||
final product = await _createProduct(intent, details);
|
final product = await _createProduct(intent, details);
|
||||||
iapStore.putProduct(product);
|
iapStore.putProduct(product);
|
||||||
if (intent.productId.hasOffer && !iapStore.existsProduct(productId)) {
|
if (intent.productId.hasOffer && !iapStore.existsProduct(productId)) {
|
||||||
final originProduct = await _createProduct(
|
final originProduct =
|
||||||
productId.createIntent(scene: intent.scene), details);
|
await _createProduct(productId.createIntent(scene: intent.scene), details);
|
||||||
iapStore.putProduct(originProduct);
|
iapStore.putProduct(originProduct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1222,8 +1250,7 @@ class IapManager {
|
||||||
result = await _inAppPurchase.buyNonConsumable(purchaseParam: param);
|
result = await _inAppPurchase.buyNonConsumable(purchaseParam: param);
|
||||||
}
|
}
|
||||||
if (!result) {
|
if (!result) {
|
||||||
Log.d(
|
Log.d("_requestPurchases error! ${product.productId} ${product.details.price}",
|
||||||
"_requestPurchases error! ${product.productId} ${product.details.price}",
|
|
||||||
syncFirebase: true);
|
syncFirebase: true);
|
||||||
GuruAnalytics.instance.logGuruEvent("dev_iap_action", {
|
GuruAnalytics.instance.logGuruEvent("dev_iap_action", {
|
||||||
"item_category": "request",
|
"item_category": "request",
|
||||||
|
|
@ -1252,13 +1279,12 @@ class IapManager {
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase
|
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
||||||
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
_inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||||
final response = await androidAddition.queryPastPurchases();
|
final response = await androidAddition.queryPastPurchases();
|
||||||
for (var purchase in response.pastPurchases) {
|
for (var purchase in response.pastPurchases) {
|
||||||
androidAddition.consumePurchase(purchase);
|
androidAddition.consumePurchase(purchase);
|
||||||
Log.w(
|
Log.w("[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}");
|
||||||
"[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}");
|
|
||||||
_inAppPurchase.completePurchase(purchase);
|
_inAppPurchase.completePurchase(purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1270,30 +1296,26 @@ class IapManager {
|
||||||
Future manualConsumePurchase(PurchaseDetails purchase) async {
|
Future manualConsumePurchase(PurchaseDetails purchase) async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
||||||
_inAppPurchase
|
_inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||||
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
|
||||||
await androidAddition.consumePurchase(purchase);
|
await androidAddition.consumePurchase(purchase);
|
||||||
_inAppPurchase.completePurchase(purchase);
|
_inAppPurchase.completePurchase(purchase);
|
||||||
await GuruDB.instance.deletePendingOrderBySku(sku: purchase.productID);
|
await GuruDB.instance.deletePendingOrderBySku(sku: purchase.productID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future manualConsumeAllPurchases(
|
Future manualConsumeAllPurchases(List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
||||||
List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
|
||||||
for (var tuple in tuples) {
|
for (var tuple in tuples) {
|
||||||
try {
|
try {
|
||||||
final productId = tuple.item1;
|
final productId = tuple.item1;
|
||||||
final purchase = tuple.item2;
|
final purchase = tuple.item2;
|
||||||
await manualConsumePurchase(purchase);
|
await manualConsumePurchase(purchase);
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Log.w("consumePurchase error! $error",
|
Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true);
|
||||||
stackTrace: stacktrace, syncFirebase: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future completeAllPurchases(
|
Future completeAllPurchases(List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
||||||
List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
|
||||||
for (var tuple in tuples) {
|
for (var tuple in tuples) {
|
||||||
try {
|
try {
|
||||||
final productId = tuple.item1;
|
final productId = tuple.item1;
|
||||||
|
|
@ -1306,8 +1328,7 @@ class IapManager {
|
||||||
"item_name": productId.sku,
|
"item_name": productId.sku,
|
||||||
"result": "true",
|
"result": "true",
|
||||||
});
|
});
|
||||||
final order =
|
final order = await _completePurchase(productId, productDetails, details);
|
||||||
await _completePurchase(productId, productDetails, details);
|
|
||||||
} else {
|
} else {
|
||||||
GuruAnalytics.instance.logGuruEvent("dev_iap_action", {
|
GuruAnalytics.instance.logGuruEvent("dev_iap_action", {
|
||||||
"item_category": "pending_consume",
|
"item_category": "pending_consume",
|
||||||
|
|
@ -1325,8 +1346,7 @@ class IapManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Log.w("consumePurchase error! $error",
|
Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true);
|
||||||
stackTrace: stacktrace, syncFirebase: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1339,13 +1359,10 @@ class IapManager {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Map<String, ProductId> _filterProductSkus(
|
Map<String, ProductId> _filterProductSkus(
|
||||||
{required Set<ProductId> ids,
|
{required Set<ProductId> ids, required Set<int> attrs, Set<ProductId>? validIds}) {
|
||||||
required Set<int> attrs,
|
|
||||||
Set<ProductId>? validIds}) {
|
|
||||||
final List<MapEntry<String, ProductId>> entries = ids
|
final List<MapEntry<String, ProductId>> entries = ids
|
||||||
.where((productId) =>
|
.where((productId) =>
|
||||||
(validIds?.contains(productId) != false) &&
|
(validIds?.contains(productId) != false) && attrs.contains(productId.attr))
|
||||||
attrs.contains(productId.attr))
|
|
||||||
.map((productId) => MapEntry(productId.sku, productId))
|
.map((productId) => MapEntry(productId.sku, productId))
|
||||||
.toList();
|
.toList();
|
||||||
return Map.fromEntries(entries);
|
return Map.fromEntries(entries);
|
||||||
|
|
@ -1393,16 +1410,13 @@ class IapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
final queryProductIds = queryOneOffChargeSkuMap.keys.toSet();
|
final queryProductIds = queryOneOffChargeSkuMap.keys.toSet();
|
||||||
queryProductIds.addAll(
|
queryProductIds.addAll(GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku));
|
||||||
GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku));
|
|
||||||
Log.d("refresh product:", tag: "IAP");
|
Log.d("refresh product:", tag: "IAP");
|
||||||
for (var productId in queryProductIds) {
|
for (var productId in queryProductIds) {
|
||||||
Log.d(" => $productId", tag: "IAP");
|
Log.d(" => $productId", tag: "IAP");
|
||||||
}
|
}
|
||||||
final response =
|
final response = await _queryProducts(queryProductIds).catchError((error, stacktrace) {
|
||||||
await _queryProducts(queryProductIds).catchError((error, stacktrace) {
|
Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace", tag: "IAP");
|
||||||
Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace",
|
|
||||||
tag: "IAP");
|
|
||||||
return _emptyResponse;
|
return _emptyResponse;
|
||||||
});
|
});
|
||||||
Log.i("refreshProduct COMPLETED:", tag: "IAP");
|
Log.i("refreshProduct COMPLETED:", tag: "IAP");
|
||||||
|
|
@ -1419,8 +1433,8 @@ class IapManager {
|
||||||
detailsMap.addAll(extractProducts(details));
|
detailsMap.addAll(extractProducts(details));
|
||||||
}
|
}
|
||||||
|
|
||||||
GuruAnalytics.instance.logGuruEvent(
|
GuruAnalytics.instance
|
||||||
"dev_iap_action", {"item_category": "load", "result": "true"});
|
.logGuruEvent("dev_iap_action", {"item_category": "load", "result": "true"});
|
||||||
final newProductDetails = Map.of(loadedProductDetails);
|
final newProductDetails = Map.of(loadedProductDetails);
|
||||||
newProductDetails.addAll(detailsMap);
|
newProductDetails.addAll(detailsMap);
|
||||||
_productDetailsSubject.addEx(newProductDetails);
|
_productDetailsSubject.addEx(newProductDetails);
|
||||||
|
|
@ -1440,8 +1454,7 @@ class IapManager {
|
||||||
final googlePlayProductDetails = details as GooglePlayProductDetails;
|
final googlePlayProductDetails = details as GooglePlayProductDetails;
|
||||||
final productDetails = googlePlayProductDetails.productDetails;
|
final productDetails = googlePlayProductDetails.productDetails;
|
||||||
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
||||||
final offerProductDetails =
|
final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails);
|
||||||
GooglePlayProductDetails.fromProductDetails(productDetails);
|
|
||||||
for (var id in ids) {
|
for (var id in ids) {
|
||||||
final expectBasePlan = id.basePlan;
|
final expectBasePlan = id.basePlan;
|
||||||
final expectOfferId = id.offerId;
|
final expectOfferId = id.offerId;
|
||||||
|
|
@ -1452,8 +1465,7 @@ class IapManager {
|
||||||
final offer = subscriptionOfferDetails[i];
|
final offer = subscriptionOfferDetails[i];
|
||||||
Log.d(
|
Log.d(
|
||||||
"$i expectOfferId:$expectOfferId offerId:${offer.offerId} expectBasePlan:$expectBasePlan basePlanId:${offer.basePlanId}");
|
"$i expectOfferId:$expectOfferId offerId:${offer.offerId} expectBasePlan:$expectBasePlan basePlanId:${offer.basePlanId}");
|
||||||
if (expectBasePlan != offer.basePlanId ||
|
if (expectBasePlan != offer.basePlanId || expectOfferId != offer.offerId) {
|
||||||
expectOfferId != offer.offerId) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
detailsMap[id] = offerProductDetails[i];
|
detailsMap[id] = offerProductDetails[i];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:guru_app/analytics/guru_analytics.dart';
|
||||||
|
import 'package:guru_app/database/guru_db.dart';
|
||||||
|
import 'package:guru_app/financial/asset/assets_model.dart';
|
||||||
|
import 'package:guru_app/financial/asset/assets_store.dart';
|
||||||
|
import 'package:guru_app/financial/data/db/order_database.dart';
|
||||||
|
import 'package:guru_app/financial/igb/igb_product.dart';
|
||||||
|
import 'package:guru_app/financial/igc/igc_model.dart';
|
||||||
|
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
||||||
|
import 'package:guru_app/financial/product/product_model.dart';
|
||||||
|
import 'package:guru_app/guru_app.dart';
|
||||||
|
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||||
|
import 'package:guru_app/property/app_property.dart';
|
||||||
|
import 'package:guru_utils/extensions/extensions.dart';
|
||||||
|
import 'package:guru_utils/log/log.dart';
|
||||||
|
import 'package:guru_utils/property/app_property.dart';
|
||||||
|
|
||||||
|
/// Created by Haoyi on 2023/2/18
|
||||||
|
|
||||||
|
class IgbManager {
|
||||||
|
static final IgbManager _instance = IgbManager._();
|
||||||
|
|
||||||
|
static IgbManager get instance => _instance;
|
||||||
|
|
||||||
|
IgbManager._();
|
||||||
|
|
||||||
|
Future init() async {
|
||||||
|
await InventoryManager.instance.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future reloadAssets() async {}
|
||||||
|
|
||||||
|
Future<bool> accumulate(int igc, TransactionMethod method, {String? scene}) async {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future clear() async {}
|
||||||
|
|
||||||
|
Future<IgbProduct> buildIgbProduct(TransactionIntent intent) async {
|
||||||
|
final manifest = await ManifestManager.instance.createManifest(intent);
|
||||||
|
return IgbProduct(intent.productId, manifest, intent.igbCost);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> redeem(IgbProduct product) async {
|
||||||
|
Log.v("Igb buy");
|
||||||
|
final result = await InventoryManager.instance.consume(product.cost, product.manifest);
|
||||||
|
if (result) {
|
||||||
|
await ManifestManager.instance.deliver(product.manifest, TransactionMethod.igb);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import 'package:guru_app/financial/data/db/order_database.dart';
|
||||||
|
import 'package:guru_app/financial/product/product_model.dart';
|
||||||
|
import 'package:guru_app/inventory/db/inventory_database.dart';
|
||||||
|
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||||
|
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||||
|
import 'package:guru_utils/id/id_utils.dart';
|
||||||
|
import 'package:guru_utils/manifest/manifest.dart';
|
||||||
|
|
||||||
|
// In-game barter Product
|
||||||
|
class IgbProduct implements Product {
|
||||||
|
@override
|
||||||
|
final ProductId productId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Manifest manifest;
|
||||||
|
|
||||||
|
final List<StockItem> cost;
|
||||||
|
|
||||||
|
String get sku => productId.sku;
|
||||||
|
|
||||||
|
IgbProduct(this.productId, this.manifest, this.cost);
|
||||||
|
|
||||||
|
bool isConsumable() {
|
||||||
|
return productId.isConsumable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'StockProduct{productId: $productId}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
OrderEntity createOrder() {
|
||||||
|
return OrderEntity(
|
||||||
|
orderId: IdUtils.uuidV4(),
|
||||||
|
sku: productId.sku,
|
||||||
|
state: TransactionState.success,
|
||||||
|
attr: productId.attr,
|
||||||
|
method: TransactionMethod.igc.index,
|
||||||
|
currency: TransactionCurrency.igc,
|
||||||
|
cost: 1,
|
||||||
|
category: manifest.category,
|
||||||
|
timestamp: DateTimeUtils.currentTimeInMillis(),
|
||||||
|
manifest: manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,10 @@ class IgcManager {
|
||||||
await reloadAssets();
|
await reloadAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future switchSession() async {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
Future reloadAssets() async {
|
Future reloadAssets() async {
|
||||||
final orders = await GuruDB.instance
|
final orders = await GuruDB.instance
|
||||||
.selectOrders(method: TransactionMethod.igc, attrs: [TransactionAttributes.asset]);
|
.selectOrders(method: TransactionMethod.igc, attrs: [TransactionAttributes.asset]);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,15 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:guru_app/financial/igc/igc_manager.dart';
|
import 'package:guru_app/financial/igc/igc_manager.dart';
|
||||||
import 'package:guru_app/financial/product/product_model.dart';
|
import 'package:guru_app/financial/product/product_model.dart';
|
||||||
|
import 'package:guru_app/inventory/inventory_manager.dart';
|
||||||
import 'manifest.dart';
|
import 'manifest.dart';
|
||||||
|
|
||||||
/// Created by Haoyi on 2022/8/21
|
/// 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);
|
typedef ManifestBuilder = Future<Manifest?> Function(TransactionIntent);
|
||||||
|
|
||||||
|
|
@ -27,15 +31,31 @@ class ManifestManager {
|
||||||
|
|
||||||
final List<ManifestBuilder> builders = [];
|
final List<ManifestBuilder> builders = [];
|
||||||
|
|
||||||
static Future<bool> _deliverIgcDetails(
|
static Future<List<StockItem>> _deliverIgcDetails(
|
||||||
Details details, TransactionMethod method, String scene) async {
|
Details details, TransactionMethod method, String scene) async {
|
||||||
if (details.amount > 0) {
|
if (details.amount > 0) {
|
||||||
IgcManager.instance.accumulate(details.amount, method, scene: scene);
|
await IgcManager.instance.accumulate(details.amount, method, scene: scene);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
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) {
|
void addDistributor(String type, DetailsDistributor distributor) {
|
||||||
distributors[type] = distributor;
|
distributors[type] = distributor;
|
||||||
}
|
}
|
||||||
|
|
@ -48,11 +68,55 @@ class ManifestManager {
|
||||||
this.builders.addAll(builders);
|
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 {
|
Future<bool> deliver(Manifest manifest, TransactionMethod method) async {
|
||||||
bool result = false;
|
bool result = false;
|
||||||
|
final List<StockItem> unsold = [];
|
||||||
for (var details in manifest.details) {
|
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);
|
deliveredManifestStream.add(manifest);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -74,4 +138,12 @@ class ManifestManager {
|
||||||
final extras = <String, dynamic>{ExtraReservedField.scene: scene};
|
final extras = <String, dynamic>{ExtraReservedField.scene: scene};
|
||||||
return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details);
|
return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manifest createIgbManifest(int igc, {String? category, String scene = ""}) {
|
||||||
|
// final details = <Details>[];
|
||||||
|
// details.add(Details.define(DetailsReservedType.igc, igc));
|
||||||
|
//
|
||||||
|
// final extras = <String, dynamic>{ExtraReservedField.scene: scene};
|
||||||
|
// return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:guru_app/financial/data/db/order_database.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/manifest/manifest.dart';
|
import 'package:guru_app/financial/manifest/manifest.dart';
|
||||||
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
import 'package:guru_app/financial/manifest/manifest_manager.dart';
|
||||||
import 'package:guru_app/guru_app.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:guru_utils/hash/hash.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'package:guru_app/financial/iap/iap_model.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
|
// Offer products for sale in your app for a one-off charge
|
||||||
@deprecated
|
@deprecated
|
||||||
static const possessive = 1;
|
static const possessive = DetailsAttr.permanent;
|
||||||
static const asset = 1;
|
static const asset = DetailsAttr.permanent;
|
||||||
static const consumable = 2;
|
static const consumable = DetailsAttr.consumable;
|
||||||
|
|
||||||
static const Set<int> oneOffChargeAttributes = <int>{asset, consumable};
|
static const Set<int> oneOffChargeAttributes = <int>{asset, consumable};
|
||||||
|
|
||||||
|
|
@ -174,6 +176,7 @@ class ProductId {
|
||||||
TransactionIntent createIntent(
|
TransactionIntent createIntent(
|
||||||
{required String scene,
|
{required String scene,
|
||||||
int igcCost = 0,
|
int igcCost = 0,
|
||||||
|
List<StockItem> igbCost = const <StockItem>[],
|
||||||
bool sales = false,
|
bool sales = false,
|
||||||
double rate = 1.0,
|
double rate = 1.0,
|
||||||
EligibilityCriteria eligibilityCriteria =
|
EligibilityCriteria eligibilityCriteria =
|
||||||
|
|
@ -193,6 +196,13 @@ class ProductId {
|
||||||
final manifest = await ManifestManager.instance.createManifest(intent);
|
final manifest = await ManifestManager.instance.createManifest(intent);
|
||||||
return IgcProduct(this, manifest, igcCost);
|
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 {
|
abstract class Product {
|
||||||
|
|
@ -207,12 +217,14 @@ abstract class Product {
|
||||||
|
|
||||||
factory Product.reward(ProductId productId, Manifest manifest) = RewardProduct;
|
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.gems(ProductId productId, int price, Manifest manifest) = GemProduct;
|
||||||
//
|
//
|
||||||
// factory Product.reward(Reward reward) = RewardProduct;
|
// factory Product.reward(Reward reward) = RewardProduct;
|
||||||
//
|
//
|
||||||
OrderEntity createOrder();
|
// OrderEntity createOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransactionState {
|
class TransactionState {
|
||||||
|
|
@ -223,11 +235,17 @@ class TransactionState {
|
||||||
static const expired = -3;
|
static const expired = -3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 交易方式
|
||||||
enum TransactionMethod {
|
enum TransactionMethod {
|
||||||
iap, // IAP购买
|
iap, // IAP购买
|
||||||
igc, // In-game currency 购买
|
igc, // In-game currency 购买(coin/gems..)
|
||||||
reward, // 奖励获得
|
reward, // 奖励获得
|
||||||
none
|
|
||||||
|
bonus, // 优惠
|
||||||
|
igb, // In-game barter
|
||||||
|
free,
|
||||||
|
migrate,
|
||||||
|
unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
String convertTransactionMethodName(TransactionMethod method) {
|
String convertTransactionMethodName(TransactionMethod method) {
|
||||||
|
|
@ -238,14 +256,23 @@ String convertTransactionMethodName(TransactionMethod method) {
|
||||||
return "igc";
|
return "igc";
|
||||||
case TransactionMethod.reward:
|
case TransactionMethod.reward:
|
||||||
return "reward";
|
return "reward";
|
||||||
|
case TransactionMethod.bonus:
|
||||||
|
return "bonus";
|
||||||
|
case TransactionMethod.igb:
|
||||||
|
return "igb";
|
||||||
|
case TransactionMethod.free:
|
||||||
|
return "prop";
|
||||||
|
case TransactionMethod.migrate:
|
||||||
|
return "migrate";
|
||||||
default:
|
default:
|
||||||
return "none";
|
return "unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransactionIntent {
|
class TransactionIntent {
|
||||||
final ProductId productId;
|
final ProductId productId;
|
||||||
final int igcCost;
|
final int igcCost; // In-game currency cost
|
||||||
|
final List<StockItem> igbCost; // In-game barter cost
|
||||||
final String scene; // 购买场景(最终会用到logEarnVirtualCurrency的item_category上)
|
final String scene; // 购买场景(最终会用到logEarnVirtualCurrency的item_category上)
|
||||||
final bool sales; // 是否为促销商品
|
final bool sales; // 是否为促销商品
|
||||||
final double rate; // 默认1.0 指收益率,如:打折促销时设成1.2,最终的收益将是1.2倍
|
final double rate; // 默认1.0 指收益率,如:打折促销时设成1.2,最终的收益将是1.2倍
|
||||||
|
|
@ -253,6 +280,7 @@ class TransactionIntent {
|
||||||
|
|
||||||
TransactionIntent(this.productId, this.scene,
|
TransactionIntent(this.productId, this.scene,
|
||||||
{this.igcCost = 0,
|
{this.igcCost = 0,
|
||||||
|
this.igbCost = const <StockItem>[],
|
||||||
this.sales = false,
|
this.sales = false,
|
||||||
this.rate = 1.0,
|
this.rate = 1.0,
|
||||||
this.eligibilityCriteria = EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup});
|
this.eligibilityCriteria = EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup});
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class ProductProfile {
|
||||||
final Set<ProductId> iapIds = {};
|
final Set<ProductId> iapIds = {};
|
||||||
final Set<ProductId> igcIds = {};
|
final Set<ProductId> igcIds = {};
|
||||||
final Set<ProductId> rewardIds = {};
|
final Set<ProductId> rewardIds = {};
|
||||||
|
final Set<ProductId> igbIds = {};
|
||||||
|
|
||||||
final Map<String, String> groupMap;
|
final Map<String, String> groupMap;
|
||||||
|
|
||||||
|
|
@ -19,16 +20,17 @@ class ProductProfile {
|
||||||
final Map<String, Set<ProductId>> _offerIds = {};
|
final Map<String, Set<ProductId>> _offerIds = {};
|
||||||
|
|
||||||
final List<Map<String, ProductId>> _idsMap =
|
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,
|
ProductProfile(
|
||||||
required Set<ProductId> subscriptionsIapIds,
|
{required Set<ProductId> oneOffChargeIapIds,
|
||||||
Set<ProductId> pointsIapIds = const <ProductId>{},
|
required Set<ProductId> subscriptionsIapIds,
|
||||||
Set<ProductId> igcIds = const <ProductId>{},
|
Set<ProductId> pointsIapIds = const <ProductId>{},
|
||||||
Set<ProductId> rewardIds = const <ProductId>{},
|
Set<ProductId> igcIds = const <ProductId>{},
|
||||||
this.groupMap = const <String, String>{},
|
Set<ProductId> rewardIds = const <ProductId>{},
|
||||||
List<ManifestBuilder> manifestBuilders = const <ManifestBuilder>[],
|
this.groupMap = const <String, String>{},
|
||||||
this.noAdsCapIds = const <ProductId>{}}) {
|
List<ManifestBuilder> manifestBuilders = const <ManifestBuilder>[],
|
||||||
|
this.noAdsCapIds = const <ProductId>{}}) {
|
||||||
for (var productId in oneOffChargeIapIds) {
|
for (var productId in oneOffChargeIapIds) {
|
||||||
_define(productId, TransactionMethod.iap);
|
_define(productId, TransactionMethod.iap);
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +72,16 @@ class ProductProfile {
|
||||||
case TransactionMethod.reward:
|
case TransactionMethod.reward:
|
||||||
rewardIds.add(definedProductId);
|
rewardIds.add(definedProductId);
|
||||||
break;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
_idsMap[productId.attr][productId.sku] = definedProductId;
|
_idsMap[productId.attr][productId.sku] = definedProductId;
|
||||||
|
|
@ -114,11 +125,12 @@ class IapProfile {
|
||||||
final List<ProductId> subscriptionsIapIds = [];
|
final List<ProductId> subscriptionsIapIds = [];
|
||||||
final List<ProductId> noAdsCapIds;
|
final List<ProductId> noAdsCapIds;
|
||||||
final List<Map<String, ProductId>> _idsMap =
|
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,
|
IapProfile(
|
||||||
required List<ProductId> subscriptionsIapIds,
|
{required List<ProductId> oneOffChargeIapIds,
|
||||||
this.noAdsCapIds = const <ProductId>[]}) {
|
required List<ProductId> subscriptionsIapIds,
|
||||||
|
this.noAdsCapIds = const <ProductId>[]}) {
|
||||||
for (var productId in oneOffChargeIapIds) {
|
for (var productId in oneOffChargeIapIds) {
|
||||||
_define(productId);
|
_define(productId);
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +142,7 @@ class IapProfile {
|
||||||
bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty;
|
bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty;
|
||||||
|
|
||||||
static final IapProfile invalid =
|
static final IapProfile invalid =
|
||||||
IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []);
|
IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []);
|
||||||
|
|
||||||
ProductId _define(ProductId productId) {
|
ProductId _define(ProductId productId) {
|
||||||
if (productId.isOneOffCharge) {
|
if (productId.isOneOffCharge) {
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,13 @@ class RewardManager {
|
||||||
await reloadAssets();
|
await reloadAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future switchSession() async {
|
||||||
|
reloadAssets();
|
||||||
|
}
|
||||||
|
|
||||||
Future reloadAssets() async {
|
Future reloadAssets() async {
|
||||||
final transactions = await GuruDB.instance.selectOrders(
|
final transactions = await GuruDB.instance
|
||||||
method: TransactionMethod.reward, attrs: [TransactionAttributes.asset]);
|
.selectOrders(method: TransactionMethod.reward, attrs: [TransactionAttributes.asset]);
|
||||||
final newAssetsStore = AssetsStore<Asset>();
|
final newAssetsStore = AssetsStore<Asset>();
|
||||||
for (var transaction in transactions) {
|
for (var transaction in transactions) {
|
||||||
final productId = transaction.productId;
|
final productId = transaction.productId;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||||
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||||
import 'package:guru_app/ads/core/ads_config.dart';
|
import 'package:guru_app/ads/core/ads_config.dart';
|
||||||
import 'package:guru_app/analytics/data/analytics_model.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/firebase/remoteconfig/reserved_remote_config_models.dart';
|
||||||
import 'package:guru_app/guru_app.dart';
|
import 'package:guru_app/guru_app.dart';
|
||||||
import 'package:guru_utils/http/http_model.dart';
|
import 'package:guru_utils/http/http_model.dart';
|
||||||
|
|
@ -18,8 +19,7 @@ part "remote_config_interface.dart";
|
||||||
part "remote_config_reserved_constants.dart";
|
part "remote_config_reserved_constants.dart";
|
||||||
|
|
||||||
class RemoteConfigManager extends IRemoteConfig {
|
class RemoteConfigManager extends IRemoteConfig {
|
||||||
final BehaviorSubject<FirebaseRemoteConfig?> _subject =
|
final BehaviorSubject<FirebaseRemoteConfigWrapper?> _subject = BehaviorSubject.seeded(null);
|
||||||
BehaviorSubject.seeded(null);
|
|
||||||
static RemoteConfigManager? _instance;
|
static RemoteConfigManager? _instance;
|
||||||
|
|
||||||
static RemoteConfigManager _getInstance() {
|
static RemoteConfigManager _getInstance() {
|
||||||
|
|
@ -31,7 +31,7 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
|
|
||||||
static RemoteConfigManager get instance => _getInstance();
|
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();
|
RemoteConfigManager._internal();
|
||||||
|
|
||||||
|
|
@ -42,17 +42,16 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
minimumFetchInterval: const Duration(hours: 2),
|
minimumFetchInterval: const Duration(hours: 2),
|
||||||
));
|
));
|
||||||
|
|
||||||
_subject.add(remoteConfig);
|
_subject.add(FirebaseRemoteConfigWrapper._(remoteConfig));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await remoteConfig.setDefaults(defaultConfigs);
|
await remoteConfig.setDefaults(defaultConfigs);
|
||||||
await remoteConfig.activate();
|
await remoteConfig.activate();
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
Log.d(
|
Log.d("Unable to fetch remote config. Cached or default values will be used!",
|
||||||
"Unable to fetch remote config. Cached or default values will be used!",
|
|
||||||
error: exception);
|
error: exception);
|
||||||
} finally {
|
} finally {
|
||||||
_subject.add(remoteConfig);
|
_subject.add(FirebaseRemoteConfigWrapper._(remoteConfig));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,11 +60,10 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
try {
|
try {
|
||||||
await remoteConfig.fetchAndActivate();
|
await remoteConfig.fetchAndActivate();
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
Log.d(
|
Log.d("Unable to fetch remote config. Cached or default values will be used!",
|
||||||
"Unable to fetch remote config. Cached or default values will be used!",
|
|
||||||
error: exception);
|
error: exception);
|
||||||
} finally {
|
} finally {
|
||||||
_subject.add(remoteConfig);
|
_subject.add(FirebaseRemoteConfigWrapper._(remoteConfig));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,12 +93,10 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
final data = config.getAll();
|
final data = config.getAll();
|
||||||
final result = {
|
final result = {
|
||||||
for (var entry in data.entries)
|
for (var entry in data.entries)
|
||||||
"${entry.key} [${valueSourceToString(entry.value.source)}]":
|
"${entry.key} [${valueSourceToString(entry.value.source)}]": entry.value.asString()
|
||||||
entry.value.asString()
|
|
||||||
};
|
};
|
||||||
result["last_fetch_remote_config_time"] = config.lastFetchTime.toString();
|
result["last_fetch_remote_config_time"] = config.lastFetchTime.toString();
|
||||||
result["last_fetch_remote_config_status"] =
|
result["last_fetch_remote_config_status"] = config.lastFetchStatus.toString();
|
||||||
config.lastFetchStatus.toString();
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,14 +109,13 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
));
|
));
|
||||||
await remoteConfig.fetchAndActivate();
|
await remoteConfig.fetchAndActivate();
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
Log.d(
|
Log.d("Unable to fetch remote config. Cached or default values will be used $exception",
|
||||||
"Unable to fetch remote config. Cached or default values will be used $exception",
|
|
||||||
error: exception);
|
error: exception);
|
||||||
if (debug) {
|
if (debug) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
_subject.add(remoteConfig);
|
_subject.add(FirebaseRemoteConfigWrapper._(remoteConfig));
|
||||||
await remoteConfig.setConfigSettings(RemoteConfigSettings(
|
await remoteConfig.setConfigSettings(RemoteConfigSettings(
|
||||||
fetchTimeout: const Duration(seconds: 15),
|
fetchTimeout: const Duration(seconds: 15),
|
||||||
minimumFetchInterval: const Duration(hours: 2),
|
minimumFetchInterval: const Duration(hours: 2),
|
||||||
|
|
@ -143,7 +138,7 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
for (var jsonEntry in jsonValue.entries) {
|
for (var jsonEntry in jsonValue.entries) {
|
||||||
if (jsonEntry.key.contains("guru_ab_")) {
|
if (jsonEntry.key.contains("guru_ab_")) {
|
||||||
String abName = jsonEntry.key.replaceFirst("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");
|
Log.w("abName($abName) length is invalid! $abName");
|
||||||
invalidABKeys.add(abName);
|
invalidABKeys.add(abName);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -151,7 +146,7 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
invalidABKeys.add(abName);
|
invalidABKeys.add(abName);
|
||||||
abName = abName.substring(0, 20);
|
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}");
|
Log.i("abName:ab_$abName value:${jsonEntry.value}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,15 +159,14 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (invalidABKeys.isNotEmpty) {
|
if (invalidABKeys.isNotEmpty) {
|
||||||
GuruAnalytics.instance.logException(
|
GuruAnalytics.instance
|
||||||
InvalidABPropertyKeysException(invalidABKeys, cause: cause));
|
.logException(InvalidABPropertyKeysException(invalidABKeys, cause: cause));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool? getBool(String name, {bool? defaultValue}) =>
|
bool? getBool(String name, {bool? defaultValue}) => _subject.value?.getBool(name) ?? defaultValue;
|
||||||
_subject.value?.getBool(name) ?? defaultValue;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? getString(String name, {String? defaultValue}) =>
|
String? getString(String name, {String? defaultValue}) =>
|
||||||
|
|
@ -185,11 +179,10 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
_subject.value?.getDouble(name) ?? defaultValue;
|
_subject.value?.getDouble(name) ?? defaultValue;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int? getInt(String name, {int? defaultValue}) =>
|
int? getInt(String name, {int? defaultValue}) => _subject.value?.getInt(name) ?? defaultValue;
|
||||||
_subject.value?.getInt(name) ?? defaultValue;
|
|
||||||
|
|
||||||
Stream<FirebaseRemoteConfig> observeConfig() =>
|
Stream<FirebaseRemoteConfigWrapper> observeConfig() => _subject.stream
|
||||||
_subject.stream.map((config) => config ?? FirebaseRemoteConfig.instance);
|
.map((config) => config ?? FirebaseRemoteConfigWrapper._(FirebaseRemoteConfig.instance));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<bool?> observeBool(String name, {bool? defaultValue}) =>
|
Stream<bool?> observeBool(String name, {bool? defaultValue}) =>
|
||||||
|
|
@ -207,3 +200,23 @@ class RemoteConfigManager extends IRemoteConfig {
|
||||||
Stream<int?> observeInt(String name, {int? defaultValue}) =>
|
Stream<int?> observeInt(String name, {int? defaultValue}) =>
|
||||||
observeConfig().map((config) => config.getInt(name));
|
observeConfig().map((config) => config.getInt(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FirebaseRemoteConfigWrapper {
|
||||||
|
final FirebaseRemoteConfig config;
|
||||||
|
|
||||||
|
const FirebaseRemoteConfigWrapper._(this.config);
|
||||||
|
|
||||||
|
DateTime get lastFetchTime => config.lastFetchTime;
|
||||||
|
|
||||||
|
RemoteConfigFetchStatus get lastFetchStatus => config.lastFetchStatus;
|
||||||
|
|
||||||
|
static String _key(String key) => GuruApp.instance.getRemoteConfigKey(key);
|
||||||
|
|
||||||
|
String getString(String key) => config.getString(_key(key));
|
||||||
|
|
||||||
|
bool getBool(String key) => config.getBool(_key(key));
|
||||||
|
|
||||||
|
double getDouble(String key) => config.getDouble(_key(key));
|
||||||
|
|
||||||
|
int getInt(String key) => config.getInt(_key(key));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,6 @@ extension RemoteConfigReservedConstants on RemoteConfigManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
static String? getDefaultConfigString(String key) {
|
static String? getDefaultConfigString(String key) {
|
||||||
return GuruApp.instance.defaultRemoteConfig[key];
|
return GuruApp.instance.defaultRemoteConfig[GuruApp.instance.getRemoteConfigKey(key)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:adjust_sdk/adjust_event.dart';
|
import 'package:adjust_sdk/adjust_event.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.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:flutter/material.dart';
|
||||||
import 'package:guru_app/account/account_data_store.dart';
|
import 'package:guru_app/account/account_data_store.dart';
|
||||||
import 'package:guru_app/account/account_manager.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/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/analytics/guru_analytics.dart';
|
||||||
import 'package:guru_app/app/app_models.dart';
|
import 'package:guru_app/app/app_models.dart';
|
||||||
import 'package:guru_app/database/guru_db.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/product/product_model.dart';
|
||||||
import 'package:guru_app/financial/reward/reward_manager.dart';
|
import 'package:guru_app/financial/reward/reward_manager.dart';
|
||||||
import 'package:guru_app/firebase/dxlinks/dxlink_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_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/collection/collectionutils.dart';
|
||||||
import 'package:guru_utils/controller/aware/ads/overlay/ads_overlay.dart';
|
import 'package:guru_utils/controller/aware/ads/overlay/ads_overlay.dart';
|
||||||
import 'package:guru_utils/datetime/datetime_utils.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/lifecycle/lifecycle_manager.dart';
|
||||||
import 'package:guru_utils/network/network_utils.dart';
|
import 'package:guru_utils/network/network_utils.dart';
|
||||||
import 'package:guru_utils/property/app_property.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/packages/guru_package.dart';
|
||||||
import 'package:guru_utils/ads/ads.dart';
|
import 'package:guru_utils/ads/ads.dart';
|
||||||
import 'package:guru_utils/guru_utils.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:logger/logger.dart' as Logger;
|
||||||
import 'package:guru_utils/aigc/bi/ai_bi.dart';
|
import 'package:guru_utils/aigc/bi/ai_bi.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:guru_popup/guru_popup.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:firebase_core/firebase_core.dart';
|
||||||
export 'package:guru_app/app/app_models.dart';
|
export 'package:guru_app/app/app_models.dart';
|
||||||
export 'package:guru_utils/log/log.dart';
|
export 'package:guru_utils/log/log.dart';
|
||||||
|
|
@ -49,13 +60,15 @@ export 'dart:io';
|
||||||
export 'dart:math';
|
export 'dart:math';
|
||||||
export 'package:guru_app/financial/manifest/manifest.dart';
|
export 'package:guru_app/financial/manifest/manifest.dart';
|
||||||
export 'package:guru_app/firebase/messaging/remote_messaging_manager.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
|
/// Created by Haoyi on 2022/8/25
|
||||||
|
|
||||||
abstract class AppSpec {
|
abstract class AppSpec {
|
||||||
String get appName;
|
String get appName;
|
||||||
|
|
||||||
|
AppCategory get appCategory;
|
||||||
|
|
||||||
String get flavor;
|
String get flavor;
|
||||||
|
|
||||||
AppDetails get details;
|
AppDetails get details;
|
||||||
|
|
@ -69,6 +82,10 @@ abstract class AppSpec {
|
||||||
Deployment get deployment;
|
Deployment get deployment;
|
||||||
|
|
||||||
Map<String, dynamic> get defaultRemoteConfig;
|
Map<String, dynamic> get defaultRemoteConfig;
|
||||||
|
|
||||||
|
Map<String, ABTestExperiment> get localABTestExperiments;
|
||||||
|
|
||||||
|
String getRemoteConfigKey(String key);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotImplementationAppSpecCreatorException implements Exception {
|
class NotImplementationAppSpecCreatorException implements Exception {
|
||||||
|
|
@ -83,14 +100,12 @@ class NotImplementationAppSpecCreatorException implements Exception {
|
||||||
class AppEnv {
|
class AppEnv {
|
||||||
final AppSpec spec;
|
final AppSpec spec;
|
||||||
final RootPackage package;
|
final RootPackage package;
|
||||||
final BackgroundMessageHandler? backgroundMessageHandler;
|
final IGuruSdkProtocol protocol;
|
||||||
final ToastDelegate? toastDelegate;
|
|
||||||
|
|
||||||
AppEnv(
|
AppEnv(
|
||||||
{required this.spec,
|
{required this.spec,
|
||||||
required this.package,
|
required this.package,
|
||||||
this.backgroundMessageHandler,
|
required this.protocol});
|
||||||
this.toastDelegate});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _GuruPackageExtension on GuruPackage {
|
extension _GuruPackageExtension on GuruPackage {
|
||||||
|
|
@ -130,8 +145,80 @@ extension _GuruPackageExtension on GuruPackage {
|
||||||
child._dispatchInitializeAsync();
|
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 {
|
class GuruApp {
|
||||||
static late GuruApp _instance;
|
static late GuruApp _instance;
|
||||||
|
|
||||||
|
|
@ -141,6 +228,12 @@ class GuruApp {
|
||||||
|
|
||||||
final AppSpec appSpec;
|
final AppSpec appSpec;
|
||||||
|
|
||||||
|
final IGuruSdkProtocol protocol;
|
||||||
|
RemoteDeployment? _remoteDeployment;
|
||||||
|
|
||||||
|
RemoteDeployment get remoteDeployment =>
|
||||||
|
_remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment();
|
||||||
|
|
||||||
String get appName => appSpec.appName;
|
String get appName => appSpec.appName;
|
||||||
|
|
||||||
String get flavor => appSpec.flavor;
|
String get flavor => appSpec.flavor;
|
||||||
|
|
@ -157,8 +250,12 @@ class GuruApp {
|
||||||
|
|
||||||
Set<String> get conversionEvents => appSpec.deployment.conversionEvents;
|
Set<String> get conversionEvents => appSpec.deployment.conversionEvents;
|
||||||
|
|
||||||
GuruApp._({required this.appSpec, required this.rootPackage, ToastDelegate? toastDelegate}) {
|
GuruApp._(
|
||||||
GuruUtils.toastDelegate = toastDelegate;
|
{required this.appSpec,
|
||||||
|
required this.rootPackage,
|
||||||
|
required this.protocol,
|
||||||
|
}) {
|
||||||
|
GuruUtils.toastDelegate = protocol.toastDelegate;
|
||||||
AdsOverlay.bind(showBanner: GuruPopup.instance.showAdsBanner);
|
AdsOverlay.bind(showBanner: GuruPopup.instance.showAdsBanner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,8 +264,15 @@ class GuruApp {
|
||||||
Iterable<LocalizationsDelegate<dynamic>> get localizationsDelegates =>
|
Iterable<LocalizationsDelegate<dynamic>> get localizationsDelegates =>
|
||||||
rootPackage._mergeLocalizationsDelegates();
|
rootPackage._mergeLocalizationsDelegates();
|
||||||
|
|
||||||
|
String getRemoteConfigKey(String key) => appSpec.getRemoteConfigKey(key);
|
||||||
|
|
||||||
bool? _check;
|
bool? _check;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static void setMockInstance(GuruApp app) {
|
||||||
|
_instance = app;
|
||||||
|
}
|
||||||
|
|
||||||
Future _initialize() async {
|
Future _initialize() async {
|
||||||
try {
|
try {
|
||||||
await GuruDB.instance.initDatabase();
|
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 {
|
Future<bool> _checkApp() async {
|
||||||
try {
|
try {
|
||||||
final pkgName = (await PackageInfo.fromPlatform()).appName;
|
final pkgName = (await PackageInfo.fromPlatform()).appName;
|
||||||
|
|
@ -210,6 +365,9 @@ class GuruApp {
|
||||||
|
|
||||||
Future _dispatchInitializeSync() async {
|
Future _dispatchInitializeSync() async {
|
||||||
await RemoteConfigManager.instance.init(appSpec.defaultRemoteConfig);
|
await RemoteConfigManager.instance.init(appSpec.defaultRemoteConfig);
|
||||||
|
refreshRemoteDeployment();
|
||||||
|
await DeviceUtils.initialize();
|
||||||
|
await GuruAnalytics.instance.prepare();
|
||||||
await rootPackage._dispatchInitialize();
|
await rootPackage._dispatchInitialize();
|
||||||
try {
|
try {
|
||||||
GuruUtils.isTablet = (await GuruApplovinFlutter.instance.isTablet()) ?? false;
|
GuruUtils.isTablet = (await GuruApplovinFlutter.instance.isTablet()) ?? false;
|
||||||
|
|
@ -237,7 +395,7 @@ class GuruApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future initialize({required AppEnv appEnv}) async {
|
static Future initialize({required AppEnv appEnv}) async {
|
||||||
final backgroundMessageHandler = appEnv.backgroundMessageHandler;
|
final backgroundMessageHandler = appEnv.protocol.backgroundMessageHandler;
|
||||||
if (backgroundMessageHandler != null) {
|
if (backgroundMessageHandler != null) {
|
||||||
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
||||||
}
|
}
|
||||||
|
|
@ -248,9 +406,18 @@ class GuruApp {
|
||||||
Log.e("Firebase.initializeApp() error!", error: error, stackTrace: stacktrace);
|
Log.e("Firebase.initializeApp() error!", error: error, stackTrace: stacktrace);
|
||||||
}
|
}
|
||||||
GuruUtils.flavor = appEnv.spec.flavor;
|
GuruUtils.flavor = appEnv.spec.flavor;
|
||||||
|
|
||||||
|
/// 这里不用担心重复初始化,因为initialize会把对应的 AuthType 重新赋值
|
||||||
|
/// 如果传入的 AuthType 有重复,会覆盖之前的 AuthType
|
||||||
|
AuthCredentialManager.initialize([
|
||||||
|
...AccountManager.defaultSupportedAuthCredentialDelegates,
|
||||||
|
...appEnv.protocol.accountAuthDelegate?.supportedAuthCredentialDelegates ?? []
|
||||||
|
]);
|
||||||
try {
|
try {
|
||||||
_instance = GuruApp._(
|
_instance = GuruApp._(
|
||||||
appSpec: appEnv.spec, rootPackage: appEnv.package, toastDelegate: appEnv.toastDelegate);
|
appSpec: appEnv.spec,
|
||||||
|
rootPackage: appEnv.package,
|
||||||
|
protocol: appEnv.protocol);
|
||||||
Log.init(_instance.appName,
|
Log.init(_instance.appName,
|
||||||
persistentLogFileSize: appEnv.spec.deployment.logFileSizeLimit,
|
persistentLogFileSize: appEnv.spec.deployment.logFileSizeLimit,
|
||||||
persistentLogCount: appEnv.spec.deployment.logFileCount,
|
persistentLogCount: appEnv.spec.deployment.logFileCount,
|
||||||
|
|
@ -279,8 +446,7 @@ extension GuruAppInitializerExt on GuruApp {
|
||||||
await RemoteConfigManager.instance.fetchAndActivate();
|
await RemoteConfigManager.instance.fetchAndActivate();
|
||||||
final cdnConfig = RemoteConfigManager.instance.getCdnConfig();
|
final cdnConfig = RemoteConfigManager.instance.getCdnConfig();
|
||||||
HttpEx.init(cdnConfig, GuruApp.instance.appSpec.details.storagePrefix);
|
HttpEx.init(cdnConfig, GuruApp.instance.appSpec.details.storagePrefix);
|
||||||
|
refreshRemoteDeployment();
|
||||||
final remoteDeployment = RemoteConfigManager.instance.getRemoteDeployment();
|
|
||||||
Settings.get()
|
Settings.get()
|
||||||
.keepOnScreenDuration
|
.keepOnScreenDuration
|
||||||
.set(remoteDeployment.keepScreenOnDuration * DateTimeUtils.minuteInMillis);
|
.set(remoteDeployment.keepScreenOnDuration * DateTimeUtils.minuteInMillis);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,408 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:guru_app/database/guru_db.dart';
|
||||||
|
import 'package:guru_app/financial/product/product_model.dart';
|
||||||
|
import 'package:guru_utils/database/batch/batch_data.dart';
|
||||||
|
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||||
|
import 'package:guru_utils/id/identifiable.dart';
|
||||||
|
import 'package:guru_utils/log/log.dart';
|
||||||
|
import 'package:guru_utils/manifest/manifest.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'inventory_database.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class LimitedBalance {
|
||||||
|
@JsonKey(name: "a", defaultValue: 0)
|
||||||
|
final int amount;
|
||||||
|
|
||||||
|
@JsonKey(name: "e", defaultValue: -1)
|
||||||
|
final int expireAt;
|
||||||
|
|
||||||
|
@JsonKey(name: "r", defaultValue: false)
|
||||||
|
final bool recycle;
|
||||||
|
|
||||||
|
LimitedBalance(this.amount, this.expireAt, {this.recycle = false});
|
||||||
|
|
||||||
|
factory LimitedBalance.fromJson(Map<String, dynamic> json) => _$LimitedBalanceFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$LimitedBalanceToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class TimeSensitiveData {
|
||||||
|
@JsonKey(name: "v", defaultValue: [])
|
||||||
|
final List<LimitedBalance> valid;
|
||||||
|
|
||||||
|
@JsonKey(name: "e", defaultValue: [])
|
||||||
|
final List<LimitedBalance> expired;
|
||||||
|
|
||||||
|
int get validBalance =>
|
||||||
|
valid.isEmpty ? 0 : valid.map((e) => e.amount).reduce((total, amount) => total + amount);
|
||||||
|
|
||||||
|
int get expiredBalance =>
|
||||||
|
expired.isEmpty ? 0 : expired.map((e) => e.amount).reduce((total, amount) => total + amount);
|
||||||
|
|
||||||
|
const TimeSensitiveData(
|
||||||
|
{this.valid = const <LimitedBalance>[], this.expired = const <LimitedBalance>[]});
|
||||||
|
|
||||||
|
TimeSensitiveData.create({LimitedBalance? balance})
|
||||||
|
: valid = ((balance?.expireAt ?? 0) > DateTimeUtils.currentTimeInMillis()) ? [balance!] : [],
|
||||||
|
expired = [];
|
||||||
|
|
||||||
|
factory TimeSensitiveData.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$TimeSensitiveDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$TimeSensitiveDataToJson(this);
|
||||||
|
|
||||||
|
TimeSensitiveData attachLimitedBalance(LimitedBalance balance) {
|
||||||
|
if (balance.expireAt > DateTimeUtils.currentTimeInMillis()) {
|
||||||
|
return TimeSensitiveData(valid: List.of(valid)..add(balance), expired: List.of(expired));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
TimeSensitiveData consume(int balance) {
|
||||||
|
final now = DateTimeUtils.currentTimeInMillis();
|
||||||
|
final newValid = <LimitedBalance>[];
|
||||||
|
for (var item in valid) {
|
||||||
|
int amount = item.amount;
|
||||||
|
if (now < item.expireAt) {
|
||||||
|
if (balance >= item.amount) {
|
||||||
|
balance -= item.amount;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
amount -= balance;
|
||||||
|
balance = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 这里只针对未过期的道具进行处理
|
||||||
|
newValid.add(LimitedBalance(amount, item.expireAt));
|
||||||
|
}
|
||||||
|
newValid.sort((a, b) => b.expireAt.compareTo(a.expireAt));
|
||||||
|
return TimeSensitiveData(valid: newValid, expired: expired);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecycleResult recycleExpiredBalance({int? transactionTs}) {
|
||||||
|
final now = transactionTs ?? DateTimeUtils.currentTimeInMillis();
|
||||||
|
final newValid = <LimitedBalance>[];
|
||||||
|
final newExpired = <LimitedBalance>[];
|
||||||
|
int expiredBalance = 0;
|
||||||
|
for (var item in valid) {
|
||||||
|
if (now >= item.expireAt) {
|
||||||
|
expiredBalance += item.amount;
|
||||||
|
if (item.recycle) {
|
||||||
|
newExpired.add(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newValid.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newValid.sort((a, b) => b.expireAt.compareTo(a.expireAt));
|
||||||
|
return RecycleResult(expiredBalance, TimeSensitiveData(valid: newValid, expired: newExpired));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecycleResult {
|
||||||
|
final int expiredBalance;
|
||||||
|
final TimeSensitiveData timeSensitiveData;
|
||||||
|
|
||||||
|
RecycleResult(this.expiredBalance, this.timeSensitiveData);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsumeResult {
|
||||||
|
final bool consumed;
|
||||||
|
final InventoryItem item;
|
||||||
|
|
||||||
|
ConsumeResult.success(this.item) : consumed = true;
|
||||||
|
|
||||||
|
ConsumeResult.error(this.item) : consumed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable(constructor: "_")
|
||||||
|
class InventoryDetails {
|
||||||
|
@JsonKey(name: "a", defaultValue: {})
|
||||||
|
final Map<TransactionMethod, int> acquired;
|
||||||
|
|
||||||
|
@JsonKey(name: "c", defaultValue: {})
|
||||||
|
final Map<String, int> consumed;
|
||||||
|
|
||||||
|
@JsonKey(name: "d")
|
||||||
|
final Map<String, dynamic> data;
|
||||||
|
|
||||||
|
const InventoryDetails._(this.acquired, this.consumed, this.data);
|
||||||
|
|
||||||
|
InventoryDetails.empty()
|
||||||
|
: acquired = const <TransactionMethod, int>{},
|
||||||
|
consumed = const <String, int>{},
|
||||||
|
data = const <String, dynamic>{};
|
||||||
|
|
||||||
|
InventoryDetails.create(int amount)
|
||||||
|
: acquired = (amount == 0)
|
||||||
|
? const <TransactionMethod, int>{}
|
||||||
|
: <TransactionMethod, int>{TransactionMethod.unknown: amount},
|
||||||
|
consumed = const <String, int>{},
|
||||||
|
data = <String, dynamic>{};
|
||||||
|
|
||||||
|
// 通过什么方式获得了多少
|
||||||
|
InventoryDetails acquire(TransactionMethod method, int amount) {
|
||||||
|
return InventoryDetails._(
|
||||||
|
Map<TransactionMethod, int>.from(acquired)..[method] = (acquired[method] ?? 0) + amount,
|
||||||
|
Map<String, int>.from(consumed),
|
||||||
|
Map<String, dynamic>.from(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在什么场景消耗了多少
|
||||||
|
InventoryDetails consume(String scene, int amount) {
|
||||||
|
return InventoryDetails._(
|
||||||
|
Map<TransactionMethod, int>.from(acquired),
|
||||||
|
Map<String, int>.from(consumed)..[scene] = (consumed[scene] ?? 0) + amount,
|
||||||
|
Map<String, dynamic>.from(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InventoryDetails.fromJson(Map<String, dynamic> json) => _$InventoryDetailsFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$InventoryDetailsToJson(this);
|
||||||
|
|
||||||
|
void setInt(String key, int value) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDouble(String key, double value) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setString(String key, String value) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setBool(String key, bool value) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? getInt(String key) {
|
||||||
|
return data[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
double? getDouble(String key) {
|
||||||
|
return data[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getString(String key) {
|
||||||
|
return data[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool? getBool(String key) {
|
||||||
|
return data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InventoryDetailsStringConvert implements JsonConverter<InventoryDetails, String> {
|
||||||
|
const InventoryDetailsStringConvert();
|
||||||
|
|
||||||
|
@override
|
||||||
|
InventoryDetails fromJson(String json) {
|
||||||
|
if (json.isEmpty) {
|
||||||
|
return InventoryDetails.empty();
|
||||||
|
}
|
||||||
|
final result = jsonDecode(json);
|
||||||
|
return InventoryDetails.fromJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toJson(InventoryDetails transaction) {
|
||||||
|
return jsonEncode(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeSensitiveDataStringConvert implements JsonConverter<TimeSensitiveData, String> {
|
||||||
|
const TimeSensitiveDataStringConvert();
|
||||||
|
|
||||||
|
@override
|
||||||
|
TimeSensitiveData fromJson(String json) {
|
||||||
|
if (json.isEmpty) {
|
||||||
|
return const TimeSensitiveData();
|
||||||
|
}
|
||||||
|
final result = jsonDecode(json);
|
||||||
|
return TimeSensitiveData.fromJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toJson(TimeSensitiveData transaction) {
|
||||||
|
return jsonEncode(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const InventoryDetailsStringConvert inventoryDetailsStringConvert = InventoryDetailsStringConvert();
|
||||||
|
const TimeSensitiveDataStringConvert timeSensitiveDataStringConvert =
|
||||||
|
TimeSensitiveDataStringConvert();
|
||||||
|
|
||||||
|
class InventoryTable {
|
||||||
|
static const tbName = "inventory"; // Product Transaction Table
|
||||||
|
static const dbSku = "sku";
|
||||||
|
static const dbBalance = "balance";
|
||||||
|
static const dbCategory = "cat";
|
||||||
|
static const dbAttr = "attr";
|
||||||
|
static const dbDetails = "details";
|
||||||
|
static const dbTimeSensitive = "tsv";
|
||||||
|
static const dbUpdateAt = "update_at";
|
||||||
|
static const dbCreateAt = "create_at";
|
||||||
|
|
||||||
|
static Future createTable(Transaction delegate) async {
|
||||||
|
const v1Fields = "${InventoryTable.dbSku} TEXT PRIMARY KEY,"
|
||||||
|
"${InventoryTable.dbBalance} INTEGER NOT NULL DEFAULT 0,"
|
||||||
|
"${InventoryTable.dbCategory} TEXT NOT NULL DEFAULT '',"
|
||||||
|
"${InventoryTable.dbAttr} INTEGER NOT NULL DEFAULT ${DetailsAttr.consumable},"
|
||||||
|
"${InventoryTable.dbDetails} TEXT NOT NULL DEFAULT '',"
|
||||||
|
"${InventoryTable.dbTimeSensitive} TEXT NOT NULL DEFAULT '',"
|
||||||
|
"${InventoryTable.dbCreateAt} INTEGER NOT NULL DEFAULT 0,"
|
||||||
|
"${InventoryTable.dbUpdateAt} INTEGER NOT NULL DEFAULT 0";
|
||||||
|
|
||||||
|
const cmd = "CREATE TABLE ${InventoryTable.tbName} ("
|
||||||
|
"$v1Fields"
|
||||||
|
");";
|
||||||
|
|
||||||
|
Log.v("#### cmd: $cmd");
|
||||||
|
|
||||||
|
await delegate.execute(cmd);
|
||||||
|
await delegate.execute(
|
||||||
|
"CREATE INDEX inventory_item_idx ON ${InventoryTable.tbName} (${InventoryTable.dbSku});");
|
||||||
|
await delegate.execute(
|
||||||
|
"CREATE INDEX inventory_item_category_idx ON ${InventoryTable.tbName} (${InventoryTable.dbCategory});");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable(constructor: "_")
|
||||||
|
class InventoryItem implements Identifiable {
|
||||||
|
@JsonKey(name: InventoryTable.dbSku)
|
||||||
|
final String sku;
|
||||||
|
|
||||||
|
@JsonKey(name: InventoryTable.dbBalance)
|
||||||
|
final int balance; // 永久的+时效
|
||||||
|
|
||||||
|
@JsonKey(name: InventoryTable.dbCategory)
|
||||||
|
final String category;
|
||||||
|
|
||||||
|
@JsonKey(name: InventoryTable.dbAttr)
|
||||||
|
final int attr;
|
||||||
|
|
||||||
|
@JsonKey(name: InventoryTable.dbTimeSensitive)
|
||||||
|
@timeSensitiveDataStringConvert
|
||||||
|
final TimeSensitiveData timeSensitive;
|
||||||
|
|
||||||
|
@JsonKey(name: InventoryTable.dbUpdateAt)
|
||||||
|
final int updateAt;
|
||||||
|
|
||||||
|
@JsonKey(name: InventoryTable.dbCreateAt)
|
||||||
|
final int createAt;
|
||||||
|
|
||||||
|
@JsonKey(name: InventoryTable.dbDetails)
|
||||||
|
@inventoryDetailsStringConvert
|
||||||
|
final InventoryDetails details;
|
||||||
|
|
||||||
|
InventoryItem._(this.sku, this.balance, this.category, this.attr, this.details,
|
||||||
|
this.timeSensitive, this.createAt, this.updateAt);
|
||||||
|
|
||||||
|
InventoryItem.create(this.sku, this.category, this.attr,
|
||||||
|
{int expireAt = -1, this.balance = 0, TransactionMethod method = TransactionMethod.unknown})
|
||||||
|
: updateAt = DateTimeUtils.currentTimeInMillis(),
|
||||||
|
createAt = DateTimeUtils.currentTimeInMillis(),
|
||||||
|
details = InventoryDetails.create(balance),
|
||||||
|
timeSensitive = TimeSensitiveData.create(balance: LimitedBalance(balance, expireAt));
|
||||||
|
|
||||||
|
InventoryItem acquire(TransactionMethod method, int amount, {int? expiredAt}) {
|
||||||
|
final now = DateTimeUtils.currentTimeInMillis();
|
||||||
|
if (expiredAt != null && expiredAt > now) {
|
||||||
|
timeSensitive.valid.add(LimitedBalance(amount, expiredAt));
|
||||||
|
}
|
||||||
|
final recycled = timeSensitive.recycleExpiredBalance();
|
||||||
|
final target = balance + amount;
|
||||||
|
return InventoryItem._(sku, (target - recycled.expiredBalance).clamp(0, target), category, attr,
|
||||||
|
details.acquire(method, amount), recycled.timeSensitiveData, createAt, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsumeResult consume(int amount, {String scene = ""}) {
|
||||||
|
final now = DateTimeUtils.currentTimeInMillis();
|
||||||
|
final recycled = timeSensitive.recycleExpiredBalance();
|
||||||
|
int remainBalance = (balance - recycled.expiredBalance).clamp(0, balance);
|
||||||
|
if (remainBalance >= amount && attr == DetailsAttr.consumable) {
|
||||||
|
remainBalance -= amount;
|
||||||
|
return ConsumeResult.success(InventoryItem._(
|
||||||
|
sku,
|
||||||
|
remainBalance,
|
||||||
|
category,
|
||||||
|
attr,
|
||||||
|
details.consume(scene, amount),
|
||||||
|
recycled.timeSensitiveData.consume(amount),
|
||||||
|
createAt,
|
||||||
|
now));
|
||||||
|
}
|
||||||
|
return ConsumeResult.error(InventoryItem._(
|
||||||
|
sku, remainBalance, category, attr, details, recycled.timeSensitiveData, createAt, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsumeResult consumeTimeSensitiveOnly(int amount, {String scene = "", int? transactionTs}) {
|
||||||
|
final now = transactionTs ?? DateTimeUtils.currentTimeInMillis();
|
||||||
|
final recycled = timeSensitive.recycleExpiredBalance(transactionTs: transactionTs);
|
||||||
|
final validBalance = recycled.timeSensitiveData.validBalance;
|
||||||
|
int remainBalance = (balance - recycled.expiredBalance).clamp(0, balance);
|
||||||
|
|
||||||
|
/// 这里只是针对有时效性的道具进行消耗,如果时效性道具不足,将返回错误
|
||||||
|
if (validBalance >= amount && attr == DetailsAttr.consumable) {
|
||||||
|
/// 虽然这里只是针对时效性道具进行消耗,但是这里的balance还是会进行更新
|
||||||
|
remainBalance = (remainBalance - amount).clamp(0, remainBalance);
|
||||||
|
return ConsumeResult.success(InventoryItem._(
|
||||||
|
sku,
|
||||||
|
remainBalance,
|
||||||
|
category,
|
||||||
|
attr,
|
||||||
|
details.consume(scene, amount),
|
||||||
|
recycled.timeSensitiveData.consume(amount),
|
||||||
|
createAt,
|
||||||
|
now));
|
||||||
|
}
|
||||||
|
return ConsumeResult.error(InventoryItem._(
|
||||||
|
sku, remainBalance, category, attr, details, recycled.timeSensitiveData, createAt, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InventoryItem.fromJson(Map<String, dynamic> json) => _$InventoryItemFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$InventoryItemToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => sku;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InventoryDatabase on GuruDB {
|
||||||
|
Future<BatchData<InventoryItem>> loadInventoryItems() async {
|
||||||
|
final db = getDb();
|
||||||
|
final result = await db.rawQuery("SELECT * FROM ${InventoryTable.tbName}");
|
||||||
|
final batchData = BatchData<InventoryItem>.empty();
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
batchData.queryAll([for (var item in result) InventoryItem.fromJson(item)]);
|
||||||
|
}
|
||||||
|
return batchData;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<BatchData<InventoryItem>> updateInventoryItem(InventoryItem item) async {
|
||||||
|
final db = getDb();
|
||||||
|
await db.insert(InventoryTable.tbName, item.toJson(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
return BatchData<InventoryItem>.singleSuccess(BatchMethod.update, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<BatchData<InventoryItem>> updateInventoryItems(List<InventoryItem> items) async {
|
||||||
|
final BatchData<InventoryItem> batchData = BatchData<InventoryItem>.empty();
|
||||||
|
return runInTransaction((txn) async {
|
||||||
|
for (var item in items) {
|
||||||
|
await txn.insert(InventoryTable.tbName, item.toJson(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
batchData.update(item);
|
||||||
|
}
|
||||||
|
return batchData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'inventory_database.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
LimitedBalance _$LimitedBalanceFromJson(Map<String, dynamic> json) =>
|
||||||
|
LimitedBalance(
|
||||||
|
json['a'] as int? ?? 0,
|
||||||
|
json['e'] as int? ?? -1,
|
||||||
|
recycle: json['r'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LimitedBalanceToJson(LimitedBalance instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'a': instance.amount,
|
||||||
|
'e': instance.expireAt,
|
||||||
|
'r': instance.recycle,
|
||||||
|
};
|
||||||
|
|
||||||
|
TimeSensitiveData _$TimeSensitiveDataFromJson(Map<String, dynamic> json) =>
|
||||||
|
TimeSensitiveData(
|
||||||
|
valid: (json['v'] as List<dynamic>?)
|
||||||
|
?.map((e) => LimitedBalance.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
expired: (json['e'] as List<dynamic>?)
|
||||||
|
?.map((e) => LimitedBalance.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TimeSensitiveDataToJson(TimeSensitiveData instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'v': instance.valid,
|
||||||
|
'e': instance.expired,
|
||||||
|
};
|
||||||
|
|
||||||
|
InventoryDetails _$InventoryDetailsFromJson(Map<String, dynamic> json) =>
|
||||||
|
InventoryDetails._(
|
||||||
|
(json['a'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) =>
|
||||||
|
MapEntry($enumDecode(_$TransactionMethodEnumMap, k), e as int),
|
||||||
|
) ??
|
||||||
|
{},
|
||||||
|
(json['c'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, e as int),
|
||||||
|
) ??
|
||||||
|
{},
|
||||||
|
json['d'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$InventoryDetailsToJson(InventoryDetails instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'a': instance.acquired
|
||||||
|
.map((k, e) => MapEntry(_$TransactionMethodEnumMap[k]!, e)),
|
||||||
|
'c': instance.consumed,
|
||||||
|
'd': instance.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$TransactionMethodEnumMap = {
|
||||||
|
TransactionMethod.iap: 'iap',
|
||||||
|
TransactionMethod.igc: 'igc',
|
||||||
|
TransactionMethod.reward: 'reward',
|
||||||
|
TransactionMethod.bonus: 'bonus',
|
||||||
|
TransactionMethod.igb: 'igb',
|
||||||
|
TransactionMethod.free: 'free',
|
||||||
|
TransactionMethod.migrate: 'migrate',
|
||||||
|
TransactionMethod.unknown: 'unknown',
|
||||||
|
};
|
||||||
|
|
||||||
|
InventoryItem _$InventoryItemFromJson(Map<String, dynamic> json) =>
|
||||||
|
InventoryItem._(
|
||||||
|
json['sku'] as String,
|
||||||
|
json['balance'] as int,
|
||||||
|
json['cat'] as String,
|
||||||
|
json['attr'] as int,
|
||||||
|
inventoryDetailsStringConvert.fromJson(json['details'] as String),
|
||||||
|
timeSensitiveDataStringConvert.fromJson(json['tsv'] as String),
|
||||||
|
json['create_at'] as int,
|
||||||
|
json['update_at'] as int,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$InventoryItemToJson(InventoryItem instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'sku': instance.sku,
|
||||||
|
'balance': instance.balance,
|
||||||
|
'cat': instance.category,
|
||||||
|
'attr': instance.attr,
|
||||||
|
'tsv': timeSensitiveDataStringConvert.toJson(instance.timeSensitive),
|
||||||
|
'update_at': instance.updateAt,
|
||||||
|
'create_at': instance.createAt,
|
||||||
|
'details': inventoryDetailsStringConvert.toJson(instance.details),
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
import 'package:guru_app/analytics/guru_analytics.dart';
|
||||||
|
import 'package:guru_app/database/guru_db.dart';
|
||||||
|
import 'package:guru_app/financial/product/product_model.dart';
|
||||||
|
import 'package:guru_app/guru_app.dart';
|
||||||
|
import 'package:guru_app/inventory/db/inventory_database.dart';
|
||||||
|
import 'package:guru_utils/database/batch/batch_aware.dart';
|
||||||
|
import 'package:guru_utils/log/log.dart';
|
||||||
|
import 'package:guru_utils/manifest/manifest.dart';
|
||||||
|
|
||||||
|
class InventoryCategory {
|
||||||
|
static const String prop = "prop"; // 道具类
|
||||||
|
}
|
||||||
|
|
||||||
|
class PropCategory {
|
||||||
|
// 增益类:提供临时或永久的能力提升,如速度增加、力量提升、生命值恢复等。
|
||||||
|
static const String boosts = 'Boosts';
|
||||||
|
|
||||||
|
// 减益类:施加在对手或玩家身上,造成能力降低、速度减慢、伤害增加等负面效果。
|
||||||
|
static const String debuffs = 'Debuffs';
|
||||||
|
|
||||||
|
// 治疗类:恢复生命值或状态,解除负面效果。
|
||||||
|
static const String healing = 'Healing';
|
||||||
|
|
||||||
|
// 防御类:提供防护,减少受到的伤害或完全避免某些类型的伤害。
|
||||||
|
static const String defensive = 'Defensive';
|
||||||
|
|
||||||
|
// 攻击类:用于对敌人造成伤害或施加减益效果,如武器、陷阱、魔法等。
|
||||||
|
static const String offensive = 'Offensive';
|
||||||
|
|
||||||
|
// 辅助类:提供团队增益、增强队友能力、提供战术优势等非直接攻击手段的道具。
|
||||||
|
static const String supportive = 'Supportive';
|
||||||
|
|
||||||
|
// 探索类:用于揭示地图、发现隐藏物品、解锁新区域或提供关键信息的道具。
|
||||||
|
static const String exploratory = 'Exploratory';
|
||||||
|
|
||||||
|
// 交互类:与游戏世界中的其他元素或玩家进行交互的道具,如钥匙、开关、通信设备等。
|
||||||
|
static const String interactive = 'Interactive';
|
||||||
|
|
||||||
|
// 资源类:用于制造、升级或交易的材料和货币类型道具。
|
||||||
|
static const String resources = 'Resources';
|
||||||
|
|
||||||
|
// 限定类:只能使用一定次数的道具,使用后消失或需要充能。
|
||||||
|
static const String limitedUse = 'LimitedUse';
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin InventoryDelegate {
|
||||||
|
// 返回指定id的库存类别
|
||||||
|
// 如 hint,zoom这些道具统一返回
|
||||||
|
String getInventoryCategory(String id) {
|
||||||
|
return InventoryCategory.prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<StockItem>> getMigrateStockItems() async {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StockItem {
|
||||||
|
final String sku;
|
||||||
|
final int amount;
|
||||||
|
final int attr;
|
||||||
|
final DateTime? expired;
|
||||||
|
|
||||||
|
const StockItem.consumable(this.sku, this.amount, {this.expired}) : attr = DetailsAttr.consumable;
|
||||||
|
|
||||||
|
const StockItem.permanent(this.sku, this.amount, {this.expired}) : attr = DetailsAttr.permanent;
|
||||||
|
|
||||||
|
const StockItem.igc(this.amount, {this.sku = "igc"})
|
||||||
|
: attr = DetailsAttr.consumable,
|
||||||
|
expired = null;
|
||||||
|
|
||||||
|
StockItem.fromDetails(Details details)
|
||||||
|
: sku = details.sku,
|
||||||
|
attr = details.attr,
|
||||||
|
amount = details.amount,
|
||||||
|
expired = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InventoryManager with BatchAware<InventoryItem> {
|
||||||
|
static final InventoryManager instance = InventoryManager._();
|
||||||
|
|
||||||
|
InventoryManager._();
|
||||||
|
|
||||||
|
String getInventoryCategory(String id) {
|
||||||
|
return GuruApp.instance.protocol.inventoryDelegate?.getInventoryCategory(id) ??
|
||||||
|
InventoryCategory.prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future init() async {
|
||||||
|
final batch = await GuruDB.instance.loadInventoryItems();
|
||||||
|
processBatchData(batch);
|
||||||
|
await _migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _migrate() async {
|
||||||
|
final migrateStockItems =
|
||||||
|
await GuruApp.instance.protocol.inventoryDelegate?.getMigrateStockItems() ?? [];
|
||||||
|
|
||||||
|
if (migrateStockItems.isNotEmpty) {
|
||||||
|
final needMigrateItems = migrateStockItems.where((item) => !exists(item.sku)).toList();
|
||||||
|
await acquire(needMigrateItems, TransactionMethod.migrate, "migrate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 通过[method]中的的特定[specific]方式 获得了指定的 [items]
|
||||||
|
/// * method: iap -> specific: sku
|
||||||
|
/// * method: igc -> specific: coin/gems...
|
||||||
|
/// * method: reward -> specific: ads/lottery/daily/...
|
||||||
|
/// * method: bonus -> specific: ads/other/...
|
||||||
|
/// * method: igb -> specific: hint/hammer/swap/magic/..
|
||||||
|
///
|
||||||
|
/// method 最终会在 earnVirtualCurrency 中成为 item_category
|
||||||
|
/// specific 最终会在 earnVirtualCurrency 中成为 item_name
|
||||||
|
///
|
||||||
|
Future acquire(List<StockItem> items, TransactionMethod method, String specific,
|
||||||
|
{String? scene}) async {
|
||||||
|
final acquired = <InventoryItem>[];
|
||||||
|
for (var item in items) {
|
||||||
|
final category = getInventoryCategory(item.sku);
|
||||||
|
final invItem = getData(item.sku)
|
||||||
|
?.acquire(method, item.amount, expiredAt: item.expired?.millisecondsSinceEpoch) ??
|
||||||
|
InventoryItem.create(item.sku, category, item.attr,
|
||||||
|
balance: item.amount,
|
||||||
|
method: method,
|
||||||
|
expireAt: item.expired?.millisecondsSinceEpoch ?? -1);
|
||||||
|
acquired.add(invItem);
|
||||||
|
|
||||||
|
GuruAnalytics.instance.logEarnVirtualCurrency(
|
||||||
|
virtualCurrencyName: item.sku,
|
||||||
|
method: convertTransactionMethodName(method),
|
||||||
|
specific: specific,
|
||||||
|
balance: invItem.balance,
|
||||||
|
value: item.amount,
|
||||||
|
scene: scene);
|
||||||
|
}
|
||||||
|
final batchData = await GuruDB.instance.updateInventoryItems(acquired);
|
||||||
|
processBatchData(batchData);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsumeResult? _consumeItem(StockItem item, Manifest redeemed, {bool timeSensitiveOnly = false}) {
|
||||||
|
final inventoryItem = getData(item.sku);
|
||||||
|
return timeSensitiveOnly
|
||||||
|
? inventoryItem?.consumeTimeSensitiveOnly(item.amount,
|
||||||
|
scene: redeemed.scene, transactionTs: redeemed.transactionTs)
|
||||||
|
: inventoryItem?.consume(item.amount, scene: redeemed.scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 消耗指定的道具[items],如果可以成功消费,便可得到相应的[redeemed]清单
|
||||||
|
Future<bool> consume(List<StockItem> items, Manifest redeemed,
|
||||||
|
{bool timeSensitiveOnly = false}) async {
|
||||||
|
final consumed = <InventoryItem>[];
|
||||||
|
bool isConsumed = true;
|
||||||
|
for (var item in items) {
|
||||||
|
/// 这里只是针对对应的 SKU 商品进行 consume标记,真正的消耗在下面的updateInventoryItems中
|
||||||
|
final result = _consumeItem(item, redeemed, timeSensitiveOnly: timeSensitiveOnly);
|
||||||
|
if (result == null) {
|
||||||
|
Log.e("consume failed! Not Found inventory item: ${item.sku}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
consumed.add(result.item);
|
||||||
|
// 这里如果没有消耗掉,将跳出
|
||||||
|
if (!result.consumed) {
|
||||||
|
Log.w("consume failed: ${item.sku} ${item.amount}");
|
||||||
|
isConsumed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (consumed.isEmpty) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (isConsumed && consumed.length == items.length) {
|
||||||
|
for (int i = 0; i < items.length; ++i) {
|
||||||
|
GuruAnalytics.instance.logSpendCredits(
|
||||||
|
redeemed.contentId, redeemed.category, items[i].amount,
|
||||||
|
virtualCurrencyName: consumed[i].sku,
|
||||||
|
balance: consumed[i].balance,
|
||||||
|
scene: redeemed.scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 这里会更新消耗掉的道具
|
||||||
|
final batchData = await GuruDB.instance.updateInventoryItems(consumed);
|
||||||
|
processBatchData(batchData);
|
||||||
|
return isConsumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canAfford(String id, int amount) {
|
||||||
|
final item = getData(id);
|
||||||
|
return item != null && item.balance >= amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,13 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:guru_app/account/model/account.dart';
|
import 'package:guru_app/account/model/account.dart';
|
||||||
import 'package:guru_app/account/model/account_profile.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/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/api/data/orders/orders_model.dart';
|
||||||
import 'package:guru_app/guru_app.dart';
|
import 'package:guru_app/guru_app.dart';
|
||||||
import 'package:guru_app/property/property_keys.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/property/property_model.dart';
|
||||||
import 'package:guru_utils/datetime/datetime_utils.dart';
|
import 'package:guru_utils/datetime/datetime_utils.dart';
|
||||||
import 'package:guru_utils/device/device_info.dart';
|
import 'package:guru_utils/device/device_info.dart';
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,25 @@
|
||||||
part of "../app_property.dart";
|
part of "../app_property.dart";
|
||||||
|
|
||||||
extension AccountPropertyExtension on AppProperty {
|
extension AccountPropertyExtension on AppProperty {
|
||||||
void setAccountSaasUser(SaasUser saasUser) {
|
Future setAccountGuruUser(GuruUser guruUser) async {
|
||||||
final data = jsonEncode(saasUser);
|
final data = jsonEncode(guruUser);
|
||||||
setString(PropertyKeys.accountSaasUser, data);
|
await setString(PropertyKeys.accountGuruUser, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAccountDevice(DeviceInfo deviceInfo) {
|
Future setAccountDevice(DeviceInfo deviceInfo) async {
|
||||||
final data = jsonEncode(deviceInfo);
|
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);
|
final data = jsonEncode(accountProfile);
|
||||||
setString(PropertyKeys.accountProfile, data);
|
await setString(PropertyKeys.accountProfile, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// refer updateLocalProfile
|
// refer updateLocalProfile
|
||||||
void setDirtyAccountProfile(AccountProfile accountProfile) {
|
Future setDirtyAccountProfile(AccountProfile accountProfile) async {
|
||||||
final data = jsonEncode(accountProfile.copyWith(dirty: true));
|
final data = jsonEncode(accountProfile.copyWith(dirty: true));
|
||||||
setString(PropertyKeys.accountProfile, data);
|
await setString(PropertyKeys.accountProfile, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DeviceInfo?> getAccountDevice() async {
|
Future<DeviceInfo?> getAccountDevice() async {
|
||||||
|
|
@ -37,14 +37,14 @@ extension AccountPropertyExtension on AppProperty {
|
||||||
Log.v("loadValuesByTag is empty, $error");
|
Log.v("loadValuesByTag is empty, $error");
|
||||||
return PropertyBundle.empty();
|
return PropertyBundle.empty();
|
||||||
});
|
});
|
||||||
SaasUser? saasUser;
|
GuruUser? guruUser;
|
||||||
DeviceInfo? device;
|
DeviceInfo? device;
|
||||||
AccountProfile? accountProfile;
|
AccountProfile? accountProfile;
|
||||||
|
|
||||||
final saasUserString = accountBundle.getString(PropertyKeys.accountSaasUser);
|
final saasUserString = accountBundle.getString(PropertyKeys.accountGuruUser);
|
||||||
if (DartExt.isNotBlank(saasUserString)) {
|
if (DartExt.isNotBlank(saasUserString)) {
|
||||||
final map = jsonDecode(saasUserString!);
|
final map = jsonDecode(saasUserString!);
|
||||||
saasUser = SaasUser.fromJson(map);
|
guruUser = GuruUser.fromJson(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
final accountDeviceString = accountBundle.getString(PropertyKeys.accountDevice);
|
final accountDeviceString = accountBundle.getString(PropertyKeys.accountDevice);
|
||||||
|
|
@ -59,7 +59,28 @@ extension AccountPropertyExtension on AppProperty {
|
||||||
accountProfile = AccountProfile.fromJson(map);
|
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 {
|
Future<int> getLatestReportDeviceTimestamp() async {
|
||||||
|
|
@ -78,4 +99,34 @@ extension AccountPropertyExtension on AppProperty {
|
||||||
}
|
}
|
||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future clearAnonymousSecretKey() async {
|
||||||
|
await remove(PropertyKeys.anonymousSecretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future saveCredential(Credential credential) async {
|
||||||
|
final data = jsonEncode(credential);
|
||||||
|
await setString(PropertyKeys.buildAuthCredentialKey(credential.authType), data);
|
||||||
|
final historicalSocialAuths = await getHistoricalSocialAuths();
|
||||||
|
final authName = getAuthName(credential.authType);
|
||||||
|
if (!historicalSocialAuths.contains(authName)) {
|
||||||
|
historicalSocialAuths.add(authName);
|
||||||
|
setHistoricalSocialAuths(historicalSocialAuths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future deleteCredential(AuthType authType) async {
|
||||||
|
await remove(PropertyKeys.buildAuthCredentialKey(authType));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<String>> getHistoricalSocialAuths() async {
|
||||||
|
final data = await getString(PropertyKeys.historicalSocialAuths, defValue: "");
|
||||||
|
return data.isNotEmpty ? (data.split("|").toSet()..remove("")) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setHistoricalSocialAuths(Set<String> historicalSocialAuths) async {
|
||||||
|
historicalSocialAuths.remove("");
|
||||||
|
await setString(PropertyKeys.historicalSocialAuths, historicalSocialAuths.join("|"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,73 @@ extension AnalyticsPropertyExtension on AppProperty {
|
||||||
await setString(PropertyKeys.analyticsIdfa, idfa);
|
await setString(PropertyKeys.analyticsIdfa, idfa);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, ABTestExperiment>> loadRunningExperiments() async {
|
||||||
|
final bundle = await AppProperty.getInstance().loadValuesByTag(PropertyTags.guruExperiment);
|
||||||
|
final result = <String, ABTestExperiment>{};
|
||||||
|
bundle.forEach((key, value) {
|
||||||
|
try {
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
final map = json.decode(value);
|
||||||
|
final experiment = ABTestExperiment.fromJson(map);
|
||||||
|
result[key.name] = experiment;
|
||||||
|
Log.d("loadRunningExperiments: ${key.name} => $experiment");
|
||||||
|
}
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.w("getExperiment error! $error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ABTestExperiment?> getExperiment(String experimentName, {PropertyBundle? bundle}) async {
|
||||||
|
final experimentKey = PropertyKeys.buildExperimentProperty(experimentName);
|
||||||
|
final result = bundle?.getString(experimentKey) ?? await getString(experimentKey, defValue: "");
|
||||||
|
try {
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
final map = json.decode(result);
|
||||||
|
return ABTestExperiment.fromJson(map);
|
||||||
|
}
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
Log.w("getExperiment error! $error");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getExperimentVariant(String experimentName) async {
|
||||||
|
final variantKey =
|
||||||
|
PropertyKeys.buildABTestProperty(GuruAnalytics.buildVariantKey(experimentName));
|
||||||
|
return await getString(variantKey, defValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> setExperiment(ABTestExperiment experiment) async {
|
||||||
|
PropertyBundle propertyBundle = PropertyBundle();
|
||||||
|
|
||||||
|
final variantKey =
|
||||||
|
PropertyKeys.buildABTestProperty(GuruAnalytics.buildVariantKey(experiment.name));
|
||||||
|
final variantName = experiment.variantName;
|
||||||
|
propertyBundle.setString(variantKey, variantName);
|
||||||
|
|
||||||
|
final experimentKey = PropertyKeys.buildExperimentProperty(experiment.name);
|
||||||
|
propertyBundle.setString(experimentKey, json.encode(experiment));
|
||||||
|
await setProperties(propertyBundle);
|
||||||
|
return variantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeExperiment(String experimentName) async {
|
||||||
|
final experimentKey = PropertyKeys.buildExperimentProperty(experimentName);
|
||||||
|
await remove(experimentKey);
|
||||||
|
final variantKey = PropertyKeys.buildABTestProperty(experimentName);
|
||||||
|
await remove(variantKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> refreshGoogleDma(String googleDma) async {
|
||||||
|
final oldGoogleDma =
|
||||||
|
await AppProperty.getInstance().getString(PropertyKeys.googleDma, defValue: "");
|
||||||
|
if (googleDma != oldGoogleDma) {
|
||||||
|
await AppProperty.getInstance().setString(PropertyKeys.googleDma, googleDma);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ part of "../app_property.dart";
|
||||||
/// Created by @Haoyi on 5/14/21
|
/// Created by @Haoyi on 5/14/21
|
||||||
|
|
||||||
extension DefaultPropertyExtension on AppProperty {
|
extension DefaultPropertyExtension on AppProperty {
|
||||||
Future<String> getDeviceId() async {
|
Future<String> getDeviceId({String? forceDeviceId}) async {
|
||||||
return getOrCreateString(PropertyKeys.deviceId, IdUtils.uuidV4());
|
return getOrCreateString(PropertyKeys.deviceId, forceDeviceId ?? IdUtils.uuidV4());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getFirstInstallTime() async {
|
Future<int> getFirstInstallTime() async {
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,14 @@ extension IapPropertyExtension on AppProperty {
|
||||||
Future<void> removeReportSuccessOrder(PropertyKey key) async {
|
Future<void> removeReportSuccessOrder(PropertyKey key) async {
|
||||||
remove(key);
|
remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> increaseGraceCount() async {
|
||||||
|
final count = await getInt(PropertyKeys.subscriptionGraceCount, defValue: 0);
|
||||||
|
await setInt(PropertyKeys.subscriptionGraceCount, count + 1);
|
||||||
|
return count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resetGraceCount() async {
|
||||||
|
await setInt(PropertyKeys.subscriptionGraceCount, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:guru_app/guru_app.dart';
|
import 'package:guru_app/guru_app.dart';
|
||||||
import 'package:guru_app/property/app_property.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/id/id_utils.dart';
|
||||||
import 'package:guru_utils/property/property_model.dart';
|
import 'package:guru_utils/property/property_model.dart';
|
||||||
import 'package:guru_utils/settings/settings.dart';
|
import 'package:guru_utils/settings/settings.dart';
|
||||||
|
|
@ -21,9 +22,12 @@ class PropertyKeys {
|
||||||
static const PropertyKey debugMode = UtilsSettingsKeys.debugMode;
|
static const PropertyKey debugMode = UtilsSettingsKeys.debugMode;
|
||||||
static const PropertyKey latestLtDate = UtilsSettingsKeys.latestLtDate;
|
static const PropertyKey latestLtDate = UtilsSettingsKeys.latestLtDate;
|
||||||
static const PropertyKey ltDays = UtilsSettingsKeys.ltDays;
|
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);
|
PropertyKey.general("account_saas_user", tag: PropertyTags.account);
|
||||||
|
@Deprecated("use accountGuruUser instead")
|
||||||
|
static const PropertyKey accountSaasUser = accountGuruUser;
|
||||||
static const PropertyKey accountDevice =
|
static const PropertyKey accountDevice =
|
||||||
PropertyKey.general("account_device", tag: PropertyTags.account);
|
PropertyKey.general("account_device", tag: PropertyTags.account);
|
||||||
static const PropertyKey accountProfile =
|
static const PropertyKey accountProfile =
|
||||||
|
|
@ -33,11 +37,18 @@ class PropertyKeys {
|
||||||
static const PropertyKey anonymousSecretKey =
|
static const PropertyKey anonymousSecretKey =
|
||||||
PropertyKey.general("anonymous_secret_key", tag: PropertyTags.account);
|
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 isNoAds = PropertyKey.setting("no_ads", tag: PropertyTags.ads);
|
||||||
|
|
||||||
static const PropertyKey totalRevenue =
|
static const PropertyKey totalRevenue =
|
||||||
PropertyKey.general("total_revenue", tag: PropertyTags.financial);
|
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 =
|
static const PropertyKey userRewardedCount =
|
||||||
PropertyKey.general("user_rewarded_count", tag: PropertyTags.ads);
|
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 iapIgc = PropertyKey.general("iap_igc", tag: PropertyTags.iap);
|
||||||
static const PropertyKey noIapIgc = PropertyKey.general("no_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 =
|
static const PropertyKey admobConsentTestDeviceId =
|
||||||
PropertyKey.general("admob_consent_test_device_id", tag: PropertyTags.ads);
|
PropertyKey.general("admob_consent_test_device_id", tag: PropertyTags.ads);
|
||||||
static const PropertyKey admobConsentDebugGeography =
|
static const PropertyKey admobConsentDebugGeography =
|
||||||
|
|
@ -97,6 +110,9 @@ class PropertyKeys {
|
||||||
static const PropertyKey analyticsIdfa =
|
static const PropertyKey analyticsIdfa =
|
||||||
PropertyKey.general("analytics_idfa", tag: PropertyTags.analytics);
|
PropertyKey.general("analytics_idfa", tag: PropertyTags.analytics);
|
||||||
|
|
||||||
|
static const PropertyKey googleDma =
|
||||||
|
PropertyKey.general("google_dma_result", tag: PropertyTags.analytics);
|
||||||
|
|
||||||
static const PropertyKey currentIgcBalance =
|
static const PropertyKey currentIgcBalance =
|
||||||
PropertyKey.general("current_balance", tag: PropertyTags.igc);
|
PropertyKey.general("current_balance", tag: PropertyTags.igc);
|
||||||
static const PropertyKey currentIgcBalanceValidation =
|
static const PropertyKey currentIgcBalanceValidation =
|
||||||
|
|
@ -106,6 +122,10 @@ class PropertyKeys {
|
||||||
return PropertyKey.general("abtest_$key", tag: PropertyTags.guruAB);
|
return PropertyKey.general("abtest_$key", tag: PropertyTags.guruAB);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PropertyKey buildExperimentProperty(String key) {
|
||||||
|
return PropertyKey.general("exp_$key", tag: PropertyTags.guruExperiment);
|
||||||
|
}
|
||||||
|
|
||||||
static PropertyKey requestNotificationPermissionTimes =
|
static PropertyKey requestNotificationPermissionTimes =
|
||||||
const PropertyKey.general("request_notification_permission_times");
|
const PropertyKey.general("request_notification_permission_times");
|
||||||
|
|
||||||
|
|
@ -114,4 +134,9 @@ class PropertyKeys {
|
||||||
|
|
||||||
static PropertyKey deniedNotificationPermissionTimes =
|
static PropertyKey deniedNotificationPermissionTimes =
|
||||||
const PropertyKey.general("denied_notification_permission_times");
|
const PropertyKey.general("denied_notification_permission_times");
|
||||||
|
|
||||||
|
static PropertyKey buildAuthCredentialKey(AuthType authType) {
|
||||||
|
return PropertyKey.general("${getAuthName(authType)}_auth_credential",
|
||||||
|
tag: PropertyTags.account);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class PropertyTags {
|
||||||
static const String failedOrders = "failed_orders";
|
static const String failedOrders = "failed_orders";
|
||||||
static const String strategyAds = "StrategyAds";
|
static const String strategyAds = "StrategyAds";
|
||||||
static const String guruAB = "GuruAB";
|
static const String guruAB = "GuruAB";
|
||||||
|
static const String guruExperiment = "guru_experiment";
|
||||||
|
|
||||||
static const String iap = UtilsPropertyTags.iap;
|
static const String iap = UtilsPropertyTags.iap;
|
||||||
static const String ads = UtilsPropertyTags.ads;
|
static const String ads = UtilsPropertyTags.ads;
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@ mixin GlobalSettings {
|
||||||
final SettingBoolData isNoAds = SettingBoolData(PropertyKeys.isNoAds, defaultValue: false);
|
final SettingBoolData isNoAds = SettingBoolData(PropertyKeys.isNoAds, defaultValue: false);
|
||||||
|
|
||||||
final SettingIntData bestLevel = SettingIntData(PropertyKeys.bestLevel, defaultValue: 1);
|
final SettingIntData bestLevel = SettingIntData(PropertyKeys.bestLevel, defaultValue: 1);
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:math';
|
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/data/db/order_database.dart';
|
||||||
import 'package:guru_app/financial/manifest/manifest.dart';
|
import 'package:guru_app/financial/manifest/manifest.dart';
|
||||||
import 'package:guru_app/guru_app.dart';
|
import 'package:guru_app/guru_app.dart';
|
||||||
|
|
@ -11,6 +12,4 @@ part 'test_guru_app_creator.g.dart';
|
||||||
@guruSpecCreator
|
@guruSpecCreator
|
||||||
AppSpec createSampleAppSpec(String flavor) {
|
AppSpec createSampleAppSpec(String flavor) {
|
||||||
return _GuruSpecFactory.create(flavor);
|
return _GuruSpecFactory.create(flavor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,6 +24,8 @@ class _Guru_testRemoteConfigConstants {
|
||||||
};
|
};
|
||||||
|
|
||||||
static String getDefaultConfigString(String key) => _defaultConfigs[key];
|
static String getDefaultConfigString(String key) => _defaultConfigs[key];
|
||||||
|
|
||||||
|
static String getKey(String key) => key;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Guru_testAppSpec extends AppSpec {
|
class _Guru_testAppSpec extends AppSpec {
|
||||||
|
|
@ -34,6 +36,9 @@ class _Guru_testAppSpec extends AppSpec {
|
||||||
@override
|
@override
|
||||||
final appName = 'GuruApp';
|
final appName = 'GuruApp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final appCategory = AppCategory.app;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final flavor = 'guru_test';
|
final flavor = 'guru_test';
|
||||||
|
|
||||||
|
|
@ -95,6 +100,9 @@ class _Guru_testAppSpec extends AppSpec {
|
||||||
trackingNotificationPermissionPassLimitTimes: 10,
|
trackingNotificationPermissionPassLimitTimes: 10,
|
||||||
allowInterstitialAsAlternativeReward: false,
|
allowInterstitialAsAlternativeReward: false,
|
||||||
showInternalAdsWhenBannerUnavailable: true,
|
showInternalAdsWhenBannerUnavailable: true,
|
||||||
|
subscriptionRestoreGraceCount: 3,
|
||||||
|
fullscreenAdsMinInterval: 60,
|
||||||
|
enabledSyncAccountProfile: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -158,6 +166,13 @@ class _Guru_testAppSpec extends AppSpec {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final defaultRemoteConfig = _Guru_testRemoteConfigConstants._defaultConfigs;
|
final defaultRemoteConfig = _Guru_testRemoteConfigConstants._defaultConfigs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final localABTestExperiments = _GuruTestABTestExperiments.experiments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getRemoteConfigKey(String key) =>
|
||||||
|
_Guru_testRemoteConfigConstants.getKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Guru_testProducts {
|
class _Guru_testProducts {
|
||||||
|
|
@ -165,8 +180,6 @@ class _Guru_testProducts {
|
||||||
|
|
||||||
static final propRegExp = RegExp(r"^theme_(.*)_(.*)$");
|
static final propRegExp = RegExp(r"^theme_(.*)_(.*)$");
|
||||||
|
|
||||||
static final themeMulRegExp = RegExp(r"^theme_(.*)_(.*)$");
|
|
||||||
|
|
||||||
static const noAds = ProductId(
|
static const noAds = ProductId(
|
||||||
android: 'so.a.iap.noads.699',
|
android: 'so.a.iap.noads.699',
|
||||||
ios: 'so.i.iap.noads.699',
|
ios: 'so.i.iap.noads.699',
|
||||||
|
|
@ -314,8 +327,7 @@ class _Guru_testProducts {
|
||||||
buildCoin200Manifest,
|
buildCoin200Manifest,
|
||||||
buildStagePackManifest,
|
buildStagePackManifest,
|
||||||
buildPremiumWeekManifest,
|
buildPremiumWeekManifest,
|
||||||
buildPremiumYearManifest,
|
buildPremiumYearManifest
|
||||||
buildThemeMulManifest
|
|
||||||
];
|
];
|
||||||
|
|
||||||
static Future<Manifest?> buildNoAdsManifest(TransactionIntent intent) async {
|
static Future<Manifest?> buildNoAdsManifest(TransactionIntent intent) async {
|
||||||
|
|
@ -323,6 +335,7 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
|
|
@ -344,17 +357,21 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
};
|
};
|
||||||
final details = <Details>[];
|
final details = <Details>[];
|
||||||
details.add(Details.define(
|
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(
|
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(
|
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);
|
return Manifest('no_ads', extras: extras, details: details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,6 +396,7 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
|
|
@ -413,17 +431,20 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
};
|
};
|
||||||
final details = <Details>[];
|
final details = <Details>[];
|
||||||
details.add(Details.define(
|
details.add(Details.define(
|
||||||
'prop', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)
|
'prop', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1,
|
||||||
..setString('prop_id', matches.first.group(1)!));
|
sku: '${matches.first.group(1)!}_${matches.first.group(2)!}')
|
||||||
|
..setString('theme_id', matches.first.group(1)!));
|
||||||
details.add(Details.define(
|
details.add(Details.define(
|
||||||
'pc', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)
|
'pc', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1,
|
||||||
..setString('prop_id', matches.first.group(2)!));
|
sku: 'pc')
|
||||||
|
..setString('theme_id', matches.first.group(2)!));
|
||||||
return Manifest('prop', extras: extras, details: details);
|
return Manifest('prop', extras: extras, details: details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -444,12 +465,13 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
};
|
};
|
||||||
final details = <Details>[];
|
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);
|
return Manifest('no_ads', extras: extras, details: details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,13 +489,15 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
};
|
};
|
||||||
final details = <Details>[];
|
final details = <Details>[];
|
||||||
details.add(Details.define(
|
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);
|
return Manifest('coin', extras: extras, details: details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,13 +507,15 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
};
|
};
|
||||||
final details = <Details>[];
|
final details = <Details>[];
|
||||||
details.add(Details.define(
|
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));
|
..setInt('stage', 1));
|
||||||
return Manifest('stage_1', extras: extras, details: details);
|
return Manifest('stage_1', extras: extras, details: details);
|
||||||
}
|
}
|
||||||
|
|
@ -500,6 +526,7 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
|
|
@ -510,7 +537,8 @@ class _Guru_testProducts {
|
||||||
}
|
}
|
||||||
final details = <Details>[];
|
final details = <Details>[];
|
||||||
details.add(Details.define(
|
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);
|
return Manifest('sub', extras: extras, details: details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,6 +548,7 @@ class _Guru_testProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
|
|
@ -529,51 +558,54 @@ class _Guru_testProducts {
|
||||||
extras[ExtraReservedField.offerId] = intent.productId.offerId;
|
extras[ExtraReservedField.offerId] = intent.productId.offerId;
|
||||||
}
|
}
|
||||||
final details = <Details>[];
|
final details = <Details>[];
|
||||||
details.add(Details.define('igc',
|
details.add(Details.define(
|
||||||
intent.sales ? max(16000, (intent.rate * 16000).toInt()) : 16000));
|
'igc', intent.sales ? max(16000, (intent.rate * 16000).toInt()) : 16000,
|
||||||
|
sku: 'igc'));
|
||||||
return Manifest('sub', extras: extras, details: details);
|
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 =>
|
static Set<ProductId> get iapIds =>
|
||||||
{...oneOffChargeIapIds, ...subscriptionsIapIds};
|
{...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 {
|
class _SpiderRemoteConfigConstants {
|
||||||
static const iadsConfig = 'iads_config';
|
static const iadsConfig = 'iads_config';
|
||||||
|
|
||||||
|
|
@ -589,6 +621,8 @@ class _SpiderRemoteConfigConstants {
|
||||||
};
|
};
|
||||||
|
|
||||||
static String getDefaultConfigString(String key) => _defaultConfigs[key];
|
static String getDefaultConfigString(String key) => _defaultConfigs[key];
|
||||||
|
|
||||||
|
static String getKey(String key) => key;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SpiderAppSpec extends AppSpec {
|
class _SpiderAppSpec extends AppSpec {
|
||||||
|
|
@ -599,6 +633,9 @@ class _SpiderAppSpec extends AppSpec {
|
||||||
@override
|
@override
|
||||||
final appName = 'Spider';
|
final appName = 'Spider';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final appCategory = AppCategory.game;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final flavor = 'Spider';
|
final flavor = 'Spider';
|
||||||
|
|
||||||
|
|
@ -680,6 +717,13 @@ class _SpiderAppSpec extends AppSpec {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final defaultRemoteConfig = _SpiderRemoteConfigConstants._defaultConfigs;
|
final defaultRemoteConfig = _SpiderRemoteConfigConstants._defaultConfigs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final localABTestExperiments = _SpiderABTestExperiments.experiments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getRemoteConfigKey(String key) =>
|
||||||
|
_SpiderRemoteConfigConstants.getKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SpiderProducts {
|
class _SpiderProducts {
|
||||||
|
|
@ -726,6 +770,7 @@ class _SpiderProducts {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final extras = <String, dynamic>{
|
final extras = <String, dynamic>{
|
||||||
|
ExtraReservedField.contentId: intent.productId.sku,
|
||||||
ExtraReservedField.scene: intent.scene,
|
ExtraReservedField.scene: intent.scene,
|
||||||
ExtraReservedField.rate: intent.rate,
|
ExtraReservedField.rate: intent.rate,
|
||||||
ExtraReservedField.sales: intent.sales,
|
ExtraReservedField.sales: intent.sales,
|
||||||
|
|
@ -733,7 +778,8 @@ class _SpiderProducts {
|
||||||
};
|
};
|
||||||
final details = <Details>[];
|
final details = <Details>[];
|
||||||
details.add(Details.define(
|
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)!}',
|
return Manifest('${matches.first.group(1)!}',
|
||||||
extras: extras, details: details);
|
extras: extras, details: details);
|
||||||
}
|
}
|
||||||
|
|
@ -754,6 +800,10 @@ class _SpiderProducts {
|
||||||
{...oneOffChargeIapIds, ...subscriptionsIapIds};
|
{...oneOffChargeIapIds, ...subscriptionsIapIds};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SpiderABTestExperiments {
|
||||||
|
static final experiments = <String, ABTestExperiment>{};
|
||||||
|
}
|
||||||
|
|
||||||
class RemoteConfigConstants {
|
class RemoteConfigConstants {
|
||||||
static const iadsConfig = 'iads_config';
|
static const iadsConfig = 'iads_config';
|
||||||
|
|
||||||
|
|
@ -764,6 +814,18 @@ class RemoteConfigConstants {
|
||||||
static const analyticsConfig = 'analytics_config';
|
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 {
|
class ProductIds {
|
||||||
static ProductId get noAds {
|
static ProductId get noAds {
|
||||||
if (GuruApp.instance.flavor == 'guru_test') {
|
if (GuruApp.instance.flavor == 'guru_test') {
|
||||||
|
|
@ -872,16 +934,6 @@ class ProductIds {
|
||||||
return ProductId.invalid;
|
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 {
|
static Set<ProductId> get noAdsCapIds {
|
||||||
if (GuruApp.instance.flavor == 'guru_test') {
|
if (GuruApp.instance.flavor == 'guru_test') {
|
||||||
return _Guru_testProducts.noAdsCapIds;
|
return _Guru_testProducts.noAdsCapIds;
|
||||||
|
|
@ -921,10 +973,6 @@ class ProductCategory {
|
||||||
static theme(String themeId) {
|
static theme(String themeId) {
|
||||||
"theme_${themeId}";
|
"theme_${themeId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
static themeMul(String category) {
|
|
||||||
"${category}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GuruSpecFactory {
|
class _GuruSpecFactory {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
build/
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
* TODO: Describe initial release.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
TODO: Add your license here.
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!--
|
||||||
|
This README describes the package. If you publish this package to pub.dev,
|
||||||
|
this README's contents appear on the landing page for your package.
|
||||||
|
|
||||||
|
For information about how to write a good package README, see the guide for
|
||||||
|
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||||
|
|
||||||
|
For general information about developing packages, see the Dart guide for
|
||||||
|
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||||
|
and the Flutter guide for
|
||||||
|
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||||
|
-->
|
||||||
|
|
||||||
|
TODO: Put a short description of the package here that helps potential users
|
||||||
|
know whether this package might be useful for them.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
TODO: List prerequisites and provide or point to information on how to
|
||||||
|
start using the package.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
TODO: Include short and useful examples for package users. Add longer examples
|
||||||
|
to `/example` folder.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const like = 'sample';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
||||||
|
TODO: Tell users more about the package: where to find more information, how to
|
||||||
|
contribute to the package, how to file issues, what response they can expect
|
||||||
|
from the package authors, and more.
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
library guru_fiam;
|
||||||
|
|
||||||
|
/// A Calculator.
|
||||||
|
class Calculator {
|
||||||
|
/// Returns [value] plus 1.
|
||||||
|
int addOne(int value) => value + 1;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
name: guru_fiam
|
||||||
|
description: "A new Flutter project."
|
||||||
|
version: 3.0.0
|
||||||
|
homepage:
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.2.3 <4.0.0'
|
||||||
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
firebase_in_app_messaging: 0.7.4+8
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# To add assets to your package, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
#
|
||||||
|
# For details regarding assets in packages, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
#
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|
||||||
|
# To add custom fonts to your package, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts in packages, see
|
||||||
|
# https://flutter.dev/custom-fonts/#from-packages
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:guru_fiam/guru_fiam.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('adds one to input values', () {
|
||||||
|
final calculator = Calculator();
|
||||||
|
expect(calculator.addOne(2), 3);
|
||||||
|
expect(calculator.addOne(-7), -6);
|
||||||
|
expect(calculator.addOne(0), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ class _RootPackage extends RootPackage {
|
||||||
Log.isDebug = kDebugMode;
|
Log.isDebug = kDebugMode;
|
||||||
|
|
||||||
await DebugSettings.instance.refresh();
|
await DebugSettings.instance.refresh();
|
||||||
|
|
||||||
Initializer.initialPath = AppPages.initialPath;
|
Initializer.initialPath = AppPages.initialPath;
|
||||||
|
|
||||||
RouteCenter.initialize(routeMatchers: [
|
RouteCenter.initialize(routeMatchers: [
|
||||||
|
|
@ -42,23 +42,34 @@ class _RootPackage extends RootPackage {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// TODO: implement localizationsDelegates
|
// TODO: implement localizationsDelegates
|
||||||
Iterable<LocalizationsDelegate> get localizationsDelegates => const [
|
Iterable<LocalizationsDelegate> get localizationsDelegates => const [
|
||||||
// Built-in localization of basic text for Material widgets
|
// Built-in localization of basic text for Material widgets
|
||||||
GlobalMaterialLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
// Built-in localization for text direction LTR/RTL
|
// Built-in localization for text direction LTR/RTL
|
||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
// Built-in localization of basic text for Cupertino widgets
|
// Built-in localization of basic text for Cupertino widgets
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// TODO: implement supportedLocales
|
// TODO: implement supportedLocales
|
||||||
Iterable<Locale> get supportedLocales => throw UnimplementedError();
|
Iterable<Locale> get supportedLocales => throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ComplianceProtocol implements IGuruSdkComplianceProtocol {
|
||||||
|
@override
|
||||||
|
int getCurrentLevel() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getLevelName() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
class Initializer {
|
class Initializer {
|
||||||
// final TransactionService transactionService;
|
// final TransactionService transactionService;
|
||||||
|
|
@ -79,7 +90,10 @@ class Initializer {
|
||||||
static AppEnv _buildAppEnv({String flavor = ""}) {
|
static AppEnv _buildAppEnv({String flavor = ""}) {
|
||||||
final rootPackage = _RootPackage();
|
final rootPackage = _RootPackage();
|
||||||
|
|
||||||
return AppEnv(spec: createAppSpec(flavor), package: rootPackage);
|
return AppEnv(
|
||||||
|
spec: createAppSpec(flavor),
|
||||||
|
package: rootPackage,
|
||||||
|
complianceProtocol: ComplianceProtocol());
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future ensureInitialized() async {
|
static Future ensureInitialized() async {
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,6 @@ class _GuruSpecFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Flavors {}
|
class Flavors {
|
||||||
|
static const String classic = "classic";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ packages:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.12"
|
version: "1.3.16"
|
||||||
adjust_sdk:
|
adjust_sdk:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: adjust_sdk
|
name: adjust_sdk
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.33.0"
|
version: "4.36.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -175,21 +175,21 @@ packages:
|
||||||
name: cloud_firestore
|
name: cloud_firestore
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.1"
|
version: "4.13.6"
|
||||||
cloud_firestore_platform_interface:
|
cloud_firestore_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cloud_firestore_platform_interface
|
name: cloud_firestore_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.10.1"
|
version: "6.0.10"
|
||||||
cloud_firestore_web:
|
cloud_firestore_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cloud_firestore_web
|
name: cloud_firestore_web
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.8.10"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -265,7 +265,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "packages/design"
|
path: "packages/design"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||||
url: "git@github.com:castbox/guru_ui.git"
|
url: "git@github.com:castbox/guru_ui.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
|
@ -274,7 +274,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "packages/design_generator"
|
path: "packages/design_generator"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||||
url: "git@github.com:castbox/guru_ui.git"
|
url: "git@github.com:castbox/guru_ui.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
|
@ -283,7 +283,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "packages/design_spec"
|
path: "packages/design_spec"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||||
url: "git@github.com:castbox/guru_ui.git"
|
url: "git@github.com:castbox/guru_ui.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
|
@ -300,7 +300,7 @@ packages:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.2.2"
|
version: "9.1.1"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -356,147 +356,147 @@ packages:
|
||||||
name: firebase_analytics
|
name: firebase_analytics
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.0"
|
version: "10.7.4"
|
||||||
firebase_analytics_platform_interface:
|
firebase_analytics_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_analytics_platform_interface
|
name: firebase_analytics_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.17"
|
version: "3.9.0"
|
||||||
firebase_analytics_web:
|
firebase_analytics_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_analytics_web
|
name: firebase_analytics_web
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1+8"
|
version: "0.5.5+12"
|
||||||
firebase_auth:
|
firebase_auth:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_auth
|
name: firebase_auth
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.4"
|
version: "4.15.3"
|
||||||
firebase_auth_platform_interface:
|
firebase_auth_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_auth_platform_interface
|
name: firebase_auth_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.11.7"
|
version: "7.0.9"
|
||||||
firebase_auth_web:
|
firebase_auth_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_auth_web
|
name: firebase_auth_web
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.4"
|
version: "5.8.13"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.24.2"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_platform_interface
|
name: firebase_core_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.0"
|
version: "5.0.0"
|
||||||
firebase_core_web:
|
firebase_core_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_web
|
name: firebase_core_web
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.10.0"
|
||||||
firebase_crashlytics:
|
firebase_crashlytics:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_crashlytics
|
name: firebase_crashlytics
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.4.8"
|
||||||
firebase_crashlytics_platform_interface:
|
firebase_crashlytics_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_crashlytics_platform_interface
|
name: firebase_crashlytics_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.11"
|
version: "3.6.16"
|
||||||
firebase_dynamic_links:
|
firebase_dynamic_links:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_dynamic_links
|
name: firebase_dynamic_links
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.11"
|
version: "5.4.8"
|
||||||
firebase_dynamic_links_platform_interface:
|
firebase_dynamic_links_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_dynamic_links_platform_interface
|
name: firebase_dynamic_links_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.3+26"
|
version: "0.2.6+16"
|
||||||
firebase_in_app_messaging:
|
firebase_in_app_messaging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_in_app_messaging
|
name: firebase_in_app_messaging
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0+10"
|
version: "0.7.4+8"
|
||||||
firebase_in_app_messaging_platform_interface:
|
firebase_in_app_messaging_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_in_app_messaging_platform_interface
|
name: firebase_in_app_messaging_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+29"
|
version: "0.2.4+16"
|
||||||
firebase_messaging:
|
firebase_messaging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging
|
name: firebase_messaging
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.1"
|
version: "14.7.9"
|
||||||
firebase_messaging_platform_interface:
|
firebase_messaging_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging_platform_interface
|
name: firebase_messaging_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.10"
|
version: "4.5.18"
|
||||||
firebase_messaging_web:
|
firebase_messaging_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging_web
|
name: firebase_messaging_web
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.11"
|
version: "3.5.18"
|
||||||
firebase_remote_config:
|
firebase_remote_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_remote_config
|
name: firebase_remote_config
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "4.3.8"
|
||||||
firebase_remote_config_platform_interface:
|
firebase_remote_config_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_remote_config_platform_interface
|
name: firebase_remote_config_platform_interface
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.29"
|
version: "1.4.16"
|
||||||
firebase_remote_config_web:
|
firebase_remote_config_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_remote_config_web
|
name: firebase_remote_config_web
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.18"
|
version: "1.4.16"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -504,11 +504,25 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
|
flame:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flame
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_blurhash:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -605,19 +619,17 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v2.3.1"
|
ref: "v2.3.4"
|
||||||
resolved-ref: e4438b7ece793a85da477b685e60c79981be281a
|
resolved-ref: "804fd22ddc1fc31acecdf72e936dabc0193379c5"
|
||||||
url: "git@github.com:castbox/guru_analytics_flutter.git"
|
url: "git@github.com:castbox/guru_analytics_flutter.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
guru_app:
|
guru_app:
|
||||||
dependency: "direct dev"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "../../.."
|
||||||
ref: "v2.3.0"
|
relative: true
|
||||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
source: path
|
||||||
url: "git@github.com:castbox/guru_app.git"
|
|
||||||
source: git
|
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
guru_applifecycle_flutter:
|
guru_applifecycle_flutter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|
@ -633,7 +645,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v2.3.8"
|
ref: "v2.3.8"
|
||||||
resolved-ref: "6fb5191d4cffc6a5728df7ee8af793fcc27fd081"
|
resolved-ref: "4cb520a2f9bea14300b0d2b452e183bcc42779f9"
|
||||||
url: "git@github.com:castbox/guru_applovin_flutter.git"
|
url: "git@github.com:castbox/guru_applovin_flutter.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
|
@ -649,7 +661,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "plugins/guru_navigator"
|
path: "plugins/guru_navigator"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||||
url: "git@github.com:castbox/guru_app.git"
|
url: "git@github.com:castbox/guru_app.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
|
@ -658,7 +670,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "plugins/guru_platform_data"
|
path: "plugins/guru_platform_data"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||||
url: "git@github.com:castbox/guru_app.git"
|
url: "git@github.com:castbox/guru_app.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
|
@ -667,34 +679,30 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "packages/guru_popup"
|
path: "packages/guru_popup"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||||
url: "git@github.com:castbox/guru_ui.git"
|
url: "git@github.com:castbox/guru_ui.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
guru_spec:
|
guru_spec:
|
||||||
dependency: "direct dev"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "packages/guru_spec"
|
path: "../../guru_spec"
|
||||||
ref: "v2.3.0"
|
relative: true
|
||||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
source: path
|
||||||
url: "git@github.com:castbox/guru_app.git"
|
|
||||||
source: git
|
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
guru_utils:
|
guru_utils:
|
||||||
dependency: "direct dev"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "packages/guru_utils"
|
path: "../../guru_utils"
|
||||||
ref: "v2.3.0"
|
relative: true
|
||||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
source: path
|
||||||
url: "git@github.com:castbox/guru_app.git"
|
|
||||||
source: git
|
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
guru_widgets:
|
guru_widgets:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
path: "packages/guru_widgets"
|
path: "packages/guru_widgets"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b"
|
resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b"
|
||||||
url: "git@github.com:castbox/guru_ui.git"
|
url: "git@github.com:castbox/guru_ui.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
|
@ -866,6 +874,13 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
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:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -997,7 +1012,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "plugins/persistent"
|
path: "plugins/persistent"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||||
url: "git@github.com:castbox/guru_app.git"
|
url: "git@github.com:castbox/guru_app.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
|
@ -1109,7 +1124,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "plugins/soundpool"
|
path: "plugins/soundpool"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||||
url: "git@github.com:castbox/guru_app.git"
|
url: "git@github.com:castbox/guru_app.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
|
@ -1321,7 +1336,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "plugins/vibration"
|
path: "plugins/vibration"
|
||||||
ref: "v2.3.0"
|
ref: "v2.3.0"
|
||||||
resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2"
|
resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7
|
||||||
url: "git@github.com:castbox/guru_app.git"
|
url: "git@github.com:castbox/guru_app.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.7.5"
|
version: "1.7.5"
|
||||||
|
|
@ -1388,6 +1403,13 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.4"
|
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:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,16 @@ dev_dependencies:
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
# The following section is specific to Flutter packages.
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
guru_app:
|
||||||
|
path: ../../../
|
||||||
|
|
||||||
|
guru_utils:
|
||||||
|
path: ../../guru_utils
|
||||||
|
|
||||||
|
guru_spec:
|
||||||
|
path: ../../guru_spec
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
# The following line ensures that the Material Icons font is
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ packages:
|
||||||
sha256: "73ff438fe46028f0e19f55da18b6ddc6906ab750562cd7d9ffab77ff8c0c4307"
|
sha256: "73ff438fe46028f0e19f55da18b6ddc6906ab750562cd7d9ffab77ff8c0c4307"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.0.10"
|
||||||
cloud_firestore_web:
|
cloud_firestore_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -216,7 +216,7 @@ packages:
|
||||||
sha256: "232e45e95970d3a6baab8f50f9c3a6e2838d145d9d91ec9a7392837c44296397"
|
sha256: "232e45e95970d3a6baab8f50f9c3a6e2838d145d9d91ec9a7392837c44296397"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.0"
|
version: "3.8.10"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -578,6 +578,13 @@ packages:
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_animate:
|
flutter_animate:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_animate
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.4.0"
|
||||||
|
flutter_blurhash:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_animate
|
name: flutter_animate
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
build/
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
* TODO: Describe initial release.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
TODO: Add your license here.
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!--
|
||||||
|
This README describes the package. If you publish this package to pub.dev,
|
||||||
|
this README's contents appear on the landing page for your package.
|
||||||
|
|
||||||
|
For information about how to write a good package README, see the guide for
|
||||||
|
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||||
|
|
||||||
|
For general information about developing packages, see the Dart guide for
|
||||||
|
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||||
|
and the Flutter guide for
|
||||||
|
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||||
|
-->
|
||||||
|
|
||||||
|
TODO: Put a short description of the package here that helps potential users
|
||||||
|
know whether this package might be useful for them.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
TODO: List prerequisites and provide or point to information on how to
|
||||||
|
start using the package.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
TODO: Include short and useful examples for package users. Add longer examples
|
||||||
|
to `/example` folder.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const like = 'sample';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
||||||
|
TODO: Tell users more about the package: where to find more information, how to
|
||||||
|
contribute to the package, how to file issues, what response they can expect
|
||||||
|
from the package authors, and more.
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# example
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
// This widget is the root of your application.
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Flutter Demo',
|
||||||
|
theme: ThemeData(
|
||||||
|
// This is the theme of your application.
|
||||||
|
//
|
||||||
|
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||||
|
// the application has a purple toolbar. Then, without quitting the app,
|
||||||
|
// try changing the seedColor in the colorScheme below to Colors.green
|
||||||
|
// and then invoke "hot reload" (save your changes or press the "hot
|
||||||
|
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||||
|
// the command line to start the app).
|
||||||
|
//
|
||||||
|
// Notice that the counter didn't reset back to zero; the application
|
||||||
|
// state is not lost during the reload. To reset the state, use hot
|
||||||
|
// restart instead.
|
||||||
|
//
|
||||||
|
// This works for code too, not just values: Most code changes can be
|
||||||
|
// tested with just a hot reload.
|
||||||
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyHomePage extends StatefulWidget {
|
||||||
|
const MyHomePage({super.key, required this.title});
|
||||||
|
|
||||||
|
// This widget is the home page of your application. It is stateful, meaning
|
||||||
|
// that it has a State object (defined below) that contains fields that affect
|
||||||
|
// how it looks.
|
||||||
|
|
||||||
|
// This class is the configuration for the state. It holds the values (in this
|
||||||
|
// case the title) provided by the parent (in this case the App widget) and
|
||||||
|
// used by the build method of the State. Fields in a Widget subclass are
|
||||||
|
// always marked "final".
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyHomePage> createState() => _MyHomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
int _counter = 0;
|
||||||
|
|
||||||
|
void _incrementCounter() {
|
||||||
|
setState(() {
|
||||||
|
// This call to setState tells the Flutter framework that something has
|
||||||
|
// changed in this State, which causes it to rerun the build method below
|
||||||
|
// so that the display can reflect the updated values. If we changed
|
||||||
|
// _counter without calling setState(), then the build method would not be
|
||||||
|
// called again, and so nothing would appear to happen.
|
||||||
|
_counter++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// This method is rerun every time setState is called, for instance as done
|
||||||
|
// by the _incrementCounter method above.
|
||||||
|
//
|
||||||
|
// The Flutter framework has been optimized to make rerunning build methods
|
||||||
|
// fast, so that you can just rebuild anything that needs updating rather
|
||||||
|
// than having to individually change instances of widgets.
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
// TRY THIS: Try changing the color here to a specific color (to
|
||||||
|
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||||
|
// change color while the other colors stay the same.
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||||
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
|
// the App.build method, and use it to set our appbar title.
|
||||||
|
title: Text(widget.title),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
// Center is a layout widget. It takes a single child and positions it
|
||||||
|
// in the middle of the parent.
|
||||||
|
child: Column(
|
||||||
|
// Column is also a layout widget. It takes a list of children and
|
||||||
|
// arranges them vertically. By default, it sizes itself to fit its
|
||||||
|
// children horizontally, and tries to be as tall as its parent.
|
||||||
|
//
|
||||||
|
// Column has various properties to control how it sizes itself and
|
||||||
|
// how it positions its children. Here we use mainAxisAlignment to
|
||||||
|
// center the children vertically; the main axis here is the vertical
|
||||||
|
// axis because Columns are vertical (the cross axis would be
|
||||||
|
// horizontal).
|
||||||
|
//
|
||||||
|
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||||
|
// action in the IDE, or press "p" in the console), to see the
|
||||||
|
// wireframe for each widget.
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text(
|
||||||
|
'You have pushed the button this many times:',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$_counter',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: _incrementCounter,
|
||||||
|
tooltip: 'Increment',
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.0"
|
||||||
|
cupertino_icons:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cupertino_icons
|
||||||
|
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.16"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.8.3"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.99"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.0"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.1"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.2.3 <4.0.0"
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
name: example
|
||||||
|
description: "A new Flutter project."
|
||||||
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.2.3 <4.0.0'
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
cupertino_icons: ^1.0.2
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
# package. See that file for information about deactivating specific lint
|
||||||
|
# rules and activating additional ones.
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/custom-fonts/#from-packages
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// This is a basic Flutter widget test.
|
||||||
|
//
|
||||||
|
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||||
|
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||||
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:example/main.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
|
// Build our app and trigger a frame.
|
||||||
|
await tester.pumpWidget(const MyApp());
|
||||||
|
|
||||||
|
// Verify that our counter starts at 0.
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
expect(find.text('1'), findsNothing);
|
||||||
|
|
||||||
|
// Tap the '+' icon and trigger a frame.
|
||||||
|
await tester.tap(find.byIcon(Icons.add));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Verify that our counter has incremented.
|
||||||
|
expect(find.text('0'), findsNothing);
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
let globalLogger;
|
||||||
|
|
||||||
|
class EventLogger {
|
||||||
|
appId = '';
|
||||||
|
deviceInfo = {};
|
||||||
|
version = 10
|
||||||
|
deviceStr = '';
|
||||||
|
events = [];
|
||||||
|
info = {}; //用户信息
|
||||||
|
|
||||||
|
|
||||||
|
constructor({ appId, deviceInfo, info }) {
|
||||||
|
this.appId = appId;
|
||||||
|
let u = navigator.userAgent;
|
||||||
|
let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;
|
||||||
|
let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
|
||||||
|
let tzOffset = new Date().valueOf();
|
||||||
|
this.deviceInfo = {
|
||||||
|
platform: isAndroid ? 'ANDROID' : 'IOS',
|
||||||
|
country: navigator.languages[1],
|
||||||
|
tzOffset,
|
||||||
|
deviceType: 'other',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
screenH: document.body.clientHeight,
|
||||||
|
screenW: document.body.clientWidth,
|
||||||
|
osVersion: '',
|
||||||
|
language: navigator.language,
|
||||||
|
...deviceInfo
|
||||||
|
};
|
||||||
|
for (let key in this.deviceInfo) this.deviceStr += `${key}=${this.deviceInfo[key]};`;
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getHeaders() {
|
||||||
|
let headers = new Headers();
|
||||||
|
headers.append('X-APP-ID', this.appId);
|
||||||
|
headers.append('X-DEVICE-INFO', this.deviceStr)
|
||||||
|
headers.append('content-type', 'application/json');
|
||||||
|
headers.append('Content-Encoding', 'gzip');
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
zip(str) {
|
||||||
|
const binaryString = pako.gzip(str)
|
||||||
|
return binaryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(event) {
|
||||||
|
event = {
|
||||||
|
...event,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
info: this.info
|
||||||
|
};
|
||||||
|
this.events.push(event);
|
||||||
|
if (this.events.length > 0) this.logEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async logEvent() {
|
||||||
|
const requestBody = {
|
||||||
|
version: this.version,
|
||||||
|
deviceInfo: this.deviceInfo,
|
||||||
|
events: [...this.events]
|
||||||
|
};
|
||||||
|
const bodyStr = JSON.stringify(requestBody);
|
||||||
|
const gzippedStr = this.zip(bodyStr);
|
||||||
|
const config = {
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
body: gzippedStr,
|
||||||
|
method: 'POST',
|
||||||
|
};
|
||||||
|
await fetch('https://collect.saas.castbox.fm/event', config);
|
||||||
|
this.events = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* appInfo {
|
||||||
|
* appId
|
||||||
|
* deviceInfo
|
||||||
|
* version,
|
||||||
|
* info
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
function initEventLogger(appInfo) {
|
||||||
|
globalLogger = new EventLogger(JSON.parse(appInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
function castboxLogEvent(eventName, param, properties) {
|
||||||
|
if (globalLogger) {
|
||||||
|
globalLogger.log({
|
||||||
|
event: eventName,
|
||||||
|
param: JSON.parse(param),
|
||||||
|
properties: JSON.parse(properties)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 917 B |
|
|
@ -0,0 +1,331 @@
|
||||||
|
/**
|
||||||
|
* 获取facebook 用户基本信息
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getFBProfile() {
|
||||||
|
const photo = FBInstant.player.getPhoto()
|
||||||
|
const name = FBInstant.player.getName()
|
||||||
|
const id = FBInstant.player.getID()
|
||||||
|
const contextType = FBInstant.context.getType()
|
||||||
|
return JSON.stringify({ photo, name, id, contextType })
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFBProfileWithUid(success, error) {
|
||||||
|
const photo = FBInstant.player.getPhoto()
|
||||||
|
const name = FBInstant.player.getName()
|
||||||
|
const id = FBInstant.player.getID()
|
||||||
|
const contextType = FBInstant.context.getType()
|
||||||
|
FBInstant.player.getASIDAsync().then((userId) => {
|
||||||
|
success(JSON.stringify({ photo, name, id, contextType, userId }))
|
||||||
|
}).catch(err => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntryPointData() {
|
||||||
|
const data = FBInstant.getEntryPointData();
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocale() {
|
||||||
|
return FBInstant.getLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
//IOS, Android, WEB
|
||||||
|
function getPlatform() {
|
||||||
|
return FBInstant.getPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享游戏
|
||||||
|
function shareGame(data, success, error) {
|
||||||
|
FBInstant.shareAsync(JSON.parse(data))
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function logEvent(eventName, valueToSum, parameters) {
|
||||||
|
FBInstant.logEvent(eventName, valueToSum, JSON.parse(parameters))
|
||||||
|
}
|
||||||
|
|
||||||
|
function inviteUser(data, success, error) {
|
||||||
|
FBInstant.inviteAsync(JSON.parse(data))
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测当前支持广告类型 ["getInterstitialAdAsync", "getRewardedVideoAsync"]
|
||||||
|
function checkSupportedAds() {
|
||||||
|
let supportedAPIs = FBInstant.getSupportedAPIs()
|
||||||
|
return JSON.stringify(supportedAPIs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载,展示插屏广告
|
||||||
|
let preloadedInterstitial = null
|
||||||
|
function loadInterstitial(id, success, error) {
|
||||||
|
//插屏预载
|
||||||
|
FBInstant.getInterstitialAdAsync(id)
|
||||||
|
.then((res) => {
|
||||||
|
preloadedInterstitial = res
|
||||||
|
return preloadedInterstitial.loadAsync
|
||||||
|
}).then(res => {
|
||||||
|
success()
|
||||||
|
}).catch((err) => {
|
||||||
|
error(err.message);
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function showInterstitial(success, error) {
|
||||||
|
if (preloadedInterstitial == null) {
|
||||||
|
return error("showInterstitial preloadedInterstitial is null")
|
||||||
|
}
|
||||||
|
//插屏播放
|
||||||
|
preloadedInterstitial
|
||||||
|
.showAsync()
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 加载,展示激励视频
|
||||||
|
let preloadedRewardedVideo = null
|
||||||
|
function loadRewarded(id, success, error) {
|
||||||
|
//激励视频预载
|
||||||
|
FBInstant.getRewardedVideoAsync(id)
|
||||||
|
.then((res) => {
|
||||||
|
preloadedRewardedVideo = res
|
||||||
|
return preloadedRewardedVideo.loadAsync
|
||||||
|
}).then(res => {
|
||||||
|
success()
|
||||||
|
}).catch((err) => {
|
||||||
|
error(err.message);
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function showRewarded(success, error) {
|
||||||
|
if (preloadedRewardedVideo == null) {
|
||||||
|
return error("showInterstitial preloadedRewardedVideo is null")
|
||||||
|
}
|
||||||
|
//插屏播放
|
||||||
|
preloadedRewardedVideo
|
||||||
|
.showAsync()
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 展示,隐藏banner广告
|
||||||
|
function showBannerAds(id, success, error) {
|
||||||
|
FBInstant.loadBannerAdAsync(id)
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function hideBannerAds(id, success, error) {
|
||||||
|
FBInstant.hideBannerAdAsync(id)
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// tournament 相关
|
||||||
|
|
||||||
|
function createTournament(initialScore, config, data, success, error) {
|
||||||
|
/**
|
||||||
|
* initialScore: number
|
||||||
|
* config: {
|
||||||
|
* title?: string
|
||||||
|
* image?: base64,
|
||||||
|
* sortOrder?: "HIGHER_IS_BETTER"| "LOWER_IS_BETTER"
|
||||||
|
* scoreFormat?: "NUMERIC" | "TIME"
|
||||||
|
* endTime?: unix timestamp (seconds)
|
||||||
|
* }
|
||||||
|
* data: {} customized
|
||||||
|
*/
|
||||||
|
FBInstant.tournament
|
||||||
|
.createAsync({
|
||||||
|
initialScore,
|
||||||
|
config: JSON.parse(config),
|
||||||
|
data: JSON.parse(data),
|
||||||
|
})
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function postTournamentScore(id, score, success, error) {
|
||||||
|
FBInstant.tournament
|
||||||
|
.joinAsync(id)
|
||||||
|
.then(function () {
|
||||||
|
return FBInstant.tournament.postScoreAsync(score);
|
||||||
|
})
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareTournament(score, data, success, error) {
|
||||||
|
//data: customized {}
|
||||||
|
FBInstant.tournament
|
||||||
|
.shareAsync({
|
||||||
|
score,
|
||||||
|
data: JSON.parse(data),
|
||||||
|
})
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTournament(success, error) {
|
||||||
|
FBInstant.getTournamentAsync()
|
||||||
|
.then(function (tournament) {
|
||||||
|
success(
|
||||||
|
JSON.stringify({
|
||||||
|
id: tournament.getContextID(),
|
||||||
|
endTime: tournament.getEndTime(),
|
||||||
|
title: tournament.getTitle(),
|
||||||
|
payLoad: tournament.getPayload(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// player 相关
|
||||||
|
|
||||||
|
function fbSetData(data) {
|
||||||
|
FBInstant.player.setDataAsync(JSON.parse(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function fbGetData(keys, success, error) {
|
||||||
|
FBInstant.player
|
||||||
|
.getDataAsync(JSON.parse(keys))
|
||||||
|
.then((res) => {
|
||||||
|
success(JSON.stringify(res))
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeBot(success, error) {
|
||||||
|
FBInstant.player
|
||||||
|
.canSubscribeBotAsync()
|
||||||
|
.then((can_subscribe) => {
|
||||||
|
if (can_subscribe) {
|
||||||
|
return FBInstant.player.subscribeBotAsync()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConnectedPlayersAsync(success, error) {
|
||||||
|
//Fetches an array of ConnectedPlayer objects containing information about active players (people who played the game in the last 90 days) that are connected to the current player.
|
||||||
|
FBInstant.player.getConnectedPlayersAsync()
|
||||||
|
.then(function (res) {
|
||||||
|
var players = res.map(function (player) {
|
||||||
|
return {
|
||||||
|
player_id: player.getID(),
|
||||||
|
name: player.getName(),
|
||||||
|
picUrl: player.getPhoto()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
success(JSON.stringify(players))
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// context 相关
|
||||||
|
|
||||||
|
function getContextInfo() {
|
||||||
|
return JSON.stringify({
|
||||||
|
id: FBInstant.context.getID(),
|
||||||
|
type: FBInstant.context.getType(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseContext(success, error) {
|
||||||
|
FBInstant.context.chooseAsync({
|
||||||
|
filters: ['INCLUDE_EXISTING_CHALLENGES'],
|
||||||
|
minSize: 2,
|
||||||
|
maxSize: 2,
|
||||||
|
}).then(function () {
|
||||||
|
var contextInfo = {
|
||||||
|
id: FBInstant.context.getID(),
|
||||||
|
type: FBInstant.context.getType(),
|
||||||
|
}
|
||||||
|
console.log("chooseContext after", contextInfo)
|
||||||
|
success(JSON.stringify(contextInfo))
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
console.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAsync(playerId, success, error) {
|
||||||
|
FBInstant.context
|
||||||
|
.createAsync(playerId)
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("createAsync error", JSON.stringify(err))
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendPlayWith(data, success, error) {
|
||||||
|
FBInstant.context
|
||||||
|
.chooseAsync(JSON.parse(data))
|
||||||
|
.then(success)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err.message)
|
||||||
|
error(JSON.stringify(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAsync(img) {
|
||||||
|
FBInstant.updateAsync({
|
||||||
|
action: 'CUSTOM',
|
||||||
|
cta: 'Join The Fight',
|
||||||
|
image: img,
|
||||||
|
text: {
|
||||||
|
default: 'X just invaded Y\'s village!',
|
||||||
|
},
|
||||||
|
template: 'VILLAGE_INVASION',
|
||||||
|
data: { myReplayData: 'test', date: new Date().getMilliseconds() },
|
||||||
|
strategy: 'IMMEDIATE',
|
||||||
|
notification: 'NO_PUSH',
|
||||||
|
}).then(function () {
|
||||||
|
// closes the game after the update is posted.
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,187 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--
|
||||||
|
If you are serving your web app in a path other than the root, change the
|
||||||
|
href value below to reflect the base path you are serving from.
|
||||||
|
|
||||||
|
The path provided below has to start and end with a slash "/" in order for
|
||||||
|
it to work correctly.
|
||||||
|
|
||||||
|
For more details:
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
|
|
||||||
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
|
the `--base-href` argument provided to `flutter build`.
|
||||||
|
-->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=GT-K5856LV"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag() {
|
||||||
|
dataLayer.push(arguments);
|
||||||
|
}
|
||||||
|
gtag("js", new Date());
|
||||||
|
|
||||||
|
gtag("config", "G-K57CGD10ST", {
|
||||||
|
cookie_flags: "max-age=7200;secure;samesite=none",
|
||||||
|
cookieDomain: 'none'
|
||||||
|
});
|
||||||
|
function gaEventLogger(eventName, eventParameters) {
|
||||||
|
gtag("event", eventName, JSON.parse(eventParameters));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="A new Flutter project.">
|
||||||
|
|
||||||
|
<!-- iOS meta tags & icons -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="example">
|
||||||
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
|
<title>example</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// The value below is injected by flutter build, do not touch.
|
||||||
|
const serviceWorkerVersion = null;
|
||||||
|
</script>
|
||||||
|
<!-- This script adds the flutter initialization JS code -->
|
||||||
|
<script src="flutter.js" defer></script>
|
||||||
|
<!-- Event logger-->
|
||||||
|
<script src="pako.min.js"></script>
|
||||||
|
<script src="event-logger.js"></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter-view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launch_view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launch_view .logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 48%;
|
||||||
|
max-width: 75vw;
|
||||||
|
width: 50vw;
|
||||||
|
top: 30vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launch_view .cover {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launch_view .loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 54%;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 4px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 7vw;
|
||||||
|
height: 7vw;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launch_view .guru_text {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 85%;
|
||||||
|
color: #000;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#debug_dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#debug_dialog .content {
|
||||||
|
width: 80%;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
height: 80%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="https://connect.facebook.net/en_US/fbinstant.7.0.js"></script>
|
||||||
|
<script src="index.js"></script>
|
||||||
|
<script>
|
||||||
|
showLaunchView()
|
||||||
|
window.flutterConfiguration = {
|
||||||
|
canvasKitBaseUrl: "./canvaskit/",
|
||||||
|
};
|
||||||
|
async function updateProgress() {
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
await new Promise((res) => setTimeout(res, 200));
|
||||||
|
FBInstant.setLoadingProgress((i / 10) * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onStart() {
|
||||||
|
// Download main.dart.js
|
||||||
|
window.gtag && gtag("event", "app_open", { source: "" });
|
||||||
|
_flutter.loader.loadEntrypoint({
|
||||||
|
serviceWorker: {
|
||||||
|
serviceWorkerVersion: serviceWorkerVersion,
|
||||||
|
},
|
||||||
|
onEntrypointLoaded: function(engineInitializer) {
|
||||||
|
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||||
|
appRunner.runApp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.onload = function () {
|
||||||
|
updateProgress();
|
||||||
|
//onStart();
|
||||||
|
FBInstant.initializeAsync().then(async () => {
|
||||||
|
return FBInstant.startGameAsync().then(onStart);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<div id="debug_dialog" onclick="closeDebugeDialog(event)">
|
||||||
|
<div class="content"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* showLaunchView 展示首页
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {string} options.cover 顶部图片url,有则显示,没有不显示
|
||||||
|
* @param {string} options.logo logo url,有则显示,没有不显示
|
||||||
|
* @param {boolean} options.loading loading,有则显示,没有不显示
|
||||||
|
* @param {string} options.guruLimited guru limited 文案,默认"@ Guru network limited"
|
||||||
|
*/
|
||||||
|
function showLaunchView(options = {
|
||||||
|
guruLimited: '@ Guru network limited'
|
||||||
|
}) {
|
||||||
|
const view = document.createElement('div')
|
||||||
|
view.id = 'launch_view'
|
||||||
|
|
||||||
|
if (options.cover) {
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = options.cover
|
||||||
|
img.className = 'cover'
|
||||||
|
view.appendChild(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.logo) {
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = options.logo
|
||||||
|
img.className = 'logo'
|
||||||
|
view.appendChild(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.loading) {
|
||||||
|
const loading = document.createElement('div')
|
||||||
|
loading.className = 'loading'
|
||||||
|
view.appendChild(loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = document.createElement('p')
|
||||||
|
text.className = 'guru_text'
|
||||||
|
view.appendChild(text)
|
||||||
|
|
||||||
|
document.body.append(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLaunchView() {
|
||||||
|
const view = document.querySelector('#launch_view')
|
||||||
|
if (view) {
|
||||||
|
view.style.display = 'none'
|
||||||
|
}
|
||||||
|
showFlutterView()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFlutterView() {
|
||||||
|
document.querySelector("flutter-view").style.display = "block"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开debug dialog
|
||||||
|
* @param {string} message 展示内容
|
||||||
|
*/
|
||||||
|
function showDebugDialog(message) {
|
||||||
|
const view = document.querySelector("#debug_dialog")
|
||||||
|
if (view) view.style.display = "flex"
|
||||||
|
const content = document.querySelector(".content")
|
||||||
|
content.innerHTML = message
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDebugeDialog(e) {
|
||||||
|
e.target.style.display = "none"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"short_name": "example",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0175C2",
|
||||||
|
"theme_color": "#0175C2",
|
||||||
|
"description": "A new Flutter project.",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,59 @@
|
||||||
|
library guru_fb_game;
|
||||||
|
import 'package:guru_fb_game/model/model.dart';
|
||||||
|
import 'package:guru_fb_game/utils.dart';
|
||||||
|
|
||||||
|
class AppConfig {
|
||||||
|
final String appVersion;
|
||||||
|
final String appBuildNumber;
|
||||||
|
final String guruLogAppId;
|
||||||
|
final String bundleId;
|
||||||
|
|
||||||
|
final String? adUnitIdBanner;
|
||||||
|
final String? adUnitIdInter;
|
||||||
|
final String? adUnitIdRewardVideo;
|
||||||
|
|
||||||
|
const AppConfig({
|
||||||
|
required this.appVersion,
|
||||||
|
this.appBuildNumber = "1",
|
||||||
|
required this.guruLogAppId,
|
||||||
|
required this.bundleId,
|
||||||
|
this.adUnitIdBanner,
|
||||||
|
this.adUnitIdInter,
|
||||||
|
this.adUnitIdRewardVideo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class GuruFbGame {
|
||||||
|
static final GuruFbGame _instance = GuruFbGame._();
|
||||||
|
|
||||||
|
static GuruFbGame get instance => _instance;
|
||||||
|
|
||||||
|
GuruFbGame._();
|
||||||
|
|
||||||
|
FbProfile? fbProfile;
|
||||||
|
|
||||||
|
Map<String, String>? fbtournament;
|
||||||
|
|
||||||
|
Future _initialize(AppConfig config) async {
|
||||||
|
// setUrlStrategy(null);
|
||||||
|
fbProfile = await FbGameGlobalUtils.getFBProfile();
|
||||||
|
final fbPlatform = FbGameGlobalUtils.getPlatform();
|
||||||
|
final deviceInfo = GuruLogDeviceData(appId: config.bundleId, version: config.appVersion, fbPlatform: fbPlatform);
|
||||||
|
final logInfoData = GuruLogInfoData(deveiceId: '', uid: fbProfile?.userId ?? '');
|
||||||
|
final initData = InitGuruLogEventData(appId: config.guruLogAppId, deviceInfo: deviceInfo, info: logInfoData);
|
||||||
|
FbGameLogUtils.initGuruLogEvent(initData);
|
||||||
|
fbtournament = await FbGameTournamentUtils.getCurrentTournament();
|
||||||
|
await initAds(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future initAds(AppConfig config) async{
|
||||||
|
List<String> list = FbGameAdUtils.checkSupportedAds();
|
||||||
|
if (list.contains('getInterstitialAdAsync') && config.adUnitIdInter != null) {
|
||||||
|
FbGameAdUtils.loadInterstitial(config.adUnitIdInter!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.contains('getRewardedVideoAsync') && config.adUnitIdRewardVideo != null) {
|
||||||
|
FbGameAdUtils.loadRewarded(config.adUnitIdRewardVideo!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'model.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FbProfile {
|
||||||
|
final String photo;
|
||||||
|
final String name;
|
||||||
|
final String id;
|
||||||
|
final String contextType;
|
||||||
|
final String? userId;
|
||||||
|
|
||||||
|
const FbProfile({
|
||||||
|
required this.photo,
|
||||||
|
required this.name,
|
||||||
|
required this.id,
|
||||||
|
required this.contextType,
|
||||||
|
this.userId
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FbProfile.fromJson(Map<String, dynamic> json) => _$FbProfileFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FbProfileToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FbShareData {
|
||||||
|
final String image;
|
||||||
|
final String text;
|
||||||
|
final Map<String, String> data;
|
||||||
|
final List<String> shareDestination;
|
||||||
|
final bool switchContext;
|
||||||
|
|
||||||
|
const FbShareData({
|
||||||
|
this.image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAMAAADQmBKKAAAAAXNSR0IArs4c6QAAAkxQTFRFAAAAJCQkGhoaDAwMFxcXFhYWFBQUDAwMDAwMEBAQEBAQDw8PDg4ODg4ODQ0NDQ0NDQ0NDAwMDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDAwMDAwMDAwMBwcHCAgICQkJCgoKCwsLDAwMDg4ODw8PEBAQEhISFxcXGhoaGxsbHR0dHh4eHx8fICAgISEhIyMjJCQkJSUlJiYmLCwsLS0tMDAwMjIyMzMzNjY2Nzc3ODg4QEBAQUFBRERERkZGTk5OT09PU1NTV1dXWFhYW1tbXFxcXV1dX19fYGBgYWFhZmZmaGhoaWlpbGxsbm5ucXFxcnJydXV1dnZ2eHh4enp6e3t7fHx8fX19fn5+f39/gICAgoKCg4ODhISEhYWFiYmJi4uLjIyMjY2Njo6Oj4+PkJCQkZGRk5OTlZWVl5eXmZmZmpqam5ubnZ2dnp6eoKCgoaGho6OjpKSkp6enqKioqampqqqqrKysra2trq6ur6+vsLCwsrKys7OztLS0tbW1tra2t7e3uLi4ubm5urq6u7u7vLy8vr6+v7+/wMDAwcHBwsLCxMTExcXFxsbGx8fHyMjIycnJysrKy8vLzc3Nzs7Oz8/P0NDQ0dHR0tLS09PT1NTU1dXV1tbW2NjY2dnZ2tra29vb3t7e39/f4ODg4eHh4uLi4+Pj5OTk5eXl5ubm5+fn6Ojo6enp6urq6+vr7Ozs7e3t7u7u7+/v8PDw8fHx8vLy8/Pz9PT09fX19vb29/f3+Pj4+fn5+vr6+/v7/Pz8/f39/v7+////CfwD0AAAAB10Uk5TAAcKFRYXGT4/QEFGSXBzeaCms8LDxsff7PD5+/2lKaEhAAAGb0lEQVR42s3cCV8UZRwH8BWNI0CWQ46FnZ0ZyJJIs+ygsrSsqLD7wizKCsvMsuzGsEM6sBDIEkKJQwoRxJVjYVnYnXljLbvM7szu8zzzzDPPs/P8XsDO9/N/ZmafeS6XC5is/CJ3eUWlR2YQT2VFubsoP8uFnU2FpUwkKa7Swk1YnJySajlDqS7JMeXcWCZnNGV5SE52sZzxFGfDPQVVsgOpKoB5NtfIjqRmM5CzwS07FveGdM/GLbKD2bIxrT6OeqKi1Bq5ZYfjTrmfZcdjuLMLCJ8viSSwZ0339GdXZciCJFUl35DFGcQgRMWaJy+zGniraf9rZbY5IkYwRGVxTy45R7QYsxLlxkAlhBzdhXzmMYogVyiJ9Q+rSTj4EiMKDape60MWWucYMQJeDCLYZQqjoFKrnqRGsBadCHad0uj3hceaJ1XjxY0mQoI8Wa58Io5FjEYyBcn5riILHj0nfo1b9rR82jsy6V8Mq1gxBRWZ9zvSPBpn29MnJxTVWrSbCNELKbfiSXLqW4cjqvVobQa9XLmrAtMTL886Z3/vqkoUU1CFqxLbEy+P1yu+ekUljSmo0uWx5IlyWmdUlR3I48L3xMrzyGVVZQmSXZY8O39VVA5A6x6v99klVXUQlOqp/VZVnQSleu4dV7kAaZ69iyoNkEgMSvE8FVIpgSQyUIqnJazyAop5Dih0PKpICjIWqJlSfWyD1j17Qyo9kEQEMjRY45JKDyTZAa0VSBBq/6XnUSUykKFA36u8gOKe53Cuo0RWljESCpGB9AW6I2hiiUy0t+yuozP6gQbFC3QGrRl+5SaKwzEYoH2oN6LSu4vq8BAMpPNIiN6zMrybPscM9DbcM/+YhatYCAwUewdJ16Ge0XoWGiQoWqDXoZ5OkQ0HBNK12FXYw3VIZMRBgoRnYPVpY+eBgWIF+gvi+UVkxkGCGiC9oFFZZOdBgIQ2sGeugaUnHZR86C+BQfvNO1ksQNEWA/9rjIpMC4QAPQ/+w3iIbYEQoE4g6E/GHgQI+FZU7nMMVA+8hcbMv/RYgR4GttibrAsEB7UCW2w76wKBQWuvoXYQaMrnFMjnGwD2OhwEAQc32xwEXQOBHncQtAAC3e1z7qZeBoFudQi0ViLQ1IoicAty4MUIHKSq9zn31xEAge7y4ZdoYhoQGyA/CPSoBRCwxDZAk6Dfe0swn3pnBboA+r3vkvPKpiLKILED9HsTAn6JaIE00SHgN3QDfolog/YBO2gHBYwlJWxA4K+gIUFfIilzoKgI+NxH7jGKpAyCuoBtdkYwNBpCRA2ktdnL4A/FB1NFUqZAt4E/pQe9aSIpE6CoaBI82NAEEIFM9EFHwKDpbcK6SESO/gYjgBANC2uiHZDlL/0+4/ItSAIrxqxGEyYaOE+UaAgypPd1fLGUT0RWaXbJkGAwuBxasTO1IIkvwgY9DyRXlMFN07P6zM3NLwQWl8hAiRLNQUCrL2kiH3xV5/gVXaampq/O+GfnbU7gvQMdOP8ssewOahoa1Wfs0vh/k9PXJHtTnLUBqOjczXGSAF3WeW5Al78HL1wcHhmftAeSxA/gky+X96wvToShurp1OdvT98f5wX/GSCeBEyWaRUxPnW30Jk3p6fhRl9OdP3f93td/kXSaPFGiJ1ETeOGO23XLOFNz4ktdvvrm5KnOrp7zou2lFv3IKc6V7mYJtq7zw491Of7JiS/af+jqsw+602x9YGjgaHOjCAAdfl+XI0ePHf+8/fRv5ItREqKDWNPkoXl/amZBL0YboISom+ZCAhsLmhJLvrb6aYJoLIq7f4UTUELUFOYMJL2g8AFKit5V+ADRF9kF6VotwgcoKXpilQ9QUvTAdT5ASdHWHmff1Oki6Y1VCiDJNkgn2jWo8ADSk5oXWIM8FkV1x4I2QKYdNI/JNgqASKo7HGC39LTSbKMJkCS1jBC+J02/OipMt+JASDs/miG5v02/y8qtbZI2fLzveK1nQaENcmNs54KTJGl703s/jfgDy2GFEqgIZ8MbkkR5KWM+3pZA2ib4U5+FuWmSLgr+q6XY20opqlA/WGhh420mEtt4i781mX1K4ofF8APKsb69nWnKtANseAHlkR6RwCjFtg6RoB/dIRLEx2zQTE0BzweR8HdUC3+H2XB33A9/ByLxd2QUh4dq8Xfs2NrBNnwdzBY7SiaTR9flYp2mdwNfh/s5fPzh/zcuYJd5bFPWAAAAAElFTkSuQmCC',
|
||||||
|
required this.text,
|
||||||
|
this.data = const {},
|
||||||
|
this.shareDestination = const [
|
||||||
|
'NEWSFEED',
|
||||||
|
'GROUP',
|
||||||
|
'COPY_LINK',
|
||||||
|
'MESSENGER'
|
||||||
|
],
|
||||||
|
this.switchContext = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FbShareData.fromJson(Map<String, dynamic> json) => _$FbShareDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FbShareDataToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FbInviteTextData {
|
||||||
|
final String text;
|
||||||
|
// localizations: {
|
||||||
|
// ar_AR: 'X \u0641\u0642\u0637 \u063A\u0632\u062A ' +
|
||||||
|
// '\u0642\u0631\u064A\u0629 Y!',
|
||||||
|
// en_US: 'X just invaded Y\'s village!',
|
||||||
|
// es_LA: '\u00A1X acaba de invadir el pueblo de Y!'
|
||||||
|
// }
|
||||||
|
final Map<String, String> localizations;
|
||||||
|
|
||||||
|
const FbInviteTextData({required this.text, required this.localizations});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'default': text,
|
||||||
|
'localizations': localizations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
factory FbInviteTextData.fromJson(Map<String, dynamic> json) => _$FbInviteTextDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FbInviteTextDataToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FbInviteData {
|
||||||
|
final String image;
|
||||||
|
final FbInviteTextData text;
|
||||||
|
final FbInviteTextData? cta;
|
||||||
|
final FbInviteTextData? dialogTitle;
|
||||||
|
final List<String>? filters; // ['NEW_CONTEXT_ONLY', 'EXISTING_PLAYERS_ONLY']
|
||||||
|
// sections: [
|
||||||
|
// {sectionType: 'GROUPS', maxResults: 2},
|
||||||
|
// {sectionType: 'USERS'}
|
||||||
|
// ],
|
||||||
|
final List<Map<String, dynamic>>? sections;
|
||||||
|
|
||||||
|
const FbInviteData({
|
||||||
|
required this.image,
|
||||||
|
required this.text,
|
||||||
|
this.cta,
|
||||||
|
this.dialogTitle,
|
||||||
|
this.filters,
|
||||||
|
this.sections,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FbInviteData.fromJson(Map<String, dynamic> json) => _$FbInviteDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FbInviteDataToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class GuruLogDeviceData{
|
||||||
|
final String appId;
|
||||||
|
final String version;
|
||||||
|
final String fbPlatform;
|
||||||
|
|
||||||
|
GuruLogDeviceData({
|
||||||
|
required this.appId,
|
||||||
|
this.version = '1.0.0',
|
||||||
|
required this.fbPlatform
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GuruLogDeviceData.fromJson(Map<String, dynamic> json) => _$GuruLogDeviceDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$GuruLogDeviceDataToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class GuruLogInfoData {
|
||||||
|
final String? deveiceId;
|
||||||
|
final String? uid;
|
||||||
|
final String? adjustId;
|
||||||
|
final String? adId;
|
||||||
|
final String? firebaseId;
|
||||||
|
|
||||||
|
const GuruLogInfoData({
|
||||||
|
this.deveiceId,
|
||||||
|
this.uid,
|
||||||
|
this.adjustId,
|
||||||
|
this.adId,
|
||||||
|
this.firebaseId
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GuruLogInfoData.fromJson(Map<String, dynamic> json) => _$GuruLogInfoDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$GuruLogInfoDataToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class InitGuruLogEventData {
|
||||||
|
final String appId;
|
||||||
|
final GuruLogDeviceData deviceInfo;
|
||||||
|
final GuruLogInfoData info;
|
||||||
|
|
||||||
|
const InitGuruLogEventData({
|
||||||
|
required this.appId,
|
||||||
|
required this.deviceInfo,
|
||||||
|
required this.info
|
||||||
|
});
|
||||||
|
|
||||||
|
factory InitGuruLogEventData.fromJson(Map<String, dynamic> json) => _$InitGuruLogEventDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$InitGuruLogEventDataToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FBTournamentSortOrder{
|
||||||
|
HIGHER_IS_BETTER, LOWER_IS_BETTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FBTournamentSortFormat{
|
||||||
|
NUMERIC, TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FbTournamentConfig{
|
||||||
|
final String? title;
|
||||||
|
final String? image;
|
||||||
|
// "HIGHER_IS_BETTER"| "LOWER_IS_BETTER"
|
||||||
|
final FBTournamentSortOrder? sortOrder;
|
||||||
|
// "NUMERIC" | "TIME"
|
||||||
|
final FBTournamentSortFormat? scoreFormat;
|
||||||
|
// timestamp
|
||||||
|
final num? endTime;
|
||||||
|
|
||||||
|
const FbTournamentConfig({
|
||||||
|
this.title,
|
||||||
|
this.image,
|
||||||
|
this.sortOrder,
|
||||||
|
this.scoreFormat,
|
||||||
|
this.endTime
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FbTournamentConfig.fromJson(Map<String, dynamic> json) => _$FbTournamentConfigFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FbTournamentConfigToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FbUserInfo {
|
||||||
|
@JsonKey(name: 'player_id', defaultValue: "")
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@JsonKey(name: 'user_id', defaultValue: "")
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'name', defaultValue: "")
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
@JsonKey(name: 'picUrl', defaultValue: "")
|
||||||
|
final String picUrl;
|
||||||
|
|
||||||
|
FbUserInfo(this.id, this.userId, this.name, this.picUrl);
|
||||||
|
|
||||||
|
factory FbUserInfo.fromJson(Map<String, dynamic> json) => _$FbUserInfoFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FbUserInfoToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FBContextType {
|
||||||
|
@JsonValue("POST") POST,
|
||||||
|
@JsonValue("THREAD") THREAD,
|
||||||
|
@JsonValue("GROUP") GROUP,
|
||||||
|
@JsonValue("SOLO") SOLO
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FBContextInfo {
|
||||||
|
final String id;
|
||||||
|
final FBContextType type;
|
||||||
|
|
||||||
|
const FBContextInfo({
|
||||||
|
required this.id,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FBContextInfo.fromJson(Map<String, dynamic> json) => _$FBContextInfoFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FBContextInfoToJson(this);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
FbProfile _$FbProfileFromJson(Map<String, dynamic> json) => FbProfile(
|
||||||
|
photo: json['photo'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
id: json['id'] as String,
|
||||||
|
contextType: json['contextType'] as String,
|
||||||
|
userId: json['userId'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FbProfileToJson(FbProfile instance) => <String, dynamic>{
|
||||||
|
'photo': instance.photo,
|
||||||
|
'name': instance.name,
|
||||||
|
'id': instance.id,
|
||||||
|
'contextType': instance.contextType,
|
||||||
|
'userId': instance.userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
FbShareData _$FbShareDataFromJson(Map<String, dynamic> json) => FbShareData(
|
||||||
|
image: json['image'] as String? ??
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAMAAADQmBKKAAAAAXNSR0IArs4c6QAAAkxQTFRFAAAAJCQkGhoaDAwMFxcXFhYWFBQUDAwMDAwMEBAQEBAQDw8PDg4ODg4ODQ0NDQ0NDQ0NDAwMDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDAwMDAwMDAwMBwcHCAgICQkJCgoKCwsLDAwMDg4ODw8PEBAQEhISFxcXGhoaGxsbHR0dHh4eHx8fICAgISEhIyMjJCQkJSUlJiYmLCwsLS0tMDAwMjIyMzMzNjY2Nzc3ODg4QEBAQUFBRERERkZGTk5OT09PU1NTV1dXWFhYW1tbXFxcXV1dX19fYGBgYWFhZmZmaGhoaWlpbGxsbm5ucXFxcnJydXV1dnZ2eHh4enp6e3t7fHx8fX19fn5+f39/gICAgoKCg4ODhISEhYWFiYmJi4uLjIyMjY2Njo6Oj4+PkJCQkZGRk5OTlZWVl5eXmZmZmpqam5ubnZ2dnp6eoKCgoaGho6OjpKSkp6enqKioqampqqqqrKysra2trq6ur6+vsLCwsrKys7OztLS0tbW1tra2t7e3uLi4ubm5urq6u7u7vLy8vr6+v7+/wMDAwcHBwsLCxMTExcXFxsbGx8fHyMjIycnJysrKy8vLzc3Nzs7Oz8/P0NDQ0dHR0tLS09PT1NTU1dXV1tbW2NjY2dnZ2tra29vb3t7e39/f4ODg4eHh4uLi4+Pj5OTk5eXl5ubm5+fn6Ojo6enp6urq6+vr7Ozs7e3t7u7u7+/v8PDw8fHx8vLy8/Pz9PT09fX19vb29/f3+Pj4+fn5+vr6+/v7/Pz8/f39/v7+////CfwD0AAAAB10Uk5TAAcKFRYXGT4/QEFGSXBzeaCms8LDxsff7PD5+/2lKaEhAAAGb0lEQVR42s3cCV8UZRwH8BWNI0CWQ46FnZ0ZyJJIs+ygsrSsqLD7wizKCsvMsuzGsEM6sBDIEkKJQwoRxJVjYVnYnXljLbvM7szu8zzzzDPPs/P8XsDO9/N/ZmafeS6XC5is/CJ3eUWlR2YQT2VFubsoP8uFnU2FpUwkKa7Swk1YnJySajlDqS7JMeXcWCZnNGV5SE52sZzxFGfDPQVVsgOpKoB5NtfIjqRmM5CzwS07FveGdM/GLbKD2bIxrT6OeqKi1Bq5ZYfjTrmfZcdjuLMLCJ8viSSwZ0339GdXZciCJFUl35DFGcQgRMWaJy+zGniraf9rZbY5IkYwRGVxTy45R7QYsxLlxkAlhBzdhXzmMYogVyiJ9Q+rSTj4EiMKDape60MWWucYMQJeDCLYZQqjoFKrnqRGsBadCHad0uj3hceaJ1XjxY0mQoI8Wa58Io5FjEYyBcn5riILHj0nfo1b9rR82jsy6V8Mq1gxBRWZ9zvSPBpn29MnJxTVWrSbCNELKbfiSXLqW4cjqvVobQa9XLmrAtMTL886Z3/vqkoUU1CFqxLbEy+P1yu+ekUljSmo0uWx5IlyWmdUlR3I48L3xMrzyGVVZQmSXZY8O39VVA5A6x6v99klVXUQlOqp/VZVnQSleu4dV7kAaZ69iyoNkEgMSvE8FVIpgSQyUIqnJazyAop5Dih0PKpICjIWqJlSfWyD1j17Qyo9kEQEMjRY45JKDyTZAa0VSBBq/6XnUSUykKFA36u8gOKe53Cuo0RWljESCpGB9AW6I2hiiUy0t+yuozP6gQbFC3QGrRl+5SaKwzEYoH2oN6LSu4vq8BAMpPNIiN6zMrybPscM9DbcM/+YhatYCAwUewdJ16Ge0XoWGiQoWqDXoZ5OkQ0HBNK12FXYw3VIZMRBgoRnYPVpY+eBgWIF+gvi+UVkxkGCGiC9oFFZZOdBgIQ2sGeugaUnHZR86C+BQfvNO1ksQNEWA/9rjIpMC4QAPQ/+w3iIbYEQoE4g6E/GHgQI+FZU7nMMVA+8hcbMv/RYgR4GttibrAsEB7UCW2w76wKBQWuvoXYQaMrnFMjnGwD2OhwEAQc32xwEXQOBHncQtAAC3e1z7qZeBoFudQi0ViLQ1IoicAty4MUIHKSq9zn31xEAge7y4ZdoYhoQGyA/CPSoBRCwxDZAk6Dfe0swn3pnBboA+r3vkvPKpiLKILED9HsTAn6JaIE00SHgN3QDfolog/YBO2gHBYwlJWxA4K+gIUFfIilzoKgI+NxH7jGKpAyCuoBtdkYwNBpCRA2ktdnL4A/FB1NFUqZAt4E/pQe9aSIpE6CoaBI82NAEEIFM9EFHwKDpbcK6SESO/gYjgBANC2uiHZDlL/0+4/ItSAIrxqxGEyYaOE+UaAgypPd1fLGUT0RWaXbJkGAwuBxasTO1IIkvwgY9DyRXlMFN07P6zM3NLwQWl8hAiRLNQUCrL2kiH3xV5/gVXaampq/O+GfnbU7gvQMdOP8ssewOahoa1Wfs0vh/k9PXJHtTnLUBqOjczXGSAF3WeW5Al78HL1wcHhmftAeSxA/gky+X96wvToShurp1OdvT98f5wX/GSCeBEyWaRUxPnW30Jk3p6fhRl9OdP3f93td/kXSaPFGiJ1ETeOGO23XLOFNz4ktdvvrm5KnOrp7zou2lFv3IKc6V7mYJtq7zw491Of7JiS/af+jqsw+602x9YGjgaHOjCAAdfl+XI0ePHf+8/fRv5ItREqKDWNPkoXl/amZBL0YboISom+ZCAhsLmhJLvrb6aYJoLIq7f4UTUELUFOYMJL2g8AFKit5V+ADRF9kF6VotwgcoKXpilQ9QUvTAdT5ASdHWHmff1Oki6Y1VCiDJNkgn2jWo8ADSk5oXWIM8FkV1x4I2QKYdNI/JNgqASKo7HGC39LTSbKMJkCS1jBC+J02/OipMt+JASDs/miG5v02/y8qtbZI2fLzveK1nQaENcmNs54KTJGl703s/jfgDy2GFEqgIZ8MbkkR5KWM+3pZA2ib4U5+FuWmSLgr+q6XY20opqlA/WGhh420mEtt4i781mX1K4ofF8APKsb69nWnKtANseAHlkR6RwCjFtg6RoB/dIRLEx2zQTE0BzweR8HdUC3+H2XB33A9/ByLxd2QUh4dq8Xfs2NrBNnwdzBY7SiaTR9flYp2mdwNfh/s5fPzh/zcuYJd5bFPWAAAAAElFTkSuQmCC',
|
||||||
|
text: json['text'] as String,
|
||||||
|
data: (json['data'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, e as String),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
|
shareDestination: (json['shareDestination'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const ['NEWSFEED', 'GROUP', 'COPY_LINK', 'MESSENGER'],
|
||||||
|
switchContext: json['switchContext'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FbShareDataToJson(FbShareData instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'image': instance.image,
|
||||||
|
'text': instance.text,
|
||||||
|
'data': instance.data,
|
||||||
|
'shareDestination': instance.shareDestination,
|
||||||
|
'switchContext': instance.switchContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
FbInviteTextData _$FbInviteTextDataFromJson(Map<String, dynamic> json) =>
|
||||||
|
FbInviteTextData(
|
||||||
|
text: json['text'] as String,
|
||||||
|
localizations: Map<String, String>.from(json['localizations'] as Map),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FbInviteTextDataToJson(FbInviteTextData instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'text': instance.text,
|
||||||
|
'localizations': instance.localizations,
|
||||||
|
};
|
||||||
|
|
||||||
|
FbInviteData _$FbInviteDataFromJson(Map<String, dynamic> json) => FbInviteData(
|
||||||
|
image: json['image'] as String,
|
||||||
|
text: FbInviteTextData.fromJson(json['text'] as Map<String, dynamic>),
|
||||||
|
cta: json['cta'] == null
|
||||||
|
? null
|
||||||
|
: FbInviteTextData.fromJson(json['cta'] as Map<String, dynamic>),
|
||||||
|
dialogTitle: json['dialogTitle'] == null
|
||||||
|
? null
|
||||||
|
: FbInviteTextData.fromJson(
|
||||||
|
json['dialogTitle'] as Map<String, dynamic>),
|
||||||
|
filters:
|
||||||
|
(json['filters'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||||
|
sections: (json['sections'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as Map<String, dynamic>)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FbInviteDataToJson(FbInviteData instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'image': instance.image,
|
||||||
|
'text': instance.text,
|
||||||
|
'cta': instance.cta,
|
||||||
|
'dialogTitle': instance.dialogTitle,
|
||||||
|
'filters': instance.filters,
|
||||||
|
'sections': instance.sections,
|
||||||
|
};
|
||||||
|
|
||||||
|
GuruLogDeviceData _$GuruLogDeviceDataFromJson(Map<String, dynamic> json) =>
|
||||||
|
GuruLogDeviceData(
|
||||||
|
appId: json['appId'] as String,
|
||||||
|
version: json['version'] as String? ?? '1.0.0',
|
||||||
|
fbPlatform: json['fbPlatform'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$GuruLogDeviceDataToJson(GuruLogDeviceData instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'appId': instance.appId,
|
||||||
|
'version': instance.version,
|
||||||
|
'fbPlatform': instance.fbPlatform,
|
||||||
|
};
|
||||||
|
|
||||||
|
GuruLogInfoData _$GuruLogInfoDataFromJson(Map<String, dynamic> json) =>
|
||||||
|
GuruLogInfoData(
|
||||||
|
deveiceId: json['deveiceId'] as String?,
|
||||||
|
uid: json['uid'] as String?,
|
||||||
|
adjustId: json['adjustId'] as String?,
|
||||||
|
adId: json['adId'] as String?,
|
||||||
|
firebaseId: json['firebaseId'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$GuruLogInfoDataToJson(GuruLogInfoData instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'deveiceId': instance.deveiceId,
|
||||||
|
'uid': instance.uid,
|
||||||
|
'adjustId': instance.adjustId,
|
||||||
|
'adId': instance.adId,
|
||||||
|
'firebaseId': instance.firebaseId,
|
||||||
|
};
|
||||||
|
|
||||||
|
InitGuruLogEventData _$InitGuruLogEventDataFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
InitGuruLogEventData(
|
||||||
|
appId: json['appId'] as String,
|
||||||
|
deviceInfo: GuruLogDeviceData.fromJson(
|
||||||
|
json['deviceInfo'] as Map<String, dynamic>),
|
||||||
|
info: GuruLogInfoData.fromJson(json['info'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$InitGuruLogEventDataToJson(
|
||||||
|
InitGuruLogEventData instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'appId': instance.appId,
|
||||||
|
'deviceInfo': instance.deviceInfo,
|
||||||
|
'info': instance.info,
|
||||||
|
};
|
||||||
|
|
||||||
|
FbTournamentConfig _$FbTournamentConfigFromJson(Map<String, dynamic> json) =>
|
||||||
|
FbTournamentConfig(
|
||||||
|
title: json['title'] as String?,
|
||||||
|
image: json['image'] as String?,
|
||||||
|
sortOrder: $enumDecodeNullable(
|
||||||
|
_$FBTournamentSortOrderEnumMap, json['sortOrder']),
|
||||||
|
scoreFormat: $enumDecodeNullable(
|
||||||
|
_$FBTournamentSortFormatEnumMap, json['scoreFormat']),
|
||||||
|
endTime: json['endTime'] as num?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FbTournamentConfigToJson(FbTournamentConfig instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'title': instance.title,
|
||||||
|
'image': instance.image,
|
||||||
|
'sortOrder': _$FBTournamentSortOrderEnumMap[instance.sortOrder],
|
||||||
|
'scoreFormat': _$FBTournamentSortFormatEnumMap[instance.scoreFormat],
|
||||||
|
'endTime': instance.endTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$FBTournamentSortOrderEnumMap = {
|
||||||
|
FBTournamentSortOrder.HIGHER_IS_BETTER: 'HIGHER_IS_BETTER',
|
||||||
|
FBTournamentSortOrder.LOWER_IS_BETTER: 'LOWER_IS_BETTER',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$FBTournamentSortFormatEnumMap = {
|
||||||
|
FBTournamentSortFormat.NUMERIC: 'NUMERIC',
|
||||||
|
FBTournamentSortFormat.TIME: 'TIME',
|
||||||
|
};
|
||||||
|
|
||||||
|
FbUserInfo _$FbUserInfoFromJson(Map<String, dynamic> json) => FbUserInfo(
|
||||||
|
json['player_id'] as String? ?? '',
|
||||||
|
json['user_id'] as String? ?? '',
|
||||||
|
json['name'] as String? ?? '',
|
||||||
|
json['picUrl'] as String? ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FbUserInfoToJson(FbUserInfo instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'player_id': instance.id,
|
||||||
|
'user_id': instance.userId,
|
||||||
|
'name': instance.name,
|
||||||
|
'picUrl': instance.picUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
FBContextInfo _$FBContextInfoFromJson(Map<String, dynamic> json) =>
|
||||||
|
FBContextInfo(
|
||||||
|
id: json['id'] as String,
|
||||||
|
type: $enumDecode(_$FBContextTypeEnumMap, json['type']),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FBContextInfoToJson(FBContextInfo instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'type': _$FBContextTypeEnumMap[instance.type]!,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$FBContextTypeEnumMap = {
|
||||||
|
FBContextType.POST: 'POST',
|
||||||
|
FBContextType.THREAD: 'THREAD',
|
||||||
|
FBContextType.GROUP: 'GROUP',
|
||||||
|
FBContextType.SOLO: 'SOLO',
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:js' as js;
|
||||||
|
|
||||||
|
import 'package:guru_fb_game/model/model.dart';
|
||||||
|
|
||||||
|
class FbGameGlobalUtils {
|
||||||
|
static Future<dynamic> getFBProfile() {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('getFBProfileWithUid', [
|
||||||
|
(success) => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> shareGame(FbShareData data) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('shareGame', [
|
||||||
|
json.encode(data.toJson()),
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> inviteUser(FbInviteData data) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('inviteUser', [
|
||||||
|
json.encode(data.toJson()),
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic> getEntryPointData() {
|
||||||
|
final result = js.context.callMethod('getEntryPointData');
|
||||||
|
return json.decode(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getLocale() {
|
||||||
|
final result = js.context.callMethod('getLocale');
|
||||||
|
return result ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getPlatform() {
|
||||||
|
String result = "web";
|
||||||
|
result = js.context.callMethod('getPlatform');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FbGamePlayerUtils {
|
||||||
|
static void saveData(Map<String, dynamic> data) {
|
||||||
|
js.context.callMethod('fbSetData', [json.encode(data)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> getData(List<String> keys) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('fbGetData', [
|
||||||
|
json.encode(keys),
|
||||||
|
(success) => completer.complete(json.decode(success)),
|
||||||
|
(error) => completer.completeError(json.decode(error)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取当前环境下的用户信息,包括用户id,用户名称,用户头像
|
||||||
|
Future<dynamic> getPlayersAsync() async {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
js.context.callMethod('getPlayersAsync', [
|
||||||
|
(value) {
|
||||||
|
List<Map<String, dynamic>> players = json.decode(value);
|
||||||
|
final users = players.map((e) => FbUserInfo.fromJson(e)).toList();
|
||||||
|
completer.complete(users);
|
||||||
|
},
|
||||||
|
(error) => completer.completeError(json.decode(error)),
|
||||||
|
]);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家同玩好友的信息
|
||||||
|
* 返回的值是数组
|
||||||
|
*/
|
||||||
|
Future<dynamic> getConnectedPlayersAsync() {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
js.context.callMethod('getConnectedPlayersAsync', [
|
||||||
|
(value) {
|
||||||
|
List<Map<String, dynamic>> players = json.decode(value);
|
||||||
|
final users = players.map((e) => FbUserInfo.fromJson(e)).toList();
|
||||||
|
completer.complete(users);
|
||||||
|
},
|
||||||
|
(error) => completer.completeError(json.decode(error)),
|
||||||
|
]);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> subscribeBot() {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('subscribeBot', [
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(json.decode(error)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FbGameTournamentUtils {
|
||||||
|
static Future<dynamic> createTournament(num initialScore, FbTournamentConfig config, Map<String, String> data) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('createTournament', [
|
||||||
|
initialScore,
|
||||||
|
json.encode(config.toJson()),
|
||||||
|
json.encode(data),
|
||||||
|
(value) => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> postTournamentScore(String id, num score) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('postTournamentScore', [
|
||||||
|
id,
|
||||||
|
score,
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> shareTournament(num score, Map<String, dynamic> data) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('shareTournament', [
|
||||||
|
score,
|
||||||
|
json.encode(data),
|
||||||
|
(value) => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> getCurrentTournament() {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('getCurrentTournament', [
|
||||||
|
(success) => completer.complete(json.decode(success)),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// success 返回 map
|
||||||
|
// {
|
||||||
|
// id: tournament.getContextID(),
|
||||||
|
// endTime: tournament.getEndTime(),
|
||||||
|
// title: tournament.getTitle(),
|
||||||
|
// payLoad: tournament.getPayload()
|
||||||
|
// }
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FbGameContextUtils {
|
||||||
|
static Future<dynamic> createAsync(String playerId) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
js.context.callMethod('createAsync', [
|
||||||
|
playerId,
|
||||||
|
(value) => completer.complete(),
|
||||||
|
(error) => completer.completeError(json.decode(error)),
|
||||||
|
]);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateAsync(String img) {
|
||||||
|
js.context.callMethod('updateAsync', [img]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FBContextInfo getContext() {
|
||||||
|
final result = js.context.callMethod('getContextInfo');
|
||||||
|
return FBContextInfo.fromJson(json.decode(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> contextSwitchAsync(String contextId) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
js.context.callMethod('contextSwitchAsync', [
|
||||||
|
contextId,
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(json.decode(error)),
|
||||||
|
]);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
//切换当前环境,如果选择朋友,则可以获取到朋友的信息
|
||||||
|
static Future<dynamic> chooseContext() async {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
js.context.callMethod('chooseContext', [
|
||||||
|
(value) => completer.complete(FBContextInfo.fromJson(json.decode(value))),
|
||||||
|
(error) => completer.completeError(json.decode(error)),
|
||||||
|
]);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FbGameLogUtils {
|
||||||
|
static void fbLogEvent(String eventName, int valueToSum, Map<String, dynamic> params) {
|
||||||
|
js.context.callMethod('logEvent', [eventName, valueToSum, json.encode(params)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initGuruLogEvent(InitGuruLogEventData data) {
|
||||||
|
js.context.callMethod('initEventLogger', [json.encode(data.toJson())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void guruLogEvent(String eventName, Map<String, dynamic> params, Map<String, dynamic> properties) {
|
||||||
|
js.context.callMethod('castboxLogEvent', [eventName, json.encode(params), json.encode(properties)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FbGameAdUtils {
|
||||||
|
static List<String> checkSupportedAds() {
|
||||||
|
final String listStr = js.context.callMethod("checkSupportedAds");
|
||||||
|
List<String> list = json.decode(listStr);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> showBanner(String id) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('showBannerAds', [
|
||||||
|
id,
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> hideBanner(String id) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('hideBannerAds', [
|
||||||
|
id,
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> loadInterstitial(String id) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('loadInterstitial', [
|
||||||
|
id,
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> showInterstitial() {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('showInterstitial', [
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> loadRewarded(String id) {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('loadRewarded', [
|
||||||
|
id,
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<dynamic> showRewarded() {
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
|
||||||
|
js.context.callMethod('showRewarded', [
|
||||||
|
() => completer.complete(),
|
||||||
|
(error) => completer.completeError(error),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
name: guru_fb_game
|
||||||
|
description: "A new Flutter package project."
|
||||||
|
version: 0.0.1
|
||||||
|
homepage:
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.2.3 <4.0.0'
|
||||||
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
build_runner: 2.4.7
|
||||||
|
json_serializable: 6.7.1
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# To add assets to your package, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
#
|
||||||
|
# For details regarding assets in packages, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
#
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|
||||||
|
# To add custom fonts to your package, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts in packages, see
|
||||||
|
# https://flutter.dev/custom-fonts/#from-packages
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:guru_fb_game/guru_fb_game.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('adds one to input values', () {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
build/
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue