Compare commits

...

3 Commits

Author SHA1 Message Date
Haoyi c6549bf584 optimize ads initialize
Signed-off-by: Haoyi <haoyi.zhang@castbox.fm>
2024-04-11 14:05:11 +08:00
Haoyi ea55fd4551 update guru_app/guru_ui
Signed-off-by: Haoyi <haoyi.zhang@castbox.fm>
2024-03-07 11:46:50 +08:00
Haoyi 59b48f342c sdk v3.0.0
Signed-off-by: Haoyi <haoyi.zhang@castbox.fm>
2024-01-15 11:24:42 +08:00
2052 changed files with 141844 additions and 0 deletions

9
guru_app/.github/release.yaml vendored Normal file
View File

@ -0,0 +1,9 @@
# .github/release.yml
changelog:
categories:
- title: 🟢 Features
labels:
- Feature
- title: 🟠 Optimize
labels:
- optimize

View File

@ -0,0 +1,21 @@
name: github-project-issue-to-sheets
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
workflow_dispatch:
issues:
types: [opened, deleted, transferred, closed, reopened, assigned, unassigned, labeled, unlabeled]
jobs:
github-project-issue-to-sheets:
runs-on: ubuntu-latest
name: github-project-issue-to-sheets
steps:
- name: Transfer GitHub Project Issues into Google Sheets
id: github-project-issue-to-sheets
uses: ViRGiL175/github-project-issue-to-sheets@v2.0.0
with:
google-api-service-account-credentials: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_DATA }}
document-id: '1TMTFBE7xPmgIsJVfYbTMIoQ7D_s42KGqFHxqu3kAFEA'
sheet-name: 'GitHub Issues'

120
guru_app/.gitignore vendored Normal file
View File

@ -0,0 +1,120 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
.vscode
.gradle
.idea
/local.properties
.DS_Store
/build
.metadata
ios/Flutter/flutter_export_environment.sh
**/.settings
**/.project
**/.classpath
# build id
build_id.properties
# IntelliJ related
*.ipr
*.iws
.idea/
# Visual Studio Code related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.packages
.pub-cache/
.pub/
/build/
flutter_*
.flutter-plugins-dependencies
# goCli related
go-cli/.packages
go-cli/pubspec.lock
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/gradlew
**/android/gradlew.bat
**/android/fastlane/report.xml
**/android/fastlane/README.md
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
**/ios/build/*
**/ios/fastlane/README.md
**/ios/fastlane/report.xml
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# Crashlytics
debugSymbols/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# i18n
**/android/res/values/strings_en.arb
lib/generated/i18n.dart
# output
output/ios/adhok/*
output/ios/appstore/*
ios/fastlane/report.xml
android/fastlane/report.xml
**/android/fastlane/metadata/android/**/images/**
**/ios/fastlane/screenshots/**

74
guru_app/CHANGELOG.md Normal file
View File

@ -0,0 +1,74 @@
## v1.2.0(beta)
- **`[guru_app]`**
- 在financial模块添加igc购买流程
- 取消iapManager的restorePurchase机制改用reloadOrders本地读取机制
- Deployment中添加`autoRestoreIap`、`enableAnalyticsStatistic`、`initIgc`和`igcBalanceSecret`的支持
- **`[guru_spec]`**
- deployment解析器添加`auto_restore_iap`的支持取值范围为true或falsetrue表示在IapManager将自动进行restore操作false反之
- deployment解析器添加`enable_analytics_statistic`的支持取值范围为true或falsetrue表示在GuruAnalytics将自动添加统计的UserProperty
- deployment解析器添加`init_igc`的支持取值为int型在IgcManager初始化的时候第一次使用时会给予相应的igc数量做为初始值
- deployment解析器添加`igcBalanceSecret`的支持取值int型在igc混淆用于防修改的安全secret key
## v1.1.0
- **`[guru_app]`**
- 在financial模块添加reward购买流程
- 升级数据库针对order表添加category字段来记录商品分类方便后续搜索
- 移除guru_app中的通用广告model定义移至**guru_utils**
- 移除BaseController,LifecycleController和AdsController移至**guru_utils**
- 移除RewardsAware,InterstitialAware,BannerAware移至**guru_utils**
- **`[guru_spec]`**
- 添加`products`解析器
- 支持`manifest`定义
- 支持manifest category的汇总
- 支持category的lint检查在定义相似内容时会报错
- 支持同category的manifest参数lint检查在定义同category的manifest时如果参数不匹配将报错
- 移除`iap_profile`解析器
- **`[guru_utils]`**
- 添加通用广告model定义
- 添加controller定义
- 添加RewardsAware,InterstitialAware,BannerAware
- **`[guru_navigator]`**
- 添加`guru_navigator`plugin针对Android的deepLink和ios的universalLink的处理
## v1.0.1
- **`[guru_app]`**
- iap相关逻辑优化
- **`[guru_utils]`**
- 抽象RemoteUtils以便兼容老项目
- 抽象AnalyticsUtils以便兼容老项目
- Vibration库的抽象及优化
- **`[guru_spec]`**
- 强化对兄弟包的支持
## v1.0.0
- **`[guru_app]`**
该库包含guru自身的相关业务逻辑将公司的业务逻辑进行统一封装统一管理相应三方库的版本进行统一调优主要包括如下主要模块
- ***`Account`***
处理匿名登陆相关逻辑,并完成设备上报,错误重试,恢复等相关机制
- ***`Ads`***
处理广告逻辑MAX支持插屏激励视频Banner
- ***`Analytics`***
处理打点相关逻辑现集成Firebase、Facebook、Guru和Adjust并封装了相应的标准点和Guru标准点
- ***`DxLink`***
支持处理DynamicLink和Deeplink的回跳相关逻辑
- ***`CloudMessaging`***
处理Push/In-app Messaging相关逻辑
- ***`RemoteConfig`***
处理相关的RemoteConfig相关逻辑这个依赖于GuruApp中**GuruSpec**的配置生成
- ***`Financial`***
处理相关交易信息当前版本支持IAP后续将扩展虚拟货币及Rewards相关的购买逻辑
- ***`Audio`***
音频处理逻辑通过soundpool逻辑进行二次封装支持更高效的音效输出
- ***`Router`***
依赖于Get的路由机制
- ***`Controller`***
依赖于Get的GetWidget来配合Controller的逻辑现在实现了LifecycleController和AdsController并封装了相应的业务逻辑并实现了相应的Aware来支持辅助扩展
- **`[guru_utils]`**
该packages是一个通用的工具类实现了大部分常用操作大概模块集合网络Math, ui,该库没有引用任何GPADS, Firebase相关库因此老项目可以正常引入
- **`[guru_spec]`**
该packages是一个方便生成APP基础信息的一个生成器这样在配置文件中生成后将可以将信息生成到代码中支持flavors该库依赖GPADS, Firebase因此需要引入GuruApp库
- **`[guru_platform_data]`**
该库封装了一些平台相关的原生操作该库弥补pub.dev上未实现的原生特殊功能
- **`[soundpool]`**
该库移植自原有soundpool但由于该库长期不更新并内部依赖有错误因此单独抽出来进行适配。

1
guru_app/LICENSE Normal file
View File

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

39
guru_app/README.md Normal file
View File

@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View File

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -0,0 +1,125 @@
app_name: Spider
# App接入GuruApp的基础信息下面内容必填
details:
# 中台接口上报时的app_id影响中台push接入和后期的中台打点接入必填
saas_app_id: spider
# 针对DynamicLink和Deeplink的判断必填
authority: solitaire.fungame.studio
# 对应Firebase项目中的基础链接必填
storage_prefix: https://firebasestorage.googleapis.com/v0/b/solitaire-66fbf.appspot.com/o
# 对应CDN云控中的默认cdn链接必填
default_cdn_prefix: https://cdn1.solitaire.fungame.studio
# Android的商店链接后期分享用户反馈或PubMatic等广告源的使用必填
android_gp_url: https://play.google.com/store/apps/details?id=solitaire.patience.card.games.klondike.free
# IOS的商店链接后期分享用户反馈或PubMatic等广告源的使用必填
ios_spp_store_url:
# 隐私协议的链接(必填)
policy_url: https://solitaire.fungame.studio/policy.html
# 隐私条款的链接(必填)
terms_url: https://solitaire.fungame.studio/termsofservice.html
# 联系邮箱 (必填)
email_url: card@fungame.studio
deployment:
# AppProperty Cache的大小默认是256
property_cache_size: 512
# Paint.enableDithering默认是true
enable_dithering: false
# 禁用激励视频默认是false
disable_rewards_ads: true
# 广告配置
ads_profile:
# Banner广告ID(变现提供)
banner_ad_unit_id:
android: a1dc70299fd5d487
ios: 97da0e2028ba80b7
# Interstitial广告ID(变现提供)
interstitial_ad_unit_id:
android: 25b7c47878fcbf6a
ios: 4e7ba2c4921ecdfb
# Rewards广告ID(变现提供)
rewards_ad_unit_id:
android: 3cd13a4e5c388e7b
ios: 2a65c75c3ed690b2
# Amazon广告的AppId(变现提供)
amz_app_id:
android: 22296b56-f6b3-4bee-9fd1-0cd6d5cc69bc
ios: 9fdfd4c0-3f34-4bd4-b9b4-1f649ff50a2a
# Amazon广告的Banner Slot Id(变现提供)
banner_amz_slot_id:
android: 3c10ec33-a2bf-44be-ac9f-707853e63ff2
ios: 7cb36f8a-2953-4f02-a1cb-ec3dfdf33878
# Amazon广告的Interstitial Slot Id(变现提供)
interstitial_amz_slot_id:
android: b7fac191-5986-4144-9fdb-691556b2e092
ios: 82d23cfa-2b5d-4501-bfc3-1cd2b688ed41
# attr
# possessive: 购买后永久有效
# consumable: 可消耗商品
# subscriptions: 订阅类商品
iap_profile:
# 去广告的 SKU(名称固定不可变否则无法适配相关模块的去广告机制内部sku值可改变)
# no_ads:
# android: so.a.iap.noads.699
# ios: so.i.iap.noads.699
# attr: possessive
# 名字可自定义根据自身产品定义
# coin200:
# android: so.a.iapc.coin.200
# ios: so.i.iapc.coin.200
# attr: consumable
remote_config:
iads_config: '{"free_s":600,"win_count":4,"scene":"game_start|new_block|p2g|p2h|reset_keep|reset_scs|ads_break|double|nap","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}'
rads_config: '{"win_count":3}'
bads_config: '{"free_s":180,"win_count":1}'
# adjust 相关配置
adjust_profile:
# 对应adjust的appToken,必填项
app_token:
android: fwbn7l32vpc0
ios: xxakw3rgxnnk
# 如果有对应的事件映射在这里统一定义
event_map:
level_start:
android: hq0xzz
ios: b8khry
in_app_purchase:
android: yzy3uh
ios: z0gje7
revenue: true
level_end:
android: so63k4
ios: 1p8z5t
tutorial_complete:
android: 95fu7q
ios: 1p8z5t

View File

@ -0,0 +1,503 @@
app_name: GuruApp
app_category: app
flavor: "guru_test"
# App接入GuruApp的基础信息下面内容必填
details:
# 中台接口上报时的app_id影响中台push接入和后期的中台打点接入必填
saas_app_id: guruapp
# 针对DynamicLink和Deeplink的判断必填
authority: demo.gurugame.fun
# 对应Firebase项目中的基础链接必填
storage_prefix: https://firebasestorage.googleapis.com/v0/b/example.appspot.com/o
# 对应CDN云控中的默认cdn链接必填
default_cdn_prefix: https://cdn1.example.gurugame.fun
# Android的商店链接后期分享用户反馈或PubMatic等广告源的使用必填
android_gp_url: https://play.google.com/store/apps/details?id=app_package_id
# IOS的商店链接后期分享用户反馈或PubMatic等广告源的使用必填
ios_spp_store_url:
# 隐私协议的链接(必填)
policy_url: https://solitaire.fungame.studio/policy.html
# 隐私条款的链接(必填)
terms_url: https://solitaire.fungame.studio/termsofservice.html
# 联系邮箱 (必填)
email_url: demo@gurugame.fun
# Android Package Name (必填)
package_name: guru.app.demo
# iOS Bundle Id (必填)
bundle_id: guru.app.demo
# Facebook App Id
facebook_app_id: 123456789
deployment:
# AppProperty Cache的大小默认是256
property_cache_size: 512
# Paint.enableDithering默认是true
enable_dithering: false
# 禁用激励视频默认是false
disable_rewards_ads: true
# 是否启用 Analytics Statistic 统计
enable_analytics_statistic: true
# 是否自动恢复IAP购买数据
auto_restore_iap: false
# 初始的游戏币数量,默认是 0
init_igc: 500
# igc游戏内货币 验证密钥混int类型防止igc被外部修改
igc_balance_secret: 2654404609
# GuruApp Persistent Log 默认10M
log_file_size_limit: 10485760
# GuruApp Persistent Log 保存的个数默认7个
log_file_count: 7
# 使用persistent log的最小等级,最终 >= 该level的日志将会被存储到本地
# verbose: 0
# debug: 1
# info: 2
# warning: 3
# error: 4
# wtf: 5
# nothing: 6
persistent_log_level: 1
# ios 验证服务器的密码
ios_validate_receipt_password: aa998877665544332211bb00cc
# 被标注的conversion点在自打点库中将被以Emergency的优先级进行发送
conversion_events:
- first_rads_rewarded
- level_end_success_1
- level_end_success_6
- level_end_success_10
- level_end_success_12
- level_end_success_15
- level_up
- level_up_1
- level_up_3
- level_up_5
- level_up_7
- level_up_10
- level_up_12
- level_up_15
- tch_ad_rev_roas_001
- tutorial_complete
api_connect_timeout: 15000
api_receive_timeout: 15000
#
# Sandbox lets you test subscription events, such as renewals, state changes, and interrupted purchases,
# without having to wait the length of the subscription duration. Once you added testers in sandbox,
# you can choose a subscription renewal speed for each tester to determine how quickly subscriptions renew.
# By default, accounts are set to a speed equalization of 1 month = 5 minutes,
# but you can slow down or speed up the renewal period, based on the options below.
# Subscriptions renew up to 12 times before auto-renewal turns off on the thirteenth renewal attempt.
# ┌───────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┐
# │ Subscription │ Renewal every │ Renewal every │ Renewal every │ Renewal every │ Renewal every │
# │ Duration │ 3 Minutes │ 5 Minutes │ 15 Minutes │ 30 Minutes │ Hour │
# ├───────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┤
# │ 1 Week │ 3 minutes │ 3 minutes │ 5 minutes │ 10 minutes │ 15 minutes │
# ├───────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┤
# │ 1 Month │ 3 minutes │ 5 minutes │ 15 minutes │ 30 minutes │ 1 hour │
# ├───────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┤
# │ 6 Months │ 18 minutes │ 30 minutes │ 90 minutes │ 3 hours │ 6 hours │
# ├───────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┤
# │ 1 Year │ 36 minutes │ 1 hour │ 3 hours │ 6 hours │ 12 hours │
# └───────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┘
#
# level 1: Renewal every 3 minutes per month
# level 2: Renewal every 5 minutes per month (default)
# level 3: Renewal every 15 minutes per month
# level 4: Renewal every 30 minutes per month
# level 5: Renewal every 1 hour per month
ios_sandbox_subs_renewal_speed: 2
# 是否使用广告合规初始化的逻辑合规初始化是指先收集GDPR在初始化广告
ads_compliant_initialization: false
# 自动请求通知栏权限默认是false
auto_request_notification_permission: false
# 请求通知栏权限时的提示出发机制
# rationale: 依赖Android原生的shouldShowRequestRationale返回值来展示对应的Rationale页面
# request: 依赖请求的次数来展示对应的Rationale页面
notification_permission_prompt_trigger: rationale
# 是否追踪通知栏权限的通过率默认是false。
# 如果为true时将会上报对应noti_perm_req_`n`和noti_perm_pass_`n`
# n: 表示第几次请求
# 注意如果开启了追踪通知栏权限的通过率统计点位那么firebase打点中将会出现`n`个noti_perm_req_`n`和noti_perm_pass_`n`这两个打点
# 这里的n的限制需要配置tracking_notification_permission_pass_limit_times
tracking_notification_permission_pass: false
# 如果追踪通知栏权限的通过率,这个值表示最大的追踪次数,超过最大次数后将不再追踪
tracking_notification_permission_pass_limit_times: 10
# 是否打开GuruAnalytics的策略默认是false
enabled_guru_analytics_strategy: false
# 在RewardedAware中调用 showRewardedAd 方法时
# 在激励视频不可用时,是否允许使用插屏做为替代奖励,默认是 false
# 注意:即使这里设置成 True在你使用的页面 Controller中要确保 with InterstitialAware
allow_interstitial_as_alternative_reward: false
# 在 Banner 广告未成功加载的期间,填充一个内部的广告,可以是一个推广,一个内部的广告。
# 它不会影响正常的广告展示逻辑,只要 Banner 广告正常加载,都会将其进行隐藏。
# 因此它的优先级永远不会大于正常的 Banner 广告, 默认值是 false
show_internal_ads_when_banner_unavailable: true
# 由于订阅订单比较重要,而从用户反馈的日志上来看,会存在接口返回异常的问题
# 因此针对这种情况,添加订阅的恢复宽限次数,默认为 3 次
# 当订阅订单恢复失败次数超过该次数,才会真正删除
subscription_restore_grace_count: 3
# 插屏在展示广告前,为了保证用户的体验,会有一个广告的保护时间,
# 即:距上一次全屏广告(插屏广告和激励广告)的结束间隔时间,
# 默认的间隔保护时间为 1 分钟60 秒)单位为秒
fullscreen_ads_min_interval: 60
# 是否打开中台的 AccountProfile 同步机制
# 打开后,在登陆后(包括匿名登陆) 会启动向 Firestore 进行同步AccountProfile的机制
# Firestore 针对 AccountProfile的存储位置默认放在 users 表中
enabled_sync_account_profile: false
# 根据 BI 的需求,对应的 Purchase事件只能报太极的 001 或 020的其中一个
# 因此添加 Purchase Event 的 trigger, 默认值为 1
# 1: 表示在发生购买时打 tch_ad_rev_roas_001
# 2: 表示在发生购买时打 tch_ad_rev_roas_020
# 在广告展示时也会依据该 trigger 的值,在不同的时机打对应的 purchase事件
purchase_event_trigger: 1
# tracking_notification_permission_pass_analytics_type : guru|firebase
# 广告配置
ads_profile:
# Banner广告ID(变现提供)
banner_ad_unit_id:
android: xxxxxxxxxxxxxxxx
ios: xxxxxxxxxxxxxxxx
# Interstitial广告ID(变现提供)
interstitial_ad_unit_id:
android: xxxxxxxxxxxxxxxx
ios: xxxxxxxxxxxxxxxx
# Rewards广告ID(变现提供)
rewards_ad_unit_id:
android: xxxxxxxxxxxxxxxx
ios: xxxxxxxxxxxxxxxx
# Amazon广告的AppId(变现提供)
amz_app_id:
android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Amazon广告的Banner Slot Id(变现提供)
banner_amz_slot_id:
android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Amazon广告的Interstitial Slot Id(变现提供)
interstitial_amz_slot_id:
android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
rewarded_amz_slot_id:
android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
remote_config:
# 保留配置,插屏广告相关配置
iads_config: '{"free_s":600,"win_count":4,"scene":"game_start","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}'
# 保留配置,激励广告相关配置
rads_config: '{"win_count":3}'
# 保留配置Banner广告相关配置
bads_config: '{"free_s":180,"win_count":1}'
# 保留配置,打点相关配置
analytics_config: '{"cap":"firebase|facebook|guru", "init_delay_s": 10}'
#
# _mapping:
# cdn_config: "cdn2_config"
products:
# sku
# 商品的ID如果同时指定了android和ios优先级sku为主
# 支持参数定义见下面的样例商品theme的sku
#
# android,ios
# 各个平台的SKU,如果指定了sku将忽略这两个选择
#
# attr
# possessive: 购买后永久有效
# consumable: 可消耗商品
# subscriptions: 订阅类商品
#
# method:
# 购买的方式支持iap,igc,reward
# iap: 通过IAP购买的商品
# igc: 通过虚拟游戏币购买的商品
# reward: 通过奖励的方式获取的商品
# 如果这个商品支持多种方式购买,可以通过逗号的形式进行串联
#
# capabilities:
# 表示该商品的能力现阶段只支持noAds
#
# manifest:
# 指购买该商品后所获得的清单
# category:
# 该manifest的种类这个主要用于后面的打点
# 如果在guru_spec.yaml中定义相同的category该category的名字必须能保证名字是不相似的
# 如果出现名字相似的情况下,将会视为冲突,如:noAds和no_ads这类相似的名字都将无法生成
# details,details1,details2...detailsN
# 指该商品的获取明细如igc多少个道具多少个
# details中必须定义type和amount否则将无法生成
# details中的type现阶段系统预定义了igc表示游戏内货币
# 如果需要处理模板库不支持的类型需要在使用模板库的时候添加相应的distributor
# 自定义参数
# 可以指定不同的参数也可以引用sku中的参数见下面的样例商品theme的manifest
# 如果在guru_spec.yaml中定义相同的category的商品自定义参数必须一至
# 否则将无法生成
#
no_ads:
android: so.a.iap.noads.699
ios: so.i.iap.noads.699
attr: asset
method: iap,reward,igc
manifest:
category: "no_ads"
no_ads_coin_bundle:
android: so.a.iap.noads.coin.799
ios: so.i.iap.noads.coin.799
attr: asset
method: iap
capabilities: noAds
manifest:
category: "no_ads"
details:
type: "igc" # in-game currency
amount: 500
details1:
type: "cup"
amount: 1
details2:
type: "frag"
amount: 20
theme:
sku: "theme_{theme_id}"
method: igc,reward
attr: possessive
manifest:
category: "theme_{1}"
theme_id: "{1}"
# theme2:
# sku: "theme2_{theme_id}"
# method: igc,reward
# attr: possessive
# manifest:
# category: "theme"
# theme_id: "{1}"
# details:
# type: "theme"
# amount: 1
# details2:
# type: "theme3"
# amount: 1
prop:
sku: "theme_{prop_id}_{pc_id}"
method: igc,reward
attr: possessive
manifest:
category: "prop"
details:
sku: "{1}_{2}"
type: "prop"
amount: 1
theme_id: "{1}"
details2:
type: "pc"
amount: 1
theme_id: "{2}"
no_ads2:
android: so.a.iap.noads.699
ios: so.i.iap.noads.699
attr: possessive
method: iap
capabilities: noAds
manifest:
category: no_ads
details:
type: no_ads
amount: 1
ignore_sales: true
coin200:
android: so.a.iapc.coin.200
ios: so.i.iapc.coin.200
method: iap
attr: consumable
points: true
manifest:
category: coin
details:
type: coin
amount: 200
stage_pack:
android: so.a.iap.stage.1
ios: so.i.iap.stage.1
attr: consumable
method: iap
manifest:
category: "stage_1"
details:
type: "stage" # in-game currency
amount: 1
stage: 1
premium_week:
android: "m2.a.sub.premium"
ios: "m2.i.sub.premium.p1w"
attr: subscriptions
method: iap
capabilities: noAds
base_plan: weekly
group: premium
offers:
- freetrial
- discount
manifest:
category: "sub"
details:
type: "igc"
amount: 8000
premium_year:
android: "m2.a.sub.premium"
ios: "m2.i.sub.premium.p1y"
attr: subscriptions
method: iap
capabilities: noAds
base_plan: yearly
group: premium
offers:
- freetrial
- discount
manifest:
category: "sub"
details:
type: "igc"
amount: 16000
#
# theme_mul:
# sku: "theme_{category}_{theme_id}"
# attr: possessive
# method: igc
# manifest:
# category: "{1}"
# theme_id: "{2}"
# cate: "{1}"
# adjust 相关配置
adjust_profile:
# 对应adjust的appToken,必填项
app_token:
android: testapptoken
ios: testapptoken
# 如果有对应的事件映射在这里统一定义
event_map:
level_start:
android: hq0xzz
ios: b8khry
iap_purchase:
android: yzy3uh
ios: z0gje7
params: true
sub_purchase:
android: yzy3uh
ios: z0gje7
params: true
level_end:
android: so63k4
ios: 1p8z5t
tutorial_complete:
android: 95fu7q
ios: 1p8z5t
experiments:
test:
start: 20240129T000000
end: 20240129T000000
audience:
filters:
- version:
opt: lt
mmp: 2.3.0
- country:
included: ""
excluded: "us,cn,en"
- platform:
android:
opt: gte
ver: 33
ios:
opt: gte
ver: 14
variant: 2
test2:
start: 20240129T000000
end: 20240129T000000
audience:
filters:
- version:
opt: lt
mmp: 2.3.0
- country:
included: "cn"
excluded: "us"
- platform:
android:
opt: lt
ver: 24
ios:
opt: gte
ver: 14
- new_user: true
variant: 5

View File

@ -0,0 +1,151 @@
app_name: Spider
flavor: Spider
# App接入GuruApp的基础信息下面内容必填
details:
# 中台接口上报时的app_id影响中台push接入和后期的中台打点接入必填
saas_app_id: spider
# 针对DynamicLink和Deeplink的判断必填
authority: solitaire.fungame.studio
# 对应Firebase项目中的基础链接必填
storage_prefix: https://firebasestorage.googleapis.com/v0/b/solitaire-66fbf.appspot.com/o
# 对应CDN云控中的默认cdn链接必填
default_cdn_prefix: https://cdn1.solitaire.fungame.studio
# Android的商店链接后期分享用户反馈或PubMatic等广告源的使用必填
android_gp_url: https://play.google.com/store/apps/details?id=solitaire.patience.card.games.klondike.free
# IOS的商店链接后期分享用户反馈或PubMatic等广告源的使用必填
ios_spp_store_url:
# 隐私协议的链接(必填)
policy_url: https://solitaire.fungame.studio/policy.html
# 隐私条款的链接(必填)
terms_url: https://solitaire.fungame.studio/termsofservice.html
# 联系邮箱 (必填)
email_url: card@fungame.studio
# Android Package Name
package_name: guru.app.demo
# iOS Bundle Id
bundle_id: guru.app.demo
# Facebook App Id
facebook_app_id: 987654321
deployment:
# AppProperty Cache的大小默认是256
property_cache_size: 512
# Paint.enableDithering默认是true
enable_dithering: false
# 禁用激励视频默认是false
disable_rewards_ads: true
# 广告配置
ads_profile:
# Banner广告ID(变现提供)
banner_ad_unit_id:
android: a1dc70299fd5d487
ios: 97da0e2028ba80b7
# Interstitial广告ID(变现提供)
interstitial_ad_unit_id:
android: 25b7c47878fcbf6a
ios: 4e7ba2c4921ecdfb
# Rewards广告ID(变现提供)
rewards_ad_unit_id:
android: 3cd13a4e5c388e7b
ios: 2a65c75c3ed690b2
# Amazon广告的AppId(变现提供)
amz_app_id:
android: 22296b56-f6b3-4bee-9fd1-0cd6d5cc69bc
ios: 9fdfd4c0-3f34-4bd4-b9b4-1f649ff50a2a
# Amazon广告的Banner Slot Id(变现提供)
banner_amz_slot_id:
android: 3c10ec33-a2bf-44be-ac9f-707853e63ff2
ios: 7cb36f8a-2953-4f02-a1cb-ec3dfdf33878
# Amazon广告的Interstitial Slot Id(变现提供)
interstitial_amz_slot_id:
android: b7fac191-5986-4144-9fdb-691556b2e092
ios: 82d23cfa-2b5d-4501-bfc3-1cd2b688ed41
# attr
# asset(or possessive): 购买后永久有效
# consumable: 可消耗商品
# subscriptions: 订阅类商品
# capabilities
# noAds
products:
# 去广告的 SKU(名称固定不可变否则无法适配相关模块的去广告机制内部sku值可改变)
no_ads:
android: so.a.iap.noads.699
ios: so.i.iap.noads.699
attr: possessive
capabilities: noAds
# 名字可自定义根据自身产品定义
coin200:
android: so.a.iapc.coin.200
ios: so.i.iapc.coin.200
attr: consumable
theme:
sku: "theme_{theme_id}"
method: igc,reward
attr: possessive
manifest:
category: "{1}"
theme_id: "{1}"
details:
type: "theme"
amount: 1
remote_config:
iads_config: '{"free_s":600,"win_count":4,"scene":"game_start|new_block|p2g|p2h|reset_keep|reset_scs|ads_break|double|nap","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}'
rads_config: '{"win_count":3}'
bads_config: '{"free_s":180,"win_count":1}'
# adjust 相关配置
adjust_profile:
# 对应adjust的appToken,必填项
app_token:
android: fwbn7l32vpc0
ios: xxakw3rgxnnk
# 如果有对应的事件映射在这里统一定义
event_map:
level_start:
android: hq0xzz
ios: b8khry
in_app_purchase:
android: yzy3uh
ios: z0gje7
revenue: true
level_end:
android: so63k4
ios: 1p8z5t
tutorial_complete:
android: 95fu7q
ios: 1p8z5t

View File

@ -0,0 +1,73 @@
/// Created by Haoyi on 2021/7/26
part of "account_manager.dart";
extension AccountAuthExtension on AccountManager {
Future<FirebaseAccountAuth> _loginFirebase(GuruUser guruUser,
{bool canRefreshFirebaseToken = true}) async {
User? firebaseUser;
GuruUser newGuruUser = guruUser;
firebaseUser = await _authenticateFirebase(guruUser).catchError((error) {
Log.e("_authenticateFirebase error! $error", tag: "Account");
return null;
});
if (firebaseUser == null && canRefreshFirebaseToken) {
try {
newGuruUser = await _refreshFirebaseToken(guruUser);
return _loginFirebase(newGuruUser, canRefreshFirebaseToken: false);
} catch (error, stacktrace) {
return FirebaseAccountAuth(guruUser, firebaseUser: null);
}
}
return FirebaseAccountAuth(newGuruUser, firebaseUser: firebaseUser);
}
Future<GuruUser> _refreshFirebaseToken(GuruUser oldSaasUser) async {
return await GuruApi.instance
.renewFirebaseToken()
.then((tokenData) => oldSaasUser.copyWith(firebaseToken: tokenData.firebaseToken));
}
Future<User?> _authenticateFirebase(GuruUser guruUser) async {
int retry = 0;
dynamic lastError;
while (retry < 1) {
try {
Log.i("[$retry] _authenticateFirebase:${guruUser.firebaseToken}", tag: "Account");
return await FirebaseAuth.instance
.signInWithCustomToken(guruUser.firebaseToken)
.then((result) => result.user);
} catch (error, stacktrace) {
await Future.delayed(const Duration(milliseconds: 600));
retry++;
Log.i("[$retry] _authenticateFirebase error :$error, $stacktrace", tag: "Account");
lastError = 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;
}
}

View File

@ -0,0 +1,26 @@
part of "account_manager.dart";
extension AccountAuthInvoker on AccountManager {
Future<bool> _invokeLogin(GuruUser loginUser, Credential credential) async {
return await GuruApp.instance.protocol.accountAuthDelegate?.onLogin(loginUser, credential) ??
true;
}
Future<bool> _invokeLogout(GuruUser logoutUser) async {
return await GuruApp.instance.protocol.accountAuthDelegate?.onLogout(logoutUser) ?? true;
}
Future<bool> _invokeAnonymousLogout(GuruUser logoutUser) async {
return await GuruApp.instance.protocol.accountAuthDelegate?.onAnonymousLogout(logoutUser) ??
true;
}
Future<bool> _invokeAnonymousLogin(GuruUser loginUser, Credential credential) async {
return await GuruApp.instance.protocol.accountAuthDelegate?.onAnonymousLogin(loginUser, credential) ??
true;
}
Future<bool> _invokeConflict() async {
return await GuruApp.instance.protocol.accountAuthDelegate?.onConflict() ?? false;
}
}

View File

@ -0,0 +1,187 @@
import 'dart:convert';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:guru_app/account/model/account.dart';
import 'package:guru_app/account/model/account_profile.dart';
import 'package:guru_app/account/model/user.dart';
import 'package:guru_app/analytics/guru_analytics.dart';
import 'package:guru_app/api/guru_api.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_utils/auth/auth_credential_manager.dart';
import 'package:guru_utils/device/device_info.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:http/http.dart' as http;
/// Created by Haoyi on 6/3/21
///
enum AccountDataStatus { idle, initializing, initialized, waiting, error }
class AccountDataStore {
static final AccountDataStore instance = AccountDataStore._();
final BehaviorSubject<DeviceInfo?> _deviceInfoSubject = BehaviorSubject.seeded(null);
final BehaviorSubject<GuruUser?> _guruUserSubject = BehaviorSubject.seeded(null);
final BehaviorSubject<User?> _firebaseUser = BehaviorSubject.seeded(null);
final BehaviorSubject<AccountProfile?> _accountProfile = BehaviorSubject.seeded(null);
final BehaviorSubject<AccountDataStatus> _accountDataStatus =
BehaviorSubject<AccountDataStatus>.seeded(AccountDataStatus.idle);
final BehaviorSubject<Map<AuthType, Credential>> _credentials =
BehaviorSubject.seeded(<AuthType, Credential>{});
int initRetryCount = 0;
Stream<AccountProfile?> get observableAccountProfile => _accountProfile.stream;
// final EnvConfig envConfig;
AccountDataStore._();
@Deprecated("use guruToken instead")
String? get saasToken => _guruUserSubject.value?.token;
String? get guruToken => _guruUserSubject.value?.token;
String? get uid => _guruUserSubject.value?.uid;
AccountProfile? get accountProfile => _accountProfile.value;
String? get nickname => _accountProfile.value?.nickname;
String? get countryCode => _accountProfile.value?.countryCode;
GuruUser? get user => _guruUserSubject.value;
String? get avatar => _accountProfile.value?.avatar;
DeviceInfo? get currentDevice => _deviceInfoSubject.value;
AccountDataStatus get accountDataStatus => _accountDataStatus.value;
bool get initialized => _accountDataStatus.value == AccountDataStatus.initialized;
Stream<bool> get observableInitialized =>
_accountDataStatus.stream.map((status) => status == AccountDataStatus.initialized);
bool get hasUid => uid?.isNotEmpty == true;
bool get isAnonymous =>
(uid?.isNotEmpty != true) ||
(_credentials.value.containsKey(AuthType.anonymous) && _credentials.value.length == 1);
Stream<GuruUser?> get observableSaasUser => _guruUserSubject.stream;
Map<AuthType, Credential> get credentials => _credentials.value;
Account get account => Account.restore(
guruUser: user,
device: currentDevice,
accountProfile: accountProfile,
firebaseUser: _firebaseUser.value,
credentials: credentials);
Stream<Account> get observableAccount => Rx.combineLatestList([
_guruUserSubject.stream,
_deviceInfoSubject.stream,
_accountProfile.stream,
_firebaseUser.stream,
_credentials.stream
]).debounceTime(const Duration(milliseconds: 100)).map((_) => account);
bool get isSocialLogged => (uid?.isNotEmpty == true) && credentials.isNotEmpty;
void dispose() {
_deviceInfoSubject.close();
_guruUserSubject.close();
_firebaseUser.close();
_accountProfile.close();
_credentials.close();
}
Future<GuruUser?> signInAnonymousInLocked() async {
// 使http postDIO
final secret = await AppProperty.getInstance().getAnonymousSecretKey();
final headers = {
"X-APP-ID": GuruApp.instance.details.saasAppId,
"content-type": "application/json"
};
try {
final uri = Uri.parse("${GuruApi.saasApiHost}/auth/api/v1/tokens/provider/secret");
final response = await http
.post(uri,
headers: headers,
body: jsonEncode(AnonymousLoginReqBody(secret: secret)),
encoding: utf8)
.timeout(const Duration(seconds: 30));
final data = const Utf8Decoder().convert(response.bodyBytes);
if (data.isNotEmpty) {
final result = json.decode(data);
return GuruUser.fromJson(result["data"]);
}
} catch (error, stacktrace) {
Log.v("signInAnonymousInLocked error:$error", tag: "Account");
}
return null;
}
Future refreshAuth() async {
final guruUser = await signInAnonymousInLocked();
if (guruUser != null) {
updateGuruUser(guruUser);
}
}
void updateDeviceInfo(DeviceInfo deviceInfo) {
_deviceInfoSubject.addEx(deviceInfo);
}
@Deprecated("use updateGuruUser instead")
void updateSaasUser(GuruUser saasUser) {
updateGuruUser(saasUser);
}
void updateGuruUser(GuruUser guruUser) {
_guruUserSubject.addEx(guruUser);
if (guruUser.createAtTimestamp > 0) {
GuruAnalytics.instance
.setUserProperty("user_created_timestamp", guruUser.createAtTimestamp.toString());
}
}
void updateFirebaseUser(User user) {
_firebaseUser.addEx(user);
}
void updateAccountProfile(AccountProfile 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}) {
return _accountDataStatus.addIfChanged(status);
}
logout() {
_guruUserSubject.addEx(null);
_firebaseUser.addEx(null);
_deviceInfoSubject.addEx(null);
_accountProfile.addEx(null);
_accountDataStatus.addIfChanged(AccountDataStatus.idle);
}
}

View File

@ -0,0 +1,287 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/account/model/account.dart';
import 'package:guru_app/account/model/account_profile.dart';
import 'package:guru_app/account/model/user.dart';
import 'package:guru_app/analytics/guru_analytics.dart';
import 'package:guru_app/api/guru_api.dart';
import 'package:guru_app/firebase/firebase.dart';
import 'package:guru_app/firebase/firestore/firestore_manager.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_app/property/settings/guru_settings.dart';
import 'package:guru_utils/auth/auth_credential_manager.dart';
import 'package:guru_utils/collection/collectionutils.dart';
import 'package:guru_utils/core/ext.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/device/device_info.dart';
import 'package:guru_utils/device/device_utils.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/network/network_utils.dart';
import 'model/credential.dart';
/// Created by Haoyi on 6/3/21
///
///
part "account_service_extension.dart";
part "account_auth_extension.dart";
part "account_auth_invoker.dart";
class ModifyNicknameException implements Exception {
final String? message;
final dynamic cause;
ModifyNicknameException(this.message, {this.cause});
@override
String toString() {
return "ModifyNicknameException: $message cause:$cause";
}
}
class ModifyLevelException implements Exception {
final String? message;
final dynamic cause;
ModifyLevelException(this.message, {this.cause});
@override
String toString() {
return "ModifyLevelException: $message cause:$cause";
}
}
class AccountManager {
final AccountDataStore accountDataStore;
Timer? retryTimer;
static final AccountManager instance = AccountManager();
static const List<AuthCredentialDelegate> defaultSupportedAuthCredentialDelegates = [
AnonymousCredentialDelegate()
];
AccountManager() : accountDataStore = AccountDataStore.instance;
Future init({Completer<dynamic>? completer}) async {
try {
final result = accountDataStore.transitionTo(AccountDataStatus.initializing);
if (!result) {
Log.w(
"init account error, current initializing! please wait result! retry[${accountDataStore.initRetryCount}]",
tag: "Account");
return;
}
retryTimer?.cancel();
final account = await AppProperty.getInstance().loadAccount();
final restoreResult = await _restoreAccount(account);
if (!restoreResult) {
Log.v("init account error: restoreAccount error! retry[${accountDataStore.initRetryCount}]",
tag: "Account");
_retry();
} else {
accountDataStore.initRetryCount = 0;
accountDataStore.transitionTo(AccountDataStatus.initialized);
Log.v("init account success!", tag: "Account");
}
} catch (error, stacktrace) {
completer?.complete(error);
Log.v("init account error retry[${accountDataStore.initRetryCount}]:$error $stacktrace",
tag: "Account");
_retry();
}
completer?.complete(true);
}
void _retry() {
final intervalSeconds = (accountDataStore.initRetryCount * 2 + 8).clamp(8, 30);
retryTimer?.cancel();
accountDataStore.transitionTo(AccountDataStatus.waiting);
retryTimer = Timer(Duration(seconds: intervalSeconds), () {
init();
accountDataStore.initRetryCount++;
});
}
Future updateLocalProfile(Map<String, dynamic> modifiedJson) async {
modifiedJson[AccountProfile.dirtyField] = true;
final dirtyAccountProfile = accountDataStore.accountProfile?.merge(modifiedJson) ??
AccountProfile.fromJson(modifiedJson);
AppProperty.getInstance().setAccountProfile(dirtyAccountProfile);
accountDataStore.updateAccountProfile(dirtyAccountProfile);
}
///
///
/// [authType]
/// [onConflict]
/// [onLogin]
///
Future<bool> loginWith(AuthType authType) async {
late final Credential? credential;
try {
final result = await AuthCredentialManager.instance.loginWith(authType);
credential = result.credential;
if (!result.isSuccess || credential == null) {
Log.w("loginWith $authType error! credential: [$credential]", tag: "Account");
return false;
}
} catch (error, stacktrace) {
Log.e("loginWith $authType error:$error, $stacktrace");
return false;
}
try {
/// 409
final guruUser = await _requestGuruUser(credential);
await processLogin(guruUser, credential);
return true;
} catch (error, stacktrace) {
Log.w("loginWith $authType error:$error, $stacktrace");
if (error is DioError && error.response?.statusCode == 409) {
return await _processConflict(credential);
} else {
return false;
}
}
}
Future<bool> processLogin(GuruUser user, Credential credential) async {
await _updateGuruUser(user);
await _bindCredential(credential);
try {
await _verifyOrReportAuthDevice(user);
authenticateFirebase();
} catch (error, stacktrace) {
Log.e("_verifyOrReportAuthDevice error!$error $stacktrace");
}
if (credential.isAnonymous) {
return await _invokeAnonymousLogin(user, credential);
} else {
return await _invokeLogin(user, credential);
}
}
///
/// credential
/// credential
///
/// logout AuthType
/// logout unbind
/// authTypes
/// unbind logout
/// 1.
/// 2.
/// unbind logout
/// unbind logout onLogout
///
/// AuthType onLogout
///
Future<GuruUser?> logout({bool switching = false, Set<AuthType>? authTypes}) async {
bool isUnbind = false;
if (authTypes != null && authTypes.isNotEmpty) {
final currentCredentials = accountDataStore.credentials.keys.toSet();
currentCredentials.removeAll(authTypes);
currentCredentials.remove(AuthType.anonymous);
isUnbind = currentCredentials.isNotEmpty;
}
final logoutUser = accountDataStore.user?.copyWith();
try {
if (!isUnbind && logoutUser != null) {
final result = await _invokeLogout(logoutUser);
if (!result) {
Log.w("logout error! ignore!");
return null;
}
}
} catch (error, stacktrace) {
Log.w("invokeLogout error! $error!");
return null;
}
for (var authType in accountDataStore.credentials.keys) {
/// unbind credentials
///
/// authTypes null, False,
///
if (authTypes?.contains(authType) != false && authType != AuthType.anonymous) {
await AuthCredentialManager.instance.logout(authType);
_unbindCredential(authType);
}
}
///
///
if (!switching && accountDataStore.credentials.isEmpty) {
final auth = await _retrieveAnonymous();
if (auth != null) {
await processLogin(auth.user, auth.credential!);
}
}
return logoutUser;
}
Future<bool> modifyProfile(
{String? nickname,
String? avatar,
String? countryCode,
Map<String, dynamic> userData = const <String, dynamic>{}}) async {
int retryCount = 2;
Log.i("modifyProfile $nickname $avatar $countryCode", syncFirebase: true, tag: "Account");
if (nickname == null && avatar == null && countryCode == null && userData.isEmpty) {
return false;
}
final now = DateTimeUtils.currentTimeInMillis();
final modifiedJson = CollectionUtils.filterOutNulls(<String, dynamic>{
AccountProfile.uidField: accountDataStore.uid,
AccountProfile.nicknameField: nickname,
AccountProfile.countryField: countryCode?.toLowerCase(),
AccountProfile.avatarField: avatar,
AccountProfile.updateAtField: now,
AccountProfile.versionField: GuruSettings.instance.version.get(),
AccountProfile.roleField:
GuruSettings.instance.debugMode.get() == true ? UserAttr.tester : UserAttr.real,
AccountProfile.dirtyField: true,
...userData
});
await updateLocalProfile(modifiedJson);
/// AccountProfile true
if (!GuruApp.instance.appSpec.deployment.enabledSyncAccountProfile) {
return true;
}
while ((await NetworkUtils.isNetworkConnected()) && retryCount-- > 0) {
final accountProfile =
await FirestoreManager.instance.modifyProfile(modifiedJson).onError((error, stackTrace) {
Log.i("modifyProfile error!:$error");
GuruAnalytics.instance.logException(ModifyLevelException("modifyProfile error!:$error"),
stacktrace: stackTrace);
return null;
});
if (accountProfile != null) {
Log.i("modifyProfile success! $accountProfile", tag: "Account");
AppProperty.getInstance().setAccountProfile(accountProfile);
accountDataStore.updateAccountProfile(accountProfile);
return true;
} else {
Log.i("[$retryCount] modify profile error!", tag: "Account");
await authenticateFirebase()
.timeout(const Duration(seconds: 15))
.catchError((error, stackTrace) {
Log.i("re-authenticate error:$error", stackTrace: stackTrace, tag: "Account");
});
await Future.delayed(const Duration(seconds: 1));
}
}
return false;
}
}

View File

@ -0,0 +1,274 @@
/// Created by Haoyi on 6/3/21
part of "account_manager.dart";
extension AccountServiceExtension on AccountManager {
Future<bool> _restoreAccount(Account account) async {
AccountAuth? anonymousAuth;
GuruUser? guruUser = account.guruUser;
Log.d("restoreAccount $guruUser", tag: "Account");
try {
if (guruUser == null) {
anonymousAuth = await _retrieveAnonymous();
guruUser = anonymousAuth?.user;
}
} catch (error, stacktrace) {
Log.w("loginWith Anonymous error:$error, $stacktrace");
}
Log.v("_restoreAccount saasUser:$guruUser", tag: "Account");
final device = account.device;
if (device != null) {
_updateDevice(device);
}
final accountProfile = account.accountProfile;
if (accountProfile != null) {
_updateAccountProfile(accountProfile);
}
final credentials = account.credentials;
if (credentials.isNotEmpty) {
_restoreCredentials(credentials);
}
if (guruUser != null) {
await _updateGuruUser(guruUser);
await _verifyOrReportAuthDevice(guruUser);
await authenticateFirebase();
if (accountProfile != null) {
await _checkOrUploadAccountProfile(accountProfile);
}
if (anonymousAuth != null) {
final anonymousCredential = anonymousAuth.credential;
if (anonymousCredential != null) {
_bindCredential(anonymousCredential);
return await _invokeAnonymousLogin(anonymousAuth.user, anonymousCredential);
}
}
return true;
} else {
return false;
}
}
Future switchUser(GuruUser newUser) async {
/// login
_updateGuruUser(newUser);
try {
await _verifyOrReportAuthDevice(newUser);
// firebase
authenticateFirebase();
} catch (error, stacktrace) {
Log.w("loginWithCredential error:$error, $stacktrace");
}
}
Future<bool> _switchAccount(Credential credential) async {
GuruUser? loginUser;
GuruUser? logoutUser;
/// ,
try {
loginUser = await _loginGuruWithCredential(credential);
} catch (error, stacktrace) {
Log.w("loginWithCredential[${credential.authType}] error:$error, $stacktrace");
return false;
}
if (loginUser.isSame(accountDataStore.user)) {
Log.w("loginWithCredential same user!", tag: "Account");
_bindCredential(credential);
return false;
}
bool result = false;
/// logout logoutUser
/// logout switch
/// SwitchAccount
logoutUser = await logout(switching: true);
/// 退退
/// switchAccount
if (logoutUser != null) {
result = await GuruApp.instance.switchAccount(loginUser, credential, oldUser: logoutUser);
}
return result;
}
Future<bool> _processConflict(Credential credential) async {
final historicalSocialAuths = await AppProperty.getInstance().getHistoricalSocialAuths();
///
///
if (accountDataStore.isAnonymous && historicalSocialAuths.isEmpty) {
Log.d("associate conflict: _loginGuruWithCredential!");
final user = accountDataStore.user;
final oldUid = user?.uid ?? "";
if (user != null) {
await _invokeAnonymousLogout(user);
}
///
final guruUser = await _loginGuruWithCredential(credential);
///
///
/// 退
///
await _unbindCredential(AuthType.anonymous);
///
await processLogin(guruUser, credential);
GuruAnalytics.instance.logGuruEvent("switch_account", {
"auth": getAuthName(credential.authType),
"old_uid": oldUid,
"new_uid": guruUser.uid,
"silent": true
});
return true;
} else {
final canSwitch = await _invokeConflict();
if (canSwitch) {
return await _switchAccount(credential);
}
}
return false;
}
Future<DeviceTrack> _buildDevice(GuruUser saasUser) async {
final DeviceInfo? deviceInfo = await AppProperty.getInstance().getAccountDevice();
final firebasePushToken = await RemoteMessagingManager.instance.getToken();
if (firebasePushToken != null) {
final deviceId = await AppProperty.getInstance().getDeviceId();
final newDeviceInfo = await DeviceUtils.buildDeviceInfo(
deviceId: deviceId, firebasePushToken: firebasePushToken, uid: saasUser.uid);
return DeviceTrack(newDeviceInfo, deviceInfo);
}
return DeviceTrack(null, deviceInfo);
}
Future<AccountAuth?> _retrieveAnonymous() async {
final result = await AuthCredentialManager.instance.loginWith(AuthType.anonymous);
final credential = result.credential;
if (!result.isSuccess || credential == null) {
Log.w("_retrieveAnonymous error!", tag: "Account");
return null;
}
final user = await _requestGuruUser(credential);
return AccountAuth(user, credential: credential);
}
Future<GuruUser> _loginGuruWithCredential(Credential credential) async {
return await GuruApi.instance.loginGuruWithCredential(credential: credential);
}
Future<GuruUser> _associateCredential(Credential credential) async {
return await GuruApi.instance.associateCredential(credential: credential);
}
Future<GuruUser> _requestGuruUser(Credential credential) async {
//MetaData GuruUser IdsignIn
if (!accountDataStore.hasUid || credential.isAnonymous) {
Log.d("_loginGuruWithCredential!", tag: "Account");
return await _loginGuruWithCredential(credential);
} else {
Log.d("_associateCredential!");
// GuruUser idMetaDataTokenassociateSaasUser
return await _associateCredential(credential);
}
}
Future _verifyOrReportAuthDevice(GuruUser guruUser) async {
final deviceTrack = await _buildDevice(guruUser);
final latestReportDeviceTimestamp =
await AppProperty.getInstance().getLatestReportDeviceTimestamp();
final elapsedInterval = DateTimeUtils.currentTimeInMillis() - latestReportDeviceTimestamp;
final isChanged = (elapsedInterval > DateTimeUtils.sixHourInMillis) || deviceTrack.isChanged;
final reportDevice = deviceTrack.device;
final deviceId = deviceTrack.device?.deviceId ?? "";
if (deviceId.isNotEmpty) {
GuruAnalytics.instance.setDeviceId(deviceId);
}
if (isChanged && reportDevice?.isValid == true && guruUser.isValid == true) {
final result = await GuruApi.instance.reportDevice(reportDevice!).then((_) {
return true;
}).catchError((error) {
Log.i("reportDevice error:$error", tag: "Account");
return false;
});
if (result) {
reportDevice.dumpDevice(msg: "REPORT DEVICE SUCCESS");
_updateDevice(reportDevice);
AppProperty.getInstance()
.setLatestReportDeviceTimestamp(DateTimeUtils.currentTimeInMillis());
AppProperty.getInstance().setAccountDevice(reportDevice);
}
}
}
Future _checkOrUploadAccountProfile(AccountProfile accountProfile) async {
bool upload = accountProfile.dirty;
String? changedCountryCode = DeviceUtils.buildLocaleInfo().countryCode.toLowerCase();
if (DartExt.isBlank(changedCountryCode) || accountProfile.countryCode == changedCountryCode) {
changedCountryCode = null;
} else {
upload = true;
}
Log.d(
"_checkOrUploadAccountProfile dirty:${accountProfile.dirty} upload:$upload $changedCountryCode",
tag: "Account");
if (upload) {
await modifyProfile(countryCode: changedCountryCode);
}
}
void refreshFcmToken() {
final saasUser = accountDataStore.user;
if (saasUser != null) {
_verifyOrReportAuthDevice(saasUser);
}
}
void _updateDevice(DeviceInfo device) {
accountDataStore.updateDeviceInfo(device);
}
Future _bindCredential(Credential credential) async {
accountDataStore.bindCredential(credential);
///
if (credential.authType != AuthType.anonymous) {
await AppProperty.getInstance().saveCredential(credential);
}
}
Future _unbindCredential(AuthType authType) async {
accountDataStore.unbindCredential(authType);
if (authType != AuthType.anonymous) {
await AppProperty.getInstance().deleteCredential(authType);
} else {
await AppProperty.getInstance().clearAnonymousSecretKey();
}
}
void _restoreCredentials(Map<AuthType, Credential> credentials) {
accountDataStore.updateCredentials(credentials);
}
Future _updateGuruUser(GuruUser guruUser) async {
accountDataStore.updateGuruUser(guruUser);
await AppProperty.getInstance().setAccountGuruUser(guruUser);
await GuruAnalytics.instance.setUserId(guruUser.uid);
}
void _updateFirebaseUser(User user) {
accountDataStore.updateFirebaseUser(user);
}
void _updateAccountProfile(AccountProfile accountProfile) {
accountDataStore.updateAccountProfile(accountProfile);
}
}

View File

@ -0,0 +1,108 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:guru_app/account/account_manager.dart';
import 'package:guru_app/account/model/account_profile.dart';
import 'package:guru_app/account/model/user.dart';
import 'package:guru_utils/auth/auth_credential_manager.dart';
import 'package:guru_utils/device/device_info.dart';
import 'package:guru_utils/property/app_property.dart';
/// Created by Haoyi on 6/3/21
class Account {
final GuruUser? guruUser;
final DeviceInfo? device;
final AccountProfile? accountProfile;
final User? firebaseUser;
final Map<AuthType, Credential> credentials; // facebook, google, apple, anonymous
@Deprecated("use guruUser instead")
SaasUser? get saasUser => guruUser;
String? get uid => guruUser?.uid;
String? get nickname => accountProfile?.nickname;
Account.restore(
{this.guruUser,
this.device,
this.accountProfile,
this.firebaseUser,
this.credentials = const {}});
}
class AccountAuth {
final GuruUser user;
final Credential? credential;
AccountAuth(this.user, {this.credential});
bool get isValid => uid != null && uid != "";
String? get saasToken => user.token;
String? get uid => user.uid;
// bool get existsFirebaseUser => firebaseUser != null;
@override
String toString() {
return 'AccountAuth{user: $user}';
}
}
class FirebaseAccountAuth {
final GuruUser user;
final User? firebaseUser;
FirebaseAccountAuth(this.user, {this.firebaseUser});
bool get isValid => uid != null && uid != "";
String? get guruToken => user.token;
String? get uid => user.uid;
bool get existsFirebaseUser => firebaseUser != null;
@override
String toString() {
return 'AccountAuth{user: $user}';
}
}
abstract class IAccountAuthDelegate {
/// ,
List<AuthCredentialDelegate> get supportedAuthCredentialDelegates =>
AccountManager.defaultSupportedAuthCredentialDelegates;
/// , KEY
///
Set<PropertyKey> get deviceSharedProperties => {};
///
/// 使 processor
/// processor
Future<bool> onLogin(GuruUser loginUser, Credential credential);
///
/// 使 processor
/// processor
Future<bool> onLogout(GuruUser logoutUser);
///
/// onAnonymousLogout
Future<bool> onAnonymousLogout(GuruUser logoutUser) async {
return true;
}
/// APPCredential
/// onAnonymousLogin
Future<bool> onAnonymousLogin(GuruUser loginUser, Credential credential) async {
return true;
}
///
/// onLogout
///
Future<bool> onConflict();
}

View File

@ -0,0 +1,135 @@
import 'package:json_annotation/json_annotation.dart';
/// Created by Haoyi on 2021/7/28
///
part "account_profile.g.dart";
class AccountRole {
static const normal = 0;
static const tester = 10;
static const machine = 100;
}
// 使genericArgumentFactories
// flatten
@JsonSerializable()
class AccountProfile {
static const String uidField = "uid";
static const String nicknameField = "nickname";
static const String countryField = "country";
static const String bestScoreField = "score";
static const String avatarField = "avatar";
static const String versionField = "ver";
static const String updateAtField = "upt";
static const String dirtyField = "dirty";
static const String roleField = "role";
static final _generalFieldSet = {
uidField,
nicknameField,
countryField,
bestScoreField,
avatarField,
versionField,
updateAtField,
dirtyField,
roleField
};
@JsonKey(name: uidField, defaultValue: "")
final String uid;
@JsonKey(name: nicknameField, defaultValue: "")
final String nickname;
@JsonKey(name: countryField, defaultValue: "")
final String countryCode;
@JsonKey(name: avatarField, defaultValue: "")
final String avatar;
@JsonKey(name: versionField, defaultValue: "")
final String version;
@JsonKey(name: dirtyField, defaultValue: false)
final bool dirty;
@JsonKey(name: updateAtField, defaultValue: 0)
final int updateAt;
@JsonKey(name: roleField, defaultValue: 0)
final int role;
@JsonKey(ignore: true)
final Map<String, dynamic> userData;
const AccountProfile._({this.uid = "",
this.nickname = "",
this.countryCode = "",
this.avatar = "avatar_1",
this.version = "",
this.dirty = false,
this.role = AccountRole.normal,
this.userData = const <String, dynamic>{},
this.updateAt = 0});
static const AccountProfile empty = AccountProfile._();
AccountProfile({this.uid = "",
this.nickname = "",
this.countryCode = "",
this.avatar = "avatar_1",
this.version = "",
this.dirty = false,
this.role = AccountRole.normal,
Map<String, dynamic> userData = const <String, dynamic>{},
this.updateAt = 0}) : userData = Map.from(userData);
factory AccountProfile.fromJson(Map<String, dynamic> json) =>
_$AccountProfileFromJson(json)
..userData.addAll(_validateUserData(json, direct: false));
Map<String, dynamic> toJson() =>
_$AccountProfileToJson(this)
..addAll(_validateUserData(userData));
static Map<String, dynamic> _validateUserData(Map<String, dynamic> data, {bool direct = true}) {
return (direct ? data : Map.from(data))
..removeWhere((key, value) => _generalFieldSet.contains(key));
}
AccountProfile copyWith({String? nickname,
String? countryCode,
String? avatar,
String? version,
bool? dirty,
int? role,
Map<String, dynamic>? userData,
bool mergeUserData = true}) {
final changedUserData = <String, dynamic>{if (mergeUserData) ...this.userData};
if (userData != null) {
changedUserData.addAll(userData);
}
return AccountProfile(
uid: uid,
nickname: nickname ?? this.nickname,
countryCode: countryCode ?? this.countryCode,
avatar: avatar ?? this.avatar,
version: version ?? this.version,
role: role ?? this.role,
userData: changedUserData,
dirty: dirty ?? this.dirty);
}
AccountProfile merge(Map<String, dynamic> replaceJson) {
return AccountProfile.fromJson(toJson()
..addAll(replaceJson));
}
@override
String toString() {
return 'AccountProfile{nickname: $nickname, countryCode: $countryCode}';
}
}

View File

@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account_profile.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AccountProfile _$AccountProfileFromJson(Map<String, dynamic> json) =>
AccountProfile(
uid: json['uid'] as String? ?? '',
nickname: json['nickname'] as String? ?? '',
countryCode: json['country'] as String? ?? '',
avatar: json['avatar'] as String? ?? '',
version: json['ver'] as String? ?? '',
dirty: json['dirty'] as bool? ?? false,
role: json['role'] as int? ?? 0,
updateAt: json['upt'] as int? ?? 0,
);
Map<String, dynamic> _$AccountProfileToJson(AccountProfile instance) =>
<String, dynamic>{
'uid': instance.uid,
'nickname': instance.nickname,
'country': instance.countryCode,
'avatar': instance.avatar,
'ver': instance.version,
'dirty': instance.dirty,
'upt': instance.updateAt,
'role': instance.role,
};

View File

@ -0,0 +1,37 @@
import 'package:guru_app/property/app_property.dart';
import 'package:guru_utils/auth/auth_credential_manager.dart';
class AnonymousCredential extends Credential {
final String secretKey;
AnonymousCredential(this.secretKey);
@override
String get token => secretKey;
@override
AuthType get authType => AuthType.anonymous;
String toJson() => secretKey;
}
class AnonymousCredentialDelegate extends AuthCredentialDelegate {
const AnonymousCredentialDelegate();
@override
AuthType get authType => AuthType.anonymous;
@override
Future<AuthResult> login() async {
final secretKey = await AppProperty.getInstance().getAnonymousSecretKey();
return AuthResult.success(AnonymousCredential(secretKey));
}
@override
Future logout() async {}
@override
Credential deserializeCredential(String data) {
return AnonymousCredential(data);
}
}

View File

@ -0,0 +1,195 @@
import 'package:json_annotation/json_annotation.dart';
/// Created by Haoyi on 6/3/21
part 'user.g.dart';
@Deprecated("Use Guru User instead")
typedef SaasUser = GuruUser;
@JsonSerializable()
class GuruUser {
@JsonKey(name: 'uid', defaultValue: "")
final String uid;
@JsonKey(name: 'token', defaultValue: "")
final String token;
@JsonKey(name: 'firebaseToken', defaultValue: "")
final String firebaseToken;
@JsonKey(name: 'createdAtTimestamp', defaultValue: 0)
final int createAtTimestamp;
// bool get isAnonymous => (type == null) || (type == LOGIN_WITH_ANONYMOUS);
bool get isValid =>
(uid != "") && (token.isNotEmpty == true) && (firebaseToken.isNotEmpty == true);
GuruUser(
{required this.uid,
required this.token,
required this.firebaseToken,
this.createAtTimestamp = 0});
factory GuruUser.fromJson(Map<String, dynamic> json) => _$GuruUserFromJson(json);
Map<String, dynamic> toJson() => _$GuruUserToJson(this);
GuruUser copyWith({String? firebaseToken, String? token}) {
return GuruUser(
uid: uid, token: token ?? this.token, firebaseToken: firebaseToken ?? this.firebaseToken);
}
bool isSame(GuruUser? user) {
return uid == user?.uid &&
token == user?.token &&
firebaseToken == user?.firebaseToken &&
createAtTimestamp == user?.createAtTimestamp;
}
@override
String toString() {
return 'SaasUser{uid: $uid, token: $token, firebaseToken: $firebaseToken, createAtTimestamp: $createAtTimestamp}';
}
}
//
@JsonSerializable()
class AnonymousLoginReqBody {
@JsonKey(name: 'secret', defaultValue: "")
final String secret;
AnonymousLoginReqBody({required this.secret});
// @override
// String toString() {
// return "{secret: '$secret'}";
// }
factory AnonymousLoginReqBody.fromJson(Map<String, dynamic> json) =>
_$AnonymousLoginReqBodyFromJson(json);
Map<String, dynamic> toJson() => _$AnonymousLoginReqBodyToJson(this);
}
@JsonSerializable()
class FacebookLoginReqBody {
@JsonKey(name: 'accessToken', defaultValue: "")
final String? accessToken;
FacebookLoginReqBody({this.accessToken});
@override
String toString() {
return 'FacebookLoginReqBody{accessToken: $accessToken}';
}
factory FacebookLoginReqBody.fromJson(Map<String, dynamic> json) =>
_$FacebookLoginReqBodyFromJson(json);
Map<String, dynamic> toJson() => _$FacebookLoginReqBodyToJson(this);
}
@JsonSerializable()
class GoogleLoginReqBody {
@JsonKey(name: 'idToken', defaultValue: "")
final String? idToken;
GoogleLoginReqBody({this.idToken});
@override
String toString() {
return 'GoogleLoginReqBody{idToken: $idToken}';
}
factory GoogleLoginReqBody.fromJson(Map<String, dynamic> json) =>
_$GoogleLoginReqBodyFromJson(json);
Map<String, dynamic> toJson() => _$GoogleLoginReqBodyToJson(this);
}
@JsonSerializable()
class AppleLoginReqBody {
@JsonKey(name: 'token', defaultValue: "")
final String? token;
@JsonKey(name: 'clientType', defaultValue: "ios")
final String clientType;
AppleLoginReqBody({this.token, this.clientType = "ios"});
@override
String toString() {
return 'AppleLoginReqBody{token: $token, clientType: $clientType}';
}
factory AppleLoginReqBody.fromJson(Map<String, dynamic> json) =>
_$AppleLoginReqBodyFromJson(json);
Map<String, dynamic> toJson() => _$AppleLoginReqBodyToJson(this);
}
@JsonSerializable()
class FirebaseTokenData {
@JsonKey(name: 'uid', defaultValue: "")
final String uid;
@JsonKey(name: 'firebaseToken', defaultValue: "")
final String firebaseToken;
FirebaseTokenData({this.uid = "", this.firebaseToken = ""});
factory FirebaseTokenData.fromJson(Map<String, dynamic> json) =>
_$FirebaseTokenDataFromJson(json);
Map<String, dynamic> toJson() => _$FirebaseTokenDataToJson(this);
@override
String toString() {
return 'FirebaseTokenData{uid: $uid, firebaseToken: $firebaseToken}';
}
}
@JsonSerializable()
class UserAuthInfo {
@JsonKey(name: "secret", defaultValue: "")
final String secret;
@JsonKey(name: 'providerList', defaultValue: const <String>[])
final List<String> providerList;
factory UserAuthInfo.fromJson(Map<String, dynamic> json) => _$UserAuthInfoFromJson(json);
Map<String, dynamic> toJson() => _$UserAuthInfoToJson(this);
UserAuthInfo({this.secret = "", this.providerList = const []});
@override
String toString() {
return 'UserAuthList{providerList: $providerList}';
}
}
@JsonSerializable()
class UnbindReqBody {
@JsonKey(name: 'provider', defaultValue: "")
final String provider;
UnbindReqBody({this.provider = ""});
@override
String toString() {
return 'UnbindReqBody{provider: $provider}';
}
factory UnbindReqBody.fromJson(Map<String, dynamic> json) => _$UnbindReqBodyFromJson(json);
Map<String, dynamic> toJson() => _$UnbindReqBodyToJson(this);
}
class UserAttr {
static const real = 0;
static const tester = 10;
static const machine = 100;
}

View File

@ -0,0 +1,103 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GuruUser _$GuruUserFromJson(Map<String, dynamic> json) => GuruUser(
uid: json['uid'] as String? ?? '',
token: json['token'] as String? ?? '',
firebaseToken: json['firebaseToken'] as String? ?? '',
createAtTimestamp: json['createdAtTimestamp'] as int? ?? 0,
);
Map<String, dynamic> _$GuruUserToJson(GuruUser instance) => <String, dynamic>{
'uid': instance.uid,
'token': instance.token,
'firebaseToken': instance.firebaseToken,
'createdAtTimestamp': instance.createAtTimestamp,
};
AnonymousLoginReqBody _$AnonymousLoginReqBodyFromJson(
Map<String, dynamic> json) =>
AnonymousLoginReqBody(
secret: json['secret'] as String? ?? '',
);
Map<String, dynamic> _$AnonymousLoginReqBodyToJson(
AnonymousLoginReqBody instance) =>
<String, dynamic>{
'secret': instance.secret,
};
FacebookLoginReqBody _$FacebookLoginReqBodyFromJson(
Map<String, dynamic> json) =>
FacebookLoginReqBody(
accessToken: json['accessToken'] as String? ?? '',
);
Map<String, dynamic> _$FacebookLoginReqBodyToJson(
FacebookLoginReqBody instance) =>
<String, dynamic>{
'accessToken': instance.accessToken,
};
GoogleLoginReqBody _$GoogleLoginReqBodyFromJson(Map<String, dynamic> json) =>
GoogleLoginReqBody(
idToken: json['idToken'] as String? ?? '',
);
Map<String, dynamic> _$GoogleLoginReqBodyToJson(GoogleLoginReqBody instance) =>
<String, dynamic>{
'idToken': instance.idToken,
};
AppleLoginReqBody _$AppleLoginReqBodyFromJson(Map<String, dynamic> json) =>
AppleLoginReqBody(
token: json['token'] as String? ?? '',
clientType: json['clientType'] as String? ?? 'ios',
);
Map<String, dynamic> _$AppleLoginReqBodyToJson(AppleLoginReqBody instance) =>
<String, dynamic>{
'token': instance.token,
'clientType': instance.clientType,
};
FirebaseTokenData _$FirebaseTokenDataFromJson(Map<String, dynamic> json) =>
FirebaseTokenData(
uid: json['uid'] as String? ?? '',
firebaseToken: json['firebaseToken'] as String? ?? '',
);
Map<String, dynamic> _$FirebaseTokenDataToJson(FirebaseTokenData instance) =>
<String, dynamic>{
'uid': instance.uid,
'firebaseToken': instance.firebaseToken,
};
UserAuthInfo _$UserAuthInfoFromJson(Map<String, dynamic> json) => UserAuthInfo(
secret: json['secret'] as String? ?? '',
providerList: (json['providerList'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
Map<String, dynamic> _$UserAuthInfoToJson(UserAuthInfo instance) =>
<String, dynamic>{
'secret': instance.secret,
'providerList': instance.providerList,
};
UnbindReqBody _$UnbindReqBodyFromJson(Map<String, dynamic> json) =>
UnbindReqBody(
provider: json['provider'] as String? ?? '',
);
Map<String, dynamic> _$UnbindReqBodyToJson(UnbindReqBody instance) =>
<String, dynamic>{
'provider': instance.provider,
};

View File

@ -0,0 +1,12 @@
part of 'ads_manager.dart';
/// Created by Haoyi on 2022/4/28
extension AdsGlobalProperty on AdsManager {
int get latestFullscreenAdsHiddenTimestamps =>
adsGlobalProperties["latestFullscreenAdsHiddenTimestamps"] as int? ?? 0;
set latestFullscreenAdsHiddenTimestamps(int ts) {
adsGlobalProperties["latestFullscreenAdsHiddenTimestamps"] = ts;
}
}

View File

@ -0,0 +1,726 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:guru_app/account/model/user.dart';
import 'package:guru_app/ads/applovin/banner/applovin_banner_ads.dart';
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/ads_config.dart';
import 'package:guru_app/ads/core/ads_impression.dart';
import 'package:guru_app/ads/core/strategy/interstitial/max_strategy_interstitial_ads.dart';
import 'package:guru_app/financial/asset/assets_model.dart';
import 'package:guru_app/financial/asset/assets_store.dart';
import 'package:guru_app/financial/iap/iap_manager.dart';
import 'package:guru_app/financial/iap/iap_model.dart';
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_utils/lifecycle/lifecycle_manager.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_app/property/property_keys.dart';
import 'package:guru_app/property/settings/guru_settings.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/network/network_utils.dart';
import 'package:guru_utils/tuple/tuple.dart';
import 'package:guru_utils/ads/ads.dart';
import 'applovin/interstitial/applovin_interstitial_ads.dart';
import 'applovin/rewarded/applovin_rewarded_ads.dart';
import 'utils/ads_exception.dart';
import 'package:device_info_plus/device_info_plus.dart';
part 'ads_global_property.dart';
/// Created by Haoyi on 2022/3/2
class AdsManager extends AdsManagerDelegate {
static final AdsManager _instance = AdsManager._();
static AdsManager get instance => _instance;
AdsManager._();
final Map<AdUnitId, Ads> interstitialAds = {};
final Map<AdUnitId, ApplovinRewardedAds> rewardsAds = {};
final AdImpressionController adImpressionController =
AdImpressionController();
final BehaviorSubject<AdsConfig> _adsConfigSubject =
BehaviorSubject.seeded(AdsConfig.defaultAdsConfig);
final BehaviorSubject<AdsProfile> _adsProfileSubject =
BehaviorSubject.seeded(GuruApp.instance.adsProfile);
final BehaviorSubject<bool> _initializedSubject =
BehaviorSubject.seeded(false);
final BehaviorSubject<bool> noBannerAndInterstitialAdsSubject =
BehaviorSubject.seeded(false);
final Map<String, dynamic> adsGlobalProperties = <String, dynamic>{};
static const Set<String> _reservedKeywords = {
"app_version",
"lt",
"paid",
"blv",
"os_version",
"connection"
};
static const List<int> ltSamples = [
0,
1,
2,
3,
4,
5,
6,
14,
30,
60,
90,
120,
180
];
@override
Stream<bool> get observableInitialized => _initializedSubject.stream;
ConnectivityResult get connectivityStatus => NetworkUtils.currentConnectivityStatus;
Stream<ConnectivityResult> get observableConnectivityStatus =>
NetworkUtils.observableConnectivityStatus;
@override
Stream<bool> get observableNoAds => noBannerAndInterstitialAdsSubject.stream;
final CompositeSubscription subscriptions = CompositeSubscription();
AdsProfile get adsProfile => _adsProfileSubject.value;
AdsConfig get adsConfig => _adsConfigSubject.value;
bool get hasAmazonBannerAds => adsConfig.bannerConfig.amazonEnable;
bool get hasAmazonInterstitialAds =>
adsConfig.interstitialConfig.amazonEnable;
bool get hasAmazonAds => hasAmazonBannerAds || hasAmazonInterstitialAds;
final BehaviorSubject<Map<String, String>> keywordsSubject =
BehaviorSubject.seeded({});
Stream<Map<String, String>> get observableKeywords => keywordsSubject.stream;
Map<String, String> get adsKeywords => keywordsSubject.value;
String? consentTestDeviceId;
int? consentDebugGeography;
static final RegExp _nonAlphaNumeric = RegExp('[^a-zA-Z0-9_]');
static final RegExp _alpha = RegExp('[a-zA-Z]');
@override
bool get isPurchasedNoAd => noBannerAndInterstitialAdsSubject.value;
void setProperty(String key, String value) {
adsGlobalProperties[key] = value;
}
void setNoAds(bool noAds) {
noBannerAndInterstitialAdsSubject.addIfChanged(noAds);
GuruSettings.instance.isNoAds.set(noAds);
GuruAnalytics.instance
.setUserProperty("user_type", noAds ? "noads" : "default");
setProperty("user_type", noAds ? "noads" : "default");
}
void ensureInitialize() {}
void listenIap() {
final obs = Rx.combineLatest2<bool, AssetsStore<Asset>,
Tuple2<bool, AssetsStore<Asset>>>(
IapManager.instance.observableAvailable,
IapManager.instance.observableAssetStore,
(a, b) => Tuple2(a, b));
subscriptions.add(obs.listen((tuple) {
final available = tuple.item1;
final purchasedStore = tuple.item2;
if (available && purchasedStore.isActive) {
final tempIsNoAds = purchasedStore
.existsAssets(GuruApp.instance.productProfile.noAdsCapIds);
final isNoAds = isPurchasedNoAd;
Log.i(
"purchased store changed active! tempIsNoAds:$tempIsNoAds isNoAds:$isNoAds",
syncFirebase: true);
if (isNoAds != tempIsNoAds) {
if (!tempIsNoAds) {
GuruAnalytics.instance.logException(NoAdsException(
"The payment system is abnormal, it shouldn't appear that the purchased item become unpurchased"));
}
setNoAds(tempIsNoAds);
}
}
}));
}
static bool initializedSdk = false;
Future initialize({SaasUser? saasUser}) async {
_adsProfileSubject.addEx(GuruApp.instance.adsProfile);
await initEnv();
final connected = await NetworkUtils.isNetworkConnected();
Log.d("adsManager initialize connected:$connected", tag: "Ads");
if (connected) {
await initSdk(
saasUser: saasUser,
onInitialized: () {
// loadAds();
adImpressionController.init();
checkAndPreload();
// GuruSettings.instance.totalLevelUp
// .observe()
// .throttleTime(const Duration(seconds: 1))
// .listen((count) {
// checkAndPreload();
// });
Log.i("ADS Initialized", tag: "Ads", syncFirebase: true);
});
} else {
NetworkUtils.observableConnectivityTrack.listen((track) async {
if (track.newResult != ConnectivityResult.none) {
if (!initializedSdk) {
initializedSdk = true;
await initSdk(onInitialized: () {
adImpressionController.init();
checkAndPreload();
Log.i("ADS Initialized", tag: "Ads", syncFirebase: true);
});
}
}
if (track.newResult != track.oldResult) {
Log.i("connectivity result changed! retry ads!", tag: "Ads");
setKeyword("connection", track.newResult.toString());
if (LifecycleManager.instance.isAppForeground()) {
if (track.newResult == ConnectivityResult.none &&
track.oldResult != ConnectivityResult.none) {
Log.i("connectivity changed! retry ads!", tag: "Ads");
retry();
}
}
}
});
}
subscriptions.add(RemoteConfigManager.instance.observeConfig().listen((_) {
refreshAdsConfig();
}, onError: (error, stacktrace) {
Log.i("init config error!",
tag: "Ads", error: error, stackTrace: stacktrace);
}));
listenIap();
}
// void initLifecycleConnectivity() {
// StreamSubscription? streamSubscription;
// LifecycleManager.instance.observableAppLifecycle.listen((foreground) {
// if (foreground) {
// streamSubscription = Connectivity()
// .onConnectivityChanged
// .listen((ConnectivityResult result) {
// Log.i("Connectivity: $result", tag: "Connectivity");
// if (connectivityStatus == ConnectivityResult.none &&
// result != ConnectivityResult.none) {
// Log.i("connectivity changed! retry ads!", tag: "Ads");
// retry();
// }
// final changed = connectivityStatusSubject.addIfChanged(result);
// if (changed) {
// setKeyword("connection", result.toString());
// }
// });
// } else {
// streamSubscription?.cancel();
// streamSubscription = null;
// }
// });
// }
void initAdsProfile() {
final _hasAmazonBannerAds = hasAmazonBannerAds;
final _hasAmazonInterstitialAds = hasAmazonInterstitialAds;
final _hasAmazonAds = _hasAmazonBannerAds || _hasAmazonInterstitialAds;
final defaultAdsProfile = GuruApp.instance.adsProfile;
final strategyInterstitialIds = adsConfig.strategyAdsConfig.interstitialIds;
final newAdsProfile = adsProfile.copyWith(
amazonAppId: _hasAmazonAds ? defaultAdsProfile.amazonAppId : null,
amazonBannerSlotId:
_hasAmazonBannerAds ? defaultAdsProfile.amazonBannerSlotId : null,
amazonInterstitialSlotId: _hasAmazonInterstitialAds
? defaultAdsProfile.amazonInterstitialSlotId
: null,
strategyInterstitialIds: strategyInterstitialIds);
_adsProfileSubject.addEx(newAdsProfile);
}
Future initEnv() async {
final adsPropertyBundle =
await AppProperty.getInstance().loadValuesByTag(PropertyTags.ads);
final isNoAds = adsPropertyBundle.getBool(PropertyKeys.isNoAds) ?? false;
consentTestDeviceId =
adsPropertyBundle.getString(PropertyKeys.admobConsentTestDeviceId);
consentDebugGeography =
adsPropertyBundle.getInt(PropertyKeys.admobConsentDebugGeography);
noBannerAndInterstitialAdsSubject.addIfChanged(isNoAds);
GuruAnalytics.instance
.setUserProperty("user_type", isNoAds ? "noads" : "default");
setProperty("user_type", isNoAds ? "noads" : "default");
final result = await Connectivity().checkConnectivity().catchError((error) {
Log.w("checkConnectivity error! $error");
});
// connectivityStatusSubject.addEx(result);
setProperty("connectivityStatus", result.toString());
refreshAdsConfig();
initAdsProfile();
}
Future initSdk(
{SaasUser? saasUser,
required VoidCallback onInitialized,
Duration retryPeriod = const Duration(seconds: 15)}) async {
final _adsProfile = adsProfile;
bool initializeResult = false;
if (GuruApp.instance.appSpec.deployment.adsCompliantInitialization &&
adsConfig.commonAdsConfig.compliantInitialization &&
Platform.isAndroid) {
initializeResult = await GuruApplovinFlutter.instance
.gatherConsentAndInitialize(
userId: saasUser?.uid,
amazonAppId: _adsProfile.amazonAppId?.id,
pubmaticStoreUrl: adsProfile.pubmaticAppStoreUrl,
testDeviceId: consentTestDeviceId,
debugGeography: consentDebugGeography)
.catchError((err) => false) ??
false;
} else {
initializeResult = await GuruApplovinFlutter.instance
.initialize(
userId: saasUser?.uid,
amazonAppId: _adsProfile.amazonAppId?.id,
pubmaticStoreUrl: adsProfile.pubmaticAppStoreUrl)
.catchError((err) => false) ??
false;
}
_initializedSubject.addEx(initializeResult);
Log.d("MAX sdk initialize result: $initializeResult");
if (initializeResult) {
try {
await initKeywords();
} catch (error, stacktrace) {
Log.e("initKeywords error! $error $stacktrace", tag: "Ads");
}
onInitialized.call();
} else {
Future.delayed(retryPeriod, () {
initSdk(onInitialized: onInitialized, retryPeriod: retryPeriod);
});
Log.w("Ads Initialize error! retry", tag: "Ads", syncFirebase: true);
}
return initializeResult;
}
void checkAndPreload(
{AdsValidator? rewardedValidator,
AdsValidator? interstitialValidator}) async {
final canPreloadReward =
await adsConfig.rewardedConfig.canPreload(validator: rewardedValidator);
if (canPreloadReward) {
Log.d("preload reward canPreload!");
final reward = await getRewardsAds();
if (reward.loadCount <= 0) {
reward.preload();
}
}
final canPreloadInterstitial = await adsConfig.interstitialConfig
.canPreload(validator: interstitialValidator);
if (!isPurchasedNoAd && canPreloadInterstitial) {
Log.d("preload interstitial canPreload!");
final interstitial = await getInterstitialAds();
if (interstitial is AdsAudit && interstitial.loadCount <= 0) {
interstitial.preload();
}
}
}
void retry() async {
final canPreloadReward = await adsConfig.rewardedConfig.canPreload();
if (canPreloadReward) {
final reward = await getRewardsAds();
reward.retry();
}
final canPreload = await adsConfig.interstitialConfig.canPreload();
if (canPreload) {
Log.d("preload interstitial canPreload!");
final interstitial = await getInterstitialAds();
interstitial.retry();
}
}
static int _nearestLt(int low, int high, int lt) {
if (low > high) {
return -low;
}
while (low <= high) {
final int mid = (low + high) >> 1;
if (lt == ltSamples[mid]) {
return mid;
} else if (lt < ltSamples[mid]) {
return _nearestLt(low, mid - 1, lt);
} else {
return _nearestLt(mid + 1, high, lt);
}
}
return -low;
}
Future<int> getKeywordLt() async {
final latestLtDate = await AppProperty.getInstance().getLatestLtDate();
final dateNum = DateTimeUtils.yyyyMMddUtcNum;
int lt = await AppProperty.getInstance().getLtDays();
if (dateNum != latestLtDate) {
if (dateNum > latestLtDate) {
lt = lt + 1;
await AppProperty.getInstance().setLtDays(lt);
}
await AppProperty.getInstance().setLatestLtDate(dateNum);
}
final idx = _nearestLt(0, ltSamples.lastIndex, lt)
.abs()
.clamp(0, ltSamples.lastIndex);
Log.d(
"getKeywordLt: installTime:$latestLtDate now:$dateNum lt:$lt keywordLt:${ltSamples[idx]}");
return ltSamples[idx];
}
Future<String> getOSVersion() async {
try {
final deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
final info = await deviceInfo.androidInfo;
return info.version.release;
} else if (Platform.isIOS) {
final info = await deviceInfo.iosInfo;
return info.systemVersion;
}
} catch (error, stacktrace) {
Log.w("getOSVersion error! $error");
}
return "unknown";
}
Future<String> getConnection() async {
try {
final connectivity = await Connectivity().checkConnectivity();
return connectivity.toString();
} catch (error, stacktrace) {
Log.w("getConnection error! $error");
}
return "unknown";
}
Future initKeywords() async {
final paidUser = await AppProperty.getInstance().isPaidUser();
final version = Settings.get().version.get();
final lt = await getKeywordLt();
final osVersion = await getOSVersion();
final connection = await getConnection();
final keywords = <String, String>{
"app_version": version,
"paid": paidUser ? "true" : "false",
"lt": lt.toString(),
"os_version": osVersion,
"connection": connection
};
keywordsSubject.stream.listen((keywords) {
if (keywords.isNotEmpty) {
Log.i("invoke setKeywords: $keywords", tag: "Ads");
GuruApplovinFlutter.instance.setKeywords(keywords);
}
});
keywordsSubject.addEx(keywords);
}
Future restoreKeywords(Map<String, String> keywords) async {
if (GuruSettings.instance.debugMode.get()) {
final newKeywords = Map.of(keywords);
keywordsSubject.addEx(newKeywords);
}
}
void setKeyword(String key, String value, {bool debugForce = false}) {
if (!GuruSettings.instance.debugMode.get() || !debugForce) {
if (_reservedKeywords.contains(key)) {
Log.w("setKeyword error! the key($key) is reserved and cannot be used!",
tag: "Ads");
return;
}
if (key.isEmpty ||
key.length > 36 ||
key.indexOf(_alpha) != 0 ||
key.contains(_nonAlphaNumeric)) {
Log.w(
"setKeyword error! the key($key) must contain 1 to 36 alphanumeric characters.",
tag: "Ads");
return;
}
}
final newKeywords = Map.of(keywordsSubject.value);
newKeywords[key] = value;
keywordsSubject.addEx(newKeywords);
}
void removeKeyword(String key, {bool debugForce = false}) {
if (!GuruSettings.instance.debugMode.get() || !debugForce) {
if (_reservedKeywords.contains(key)) {
Log.w(
"removeKeyword error! the key($key) is reserved and cannot be used!",
tag: "Ads");
return;
}
if (key.isEmpty ||
key.length > 36 ||
key.indexOf(_alpha) != 0 ||
key.contains(_nonAlphaNumeric)) {
Log.w(
"removeKeyword error! the key($key) must contain 1 to 36 alphanumeric characters.",
tag: "Ads");
return;
}
}
final newKeywords = Map.of(keywordsSubject.value);
newKeywords.remove(key);
keywordsSubject.addEx(newKeywords);
}
Future<int> checkConsentDialogStatus() async {
return await GuruApplovinFlutter.instance.checkConsentDialogStatus();
}
Future<bool> afterAcceptPrivacy(bool consentResult) async {
return await GuruApplovinFlutter.instance.afterAcceptPrivacy(consentResult);
}
bool testParseAdsDefaultConfig() {
final iadsConfigString =
RemoteConfigReservedConstants.getDefaultConfigString(
RemoteConfigReservedConstants.iadsConfig) ??
"";
final radsConfigString =
RemoteConfigReservedConstants.getDefaultConfigString(
RemoteConfigReservedConstants.radsConfig) ??
"";
final badsConfigString =
RemoteConfigReservedConstants.getDefaultConfigString(
RemoteConfigReservedConstants.badsConfig) ??
"";
final iosAttConfigString =
RemoteConfigReservedConstants.getDefaultConfigString(
RemoteConfigReservedConstants.iosAttConfig) ??
"";
try {
final adInterstitial =
AdInterstitialConfig.fromJson(json.decode(iadsConfigString));
final adBanner = AdBannerConfig.fromJson(json.decode(badsConfigString));
final iosAttConfig =
IOSAttConfig.fromJson(json.decode(iosAttConfigString));
Log.d("==== ADS AdsConfig ====");
Log.d(" ---> [INTERSTITIAL]: $iadsConfigString");
Log.d(" ---> [BANNER]: $badsConfigString");
Log.d(" ---> [IOSATT]: $iosAttConfigString");
Log.d("=======================");
_adsConfigSubject.addEx(AdsConfig.build(
interstitialConfig: adInterstitial,
bannerConfig: adBanner,
iosAttConfig: iosAttConfig));
return true;
} catch (error, stacktrace) {
Log.e("refreshAdsConfig error $error $stacktrace");
rethrow;
}
}
bool refreshAdsConfig() {
try {
final commonAdsConfig = RemoteConfigManager.instance.getCommonAdsConfig();
final adInterstitial = RemoteConfigManager.instance.getIadsConfig();
final adReward = RemoteConfigManager.instance.getRadsConfig();
final adBanner = RemoteConfigManager.instance.getBadsConfig();
final strategyAdsConfig =
RemoteConfigManager.instance.getStrategyAdsConfig();
final iosAttConfig = RemoteConfigManager.instance.getIOSAttConfig();
Log.d("==== ADS AdsConfig ====", tag: PropertyTags.ads);
Log.d(" ---> [COMMON]: ${commonAdsConfig.toJson()}",
tag: PropertyTags.ads);
Log.d(" ---> [INTERSTITIAL]: ${adInterstitial.toJson()}",
tag: PropertyTags.ads);
Log.d(" ---> [REWARD]: ${adReward.toJson()}", tag: PropertyTags.ads);
Log.d(" ---> [BANNER]: ${adBanner.toJson()}", tag: PropertyTags.ads);
Log.d(" ---> [STRATEGY]: ${strategyAdsConfig.toJson()}",
tag: PropertyTags.ads);
Log.d(" ---> [IOSATT]: ${iosAttConfig.toJson()}", tag: PropertyTags.ads);
Log.d("=======================", tag: PropertyTags.ads);
_adsConfigSubject.addEx(AdsConfig.build(
commonAdsConfig: commonAdsConfig,
interstitialConfig: adInterstitial,
rewardedConfig: adReward,
bannerConfig: adBanner,
strategyAdsConfig: strategyAdsConfig,
iosAttConfig: iosAttConfig));
return true;
} catch (error, stacktrace) {
Log.e("refreshAdsConfig error $error $stacktrace");
rethrow;
}
}
@override
Future<Ads> getInterstitialAds() async {
final _adsProfile = adsProfile;
final strategyInterstitialIds = adsProfile.strategyInterstitialIds ?? [];
Ads? ad;
if (strategyInterstitialIds.isNotEmpty) {
if (strategyInterstitialIds.length > 1) {
ad = interstitialAds[strategyInterstitialIds.first.adUnitId] ??=
MaxStrategyInterstitialAds.create(strategyInterstitialIds)..init();
} else {
ad = interstitialAds[strategyInterstitialIds.first.adUnitId] ??=
ApplovinInterstitialAds.create(
strategyInterstitialIds.first.adUnitId,
strategyInterstitialIds.first.amazonAdSlotId)
..init();
}
} else {
ad = interstitialAds[_adsProfile.interstitialId] ??=
ApplovinInterstitialAds.create(
_adsProfile.interstitialId, _adsProfile.amazonInterstitialSlotId)
..init();
}
return ad;
}
@override
Future<ApplovinRewardedAds> getRewardsAds() async {
final _adsProfile = adsProfile;
ApplovinRewardedAds? ad = rewardsAds[_adsProfile.rewardsId];
if (ad == null) {
ad = ApplovinRewardedAds.create(_adsProfile.rewardsId,
adAmazonSlotId: _adsProfile.amazonRewardedSlotId)
..init();
rewardsAds[_adsProfile.rewardsId] = ad;
}
return ad;
}
Future<int> requestGdpr({int? debugGeography, String? testDeviceId}) async {
Log.d(
"requestGdpr! debugGeography:$debugGeography testDeviceId:$testDeviceId",
tag: "Ads");
// adb logcat -s UserMessagingPlatform
// Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("xxxx") to set this as a debug device.
final result = await GuruApplovinFlutter.instance.requestGdpr(
debugGeography: debugGeography, testDeviceId: testDeviceId);
final consentResult = await GuruAnalytics.instance.refreshConsents();
Log.d("requestGdpr result:$result consentResult:$consentResult");
return result;
}
Future<bool> resetGdpr() {
return GuruApplovinFlutter.instance.resetGdpr();
}
Future<bool> updateOrientation(int orientation) async {
final result =
await GuruApplovinFlutter.instance.updateOrientation(orientation);
return result == true;
}
@override
Future<ApplovinBannerAds> createBannerAds(
{String? scene, AdsLifecycleObserver? observer}) async {
final _adsProfile = adsProfile;
return ApplovinBannerAds.create(
_adsProfile.bannerId, _adsProfile.amazonBannerSlotId,
scene: scene, observer: observer);
}
AdCause canShowInterstitial(String scene) {
if (isPurchasedNoAd) {
return AdCause.noAds;
}
final hiddenAt = AdsManager.instance.latestFullscreenAdsHiddenTimestamps;
final now = DateTimeUtils.currentTimeInMillis();
final impGapInMillis = AdsManager.instance.adsConfig.interstitialConfig
.getSceneImpGapInSeconds(scene) *
1000;
Log.d(
"canShowInterstitial($scene): now:$now latestFullscreenAdsHiddenTimestamps:$latestFullscreenAdsHiddenTimestamps hiddenAt:$hiddenAt impGapInMillis:$impGapInMillis",
tag: "Ads");
if ((now - hiddenAt) < impGapInMillis) {
Log.d("show ads too frequency", syncFirebase: true);
return AdCause.tooFrequent;
}
return AdCause.success;
}
@override
Future<AdCause> validateInterstitial(String? scene,
{AdsValidator? validator}) {
final interstitialConfig = adsConfig.interstitialConfig;
return interstitialConfig.check(scene ?? "", validator: validator);
}
@override
Future<AdCause> validateRewards(String? scene, {AdsValidator? validator}) {
final rewardedConfig = adsConfig.rewardedConfig;
return rewardedConfig.check(scene ?? "", validator: validator);
}
@override
Future<AdCause> validateBanner(String? scene, {AdsValidator? validator}) {
final rewardedConfig = adsConfig.bannerConfig;
return rewardedConfig.check(scene ?? "", validator: validator);
}
@override
dynamic getConfig(String type) {
switch (type) {
case "bannerAutoDisposeInterval":
return adsConfig.bannerConfig.autoDisposeIntervalInMinutes;
case "allowInterstitialAsAlternativeReward":
return GuruApp
.instance.appSpec.deployment.allowInterstitialAsAlternativeReward;
case "showInternalAdsWhenBannerUnavailable":
return GuruApp
.instance.appSpec.deployment.showInternalAdsWhenBannerUnavailable;
}
}
}

View File

@ -0,0 +1,107 @@
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/ads_config.dart';
// import 'package:guru_utils/ads/data/ads.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_applovin_flutter/banner_ad.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
import 'package:guru_utils/ads/ads.dart';
/// Created by Haoyi on 5/10/21
class ApplovinBannerAds extends BannerAds<BannerAdEvent> {
late BannerAd bannerAd;
@override
final AdUnitId adUnitId;
final AdSlotId? adAmazonSlotId;
ApplovinBannerAds.create(this.adUnitId, this.adAmazonSlotId,
{String? scene, AdsLifecycleObserver? observer}) {
bannerAd = BannerAd(
adUnitId: adUnitId.id, adAmazonSlotId: adAmazonSlotId?.id, listener: dispatchEvent);
init();
this.scene = scene ?? "";
if (observer != null) {
addObserver(observer);
}
}
@override
Map<BannerAdEvent, AdsEvent> get eventsMapping => {
BannerAdEvent.onAdLoaded: AdsEvent.adLoaded,
BannerAdEvent.onAdLoadFailed: AdsEvent.adLoadFailed,
BannerAdEvent.onAdDisplayFailed: AdsEvent.adDisplayFailed,
BannerAdEvent.onAdDisplayed: AdsEvent.adDisplayed,
BannerAdEvent.onAdClicked: AdsEvent.adClick,
BannerAdEvent.onAdHidden: AdsEvent.adHidden,
};
void hideOtherBanner() {
// MoPubBannerAd.allBannerAds.forEach((key, value) {
// if (bannerAd.id != key) {
// value.hide();
// }
// });
}
@override
Future<bool> requestDispose() async {
try {
return await bannerAd.dispose() ?? false;
} catch (error, stacktrace) {
Log.d("dispose error:$error $stacktrace");
return false;
}
}
@override
Future<bool> requestHide() async {
try {
return await bannerAd.hide() ?? false;
} catch (error, stacktrace) {
Log.d("requestHide error:$error $stacktrace");
return false;
}
}
@override
Future<AdCause> requestLoad() async {
try {
Log.w("[$runtimeType] requestLoad", tag: "Ads");
final result = await bannerAd.load(placement: scene) ?? false;
if (result) {
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.w("requestLoad error! $error $stacktrace");
return AdCause.internalError;
}
}
@override
Future<AdCause> requestShow({required String scene, bool ignoreCheck = false}) async {
hideOtherBanner();
try {
final result = await bannerAd.show(anchorOffset: 3.0) ?? false;
if (result) {
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.i("Banner show error $error $stacktrace");
}
return AdCause.internalError;
}
Future<bool> checkLoaded() async {
return true;
}
@override
Future<int> getStatus() async {
return AdStatus.LOADING;
}
}

View File

@ -0,0 +1,124 @@
import 'package:guru_app/ads/ads_manager.dart';
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/ads_config.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
import 'package:guru_applovin_flutter/interstitial_ad.dart';
import 'package:guru_utils/ads/ads.dart';
/// Created by Haoyi on 5/6/21
class ApplovinInterstitialAds extends InterstitialAds<InterstitialAdEvent> {
late InterstitialAd interstitialAd;
@override
final AdUnitId adUnitId;
final AdSlotId? adAmazonSlotId;
// @override
// RetryConfig get retryConfig {
// final adsService = Injector.provide<AdsService>();
// return adsService.adsConfig.interstitialConfig.retryConfig;
// }
ApplovinInterstitialAds.create(this.adUnitId, this.adAmazonSlotId);
@override
void init() {
super.init();
interstitialAd = InterstitialAd(
adUnitId: adUnitId.id, adAmazonSlotId: adAmazonSlotId?.id, listener: dispatchEvent);
}
@override
Map<InterstitialAdEvent, AdsEvent> get eventsMapping => {
InterstitialAdEvent.onAdLoaded: AdsEvent.adLoaded,
InterstitialAdEvent.onAdLoadFailed: AdsEvent.adLoadFailed,
InterstitialAdEvent.onAdDisplayFailed: AdsEvent.adDisplayFailed,
InterstitialAdEvent.onAdDisplayed: AdsEvent.adDisplayed,
InterstitialAdEvent.onAdClicked: AdsEvent.adClick,
InterstitialAdEvent.onAdHidden: AdsEvent.adHidden,
};
@override
Future<bool> requestDispose() async {
try {
return await interstitialAd.dispose() ?? false;
} catch (error, stacktrace) {
Log.w("requestDispose error", error: error, stackTrace: stacktrace);
return false;
}
}
@override
Future<bool> requestHide() async {
return false;
}
@override
Future<AdCause> requestLoad() async {
try {
Log.w("[$runtimeType] requestLoad", tag: "Ads");
final result = await interstitialAd.load() ?? false;
if (result) {
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.w("requestLoad error! $error $stacktrace");
return AdCause.internalError;
}
}
@override
Future<AdCause> requestShow({required String scene, bool ignoreCheck = false}) async {
// final now = DateTimeUtils.currentTimeInMillis();
// final impGapInMillis = AdsManager.instance.adsConfig.interstitialConfig.impGapInSeconds * 1000;
// if ((now - AdsManager.instance.latestFullscreenAdsHiddenTimestamps) < 60000 ||
// ((now - latestHiddenAt) < impGapInMillis)) {
// Log.d("show ads too frequency", syncFirebase: true);
// return AdCause.tooFrequent;
// }
if (!ignoreCheck) {
final result = AdsManager.instance.canShowInterstitial(scene);
if (result != AdCause.success) {
return result;
}
}
Log.d("[$runtimeType] requestShow", tag: "Ads", syncFirebase: true);
try {
final result = await interstitialAd.show(placement: scene) ?? false;
if (result) {
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.w("requestShow error", error: error, stackTrace: stacktrace, syncFirebase: true);
return AdCause.internalError;
}
}
@override
Future<bool> checkLoaded() async {
try {
return await interstitialAd.isLoaded() ?? false;
} catch (error, stacktrace) {
return false;
}
}
@override
Future<int> getStatus() async {
try {
return await interstitialAd.getAdState();
} catch (error, stacktrace) {
Log.w("getInterstitialAdStatus error",
error: error, stackTrace: stacktrace, syncFirebase: true);
return AdStatus.FAILED;
}
}
}

View File

@ -0,0 +1,129 @@
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/ads_config.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
import 'package:guru_applovin_flutter/rewarded_video_ad.dart';
import 'package:guru_utils/ads/ads.dart';
/// Created by Haoyi on 5/26/21
class ApplovinRewardedAds extends RewardedAds<RewardedVideoAdEvent> {
late RewardedVideoAd rewardedVideoAd;
@override
final AdUnitId adUnitId;
final AdSlotId? adAmazonSlotId;
ApplovinRewardedAds.create(this.adUnitId, {this.adAmazonSlotId});
// @override
// RetryConfig get retryConfig {
// final adsService = Injector.provide<AdsService>();
// return adsService.adsConfig.rewardedConfig.retryConfig;
// }
@override
void init() {
super.init();
rewardedVideoAd = RewardedVideoAd(
adUnitId: adUnitId.id, adAmazonSlotId: adAmazonSlotId?.id, listener: dispatchEvent);
}
@override
Map<RewardedVideoAdEvent, AdsEvent> get eventsMapping => {
RewardedVideoAdEvent.onAdLoaded: AdsEvent.adLoaded,
RewardedVideoAdEvent.onAdLoadFailed: AdsEvent.adLoadFailed,
RewardedVideoAdEvent.onAdDisplayFailed: AdsEvent.adDisplayFailed,
RewardedVideoAdEvent.onAdDisplayed: AdsEvent.adDisplayed,
RewardedVideoAdEvent.onAdClicked: AdsEvent.adClick,
RewardedVideoAdEvent.onAdHidden: AdsEvent.adHidden,
RewardedVideoAdEvent.onUserRewarded: AdsEvent.adRewarded
};
@override
Future<bool> requestDispose() async {
try {
return await rewardedVideoAd.dispose() ?? false;
} catch (error, stacktrace) {
Log.w("requestDispose error! $error $stacktrace");
return false;
}
}
@override
Future<bool> requestHide() async {
return false;
}
@override
Future<AdCause> requestLoad() async {
try {
Log.w("[$runtimeType] requestLoad", tag: "Ads");
final result = await rewardedVideoAd.load() ?? false;
if (result) {
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.w("requestLoad error! $error $stacktrace");
return AdCause.internalError;
}
}
@override
Future<AdCause> requestReset() async {
try {
Log.w("[$runtimeType] requestLoad", tag: "Ads");
final result = await rewardedVideoAd.dispose() ?? false;
if (result) {
rewardedVideoAd = RewardedVideoAd(adUnitId: adUnitId.id, listener: dispatchEvent);
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.w("requestLoad error! $error $stacktrace");
return AdCause.internalError;
}
}
@override
Future<AdCause> requestShow({required String scene, bool ignoreCheck = false}) async {
// final now = DateTimeUtils.currentTimeInMillis();
// if (now - latestHiddenAt < 60000) {
// Log.i("show ads too frequency");
// return false;
// }
// Log.i("show interstitial");
try {
Log.d("[$hashCode]requestShow rewardedAds", syncFirebase: true);
final result = await rewardedVideoAd.show(placement: scene) ?? false;
if (result) {
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.w("requestShow rewarded error", error: error, stackTrace: stacktrace, syncFirebase: true);
return AdCause.internalError;
}
}
@override
Future<int> getStatus() async {
try {
return await rewardedVideoAd.getAdState();
} catch (error, stacktrace) {
Log.w("getRewardedAdStatus error", error: error, stackTrace: stacktrace, syncFirebase: true);
return AdStatus.FAILED;
}
}
@override
bool needReset() {
Log.d("reset check elapsedTimeInMillisSinceStartLoadAds $elapsedTimeInMillisSinceStartLoadAds",
tag: "Ads");
return elapsedTimeInMillisSinceStartLoadAds > 30 * 1000;
}
}

View File

@ -0,0 +1,201 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:guru_app/ads/ads_manager.dart';
import 'package:guru_app/ads/core/ads_config.dart';
import 'package:guru_app/aigc/bi/ai_bi.dart';
import 'package:guru_app/analytics/guru_analytics.dart';
import 'package:guru_app/hook/hook_manager.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_app/property/property_keys.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
import 'package:guru_analytics_flutter/events_constants.dart';
import 'package:rxdart/rxdart.dart';
import 'package:guru_utils/ads/ads.dart';
part 'banner/banner_ads.dart';
part 'banner/banner_ads_handler.dart';
part 'handler/ads_audit.dart';
part 'interstitial/interstitial_ads.dart';
part 'interstitial/interstitial_ads_handler.dart';
part 'rewards/rewarded_ads.dart';
part 'rewards/rewarded_ads_handler.dart';
part 'handler/ads_cache.dart';
/// Created by Haoyi on 5/6/21
abstract class Ads extends AdsLifecycleOwner with AdsDelegate {
set loaded(bool loaded);
final Map<String, String> properties = <String, String>{};
void setProperty(String name, String data) {
properties[name] = data;
}
void retry() {}
void preload() {}
}
abstract class SingleAds<T> extends Ads {
Map<T, AdsEvent> get eventsMapping;
AdUnitId get adUnitId;
@override
set loaded(bool loaded) {
loadedSubject.addEx(loaded);
setProperty("isLoaded", loaded ? "true" : "false");
}
@override
bool get loaded => loadedSubject.value == true;
final BehaviorSubject<bool> loadedSubject = BehaviorSubject.seeded(false);
@override
Stream<bool> get observableLoaded => loadedSubject.stream;
void dispatchEvent(T event, {Map<dynamic, dynamic> arguments = const <dynamic, dynamic>{}}) {
final adsEvent = eventsMapping[event];
if (adsEvent != null) {
final adsBundle = AdsBundle.create(this, arguments: arguments);
switch (adsEvent) {
case AdsEvent.adLoaded:
onAdLoaded(adsBundle);
break;
case AdsEvent.adLoadFailed:
onAdLoadFailed(adsBundle);
break;
case AdsEvent.adDisplayed:
onAdDisplayed(adsBundle);
break;
case AdsEvent.adDisplayFailed:
onAdDisplayFailed(adsBundle);
break;
case AdsEvent.adClick:
onAdClicked(adsBundle);
break;
case AdsEvent.adHidden:
onAdHidden(adsBundle);
break;
case AdsEvent.adRewarded:
onAdRewarded(adsBundle);
break;
}
}
}
@override
@mustCallSuper
Future dispose() async {
bool result = false;
try {
result = await requestDispose();
} catch (error, stacktrace) {
Log.d("requestDispose error:$error $stacktrace");
}
onRequestDispose(AdsBundle.create(this));
super.dispose();
return result;
}
@override
@mustCallSuper
Future<bool> reset() async {
AdCause result = AdCause.internalError;
try {
result = await requestReset();
} catch (error, stacktrace) {
Log.d("requestReset error:$error $stacktrace");
}
onRequestReset(AdsBundle.create(this));
return result == AdCause.success;
}
@mustCallSuper
void init() {
if (this is AdsAudit) {
Log.d("[$runtimeType]AdsAudit === add AdsAuditObserver", tag: "Ads");
addObserver(AdsAuditObserver(runtimeType.toString()));
}
if (this is AdsCache) {
Log.d("[$runtimeType]AdsReload === add AdsReloadObserver", tag: "Ads");
addObserver(AdsCacheObserver(runtimeType.toString()));
}
}
@override
Future<AdCause> load() async {
final adCause = await requestLoad().catchError((error, stacktrace) {
Log.e("load error! ", tag: "Ads", error: error, stackTrace: stacktrace, syncFirebase: true);
return AdCause.internalError;
});
Log.d("[$runtimeType]load complete!! $adCause", syncFirebase: true);
onRequestLoad(AdsBundle.create(this, arguments: {"cause": adCause}));
return adCause;
}
@override
Future hide() async {
onRequestHide(AdsBundle.create(this));
return await requestHide();
}
@override
Future<AdCause> show({required String scene, bool ignoreCheck = false}) async {
final adCause =
await requestShow(scene: scene, ignoreCheck: ignoreCheck).catchError((error, stacktrace) {
Log.e("show error! $error", stackTrace: stacktrace, syncFirebase: true);
return AdCause.internalError;
});
Log.d("[$runtimeType]show $scene complete!! $adCause", syncFirebase: true);
onRequestShow(AdsBundle.create(this, arguments: {"scene": scene, "cause": adCause}));
return adCause;
}
Future<AdCause> requestLoad();
Future<AdCause> requestShow({required String scene, bool ignoreCheck = false});
Future<bool> requestHide();
Future<bool> requestDispose();
Future<AdCause> requestReset() async {
return AdCause.internalError;
}
Future<int> getStatus();
@override
Future<AdState> getState() async {
final status = await getStatus();
return convertAdStatusToAdState(status);
}
}
enum AdsEvent {
adLoaded,
adLoadFailed,
adDisplayed,
adDisplayFailed,
adClick,
adHidden,
adRewarded
}

View File

@ -0,0 +1,484 @@
import 'dart:convert';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_app/property/property_keys.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:guru_utils/converts/converts.dart';
import 'package:guru_utils/ads/ads.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
part 'ads_config.g.dart';
AdState convertAdStatusToAdState(int status) {
switch (status) {
case AdStatus.LOADED:
return AdState.loaded;
case AdStatus.CREATED:
return AdState.created;
case AdStatus.LOADING:
return AdState.loading;
default:
return AdState.failed;
}
}
@JsonSerializable()
class TaichiConfig {
@JsonKey(name: "enable", defaultValue: false)
final bool enable;
@JsonKey(name: "threshold", defaultValue: "")
final String threshold;
@JsonKey(name: "abnormal_threshold", defaultValue: 1.0)
final double abnormalThreshold;
TaichiConfig({this.enable = false, this.threshold = "", this.abnormalThreshold = 1.0});
factory TaichiConfig.fromJson(Map<String, dynamic> json) => _$TaichiConfigFromJson(json);
@override
String toString() {
return 'TaichiConfig{enable: $enable, threshold: $threshold}';
}
Map<String, dynamic> toJson() => _$TaichiConfigToJson(this);
}
@JsonSerializable()
class ImpressionData {
@JsonKey(name: "ad_platform", defaultValue: "MAX")
final String platform;
@JsonKey(name: "id", defaultValue: "")
final String id;
@JsonKey(name: "adunit_id", defaultValue: "")
final String unitId;
@JsonKey(name: "adunit_name", defaultValue: "")
final String unitName;
@JsonKey(name: "adunit_format", defaultValue: "")
final String unitFormat;
@JsonKey(name: "adgroup_id", defaultValue: "")
final String groupId;
@JsonKey(name: "adgroup_name", defaultValue: "")
final String groupName;
@JsonKey(name: "adgroup_type", defaultValue: "")
final String groupType;
@JsonKey(name: "currency", defaultValue: "")
final String currency;
@JsonKey(name: "country", defaultValue: "")
final String country;
@JsonKey(name: "app_version", defaultValue: "")
final String appVersion;
@JsonKey(name: "adgroup_priority", defaultValue: 0)
final int groupPriority;
@JsonKey(name: "publisher_revenue", defaultValue: -1)
final double publisherRevenue;
@JsonKey(name: "network_name", defaultValue: "")
final String networkName;
@JsonKey(name: "network_placement_id", defaultValue: "")
final String networkPlacementId;
@JsonKey(name: "precision", defaultValue: "")
final String precision;
@JsonKey(ignore: true)
late Map<String, dynamic> payload;
ImpressionData derive({double? newPublisherRevenue}) {
final newPayload = Map<String, dynamic>.from(payload);
newPayload["publisher_revenue"] = newPublisherRevenue ?? publisherRevenue;
return ImpressionData.fromJson(newPayload);
}
@override
String toString() {
return 'ImpressionData{platform: $platform, id: $id, unitId: $unitId, unitName: $unitName, unitFormat: $unitFormat, groupId: $groupId, groupName: $groupName, groupType: $groupType, currency: $currency, country: $country, appVersion: $appVersion, groupPriority: $groupPriority, publisherRevenue: $publisherRevenue, networkName: $networkName, networkPlacementId: $networkPlacementId, precision: $precision}';
}
ImpressionData(
{required this.platform,
required this.id,
required this.unitId,
required this.unitName,
required this.unitFormat,
required this.groupId,
required this.groupName,
required this.groupType,
required this.currency,
required this.country,
required this.appVersion,
required this.groupPriority,
required this.publisherRevenue,
required this.networkName,
required this.networkPlacementId,
required this.precision});
factory ImpressionData.fromJson(Map<String, dynamic> json) =>
_$ImpressionDataFromJson(json)..payload = json;
Map<String, dynamic> toJson() => _$ImpressionDataToJson(this);
}
@JsonSerializable()
class CpmCalibrationData {
@JsonKey(name: "list", defaultValue: <CpmCalibrationItem>[])
final List<CpmCalibrationItem> items;
final Map<String, double> _interstitialCpmCalibrationCountryMapping = {};
final Map<String, double> _rewardedCpmCalibrationCountryMapping = {};
CpmCalibrationData(this.items);
double getCpm(String format, String country) {
_ensureInitializedData();
final upperFormat = format.toUpperCase();
if (upperFormat.contains("FULLSCREEN") || upperFormat.contains("INTERSTITIAL")) {
return _interstitialCpmCalibrationCountryMapping[country] ?? -1;
}
if (upperFormat.contains("REWARDED")) {
return _rewardedCpmCalibrationCountryMapping[country] ?? -1;
}
return -1;
}
void _ensureInitializedData() {
if (_interstitialCpmCalibrationCountryMapping.isEmpty ||
_rewardedCpmCalibrationCountryMapping.isEmpty) {
for (CpmCalibrationItem item in items) {
if (item.format == "reward") {
_rewardedCpmCalibrationCountryMapping[item.country.toUpperCase()] = item.cpm;
} else if (item.format == "inter") {
_interstitialCpmCalibrationCountryMapping[item.country.toUpperCase()] = item.cpm;
}
}
}
}
factory CpmCalibrationData.fromJson(Map<String, dynamic> json) =>
_$CpmCalibrationDataFromJson(json);
Map<String, dynamic> toJson() => _$CpmCalibrationDataToJson(this);
}
@JsonSerializable()
class CpmCalibrationItem {
@JsonKey(name: "format")
final String format;
@JsonKey(name: "cpm")
final double cpm;
@JsonKey(name: "country")
final String country;
CpmCalibrationItem(this.format, this.cpm, this.country);
factory CpmCalibrationItem.fromJson(Map<String, dynamic> json) =>
_$CpmCalibrationItemFromJson(json);
Map<String, dynamic> toJson() => _$CpmCalibrationItemToJson(this);
}
@JsonSerializable()
class IRLDConfig {
@JsonKey(name: "fb_ecpm_cache_h", defaultValue: 12)
final int fbCpmCacheInHour;
@JsonKey(name: "fb_irld_report", defaultValue: false)
final bool fbIrldReport;
@JsonKey(name: "abnormal_threshold", defaultValue: 0.1)
final double abnormalThreshold;
IRLDConfig({this.fbCpmCacheInHour = 12, this.fbIrldReport = false, this.abnormalThreshold = 0.1});
factory IRLDConfig.fromJson(Map<String, dynamic> json) => _$IRLDConfigFromJson(json);
Map<String, dynamic> toJson() => _$IRLDConfigToJson(this);
@override
String toString() {
return 'IRLDConfig{fbCpmCacheInHour: $fbCpmCacheInHour, fbIrldReport: $fbIrldReport, abnormalThreshold: $abnormalThreshold}';
}
}
class AdsConfig {
final CommonAdsConfig commonAdsConfig;
final AdInterstitialConfig interstitialConfig;
final AdRewardedConfig rewardedConfig;
final AdBannerConfig bannerConfig;
final StrategyAdsConfig strategyAdsConfig;
final IOSAttConfig iosAttConfig;
AdsConfig.build(
{CommonAdsConfig? commonAdsConfig,
AdInterstitialConfig? interstitialConfig,
AdRewardedConfig? rewardedConfig,
AdBannerConfig? bannerConfig,
StrategyAdsConfig? strategyAdsConfig,
IOSAttConfig? iosAttConfig})
: commonAdsConfig = commonAdsConfig ?? CommonAdsConfig.fromJson(<String, dynamic>{}),
interstitialConfig =
interstitialConfig ?? AdInterstitialConfig.fromJson(<String, dynamic>{}),
rewardedConfig = rewardedConfig ?? AdRewardedConfig.fromJson(<String, dynamic>{}),
bannerConfig = bannerConfig ?? AdBannerConfig.fromJson(<String, dynamic>{}),
strategyAdsConfig = strategyAdsConfig ?? StrategyAdsConfig.fromJson(<String, dynamic>{}),
iosAttConfig = iosAttConfig ?? IOSAttConfig.fromJson(<String, dynamic>{});
static AdsConfig defaultAdsConfig = AdsConfig.build();
Map<String, String> dump() {
return {
"common_config": commonAdsConfig.toJson().toString(),
"iads_config": interstitialConfig.toJson().toString(),
"rads_config": rewardedConfig.toJson().toString(),
"bads_config": bannerConfig.toJson().toString(),
"sads_config": strategyAdsConfig.toJson().toString(),
"ios_att_config": iosAttConfig.toJson().toString()
};
}
}
@JsonSerializable()
class AdBannerConfig {
@JsonKey(name: "free_s", defaultValue: 600)
final int freeInSecond;
@JsonKey(name: "validation", defaultValue: '')
final String validation;
@JsonKey(name: "amazon_enable", defaultValue: false)
final bool amazonEnable;
@JsonKey(name: "pubmatic_enable", defaultValue: false)
final bool pubmaticEnable;
@JsonKey(name: "auto_dispose_interval_m", defaultValue: 5)
final int autoDisposeIntervalInMinutes;
@override
String toString() {
return 'AdBannerConfig{freeInSecond: $freeInSecond, validation: $validation, amazonEnable: $amazonEnable, pubmaticEnable: $pubmaticEnable, autoDisposeIntervalInMinutes: $autoDisposeIntervalInMinutes}';
}
AdBannerConfig(this.freeInSecond, this.validation, this.amazonEnable, this.pubmaticEnable,
this.autoDisposeIntervalInMinutes);
factory AdBannerConfig.fromJson(Map<String, dynamic> json) => _$AdBannerConfigFromJson(json);
Map<String, dynamic> toJson() => _$AdBannerConfigToJson(this);
Future<AdCause> check(String? scene, {AdsValidator? validator}) async {
if (!(await checkFreeTime())) {
return AdCause.invalidRequest;
}
if (await validator?.call() == false) {
return AdCause.invalidRequest;
}
return AdCause.success;
}
Future<bool> checkFreeTime() async {
final firstInstallTime =
await AppProperty.getInstance().getInt(PropertyKeys.firstInstallTime, defValue: 0);
return ((DateTimeUtils.currentTimeInMillis() - firstInstallTime) / 1000) >= freeInSecond;
}
}
@JsonSerializable()
class AdInterstitialConfig {
@JsonKey(name: "free_s", defaultValue: 600)
final int freeInSecond;
@JsonKey(name: "validation", defaultValue: '')
final String validation;
@JsonKey(name: "scene", defaultValue: [])
@joinedStringConvert
final List<String> scenes;
@JsonKey(name: "sp_scene", defaultValue: {})
@configStringIntMapStringConvert
final Map<String, int> specialScenes;
@JsonKey(name: "retry_min_s", defaultValue: 4)
final int retryMinTimeInSecond;
@JsonKey(name: "retry_max_s", defaultValue: 600)
final int retryMaxTimeInSecond;
@JsonKey(name: "amazon_enable", defaultValue: false)
final bool amazonEnable;
@JsonKey(name: "imp_gap_s")
final int? impGapInSeconds;
AdInterstitialConfig(this.freeInSecond, this.validation, this.scenes, this.retryMinTimeInSecond,
this.retryMaxTimeInSecond,
{this.amazonEnable = true, required this.specialScenes, required this.impGapInSeconds});
factory AdInterstitialConfig.fromJson(Map<String, dynamic> json) =>
_$AdInterstitialConfigFromJson(json);
Map<String, dynamic> toJson() => _$AdInterstitialConfigToJson(this);
bool checkSceneEnabled(String scene) {
return scenes.contains(scene);
}
int getSceneImpGapInSeconds(String scene) {
return (specialScenes[scene] ??
impGapInSeconds ??
GuruApp.instance.appSpec.deployment.fullscreenAdsMinInterval)
.clamp(5, 600);
}
Future<bool> checkFreeTime() async {
final firstInstallTime =
await AppProperty.getInstance().getInt(PropertyKeys.firstInstallTime, defValue: 0);
return ((DateTimeUtils.currentTimeInMillis() - firstInstallTime) / 1000) >= freeInSecond;
}
Future<bool> canPreload({AdsValidator? validator}) async {
if (!(await checkFreeTime())) {
return false;
}
if (await validator?.call() == false) {
return false;
}
return true;
}
Future<AdCause> check(String scene, {AdsValidator? validator}) async {
Log.d("check: $this", tag: "Ads");
if (!checkSceneEnabled(scene)) {
return AdCause.disabledScene;
}
if (!(await checkFreeTime())) {
return AdCause.invalidRequest;
}
if (await validator?.call() == false) {
return AdCause.invalidRequest;
}
return AdCause.success;
}
@override
String toString() {
return 'AdInterstitialConfig{freeInSecond: $freeInSecond, validation: $validation, scenes: $scenes, retryMinTimeInSecond: $retryMinTimeInSecond, retryMaxTimeInSecond: $retryMaxTimeInSecond, amazonEnable: $amazonEnable}';
}
RetryConfig get retryConfig => RetryConfig(retryMinTimeInSecond, retryMaxTimeInSecond);
}
@JsonSerializable()
class StrategyAdsConfig {
@JsonKey(name: "iads")
final List<AdId>? interstitialIds;
StrategyAdsConfig({this.interstitialIds});
factory StrategyAdsConfig.fromJson(Map<String, dynamic> json) =>
_$StrategyAdsConfigFromJson(json);
Map<String, dynamic> toJson() => _$StrategyAdsConfigToJson(this);
}
class RetryConfig {
final int minInSecond;
final int maxInSecond;
RetryConfig(this.minInSecond, this.maxInSecond);
}
@JsonSerializable()
class AdRewardedConfig {
@JsonKey(name: "retry_min_s", defaultValue: 4)
final int retryMinTimeInSecond;
@JsonKey(name: "retry_max_s", defaultValue: 600)
final int retryMaxTimeInSecond;
@JsonKey(name: "reset_ads", defaultValue: true)
final bool resetAds;
@JsonKey(name: "validation", defaultValue: '')
final String validation;
AdRewardedConfig(this.retryMinTimeInSecond, this.retryMaxTimeInSecond,
{this.resetAds = true, this.validation = ''});
factory AdRewardedConfig.fromJson(Map<String, dynamic> json) => _$AdRewardedConfigFromJson(json);
Map<String, dynamic> toJson() => _$AdRewardedConfigToJson(this);
RetryConfig get retryConfig => RetryConfig(retryMinTimeInSecond, retryMaxTimeInSecond);
Future<AdCause> check(String scene, {AdsValidator? validator}) async {
if (GuruApp.instance.appSpec.deployment.disableRewardsAds) {
return AdCause.adsDisabled;
}
if (await validator?.call() == false) {
return AdCause.invalidRequest;
}
return AdCause.success;
}
Future<bool> canPreload({AdsValidator? validator}) async {
if (GuruApp.instance.appSpec.deployment.disableRewardsAds) {
return false;
}
if (await validator?.call() == false) {
return false;
}
return true;
}
@override
String toString() {
return 'AdRewardedConfig{retryMinTimeInSecond: $retryMinTimeInSecond, retryMaxTimeInSecond: $retryMaxTimeInSecond, resetAds: $resetAds}';
}
}
@JsonSerializable()
class IOSAttConfig {
@JsonKey(name: "enable", defaultValue: false)
bool enable;
IOSAttConfig({this.enable = false});
factory IOSAttConfig.fromJson(Map<String, dynamic> json) => _$IOSAttConfigFromJson(json);
Map<String, dynamic> toJson() => _$IOSAttConfigToJson(this);
}
@JsonSerializable()
class CommonAdsConfig {
@JsonKey(name: "compliant_init", defaultValue: false)
final bool compliantInitialization;
CommonAdsConfig({this.compliantInitialization = false});
factory CommonAdsConfig.fromJson(Map<String, dynamic> json) => _$CommonAdsConfigFromJson(json);
Map<String, dynamic> toJson() => _$CommonAdsConfigToJson(this);
}

View File

@ -0,0 +1,200 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'ads_config.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
TaichiConfig _$TaichiConfigFromJson(Map<String, dynamic> json) => TaichiConfig(
enable: json['enable'] as bool? ?? false,
threshold: json['threshold'] as String? ?? '',
abnormalThreshold:
(json['abnormal_threshold'] as num?)?.toDouble() ?? 1.0,
);
Map<String, dynamic> _$TaichiConfigToJson(TaichiConfig instance) =>
<String, dynamic>{
'enable': instance.enable,
'threshold': instance.threshold,
'abnormal_threshold': instance.abnormalThreshold,
};
ImpressionData _$ImpressionDataFromJson(Map<String, dynamic> json) =>
ImpressionData(
platform: json['ad_platform'] as String? ?? 'MAX',
id: json['id'] as String? ?? '',
unitId: json['adunit_id'] as String? ?? '',
unitName: json['adunit_name'] as String? ?? '',
unitFormat: json['adunit_format'] as String? ?? '',
groupId: json['adgroup_id'] as String? ?? '',
groupName: json['adgroup_name'] as String? ?? '',
groupType: json['adgroup_type'] as String? ?? '',
currency: json['currency'] as String? ?? '',
country: json['country'] as String? ?? '',
appVersion: json['app_version'] as String? ?? '',
groupPriority: json['adgroup_priority'] as int? ?? 0,
publisherRevenue: (json['publisher_revenue'] as num?)?.toDouble() ?? -1,
networkName: json['network_name'] as String? ?? '',
networkPlacementId: json['network_placement_id'] as String? ?? '',
precision: json['precision'] as String? ?? '',
);
Map<String, dynamic> _$ImpressionDataToJson(ImpressionData instance) =>
<String, dynamic>{
'ad_platform': instance.platform,
'id': instance.id,
'adunit_id': instance.unitId,
'adunit_name': instance.unitName,
'adunit_format': instance.unitFormat,
'adgroup_id': instance.groupId,
'adgroup_name': instance.groupName,
'adgroup_type': instance.groupType,
'currency': instance.currency,
'country': instance.country,
'app_version': instance.appVersion,
'adgroup_priority': instance.groupPriority,
'publisher_revenue': instance.publisherRevenue,
'network_name': instance.networkName,
'network_placement_id': instance.networkPlacementId,
'precision': instance.precision,
};
CpmCalibrationData _$CpmCalibrationDataFromJson(Map<String, dynamic> json) =>
CpmCalibrationData(
(json['list'] as List<dynamic>?)
?.map(
(e) => CpmCalibrationItem.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
Map<String, dynamic> _$CpmCalibrationDataToJson(CpmCalibrationData instance) =>
<String, dynamic>{
'list': instance.items,
};
CpmCalibrationItem _$CpmCalibrationItemFromJson(Map<String, dynamic> json) =>
CpmCalibrationItem(
json['format'] as String,
(json['cpm'] as num).toDouble(),
json['country'] as String,
);
Map<String, dynamic> _$CpmCalibrationItemToJson(CpmCalibrationItem instance) =>
<String, dynamic>{
'format': instance.format,
'cpm': instance.cpm,
'country': instance.country,
};
IRLDConfig _$IRLDConfigFromJson(Map<String, dynamic> json) => IRLDConfig(
fbCpmCacheInHour: json['fb_ecpm_cache_h'] as int? ?? 12,
fbIrldReport: json['fb_irld_report'] as bool? ?? false,
abnormalThreshold:
(json['abnormal_threshold'] as num?)?.toDouble() ?? 0.1,
);
Map<String, dynamic> _$IRLDConfigToJson(IRLDConfig instance) =>
<String, dynamic>{
'fb_ecpm_cache_h': instance.fbCpmCacheInHour,
'fb_irld_report': instance.fbIrldReport,
'abnormal_threshold': instance.abnormalThreshold,
};
AdBannerConfig _$AdBannerConfigFromJson(Map<String, dynamic> json) =>
AdBannerConfig(
json['free_s'] as int? ?? 600,
json['validation'] as String? ?? '',
json['amazon_enable'] as bool? ?? false,
json['pubmatic_enable'] as bool? ?? false,
json['auto_dispose_interval_m'] as int? ?? 5,
);
Map<String, dynamic> _$AdBannerConfigToJson(AdBannerConfig instance) =>
<String, dynamic>{
'free_s': instance.freeInSecond,
'validation': instance.validation,
'amazon_enable': instance.amazonEnable,
'pubmatic_enable': instance.pubmaticEnable,
'auto_dispose_interval_m': instance.autoDisposeIntervalInMinutes,
};
AdInterstitialConfig _$AdInterstitialConfigFromJson(
Map<String, dynamic> json) =>
AdInterstitialConfig(
json['free_s'] as int? ?? 600,
json['validation'] as String? ?? '',
json['scene'] == null
? []
: joinedStringConvert.fromJson(json['scene'] as String),
json['retry_min_s'] as int? ?? 4,
json['retry_max_s'] as int? ?? 600,
amazonEnable: json['amazon_enable'] as bool? ?? false,
specialScenes: json['sp_scene'] == null
? {}
: configStringIntMapStringConvert
.fromJson(json['sp_scene'] as String),
impGapInSeconds: json['imp_gap_s'] as int?,
);
Map<String, dynamic> _$AdInterstitialConfigToJson(
AdInterstitialConfig instance) =>
<String, dynamic>{
'free_s': instance.freeInSecond,
'validation': instance.validation,
'scene': joinedStringConvert.toJson(instance.scenes),
'sp_scene':
configStringIntMapStringConvert.toJson(instance.specialScenes),
'retry_min_s': instance.retryMinTimeInSecond,
'retry_max_s': instance.retryMaxTimeInSecond,
'amazon_enable': instance.amazonEnable,
'imp_gap_s': instance.impGapInSeconds,
};
StrategyAdsConfig _$StrategyAdsConfigFromJson(Map<String, dynamic> json) =>
StrategyAdsConfig(
interstitialIds: (json['iads'] as List<dynamic>?)
?.map((e) => AdId.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$StrategyAdsConfigToJson(StrategyAdsConfig instance) =>
<String, dynamic>{
'iads': instance.interstitialIds,
};
AdRewardedConfig _$AdRewardedConfigFromJson(Map<String, dynamic> json) =>
AdRewardedConfig(
json['retry_min_s'] as int? ?? 4,
json['retry_max_s'] as int? ?? 600,
resetAds: json['reset_ads'] as bool? ?? true,
validation: json['validation'] as String? ?? '',
);
Map<String, dynamic> _$AdRewardedConfigToJson(AdRewardedConfig instance) =>
<String, dynamic>{
'retry_min_s': instance.retryMinTimeInSecond,
'retry_max_s': instance.retryMaxTimeInSecond,
'reset_ads': instance.resetAds,
'validation': instance.validation,
};
IOSAttConfig _$IOSAttConfigFromJson(Map<String, dynamic> json) => IOSAttConfig(
enable: json['enable'] as bool? ?? false,
);
Map<String, dynamic> _$IOSAttConfigToJson(IOSAttConfig instance) =>
<String, dynamic>{
'enable': instance.enable,
};
CommonAdsConfig _$CommonAdsConfigFromJson(Map<String, dynamic> json) =>
CommonAdsConfig(
compliantInitialization: json['compliant_init'] as bool? ?? false,
);
Map<String, dynamic> _$CommonAdsConfigToJson(CommonAdsConfig instance) =>
<String, dynamic>{
'compliant_init': instance.compliantInitialization,
};

View File

@ -0,0 +1,187 @@
import 'dart:convert';
import 'dart:io';
import 'package:guru_app/analytics/guru_analytics.dart';
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_app/property/property_keys.dart';
import 'package:guru_utils/collection/collectionutils.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_applovin_flutter/ad_impression.dart';
import 'ads_config.dart';
import 'exceptions/ads_exceptions.dart';
const ltvPhaseNames = [
"tch_ad_rev_top40",
"tch_ad_rev_top30",
"tch_ad_rev_top20",
"tch_ad_rev_top10",
"tch_ad_rev_top5"
];
class AdImpressionController {
final List<double> ltvThresholds = <double>[];
final Map<String, String> adsParams = <String, String>{};
static String latestImpressionPayload = "";
TaichiConfig? taichiConfig;
AdImpressionController() {}
Future _init() async {
try {
taichiConfig = RemoteConfigManager.instance.getTaichiConfig();
final config = taichiConfig;
if (config != null && config.enable && config.threshold.isNotEmpty) {
Log.d("set thresholds prepare! ${config.threshold}");
final thresholdStrings = config.threshold.split(",");
if (thresholdStrings.isNotEmpty) {
try {
final thresholds = thresholdStrings.map((e) => double.parse(e)).toList();
ltvThresholds.clear();
ltvThresholds.addAll(thresholds);
Log.d("set thresholds success! $thresholds");
} catch (error, stacktrace) {
Log.d("set thresholds error!", error: error, stackTrace: stacktrace);
}
}
}
} catch (error, stacktrace) {
Log.d('AdImpressionController params TaichiConfig err', error: error, stackTrace: stacktrace);
}
}
void init() async {
await _init();
AdImpressionListener.addCallback((event, Map<dynamic, dynamic> arguments) async {
Log.d("------------addListener arguments$arguments");
switch (event) {
case AdImpressionEvent.onAdImpression:
if (arguments != null) {
final payload = arguments["payload"];
if (payload == null || payload.isEmpty) {
break;
}
final payloadMap = json.decode(payload);
ImpressionData impressionData = ImpressionData.fromJson(payloadMap);
await refreshLtv(impressionData);
final jsonPayload = jsonEncode(impressionData.payload);
latestImpressionPayload = jsonPayload;
if (impressionData.publisherRevenue > 0) {
// AdjustAdRevenue adRevenue = AdjustAdRevenue(AdjustConfig.AdRevenueSourceAppLovinMAX);
// adRevenue.setRevenue(impressionData.publisherRevenue, "USD");
// adRevenue.adRevenueNetwork = impressionData.networkName;
// adRevenue.adRevenueUnit = impressionData.unitId;
// adRevenue.adRevenuePlacement = impressionData.networkPlacementId;
// Adjust.trackAdRevenueNew(adRevenue);
GuruAnalytics.instance.loadAdjustAdRevenue(impressionData);
}
}
break;
}
});
}
_reportAdImpression(Map<dynamic, dynamic> arguments) {
// adFormat :BANNER,REWARDED,INTER
final adFormat = arguments["ad_format"] ?? "";
GuruAnalytics.instance.logAdImpression("max_imp", adFormat,
adName: Platform.isIOS ? "isd_$adFormat" : "sd_$adFormat");
}
void onImpression(Map<dynamic, dynamic> arguments) {}
Future refreshLtv(ImpressionData impressionData) async {
final revenue = impressionData.publisherRevenue;
final adPlatform = impressionData.platform;
final currency = impressionData.currency;
if (revenue != -1) {
_logAdRevenue(impressionData);
// if ()
// _logAdLtv(revenue: revenue, adPlatform: adPlatform, currency: currency);
}
Log.d("refreshLtv payload:${impressionData.payload}");
}
// Future _logAdLtv({double revenue = 0.0, String adPlatform = "MAX", String currency = ""}) async {
// final nowDate = DateTimeUtils.yyyyMMddUtcNum;
// final appProperty = AppProperty.getInstance();
// final totalLtv = await appProperty.getDouble(PropertyKeys.totalLtv, defValue: 0.0);
// final previousDate = await appProperty.getInt(PropertyKeys.previousLtvDate, defValue: 0);
// double previousLtv = await appProperty.getDouble(PropertyKeys.previousLtv, defValue: 0.0);
// if (previousDate != nowDate) {
// previousLtv = 0.0;
// appProperty.setInt(PropertyKeys.previousLtvDate, nowDate);
// }
// final currentLtv = previousLtv + revenue;
// for (int i = 0; i < ltvThresholds.length; ++i) {
// if (previousLtv < ltvThresholds[i] && currentLtv >= ltvThresholds[i]) {
// if (i < ltvPhaseNames.length) {
// Analytics.instance.logAdLtv(ltvPhaseNames[i], currentLtv);
// }
// }
// }
// AppProperty.getInstance().setDouble(PropertyKeys.previousLtv, currentLtv);
// AppProperty.getInstance().setDouble(PropertyKeys.totalLtv, totalLtv + revenue);
// }
_logAdRevenue(ImpressionData data) async {
final appProperty = AppProperty.getInstance();
GuruAnalytics.instance.logAdImp(data);
final abnormalThreshold = taichiConfig?.abnormalThreshold ?? 1;
if (data.publisherRevenue >= abnormalThreshold) {
try {
final parameters = CollectionUtils.filterOutNulls(<String, dynamic>{
"ad_platform": data.platform,
"value": data.publisherRevenue,
"currency": data.currency,
"ad_format": data.unitFormat,
"ad_source": data.networkName,
"ad_unit_name": data.unitName,
"country": data.country,
"precision": data.precision
});
GuruAnalytics.instance.logEvent("tch_ad_rev_value_abnormal", parameters);
final payload = data.payload;
GuruAnalytics.instance.logException(AbnormalRevenueException(payload.toString()),
stacktrace: StackTrace.current);
Log.d(payload.toString());
return;
} catch (error, stacktrace) {
Log.d("convert impression data error! $error", stackTrace: stacktrace);
}
}
double totalRevenue = await appProperty.getDouble(PropertyKeys.totalRevenue, defValue: 0.0);
totalRevenue += data.publisherRevenue;
if (totalRevenue >= 0.01) {
GuruAnalytics.instance.logAdRevenue(totalRevenue, data.platform, data.currency);
if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 1) {
GuruAnalytics.instance.logPurchase(totalRevenue,
currency: data.currency, contentId: "MAX", adPlatform: "MAX");
}
totalRevenue = .0;
}
appProperty.setDouble(PropertyKeys.totalRevenue, totalRevenue);
double totalRevenue020 =
await appProperty.getDouble(PropertyKeys.totalRevenue020, defValue: 0.0);
totalRevenue020 += data.publisherRevenue;
if (totalRevenue020 >= 0.2) {
GuruAnalytics.instance.logAdRevenue020(totalRevenue020, data.platform, data.currency);
if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 2) {
GuruAnalytics.instance.logPurchase(totalRevenue020,
currency: data.currency, contentId: "MAX", adPlatform: "MAX");
}
totalRevenue020 = .0;
}
appProperty.setDouble(PropertyKeys.totalRevenue020, totalRevenue020);
}
}

View File

@ -0,0 +1,11 @@
/// Created by Haoyi on 5/10/21
part of '../ads.dart';
abstract class BannerAds<T> extends SingleAds<T> with AdsAudit {
@override
void init() {
super.init();
addObserver(BannerAdsReportEventsObserver());
}
}

View File

@ -0,0 +1,79 @@
/// Created by Haoyi on 5/11/21
part of '../ads.dart';
class BannerAdsReportEventsObserver extends AdsLifecycleObserver {
void _apply(AdsBundle adsBundle, void Function(BannerAds ads) callback) {
if (adsBundle.ads is BannerAds) {
try {
callback(adsBundle.ads as BannerAds);
} catch (error, stacktrace) {
Log.i('BannerAdsReportEventsHandler apply error:$error, $stacktrace');
}
}
}
@override
void onRequestLoad(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("bads_load");
});
}
@override
void onAdLoaded(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
// AnalyticsUtils.logEventEx("bads_loaded",
// parameters: {"duration": ads.elapsedTimeInMillisSinceStartLoadAds});
});
}
@override
void onAdLoadFailed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
String errorCode = adsBundle.arguments["errorName"] ?? "Unknown";
// TODO
// AnalyticsUtils.logEventEx("bads_failed", itemCategory: "load", parameters: {
// "duration": ads.elapsedTimeInMillisSinceStartLoadAds,
// "error_code": errorCode
// });
});
}
@override
void onAdDisplayFailed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
String errorCode = adsBundle.arguments["errorName"] ?? "Unknown";
// TODO
// AnalyticsUtils.logEventEx("bads_failed",
// itemCategory: "imp",
// parameters: {"duration": ads.elapsedTimeInMillisSinceLoadedAds, "error_code": errorCode});
});
}
@override
void onAdDisplayed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
// AnalyticsUtils.logAdImpression("bads_imp", AdTypeName.AD_TYPE_BANNER,
// scene: ads.scene,
// adName: ads.scene,
// parameters: {"duration": "${ads.elapsedTimeInMillisSinceLoadedAds}"});
});
}
@override
void onAdClicked(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logAdClick("bads_clk", AdTypeName.AD_TYPE_BANNER,
scene: ads.scene, adName: ads.scene);
AiBi.instance.adsClk(AdsType.banner, adScene: ads.scene);
});
}
@override
void onAdHidden(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("bads_close", itemCategory: ads.scene);
AiBi.instance.adsHide(AdsType.banner, adScene: ads.scene);
});
}
}

View File

@ -0,0 +1,23 @@
/// Created by Haoyi on 2021/9/3
class AbnormalRevenueException implements Exception {
final String payload;
AbnormalRevenueException(this.payload);
@override
String toString() {
return 'AbnormalRevenueException{payload: $payload}';
}
}
class ShowRewardedVideoAdsException implements Exception {
final String message;
ShowRewardedVideoAdsException(this.message);
@override
String toString() {
return 'ShowRewardedVideoAdsException{payload: $message}';
}
}

View File

@ -0,0 +1,142 @@
/// Created by Haoyi on 5/7/21
part of '../ads.dart';
mixin AdsAudit<T> on Ads {
int latestShownAt = 0;
int latestStartLoadAt = 0;
int latestLoadedAt = 0;
int latestHiddenAt = 0;
String scene = "";
int loadCount = 0;
int get elapsedTimeInMillisSinceLoadedAds => DateTimeUtils.currentTimeInMillis() - latestLoadedAt;
int get elapsedTimeInMillisSinceStartLoadAds =>
DateTimeUtils.currentTimeInMillis() - latestStartLoadAt;
void resetLatestLoadedAt() {
latestLoadedAt = 0;
}
void resetAudit() {
latestShownAt = 0;
latestHiddenAt = 0;
latestLoadedAt = 0;
latestStartLoadAt = 0;
scene = "";
}
}
class AdsAuditObserver extends AdsLifecycleObserver {
String adsName;
final String tag;
AdsAuditObserver(this.adsName, {this.tag = PropertyTags.ads});
@override
String get name => "$adsName-AuditObs";
void _apply(AdsBundle adsBundle, void Function(AdsAudit) callback) {
if (adsBundle.ads is AdsAudit) {
callback(adsBundle.ads as AdsAudit);
}
}
@override
void onRequestShow(AdsBundle adsBundle) {
Log.i("[$name] onRequestShow! ${adsBundle.arguments}", tag: tag);
_apply(adsBundle, (adsAudit) {
final adCause = adsBundle.getValue<AdCause>("cause", defValue: AdCause.internalError);
if (adCause == AdCause.success) {
adsAudit.scene = adsBundle.getString("scene");
adsAudit.setProperty("latestScene", adsAudit.scene);
}
});
}
@override
void onRequestLoad(AdsBundle adsBundle) {
Log.i("[$name] onRequestLoad! ", tag: tag);
_apply(adsBundle, (adsAudit) {
final adCause = adsBundle.getValue<AdCause>("cause", defValue: AdCause.internalError);
final now = DateTimeUtils.currentTimeInMillis();
final humanDate = DateTime.fromMillisecondsSinceEpoch(now).toString();
if (adCause == AdCause.success) {
adsAudit.latestStartLoadAt = now;
adsAudit.setProperty("latestStartLoadTime", humanDate);
adsAudit.loadCount++;
adsAudit.setProperty("winCount", adsAudit.loadCount.toString());
} else {
adsAudit.setProperty("latestLoadCause", "[$humanDate]:${adCause.toString()}");
}
});
}
@override
void onRequestReset(AdsBundle adsBundle) {
Log.i("[$name] onRequestReset! ", tag: tag);
_apply(adsBundle, (adsAudit) async {
adsAudit.resetAudit();
});
}
@override
void onAdLoaded(AdsBundle adsBundle) {
Log.i("[$name] onAdLoaded! ", tag: tag);
_apply(adsBundle, (adsAudit) {
adsAudit.latestLoadedAt = DateTimeUtils.currentTimeInMillis();
adsAudit.setProperty("latestLoadedTime",
DateTime.fromMillisecondsSinceEpoch(adsAudit.latestLoadedAt).toString());
});
}
@override
void onAdLoadFailed(AdsBundle adsBundle) {
Log.i("[$name] onAdLoadFailed! ", tag: tag);
_apply(adsBundle, (adsAudit) {
adsAudit.setProperty("latestLoadFailedTime", DateTime.now().toString());
});
}
@override
Future<void> onAdDisplayFailed(AdsBundle adsBundle) async {
Log.i("[$name] onHidden! ", tag: tag);
_apply(adsBundle, (adsAudit) {
adsAudit.setProperty("latestDisplayFailedTime", DateTime.now().toString());
adsAudit.setProperty("latestCreativeId", adsBundle.getString("ad_creative_id"));
adsAudit.setProperty("latestNetworkName", adsBundle.getString("ad_network_name"));
});
}
@override
void onAdDisplayed(AdsBundle adsBundle) {
Log.i("[$name] onAdDisplayed! ", tag: tag);
_apply(adsBundle, (adsAudit) {
adsAudit.setProperty("latestDisplayTime", DateTime.now().toString());
adsAudit.setProperty("latestCreativeId", adsBundle.getString("ad_creative_id"));
adsAudit.setProperty("latestNetworkName", adsBundle.getString("ad_network_name"));
});
}
@override
void onAdClicked(AdsBundle adsBundle) {}
@override
void onAdHidden(AdsBundle adsBundle) {
Log.i("[$name] onHidden! ", tag: tag);
_apply(adsBundle, (adsAudit) {
final now = DateTime.now();
adsAudit.latestHiddenAt = now.millisecondsSinceEpoch;
adsAudit.setProperty("latestHiddenTime", now.toString());
AdsManager.instance.latestFullscreenAdsHiddenTimestamps = now.millisecondsSinceEpoch;
});
}
@override
void onAdRewarded(AdsBundle adsBundle) {
HookManager.instance.watchRewardAds();
}
}

View File

@ -0,0 +1,170 @@
/// Created by Haoyi on 5/7/21
part of '../ads.dart';
class RetryConfig {
final int minInSecond;
final int maxInSecond;
RetryConfig(this.minInSecond, this.maxInSecond);
}
mixin AdsCache<T> on Ads {
int retryAttempt = 0;
DateTime latestPreloadAt = DateTime.now();
Timer? _retryTimer;
RetryConfig get retryConfig => RetryConfig(4, 30);
bool get isLoadingRewardAdsDelayed => _retryTimer != null;
void _resetRetryTimer() {
try {
_retryTimer?.cancel();
_retryTimer = null;
} catch (error, stacktrace) {
_retryTimer = null;
}
}
void _resetPreload() {
retryAttempt = 0;
_resetRetryTimer();
setProperty("Retry Attempt", "0");
}
@override
void preload() {
latestPreloadAt = DateTime.now();
setProperty("Latest Preload Time", latestPreloadAt.toString());
_resetRetryTimer();
if (AdsManager.instance.connectivityStatus != ConnectivityResult.none) {
load();
}
}
@override
void retry() {
if (_retryTimer?.isActive == true) {
return;
}
retryAttempt++;
final delaySecond = pow(2, retryAttempt).toInt();
final config = retryConfig;
final duration = Duration(seconds: delaySecond.clamp(config.minInSecond, config.maxInSecond));
setProperty("Retry Attempt", retryAttempt.toString());
setProperty("Retry Interval", duration.toString());
_retryTimer = Timer(duration, () {
preload();
});
}
}
class AdsCacheObserver extends AdsLifecycleObserver {
final String adsName;
AdsCacheObserver(this.adsName);
@override
String get name => "$adsName-AdsCache";
void _apply(AdsBundle adsBundle, void Function(AdsCache) callback) {
if (adsBundle.ads is AdsCache) {
callback(adsBundle.ads as AdsCache);
}
}
@override
void onRequestShow(AdsBundle adsBundle) {
Log.i("[$name] onRequestShow! ${adsBundle.arguments}", tag: "Ads");
_apply(adsBundle, (adsCache) async {
final adCause = adsBundle.getValue<AdCause>("cause", defValue: AdCause.internalError);
if (adCause != AdCause.success && adCause != AdCause.tooFrequent) {
final state = await adsCache.getState();
Log.d("[$name] onRequestShow state:$state", tag: "Ads");
if (state != AdState.loaded) {
adsCache.preload();
} else {
Log.d("[$name] onRequestShow ignore preload! AdsState: $state", tag: "Ads");
}
}
});
}
@override
void onRequestLoad(AdsBundle adsBundle) {
// Log.i("[$name] onRequestLoad! ${adsBundle.arguments}", tag: "Ads");
_apply(adsBundle, (adsCache) async {
final state = await adsCache.getState();
final adCause = adsBundle.getValue<AdCause>("cause", defValue: AdCause.internalError);
if (adCause != AdCause.success && state != AdState.loaded && state != AdState.loading) {
Log.d("[$name] onRequestLoad adCause:$adCause state:$state", tag: "Ads");
adsCache.retry();
} else {
if (state == AdState.loaded) {
adsCache._resetPreload();
adsCache.loaded = true;
}
Log.d("[$name] onRequestLoad ignore preload! AdsState: $state", tag: "Ads");
}
});
}
@override
void onRequestReset(AdsBundle adsBundle) {
Log.i("[$name] onRequestReset! ", tag: "Ads");
_apply(adsBundle, (adsCache) async {
adsCache._resetPreload();
});
}
@override
void onAdDisplayed(AdsBundle adsBundle) {
Log.i("[$name] onAdDisplayed! ", tag: "Ads");
_apply(adsBundle, (adsCache) {
adsCache.loaded = false;
});
}
@override
void onAdLoaded(AdsBundle adsBundle) {
Log.i("[$name] onAdLoaded! ", tag: "Ads");
_apply(adsBundle, (adsCache) {
adsCache._resetPreload();
adsCache.loaded = true;
});
}
@override
void onAdLoadFailed(AdsBundle adsBundle) {
Log.i("[$name] onAdLoadFailed! ", tag: "Ads");
_apply(adsBundle, (adsCache) async {
adsCache.retry();
adsCache.loaded = false;
});
}
@override
void onAdDisplayFailed(AdsBundle adsBundle) {
Log.i("[$name] onAdDisplayFailed! ", tag: "Ads");
_apply(adsBundle, (adsCache) {
adsCache.preload();
adsCache.loaded = false;
});
}
@override
void onAdHidden(AdsBundle adsBundle) {
Log.i("[$name] onHidden! ", tag: "Ads");
_apply(adsBundle, (adsCache) async {
final state = await adsCache.getState();
if (state != AdState.loaded && state != AdState.loading) {
adsCache.loaded = false;
adsCache.preload();
} else {
Log.d("[$name] onHidden ignore preload! AdsState: $state", tag: "Ads");
}
});
}
}

View File

@ -0,0 +1,11 @@
/// Created by Haoyi on 5/6/21
part of '../ads.dart';
abstract class InterstitialAds<T> extends SingleAds<T> with AdsCache, AdsAudit {
@override
void init() {
super.init();
addObserver(InterstitialAdsReportEventsObserver());
}
}

View File

@ -0,0 +1,102 @@
/// Created by Haoyi on 5/10/21
part of '../ads.dart';
class InterstitialAdsReportEventsObserver extends AdsLifecycleObserver {
@override
String get name => "InterstitialAdsReportEventsHandler";
void _apply(AdsBundle adsBundle, void Function(AdsAudit ads) callback) {
if (adsBundle.ads is AdsAudit) {
try {
callback(adsBundle.ads as AdsAudit);
} catch (error, stacktrace) {
Log.i('InterstitialAdsHandler apply error:$error, $stacktrace');
}
}
}
@override
void onRequestLoad(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("iads_load");
});
}
@override
void onRequestReset(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("iads_rebuild");
});
}
@override
void onAdLoaded(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("iads_loaded",
parameters: {"duration": ads.elapsedTimeInMillisSinceStartLoadAds});
AiBi.instance.adsLoaded(AdsType.interstitial,
adScene: ads.scene, duration: ads.elapsedTimeInMillisSinceStartLoadAds);
});
}
@override
void onAdLoadFailed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
String errorCode = adsBundle.arguments["errorName"] ?? "Unknown";
GuruAnalytics.instance.logEventEx("iads_failed", itemCategory: "load", parameters: {
"duration": ads.elapsedTimeInMillisSinceStartLoadAds,
"error_code": errorCode
});
AiBi.instance.adsFailed(AdsType.interstitial, adScene: ads.scene);
});
}
@override
void onAdDisplayFailed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
String errorCode = adsBundle.arguments["errorName"] ?? "Unknown";
GuruAnalytics.instance.logEventEx("iads_failed",
itemCategory: "imp",
parameters: {"duration": ads.elapsedTimeInMillisSinceLoadedAds, "error_code": errorCode});
Log.d(
"iads_display_failed creativeId:${adsBundle.getString("ad_creative_id")} networkName:${adsBundle.getString("ad_network_name")} errorCode:$errorCode duration:${ads.elapsedTimeInMillisSinceLoadedAds}",
syncFirebase: true);
AiBi.instance.adsFailed(AdsType.interstitial, adScene: ads.scene);
});
}
@override
void onAdDisplayed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logAdImpression("iads_imp", AdTypeName.AD_TYPE_INTERSTITIAL,
scene: ads.scene,
adName: ads.scene,
parameters: {"duration": "${ads.elapsedTimeInMillisSinceLoadedAds}"});
Log.d(
"iads_imp creativeId:${adsBundle.getString("ad_creative_id")} networkName:${adsBundle.getString("ad_network_name")} duration:${ads.elapsedTimeInMillisSinceLoadedAds}",
syncFirebase: true);
AiBi.instance.adsImp(AdsType.interstitial,
adScene: ads.scene,
adRevenue: adsBundle.getDouble("ad_revenue", defValue: 0.0),
network: adsBundle.getString("ad_network_name", defValue: "unknown"));
});
}
@override
void onAdClicked(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logAdClick("iads_clk", AdTypeName.AD_TYPE_INTERSTITIAL,
scene: ads.scene, adName: ads.scene);
AiBi.instance.adsClk(AdsType.interstitial, adScene: ads.scene);
});
}
@override
void onAdHidden(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("iads_close", itemCategory: ads.scene);
AiBi.instance.adsHide(AdsType.interstitial, adScene: ads.scene);
});
}
}

View File

@ -0,0 +1,11 @@
/// Created by Haoyi on 5/26/21
part of '../ads.dart';
abstract class RewardedAds<T> extends SingleAds<T> with AdsCache, AdsAudit {
@override
void init() {
super.init();
addObserver(RewardedAdsReportEventsObserver());
}
}

View File

@ -0,0 +1,116 @@
/// Created by Haoyi on 5/26/21
part of '../ads.dart';
class RewardedAdsReportEventsObserver extends AdsLifecycleObserver {
String get name => "RewardedAdsReportEventsHandler";
void _apply(AdsBundle adsBundle, void Function(RewardedAds ads) callback) {
if (adsBundle.ads is RewardedAds) {
try {
callback(adsBundle.ads as RewardedAds);
} catch (error, stacktrace) {
Log.w('RewardedAdsHandler apply error', error: error, stackTrace: stacktrace);
}
}
}
@override
void onRequestLoad(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("rads_load");
AiBi.instance.adsLoad(AdsType.rewarded, adScene: ads.scene);
});
}
@override
void onAdLoaded(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("rads_loaded",
parameters: {"duration": ads.elapsedTimeInMillisSinceStartLoadAds});
AiBi.instance.adsLoaded(AdsType.rewarded,
adScene: ads.scene, duration: ads.elapsedTimeInMillisSinceStartLoadAds);
});
}
@override
void onRequestReset(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("rads_rebuild");
});
}
@override
void onAdLoadFailed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
String errorCode = adsBundle.arguments["errorName"] ?? "Unknown";
GuruAnalytics.instance.logEventEx("rads_failed", itemCategory: "load", parameters: {
"duration": ads.elapsedTimeInMillisSinceStartLoadAds,
"error_code": errorCode
});
AiBi.instance.adsFailed(AdsType.rewarded, adScene: ads.scene);
});
}
@override
void onAdDisplayFailed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
String errorCode = adsBundle.arguments["errorName"] ?? "Unknown";
GuruAnalytics.instance.logEventEx("rads_failed",
itemCategory: "imp",
parameters: {"duration": ads.elapsedTimeInMillisSinceLoadedAds, "error_code": errorCode});
Log.d(
"rads_display_failed creativeId:${adsBundle.getString("ad_creative_id")} networkName:${adsBundle.getString("ad_network_name")} errorCode:$errorCode duration:${ads.elapsedTimeInMillisSinceLoadedAds}",
syncFirebase: true);
AiBi.instance.adsFailed(AdsType.rewarded, adScene: ads.scene);
});
}
@override
void onAdDisplayed(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logAdImpression("rads_imp", AdTypeName.AD_TYPE_REWARDED_VIDEO,
scene: ads.scene,
adName: ads.scene,
parameters: {"duration": "${ads.elapsedTimeInMillisSinceLoadedAds}"});
Log.d(
"rads_imp creativeId:${adsBundle.getString("ad_creative_id")} networkName:${adsBundle.getString("ad_network_name")} duration:${ads.elapsedTimeInMillisSinceLoadedAds}",
syncFirebase: true);
AiBi.instance.adsImp(AdsType.rewarded,
adScene: ads.scene,
adRevenue: adsBundle.getDouble("ad_revenue", defValue: 0),
network: adsBundle.getString("ad_network_name", defValue: "unknown"));
});
}
@override
void onAdClicked(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logAdClick("rads_clk", AdTypeName.AD_TYPE_REWARDED_VIDEO,
scene: ads.scene, adName: ads.scene);
AiBi.instance.adsClk(AdsType.rewarded, adScene: ads.scene);
});
}
@override
void onAdHidden(AdsBundle adsBundle) {
_apply(adsBundle, (ads) {
GuruAnalytics.instance.logEventEx("rads_close", itemCategory: ads.scene);
AiBi.instance.adsHide(AdsType.rewarded, adScene: ads.scene);
});
}
@override
void onAdRewarded(AdsBundle adsBundle) {
_apply(adsBundle, (ads) async {
GuruAnalytics.instance.logEventEx("rads_rewarded", itemCategory: ads.scene);
final userRewardedCount =
await AppProperty.getInstance().getInt(PropertyKeys.userRewardedCount, defValue: 0);
if (userRewardedCount == 0) {
GuruAnalytics.instance.logEventEx("first_rads_rewarded", itemCategory: ads.scene);
}
await AppProperty.getInstance().setInt(PropertyKeys.userRewardedCount, userRewardedCount + 1);
AiBi.instance.adsRewarded(ads.scene);
});
}
}

View File

@ -0,0 +1,17 @@
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/strategy/handler/ad_unit_cache.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
/// Created by Haoyi on 2023/6/28
abstract class AdUnit<T> extends SingleAds<T> with AdsAudit<T> {
@override
final AdUnitId adUnitId;
final AdSlotId? amazonAdSlotId;
AdUnit(this.adUnitId, {this.amazonAdSlotId}) {
// addObserver(AdUnitCacheObserver(adUnitId.id));
addObserver(AdsAuditObserver("AdUnit[${adUnitId.id}]", tag: PropertyTags.strategyAds));
}
}

View File

@ -0,0 +1,70 @@
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/strategy/ad_unit.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
import '../../../../guru_app.dart';
/// Created by Haoyi on 2023/6/28
mixin AdUnitCache<T> on SingleAds<T> {
}
class AdUnitCacheObserver extends AdsLifecycleObserver {
final String adsName;
AdUnitCacheObserver(this.adsName);
@override
String get name => "$adsName-AdUnitCache";
void _apply(AdsBundle adsBundle, void Function(AdUnitCache) callback) {
if (adsBundle.ads is AdUnitCache) {
callback(adsBundle.ads as AdUnitCache);
}
}
@override
void onAdDisplayed(AdsBundle adsBundle) {
_apply(adsBundle, (adsCache) {
adsCache.loaded = false;
});
}
@override
void onAdLoaded(AdsBundle adsBundle) {
Log.i("[$name] onAdLoaded! ", tag: PropertyTags.strategyAds);
_apply(adsBundle, (adsCache) {
adsCache.loaded = true;
});
}
@override
void onAdLoadFailed(AdsBundle adsBundle) {
Log.i("[$name] onAdLoadFailed! ", tag: PropertyTags.strategyAds);
_apply(adsBundle, (adsCache) async {
adsCache.loaded = false;
});
}
@override
void onAdDisplayFailed(AdsBundle adsBundle) {
Log.i("[$name] onAdDisplayFailed! ", tag: PropertyTags.strategyAds);
_apply(adsBundle, (adsCache) {
adsCache.loaded = false;
});
}
@override
void onAdHidden(AdsBundle adsBundle) {
Log.i("[$name] onHidden! ", tag: PropertyTags.strategyAds);
_apply(adsBundle, (adsCache) async {
final state = await adsCache.getStatus();
if (state != AdStatus.LOADED && state != AdStatus.LOADING) {
adsCache.loaded = false;
} else {
Log.d("[$name] onHidden ignore preload! AdsState: $state", tag: PropertyTags.strategyAds);
}
});
}
}

View File

@ -0,0 +1,107 @@
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/strategy/ad_unit.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_applovin_flutter/guru_applovin_flutter.dart';
import 'package:guru_applovin_flutter/interstitial_ad.dart';
import 'package:guru_utils/ads/data/ads_model.dart';
/// Created by Haoyi on 2023/6/28
class MaxInterstitialAdUnit extends AdUnit<InterstitialAdEvent> {
late InterstitialAd interstitialAd;
@override
final String name;
MaxInterstitialAdUnit.create(AdUnitId adUnitId, AdSlotId? amazonAdSlotId)
: name = "MaxInterAdUnit-${adUnitId.id}",
super(adUnitId, amazonAdSlotId: amazonAdSlotId) {
addObserver(InterstitialAdsReportEventsObserver());
}
@override
void init() {
super.init();
interstitialAd = InterstitialAd(
adUnitId: adUnitId.id, adAmazonSlotId: amazonAdSlotId?.id, listener: dispatchEvent);
}
@override
Map<InterstitialAdEvent, AdsEvent> get eventsMapping => {
InterstitialAdEvent.onAdLoaded: AdsEvent.adLoaded,
InterstitialAdEvent.onAdLoadFailed: AdsEvent.adLoadFailed,
InterstitialAdEvent.onAdDisplayFailed: AdsEvent.adDisplayFailed,
InterstitialAdEvent.onAdDisplayed: AdsEvent.adDisplayed,
InterstitialAdEvent.onAdClicked: AdsEvent.adClick,
InterstitialAdEvent.onAdHidden: AdsEvent.adHidden,
};
@override
Future<bool> requestDispose() async {
try {
return await interstitialAd.dispose() ?? false;
} catch (error, stacktrace) {
Log.w("[$name] requestDispose error", error: error, stackTrace: stacktrace);
return false;
}
}
@override
Future<bool> requestHide() async {
return false;
}
@override
Future<AdCause> requestLoad() async {
try {
Log.w("[$name] requestLoad", tag: PropertyTags.strategyAds);
final result = await interstitialAd.load() ?? false;
if (result) {
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.w("requestLoad error! $error $stacktrace", tag: PropertyTags.strategyAds);
return AdCause.internalError;
}
}
@override
Future<AdCause> requestShow({required String scene, bool ignoreCheck = false}) async {
Log.d("[$name] requestShow", tag: PropertyTags.strategyAds, syncFirebase: true);
try {
final result = await interstitialAd.show() ?? false;
if (result) {
return AdCause.success;
} else {
return AdCause.requestFailed;
}
} catch (error, stacktrace) {
Log.w("requestShow error",
error: error, stackTrace: stacktrace, syncFirebase: true, tag: PropertyTags.strategyAds);
return AdCause.internalError;
}
}
@override
Future<bool> checkLoaded() async {
try {
return await interstitialAd.isLoaded() ?? false;
} catch (error, stacktrace) {
return false;
}
}
@override
Future<int> getStatus() async {
try {
return await interstitialAd.getAdState();
} catch (error, stacktrace) {
Log.w("[$name] getInterstitialAdStatus error",
error: error, stackTrace: stacktrace, syncFirebase: true, tag: PropertyTags.strategyAds);
return AdStatus.FAILED;
}
}
}

View File

@ -0,0 +1,522 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:guru_app/ads/ads_manager.dart';
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/strategy/ad_unit.dart';
import 'package:guru_app/ads/core/strategy/interstitial/max_interstitial_ad_unit.dart';
import 'package:guru_app/ads/core/strategy/strategy_ads.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_utils/ads/ads_delegate.dart';
import 'package:guru_utils/ads/data/ads_model.dart';
import 'package:guru_utils/ads/handler/ads_handler.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/log/log.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
/// Created by Haoyi on 2023/6/28
typedef AdsEventDispatcher = void Function(AdsEvent, {Map<dynamic, dynamic> arguments});
abstract class AdsStrategy extends AdsLifecycleObserver {
final AdsEventDispatcher eventDispatcher;
AdsStrategy(this.eventDispatcher);
bool get loaded;
Stream<bool> get observableLoaded;
Future<AdCause> requestLoad();
Future<AdCause> requestShow({required String scene});
Future<bool> requestHide();
Future<AdCause> requestReset();
Future<AdState> getState();
Future<bool> requestDispose();
void dispatchEvent(AdsEvent adsEvent,
{Map<dynamic, dynamic> arguments = const <dynamic, dynamic>{}}) {
eventDispatcher.call(adsEvent, arguments: arguments);
}
}
class AdUnitRetryAgent {
MaxInterstitialAdUnit? adUnit;
final RetryConfig retryConfig;
int retryAttempt = 0;
Timer? _retryTimer;
AdUnitRetryAgent(this.adUnit, this.retryConfig);
void dispose() {
_retryTimer?.cancel();
_retryTimer = null;
adUnit = null;
}
Future<bool> retry() async {
final ad = adUnit;
if (_retryTimer?.isActive == true || ad == null) {
return false;
}
final adState = await ad.getState();
if (adState == AdState.loaded) {
return false;
}
retryAttempt++;
final delaySecond = pow(2, retryAttempt).toInt();
final config = retryConfig;
final duration = Duration(seconds: delaySecond.clamp(config.minInSecond, config.maxInSecond));
_retryTimer = Timer(duration, () {
adUnit?.requestLoad();
});
return true;
}
}
class MaxInterstitialStrategy extends AdsStrategy {
final List<MaxInterstitialAdUnit> adUnits = [];
MaxInterstitialAdUnit? _showingAdUnit;
final BehaviorSubject<StrategyAdsState> _strategyAdsStateSubject =
BehaviorSubject.seeded(StrategyAdsState.init);
StrategyAdsState get strategyAdsState => _strategyAdsStateSubject.value;
set strategyAdsState(StrategyAdsState value) {
_strategyAdsStateSubject.add(value);
}
Stream<StrategyAdsState> get observableStrategyAdsState => _strategyAdsStateSubject.stream;
@override
bool get loaded => strategyAdsState == StrategyAdsState.loaded;
final Map<dynamic, dynamic> _loadedAdsArguments = {};
bool upcomingAdLoadedEvent = false;
@override
Stream<bool> get observableLoaded =>
observableStrategyAdsState.map((event) => event == StrategyAdsState.loaded);
final List<Timer> loadTimers = [];
AdUnitRetryAgent? retryAgent;
MaxInterstitialStrategy(List<AdId> adIds, AdsEventDispatcher dispatcher) : super(dispatcher) {
for (var adId in adIds) {
adUnits.add(MaxInterstitialAdUnit.create(adId.adUnitId, adId.amazonAdSlotId)
..addObserver(this)
..init());
}
}
@override
void onAdLoaded(AdsBundle adsBundle) {
if (strategyAdsState != StrategyAdsState.loaded) {
disposeRetryAgent();
strategyAdsState = StrategyAdsState.loaded;
_loadedAdsArguments.clear();
_loadedAdsArguments.addAll(adsBundle.arguments);
if (_showingAdUnit == null) {
dispatchEvent(AdsEvent.adLoaded, arguments: adsBundle.arguments);
} else {
upcomingAdLoadedEvent = true;
}
}
}
@override
void onAdLoadFailed(AdsBundle adsBundle) async {
// retryAgent 广广
// strategy strategy
final _retryAgent = retryAgent;
if (_retryAgent != null) {
// retryAgent广
if (_retryAgent.adUnit != adsBundle.ads) {
return;
}
_retryAgent.retry();
return;
}
final noReservePriceAdUnit = adUnits.safeLast;
// 广
if (adsBundle.ads == noReservePriceAdUnit) {
retryAgent = AdUnitRetryAgent(noReservePriceAdUnit, RetryConfig(4, 30))..retry();
return;
}
checkAndRetry(msg: "onAdLoadFailed");
}
@override
void onAdDisplayed(AdsBundle adsBundle) async {
_showingAdUnit = adsBundle.ads as MaxInterstitialAdUnit;
dispatchEvent(AdsEvent.adDisplayed, arguments: adsBundle.arguments);
}
@override
void onAdDisplayFailed(AdsBundle adsBundle) async {
final failedAdUnit = adsBundle.ads as MaxInterstitialAdUnit;
for (var adUnit in adUnits) {
final state = await adUnit.getState();
if (state != AdState.loaded || failedAdUnit == adUnit) {
continue;
}
Log.d("[${failedAdUnit.adUnitId}] AdDisplayFailed! use ${adUnit.adUnitId} Ads Instead",
tag: PropertyTags.strategyAds);
try {
final result = await adUnit.show(scene: failedAdUnit.scene);
if (result == AdCause.success) {
_showingAdUnit = adUnit;
return;
} else {
continue;
}
} catch (error, stacktrace) {
Log.w("requestShow error",
tag: PropertyTags.strategyAds,
error: error,
stackTrace: stacktrace,
syncFirebase: true);
continue;
}
}
dispatchEvent(AdsEvent.adDisplayFailed, arguments: adsBundle.arguments);
Log.d("onAdDisplayFailed [$runtimeType] Not Found valid ads! requestLoad",
tag: PropertyTags.strategyAds);
requestLoad();
}
@override
void onAdHidden(AdsBundle adsBundle) {
dispatchEvent(AdsEvent.adHidden, arguments: adsBundle.arguments);
Log.d("[${_showingAdUnit?.adUnitId}] onAdHidden!", tag: PropertyTags.strategyAds);
_showingAdUnit = null;
checkAndRetry(msg: "onAdHidden");
}
@override
void onAdClicked(AdsBundle adsBundle) {
dispatchEvent(AdsEvent.adClick, arguments: adsBundle.arguments);
}
void disposeLoadTimer() {
for (var timer in loadTimers) {
timer.cancel();
}
loadTimers.clear();
}
void disposeRetryAgent() {
retryAgent?.dispose();
retryAgent = null;
}
@override
Future<AdCause> requestLoad() async {
final loadAdUnits = <MaxInterstitialAdUnit>[];
Log.d("[$runtimeType-$hashCode] requestLoad $strategyAdsState", tag: PropertyTags.strategyAds);
switch (strategyAdsState) {
case StrategyAdsState.init:
loadAdUnits.addAll(adUnits);
break;
case StrategyAdsState.loaded:
consumeUpcomingAdLoadedEvent();
dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments);
return AdCause.success;
case StrategyAdsState.disposed:
return AdCause.loadFailed;
default:
strategyAdsState = StrategyAdsState.idle;
for (var adUnit in adUnits) {
final state = await adUnit.getState();
Log.d("[$runtimeType] requestLoad check ${adUnit.adUnitId} is $state",
tag: PropertyTags.strategyAds);
if (state == AdState.loaded) {
strategyAdsState = StrategyAdsState.loaded;
consumeUpcomingAdLoadedEvent();
dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments);
return AdCause.success;
}
if (state != AdState.loading && adUnit != _showingAdUnit) {
loadAdUnits.add(adUnit);
}
}
}
if (loadAdUnits.isEmpty) {
return AdCause.success;
}
disposeLoadTimer();
disposeRetryAgent();
final timers = <Timer>[];
const gap = Duration(seconds: 5);
Duration delay = Duration.zero;
AdCause result = AdCause.requestFailed;
for (var adUnit in loadAdUnits) {
try {
if (delay > Duration.zero) {
timers.add(Timer(delay, () async {
if (strategyAdsState != StrategyAdsState.disposed) {
final r = await adUnit.load();
Log.d("[$runtimeType] request load ${adUnit.adUnitId} $r",
tag: PropertyTags.strategyAds);
}
}));
delay += gap;
} else {
result = await adUnit.load();
if (result == AdCause.success) {
Log.d("[$runtimeType] request load ${adUnit.adUnitId} success",
tag: PropertyTags.strategyAds);
delay += gap;
}
}
} catch (error, stacktrace) {
Log.w("requestLoad error! $error $stacktrace", tag: PropertyTags.strategyAds);
continue;
}
}
loadTimers.addAll(timers);
return result;
}
@override
Future<AdCause> requestShow({required String scene}) async {
final result = AdsManager.instance.canShowInterstitial(scene);
if (result != AdCause.success) {
return result;
}
if (strategyAdsState == StrategyAdsState.disposed) {
Log.d("[$runtimeType] requestShow Ads is disposed!", tag: PropertyTags.strategyAds);
return AdCause.displayFailed;
}
strategyAdsState = StrategyAdsState.idle;
for (var adUnit in adUnits) {
final state = await adUnit.getState();
if (state != AdState.loaded) {
Log.d("[$runtimeType] requestShow Check!! Ads [${adUnit.adUnitId}] state is $state",
tag: PropertyTags.strategyAds);
continue;
}
try {
final result = await adUnit.show(scene: scene);
Log.d("[$runtimeType] requestShow [${adUnit.adUnitId}]", tag: PropertyTags.strategyAds);
if (result == AdCause.success) {
_showingAdUnit = adUnit;
return AdCause.success;
} else {
continue;
}
} catch (error, stacktrace) {
Log.w("requestShow error ${adUnit.adUnitId}",
error: error,
stackTrace: stacktrace,
syncFirebase: true,
tag: PropertyTags.strategyAds);
continue;
}
}
Log.d("requestShow [$runtimeType] Not Found valid ads! requestLoad",
tag: PropertyTags.strategyAds);
requestLoad();
return AdCause.requestFailed;
}
bool consumeUpcomingAdLoadedEvent() {
final result = upcomingAdLoadedEvent;
upcomingAdLoadedEvent = false;
return result;
}
@override
Future<bool> requestHide() async {
return false;
}
@override
Future<bool> requestDispose() async {
for (var adUnit in adUnits) {
try {
Log.d("[$runtimeType] requestDispose [${adUnit.adUnitId}]", tag: PropertyTags.strategyAds);
await adUnit.dispose();
} catch (error, stacktrace) {
Log.w("requestDispose error",
error: error,
stackTrace: stacktrace,
syncFirebase: true,
tag: PropertyTags.strategyAds);
}
}
adUnits.clear();
strategyAdsState = StrategyAdsState.disposed;
return true;
}
@override
Future<AdState> getState() async {
final adStates = <AdState>[];
for (var adUnit in adUnits) {
final state = await adUnit.getState();
adStates.add(state);
}
for (var state in adStates) {
if (state == AdState.loaded) {
return AdState.loaded;
}
}
for (var state in adStates) {
if (state == AdState.loading) {
return AdState.loading;
}
}
for (var state in adStates) {
if (state != AdState.failed) {
return AdState.created;
}
}
return AdState.failed;
}
@override
Future<AdCause> requestReset() async {
return AdCause.internalError;
}
void dispatchAdLoadedEvent() {
dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments);
}
Future checkAndRetry({String? msg}) async {
if (strategyAdsState == StrategyAdsState.disposed) {
Log.d("[${msg ?? runtimeType}] checkAndRetry Ads is disposed!",
tag: PropertyTags.strategyAds);
return;
}
bool loading = false;
for (var adUnit in adUnits) {
try {
final state = await adUnit.getState();
Log.d("[${msg ?? runtimeType}] checkAndRetry ad [${adUnit.adUnitId}] $state",
tag: PropertyTags.strategyAds);
if (state == AdState.loading) {
loading = true;
continue;
}
if (state == AdState.loaded) {
if (consumeUpcomingAdLoadedEvent()) {
dispatchEvent(AdsEvent.adLoaded, arguments: _loadedAdsArguments);
}
strategyAdsState = StrategyAdsState.loaded;
continue;
}
} catch (error, stacktrace) {
Log.w("check state error",
error: error, stackTrace: stacktrace, tag: PropertyTags.strategyAds);
}
}
if (loading) {
Log.d("[${msg ?? runtimeType}] checkAndRetry loading! waiting ads...",
tag: PropertyTags.strategyAds);
strategyAdsState = StrategyAdsState.idle;
return;
}
if (strategyAdsState != StrategyAdsState.loaded) {
Log.d("[${msg ?? runtimeType}] Not Found loaded ads! requestLoad",
tag: PropertyTags.strategyAds);
strategyAdsState = StrategyAdsState.init;
requestLoad();
}
}
}
class MaxStrategyInterstitialAds extends StrategyAds with AdsAudit {
MaxStrategyInterstitialAds.create(List<AdId> adIds) : super() {
strategy = MaxInterstitialStrategy(adIds, dispatchEvent);
}
@override
@mustCallSuper
Future dispose() async {
bool result = false;
try {
result = await strategy.requestDispose();
} catch (error, stacktrace) {
Log.d("requestDispose error:$error $stacktrace", tag: PropertyTags.strategyAds);
}
onRequestDispose(AdsBundle.create(this));
super.dispose();
return result;
}
@override
@mustCallSuper
Future<bool> reset() async {
AdCause result = AdCause.internalError;
try {
result = await strategy.requestReset();
} catch (error, stacktrace) {
Log.d("requestReset error:$error $stacktrace", tag: PropertyTags.strategyAds);
}
onRequestReset(AdsBundle.create(this));
return result == AdCause.success;
}
@mustCallSuper
void init() {}
@override
Future<AdCause> load() async {
final adCause = await strategy.requestLoad().catchError((error, stacktrace) {
Log.e("load error! ",
tag: PropertyTags.strategyAds, error: error, stackTrace: stacktrace, syncFirebase: true);
return AdCause.internalError;
});
Log.d("[$runtimeType]request load complete!! $adCause", tag: PropertyTags.strategyAds);
onRequestLoad(AdsBundle.create(this, arguments: {"cause": adCause}));
return adCause;
}
@override
Future hide() async {
onRequestHide(AdsBundle.create(this));
return await strategy.requestHide();
}
@override
Future<AdCause> show({required String scene, bool ignoreCheck = false}) async {
final adCause = await strategy.requestShow(scene: scene).catchError((error, stacktrace) {
Log.e("show error! $error", stackTrace: stacktrace, syncFirebase: true);
return AdCause.internalError;
});
Log.d("[$runtimeType]show $scene complete!! $adCause",
syncFirebase: true, tag: PropertyTags.strategyAds);
onRequestShow(AdsBundle.create(this, arguments: {"scene": scene, "cause": adCause}));
return adCause;
}
@override
Future<AdState> getState() async {
return await strategy.getState();
}
@override
void preload() {
if (AdsManager.instance.connectivityStatus != ConnectivityResult.none) {
load();
}
}
}

View File

@ -0,0 +1,60 @@
import 'package:guru_app/ads/core/ads.dart';
import 'package:guru_app/ads/core/strategy/interstitial/max_strategy_interstitial_ads.dart';
import 'package:guru_utils/ads/ads_delegate.dart';
import 'package:guru_utils/ads/data/ads_model.dart';
import 'package:guru_utils/ads/handler/ads_handler.dart';
import 'package:guru_utils/extensions/extensions.dart';
/// Created by Haoyi on 2023/6/27
enum StrategyAdsState { init, idle, loaded, disposed }
enum StrategyAdsPhase { create, loading, loaded, displayed, }
abstract class StrategyAds extends Ads {
final BehaviorSubject<bool> loadedSubject = BehaviorSubject.seeded(false);
@override
set loaded(bool loaded) {
loadedSubject.addEx(loaded);
setProperty("isLoaded", loaded ? "true" : "false");
}
@override
bool get loaded => strategy.loaded;
@override
Stream<bool> get observableLoaded => strategy.observableLoaded;
late final AdsStrategy strategy;
StrategyAds();
void dispatchEvent(AdsEvent adsEvent,
{Map<dynamic, dynamic> arguments = const <dynamic, dynamic>{}}) {
final adsBundle = AdsBundle.create(this, arguments: arguments);
switch (adsEvent) {
case AdsEvent.adLoaded:
onAdLoaded(adsBundle);
break;
case AdsEvent.adLoadFailed:
onAdLoadFailed(adsBundle);
break;
case AdsEvent.adDisplayed:
onAdDisplayed(adsBundle);
break;
case AdsEvent.adDisplayFailed:
onAdDisplayFailed(adsBundle);
break;
case AdsEvent.adClick:
onAdClicked(adsBundle);
break;
case AdsEvent.adHidden:
onAdHidden(adsBundle);
break;
case AdsEvent.adRewarded:
onAdRewarded(adsBundle);
break;
}
}
}

View File

@ -0,0 +1,6 @@
/// Created by Haoyi on 2021/11/26
class AdsCpmCalibration {
static const defaultAndroidFacebookCpmCalibrationData = '';
static const defaultIOSFacebookCpmCalibrationData = '{"data":{"list":[{"format":"inter","cpm":0.032877,"country":"us"},{"format":"reward","cpm":0.107767,"country":"us"},{"format":"inter","cpm":0.005034,"country":"in"},{"format":"reward","cpm":null,"country":"in"},{"format":"inter","cpm":0.011241,"country":"ca"},{"format":"reward","cpm":0.094526,"country":"ca"},{"format":"inter","cpm":0.003824,"country":"br"},{"format":"reward","cpm":0.009776,"country":"br"},{"format":"inter","cpm":0.049431,"country":"au"},{"format":"reward","cpm":0.056175,"country":"au"},{"format":"inter","cpm":0.012067,"country":"jp"},{"format":"reward","cpm":0.055358,"country":"jp"},{"format":"inter","cpm":0.01472,"country":"de"},{"format":"reward","cpm":0.020876,"country":"de"},{"format":"inter","cpm":0.011296,"country":"gb"},{"format":"reward","cpm":0.027093,"country":"gb"},{"format":"inter","cpm":0.006439,"country":"fr"},{"format":"reward","cpm":0.018981,"country":"fr"},{"format":"inter","cpm":null,"country":"tw"},{"format":"reward","cpm":null,"country":"tw"},{"format":"inter","cpm":null,"country":"kr"},{"format":"reward","cpm":null,"country":"kr"},{"format":"inter","cpm":0.010416,"country":"ph"},{"format":"reward","cpm":null,"country":"ph"},{"format":"inter","cpm":0.00599,"country":"ru"},{"format":"reward","cpm":0.004472,"country":"ru"},{"format":"inter","cpm":null,"country":"it"},{"format":"reward","cpm":null,"country":"it"},{"format":"inter","cpm":0.010365,"country":"es"},{"format":"reward","cpm":null,"country":"es"},{"format":"inter","cpm":null,"country":"hk"},{"format":"reward","cpm":null,"country":"hk"},{"format":"inter","cpm":0.007427,"country":"mx"},{"format":"reward","cpm":0.01035,"country":"mx"},{"format":"inter","cpm":null,"country":"nl"},{"format":"reward","cpm":null,"country":"nl"},{"format":"inter","cpm":null,"country":"sa"},{"format":"reward","cpm":null,"country":"sa"},{"format":"inter","cpm":null,"country":"be"},{"format":"reward","cpm":null,"country":"be"},{"format":"inter","cpm":null,"country":"se"},{"format":"reward","cpm":null,"country":"se"},{"format":"inter","cpm":null,"country":"sg"},{"format":"reward","cpm":null,"country":"sg"},{"format":"inter","cpm":null,"country":"cl"},{"format":"reward","cpm":null,"country":"cl"},{"format":"inter","cpm":0.01166,"country":"ch"},{"format":"reward","cpm":null,"country":"ch"},{"format":"inter","cpm":null,"country":"fi"},{"format":"reward","cpm":null,"country":"fi"},{"format":"inter","cpm":null,"country":"th"},{"format":"reward","cpm":null,"country":"th"},{"format":"inter","cpm":0.00109,"country":"pl"},{"format":"reward","cpm":null,"country":"pl"},{"format":"inter","cpm":null,"country":"dk"},{"format":"reward","cpm":null,"country":"dk"},{"format":"inter","cpm":null,"country":"ae"},{"format":"reward","cpm":null,"country":"ae"},{"format":"inter","cpm":0.009707,"country":"at"},{"format":"reward","cpm":0.011048,"country":"at"},{"format":"inter","cpm":0.004342,"country":"id"},{"format":"reward","cpm":null,"country":"id"},{"format":"inter","cpm":0.005622,"country":"vn"},{"format":"reward","cpm":0.005026,"country":"vn"},{"format":"inter","cpm":0.001684,"country":"tr"},{"format":"reward","cpm":null,"country":"tr"}]}}';
}

View File

@ -0,0 +1,15 @@
import "dart:core";
/// Created by Haoyi on 2021/11/29
///
class NoAdsException implements Exception {
final String msg;
NoAdsException(this.msg);
@override
String toString() {
return 'NoAdsException{msg: $msg}';
}
}

View File

@ -0,0 +1,2 @@
import "package:guru_utils/aigc/bi/ai_bi.dart";
export "package:guru_utils/aigc/bi/ai_bi.dart";

View File

@ -0,0 +1,388 @@
import 'dart:io';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
import 'package:guru_app/property/settings/guru_settings.dart';
import 'package:guru_utils/device/device_utils.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/random/random_utils.dart';
import 'package:json_annotation/json_annotation.dart';
part 'abtest_model.g.dart';
class ConditionOpt {
static const equals = "eq";
static const greaterThan = "gt";
static const greaterThanOrEquals = "gte";
static const lessThan = "lt";
static const lessThanOrEquals = "lte";
static const notEquals = "ne";
static bool evaluate<T extends Comparable>(T value, T target, String opt) {
final result = value.compareTo(target);
switch (opt) {
case ConditionOpt.equals:
return result == 0;
case ConditionOpt.greaterThan:
return result > 0;
case ConditionOpt.greaterThanOrEquals:
return result >= 0;
case ConditionOpt.lessThan:
return result < 0;
case ConditionOpt.lessThanOrEquals:
return result <= 0;
case ConditionOpt.notEquals:
return result != 0;
default:
return false;
}
}
}
abstract class ABTestFilter {
static const platform = 1;
static const version = 2;
static const country = 3;
static const newUser = 4;
final int type;
const ABTestFilter(this.type);
bool filter();
factory ABTestFilter.fromJson(Map<String, dynamic> json) {
final type = json["type"] = json["type"] ?? 0;
switch (type) {
case ABTestFilter.platform:
return PlatformFilter.fromJson(json);
case ABTestFilter.country:
return CountryFilter.fromJson(json);
case ABTestFilter.version:
return VersionFilter.fromJson(json);
case ABTestFilter.newUser:
return NewUserFilter.fromJson(json);
default:
throw UnimplementedError("Unknown ABTestFilter type: $type");
}
}
Map<String, dynamic> toJson() => toJson()..addAll({"type": type});
}
abstract class ABTestCondition {
bool validate();
}
///
@JsonSerializable()
class AndroidCondition extends ABTestCondition {
@JsonKey(name: "opt")
final String? opt;
@JsonKey(name: "sdk")
final int? sdkInt;
AndroidCondition({this.opt, this.sdkInt});
factory AndroidCondition.fromJson(Map<String, dynamic> json) => _$AndroidConditionFromJson(json);
Map<String, dynamic> toJson() => _$AndroidConditionToJson(this);
@override
bool validate() {
final versionOpt = opt;
final targetVersion = sdkInt;
if (versionOpt != null && targetVersion != null) {
final versionCode = DeviceUtils.peekOSVersion();
// false
if (versionCode == -1) {
return false;
}
if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
return false;
}
}
// 便
return true;
}
}
@JsonSerializable()
class IosCondition extends ABTestCondition {
@JsonKey(name: "opt")
final String? opt;
@JsonKey(name: "ver")
final int? version; //
IosCondition({this.opt, this.version});
factory IosCondition.fromJson(Map<String, dynamic> json) => _$IosConditionFromJson(json);
Map<String, dynamic> toJson() => _$IosConditionToJson(this);
@override
bool validate() {
final versionOpt = opt;
final targetVersion = version;
if (versionOpt != null && targetVersion != null) {
final versionCode = DeviceUtils.peekOSVersion();
// false
if (versionCode == -1) {
return false;
}
if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
return false;
}
}
// 便
return true;
}
}
@JsonSerializable()
class PlatformFilter extends ABTestFilter {
@JsonKey(name: "ac")
final AndroidCondition? androidCondition;
@JsonKey(name: "ic")
final IosCondition? iosCondition;
PlatformFilter({this.androidCondition, this.iosCondition}) : super(ABTestFilter.platform);
@override
bool filter() {
// Platform Filter, condition, true
if (Platform.isAndroid) {
return androidCondition?.validate() != false;
} else if (Platform.isIOS) {
return iosCondition?.validate() != false;
}
return false;
}
factory PlatformFilter.fromJson(Map<String, dynamic> json) => _$PlatformFilterFromJson(json);
@override
Map<String, dynamic> toJson() => _$PlatformFilterToJson(this);
}
@JsonSerializable(constructor: "_")
class VersionFilter extends ABTestFilter {
@JsonKey(name: "opt")
final String opt;
@JsonKey(name: "mmp")
final String mmp; // major.minor.patch
VersionFilter._(this.opt, this.mmp) : super(ABTestFilter.version);
VersionFilter.equals(this.mmp)
: opt = ConditionOpt.equals,
super(ABTestFilter.version);
VersionFilter.greaterThan(this.mmp)
: opt = ConditionOpt.greaterThan,
super(ABTestFilter.version);
VersionFilter.greaterThanOrEquals(this.mmp)
: opt = ConditionOpt.greaterThanOrEquals,
super(ABTestFilter.version);
VersionFilter.lessThan(this.mmp)
: opt = ConditionOpt.lessThan,
super(ABTestFilter.version);
VersionFilter.lessThanOrEquals(this.mmp)
: opt = ConditionOpt.lessThanOrEquals,
super(ABTestFilter.version);
VersionFilter.notEquals(this.mmp)
: opt = ConditionOpt.notEquals,
super(ABTestFilter.version);
@override
bool filter() {
final version = GuruSettings.instance.version.get();
Log.d("[$runtimeType] $version $opt $mmp");
return ConditionOpt.evaluate<String>(version, mmp, opt);
}
@override
String toString() {
return 'VersionValidator{opt: $opt, mmp: $mmp}';
}
factory VersionFilter.fromJson(Map<String, dynamic> json) => _$VersionFilterFromJson(json);
@override
Map<String, dynamic> toJson() => _$VersionFilterToJson(this);
}
@JsonSerializable(constructor: "_")
class CountryFilter extends ABTestFilter {
@JsonKey(name: "included", defaultValue: {})
final Set<String> included;
@JsonKey(name: "excluded", defaultValue: {})
final Set<String> excluded;
CountryFilter._(this.included, this.excluded) : super(ABTestFilter.country);
CountryFilter.included(this.included)
: excluded = {},
super(ABTestFilter.country);
CountryFilter.excluded(this.excluded)
: included = {},
super(ABTestFilter.country);
@override
bool filter() {
final String countryCode = Platform.localeName.split('_').safeLast?.toLowerCase() ?? "";
Log.d("[$runtimeType] $countryCode included: $included excluded: $excluded");
if (countryCode.isEmpty) {
return false;
}
// excludedvalidateexcluded
// included
if (excluded.isNotEmpty) {
return !excluded.contains(countryCode);
}
if (included.contains(countryCode)) {
return true;
}
return false;
}
factory CountryFilter.fromJson(Map<String, dynamic> json) => _$CountryFilterFromJson(json);
@override
Map<String, dynamic> toJson() => _$CountryFilterToJson(this);
}
@JsonSerializable()
class NewUserFilter extends ABTestFilter {
NewUserFilter() : super(ABTestFilter.newUser);
@override
bool filter() {
final version = GuruSettings.instance.version.get();
final fiv = GuruSettings.instance.firstInstallVersion.get();
return fiv.startsWith(version);
}
factory NewUserFilter.fromJson(Map<String, dynamic> json) => _$NewUserFilterFromJson(json);
@override
Map<String, dynamic> toJson() => _$NewUserFilterToJson(this);
}
@JsonSerializable()
class ABTestAudience {
@JsonKey(name: "filters")
final List<ABTestFilter> filters;
@JsonKey(name: "variant", defaultValue: 2)
final int variant;
ABTestAudience({required this.filters, this.variant = 2});
factory ABTestAudience.fromJson(Map<String, dynamic> json) => _$ABTestAudienceFromJson(json);
Map<String, dynamic> toJson() => _$ABTestAudienceToJson(this);
@override
String toString() {
return 'ABTestAudience{filters: $filters, variant: $variant}';
}
bool validate() {
for (var filter in filters) {
if (!filter.filter()) {
return false;
}
}
return true;
}
}
@JsonSerializable()
class ABTestExperiment {
@JsonKey(name: "name")
final String name;
@JsonKey(name: "start_ts", defaultValue: 0)
final int startTs;
@JsonKey(name: "end_ts", defaultValue: 0)
final int endTs;
@JsonKey(name: "audience")
final ABTestAudience audience;
ABTestExperiment(
{required String name, required this.startTs, required this.endTs, required this.audience})
: name = _validExperimentName(name);
@override
String toString() {
return 'ABTestExperiment{name: $name, startTs: $startTs, endTs: $endTs, audience: $audience}';
}
static String _validExperimentName(String experimentName) {
if (experimentName.contains(RemoteConfigManager.invalidABKeyRegExp)) {
Log.w("abName($experimentName) use invalid key! $experimentName! replace invalid char to _");
experimentName = experimentName.replaceAll(RemoteConfigManager.invalidABKeyRegExp, "_");
} else {
if (experimentName.length > 20) {
experimentName = experimentName.substring(0, 20);
}
}
return experimentName;
}
factory ABTestExperiment.fromJson(Map<String, dynamic> json) => _$ABTestExperimentFromJson(json);
Map<String, dynamic> toJson() => _$ABTestExperimentToJson(this);
bool isExpired() {
final now = DateTime.now().millisecondsSinceEpoch;
return now < startTs || now > endTs;
}
bool isMatchAudience() {
return audience.validate();
}
@JsonKey(includeToJson: false)
String? _variantName;
String get variantName =>
(_variantName ??= _toVariantName(RandomUtils.nextInt(audience.variant)));
static const _originalVariant = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
String _toVariantName(int value) {
String codes = "";
int nv = value;
while (true) {
final nextNum = nv ~/ _originalVariant.length;
if (nextNum <= 0) {
break;
}
codes = "${_originalVariant[nv % _originalVariant.length]}$codes";
nv = nextNum;
}
final tailIndex = nv % _originalVariant.length;
if (tailIndex >= 0) {
codes = "${_originalVariant[tailIndex]}$codes";
}
return codes.toString();
}
}

View File

@ -0,0 +1,109 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'abtest_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AndroidCondition _$AndroidConditionFromJson(Map<String, dynamic> json) =>
AndroidCondition(
opt: json['opt'] as String?,
sdkInt: json['sdk'] as int?,
);
Map<String, dynamic> _$AndroidConditionToJson(AndroidCondition instance) =>
<String, dynamic>{
'opt': instance.opt,
'sdk': instance.sdkInt,
};
IosCondition _$IosConditionFromJson(Map<String, dynamic> json) => IosCondition(
opt: json['opt'] as String?,
version: json['ver'] as int?,
);
Map<String, dynamic> _$IosConditionToJson(IosCondition instance) =>
<String, dynamic>{
'opt': instance.opt,
'ver': instance.version,
};
PlatformFilter _$PlatformFilterFromJson(Map<String, dynamic> json) =>
PlatformFilter(
androidCondition: json['ac'] == null
? null
: AndroidCondition.fromJson(json['ac'] as Map<String, dynamic>),
iosCondition: json['ic'] == null
? null
: IosCondition.fromJson(json['ic'] as Map<String, dynamic>),
);
Map<String, dynamic> _$PlatformFilterToJson(PlatformFilter instance) =>
<String, dynamic>{
'ac': instance.androidCondition,
'ic': instance.iosCondition,
};
VersionFilter _$VersionFilterFromJson(Map<String, dynamic> json) =>
VersionFilter._(
json['opt'] as String,
json['mmp'] as String,
);
Map<String, dynamic> _$VersionFilterToJson(VersionFilter instance) =>
<String, dynamic>{
'opt': instance.opt,
'mmp': instance.mmp,
};
CountryFilter _$CountryFilterFromJson(Map<String, dynamic> json) =>
CountryFilter._(
(json['included'] as List<dynamic>?)?.map((e) => e as String).toSet() ??
{},
(json['excluded'] as List<dynamic>?)?.map((e) => e as String).toSet() ??
{},
);
Map<String, dynamic> _$CountryFilterToJson(CountryFilter instance) =>
<String, dynamic>{
'included': instance.included.toList(),
'excluded': instance.excluded.toList(),
};
NewUserFilter _$NewUserFilterFromJson(Map<String, dynamic> json) =>
NewUserFilter();
Map<String, dynamic> _$NewUserFilterToJson(NewUserFilter instance) =>
<String, dynamic>{};
ABTestAudience _$ABTestAudienceFromJson(Map<String, dynamic> json) =>
ABTestAudience(
filters: (json['filters'] as List<dynamic>)
.map((e) => ABTestFilter.fromJson(e as Map<String, dynamic>))
.toList(),
variant: json['variant'] as int? ?? 2,
);
Map<String, dynamic> _$ABTestAudienceToJson(ABTestAudience instance) =>
<String, dynamic>{
'filters': instance.filters,
'variant': instance.variant,
};
ABTestExperiment _$ABTestExperimentFromJson(Map<String, dynamic> json) =>
ABTestExperiment(
name: json['name'] as String,
startTs: json['start_ts'] as int? ?? 0,
endTs: json['end_ts'] as int? ?? 0,
audience:
ABTestAudience.fromJson(json['audience'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ABTestExperimentToJson(ABTestExperiment instance) =>
<String, dynamic>{
'name': instance.name,
'start_ts': instance.startTs,
'end_ts': instance.endTs,
'audience': instance.audience,
};

View File

@ -0,0 +1,99 @@
import 'package:guru_analytics_flutter/events_constants.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:guru_utils/converts/converts.dart';
/// Created by Haoyi on 2023/2/9
///
part 'analytics_model.g.dart';
@JsonSerializable()
class AnalyticsConfig {
static const _defaultGoogleDma = [1, 0, 12, 65];
static const _defaultDmaCountry = [];
@JsonKey(name: "cap", defaultValue: ["firebase", "facebook", "guru"])
@joinedStringConvert
final List<String> capabilities;
@JsonKey(name: "init_delay_s", defaultValue: 10)
final int delayedInSeconds;
@JsonKey(name: "expired_d", defaultValue: 7)
final int expiredInDays;
@JsonKey(name: "strategy", defaultValue: '')
final String strategy;
@JsonKey(name: "enabled_strategy", defaultValue: false)
final bool enabledStrategy;
/// ad_storage,analytics_storage,personalization,user_data
@JsonKey(name: "google_dma", defaultValue: _defaultGoogleDma)
final List<int> googleDmaMask;
@JsonKey(name: "dma_country", defaultValue: _defaultDmaCountry)
final List<String> dmaCountry;
AppEventCapabilities toAppEventCapabilities() {
int capValue = 0;
if (capabilities.contains("firebase")) {
capValue |= AppEventCapabilities.firebase;
}
if (capabilities.contains("facebook")) {
capValue |= AppEventCapabilities.facebook;
}
if (capabilities.contains("guru")) {
capValue |= AppEventCapabilities.guru;
}
return AppEventCapabilities(capValue);
}
bool googleDmaGranted(ConsentType type, int flags) {
if (type.index < googleDmaMask.length) {
return (googleDmaMask[type.index] & flags) == googleDmaMask[type.index];
}
return _defaultGoogleDma[type.index] & flags == _defaultGoogleDma[type.index];
}
AnalyticsConfig(this.capabilities, this.delayedInSeconds, this.expiredInDays, this.strategy,
this.enabledStrategy, this.googleDmaMask, this.dmaCountry);
factory AnalyticsConfig.fromJson(Map<String, dynamic> json) => _$AnalyticsConfigFromJson(json);
Map<String, dynamic> toJson() => _$AnalyticsConfigToJson(this);
}
@JsonSerializable()
class UserIdentification {
@JsonKey(name: 'firebaseAppInstanceId', defaultValue: "")
final String firebaseAppInstanceId;
@JsonKey(name: 'idfa')
final String? idfa;
@JsonKey(name: "adid")
final String? adId;
@JsonKey(name: "gpsAdid")
final String? gpsAdId;
UserIdentification({this.firebaseAppInstanceId = '', this.idfa, this.adId, this.gpsAdId});
factory UserIdentification.fromJson(Map<String, dynamic> json) =>
_$UserIdentificationFromJson(json);
Map<String, dynamic> toJson() => _$UserIdentificationToJson(this);
@override
String toString() {
return 'UserIdentification{firebaseAppInstanceId: $firebaseAppInstanceId, idfa: $idfa, adId: $adId, gpsAdId: $gpsAdId}';
}
}
class ConsentFieldName {
static const adStorage = "ad_storage";
static const analyticsStorage = "analytics_storage";
static const adPersonalization = "ad_personalization";
static const adUserData = "ad_user_data";
}
enum ConsentType { adStorage, analyticsStorage, adPersonalization, adUserData }

View File

@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'analytics_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AnalyticsConfig _$AnalyticsConfigFromJson(Map<String, dynamic> json) =>
AnalyticsConfig(
json['cap'] == null
? ['firebase', 'facebook', 'guru']
: joinedStringConvert.fromJson(json['cap'] as String),
json['init_delay_s'] as int? ?? 10,
json['expired_d'] as int? ?? 7,
json['strategy'] as String? ?? '',
json['enabled_strategy'] as bool? ?? false,
(json['google_dma'] as List<dynamic>?)?.map((e) => e as int).toList() ??
[1, 0, 12, 65],
(json['dma_country'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
Map<String, dynamic> _$AnalyticsConfigToJson(AnalyticsConfig instance) =>
<String, dynamic>{
'cap': joinedStringConvert.toJson(instance.capabilities),
'init_delay_s': instance.delayedInSeconds,
'expired_d': instance.expiredInDays,
'strategy': instance.strategy,
'enabled_strategy': instance.enabledStrategy,
'google_dma': instance.googleDmaMask,
'dma_country': instance.dmaCountry,
};
UserIdentification _$UserIdentificationFromJson(Map<String, dynamic> json) =>
UserIdentification(
firebaseAppInstanceId: json['firebaseAppInstanceId'] as String? ?? '',
idfa: json['idfa'] as String?,
adId: json['adid'] as String?,
gpsAdId: json['gpsAdid'] as String?,
);
Map<String, dynamic> _$UserIdentificationToJson(UserIdentification instance) =>
<String, dynamic>{
'firebaseAppInstanceId': instance.firebaseAppInstanceId,
'idfa': instance.idfa,
'adid': instance.adId,
'gpsAdid': instance.gpsAdId,
};

View File

@ -0,0 +1,810 @@
/// Created by Haoyi on 2022/8/24
import 'dart:collection';
import 'dart:core';
import 'dart:io';
import 'package:adjust_sdk/adjust_third_party_sharing.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/foundation.dart';
import 'package:guru_analytics_flutter/event_logger.dart';
import 'package:guru_analytics_flutter/event_logger_common.dart';
import 'package:guru_analytics_flutter/events_constants.dart';
import 'package:guru_analytics_flutter/guru/guru_event_logger.dart';
import 'package:guru_analytics_flutter/guru/guru_statistic.dart';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/ads/ads_manager.dart';
import 'package:guru_app/ads/core/ads_config.dart';
import 'package:guru_app/aigc/bi/ai_bi.dart';
import 'package:guru_app/analytics/abtest/abtest_model.dart';
import 'package:guru_app/analytics/data/analytics_model.dart';
import 'package:guru_app/analytics/strategy/guru_analytics_strategy.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_app/property/property_keys.dart';
import 'package:guru_app/property/runtime_property.dart';
import 'package:guru_app/property/settings/guru_settings.dart';
import 'package:guru_platform_data/guru_platform_data.dart';
import 'package:guru_utils/collection/collectionutils.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/device/device_info.dart';
import 'package:guru_utils/device/device_utils.dart';
import 'package:guru_utils/analytics/analytics.dart';
import 'package:guru_utils/network/network_utils.dart';
import 'package:intl/intl.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:adjust_sdk/adjust_ad_revenue.dart';
import 'package:adjust_sdk/adjust_config.dart';
export 'package:adjust_sdk/adjust.dart';
part 'modules/ads_analytics.dart';
part 'modules/adjust_aware.dart';
class GuruAnalytics extends Analytics with AdjustAware {
bool get release => !_mock && (_enabledAnalytics || kReleaseMode);
String appInstanceId = "";
static bool _mock = false;
static bool _enabledAnalytics = true;
static GuruAnalytics instance = GuruAnalytics._();
/// Name of virtual currency type.
static bool initialized = false;
static final Map<String, String> facebookEventMapping = {};
static String currentScreen = "";
static final RegExp _consentPurposeRegExp = RegExp(r"^[01]+$");
static String? mockCountryCode;
static const errorEventCodes = {
14, //
22, //
101, // api
102, // api
103, // cacheControl
104, //
105, //
106, // dns
};
int latestFetchStatisticTs = 0;
final BehaviorSubject<GuruStatistic> guruEventStatistic =
BehaviorSubject.seeded(GuruStatistic.invalid);
final BehaviorSubject<Map<String, String>> abTestExperimentVariant = BehaviorSubject.seeded({});
Stream<GuruStatistic> get observableGuruEventStatistic => guruEventStatistic.stream;
Stream<Map<String, String>> get observableABTestExperimentVariant =>
abTestExperimentVariant.stream;
final BehaviorSubject<UserIdentification> userIdentificationSubject =
BehaviorSubject.seeded(UserIdentification());
UserIdentification get userIdentification => userIdentificationSubject.value;
AppEventCapabilities get currentAppEventCapabilities => EventLogger.getCapabilities();
static void setMock() {
_mock = true;
}
static void disableAnalytics() {
_enabledAnalytics = false;
}
static void enableAnalytics() {
_enabledAnalytics = true;
}
GuruAnalytics._();
String? getProperty(String 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 {
Log.d(
"AnalyticsUtil init### Platform.localeName :${Platform.localeName} ${Intl.getCurrentLocale()}");
if (!_mock && !initialized) {
final analyticsConfig = RemoteConfigManager.instance.getAnalyticsConfig();
EventLogger.setCapabilities(analyticsConfig.toAppEventCapabilities());
EventLogger.registerTransmitter(EventTransmitter({}, defaultHook: (name, parameters) {
recordEvents(name, parameters);
final fbEvent = facebookEventMapping[name];
if (fbEvent == null) {
return;
}
Log.d("transmit EVENT [$name] => [$fbEvent]");
EventLogger.facebookLogEvent(name: fbEvent);
}));
EventLogger.setGuruPriorityGetter((name, parameters) =>
GuruApp.instance.conversionEvents.contains(name)
? EventPriority.EMERGENCE
: EventPriority.DEFAULT);
String xDeviceInfo = '';
try {
final deviceId = await AppProperty.getInstance().getDeviceId();
final deviceInfo = await DeviceUtils.buildDeviceInfo(deviceId: deviceId);
xDeviceInfo = deviceInfo.toXDeviceInfo();
} catch (error, stacktrace) {
Log.e("init deviceInfo error: $error, $stacktrace");
}
await GuruAnalyticsStrategy.instance.load();
EventLogger.initialize(
appId: GuruApp.instance.appSpec.details.saasAppId,
deviceInfo: xDeviceInfo,
delayedInSeconds: analyticsConfig.delayedInSeconds,
eventExpiredInDays: analyticsConfig.expiredInDays,
callback: processAnalyticsCallback,
debug: true,
);
_initEnvProperties();
_logLocale();
_logDeviceType();
_logFirstOpen();
Future.delayed(const Duration(seconds: 1), () {
initAdjust();
initFbEventMapping();
refreshConsents();
Log.d("register transmitter");
});
initialized = true;
// if (Platform.isAndroid) {
// _logPeerApps();
// }
}
}
Future switchSession(String oldToken, String newToken) async {
_initEnvProperties();
_logLocale();
_logDeviceType();
}
Future initLocalExperiments() async {
final runningExperiments = await AppProperty.getInstance().loadRunningExperiments();
final experiments = GuruApp.instance.appSpec.localABTestExperiments;
final validRunningExperimentKeys =
runningExperiments.keys.toSet().intersection(experiments.keys.toSet());
for (var experiment in experiments.values) {
//
final needRemove = runningExperiments.containsKey(experiment.name) &&
!validRunningExperimentKeys.contains(experiment.name);
if (needRemove) {
await removeExperiment(experiment.name);
} else {
await _applyExperiment(experiment);
}
}
}
Future<String> refreshConsents({AnalyticsConfig? analyticsConfig}) async {
final config = analyticsConfig ?? RemoteConfigManager.instance.getAnalyticsConfig();
final purposeConsents = await GuruPlatformData.getPurposeConsents();
Log.i("refreshConsents: '$purposeConsents'");
if (purposeConsents.isEmpty) {
return "";
}
/// 使 10
if (!_consentPurposeRegExp.hasMatch(purposeConsents)) {
Log.i("invalid consents $purposeConsents");
return "";
}
/// countryCode, dma country
if (config.dmaCountry.isNotEmpty) {
final countryCode = getCountryCode();
if (!config.dmaCountry.contains(countryCode)) {
Log.i("invalid country $countryCode");
return "";
}
}
final length = min(purposeConsents.length, 32);
int flags = 0;
for (var i = 0; i < length; i++) {
flags |= (((purposeConsents[i] == "1") ? 1 : 0) << i);
}
final consentsData = {
ConsentFieldName.adStorage: config.googleDmaGranted(ConsentType.adStorage, flags),
ConsentFieldName.analyticsStorage:
config.googleDmaGranted(ConsentType.analyticsStorage, flags),
ConsentFieldName.adPersonalization:
config.googleDmaGranted(ConsentType.adPersonalization, flags),
ConsentFieldName.adUserData: config.googleDmaGranted(ConsentType.adUserData, flags),
};
String _flag(String key) {
return consentsData[key] == true ? "1" : "0";
}
Log.d("setConsents consentsData: $consentsData");
try {
final result = await EventLogger.guru.setConsents(consentsData);
Log.d("setConsents result: $result");
} catch (error, stacktrace) {
Log.e("setConsents error! $error, $stacktrace");
}
if (enabledAdjust) {
AdjustThirdPartySharing adjustThirdPartySharing = AdjustThirdPartySharing(null);
adjustThirdPartySharing.addGranularOption("google_dma", "eea", "1");
adjustThirdPartySharing.addGranularOption(
"google_dma", "ad_personalization", _flag(ConsentFieldName.adPersonalization));
adjustThirdPartySharing.addGranularOption(
"google_dma", "ad_user_data", _flag(ConsentFieldName.adUserData));
Adjust.trackThirdPartySharing(adjustThirdPartySharing);
Log.d("setAdjust complete!");
}
final result =
"${_flag(ConsentFieldName.adStorage)}${_flag(ConsentFieldName.analyticsStorage)}${_flag(ConsentFieldName.adPersonalization)}${_flag(ConsentFieldName.adUserData)}";
final changed = await AppProperty.getInstance().refreshGoogleDma(result);
if (changed || GuruSettings.instance.debugMode.get()) {
logEventEx("dma_gg", parameters: {"purpose": purposeConsents, "result": result});
}
return result;
}
void processAnalyticsCallback(int code, String? errorInfo) {
if (!errorEventCodes.contains(code)) {
return;
}
final parameters = {
"item_category": "error_event",
"item_name": code.toString(),
"country": AccountDataStore.instance.countryCode,
"network": AdsManager.instance.connectivityStatus.toString(),
};
if (errorInfo != null) {
parameters["err"] = errorInfo.length > 32 ? errorInfo.substring(0, 32) : errorInfo;
}
logFirebaseEvent("dev_audit", parameters);
// Guru Analytics Event(GAE)
Log.d("[GAE]($code)=>$errorInfo $parameters", tag: "Analytics");
}
void updateUserIdentification(
{String? firebaseAppInstanceId, String? idfa, String? adId, String? gpsAdId}) {
final latestUserIdentification = userIdentificationSubject.value;
bool changed = false;
String? changedFirebaseInstanceId = latestUserIdentification.firebaseAppInstanceId;
String? changedIdfa = latestUserIdentification.idfa;
String? changedAdId = latestUserIdentification.adId;
String? changedGpsAdId = latestUserIdentification.gpsAdId;
if (firebaseAppInstanceId != null &&
latestUserIdentification.firebaseAppInstanceId != firebaseAppInstanceId) {
changedFirebaseInstanceId = firebaseAppInstanceId;
changed = true;
}
if (idfa != null && latestUserIdentification.idfa != idfa) {
changedIdfa = idfa;
changed = true;
}
if (adId != null && latestUserIdentification.adId != adId) {
changedAdId = adId;
changed = true;
}
if (gpsAdId != null && latestUserIdentification.gpsAdId != gpsAdId) {
changedGpsAdId = gpsAdId;
changed = true;
}
if (changed) {
final newUserIdentification = UserIdentification(
firebaseAppInstanceId: changedFirebaseInstanceId ?? '',
idfa: changedIdfa,
adId: changedAdId,
gpsAdId: changedGpsAdId);
userIdentificationSubject.add(newUserIdentification);
Log.d("updateUserIdentification: $newUserIdentification");
}
}
void parseFbEventMapping() {
final fbEventMappingString =
RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.fbEventMapping);
Log.d("parseFbEventMapping first: $fbEventMappingString");
if (fbEventMappingString == null) {
return;
}
Map<String, String> result = {};
final eventEntries = fbEventMappingString.split(";");
for (String eventEntryString in eventEntries) {
final eventEntry = eventEntryString.split(":");
if (eventEntry.length == 2) {
result[eventEntry.first] = eventEntry.last;
}
}
facebookEventMapping.clear();
facebookEventMapping.addAll(result);
Log.d("parseFbEventMapping: $result");
}
void initFbEventMapping() {
RemoteConfigManager.instance.observeConfig().listen((config) {
parseFbEventMapping();
});
parseFbEventMapping();
}
@override
Future<String> getAppInstanceId() async {
if (appInstanceId.isNotEmpty != true) {
appInstanceId = await EventLogger.getAppInstanceId();
RuntimeProperty.instance.setString(PropertyKeys.appInstanceId, appInstanceId);
}
return appInstanceId;
}
void _initEnvProperties() async {
final bundle = await AppProperty.getInstance().loadValuesByTag(PropertyTags.analytics);
final userId = bundle.getString(PropertyKeys.analyticsUserId);
if (userId != null) {
setUserId(userId);
}
final adjustId = bundle.getString(PropertyKeys.analyticsAdjustId);
if (adjustId != null) {
setAdjustId(adjustId);
}
final adId = bundle.getString(PropertyKeys.analyticsAdId);
if (adId != null) {
setAdId(adId);
}
refreshEventStatistic();
String? firebaseId = await getAppInstanceId();
if (firebaseId.isEmpty) {
firebaseId = bundle.getString(PropertyKeys.analyticsFirebaseId);
}
if (firebaseId?.isNotEmpty == true) {
setFirebaseId(firebaseId!);
}
refreshABProperties();
}
void refreshABProperties() {
final abProperties = RemoteConfigManager.instance.getABProperties();
final PropertyBundle propertyBundle = PropertyBundle();
if (abProperties.isNotEmpty) {
for (var entry in abProperties.entries) {
setGuruUserProperty(entry.key, entry.value);
propertyBundle.setString(PropertyKeys.buildABTestProperty(entry.key), entry.value);
Log.d("setGuruUserProperty: ${entry.key} = ${entry.value}");
}
}
AppProperty.getInstance().setProperties(propertyBundle);
}
void _logFirstOpen() async {
int firstInstallTime =
RuntimeProperty.instance.getInt(PropertyKeys.firstInstallTime, defValue: -1);
if (firstInstallTime == -1) {
firstInstallTime = await AppProperty.getInstance()
.getOrCreateInt(PropertyKeys.firstInstallTime, DateTimeUtils.currentTimeInMillis());
}
setUserProperty("first_open_time", firstInstallTime.toString());
}
String getCountryCode() {
if (mockCountryCode != null) {
return mockCountryCode!;
}
final currentLocale = Platform.localeName.split('_');
if (currentLocale.length > 1) {
return currentLocale.last.toLowerCase();
}
return "";
}
void _logLocale() {
if (Platform.localeName.isNotEmpty == true) {
String lanCode = "";
String countryCode = "";
final currentLocale = Platform.localeName.split('_');
if (currentLocale.isNotEmpty) {
setUserProperty("lang_code", currentLocale[0].toLowerCase());
lanCode = currentLocale[0].toLowerCase();
}
if (currentLocale.length > 1) {
setUserProperty("country_code", currentLocale.last.toLowerCase());
countryCode = currentLocale.last.toLowerCase();
}
Log.d("## locale: [$currentLocale]");
if (lanCode.isNotEmpty && countryCode.isNotEmpty) {
// CountryCodes.init(Locale(lanCode, countryCode));
} else {
// CountryCodes.init();
}
} else {
// CountryCodes.init();
}
}
void _logDeviceType() async {
setUserProperty("device_type", DeviceUtils.isTablet() ? "tablet" : "phone");
final deviceId = await AppProperty.getInstance().getDeviceId();
setDeviceId(deviceId);
}
@override
Future setUserProperty(String key, String value) async {
recordEvents("setUserProperty", {key: value});
recordProperty(key, value);
if (release) {
await EventLogger.setUserProperty(key, value);
}
}
static String buildVariantKey(String experimentName) {
return "ab_$experimentName";
}
String getExperimentVariant(String experimentName) {
return abTestExperimentVariant.value[experimentName] ?? "BASELINE";
}
Future<bool> setLocalABTest(ABTestExperiment experiment, {PropertyBundle? bundle}) async {
Log.d("setLocalABTest: $experiment");
String experimentName = experiment.name;
final exp = await AppProperty.getInstance().getExperiment(experimentName, bundle: bundle);
if (exp != null) {
Log.w("Experiment already exists!");
experiment = exp;
}
return await _applyExperiment(experiment);
}
Future removeExperiment(String experimentName) async {
await AppProperty.getInstance().removeExperiment(experimentName);
final data = Map<String, String>.of(abTestExperimentVariant.value);
data.remove(experimentName);
abTestExperimentVariant.addIfChanged(data);
}
Future<bool> _applyExperiment(ABTestExperiment experiment) async {
final experimentName = experiment.name;
if (experiment.isExpired()) {
Log.w("Experiment($experimentName) is expired");
await removeExperiment(experimentName);
return false;
}
if (!experiment.isMatchAudience()) {
Log.i("NOT match audience! $experiment! INTO BASELINE");
return false;
}
String variantName = await AppProperty.getInstance().getExperimentVariant(experimentName);
if (variantName.isEmpty) {
variantName = await AppProperty.getInstance().setExperiment(experiment);
}
await setGuruUserProperty(buildVariantKey(experimentName), variantName);
Log.i("==> Setup Local Experiment($experimentName) variantName: $variantName");
final data = Map<String, String>.of(abTestExperimentVariant.value);
data[experimentName] = variantName;
abTestExperimentVariant.addIfChanged(data);
return true;
}
void setDeviceId(String deviceId) {
Log.d("setDeviceId: $deviceId");
recordEvents("setDeviceId", {"userId": deviceId});
recordProperty("deviceId", deviceId);
if (deviceId.isNotEmpty) {
AppProperty.getInstance().setAnalyticsDeviceId(deviceId);
if (release) {
EventLogger.setUserProperty("device_id", deviceId);
EventLogger.setDeviceId(deviceId);
}
}
}
Future setUserId(String userId) async {
Log.d("setUserId: $userId");
recordEvents("setUserId", {"userId": userId});
recordProperty("userId", userId);
if (userId.isNotEmpty) {
await AppProperty.getInstance().setUserId(userId);
if (release) {
EventLogger.setUserId(userId);
FirebaseCrashlytics.instance.setUserIdentifier(userId);
}
}
}
void setAdjustId(String adjustId) {
Log.d("setAdjustId: $adjustId");
recordEvents("setAdjustId", {"adjustId": adjustId});
recordProperty("adjustId", adjustId);
if (adjustId.isNotEmpty) {
AppProperty.getInstance().setAdjustId(adjustId);
updateUserIdentification(adId: adjustId);
if (release) {
EventLogger.setAdjustId(adjustId);
}
}
}
void setFirebaseId(String firebaseId) {
Log.d("setFirebaseId: $firebaseId");
recordEvents("setFirebaseId", {"firebaseId": firebaseId});
recordProperty("firebaseId", firebaseId);
if (firebaseId.isNotEmpty) {
AppProperty.getInstance().setFirebaseId(firebaseId);
updateUserIdentification(firebaseAppInstanceId: firebaseId);
if (release) {
EventLogger.setFirebaseId(firebaseId);
}
}
}
void setAdId(String adId) {
Log.d("setAdId: $adId");
recordEvents("setAdId", {"adId": adId});
recordProperty("adId", adId);
AppProperty.getInstance().setAdId(adId);
updateUserIdentification(gpsAdId: adId);
if (release) {
EventLogger.setAdId(adId);
}
}
void setIdfa(String idfa) {
Log.d("setIdfa: $idfa");
recordEvents("setIdfa", {"idfa": idfa});
recordProperty("idfa", idfa);
AppProperty.getInstance().setIdfa(idfa);
updateUserIdentification(idfa: idfa);
if (release) {
// idfaadId
EventLogger.setAdId(idfa);
}
}
void logScreen(String screenName) {
recordEvents("logScreen", {"name": screenName});
recordProperty("screen", screenName);
if (release) {
FirebaseCrashlytics.instance.log(screenName);
EventLogger.logScreen(screenName);
}
}
@override
void setScreen(String screenName) {
if (currentScreen != screenName) {
currentScreen = screenName;
logScreen(screenName);
}
}
@override
void logFirebase(String msg) async {
if (release) {
try {
FirebaseCrashlytics.instance.log(msg);
if (EventLogger.dumpLog) {
Log.d("[Firebase]: $msg");
}
} catch (error, stacktrace) {}
} else {
Log.d("[Firebase]: $msg");
}
}
AppEventOptions? getOptions(String eventName) {
return GuruAnalyticsStrategy.instance.getStrategyRule(eventName)?.getAppEventOptions();
}
@override
void logEvent(String eventName, Map<String, dynamic> parameters, {AppEventOptions? options}) {
refreshEventStatistic();
// Firebase Facebook log event
if (release) {
EventLogger.logEvent(eventName, parameters, options: options ?? getOptions(eventName));
_logAdjustEvent(eventName, parameters);
} else {
Log.d("logEvent: $eventName $parameters");
EventLogger.transmit(eventName, parameters);
}
}
@override
void logEventEx(String eventName,
{String? itemCategory,
String? itemName,
double? value,
Map<String, dynamic> parameters = const {},
AppEventOptions? options}) async {
Map<String, dynamic> map = Map<String, dynamic>.from(parameters);
if (itemCategory != null) {
map["item_category"] = itemCategory;
}
if (itemName != null) {
map["item_name"] = itemName;
}
if (value != null) {
map["value"] = value;
}
logEvent(eventName, map, options: options);
}
Future refreshEventStatistic({bool force = false}) async {
if (!GuruApp.instance.appSpec.deployment.enableAnalyticsStatistic) {
return;
}
final now = DateTimeUtils.currentTimeInMillis();
if (force || (now - latestFetchStatisticTs > DateTimeUtils.minuteInMillis * 2)) {
EventLogger.getStatistic().then((statistic) {
Log.d("Event Statistic:$statistic");
if (statistic != GuruStatistic.invalid && guruEventStatistic.addIfChanged(statistic)) {
setUserProperty("lgd", statistic.logged.toString());
setUserProperty("uld", statistic.uploaded.toString());
}
});
latestFetchStatisticTs = now;
}
}
Future<String> zipGuruLogs() {
return EventLogger.zipGuruLogs();
}
Map<String, dynamic> filterOutNulls(Map<String, dynamic> parameters) {
final Map<String, dynamic> filtered = <String, dynamic>{};
parameters.forEach((String key, dynamic value) {
if (value != null) {
filtered[key] = value;
}
});
return filtered;
}
@override
void logException(dynamic exception, {StackTrace? stacktrace}) async {
if (release) {
Log.d("exception! $exception");
FirebaseCrashlytics.instance.log(exception.toString());
FirebaseCrashlytics.instance
.recordError(exception, stacktrace, printDetails: EventLogger.dumpLog);
} else {
Log.w("Occur Error! $exception $stacktrace", stackTrace: stacktrace);
}
}
void logPurchase(double amount,
{String currency = "",
String contentId = "",
String adPlatform = "",
Map<String, dynamic> parameters = const <String, dynamic>{}}) async {
Log.i("logPurchase:$amount, $currency, $contentId, $adPlatform, $parameters");
try {
await EventLogger.logFbPurchase(amount,
currency: currency,
contentId: contentId,
adPlatform: adPlatform,
additionParameters: parameters);
} catch (error, stacktrace) {
Log.w("logFbPurchase error$error, $stacktrace");
}
}
void logEventShare({String? itemCategory, String? itemName}) {
logEvent("share", {
"item_category": itemCategory,
"item_name": itemName,
"content_type": itemCategory,
"item_id": itemName
});
}
void logSpendCredits(String contentId, String contentType, int price,
{required String virtualCurrencyName, required int balance, String scene = ''}) {
final levelName = GuruApp.instance.protocol.getLevelName();
if (release) {
EventLogger.logSpendCredits(contentId, contentType, price,
virtualCurrencyName: virtualCurrencyName, balance: balance, scene: scene);
} else {
final parameters = <String, dynamic>{
"item_name": contentId,
"item_category": contentType,
"virtual_currency_name": virtualCurrencyName,
"value": price,
"balance": balance,
"scene": scene,
"level_name": levelName
};
Log.d("logEvent: spend_virtual_currency $parameters");
EventLogger.transmit("spend_virtual_currency", parameters);
}
AiBi.instance.spendVirtualCurrency(balance, price.toDouble(), contentType);
}
Future<void> logEarnVirtualCurrency(
{required String virtualCurrencyName,
required String method,
required int balance,
required int value,
String? specific,
String? scene}) async {
final levelName = GuruApp.instance.protocol.getLevelName();
logEvent(
"earn_virtual_currency",
filterOutNulls(<String, dynamic>{
"virtual_currency_name": virtualCurrencyName,
"item_category": method,
"item_name": specific,
"value": value,
"balance": balance,
"level_name": levelName,
"scene": scene
}));
AiBi.instance.earnVirtualCurrency(balance, value.toDouble(), method);
}
String? peekUserProperty(String key) {
return Analytics.userProperties[key];
}
Future<void> setGuruUserProperty(String key, String value) async {
recordProperty(key, value);
return await EventLogger.setGuruUserProperty(key, value);
}
Future<void> logGuruEvent(String eventName, Map<String, dynamic> parameters) async {
EventLogger.guruLogEvent(name: eventName, parameters: parameters);
}
Future<void> logFirebaseEvent(String eventName, Map<String, dynamic> parameters) async {
if (release) {
EventLogger.firebaseLogEvent(name: eventName, parameters: parameters);
} else {
Log.d("logEvent: $eventName $parameters");
}
EventLogger.transmit(eventName, parameters);
}
}

View File

@ -0,0 +1,195 @@
part of '../guru_analytics.dart';
/// Created by Haoyi on 2022/3/12
typedef AdjustEventConverter = AdjustEvent Function(Map<String, dynamic>);
class AdjustProfile {
final String appToken;
final Map<String, AdjustEventConverter> eventNameMapping;
final bool isEnabled;
AdjustProfile({required this.appToken, required this.eventNameMapping})
: isEnabled = appToken.isNotEmpty;
static AdjustEvent createAdjustEvent(String eventToken, Map<String, dynamic> params) {
final adjustParams = Map.of(params);
final revenue = adjustParams.remove("revenue");
final currency = adjustParams.remove("currency");
final event = AdjustEvent(eventToken);
if (revenue is num && currency is String) {
event.setRevenue(revenue, currency);
}
for (var entry in adjustParams.entries) {
event.addCallbackParameter(entry.key, entry.value.toString());
}
return event;
}
}
mixin AdjustAware on Analytics {
static final List<AdjustEvent> pendingAdjustEvents = [];
//
static bool initializedAdjust = false;
bool get enabledAdjust => GuruApp.instance.adjustProfile.isEnabled;
static AdjustConfig _defaultAdjustConfigComposition(AdjustConfig adjustConfig) {
return adjustConfig;
}
static AdjustConfig Function(AdjustConfig) adjustConfigComposition =
_defaultAdjustConfigComposition;
static AdjustConfig buildAdjustConfig() {
final AdjustConfig config = AdjustConfig(GuruApp.instance.adjustProfile.appToken,
kReleaseMode ? AdjustEnvironment.production : AdjustEnvironment.sandbox);
config.fbAppId = GuruApp.instance.details.facebookAppId;
return adjustConfigComposition(config);
}
Future initAdjust() async {
if (enabledAdjust) {
await _setupAdjustSessionCall();
final adjustConfig = buildAdjustConfig();
Adjust.start(adjustConfig);
initializedAdjust = true;
_trackAllPendingAdjustEvent();
final adId = await Adjust.getAdid();
if (adId != null) {
GuruAnalytics.instance.setAdjustId(adId);
Log.d("initAdjust adId:$adId");
} else {
// https://github.com/adjust/react_native_sdk/issues/90
Log.d("adjustId is null! waiting 3s..and retry");
Future.delayed(const Duration(seconds: 3), () async {
final adId = await Adjust.getAdid();
if (adId != null) {
GuruAnalytics.instance.setAdjustId(adId);
Log.d("initAdjust adId:$adId");
} else {
Log.d("initAdjust adId is null");
}
});
}
final googleAdId = await Adjust.getGoogleAdId();
if (googleAdId != null) {
GuruAnalytics.instance.setAdId(googleAdId);
Log.d("initAdjust googleAdId:$googleAdId");
}
final idfa = Platform.isIOS ? await Adjust.getIdfa() : null;
if (idfa != null) {
GuruAnalytics.instance.setIdfa(idfa);
Log.d("initAdjust idfa:$idfa");
}
}
}
// AdjusttrackAdRevenueNew
void loadAdjustAdRevenue(ImpressionData impressionData) {
if (enabledAdjust) {
AdjustAdRevenue adRevenue = AdjustAdRevenue(AdjustConfig.AdRevenueSourceAppLovinMAX);
adRevenue.setRevenue(impressionData.publisherRevenue, "USD");
adRevenue.adRevenueNetwork = impressionData.networkName;
adRevenue.adRevenueUnit = impressionData.unitId;
adRevenue.adRevenuePlacement = impressionData.networkPlacementId;
Adjust.trackAdRevenueNew(adRevenue);
recordEvents("[Adjust]trackAdRevenue", adRevenue.toMap);
}
}
//
Future _setupAdjustSessionCall() async {
try {
final deviceId = await AppProperty.getInstance().getDeviceId();
Adjust.addSessionCallbackParameter("device_id", deviceId);
} catch (error, stacktrace) {
Log.e("setupAdjustSessionCall error $error, $stacktrace");
}
final appInstanceId = await getAppInstanceId();
Log.d("setupAdjustSessionCall $appInstanceId");
Adjust.addSessionCallbackParameter("user_pseudo_id", appInstanceId);
}
void logAdjust(String eventName,
{String? itemCategory,
String? itemName,
double? value,
Map<String, dynamic> parameters = const {}}) {
if (enabledAdjust) {
Map<String, dynamic> map = Map<String, dynamic>.from(parameters);
if (itemCategory != null) {
map["item_category"] = itemCategory;
}
if (itemName != null) {
map["item_name"] = itemName;
}
if (value != null) {
map["value"] = value;
}
_logAdjustEvent(eventName, map);
}
}
void _trackAdjustEvent(AdjustEvent adjustEvent) {
if (!enabledAdjust) {
return;
}
if (!initializedAdjust) {
pendingAdjustEvents.add(adjustEvent);
Log.d("adjust not initialized!");
return;
}
if (EventLogger.dumpLog || kDebugMode) {
Log.d("[adjust] ${adjustEvent.toMap}");
}
if (pendingAdjustEvents.isNotEmpty) {
final events = List.of(pendingAdjustEvents);
pendingAdjustEvents.clear();
for (var event in events) {
Adjust.trackEvent(event);
if (EventLogger.dumpLog || kDebugMode) {
Log.d("[adjust] ${event.toMap}");
}
}
}
Adjust.trackEvent(adjustEvent);
}
void _trackAllPendingAdjustEvent() {
if (!enabledAdjust) {
return;
}
final events = List.of(pendingAdjustEvents);
pendingAdjustEvents.clear();
for (var event in events) {
Adjust.trackEvent(event);
if (EventLogger.dumpLog || kDebugMode) {
Log.d("[adjust] ${event.toMap}");
}
}
}
AdjustEventConverter? getAdjustEventConverter(String eventName) {
return GuruAnalyticsStrategy.instance.getAdjustEventConverter(eventName) ??
GuruApp.instance.adjustProfile.eventNameMapping[eventName];
}
//
void _logAdjustEvent(String eventName, Map<String, dynamic> parameters) {
if (!enabledAdjust) {
return;
}
final AdjustEventConverter? adjustEventConverter = getAdjustEventConverter(eventName);
if (adjustEventConverter != null) {
AdjustEvent adjustEvent = adjustEventConverter(parameters);
Log.d("adjustEvent:${adjustEvent.toMap}");
_trackAdjustEvent(adjustEvent);
}
}
}

View File

@ -0,0 +1,90 @@
// /// Created by Haoyi on 2022/2/28
//
// part of "../analytics.dart";
//
//
part of "../guru_analytics.dart";
extension AdsAnalytics on GuruAnalytics {
void logAdRevenue(double adRevenue, String adPlatform, String currency,
{String? orderType, String? orderId, String? productId, int? transactionDate}) {
// logEventEx(name, itemCategory: scene, itemName: adName);
final orderExtras = CollectionUtils.filterOutNulls(<String, dynamic>{
"order_type": orderType,
"order_id": orderId,
"product_id": productId,
"trans_ts": transactionDate
});
if (release) {
EventLogger.logAdRevenue(adRevenue, adPlatform, currency, extras: orderExtras);
} else {
Log.d("[firebase] logAdRevenue ${<String, dynamic>{
"adRevenue": adRevenue,
"adPlatform": adPlatform,
"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
}}");
}
}
void logAdLtv(String phase, double ltv) {
if (release) {
EventLogger.logAdLtv(phase, ltv);
} else {
Log.d("[firebase] logAdLtv ${<String, dynamic>{"phase": phase, "ltv": ltv}}");
}
}
void logAdImpression(String name, String adType,
{String scene = "", String adName = "", Map<String, dynamic> parameters = const {}}) {
logEventEx(name, itemCategory: scene, itemName: adName, parameters: parameters);
if (release) {
EventLogger.logFbAdImpression(adType);
FirebaseCrashlytics.instance
.log("adImp: name($name) scene($scene) adName($adName) adType($adType)");
} else {
Log.d("[facebook] logEvent logFbAdImpression: $adType");
}
}
void logAdImp(ImpressionData data) {
EventLogger.logAdImpression(
adPlatform: data.platform,
adSource: data.networkName,
adFormat: data.unitFormat,
adUnitName: data.unitName,
value: data.publisherRevenue,
currency: data.currency);
}
void logAdClick(String name, String adType, {String scene = "", String adName = ""}) {
logEventEx(name, itemCategory: scene, itemName: adName);
if (release) {
EventLogger.logFbAdClick(adType);
} else {
Log.d("[facebook] logEvent logAdClick: $adType");
}
}
}

View File

@ -0,0 +1,590 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:guru_analytics_flutter/events_constants.dart';
import 'package:guru_analytics_flutter/events_constants.dart';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/analytics/guru_analytics.dart';
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_app/property/settings/guru_settings.dart';
import 'package:guru_app/utils/guru_file_utils_extension.dart';
import 'package:guru_utils/core/ext.dart';
import 'package:guru_utils/file/file_utils.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/property/runtime_property.dart';
import 'package:guru_utils/quiver/cache.dart';
import 'package:guru_utils/quiver/collection.dart';
import 'package:guru_utils/settings/settings.dart';
import 'package:guru_utils/tuple/tuple.dart';
abstract class EventMatcher {
bool match(String eventName);
}
class UniversalMatcher extends EventMatcher {
@override
bool match(String eventName) => true;
@override
String toString() {
return 'UniversalMatcher';
}
}
class RegexMatcher extends EventMatcher {
final RegExp re;
RegexMatcher(String pattern) : re = RegExp(pattern);
@override
bool match(String eventName) => re.hasMatch(eventName);
@override
String toString() {
return 'RegexMatcher:${re.pattern}';
}
}
class WildcardMatcher extends RegexMatcher {
final String wildcard;
WildcardMatcher(this.wildcard) : super("^${wildcard.replaceAll("*", ".*")}\$");
@override
bool match(String eventName) => super.match(eventName);
@override
String toString() {
return 'WildcardMatcher:$wildcard => ${re.pattern}';
}
}
abstract class StrategyValidator {
bool get alwaysVerify => false;
const StrategyValidator();
bool validate();
}
class UnlimitedValidator extends StrategyValidator {
const UnlimitedValidator();
@override
bool validate() => true;
@override
String toString() {
return 'UnlimitedValidator';
}
}
class DisabledValidator extends StrategyValidator {
@override
bool validate() => false;
@override
String toString() {
return 'DisabledValidator';
}
}
class PlatformValidator extends StrategyValidator {
final String platform;
PlatformValidator(this.platform);
@override
bool validate() => Platform.isAndroid ? platform == "android" : platform == "ios";
@override
String toString() {
return 'PlatformValidator($platform)';
}
}
class CountryCodeValidator extends StrategyValidator {
final Set<String> included;
final Set<String> excluded;
CountryCodeValidator(this.included, this.excluded);
@override
bool validate() {
final countryCode = AccountDataStore.instance.countryCode;
// excludedvalidateexcluded
// included
if (excluded.isNotEmpty) {
return !excluded.contains(countryCode);
}
if (included.contains(countryCode)) {
return true;
}
return false;
}
@override
String toString() {
return 'CountryCodeValidator{included: $included, excluded: $excluded}';
}
}
class UserPropertyValidator extends StrategyValidator {
@override
bool get alwaysVerify => true;
final List<Tuple2<String, String>> validProperties;
UserPropertyValidator(this.validProperties);
@override
bool validate() {
for (var tuple in validProperties) {
if (GuruAnalytics.instance.getProperty(tuple.item1) != tuple.item2) {
return false;
}
}
return true;
}
@override
String toString() {
return 'UserPropertyValidator{validProperties: $validProperties}';
}
}
class RandomValidator extends StrategyValidator {
final int percent;
RandomValidator(int percent) : percent = percent.clamp(10, 90);
@override
bool validate() {
final firstInstallTime =
RuntimeProperty.instance.getInt(UtilsSettingsKeys.firstInstallTime, defValue: -1);
return (firstInstallTime % 9) >= (percent ~/ 10 - 1);
}
@override
String toString() {
return 'RandomValidator{percent: $percent}';
}
}
class VersionValidator extends StrategyValidator {
final String opt;
final String buildId;
VersionValidator(this.opt, this.buildId);
@override
bool validate() {
final buildNumber = GuruSettings.instance.buildNumber.get();
switch (opt) {
case "ve":
return buildNumber == buildId;
case "vg":
return buildNumber.compareTo(buildId) > 0;
case "vge":
return buildNumber.compareTo(buildId) >= 0;
case "vl":
return buildNumber.compareTo(buildId) < 0;
case "vle":
return buildNumber.compareTo(buildId) <= 0;
default:
return false;
}
}
@override
String toString() {
return 'VersionValidator{opt: $opt, buildId: $buildId}';
}
}
class StrategyRuleTypeException implements Exception {
final String message;
StrategyRuleTypeException([this.message = "Type mismatch: Expected a StrategyRuleItem."]);
@override
String toString() => "StrategyRuleTypeException: $message";
}
class StrategyRule {
final EventMatcher? matcher;
final StrategyValidator validator;
final AppEventCapabilities appEventCapabilities;
final String? adjustToken;
AppEventOptions? _options;
StrategyRule(this.validator, this.appEventCapabilities, {this.matcher, this.adjustToken});
AppEventOptions? getAppEventOptions() {
if ((_options != null && !validator.alwaysVerify) || validator.validate()) {
return _options ??= AppEventOptions(capabilities: appEventCapabilities);
}
return null;
}
@override
String toString() {
return 'StrategyRule{matcher: $matcher, validator: $validator, appEventCapabilities: $appEventCapabilities, adjustToken: $adjustToken}';
}
}
class StrategyRuleParser {
static final invalidWildcardReg = RegExp(r'[^a-zA-Z=0-9_*]');
static final adjustTokenReg = RegExp(r'^[a-z0-9]{6}$');
static final randomStrategyReg = RegExp(r'^r([1-9]0)$');
static final userPropertyStrategyReg = RegExp(r'^up:(.+)=(.+)$');
static final versionStrategyReg = RegExp(r'^(ve|vg|vl|vge|vle)(\d{8})$');
static final countryStrategyReg = RegExp(r'^cc:(.+)$');
static final countryCodeValidReg = RegExp(r'^[a-z]{2}|\![a-z]{2}$');
final List<String> fields;
StrategyRuleParser(this.fields);
EventMatcher? createEventMatcher(String event) {
if (event == "_all_") {
return UniversalMatcher();
} else if (!invalidWildcardReg.hasMatch(event)) {
if (event.contains("*")) {
return WildcardMatcher(event);
} else {
// matcher
return null;
}
} else {
return RegexMatcher(event);
}
}
StrategyValidator? createStrategyValidator(String strategy) {
if (strategy == "unlimited") {
return const UnlimitedValidator();
} else if (strategy == "disabled") {
return DisabledValidator();
} else if (strategy == "android" || strategy == "ios") {
return PlatformValidator(strategy);
} else {
final randomMatch = randomStrategyReg.firstMatch(strategy);
final randomPercent = randomMatch?.group(1);
if (!DartExt.isBlank(randomPercent)) {
return RandomValidator(int.parse(randomPercent!));
}
final userPropertyMatch = userPropertyStrategyReg.firstMatch(strategy);
final userPropertyKey = userPropertyMatch?.group(1);
final userPropertyValue = userPropertyMatch?.group(2);
if (!DartExt.isBlank(userPropertyKey) && !DartExt.isBlank(userPropertyValue)) {
return UserPropertyValidator([Tuple2(userPropertyKey!, userPropertyValue!)]);
}
final versionMatch = versionStrategyReg.firstMatch(strategy);
final versionOpt = versionMatch?.group(1);
final versionBuildId = versionMatch?.group(2);
if (!DartExt.isBlank(versionOpt) && !DartExt.isBlank(versionBuildId)) {
return VersionValidator(versionOpt!, versionBuildId!);
}
final countryCodeMatch = countryStrategyReg.firstMatch(strategy);
final countryCodeExpression = countryCodeMatch?.group(1);
if (!DartExt.isBlank(countryCodeExpression)) {
final included = <String>{};
final excluded = <String>{};
final countryCodes = countryCodeExpression!
.split("|")
.where((cc) => countryCodeValidReg.hasMatch(cc))
.toSet();
for (var cc in countryCodes) {
if (cc.startsWith("!")) {
excluded.add(cc.substring(1));
} else {
included.add(cc);
}
}
return CountryCodeValidator(included, excluded);
}
}
return null;
}
StrategyRuleItem? fromData(List<String> data) {
if (data.length != fields.length) {
return null;
}
String? event;
EventMatcher? eventMatcher;
StrategyValidator? validator;
int appEventCapabilitiesFlag = 0;
String? adjustToken;
for (int i = 0; i < fields.length; ++i) {
final field = fields[i];
final value = data[i];
if (field == "event") {
event = value;
if (event.isEmpty) {
return null;
}
try {
eventMatcher = createEventMatcher(value);
Log.d("eventMatcher:$eventMatcher");
} catch (error, stacktrace) {
Log.w("createEventMatcher error! $error", stackTrace: stacktrace);
return null;
}
} else if (field == "guru") {
if (value == '1') {
appEventCapabilitiesFlag |= AppEventCapabilities.guru;
}
} else if (field == "firebase") {
if (value == '1') {
appEventCapabilitiesFlag |= AppEventCapabilities.firebase;
}
} else if (field == "facebook") {
if (value == '1') {
appEventCapabilitiesFlag |= AppEventCapabilities.facebook;
}
} else if (field == "adjust") {
if (value == '1') {}
} else if (field == "strategy") {
validator = createStrategyValidator(value);
} else if ((Platform.isAndroid && field == "ata") || (Platform.isIOS && field == "ati")) {
if (value.isNotEmpty && adjustTokenReg.hasMatch(value)) {
adjustToken = value;
}
}
}
if (event != null && validator != null) {
return StrategyRuleItem(
event,
StrategyRule(validator, AppEventCapabilities(appEventCapabilitiesFlag),
matcher: eventMatcher, adjustToken: adjustToken));
}
return null;
}
}
class StrategyRuleItem extends Comparable {
final String eventName;
final StrategyRule rule;
StrategyRuleItem(this.eventName, this.rule);
@override
int compareTo(other) {
if (other is StrategyRuleItem) {
return eventName.compareTo(other.eventName);
}
throw StrategyRuleTypeException();
}
}
class GuruAnalyticsStrategy {
static const String tag = "GuruAnalyticsStrategy";
final List<StrategyRule> priorityRules = [];
final SplayTreeMap<String, StrategyRule> explicitRules = SplayTreeMap();
final Map<String, AdjustEventConverter> iosAdjustEventConverters = {};
final Map<String, AdjustEventConverter> androidAdjustEventConverts = {};
bool loaded = false;
final LinkedLruHashMap<String, StrategyRule> eventRules = LinkedLruHashMap(maximumSize: 128);
GuruAnalyticsStrategy._();
static final GuruAnalyticsStrategy instance = GuruAnalyticsStrategy._();
void reset() {
priorityRules.clear();
explicitRules.clear();
}
static const guruAnalyticsStrategyExtension = ".gas"; // Guru Analytics Strategy
Future<File?> checkAndCreateLocalStrategyFile() async {
final currentLocalStrategy =
"${GuruSettings.instance.buildNumber.get()}$guruAnalyticsStrategyExtension";
final file = await FileUtils.instance.getGuruConfigFile("analytics", currentLocalStrategy);
if (!file.existsSync()) {
try {
final data = await rootBundle.loadString("assets/guru/analytics_strategy.csv");
file.writeAsStringSync(data);
Log.i("load local strategy success! [$currentLocalStrategy]", tag: tag);
return file;
} catch (error, stacktrace) {
Log.w("not config local strategy!", tag: tag);
}
}
return null;
}
Future load() async {
try {
final analyticsConfig = RemoteConfigManager.instance.getAnalyticsConfig();
if (!GuruApp.instance.appSpec.deployment.enabledGuruAnalyticsStrategy ||
!analyticsConfig.enabledStrategy) {
Log.w("analytics strategy disabled!", tag: tag);
return;
}
final String remoteAnalyticsStrategy = analyticsConfig.strategy;
final latestAnalyticsStrategy = await AppProperty.getInstance().getLatestAnalyticsStrategy();
if (remoteAnalyticsStrategy != latestAnalyticsStrategy) {
loaded = false;
}
if (loaded) {
Log.w("already loaded! ignore!", tag: tag);
return;
}
File? file;
// remoteAnalyticsStrategystrategy
if (!DartExt.isBlank(remoteAnalyticsStrategy)) {
file = await FileUtils.instance.getGuruConfigFile("analytics", remoteAnalyticsStrategy);
if (!file.existsSync()) {
try {
await FileUtils.instance.downloadFile(
"${GuruApp.instance.details.storagePrefix}/guru%2Fanalytics%2F$remoteAnalyticsStrategy?alt=media",
file);
Log.i("download analytics strategy[$remoteAnalyticsStrategy] success", tag: tag);
} catch (error, stacktrace) {
Log.w("downloadFile error! $error try to fallback", tag: tag);
// 使strategy
// strategystrategy
// SDKstrategyFirebase Storagestrategy
// strategy
// 使strategy
}
}
if (remoteAnalyticsStrategy != latestAnalyticsStrategy) {
AppProperty.getInstance().setLatestAnalyticsStrategy(remoteAnalyticsStrategy);
final latestStrategyFile =
await FileUtils.instance.getGuruConfigFile("analytics", latestAnalyticsStrategy);
if (latestStrategyFile.existsSync()) {
FileUtils.instance.deleteFile(latestStrategyFile);
}
}
}
// strategy
// 使strategy
if (file?.existsSync() != true) {
file = await checkAndCreateLocalStrategyFile();
if (file?.existsSync() != true) {
return;
}
}
final Stream<String> strategyTextStream = file!.openRead().transform(utf8.decoder);
StrategyRule? newDefaultRule;
final List<StrategyRule> newPriorityRules = [];
final Map<String, StrategyRule> newExplicitRules = {};
StrategyRuleParser? parser;
int lineNum = 0;
await for (var line in strategyTextStream.transform(const LineSplitter())) {
final list = line.split(",");
Log.d("[${lineNum++}] $list", tag: tag);
if (parser == null) {
parser = StrategyRuleParser(list);
} else {
final ruleItem = parser.fromData(list);
if (ruleItem == null) {
continue;
}
if (ruleItem.eventName == "_all_") {
newDefaultRule = ruleItem.rule;
} else if (ruleItem.rule.matcher != null) {
newPriorityRules.add(ruleItem.rule);
} else {
newExplicitRules[ruleItem.eventName] = ruleItem.rule;
}
if (ruleItem.rule.adjustToken != null) {
if (Platform.isAndroid) {
androidAdjustEventConverts[ruleItem.eventName] =
(_) => AdjustEvent(ruleItem.rule.adjustToken!);
} else if (Platform.isIOS) {
iosAdjustEventConverters[ruleItem.eventName] =
(_) => AdjustEvent(ruleItem.rule.adjustToken!);
}
}
}
}
reset();
priorityRules.addAll(newPriorityRules.reversed);
if (newDefaultRule != null) {
priorityRules.add(newDefaultRule);
}
explicitRules.addAll(newExplicitRules);
loaded = true;
Log.d(
"analytics strategy loaded! ${eventRules.length} ${explicitRules.length} ${priorityRules.length}",
tag: tag);
} catch (error, stacktrace) {}
}
StrategyRule? getStrategyRule(String eventName) {
Log.d(
"[$loaded]getStrategyRule:$eventName ${eventRules.length} ${explicitRules.length} ${priorityRules.length}");
if (!loaded) {
return null;
}
final rule = eventRules[eventName];
if (rule != null) {
return rule;
}
final explicitRule = explicitRules[eventName];
if (explicitRule != null) {
return explicitRule;
}
for (var rule in priorityRules) {
Log.d("matcher: ${rule.matcher} eventName: $eventName ${rule.matcher?.match(eventName)}",
tag: tag);
if (rule.matcher?.match(eventName) == true) {
return rule;
}
}
// strategy
return null;
}
AdjustEventConverter? getAdjustEventConverter(String eventName) {
if (Platform.isAndroid) {
return androidAdjustEventConverts[eventName];
} else if (Platform.isIOS) {
return iosAdjustEventConverters[eventName];
}
return null;
}
void testRule(String eventName) {
final rule = getStrategyRule(eventName);
if (rule?.matcher?.match(eventName) != false) {
Log.d("testMatch: $eventName => $rule success!", tag: tag);
} else {
Log.d("testMatch: $eventName => $rule error!", tag: tag);
}
}
}

View File

@ -0,0 +1,46 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:guru_utils/log/log.dart';
import 'package:http_parser/http_parser.dart';
/// Created by Haoyi on 2021-06-3.
///
class CustomTransformer extends DefaultTransformer {
CustomTransformer() : super(jsonDecodeCallback: _parseJson);
@override
Future<String> transformRequest(RequestOptions options) async {
var data = options.data ?? '';
if (data is! String) {
if (_isJsonMime(options.contentType)) {
return await _encodeToJson(options.data);
} else if (data is Map<String, dynamic>) {
return Transformer.urlEncodeMap(data);
}
}
return data.toString();
}
bool _isJsonMime(String? contentType) {
if (contentType == null) return false;
return MediaType.parse(contentType).mimeType.toLowerCase() == Headers.jsonMimeType.mimeType;
}
Future<String> _encodeToJson(dynamic data) async {
return await compute(jsonEncode, data);
}
}
// Must be top-level function
_parseAndDecode(String response) {
return jsonDecode(response);
}
_parseJson(String text) {
return compute(_parseAndDecode, text);
}
platformLogPrint(Object object) {
Log.v("[NETWORK] " + object.toString());
}

View File

@ -0,0 +1,145 @@
import 'dart:io';
import 'package:guru_app/analytics/data/analytics_model.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:json_annotation/json_annotation.dart';
part 'orders_model.g.dart';
/// Created by Haoyi on 2022/7/27
@JsonSerializable()
class OrderUserInfo {
@JsonKey(name: 'level', defaultValue: "0")
final String level;
OrderUserInfo(this.level);
factory OrderUserInfo.fromJson(Map<String, dynamic> json) => _$OrderUserInfoFromJson(json);
Map<String, dynamic> toJson() => _$OrderUserInfoToJson(this);
}
class OrderType {
static const inapp = 0;
static const subs = 1;
}
@JsonSerializable()
class OrdersReport {
// android
@JsonKey(name: 'orderType', defaultValue: 0)
int? orderType;
@JsonKey(name: 'packageName')
String? packageName;
@JsonKey(name: 'productId')
String? productId;
@JsonKey(name: 'subscriptionId')
String? subscriptionId;
@JsonKey(name: 'token')
String? token;
@JsonKey(name: 'offerId')
String? offerId;
@JsonKey(name: 'basePlanId')
String? basePlanId;
// ios
@JsonKey(name: 'bundleId')
String? bundleId;
@JsonKey(name: 'receipt')
String? receipt;
@JsonKey(name: 'sku')
String? sku;
@JsonKey(name: 'country')
String? countryCode;
// general
@JsonKey(name: 'price')
String? price;
@JsonKey(name: 'currency')
String? currency;
@JsonKey(name: 'userInfo')
OrderUserInfo? orderUserInfo;
@JsonKey(name: "eventConfig")
UserIdentification? userIdentification;
@JsonKey(name: "orderId")
String? orderId;
@JsonKey(name: "transactionDate")
int? transactionDate;
OrdersReport(
{this.orderType,
this.token,
this.packageName,
this.productId,
this.subscriptionId,
this.bundleId,
this.receipt,
this.price,
this.currency,
this.sku,
this.countryCode,
this.orderUserInfo,
this.userIdentification,
this.offerId,
this.basePlanId,
this.orderId,
this.transactionDate});
@override
String toString() {
final StringBuffer sb = StringBuffer();
sb.writeln("[OrdersReport]");
sb.writeln(" productId: $productId");
sb.writeln(" price: $price");
sb.writeln(" currency: $currency");
sb.writeln(" userIdentification: $userIdentification");
sb.writeln(" orderId: $orderId");
if (Platform.isAndroid) {
sb.writeln(" orderType: $orderType");
sb.writeln(" packageName: $packageName");
sb.writeln(" subscriptionId: $subscriptionId");
sb.writeln(" token: $token");
sb.writeln(" offerId: $offerId");
sb.writeln(" basePlanId: $basePlanId");
} else if (Platform.isIOS) {
sb.writeln(" bundleId: $bundleId");
sb.writeln(" receipt: $receipt");
sb.writeln(" sku: $sku");
sb.writeln(" countryCode: $countryCode");
}
return sb
.toString(); //'OrdersReport{orderType: $orderType, packageName: $packageName, productId: $productId, subscriptionId: $subscriptionId, token: $token, bundleId: $bundleId, receipt: $receipt, price: $price, currency: $currency, introductoryPrice: $introductoryPrice}';
}
factory OrdersReport.fromJson(Map<String, dynamic> json) => _$OrdersReportFromJson(json);
Map<String, dynamic> toJson() => _$OrdersReportToJson(this);
}
@JsonSerializable()
class OrdersResponse {
@JsonKey(name: 'usdPrice', defaultValue: 0.0)
final double usdPrice;
@JsonKey(name: 'test', defaultValue: false)
final bool test;
bool get isTestOrder => test;
OrdersResponse(this.usdPrice, this.test);
factory OrdersResponse.fromJson(Map<String, dynamic> json) => _$OrdersResponseFromJson(json);
Map<String, dynamic> toJson() => _$OrdersResponseToJson(this);
@override
String toString() {
return 'OrdersResponse{usdPrice:$usdPrice, test:$test}';
}
}

View File

@ -0,0 +1,75 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'orders_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
OrderUserInfo _$OrderUserInfoFromJson(Map<String, dynamic> json) =>
OrderUserInfo(
json['level'] as String? ?? '0',
);
Map<String, dynamic> _$OrderUserInfoToJson(OrderUserInfo instance) =>
<String, dynamic>{
'level': instance.level,
};
OrdersReport _$OrdersReportFromJson(Map<String, dynamic> json) => OrdersReport(
orderType: json['orderType'] as int? ?? 0,
token: json['token'] as String?,
packageName: json['packageName'] as String?,
productId: json['productId'] as String?,
subscriptionId: json['subscriptionId'] as String?,
bundleId: json['bundleId'] as String?,
receipt: json['receipt'] as String?,
price: json['price'] as String?,
currency: json['currency'] as String?,
sku: json['sku'] as String?,
countryCode: json['country'] as String?,
orderUserInfo: json['userInfo'] == null
? null
: OrderUserInfo.fromJson(json['userInfo'] as Map<String, dynamic>),
userIdentification: json['eventConfig'] == null
? null
: UserIdentification.fromJson(
json['eventConfig'] as Map<String, dynamic>),
offerId: json['offerId'] as String?,
basePlanId: json['basePlanId'] as String?,
orderId: json['orderId'] as String?,
transactionDate: json['transactionDate'] as int?,
);
Map<String, dynamic> _$OrdersReportToJson(OrdersReport instance) =>
<String, dynamic>{
'orderType': instance.orderType,
'packageName': instance.packageName,
'productId': instance.productId,
'subscriptionId': instance.subscriptionId,
'token': instance.token,
'offerId': instance.offerId,
'basePlanId': instance.basePlanId,
'bundleId': instance.bundleId,
'receipt': instance.receipt,
'sku': instance.sku,
'country': instance.countryCode,
'price': instance.price,
'currency': instance.currency,
'userInfo': instance.orderUserInfo,
'eventConfig': instance.userIdentification,
'orderId': instance.orderId,
'transactionDate': instance.transactionDate,
};
OrdersResponse _$OrdersResponseFromJson(Map<String, dynamic> json) =>
OrdersResponse(
(json['usdPrice'] as num?)?.toDouble() ?? 0.0,
json['test'] as bool? ?? false,
);
Map<String, dynamic> _$OrdersResponseToJson(OrdersResponse instance) =>
<String, dynamic>{
'usdPrice': instance.usdPrice,
'test': instance.test,
};

View File

@ -0,0 +1,177 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/account/model/user.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_utils/auth/auth_credential_manager.dart';
import 'package:guru_utils/device/device_info.dart';
import 'package:guru_utils/device/device_utils.dart';
import 'package:retrofit/retrofit.dart';
import 'custom_transformer.dart';
import 'data/orders/orders_model.dart';
/// Created by Haoyi on 6/3/21
part 'modules/guru_api_extension.dart';
part 'guru_api.g.dart';
abstract class DioBuilder {
Dio build();
}
class GuruDioBuilder extends DioBuilder {
final AccountDataStore accountDataStore;
GuruDioBuilder() : accountDataStore = AccountDataStore.instance;
Options toOptions(RequestOptions ro) {
return Options(
method: ro.method,
sendTimeout: ro.sendTimeout,
receiveTimeout: ro.receiveTimeout,
extra: ro.extra,
headers: ro.headers,
responseType: ro.responseType,
contentType: ro.contentType,
validateStatus: ro.validateStatus,
receiveDataWhenStatusError: ro.receiveDataWhenStatusError,
followRedirects: ro.followRedirects,
maxRedirects: ro.maxRedirects,
requestEncoder: ro.requestEncoder,
responseDecoder: ro.responseDecoder,
listFormat: ro.listFormat);
}
@override
Dio build() {
Dio dio = Dio()
..transformer = CustomTransformer()
..options.connectTimeout = Duration(milliseconds: GuruApp.instance.appSpec.deployment.apiConnectTimeout)
..options.receiveTimeout = Duration(milliseconds: GuruApp.instance.appSpec.deployment.apiReceiveTimeout);
dio.interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options, RequestInterceptorHandler handler) async {
DeviceInfo? deviceInfo = accountDataStore.currentDevice;
final deviceId = await AppProperty.getInstance().getDeviceId();
deviceInfo ??=
await DeviceUtils.buildDeviceInfo(deviceId: deviceId, firebasePushToken: "", uid: "");
final token = accountDataStore.saasToken;
options.headers
.addAll({"X-APP-ID": GuruApp.instance.details.saasAppId, "X-ACCESS-TOKEN": token ?? ''});
options.headers.addAll({"X-DEVICE-INFO": Uri.encodeFull(deviceInfo.toXDeviceInfo())});
handler.next(options);
}, onResponse: (Response response, ResponseInterceptorHandler handler) {
// Log.v("### onResponse ${response.data}");
response.data = response.data["data"] ?? response.data;
handler.next(response);
}, onError: (DioError err, ErrorInterceptorHandler handler) async {
final token = accountDataStore.saasToken;
final response = err.response;
Log.v("### onError ${err.toString()}");
if (response != null && token != null && response.statusCode == 401) {
// dio.lock();
try {
Log.v("accountDataStore.refreshAuth()");
await accountDataStore.refreshAuth(); //token
} catch (e) {
// Log.v("[NETWORK]: RefreshToken Failed.");
handler.reject(err);
} finally {
// dio.unlock();
}
final options = err.requestOptions.copyWith();
options.headers["X-ACCESS-TOKEN"] = accountDataStore.saasToken;
try {
final response = await dio.request(options.path,
data: options.data,
queryParameters: options.queryParameters,
cancelToken: options.cancelToken,
onReceiveProgress: options.onReceiveProgress,
options: toOptions(options));
handler.resolve(response);
} catch (error, stacktrace) {
Log.v("re-request error:$error $stacktrace");
handler.reject(err);
}
} else {
handler.reject(err);
}
}));
dio.interceptors
.add(LogInterceptor(requestBody: true, responseBody: true, logPrint: platformLogPrint));
return dio;
}
}
@RestApi()
abstract class GuruApiMethods {
factory GuruApiMethods(Dio dio, {String baseUrl}) = _GuruApiMethods;
static GuruApiMethods create(GuruDioBuilder dioBuilder, String baseUrl) {
return GuruApiMethods(dioBuilder.build(), baseUrl: baseUrl);
}
// Device, token
@POST("/device/api/v1/devices")
Future reportDevice(@Body() DeviceInfo body);
// Auth
@POST("/auth/api/v1/tokens/provider/secret")
Future<GuruUser> signInWithAnonymous(@Body() AnonymousLoginReqBody body);
@POST("/auth/api/v1/tokens/provider/facebook-gaming")
Future<GuruUser> signInWithFacebook(@Body() FacebookLoginReqBody body);
@POST("/auth/api/v1/tokens/provider/google")
Future<GuruUser> signInWithGoogle(@Body() GoogleLoginReqBody body);
@POST("/auth/api/v1/tokens/provider/apple")
Future<GuruUser> signInWithApple(@Body() AppleLoginReqBody body);
@POST("/auth/api/v1/bindings/provider/facebook-gaming")
Future<GuruUser> associateWithFacebook(@Body() FacebookLoginReqBody body);
@POST("/auth/api/v1/bindings/provider/google")
Future<GuruUser> associateWithGoogle(@Body() GoogleLoginReqBody body);
@POST("/auth/api/v1/bindings/provider/apple")
Future<GuruUser> associateWithApple(@Body() AppleLoginReqBody body);
@POST("/auth/api/v1/renewals/token")
Future<GuruUser> refreshSaasToken();
@POST("/auth/api/v1/renewals/firebase")
Future<FirebaseTokenData> renewFirebaseToken();
@POST("/order/api/v1/orders/ios")
Future<OrdersResponse> iOSOrdersReport(@Body() OrdersReport body);
@POST("/order/api/v1/orders/android")
Future<OrdersResponse> androidOrdersReport(@Body() OrdersReport body);
}
class GuruApi {
static const String _saasApiDevHost = "https://dev.saas.castbox.fm";
static const String _saasApiReleaseHost = "https://saas.castbox.fm";
static bool useReleaseApi = kReleaseMode;
static final GuruApi _releaseApi =
GuruApi._(GuruApiMethods.create(GuruDioBuilder(), _saasApiReleaseHost));
static final GuruApi _debugApi =
GuruApi._(GuruApiMethods.create(GuruDioBuilder(), _saasApiDevHost));
final GuruApiMethods _methods;
static GuruApi get instance => useReleaseApi ? _releaseApi : _debugApi;
static String get saasApiHost => useReleaseApi ? _saasApiReleaseHost : _saasApiDevHost;
GuruApiMethods get methods => _methods;
GuruApi._(this._methods);
}

View File

@ -0,0 +1,383 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'guru_api.dart';
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers
class _GuruApiMethods implements GuruApiMethods {
_GuruApiMethods(
this._dio, {
this.baseUrl,
});
final Dio _dio;
String? baseUrl;
@override
Future<dynamic> reportDevice(DeviceInfo 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(_setStreamType<dynamic>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/device/api/v1/devices',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = _result.data;
return value;
}
@override
Future<GuruUser> signInWithAnonymous(AnonymousLoginReqBody body) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(body.toJson());
final _result =
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/tokens/provider/secret',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = GuruUser.fromJson(_result.data!);
return value;
}
@override
Future<GuruUser> signInWithFacebook(FacebookLoginReqBody body) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(body.toJson());
final _result =
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/tokens/provider/facebook-gaming',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = GuruUser.fromJson(_result.data!);
return value;
}
@override
Future<GuruUser> signInWithGoogle(GoogleLoginReqBody body) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(body.toJson());
final _result =
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/tokens/provider/google',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = GuruUser.fromJson(_result.data!);
return value;
}
@override
Future<GuruUser> signInWithApple(AppleLoginReqBody body) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(body.toJson());
final _result =
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/tokens/provider/apple',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = GuruUser.fromJson(_result.data!);
return value;
}
@override
Future<GuruUser> associateWithFacebook(FacebookLoginReqBody body) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(body.toJson());
final _result =
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/bindings/provider/facebook-gaming',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = GuruUser.fromJson(_result.data!);
return value;
}
@override
Future<GuruUser> associateWithGoogle(GoogleLoginReqBody body) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(body.toJson());
final _result =
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/bindings/provider/google',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = GuruUser.fromJson(_result.data!);
return value;
}
@override
Future<GuruUser> associateWithApple(AppleLoginReqBody body) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(body.toJson());
final _result =
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/bindings/provider/apple',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = GuruUser.fromJson(_result.data!);
return value;
}
@override
Future<GuruUser> refreshSaasToken() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final Map<String, dynamic>? _data = null;
final _result =
await _dio.fetch<Map<String, dynamic>>(_setStreamType<GuruUser>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/renewals/token',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = GuruUser.fromJson(_result.data!);
return value;
}
@override
Future<FirebaseTokenData> renewFirebaseToken() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final Map<String, dynamic>? _data = null;
final _result = await _dio
.fetch<Map<String, dynamic>>(_setStreamType<FirebaseTokenData>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/auth/api/v1/renewals/firebase',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = FirebaseTokenData.fromJson(_result.data!);
return value;
}
@override
Future<OrdersResponse> iOSOrdersReport(OrdersReport 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<OrdersResponse>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/order/api/v1/orders/ios',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = OrdersResponse.fromJson(_result.data!);
return value;
}
@override
Future<OrdersResponse> androidOrdersReport(OrdersReport 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<OrdersResponse>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/order/api/v1/orders/android',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = OrdersResponse.fromJson(_result.data!);
return value;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
String _combineBaseUrls(
String dioBaseUrl,
String? baseUrl,
) {
if (baseUrl == null || baseUrl.trim().isEmpty) {
return dioBaseUrl;
}
final url = Uri.parse(baseUrl);
if (url.isAbsolute) {
return url.toString();
}
return Uri.parse(dioBaseUrl).resolveUri(url).toString();
}
}

View File

@ -0,0 +1,53 @@
/// Created by Haoyi on 6/4/21
///
part of "../guru_api.dart";
extension GuruApiExtension on GuruApi {
// Future<GuruUser> signInWithAnonymous({required String secret}) async {
// return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: secret));
// }
Future<GuruUser> loginGuruWithCredential({required Credential credential}) async {
switch (credential.authType) {
case AuthType.facebook:
return await methods
.signInWithFacebook(FacebookLoginReqBody(accessToken: credential.token));
case AuthType.google:
return await methods.signInWithGoogle(GoogleLoginReqBody(idToken: credential.token));
case AuthType.apple:
return await methods.signInWithApple(AppleLoginReqBody(token: credential.token));
case AuthType.anonymous:
return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: credential.token));
}
}
Future<GuruUser> associateCredential({required Credential credential}) async {
switch (credential.authType) {
case AuthType.facebook:
return await methods
.associateWithFacebook(FacebookLoginReqBody(accessToken: credential.token));
case AuthType.google:
return await methods.associateWithGoogle(GoogleLoginReqBody(idToken: credential.token));
case AuthType.apple:
return await methods.associateWithApple(AppleLoginReqBody(token: credential.token));
case AuthType.anonymous:
return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: credential.token));
}
}
Future reportDevice(DeviceInfo deviceInfo) async {
return await methods.reportDevice(deviceInfo);
}
Future<FirebaseTokenData> renewFirebaseToken() async {
return await methods.renewFirebaseToken();
}
Future<OrdersResponse> reportOrders(OrdersReport body) async {
if (Platform.isAndroid) {
return await methods.androidOrdersReport(body);
} else {
return await methods.iOSOrdersReport(body);
}
}
}

View File

@ -0,0 +1,212 @@
import 'dart:io';
import 'package:guru_app/firebase/messaging/remote_messaging_manager.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:json_annotation/json_annotation.dart';
/// Created by Haoyi on 2022/8/29
part 'app_models.g.dart';
@JsonSerializable()
class AppDetails {
@JsonKey(name: "saas_app_id")
final String saasAppId;
@JsonKey(name: "authority")
final String authority;
@JsonKey(name: "storage_prefix")
final String storagePrefix;
@JsonKey(name: "default_cdn_prefix")
final String defaultCdnPrefix;
@JsonKey(name: "android_gp_url")
final String androidGooglePlayUrl;
@JsonKey(name: "ios_spp_store_url")
final String iosAppStoreUrl;
@JsonKey(name: "policy_url")
final String policyUrl;
@JsonKey(name: "terms_url")
final String termsUrl;
@JsonKey(name: "email_url")
final String emailUrl;
@JsonKey(name: "package_name")
final String packageName;
@JsonKey(name: "bundle_id")
final String bundleId;
@JsonKey(name: "facebook_app_id")
final String facebookAppId;
String get appId => Platform.isAndroid ? packageName : bundleId;
AppDetails(
{required this.saasAppId,
required this.authority,
required this.storagePrefix,
required this.defaultCdnPrefix,
required this.androidGooglePlayUrl,
this.iosAppStoreUrl = '',
required this.policyUrl,
required this.termsUrl,
required this.emailUrl,
required this.packageName,
required this.bundleId,
required this.facebookAppId});
factory AppDetails.fromJson(Map<String, dynamic> json) => _$AppDetailsFromJson(json);
Map<String, dynamic> toJson() => _$AppDetailsToJson(this);
}
@JsonSerializable()
class Deployment {
static const int defaultIgcBalanceSecret = 2654404609;
static const int defaultLogFileSizeLimit = 1024 * 1024 * 10;
static const int defaultLogFileCount = 7;
static const int defaultPersistentLogLevel = 2;
static const int defaultApiTimeout = 15000; // 15s
static const int defaultIosSandboxSubsRenewalSpeed = 2;
static const int defaultTrackingNotificationPermissionPassLimitTimes = 10;
static const int defaultSubscriptionRestoreGraceCount = 3;
static const int defaultFullscreenMinInterval = 60;
static const int defaultSubscriptionGraceDays = DateTimeUtils.dayInMillis;
@JsonKey(name: "property_cache_size", defaultValue: 256)
final int propertyCacheSize;
@JsonKey(name: "enable_dithering", defaultValue: true)
final bool enableDithering;
@JsonKey(name: "disable_rewards_ads", defaultValue: false)
final bool disableRewardsAds;
@JsonKey(name: "enable_analytics_statistic", defaultValue: true)
final bool enableAnalyticsStatistic;
@JsonKey(name: "auto_restore_iap", defaultValue: true)
final bool autoRestoreIap;
@JsonKey(name: "init_igc", defaultValue: 500)
final int initIgc;
@JsonKey(name: "igc_balance_secret", defaultValue: defaultIgcBalanceSecret)
final int igcBalanceSecret;
@JsonKey(name: "sync_account_profile", defaultValue: true)
final bool syncAccountProfile;
@JsonKey(name: "auto_request_notification_permission", defaultValue: false)
final bool autoRequestNotificationPermission;
@JsonKey(name: "log_file_size_limit", defaultValue: defaultLogFileSizeLimit)
final int logFileSizeLimit;
@JsonKey(name: "log_file_count", defaultValue: defaultLogFileCount)
final int logFileCount;
@JsonKey(name: "persistent_log_level", defaultValue: defaultPersistentLogLevel)
final int persistentLogLevel;
@JsonKey(name: "ios_validate_receipt_password")
final String? iosValidateReceiptPassword;
@JsonKey(name: "conversion_events", defaultValue: {})
final Set<String> conversionEvents;
@JsonKey(name: "api_connect_timeout", defaultValue: defaultApiTimeout)
final int apiConnectTimeout;
@JsonKey(name: "api_receive_timeout", defaultValue: defaultApiTimeout)
final int apiReceiveTimeout;
@JsonKey(name: "ios_sandbox_subs_renewal_speed", defaultValue: defaultIosSandboxSubsRenewalSpeed)
final int iosSandboxSubsRenewalSpeed;
@JsonKey(name: "ads_compliant_initialization", defaultValue: false)
final bool adsCompliantInitialization;
@JsonKey(name: "notification_permission_prompt_trigger", defaultValue: PromptTrigger.rationale)
final PromptTrigger notificationPermissionPromptTrigger;
@JsonKey(name: "tracking_notification_permission_pass", defaultValue: false)
final bool trackingNotificationPermissionPass;
@JsonKey(
name: "tracking_notification_permission_pass_limit_times",
defaultValue: defaultTrackingNotificationPermissionPassLimitTimes)
final int trackingNotificationPermissionPassLimitTimes;
@JsonKey(name: "enabled_guru_analytics_strategy", defaultValue: false)
final bool enabledGuruAnalyticsStrategy;
@JsonKey(name: "allow_interstitial_as_alternative_reward", defaultValue: false)
final bool allowInterstitialAsAlternativeReward;
@JsonKey(name: "show_internal_ads_when_banner_unavailable", defaultValue: false)
final bool showInternalAdsWhenBannerUnavailable;
@JsonKey(name: "subscription_restore_grace_count", defaultValue: defaultSubscriptionRestoreGraceCount)
final int subscriptionRestoreGraceCount;
@JsonKey(name: "fullscreen_ads_min_interval", defaultValue: defaultFullscreenMinInterval)
final int fullscreenAdsMinInterval;
@JsonKey(name: "subscription_grace_period", defaultValue: defaultSubscriptionGraceDays)
final int subscriptionGraceDays;
@JsonKey(name: "enabled_sync_account_profile", defaultValue: false)
final bool enabledSyncAccountProfile;
@JsonKey(name: "purchase_event_trigger", defaultValue: 1)
final int purchaseEventTrigger;
Deployment(
{this.propertyCacheSize = 256,
this.enableDithering = true,
this.disableRewardsAds = false,
this.enableAnalyticsStatistic = true,
this.autoRestoreIap = false,
this.initIgc = 0,
this.igcBalanceSecret = defaultIgcBalanceSecret,
this.syncAccountProfile = true,
this.autoRequestNotificationPermission = false,
this.logFileSizeLimit = defaultLogFileSizeLimit,
this.logFileCount = defaultLogFileCount,
this.persistentLogLevel = defaultPersistentLogLevel,
this.iosValidateReceiptPassword,
this.conversionEvents = const {},
this.apiConnectTimeout = defaultApiTimeout,
this.apiReceiveTimeout = defaultApiTimeout,
this.iosSandboxSubsRenewalSpeed = defaultIosSandboxSubsRenewalSpeed,
this.adsCompliantInitialization = false,
this.notificationPermissionPromptTrigger = PromptTrigger.rationale,
this.trackingNotificationPermissionPass = false,
this.trackingNotificationPermissionPassLimitTimes =
defaultTrackingNotificationPermissionPassLimitTimes,
this.enabledGuruAnalyticsStrategy = false,
this.allowInterstitialAsAlternativeReward = 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);
Map<String, dynamic> toJson() => _$DeploymentToJson(this);
}
@JsonSerializable()
class RemoteDeployment {
@JsonKey(name: "keep_screen_on_duration_m", defaultValue: 0)
final int keepScreenOnDuration;
@JsonKey(name: "subscriptionGraceDays")
final int? subscriptionGraceDays;
RemoteDeployment(
{this.keepScreenOnDuration = 0, this.subscriptionGraceDays});
factory RemoteDeployment.fromJson(Map<String, dynamic> json) => _$RemoteDeploymentFromJson(json);
Map<String, dynamic> toJson() => _$RemoteDeploymentToJson(this);
}

View File

@ -0,0 +1,146 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_models.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AppDetails _$AppDetailsFromJson(Map<String, dynamic> json) => AppDetails(
saasAppId: json['saas_app_id'] as String,
authority: json['authority'] as String,
storagePrefix: json['storage_prefix'] as String,
defaultCdnPrefix: json['default_cdn_prefix'] as String,
androidGooglePlayUrl: json['android_gp_url'] as String,
iosAppStoreUrl: json['ios_spp_store_url'] as String? ?? '',
policyUrl: json['policy_url'] as String,
termsUrl: json['terms_url'] as String,
emailUrl: json['email_url'] as String,
packageName: json['package_name'] as String,
bundleId: json['bundle_id'] as String,
facebookAppId: json['facebook_app_id'] as String,
);
Map<String, dynamic> _$AppDetailsToJson(AppDetails instance) =>
<String, dynamic>{
'saas_app_id': instance.saasAppId,
'authority': instance.authority,
'storage_prefix': instance.storagePrefix,
'default_cdn_prefix': instance.defaultCdnPrefix,
'android_gp_url': instance.androidGooglePlayUrl,
'ios_spp_store_url': instance.iosAppStoreUrl,
'policy_url': instance.policyUrl,
'terms_url': instance.termsUrl,
'email_url': instance.emailUrl,
'package_name': instance.packageName,
'bundle_id': instance.bundleId,
'facebook_app_id': instance.facebookAppId,
};
Deployment _$DeploymentFromJson(Map<String, dynamic> json) => Deployment(
propertyCacheSize: json['property_cache_size'] as int? ?? 256,
enableDithering: json['enable_dithering'] as bool? ?? true,
disableRewardsAds: json['disable_rewards_ads'] as bool? ?? false,
enableAnalyticsStatistic:
json['enable_analytics_statistic'] as bool? ?? true,
autoRestoreIap: json['auto_restore_iap'] as bool? ?? true,
initIgc: json['init_igc'] as int? ?? 500,
igcBalanceSecret: json['igc_balance_secret'] as int? ?? 2654404609,
syncAccountProfile: json['sync_account_profile'] as bool? ?? true,
autoRequestNotificationPermission:
json['auto_request_notification_permission'] as bool? ?? false,
logFileSizeLimit: json['log_file_size_limit'] as int? ?? 10485760,
logFileCount: json['log_file_count'] as int? ?? 7,
persistentLogLevel: json['persistent_log_level'] as int? ?? 2,
iosValidateReceiptPassword:
json['ios_validate_receipt_password'] as String?,
conversionEvents: (json['conversion_events'] as List<dynamic>?)
?.map((e) => e as String)
.toSet() ??
{},
apiConnectTimeout: json['api_connect_timeout'] as int? ?? 15000,
apiReceiveTimeout: json['api_receive_timeout'] as int? ?? 15000,
iosSandboxSubsRenewalSpeed:
json['ios_sandbox_subs_renewal_speed'] as int? ?? 2,
adsCompliantInitialization:
json['ads_compliant_initialization'] as bool? ?? false,
notificationPermissionPromptTrigger: $enumDecodeNullable(
_$PromptTriggerEnumMap,
json['notification_permission_prompt_trigger']) ??
PromptTrigger.rationale,
trackingNotificationPermissionPass:
json['tracking_notification_permission_pass'] as bool? ?? false,
trackingNotificationPermissionPassLimitTimes:
json['tracking_notification_permission_pass_limit_times'] as int? ??
10,
enabledGuruAnalyticsStrategy:
json['enabled_guru_analytics_strategy'] as bool? ?? false,
allowInterstitialAsAlternativeReward:
json['allow_interstitial_as_alternative_reward'] as bool? ?? false,
showInternalAdsWhenBannerUnavailable:
json['show_internal_ads_when_banner_unavailable'] as bool? ?? false,
subscriptionRestoreGraceCount:
json['subscription_restore_grace_count'] as int? ?? 3,
fullscreenAdsMinInterval:
json['fullscreen_ads_min_interval'] as int? ?? 60,
subscriptionGraceDays:
json['subscription_grace_period'] as int? ?? 86400000,
enabledSyncAccountProfile:
json['enabled_sync_account_profile'] as bool? ?? false,
);
Map<String, dynamic> _$DeploymentToJson(Deployment instance) =>
<String, dynamic>{
'property_cache_size': instance.propertyCacheSize,
'enable_dithering': instance.enableDithering,
'disable_rewards_ads': instance.disableRewardsAds,
'enable_analytics_statistic': instance.enableAnalyticsStatistic,
'auto_restore_iap': instance.autoRestoreIap,
'init_igc': instance.initIgc,
'igc_balance_secret': instance.igcBalanceSecret,
'sync_account_profile': instance.syncAccountProfile,
'auto_request_notification_permission':
instance.autoRequestNotificationPermission,
'log_file_size_limit': instance.logFileSizeLimit,
'log_file_count': instance.logFileCount,
'persistent_log_level': instance.persistentLogLevel,
'ios_validate_receipt_password': instance.iosValidateReceiptPassword,
'conversion_events': instance.conversionEvents.toList(),
'api_connect_timeout': instance.apiConnectTimeout,
'api_receive_timeout': instance.apiReceiveTimeout,
'ios_sandbox_subs_renewal_speed': instance.iosSandboxSubsRenewalSpeed,
'ads_compliant_initialization': instance.adsCompliantInitialization,
'notification_permission_prompt_trigger':
_$PromptTriggerEnumMap[instance.notificationPermissionPromptTrigger]!,
'tracking_notification_permission_pass':
instance.trackingNotificationPermissionPass,
'tracking_notification_permission_pass_limit_times':
instance.trackingNotificationPermissionPassLimitTimes,
'enabled_guru_analytics_strategy': instance.enabledGuruAnalyticsStrategy,
'allow_interstitial_as_alternative_reward':
instance.allowInterstitialAsAlternativeReward,
'show_internal_ads_when_banner_unavailable':
instance.showInternalAdsWhenBannerUnavailable,
'subscription_restore_grace_count':
instance.subscriptionRestoreGraceCount,
'fullscreen_ads_min_interval': instance.fullscreenAdsMinInterval,
'subscription_grace_period': instance.subscriptionGraceDays,
'enabled_sync_account_profile': instance.enabledSyncAccountProfile,
};
const _$PromptTriggerEnumMap = {
PromptTrigger.rationale: 0,
PromptTrigger.request: 1,
};
RemoteDeployment _$RemoteDeploymentFromJson(Map<String, dynamic> json) =>
RemoteDeployment(
keepScreenOnDuration: json['keep_screen_on_duration_m'] as int? ?? 0,
subscriptionGraceDays: json['subscriptionGraceDays'] as int?,
);
Map<String, dynamic> _$RemoteDeploymentToJson(RemoteDeployment instance) =>
<String, dynamic>{
'keep_screen_on_duration_m': instance.keepScreenOnDuration,
'subscriptionGraceDays': instance.subscriptionGraceDays,
};

View File

@ -0,0 +1,92 @@
//
//
// import 'package:guru_app/account/account_data_store.dart';
// import 'package:guru_app/account/model/account_profile.dart';
// import 'package:guru_app/account/model/user.dart';
// import 'package:guru_utils/controller/controller.dart';
//
// /// Created by Haoyi on 2022/5/23
//
// mixin AccountAware on LifecycleController {
// AccountDataStore get accountDataStore => AccountDataStore.instance;
//
// Stream<AccountProfile?> get observableAccountProfile => accountDataStore.observableAccountProfile;
//
// String? get saasToken => accountDataStore.saasToken;
//
// String? get uid => accountDataStore.uid;
//
// AccountProfile? get accountProfile => accountDataStore.accountProfile;
//
// String? get nickname => accountDataStore.nickname;
//
// String? get countryCode => accountDataStore.countryCode;
//
// SaasUser? get user => accountDataStore.user;
//
// DeviceInfo? get currentDevice => accountDataStore.currentDevice;
//
// String? get userAvatar => accountProfile?.avatar;
//
// CumulativeInt? get bestScore => accountProfile?.bestScore;
//
// bool get accountInitialized => accountDataStore.initialized;
//
// Stream<String?> get observableNickname =>
// observableAccountProfile.map<String?>((accountProfile) => accountProfile?.nickname);
//
// Stream<bool> get observableAccountInitialized => accountDataStore.observableInitialized;
//
// void initAccount() {
// Injector.provide<AccountService>().init();
// }
//
// Future<bool> updateAccountProfile(
// {String? nickname, String? avatar, CumulativeInt? bestScore, String? countryCode}) async {
// final accountService = Injector.provide<AccountService>();
// // final rankService = Injector.provide<RankService>();
// return await accountService.modifyProfile(
// nickname: nickname, avatar: avatar, bestScore: bestScore, countryCode: countryCode);
// // if (result) {
// // await rankService.refreshAccountProfile();
// // }
// return true;
// }
//
// // Future<bool> uploadBestScore() async {
// // final accountService = Injector.provide<AccountService>();
// // final rankService = Injector.provide<RankService>();
// // final latestBestScore = StatisticManager.instance.peekBestScore();
// // final accountBestScore = bestScore ?? CumulativeInt.zero;
// //
// // Log.w("uploadBestScore latestBestScore! $latestBestScore $accountBestScore",
// // syncFirebase: true);
// // if (latestBestScore > accountBestScore) {
// // String? changedCountryCode = DeviceUtils.buildLocaleInfo().countryCode.toUpperCase();
// // if (DartExt.isBlank(changedCountryCode) || countryCode == changedCountryCode) {
// // changedCountryCode = null;
// // }
// // try {
// // await accountService.modifyProfile(
// // bestScore: latestBestScore, countryCode: changedCountryCode);
// // } catch (error, stacktrace) {
// // Log.w("modifyProfile error!", error: error, stackTrace: stacktrace, syncFirebase: true);
// // }
// // }
// // await rankService.uploadBestScore(latestBestScore);
// // return true;
// // }
// //
// // RankData buildEmptyRankData(String boardId, {CumulativeInt? bestScore}) {
// // return RankData(
// // boardId,
// // uid ?? "",
// // -1,
// // bestScore ?? this.bestScore,
// // LbUserInfo(
// // nickname: nickname ?? "",
// // countryCode: DeviceUtils.buildLocaleInfo().countryCode.toUpperCase(),
// // avatar: "avatar_1",
// // attr: UserAttr.real));
// // }
// }

View File

@ -0,0 +1,143 @@
import 'package:guru_app/financial/asset/assets_model.dart';
import 'package:guru_app/financial/asset/assets_store.dart';
import 'package:guru_app/financial/financial_manager.dart';
import 'package:guru_app/financial/iap/iap_manager.dart';
import 'package:guru_app/financial/iap/iap_model.dart';
import 'package:guru_app/financial/igb/igb_manager.dart';
import 'package:guru_app/financial/igb/igb_product.dart';
import 'package:guru_app/financial/igc/igc_manager.dart';
import 'package:guru_app/financial/igc/igc_model.dart';
import 'package:guru_app/financial/product/product_store.dart';
import 'package:guru_app/financial/reward/reward_manager.dart';
import 'package:guru_app/financial/reward/reward_model.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/inventory/db/inventory_database.dart';
import 'package:guru_app/inventory/inventory_manager.dart';
import 'package:guru_app/test/test_guru_app_creator.dart';
import 'package:guru_utils/collection/collectionutils.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/controller/controller.dart';
/// Created by Haoyi on 2022/4/7
mixin AssetsAware on LifecycleController {
final BehaviorSubject<ProductStore<IapProduct>> _productStoreSubject =
BehaviorSubject.seeded(ProductStore());
ProductStore<IapProduct> get currentProductStore => _productStoreSubject.value;
AssetsStore<Asset> get currentIapAssetStore => IapManager.instance.purchasedStore;
AssetsStore<Asset> get currentRewardedStore => RewardManager.instance.rewardedStore;
Stream<ProductStore<IapProduct>> get observableProductStore => _productStoreSubject.stream;
Stream<AssetsStore<Asset>> get observableIapPurchased => IapManager.instance.observableAssetStore;
Stream<AssetsStore<Asset>> get observableRewarded => RewardManager.instance.observableAssetStore;
Stream<AssetsStore<Asset>> get observableAssets => FinancialManager.instance.observableAssets;
int _latestRefreshIapProductTimestamp = 0;
bool get isIapCanceled => IapManager.instance.latestIapCause == IapCause.canceled;
bool get isIapError => IapManager.instance.latestIapCause == IapCause.error;
int get currentIgcBalance => IgcManager.instance.currentBalance;
Stream<int> get observableIgcBalance => IgcManager.instance.observableCurrentBalance;
Stream<Map<String, InventoryItem>> get observableInventoryItems =>
InventoryManager.instance.observableData;
Future restorePurchases() async {
return await IapManager.instance.restorePurchases();
}
Future clearIapAssets() async {
return await IapManager.instance.clearAssetRecord();
}
void observeIapProducts(Set<TransactionIntent> intents) {
addSubscription(IapManager.instance.observableProductDetails.listen((details) async {
final productStore = await IapManager.instance.buildProducts(intents);
_productStoreSubject.addEx(productStore);
}));
}
void refreshIapProducts() async {
final now = DateTimeUtils.currentTimeInMillis();
if (now - _latestRefreshIapProductTimestamp > DateTimeUtils.minuteInMillis) {
IapManager.instance.refreshProducts();
_latestRefreshIapProductTimestamp = now;
} else {
Log.w("refreshIapProducts Too Frequency!", tag: "IAP");
}
}
Future<RewardProduct> buildRewardProduct(TransactionIntent 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 {
if (product is IapProduct) {
return await IapManager.instance.buy(product);
} else if (product is IgcProduct) {
return await IgcManager.instance.purchase(product);
} else if (product is RewardProduct) {
return await RewardManager.instance.claim(product);
} else if (product is IgbProduct) {
return await IgbManager.instance.redeem(product);
} else {
return false;
}
}
}

View File

@ -0,0 +1,61 @@
import 'dart:math';
import 'package:get/get.dart' hide Rx;
import 'dart:ui' as ui show Image;
/// Created by Haoyi on 2022/7/16
///
///
// abstract class GemsController extends AdsController
// with AssetsAware, InterstitialAware, RewardedAware, VisualFeastAware {
// Future loadGemsResource(VisualFeastEngine engine) async {
// final imageFutures = [
// Flame.images.load("ic_gem.png"),
// Flame.images.load("ic_gem_add.png"),
// ];
// final loadedResources = await Future.wait([
// Future.wait(imageFutures),
// // Future.wait(lottieFutures)
// ]);
// final images = loadedResources[0] as List<ui.Image>;
// addSprite("gem", VisualFeastSprite.fromImage(images[0]));
// addSprite("gemAdd", VisualFeastSprite.fromImage(images[1]));
// }
//
// void startClaim(int gems, String method, {bool useBg = true, VoidCallback? onCompleted}) async {
// final engine = createEngine(onCompleted: onCompleted);
// await loadGemsResource(engine);
//
// final designSpec = GemsRewardsDesignSpec.get();
// final gemsBarSpec = designSpec.buildGemBarSpec();
// final gemsBar = GemsBar(
// gemBarSpec: gemsBarSpec,
// gemSprite: getSprite("gem"),
// gemAddSprite: getSprite("gemAdd"),
// assetsAware: this);
// final size = designSpec.measuredSize / 2;
// final gemsReward = GemsReward(
// Rect.fromCenter(
// center: Offset(size.width, size.height + gemsBarSpec.gemRect.width * 2),
// width: gemsBarSpec.gemRect.width,
// height: gemsBarSpec.gemRect.width), onFirstGemComplete: () {
// claimGems(gems, method);
// });
// final background = Background(gemsBarSpec);
// final gemsHeight = gemsBarSpec.gemRect.width;
// final gemsText = GemsText(gems, Offset(size.width, size.height),
// Offset(size.width, size.height - gemsHeight * 2));
// engine.attachRenders(
// ListUtils.filterOutNulls([useBg ? background : null, gemsBar, gemsReward, gemsText]));
//
// dispatch(engine);
// }
//
// Future claimGems(int gems, String method) async {
// onClaimed(gems, method);
// }
//
// void onClaimed(int gems, String method) {
// }
// }

View File

@ -0,0 +1,16 @@
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/inventory/db/inventory_database.dart';
import 'package:guru_utils/database/database.dart';
import 'package:guru_utils/property/storage/db/property_database.dart';
/// Created by Haoyi on 2023/1/13
final List<TableCreator> _creatorV1 = [PropertyEntity.createTable];
final List<TableCreator> _creatorV2 = [OrderEntity.createTable];
final List<TableCreator> _creatorV4 = [InventoryTable.createTable];
class Creators {
static final List<TableCreator> creators = [..._creatorV1, ..._creatorV2, ..._creatorV4];
}

View File

@ -0,0 +1,29 @@
import 'package:guru_app/database/creators/creators.dart';
import 'package:guru_utils/database/database.dart';
import 'package:guru_utils/property/storage/property_storage.dart';
import 'package:guru_utils/property/storage/db/property_database.dart';
import "migrations/migrations.dart";
/// Created by Haoyi on 2022/9/7
abstract class _GuruDB extends AppDatabase with PropertyStorage {}
class GuruDB extends _GuruDB with PropertyDatabase {
static final GuruDB instance = GuruDB._();
GuruDB._() {
setDatabase(this);
}
@override
String get dbName => "guru";
@override
List<Migration> get migrations => Migrations.migrations;
@override
List<TableCreator> get tableCreators => Creators.creators;
@override
int get version => 4;
}

View File

@ -0,0 +1,13 @@
/// Created by Haoyi on 2020/5/22
///
part of "migrations.dart";
class _MigrationV1toV2 implements Migration {
@override
Future<MigrateResult> migrate(Transaction transaction) async {
await OrderEntity.createTable(transaction);
return MigrateResult.success;
}
}
final migration1to2 = _MigrationV1toV2();

View File

@ -0,0 +1,22 @@
/// Created by Haoyi on 2023/2/16
part of "migrations.dart";
class _MigrationV2toV3 implements Migration {
@override
Future<MigrateResult> migrate(Transaction transaction) async {
// IF NOT EXISTStry catch
try {
await transaction
.execute(
"ALTER TABLE ${OrderEntity.tbName} ADD ${OrderEntity.dbCategory} TEXT DEFAULT ''");
await transaction.execute(
"CREATE INDEX trans_category_idx ON ${OrderEntity.tbName} (${OrderEntity.dbCategory});");
} catch(error, stacktrace) {
Log.w("ignore alter cmd!");
}
return MigrateResult.success;
}
}
final migration2to3 = _MigrationV2toV3();

View File

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

View File

@ -0,0 +1,15 @@
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/inventory/db/inventory_database.dart';
import 'package:guru_utils/database/database.dart';
import 'package:guru_utils/log/log.dart';
part "migration_v1_to_v2.dart";
part 'migration_v2_to_v3.dart';
part 'migration_v3_to_v4.dart';
/// Created by @Haoyi on 2020/5/22
///
class Migrations {
static final migrations = [migration1to2, migration2to3, migration3to4];
}

View File

@ -0,0 +1,11 @@
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/financial/product/product_model.dart';
/// Created by Haoyi on 6/1/21
class Asset {
final ProductId productId;
final OrderEntity order;
Asset(this.productId, this.order);
}

View File

@ -0,0 +1,70 @@
import 'package:guru_app/financial/asset/assets_model.dart';
import 'package:guru_app/financial/product/product_model.dart';
/// Created by Haoyi on 6/1/21
///
class AssetsStore<T extends Asset> {
final bool isActive;
final Map<ProductId, T> data = <ProductId, T>{};
AssetsStore.inactive() : isActive = false;
AssetsStore() : isActive = true;
Map<String, String> toStringMap() {
final result = <String, String>{};
int index = 0;
for (var entry in data.entries) {
result["${index++}"] = entry.toString();
}
return result;
}
void forEach(void Function(ProductId productId, T asset) callback) {
for (var element in data.entries) {
callback.call(element.key, element.value);
}
}
void removeWhere(bool Function(ProductId productId, T asset) callback) {
data.removeWhere((key, value) => callback.call(key, value));
}
void addAsset(T asset) {
data[asset.productId] = asset;
}
void clearAsset({String? category, TransactionMethod? method}) {
data.removeWhere((key, value) =>
(category == null || value.order.category == category) &&
(method == null || value.order.method == method.index));
}
void addAllAssets(List<T> assets) {
for (var asset in assets) {
addAsset(asset);
}
}
T? getAsset(ProductId? productId) {
return productId?.isValid() == true ? data[productId] : null;
}
bool isOwned(ProductId productId) {
return data.containsKey(productId);
}
bool existsAssets(Iterable<ProductId> productIds) {
for (var productId in productIds) {
if (isOwned(productId)) {
return true;
}
}
return false;
}
AssetsStore<T> clone() {
return AssetsStore()..data.addAll(data);
}
}

View File

@ -0,0 +1,391 @@
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_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/id/id_utils.dart';
import 'package:guru_utils/log/log.dart';
import 'package:sqflite/sqflite.dart';
import 'package:json_annotation/json_annotation.dart';
import '../../manifest/manifest.dart';
/// Created by Haoyi on 2022/9/5
part 'order_database.g.dart';
@JsonSerializable()
class AssetEntity {
static const tbName = "assets"; // Product Transaction Table
static const dbSku = "sku";
static const dbState = "state";
static const dbAttribute = "attr"; //
static const dbMethod = "method"; // (IAP)
static const dbCurrency = "currency"; // ()
static const dbCost = "cost"; //
static const dbTimestamp = "ts";
static const dbManifest = "manifest"; //
}
@JsonSerializable()
class OrderEntity {
static const tbName = "orders"; // Product Transaction Table
static const dbOrderId = "oid";
static const dbSku = "sku";
static const dbState = "state";
static const dbErrorInfo = "err_info";
static const dbAttribute = "attr"; //
static const dbMethod = "method"; // (IAP)
static const dbCurrency = "currency"; // ()
static const dbCategory = "category";
static const dbCost = "cost"; //
static const dbTimestamp = "ts";
static const dbManifest = "manifest"; //
@JsonKey(name: dbOrderId)
final String orderId;
@JsonKey(name: dbSku)
final String sku;
@JsonKey(name: dbState)
final int state;
@JsonKey(name: dbAttribute)
final int attr;
@JsonKey(name: dbMethod)
final int method;
@JsonKey(name: dbErrorInfo, defaultValue: "")
final String errorInfo;
@JsonKey(name: dbCurrency, defaultValue: "")
final String currency;
@JsonKey(name: dbCost, defaultValue: 0.0)
final double cost;
@JsonKey(name: dbTimestamp, defaultValue: 0)
final int timestamp;
@JsonKey(name: dbCategory, defaultValue: "")
final String category;
@JsonKey(name: dbManifest)
@manifestStringConvert
final Manifest? manifest;
@JsonKey(ignore: true)
ProductId? _productId;
OrderEntity(
{required this.orderId,
required this.sku,
required this.state,
required this.attr,
required this.method,
required this.currency,
required this.cost,
required this.timestamp,
required this.category,
required this.manifest,
this.errorInfo = ""});
OrderEntity success() {
return OrderEntity(
orderId: orderId,
sku: sku,
state: TransactionState.success,
attr: attr,
method: method,
currency: currency,
cost: cost,
category: category,
timestamp: DateTimeUtils.currentTimeInMillis(),
manifest: manifest);
}
OrderEntity error(String errorInfo) {
return OrderEntity(
orderId: orderId,
sku: sku,
state: TransactionState.error,
attr: attr,
method: method,
currency: currency,
cost: cost,
category: category,
timestamp: DateTimeUtils.currentTimeInMillis(),
manifest: manifest,
errorInfo: errorInfo);
}
OrderEntity expired() {
return OrderEntity(
orderId: orderId,
sku: sku,
state: TransactionState.expired,
attr: attr,
method: method,
currency: currency,
cost: cost,
category: category,
timestamp: DateTimeUtils.currentTimeInMillis(),
manifest: manifest,
errorInfo: "Subscription Expired");
}
bool get isConsumable => attr == TransactionAttributes.consumable;
bool get isAsset => attr == TransactionAttributes.asset;
bool get isSubscription => attr == TransactionAttributes.subscriptions;
bool get isSuccess => state == TransactionState.success;
static Future createTable(Transaction delegate) async {
// v1
const v1Fields = "$dbOrderId TEXT PRIMARY KEY,"
"$dbSku TEXT NOT NULL,"
"$dbState INTEGER NOT NULL,"
"$dbAttribute INTEGER NOT NULL,"
"$dbMethod INTEGER NOT NULL,"
"$dbErrorInfo TEXT DEFAULT '',"
"$dbCurrency TEXT NOT NULL,"
"$dbCost REAL DEFAULT 0.0,"
"$dbTimestamp INTEGER NOT NULL,"
"$dbManifest TEXT DEFAULT '',";
const v2Fields = "$dbCategory TEXT DEFAULT ''";
const cmd = "CREATE TABLE $tbName ("
"$v1Fields"
"$v2Fields"
");";
Log.v("#### cmd: $cmd");
await delegate.execute(cmd);
await delegate.execute("CREATE INDEX trans_sku_idx ON $tbName ($dbSku);");
await delegate.execute("CREATE INDEX trans_category_idx ON $tbName ($dbCategory);");
}
@override
String toString() {
return 'OrderEntity{tid: $dbOrderId, sku: $sku, state: $state, attr: $attr, method: $method, errorInfo: $errorInfo, currency: $currency, cost: $cost, timestamp: $timestamp, manifest: $manifest}';
}
factory OrderEntity.fromMap(Map<String, dynamic> json) => _$OrderEntityFromJson(json);
Map<String, dynamic> toMap() => _$OrderEntityToJson(this);
Map<String, dynamic> toUpdateMap() => toMap()..remove(dbOrderId);
ProductId get productId {
_productId ??= GuruApp.instance.defineProductId(sku, attr, TransactionMethod.values[method]);
return _productId!;
}
}
extension OrderDatabase on GuruDB {
Future<Map<String, String>> loadAllOrders() async {
final db = getDb();
final result = await db.rawQuery("SELECT * FROM ${OrderEntity.tbName}");
if (result.isNotEmpty) {
return {for (var map in result) map[OrderEntity.dbOrderId] as String: map.toString()};
}
return <String, String>{};
}
Future<List<OrderEntity>> selectOrders(
{required TransactionMethod method,
required List<int> attrs,
int state = TransactionState.success}) async {
final db = getDb();
final List<String> conditions = [
"${OrderEntity.dbMethod} = ${method.index}",
"${OrderEntity.dbState} = $state"
];
if (attrs.isNotEmpty) {
conditions.add("${OrderEntity.dbAttribute} IN (${attrs.map((attr) => '"$attr"').join(",")})");
}
final result =
await db.rawQuery("SELECT * FROM ${OrderEntity.tbName} WHERE ${conditions.join(" AND ")}");
if (result.isNotEmpty) {
return result.map((map) => OrderEntity.fromMap(map)).toList();
}
return [];
}
Future<List<OrderEntity>> getCompleteOrders(ProductId productId) async {
final db = getDb();
String where =
"${OrderEntity.dbSku} = '${productId.sku}' AND ${OrderEntity.dbState} = ${TransactionState.success}";
final result = await db.rawQuery("SELECT * FROM ${OrderEntity.tbName} WHERE $where");
if (result.isNotEmpty) {
return result.map((map) => OrderEntity.fromMap(map)).toList();
}
return [];
}
Future<List<OrderEntity>> getPendingOrders(ProductId productId,
{TransactionMethod method = TransactionMethod.iap}) async {
final db = getDb();
String where =
"${OrderEntity.dbSku} = '${productId.sku}' AND ${OrderEntity.dbMethod} = ${method.index} AND ${OrderEntity.dbState} = ${TransactionState.pending}";
final result = await db.rawQuery(
"SELECT * FROM ${OrderEntity.tbName} WHERE $where ORDER BY ${OrderEntity.dbTimestamp} DESC");
if (result.isNotEmpty) {
return result.map((map) => OrderEntity.fromMap(map)).toList();
}
return [];
}
Future<Map<String, Object>> completePendingOrders(Set<ProductId> productIds,
{TransactionMethod method = TransactionMethod.iap}) async {
final db = getDb();
final batch = db.batch();
final updateValues = <String, Object>{
OrderEntity.dbState: TransactionState.success,
OrderEntity.dbTimestamp: DateTimeUtils.currentTimeInMillis()
};
for (var productId in productIds) {
final String where =
"${OrderEntity.dbSku} = '${productId.sku}' AND ${OrderEntity.dbMethod} = ${method.index} AND ${OrderEntity.dbState} = ${TransactionState.pending}";
if (productId.isConsumable) {
batch.delete(
OrderEntity.tbName,
where: where,
);
} else {
batch.update(
OrderEntity.tbName,
updateValues,
where: where,
);
}
}
await batch.commit();
return updateValues;
}
Future<bool> removePendingOrders(Set<ProductId> productIds,
{TransactionMethod method = TransactionMethod.iap}) async {
final db = getDb();
final batch = db.batch();
for (var productId in productIds) {
final String where =
"${OrderEntity.dbSku} = '${productId.sku}' AND ${OrderEntity.dbMethod} = ${method.index} AND ${OrderEntity.dbState} = ${TransactionState.pending}";
batch.delete(
OrderEntity.tbName,
where: where,
);
}
batch.commit();
return true;
}
Future<bool> upsertOrder({required OrderEntity order}) {
return runInTransaction((txn) async {
final upsertMap = order.toMap();
if (!order.isConsumable) {
final result = await txn.rawQuery(
"SELECT * FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbSku} = '${order.sku}'");
if (result.isNotEmpty) {
upsertMap[OrderEntity.dbOrderId] = result.first[OrderEntity.dbOrderId] ?? order.orderId;
}
}
await txn.insert(OrderEntity.tbName, upsertMap, conflictAlgorithm: ConflictAlgorithm.replace);
return true;
});
}
Future<bool> replaceOrderBySku({required OrderEntity order}) {
return runInTransaction((txn) async {
final replaceMap = order.toMap();
if (!order.isConsumable) {
await txn.rawDelete(
"DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbSku} = '${order.sku}'");
}
await txn.insert(OrderEntity.tbName, replaceMap,
conflictAlgorithm: ConflictAlgorithm.replace);
return true;
});
}
Future<bool> completeOrder({required OrderEntity order}) async {
final db = getDb();
if (order.isConsumable) {
await db.rawDelete(
"DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbOrderId} = '${order.orderId}'");
} else {
await db.insert(OrderEntity.tbName, order.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
}
return true;
}
Future<bool> upsertOrders(List<OrderEntity> orders) {
final possessiveOrders = orders.where((order) => !order.isConsumable).toList();
final skus = possessiveOrders.map((order) => "'${order.sku}'").toList();
return runInTransaction((txn) async {
await txn.rawDelete(
"DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbSku} IN (${skus.join(",")})");
for (var upsertOrder in possessiveOrders) {
final upsertOrderJson = upsertOrder.toMap();
await txn.insert(OrderEntity.tbName, upsertOrderJson,
conflictAlgorithm: ConflictAlgorithm.replace);
}
return true;
});
}
Future<bool> deleteOrder({required OrderEntity order}) async {
final result = await getDb().rawDelete(
"DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbOrderId} = '${order.orderId}'");
return result > 0;
}
Future<bool> deleteOrdersBySkus(Set<String> skus) async {
final result = await getDb().rawDelete(
"DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbSku} IN (${skus.map((sku) => "'$sku'").join(",")})");
return result > 0;
}
Future<bool> deletePendingOrderBySku({required String sku}) async {
return runInTransaction((txn) async {
String where =
"${OrderEntity.dbSku} = '$sku' AND ${OrderEntity.dbState} = ${TransactionState.pending}";
final result = await txn.rawQuery(
"SELECT * FROM ${OrderEntity.tbName} WHERE $where ORDER BY ${OrderEntity.dbTimestamp} DESC");
if (result.isNotEmpty) {
final order = OrderEntity.fromMap(result[0]);
await txn.rawDelete(
"DELETE FROM ${OrderEntity.tbName} WHERE ${OrderEntity.dbOrderId} = '${order.orderId}'");
return true;
}
return false;
});
}
Future clearTransaction() async {
final db = getDb();
db.delete(OrderEntity.tbName);
}
Future<bool> clearOrders({String? category, TransactionMethod? method}) async {
final db = getDb();
final whereList = [];
if (category != null) {
whereList.add("${OrderEntity.dbCategory} = '$category'");
}
if (method != null) {
whereList.add("${OrderEntity.dbMethod} = ${method.index}");
}
final whereCondition = whereList.join(" AND ");
Log.d("clearOrders: $whereCondition");
db.rawDelete("DELETE FROM ${OrderEntity.tbName} WHERE $whereCondition");
return true;
}
}

View File

@ -0,0 +1,55 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_database.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AssetEntity _$AssetEntityFromJson(Map<String, dynamic> json) => AssetEntity();
Map<String, dynamic> _$AssetEntityToJson(AssetEntity instance) =>
<String, dynamic>{};
OrderEntity _$OrderEntityFromJson(Map<String, dynamic> json) => OrderEntity(
orderId: json['oid'] as String,
sku: json['sku'] as String,
state: json['state'] as int,
attr: json['attr'] as int,
method: json['method'] as int,
currency: json['currency'] as String? ?? '',
cost: (json['cost'] as num?)?.toDouble() ?? 0.0,
timestamp: json['ts'] as int? ?? 0,
category: json['category'] as String? ?? '',
manifest: _$JsonConverterFromJson<String, Manifest>(
json['manifest'], manifestStringConvert.fromJson),
errorInfo: json['err_info'] as String? ?? '',
);
Map<String, dynamic> _$OrderEntityToJson(OrderEntity instance) =>
<String, dynamic>{
'oid': instance.orderId,
'sku': instance.sku,
'state': instance.state,
'attr': instance.attr,
'method': instance.method,
'err_info': instance.errorInfo,
'currency': instance.currency,
'cost': instance.cost,
'ts': instance.timestamp,
'category': instance.category,
'manifest': _$JsonConverterToJson<String, Manifest>(
instance.manifest, manifestStringConvert.toJson),
};
Value? _$JsonConverterFromJson<Json, Value>(
Object? json,
Value? Function(Json json) fromJson,
) =>
json == null ? null : fromJson(json as Json);
Json? _$JsonConverterToJson<Json, Value>(
Value? value,
Json? Function(Value value) toJson,
) =>
value == null ? null : toJson(value);

View File

@ -0,0 +1,94 @@
import 'dart:io';
import 'package:json_annotation/json_annotation.dart';
part 'orders_model.g.dart';
/// Created by Haoyi on 2022/7/27
@JsonSerializable()
class OrdersReport {
// android
@JsonKey(name: 'orderType', defaultValue: 0)
int? orderType;
@JsonKey(name: 'packageName')
String? packageName;
@JsonKey(name: 'productId')
String? productId;
@JsonKey(name: 'subscriptionId')
String? subscriptionId;
@JsonKey(name: 'token')
String? token;
// ios
@JsonKey(name: 'bundleId')
String? bundleId;
@JsonKey(name: 'receipt')
String? receipt;
@JsonKey(name: 'sku')
String? sku;
@JsonKey(name: 'country')
String? countryCode;
// general
@JsonKey(name: 'price')
String? price;
@JsonKey(name: 'currency')
String? currency;
OrdersReport(
{this.orderType,
this.token,
this.packageName,
this.productId,
this.subscriptionId,
this.bundleId,
this.receipt,
this.price,
this.currency,
this.sku,
this.countryCode});
@override
String toString() {
final StringBuffer sb = StringBuffer();
sb.writeln("[OrdersReport]");
sb.writeln(" productId: $productId");
sb.writeln(" price: $price");
sb.writeln(" currency: $currency");
if (Platform.isAndroid) {
sb.writeln(" orderType: $orderType");
sb.writeln(" packageName: $packageName");
sb.writeln(" subscriptionId: $subscriptionId");
sb.writeln(" token: $token");
} else if (Platform.isIOS) {
sb.writeln(" bundleId: $bundleId");
sb.writeln(" receipt: $receipt");
sb.writeln(" sku: $sku");
sb.writeln(" countryCode: $countryCode");
}
return sb
.toString(); //'OrdersReport{orderType: $orderType, packageName: $packageName, productId: $productId, subscriptionId: $subscriptionId, token: $token, bundleId: $bundleId, receipt: $receipt, price: $price, currency: $currency, introductoryPrice: $introductoryPrice}';
}
factory OrdersReport.fromJson(Map<String, dynamic> json) => _$OrdersReportFromJson(json);
Map<String, dynamic> toJson() => _$OrdersReportToJson(this);
}
@JsonSerializable()
class OrdersResponse {
@JsonKey(name: 'usdPrice', defaultValue: 0.0)
double usdPrice;
OrdersResponse(this.usdPrice);
factory OrdersResponse.fromJson(Map<String, dynamic> json) => _$OrdersResponseFromJson(json);
Map<String, dynamic> toJson() => _$OrdersResponseToJson(this);
@override
String toString() {
return 'OrdersResponse{usdPrice:$usdPrice}';
}
}

View File

@ -0,0 +1,46 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'orders_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
OrdersReport _$OrdersReportFromJson(Map<String, dynamic> json) => OrdersReport(
orderType: json['orderType'] as int? ?? 0,
token: json['token'] as String?,
packageName: json['packageName'] as String?,
productId: json['productId'] as String?,
subscriptionId: json['subscriptionId'] as String?,
bundleId: json['bundleId'] as String?,
receipt: json['receipt'] as String?,
price: json['price'] as String?,
currency: json['currency'] as String?,
sku: json['sku'] as String?,
countryCode: json['country'] as String?,
);
Map<String, dynamic> _$OrdersReportToJson(OrdersReport instance) =>
<String, dynamic>{
'orderType': instance.orderType,
'packageName': instance.packageName,
'productId': instance.productId,
'subscriptionId': instance.subscriptionId,
'token': instance.token,
'bundleId': instance.bundleId,
'receipt': instance.receipt,
'sku': instance.sku,
'country': instance.countryCode,
'price': instance.price,
'currency': instance.currency,
};
OrdersResponse _$OrdersResponseFromJson(Map<String, dynamic> json) =>
OrdersResponse(
(json['usdPrice'] as num?)?.toDouble() ?? 0.0,
);
Map<String, dynamic> _$OrdersResponseToJson(OrdersResponse instance) =>
<String, dynamic>{
'usdPrice': instance.usdPrice,
};

View File

@ -0,0 +1,67 @@
import 'package:guru_app/financial/asset/assets_model.dart';
import 'package:guru_app/financial/asset/assets_store.dart';
import 'package:guru_app/financial/iap/iap_manager.dart';
import 'package:guru_app/financial/iap/iap_model.dart';
import 'package:guru_app/financial/igb/igb_manager.dart';
import 'package:guru_app/financial/igc/igc_manager.dart';
import 'package:guru_app/financial/reward/reward_manager.dart';
import 'package:guru_utils/extensions/extensions.dart';
/// Created by Haoyi on 2021/7/2
class FinancialManager {
static final FinancialManager instance = FinancialManager._();
FinancialManager._();
// final RewardAssetService rewardService;
Stream<AssetsStore<Asset>> get observableAssets => Rx.combineLatest3<AssetsStore<Asset>,
AssetsStore<Asset>, AssetsStore<Asset>, AssetsStore<Asset>>(
IapManager.instance.observableAssetStore,
IgcManager.instance.observableAssetStore,
RewardManager.instance.observableAssetStore, (iapPurchased, gemAssets, rewarded) {
return _merge(iapPurchased: iapPurchased, gemAssets: gemAssets, rewarded: rewarded);
});
AssetsStore<Asset> get currentAssets => _merge(
iapPurchased: IapManager.instance.purchasedStore,
gemAssets: IgcManager.instance.purchasedStore,
rewarded: RewardManager.instance.rewardedStore);
static AssetsStore<Asset> _merge(
{required AssetsStore<Asset> iapPurchased,
required AssetsStore<Asset> gemAssets,
required AssetsStore<Asset> rewarded}) {
final result = AssetsStore<Asset>();
if (iapPurchased.isActive) {
iapPurchased.forEach((productId, possessions) {
result.addAsset(possessions);
});
}
if (gemAssets.isActive) {
gemAssets.forEach((productId, possessions) {
result.addAsset(possessions);
});
}
if (rewarded.isActive) {
rewarded.forEach((productId, possessions) {
result.addAsset(possessions);
});
}
return result;
}
void init() {
IapManager.instance.init();
IgcManager.instance.init();
IgbManager.instance.init();
RewardManager.instance.init();
}
void switchSession(String fromUid, String toUid) {
IapManager.instance.switchSession();
IgcManager.instance.switchSession();
RewardManager.instance.switchSession();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
import 'dart:async';
import 'package:guru_app/financial/asset/assets_model.dart';
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/financial/manifest/manifest.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:guru_utils/id/id_utils.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
/// Created by Haoyi on 6/2/21
class IapRequest {
final IapProduct product;
final OrderEntity order;
final Completer<bool> completer;
ProductId get productId => product.productId;
IapRequest(this.product, this.order, this.completer);
void response(bool result) {
if (!completer.isCompleted) {
completer.complete(result);
}
}
}
class PurchaseError implements Exception {
final String msg;
PurchaseError(this.msg);
}
class IapProduct implements Product {
@override
final ProductId productId;
final ProductDetails details;
final ProductDetails? offerDetails;
@override
final Manifest manifest;
String get sku => productId.sku;
IapProduct(this.productId, this.details, this.manifest, {this.offerDetails});
bool isConsumable() {
return productId.isConsumable;
}
@override
String toString() {
return 'IapProduct{productId: $productId}';
}
@override
OrderEntity createOrder() {
return OrderEntity(
orderId: IdUtils.uuidV4(),
sku: productId.sku,
state: TransactionState.pending,
attr: productId.attr,
method: TransactionMethod.iap.index,
currency: details.currencyCode ?? "USD",
cost: details.rawPrice,
category: manifest.category,
timestamp: DateTimeUtils.currentTimeInMillis(),
manifest: manifest);
}
// @override
// TransactionEntity buildTransaction({String? tid}) {
// return TransactionEntity(
// tid: tid,
// sku: productId.sku,
// state: TransactionStates.pending,
// attr: productId.attr,
// method: TransactionMethods.iap,
// currency: details.currencySymbol,
// cost: details.rawPrice,
// timestamp: DateTimeUtils.currentTimeInMillis(),
// manifest: manifest);
// }
}
class IapAsset extends Asset {
final PurchaseDetails details;
IapAsset(ProductId productId, OrderEntity entity, this.details) : super(productId, entity);
IapAsset copyWith({OrderEntity? order, PurchaseDetails? details}) {
return IapAsset(productId, order ?? this.order, details ?? this.details);
}
@override
String toString() {
return 'IapPurchased{productId: $productId, item: $details}';
}
// @override
// Manifest? get manifest => ;
}

View File

@ -0,0 +1,319 @@
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_utils/converts/converts.dart';
import 'package:guru_utils/log/log.dart';
/// Created by Haoyi on 3/4/21
import 'package:json_annotation/json_annotation.dart';
part 'in_app_receipt_ios.g.dart';
enum SubscriptionType { autoRenewable, nonRenewing }
class SubscriptionPeriod {
final Duration? period;
final Duration? trial;
@override
String toString() {
return 'SubscriptionPeriod{period: $period, trial: $trial}';
}
SubscriptionPeriod(this.period, this.trial);
}
class IosReceiptStatus {
/// Not decodable status
static const unknown = -2;
/// No status returned
static const none = -1;
/// valid statua
static const valid = 0;
/// The App Store could not read the JSON object you provided.
static const jsonNotReadable = 21000;
/// The data in the receipt-data property was malformed or missing.
static const malformedOrMissingData = 21002;
/// The receipt could not be authenticated.
static const receiptCouldNotBeAuthenticated = 21003;
/// The shared secret you provided does not match the shared secret on file for your account.
static const secretNotMatching = 21004;
/// The receipt server is not currently available.
static const receiptServerUnavailable = 21005;
/// This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
static const subscriptionExpired = 21006;
/// This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.
static const testReceipt = 21007;
/// This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.
static const productionEnvironment = 21008;
bool isValid(int status) {
return status == IosReceiptStatus.valid;
}
}
class ReceiptResult {
final int status;
final int expiredDateInMillis;
// final List<ReceiptItem> items;
ReceiptResult(this.status, {this.expiredDateInMillis = 0});
@override
String toString() {
return 'SubscriptionResult{state: $status, expiredDateInMillis: $expiredDateInMillis}';
}
}
@JsonSerializable()
class ReceiptData {
@JsonKey(name: "environment")
final String? environment;
@JsonKey(name: "receipt")
final Receipt? receipt;
@JsonKey(name: "latest_receipt_info", defaultValue: <ReceiptItem>[])
final List<ReceiptItem>? latestReceiptItems;
@JsonKey(name: "pending_renewal_info", defaultValue: <PendingRenewalInfo>[])
final List<PendingRenewalInfo>? pendingRenewalInfoItems;
@JsonKey(name: "latest_receipt", defaultValue: "")
final String? latestReceipt;
@JsonKey(name: "status")
final int status;
int get requestDateInMillis => receipt?.requestDateInMillis ?? 0;
ReceiptData({this.environment, this.receipt, this.latestReceiptItems, this.pendingRenewalInfoItems, this.latestReceipt, required this.status});
factory ReceiptData.fromJson(Map<String, dynamic> json) => _$ReceiptDataFromJson(json);
Map<String, dynamic> toJson() => _$ReceiptDataToJson(this);
dumpLog() {
Log.d("========= RECEIPT DATE ========");
Log.d(" [status] => $status");
Log.d(" [receipt begin] ========>");
receipt?.dumpLog();
Log.d(" <======== [receipt end] ");
Log.d(" [latestReceiptItems] => ${latestReceiptItems?.length}");
Log.d(" [pendingRenewalInfoItems] => ${pendingRenewalInfoItems?.length}");
Log.d(" [latestReceipt] => $latestReceipt");
Log.d("========= RECEIPT DATE ========");
}
List<ReceiptItem> getCheckReceipts({SubscriptionType type = SubscriptionType.autoRenewable, List<ProductId> checkIds = const []}) {
final List<ReceiptItem> receipts = <ReceiptItem>[];
final Set<String> platformCheckIds = checkIds.map((id) => id.sku).toSet();
switch (type) {
case SubscriptionType.nonRenewing:
if (receipt?.inAppReceiptItems != null && receipt!.inAppReceiptItems.isNotEmpty) {
receipts.addAll(receipt!.inAppReceiptItems);
}
break;
default:
if (latestReceiptItems != null && latestReceiptItems!.isNotEmpty) {
receipts.addAll(latestReceiptItems!);
}
break;
}
dumpLog();
Log.d("[validate]==> receipts.length:${receipts.length}");
if (checkIds.isNotEmpty == true) {
return receipts.where((receipt) {
Log.d("[validate]==> productId:${receipt.productId} ${platformCheckIds.contains(receipt.productId)}");
return platformCheckIds.contains(receipt.productId);
}).toList();
}
return <ReceiptItem>[];
}
}
@JsonSerializable()
class Receipt {
@JsonKey(name: "receipt_type")
final String receiptType;
@JsonKey(name: "adam_id")
final int adamId;
@JsonKey(name: "app_item_id")
final int appItemId;
@JsonKey(name: "bundle_id")
final String bundleId;
@JsonKey(name: "application_version")
final String applicationVersion;
@JsonKey(name: "download_id")
final int downloadId;
@JsonKey(name: "version_external_identifier")
final int versionExternalIdentifier;
@JsonKey(name: "receipt_creation_date_ms")
@intStringConvert
final int receiptCreationDateInMillis;
@JsonKey(name: "request_date_ms")
@intStringConvert
final int requestDateInMillis;
@JsonKey(name: "original_purchase_date_ms")
@intStringConvert
final int originalPurchaseDateInMillis;
@JsonKey(name: "original_application_version")
final String originalApplicationVersion;
@JsonKey(name: "in_app", defaultValue: <ReceiptItem>[])
final List<ReceiptItem> inAppReceiptItems;
Receipt(
{required this.receiptType,
required this.adamId,
required this.appItemId,
required this.bundleId,
required this.applicationVersion,
required this.downloadId,
required this.versionExternalIdentifier,
required this.receiptCreationDateInMillis,
required this.requestDateInMillis,
required this.originalPurchaseDateInMillis,
required this.originalApplicationVersion,
required this.inAppReceiptItems});
factory Receipt.fromJson(Map<String, dynamic> json) => _$ReceiptFromJson(json);
Map<String, dynamic> toJson() => _$ReceiptToJson(this);
void dumpLog() {
Log.d(" [receiptType] => $receiptType");
Log.d(" [adamId] => $adamId");
Log.d(" [appItemId] => $appItemId");
Log.d(" [bundleId] => $bundleId");
Log.d(" [applicationVersion] => $applicationVersion");
Log.d(" [downloadId] => $downloadId");
Log.d(" [versionExternalIdentifier] => $versionExternalIdentifier");
Log.d(" [receiptCreationDateInMillis] => $receiptCreationDateInMillis");
Log.d(" [requestDateInMillis] => $requestDateInMillis");
Log.d(" [originalPurchaseDateInMillis] => $originalPurchaseDateInMillis");
Log.d(" [originalApplicationVersion] => $originalApplicationVersion");
Log.d(" [inAppReceiptItems] => ${inAppReceiptItems.length}");
}
}
@JsonSerializable()
class ReceiptItem {
@JsonKey(name: "product_id")
final String productId;
@JsonKey(name: "quantity")
@intStringConvert
final int quantity;
@JsonKey(name: "transaction_id")
final String transactionId;
@JsonKey(name: "original_transaction_id")
final String originalTransactionId;
@JsonKey(name: "purchase_date_ms")
@intStringConvert
final int purchaseDateInMillis;
@JsonKey(name: "expires_date_ms")
@intStringConvert
final int? expiresDateInMillis;
@JsonKey(name: "expires_date")
final String? expiresDateInString;
@JsonKey(name: "original_purchase_date_ms")
@intStringConvert
final int originalPurchaseDateInMillis;
@JsonKey(name: "web_order_line_item_id")
final String? webOrderLineItemId;
@JsonKey(name: "is_trial_period")
@boolStringConvert
final bool isTrialPeriod;
@JsonKey(name: "is_in_intro_offer_period")
@boolStringConvert
final bool? isInIntroOfferPeriod;
@JsonKey(name: "subscription_group_identifier")
final String? subscriptionGroupIdentifier;
@JsonKey(name: "cancellation_date")
final String? cancellationDate;
ReceiptItem(
{required this.productId,
required this.quantity,
required this.transactionId,
required this.originalTransactionId,
required this.purchaseDateInMillis,
required this.expiresDateInMillis,
required this.originalPurchaseDateInMillis,
required this.webOrderLineItemId,
required this.isTrialPeriod,
required this.isInIntroOfferPeriod,
required this.subscriptionGroupIdentifier,
required this.cancellationDate,
this.expiresDateInString});
factory ReceiptItem.fromJson(Map<String, dynamic> json) => _$ReceiptItemFromJson(json);
Map<String, dynamic> toJson() => _$ReceiptItemToJson(this);
}
@JsonSerializable()
class PendingRenewalInfo {
// @JsonKey(name: "expiration_intent")
// @INT_STRING_CONVERT
// final int expirationIntent;
@JsonKey(name: "auto_renew_product_id")
final String autoRenewProductId;
@JsonKey(name: "original_transaction_id")
final String originalTransactionId;
// @JsonKey(name: "is_in_billing_retry_period")
// @INT_STRING_CONVERT
// final int isInBillingRetryPeriod;
@JsonKey(name: "product_id")
final String productId;
@JsonKey(name: "auto_renew_status")
@intStringConvert
final int autoRenewStatus;
PendingRenewalInfo(
{
required this.autoRenewProductId,
required this.originalTransactionId,
required this.productId,
required this.autoRenewStatus});
factory PendingRenewalInfo.fromJson(Map<String, dynamic> json) => _$PendingRenewalInfoFromJson(json);
Map<String, dynamic> toJson() => _$PendingRenewalInfoToJson(this);
}

View File

@ -0,0 +1,146 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'in_app_receipt_ios.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ReceiptData _$ReceiptDataFromJson(Map<String, dynamic> json) => ReceiptData(
environment: json['environment'] as String?,
receipt: json['receipt'] == null
? null
: Receipt.fromJson(json['receipt'] as Map<String, dynamic>),
latestReceiptItems: (json['latest_receipt_info'] as List<dynamic>?)
?.map((e) => ReceiptItem.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
pendingRenewalInfoItems: (json['pending_renewal_info'] as List<dynamic>?)
?.map(
(e) => PendingRenewalInfo.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
latestReceipt: json['latest_receipt'] as String? ?? '',
status: json['status'] as int,
);
Map<String, dynamic> _$ReceiptDataToJson(ReceiptData instance) =>
<String, dynamic>{
'environment': instance.environment,
'receipt': instance.receipt,
'latest_receipt_info': instance.latestReceiptItems,
'pending_renewal_info': instance.pendingRenewalInfoItems,
'latest_receipt': instance.latestReceipt,
'status': instance.status,
};
Receipt _$ReceiptFromJson(Map<String, dynamic> json) => Receipt(
receiptType: json['receipt_type'] as String,
adamId: json['adam_id'] as int,
appItemId: json['app_item_id'] as int,
bundleId: json['bundle_id'] as String,
applicationVersion: json['application_version'] as String,
downloadId: json['download_id'] as int,
versionExternalIdentifier: json['version_external_identifier'] as int,
receiptCreationDateInMillis:
intStringConvert.fromJson(json['receipt_creation_date_ms'] as String),
requestDateInMillis:
intStringConvert.fromJson(json['request_date_ms'] as String),
originalPurchaseDateInMillis: intStringConvert
.fromJson(json['original_purchase_date_ms'] as String),
originalApplicationVersion:
json['original_application_version'] as String,
inAppReceiptItems: (json['in_app'] as List<dynamic>?)
?.map((e) => ReceiptItem.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
Map<String, dynamic> _$ReceiptToJson(Receipt instance) => <String, dynamic>{
'receipt_type': instance.receiptType,
'adam_id': instance.adamId,
'app_item_id': instance.appItemId,
'bundle_id': instance.bundleId,
'application_version': instance.applicationVersion,
'download_id': instance.downloadId,
'version_external_identifier': instance.versionExternalIdentifier,
'receipt_creation_date_ms':
intStringConvert.toJson(instance.receiptCreationDateInMillis),
'request_date_ms': intStringConvert.toJson(instance.requestDateInMillis),
'original_purchase_date_ms':
intStringConvert.toJson(instance.originalPurchaseDateInMillis),
'original_application_version': instance.originalApplicationVersion,
'in_app': instance.inAppReceiptItems,
};
ReceiptItem _$ReceiptItemFromJson(Map<String, dynamic> json) => ReceiptItem(
productId: json['product_id'] as String,
quantity: intStringConvert.fromJson(json['quantity'] as String),
transactionId: json['transaction_id'] as String,
originalTransactionId: json['original_transaction_id'] as String,
purchaseDateInMillis:
intStringConvert.fromJson(json['purchase_date_ms'] as String),
expiresDateInMillis: _$JsonConverterFromJson<String, int>(
json['expires_date_ms'], intStringConvert.fromJson),
originalPurchaseDateInMillis: intStringConvert
.fromJson(json['original_purchase_date_ms'] as String),
webOrderLineItemId: json['web_order_line_item_id'] as String?,
isTrialPeriod:
boolStringConvert.fromJson(json['is_trial_period'] as String),
isInIntroOfferPeriod: _$JsonConverterFromJson<String, bool>(
json['is_in_intro_offer_period'], boolStringConvert.fromJson),
subscriptionGroupIdentifier:
json['subscription_group_identifier'] as String?,
cancellationDate: json['cancellation_date'] as String?,
expiresDateInString: json['expires_date'] as String?,
);
Map<String, dynamic> _$ReceiptItemToJson(ReceiptItem instance) =>
<String, dynamic>{
'product_id': instance.productId,
'quantity': intStringConvert.toJson(instance.quantity),
'transaction_id': instance.transactionId,
'original_transaction_id': instance.originalTransactionId,
'purchase_date_ms':
intStringConvert.toJson(instance.purchaseDateInMillis),
'expires_date_ms': _$JsonConverterToJson<String, int>(
instance.expiresDateInMillis, intStringConvert.toJson),
'expires_date': instance.expiresDateInString,
'original_purchase_date_ms':
intStringConvert.toJson(instance.originalPurchaseDateInMillis),
'web_order_line_item_id': instance.webOrderLineItemId,
'is_trial_period': boolStringConvert.toJson(instance.isTrialPeriod),
'is_in_intro_offer_period': _$JsonConverterToJson<String, bool>(
instance.isInIntroOfferPeriod, boolStringConvert.toJson),
'subscription_group_identifier': instance.subscriptionGroupIdentifier,
'cancellation_date': instance.cancellationDate,
};
Value? _$JsonConverterFromJson<Json, Value>(
Object? json,
Value? Function(Json json) fromJson,
) =>
json == null ? null : fromJson(json as Json);
Json? _$JsonConverterToJson<Json, Value>(
Value? value,
Json? Function(Value value) toJson,
) =>
value == null ? null : toJson(value);
PendingRenewalInfo _$PendingRenewalInfoFromJson(Map<String, dynamic> json) =>
PendingRenewalInfo(
autoRenewProductId: json['auto_renew_product_id'] as String,
originalTransactionId: json['original_transaction_id'] as String,
productId: json['product_id'] as String,
autoRenewStatus:
intStringConvert.fromJson(json['auto_renew_status'] as String),
);
Map<String, dynamic> _$PendingRenewalInfoToJson(PendingRenewalInfo instance) =>
<String, dynamic>{
'auto_renew_product_id': instance.autoRenewProductId,
'original_transaction_id': instance.originalTransactionId,
'product_id': instance.productId,
'auto_renew_status': intStringConvert.toJson(instance.autoRenewStatus),
};

View File

@ -0,0 +1,110 @@
import 'dart:convert';
import 'package:guru_app/guru_app.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'in_app_receipt_ios.dart';
import 'package:http/http.dart' as http;
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
/// Created by Haoyi on 3/5/21
///
class IosReceiptValidator {
final AppStorePurchaseDetails purchasedDetails;
ReceiptData? _receiptData;
IosReceiptValidator(this.purchasedDetails);
Future<ReceiptData?> _validateReceipt(bool isSandbox) async {
Log.d("[validate] isSandbox:$isSandbox");
final iosValidateReceiptPassword =
GuruApp.instance.appSpec.deployment.iosValidateReceiptPassword;
if (iosValidateReceiptPassword == null) {
return null;
}
final Map<String, String> headers = <String, String>{
'Accept': 'application/json',
'Content-Type': 'application/json',
};
final Map<String, String> receiptBody = <String, String>{
"receipt-data": purchasedDetails.verificationData.serverVerificationData,
"password": iosValidateReceiptPassword
};
final String url = isSandbox
? 'https://sandbox.itunes.apple.com/verifyReceipt'
: 'https://buy.itunes.apple.com/verifyReceipt';
final body = jsonEncode(receiptBody);
try {
final response = await http.post(Uri.parse(url), headers: headers, body: body);
final json = jsonDecode(response.body);
return ReceiptData.fromJson(json);
} catch (error, stacktrace) {
return null;
}
}
Future<ReceiptData?> validate() async {
final receiptData = await _validateReceipt(false);
if (receiptData?.environment == "Sandbox" || receiptData?.status == 21007) {
return _validateReceipt(true);
}
return receiptData;
}
Duration? _getReceiptDuration(SubscriptionType type,
{Duration duration = const Duration(seconds: 0)}) {
switch (type) {
case SubscriptionType.nonRenewing:
return duration;
default:
return null;
}
}
// List<ReceiptItem> _sortedReceipts(List<ReceiptItem> items, {Duration? duration}) {
// final result = List.of(items);
// if (duration == null) {
// result.sort((a, b) => b.expiresDateInMillis.compareTo(a.expiresDateInMillis));
// return result;
// }
// return result;
// }
// Future<ReceiptResult> verifySubscriptions(SubscriptionType type) async {
// final receiptData = await validate();
// final duration = _getReceiptDuration(type);
// final checkReceipts =
// receiptData.getCheckReceipts(type: type, checkIds: ProductIds.iapPremiumIds);
// final nonCancelledReceipts = checkReceipts.where((receipt) => receipt.cancellationDate == null);
// if (nonCancelledReceipts.length > checkReceipts.length) {
// print(
// "[validate]==> receipt has ${nonCancelledReceipts.length} items, but only ${checkReceipts.length} were parsed");
// }
// final sortedReceipts = _sortedReceipts(checkReceipts, duration: duration);
//
// if (sortedReceipts.isEmpty) {
// print("[validate]==> sortedReceipts is Empty");
// return ReceiptResult(PurchaseStatus.notPurchased);
// }
//
// final firstReceiptItem = sortedReceipts[0];
// print(
// "[validate]==> firstReceiptItem:${firstReceiptItem.expiresDateInMillis} request:${receiptData.requestDateInMillis}");
// if (firstReceiptItem.expiresDateInMillis > receiptData.requestDateInMillis) {
// return ReceiptResult(PurchaseStatus.purchased, items: sortedReceipts);
// } else {
// return ReceiptResult(PurchaseStatus.expired,
// expiredDateInMillis: firstReceiptItem.expiresDateInMillis, items: sortedReceipts);
// }
// }
// }
}

View File

@ -0,0 +1,53 @@
import 'package:guru_app/analytics/guru_analytics.dart';
import 'package:guru_app/database/guru_db.dart';
import 'package:guru_app/financial/asset/assets_model.dart';
import 'package:guru_app/financial/asset/assets_store.dart';
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/financial/igb/igb_product.dart';
import 'package:guru_app/financial/igc/igc_model.dart';
import 'package:guru_app/financial/manifest/manifest_manager.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/inventory/inventory_manager.dart';
import 'package:guru_app/property/app_property.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/property/app_property.dart';
/// Created by Haoyi on 2023/2/18
class IgbManager {
static final IgbManager _instance = IgbManager._();
static IgbManager get instance => _instance;
IgbManager._();
Future init() async {
await InventoryManager.instance.init();
}
Future reloadAssets() async {}
Future<bool> accumulate(int igc, TransactionMethod method, {String? scene}) async {
return false;
}
Future clear() async {}
Future<IgbProduct> buildIgbProduct(TransactionIntent intent) async {
final manifest = await ManifestManager.instance.createManifest(intent);
return IgbProduct(intent.productId, manifest, intent.igbCost);
}
Future<bool> redeem(IgbProduct product) async {
Log.v("Igb buy");
final result = await InventoryManager.instance.consume(product.cost, product.manifest);
if (result) {
await ManifestManager.instance.deliver(product.manifest, TransactionMethod.igb);
}
return true;
}
void dispose() {}
}

View File

@ -0,0 +1,46 @@
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_app/inventory/db/inventory_database.dart';
import 'package:guru_app/inventory/inventory_manager.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/id/id_utils.dart';
import 'package:guru_utils/manifest/manifest.dart';
// In-game barter Product
class IgbProduct implements Product {
@override
final ProductId productId;
@override
final Manifest manifest;
final List<StockItem> cost;
String get sku => productId.sku;
IgbProduct(this.productId, this.manifest, this.cost);
bool isConsumable() {
return productId.isConsumable;
}
@override
String toString() {
return 'StockProduct{productId: $productId}';
}
@override
OrderEntity createOrder() {
return OrderEntity(
orderId: IdUtils.uuidV4(),
sku: productId.sku,
state: TransactionState.success,
attr: productId.attr,
method: TransactionMethod.igc.index,
currency: TransactionCurrency.igc,
cost: 1,
category: manifest.category,
timestamp: DateTimeUtils.currentTimeInMillis(),
manifest: manifest);
}
}

View File

@ -0,0 +1,170 @@
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/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/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 IgcManager {
static final IgcManager _instance = IgcManager._();
static IgcManager get instance => _instance;
final BehaviorSubject<int> _balanceSubject = BehaviorSubject.seeded(0);
final BehaviorSubject<AssetsStore<Asset>> _assetStoreSubject =
BehaviorSubject.seeded(AssetsStore<Asset>.inactive());
Stream<AssetsStore<Asset>> get observableAssetStore => _assetStoreSubject.stream;
AssetsStore<Asset> get purchasedStore => _assetStoreSubject.value;
int get currentBalance => _balanceSubject.value;
Stream<int> get observableCurrentBalance => _balanceSubject.stream;
final CompositeSubscription subscriptions = CompositeSubscription();
IgcManager._();
Future init() async {
final attachedGems = (await AppProperty.getInstance().isFirstUseGemsFeature())
? GuruApp.instance.appSpec.deployment.initIgc
: 0;
final _balance = await AppProperty.getInstance().accumulateIgc(attachedGems);
_balanceSubject.addEx(_balance);
GuruAnalytics.instance.setUserProperty("coin", _balance.toString());
final iapIgc = await AppProperty.getInstance().getIapIgc();
GuruAnalytics.instance.setUserProperty("iap_coin", iapIgc.toString());
final noIapIgcGems = await AppProperty.getInstance().getNoIapIgc();
GuruAnalytics.instance.setUserProperty("noniap_coin", noIapIgcGems.toString());
await reloadAssets();
}
Future switchSession() async {
init();
}
Future reloadAssets() async {
final orders = await GuruDB.instance
.selectOrders(method: TransactionMethod.igc, attrs: [TransactionAttributes.asset]);
final newAssetStore = AssetsStore<Asset>();
for (var order in orders) {
final productId = order.productId;
Log.v("init order:${order.sku} $productId");
newAssetStore.addAsset(Asset(productId, order));
}
_assetStoreSubject.addEx(newAssetStore);
}
Future clearAssets({String? category, TransactionMethod? method}) async {
await GuruDB.instance.clearOrders(category: category, method: method);
final newAssetStore = purchasedStore.clone()..clearAsset(category: category, method: method);
_assetStoreSubject.addEx(newAssetStore);
}
Future<bool> accumulate(int igc, TransactionMethod method, {String? scene}) async {
try {
int newBalance = await AppProperty.getInstance().accumulateIgc(igc);
try {
if (method == TransactionMethod.iap) {
final iapIgc = await AppProperty.getInstance().accumulateIapIgc(igc);
await GuruAnalytics.instance.setUserProperty("iap_coin", iapIgc.toString());
} else {
final noniapIgc = await AppProperty.getInstance().accumulateNoIapIgc(igc);
await GuruAnalytics.instance.setUserProperty("noniap_coin", noniapIgc.toString());
}
await GuruAnalytics.instance.setUserProperty("coin", newBalance.toString());
} catch (throwable, stacktrace) {
Log.w("accumulate error $throwable", syncFirebase: true, syncCrashlytics: true);
}
_balanceSubject.add(newBalance);
GuruAnalytics.instance.logEarnVirtualCurrency(
virtualCurrencyName: "coin",
method: scene ?? convertTransactionMethodName(method),
balance: newBalance,
value: igc);
return true;
} catch (error, stacktrace) {
Log.v("accumulate error:$error $stacktrace");
}
return false;
}
Future clear() async {
final result = await AppProperty.getInstance().clearAllIgc();
if (result) {
_balanceSubject.add(0);
}
}
Future<IgcProduct> buildIgcProduct(TransactionIntent intent) async {
final manifest = await ManifestManager.instance.createManifest(intent);
return IgcProduct(intent.productId, manifest, intent.igcCost);
}
Future<bool> purchase(IgcProduct product) async {
Log.v("Igc buy");
final purchasedItem = purchasedStore.getAsset(product.productId);
if (purchasedItem != null) {
Log.v("Coin buy ${purchasedItem.productId} direct success!");
return true;
}
return _requestPurchase(product);
}
Future<bool> _requestPurchase(IgcProduct product) async {
if (currentBalance < product.cost || product.cost < 0) {
Log.v("_requestPurchase error! $currentBalance price:${product.cost}");
return false;
}
try {
final int newBalance = await AppProperty.getInstance().consumeIgc(product.cost);
await GuruAnalytics.instance.setUserProperty("coin", newBalance.toString());
if (currentBalance != newBalance) {
_balanceSubject.add(newBalance);
}
if (product.cost != 0) {
GuruAnalytics.instance.logSpendCredits(
product.productId.sku, product.manifest.category, product.cost,
virtualCurrencyName: "coin", balance: newBalance, scene: product.manifest.scene);
}
if (!product.productId.isConsumable) {
final order = product.createOrder();
await GuruDB.instance.upsertOrder(order: order).catchError((error) {
Log.v("upsertOrder error!$error");
return false;
});
final newPurchasedStore = purchasedStore.clone();
newPurchasedStore.addAsset(Asset(product.productId, order));
_assetStoreSubject.addEx(newPurchasedStore);
}
return true;
} catch (error, stacktrace) {
Log.v("error $error, $stacktrace");
return false;
}
}
void dispose() {
// _productStoreSubject.close();
_assetStoreSubject.close();
_balanceSubject.close();
}
}

View File

@ -0,0 +1,47 @@
/// Created by Haoyi on 2023/2/18
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/financial/manifest/manifest.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/id/id_utils.dart';
/// Created by Haoyi on 2023/2/13
class IgcProduct implements Product {
@override
final ProductId productId;
@override
final Manifest manifest;
final int cost;
String get sku => productId.sku;
IgcProduct(this.productId, this.manifest, this.cost);
bool isConsumable() {
return productId.isConsumable;
}
@override
String toString() {
return 'IgcProduct{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: cost.toDouble(),
category: manifest.category,
timestamp: DateTimeUtils.currentTimeInMillis(),
manifest: manifest);
}
}

View File

@ -0,0 +1,17 @@
/// Created by Haoyi on 2023/1/24
export 'package:guru_utils/manifest/manifest.dart';
export 'manifest_manager.dart';
//
// class ReservedManifestFactory {
// static Future<Manifest?> buildNoBannerAndInterstitialAds(TransactionIntent intent) async {
// if (GuruApp.instance.productProfile.noAdsCapIds.contains(intent.productId)) {
// final details = Details.define(DetailsReservedType.noAds, 1);
// return Manifest("no_ads", details: [details]);
// }
// return null;
// }
//
// static List<ManifestBuilder> builders = [buildNoBannerAndInterstitialAds];
// }

View File

@ -0,0 +1,149 @@
import 'dart:async';
import 'package:guru_app/financial/igc/igc_manager.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_app/inventory/inventory_manager.dart';
import 'manifest.dart';
/// Created by Haoyi on 2022/8/21
// typedef DetailsDistributor = Future<bool> Function(Details, TransactionMethod, String scene);
typedef DetailsDistributor = Future<List<StockItem>> Function(
Details, TransactionMethod, String scene);
typedef ManifestBuilder = Future<Manifest?> Function(TransactionIntent);
class ManifestManager {
ManifestManager._() {
observableDeliveredManifest = deliveredManifestStream.stream.asBroadcastStream();
}
final StreamController<Manifest> deliveredManifestStream = StreamController();
static final ManifestManager instance = ManifestManager._();
late Stream<Manifest> observableDeliveredManifest;
final Map<String, DetailsDistributor> distributors = {
DetailsReservedType.igc: _deliverIgcDetails
};
final List<ManifestBuilder> builders = [];
static Future<List<StockItem>> _deliverIgcDetails(
Details details, TransactionMethod method, String scene) async {
if (details.amount > 0) {
await IgcManager.instance.accumulate(details.amount, method, scene: scene);
}
return [];
}
static Future<List<StockItem>> _deliverDefaultDetails(
Details details, TransactionMethod method, String scene) async {
if (details.amount > 0) {
return [StockItem.fromDetails(details)];
}
return [];
}
// static Future<bool> _deliverDetails(
// Details details, TransactionMethod method, String scene) async {
// final stock = StockItem.fromDetails(details);
// if (stock.amount > 0) {
// await InventoryManager.instance.acquire([stock], method, scene);
// }
// return false;
// }
void addDistributor(String type, DetailsDistributor distributor) {
distributors[type] = distributor;
}
void addBuilder(ManifestBuilder builder) {
builders.add(builder);
}
void addBuilders(List<ManifestBuilder> 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 {
bool result = false;
final List<StockItem> unsold = [];
for (var details in manifest.details) {
final items = await distributors[details.type]?.call(details, method, manifest.scene) ??
await _deliverDefaultDetails(details, method, manifest.scene);
final unsoldItems = await deliverStockItems(items, manifest, method);
unsold.addAll(unsoldItems);
result |= unsoldItems.isEmpty;
}
if (unsold.isNotEmpty) {
await _acquire(unsold, method, manifest);
}
deliveredManifestStream.add(manifest);
return result;
}
Future<Manifest> createManifest(TransactionIntent intent) async {
for (var builder in builders) {
final manifest = await builder(intent);
if (manifest != null) {
return manifest;
}
}
return Manifest.empty;
}
Manifest createIgcManifest(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);
}
// Manifest createIgbManifest(int igc, {String? category, String scene = ""}) {
// final details = <Details>[];
// details.add(Details.define(DetailsReservedType.igc, igc));
//
// final extras = <String, dynamic>{ExtraReservedField.scene: scene};
// return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details);
// }
}

View File

@ -0,0 +1,145 @@
// /// Created by Haoyi on 2021/7/1
//
// part of "../product_model.dart";
//
// class ProductProfile {
// final List<ProductId> oneOffChargeIapIds = [];
// final List<ProductId> subscriptionsIapIds = [];
// final List<ProductId> noAdsCapIds;
//
// final List<ProductId> igcIds = [];
// final List<ProductId> rewardIds = [];
//
// final List<Map<String, ProductId>> _idsMap =
// List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
//
// ProductProfile(
// {required List<ProductId> oneOffChargeIapIds,
// required List<ProductId> subscriptionsIapIds,
// List<ProductId> igcIds = const <ProductId>[],
// List<ProductId> rewardIds = const <ProductId>[],
// this.noAdsCapIds = const <ProductId>[]}) {
// for (var productId in oneOffChargeIapIds) {
// _define(productId, TransactionMethod.iap);
// }
// for (var productId in subscriptionsIapIds) {
// _define(productId, TransactionMethod.iap);
// }
// for (var productId in igcIds) {
// _define(productId, TransactionMethod.igc);
// }
// for (var productId in rewardIds) {
// _define(productId, TransactionMethod.reward);
// }
// }
//
// bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty;
//
// ProductId _define(ProductId productId, TransactionMethod method) {
// switch (method) {
// case TransactionMethod.iap:
// if (productId.isOneOffCharge) {
// oneOffChargeIapIds.add(productId);
// } else if (productId.isSubscription) {
// subscriptionsIapIds.add(productId);
// }
// break;
// case TransactionMethod.igc:
// igcIds.add(productId);
// break;
// case TransactionMethod.reward:
// rewardIds.add(productId);
// break;
// case TransactionMethod.none:
// break;
// }
// _idsMap[productId.attr][productId.sku] = productId;
// return productId;
// }
//
// ProductId findOrCreate(String sku, int attr, TransactionMethod method) {
// return _find(sku, attr) ?? _define(ProductId(android: sku, ios: sku, attr: attr), method);
// }
//
// ProductId? _find(String sku, int attr) {
// return _idsMap[attr][sku];
// }
//
// ProductId? find({String? sku, int? attr}) {
// if (sku == null) {
// return null;
// }
//
// if (attr != null) {
// return _find(sku, attr);
// } else {
// return _find(sku, TransactionAttributes.possessive) ??
// _find(sku, TransactionAttributes.subscriptions) ??
// _find(sku, TransactionAttributes.consumable);
// }
// }
// }
//
// class IapProfile {
// final List<ProductId> oneOffChargeIapIds = [];
// final List<ProductId> subscriptionsIapIds = [];
// final List<ProductId> noAdsCapIds;
// final List<Map<String, ProductId>> _idsMap =
// List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
//
// IapProfile(
// {required List<ProductId> oneOffChargeIapIds,
// required List<ProductId> subscriptionsIapIds,
// this.noAdsCapIds = const <ProductId>[]}) {
// for (var productId in oneOffChargeIapIds) {
// _define(productId);
// }
// for (var productId in subscriptionsIapIds) {
// _define(productId);
// }
// }
//
// bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty;
//
// static final IapProfile invalid =
// IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []);
//
// ProductId _define(ProductId productId) {
// if (productId.isOneOffCharge) {
// oneOffChargeIapIds.add(productId);
// } else if (productId.isSubscription) {
// subscriptionsIapIds.add(productId);
// } else {
// return productId;
// }
// _idsMap[productId.attr][productId.sku] = productId;
// return productId;
// }
//
// ProductId findOrCreate(String sku, int attr) {
// return _find(sku, attr) ?? _define(ProductId(android: sku, ios: sku, attr: attr));
// }
//
// ProductId? _find(String sku, int attr) {
// return _idsMap[attr][sku];
// }
//
// ProductId? find({String? sku, int? attr}) {
// if (sku == null) {
// return null;
// }
//
// if (attr != null) {
// return _find(sku, attr);
// } else {
// return _find(sku, TransactionAttributes.possessive) ??
// _find(sku, TransactionAttributes.subscriptions) ??
// _find(sku, TransactionAttributes.consumable);
// }
// }
// }
//
// class ProductIds {
// static const ProductId invalid =
// ProductId(android: "", attr: TransactionAttributes.unknown, ios: "");
// }

View File

@ -0,0 +1,287 @@
import 'dart:io';
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/financial/igb/igb_product.dart';
import 'package:guru_app/financial/manifest/manifest.dart';
import 'package:guru_app/financial/manifest/manifest_manager.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_app/inventory/inventory_manager.dart';
import 'package:guru_utils/hash/hash.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:guru_app/financial/iap/iap_model.dart';
import 'package:guru_app/financial/igc/igc_model.dart';
import 'package:guru_app/financial/reward/reward_model.dart';
part 'product_profile.dart';
/// Created by Haoyi on 6/1/21
class OrderMethods {
static const free = 0;
static const gem = 1; //
static const iap = 2; // IAP
static const reward = 3; //
static const limit = 4;
static String toAbbrevText(int method) {
switch (method) {
case free:
return "free";
case gem:
return "gem";
case iap:
return "iap";
case reward:
return "reward";
case limit:
return "limit";
default:
return "";
}
}
}
class OrderStates {
static const init = 0;
static const success = 1;
static const pending = -1;
static const error = -2;
}
class TransactionAttributes {
static const unknown = 0;
// Offer products for sale in your app for a one-off charge
@deprecated
static const possessive = DetailsAttr.permanent;
static const asset = DetailsAttr.permanent;
static const consumable = DetailsAttr.consumable;
static const Set<int> oneOffChargeAttributes = <int>{asset, consumable};
// Subscriptions are in-app content or services that are billed to users on a recurring basis
static const subscriptions = 10;
static const count = 11;
}
class TransactionCurrency {
static const free = "_FVC"; // Free Virtual Currency
static const igc = "_IGC"; // In-Game Virtual Currency
static const reward = "_RVC"; // Reward Virtual Currency
}
// GP GP
enum EligibilityCriteria {
// GP NEW CUSTOMERAPP
// ()
newCustomerNeverHadSubscribedThisGroup,
// GP NEW CUSTOMERAPP
//
newCustomerNeverHadThisSubscription,
// GP NEW CUSTOMERAPP
//
newCustomerNeverHadAnySubscription,
// ProductId
dependencyProductId
}
class ProductId {
final String android;
final String ios;
final int attr;
// android only
final String? basePlan;
final String? offerId;
final bool points;
final ProductId? _originId;
bool get isConsumable => attr == TransactionAttributes.consumable;
bool get hasOffer =>
Platform.isAndroid && basePlan?.isNotEmpty == true && offerId?.isNotEmpty == true;
bool get hasBasePlan => Platform.isAndroid && basePlan?.isNotEmpty == true;
static final _iapEventRegExp = RegExp(r'^.*\.[ia]\.');
ProductId get originId => _originId ?? this;
@override
String toString() {
if (hasOffer) {
return 'ProductId{sku: $sku, basePlan: $basePlan, offerId: $offerId}';
}
return 'ProductId{android: $android, ios: $ios, attr: $attr, points:$points}';
}
@deprecated
bool get isPossessive => attr == TransactionAttributes.asset;
bool get isAsset => attr == TransactionAttributes.asset;
bool get isPoints => points;
bool get isOneOffCharge =>
(attr == TransactionAttributes.consumable) || (attr == TransactionAttributes.asset);
bool get isSubscription => attr == TransactionAttributes.subscriptions;
String get iapEventName => sku.replaceFirst(_iapEventRegExp, "iap.").replaceAll(".", "_");
String get sku => Platform.isIOS ? ios : android;
static const ProductId invalid =
ProductId.fromSku(sku: "", attr: TransactionAttributes.consumable);
const ProductId(
{required this.android,
required this.ios,
required this.attr,
this.basePlan,
this.offerId,
this.points = false,
ProductId? originId})
: _originId = originId;
const ProductId.fromSku(
{required String sku, required this.attr, this.basePlan, this.offerId, this.points = false})
: android = sku,
ios = sku,
_originId = null;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ProductId &&
runtimeType == other.runtimeType &&
sku == other.sku &&
attr == other.attr &&
basePlan == other.basePlan &&
offerId == other.offerId &&
points == points;
@override
int get hashCode => hashObjects([sku, attr, basePlan ?? '', offerId ?? '', points]);
bool isValid() {
return sku.isNotEmpty;
}
TransactionIntent createIntent(
{required String scene,
int igcCost = 0,
List<StockItem> igbCost = const <StockItem>[],
bool sales = false,
double rate = 1.0,
EligibilityCriteria eligibilityCriteria =
EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup}) {
return TransactionIntent(this, scene,
igcCost: igcCost, sales: sales, rate: rate, eligibilityCriteria: eligibilityCriteria);
}
Future<RewardProduct> createRewardProduct(String scene) async {
final intent = createIntent(scene: scene);
final manifest = await ManifestManager.instance.createManifest(intent);
return RewardProduct(this, manifest);
}
Future<IgcProduct> createIgcProduct(int igcCost, String scene) async {
final intent = createIntent(scene: scene, igcCost: igcCost);
final manifest = await ManifestManager.instance.createManifest(intent);
return IgcProduct(this, manifest, igcCost);
}
Future<IgbProduct> createIgbProduct(List<StockItem> igbCost, String scene,
{Manifest? specified}) async {
final manifest = specified ??
await ManifestManager.instance.createManifest(createIntent(scene: scene, igbCost: igbCost));
return IgbProduct(this, manifest, igbCost);
}
}
abstract class Product {
ProductId get productId;
Manifest get manifest;
factory Product.iap(ProductId productId, ProductDetails details, Manifest manifest,
{ProductDetails? offerDetails}) = IapProduct;
factory Product.igc(ProductId productId, Manifest manifest, int cost) = IgcProduct;
factory Product.reward(ProductId productId, Manifest manifest) = RewardProduct;
factory Product.igb(ProductId productId, Manifest manifest, List<StockItem> cost) = IgbProduct;
//
// factory Product.gems(ProductId productId, int price, Manifest manifest) = GemProduct;
//
// factory Product.reward(Reward reward) = RewardProduct;
//
// OrderEntity createOrder();
}
class TransactionState {
static const init = 0;
static const success = 1;
static const pending = -1;
static const error = -2;
static const expired = -3;
}
//
enum TransactionMethod {
iap, // IAP
igc, // In-game currency coin/gems..)
reward, //
bonus, //
igb, // In-game barter
free,
migrate,
unknown
}
String convertTransactionMethodName(TransactionMethod method) {
switch (method) {
case TransactionMethod.iap:
return "iap_buy";
case TransactionMethod.igc:
return "igc";
case TransactionMethod.reward:
return "reward";
case TransactionMethod.bonus:
return "bonus";
case TransactionMethod.igb:
return "igb";
case TransactionMethod.free:
return "prop";
case TransactionMethod.migrate:
return "migrate";
default:
return "unknown";
}
}
class TransactionIntent {
final ProductId productId;
final int igcCost; // In-game currency cost
final List<StockItem> igbCost; // In-game barter cost
final String scene; // (logEarnVirtualCurrencyitem_category)
final bool sales; //
final double rate; // 1.0 :1.21.2
final EligibilityCriteria eligibilityCriteria;
TransactionIntent(this.productId, this.scene,
{this.igcCost = 0,
this.igbCost = const <StockItem>[],
this.sales = false,
this.rate = 1.0,
this.eligibilityCriteria = EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup});
}

View File

@ -0,0 +1,180 @@
/// Created by Haoyi on 2021/7/1
part of "product_model.dart";
class ProductProfile {
final Set<ProductId> oneOffChargeIapIds = {};
final Set<ProductId> subscriptionsIapIds = {};
final Set<ProductId> pointsIapIds = {};
final Set<ProductId> noAdsCapIds;
final Set<ProductId> iapIds = {};
final Set<ProductId> igcIds = {};
final Set<ProductId> rewardIds = {};
final Set<ProductId> igbIds = {};
final Map<String, String> groupMap;
final List<ManifestBuilder> manifestBuilders = [];
final Map<String, Set<ProductId>> _offerIds = {};
final List<Map<String, ProductId>> _idsMap =
List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
ProductProfile(
{required Set<ProductId> oneOffChargeIapIds,
required Set<ProductId> subscriptionsIapIds,
Set<ProductId> pointsIapIds = const <ProductId>{},
Set<ProductId> igcIds = const <ProductId>{},
Set<ProductId> rewardIds = const <ProductId>{},
this.groupMap = const <String, String>{},
List<ManifestBuilder> manifestBuilders = const <ManifestBuilder>[],
this.noAdsCapIds = const <ProductId>{}}) {
for (var productId in oneOffChargeIapIds) {
_define(productId, TransactionMethod.iap);
}
for (var productId in subscriptionsIapIds) {
_define(productId, TransactionMethod.iap);
}
for (var productId in igcIds) {
_define(productId, TransactionMethod.igc);
}
for (var productId in rewardIds) {
_define(productId, TransactionMethod.reward);
}
this.pointsIapIds.addAll(pointsIapIds);
this.manifestBuilders.addAll(manifestBuilders);
}
bool hasIap() => oneOffChargeIapIds.isNotEmpty;
bool hasSubs() => subscriptionsIapIds.isNotEmpty;
ProductId _define(ProductId productId, TransactionMethod method) {
ProductId definedProductId = productId;
switch (method) {
case TransactionMethod.iap:
if (productId.isOneOffCharge) {
oneOffChargeIapIds.add(productId);
} else if (productId.isSubscription) {
definedProductId = productId.originId;
subscriptionsIapIds.add(definedProductId);
if (productId.hasBasePlan) {
(_offerIds[productId.sku] ??= <ProductId>{}).add(productId);
}
}
iapIds.add(definedProductId);
break;
case TransactionMethod.igc:
igcIds.add(definedProductId);
break;
case TransactionMethod.reward:
rewardIds.add(definedProductId);
break;
case TransactionMethod.bonus:
break;
case TransactionMethod.igb:
igbIds.add(definedProductId);
break;
case TransactionMethod.free:
break;
case TransactionMethod.migrate:
break;
case TransactionMethod.unknown:
break;
}
_idsMap[productId.attr][productId.sku] = definedProductId;
return productId;
}
ProductId define(String sku, int attr, TransactionMethod method) {
final productId = _find(sku, attr) ?? ProductId.fromSku(sku: sku, attr: attr);
return _define(productId, method);
}
ProductId? _find(String sku, int attr) {
return _idsMap[attr][sku];
}
ProductId? find({String? sku, int? attr}) {
if (sku == null || sku == "") {
return null;
}
if (attr != null) {
return _find(sku, attr);
} else {
return _find(sku, TransactionAttributes.asset) ??
_find(sku, TransactionAttributes.subscriptions) ??
_find(sku, TransactionAttributes.consumable);
}
}
Set<ProductId> offerProductIds(ProductId productId) {
return _offerIds[productId.sku] ?? {};
}
String? group(ProductId productId) {
return groupMap[productId.sku];
}
}
class IapProfile {
final List<ProductId> oneOffChargeIapIds = [];
final List<ProductId> subscriptionsIapIds = [];
final List<ProductId> noAdsCapIds;
final List<Map<String, ProductId>> _idsMap =
List.generate(TransactionAttributes.count, (index) => <String, ProductId>{});
IapProfile(
{required List<ProductId> oneOffChargeIapIds,
required List<ProductId> subscriptionsIapIds,
this.noAdsCapIds = const <ProductId>[]}) {
for (var productId in oneOffChargeIapIds) {
_define(productId);
}
for (var productId in subscriptionsIapIds) {
_define(productId.originId);
}
}
bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty;
static final IapProfile invalid =
IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []);
ProductId _define(ProductId productId) {
if (productId.isOneOffCharge) {
oneOffChargeIapIds.add(productId);
} else if (productId.isSubscription) {
subscriptionsIapIds.add(productId);
} else {
return productId;
}
_idsMap[productId.attr][productId.sku] = productId;
return productId;
}
ProductId findOrCreate(String sku, int attr) {
return _find(sku, attr) ?? _define(ProductId(android: sku, ios: sku, attr: attr));
}
ProductId? _find(String sku, int attr) {
return _idsMap[attr][sku];
}
ProductId? find({String? sku, int? attr}) {
if (sku == null) {
return null;
}
if (attr != null) {
return _find(sku, attr);
} else {
return _find(sku, TransactionAttributes.possessive) ??
_find(sku, TransactionAttributes.subscriptions) ??
_find(sku, TransactionAttributes.consumable);
}
}
}

View File

@ -0,0 +1,82 @@
import 'package:guru_app/financial/product/product_model.dart';
/// Created by Haoyi on 6/1/21
class ProductStore<T extends Product> {
final Map<ProductId, T> data = <ProductId, T>{};
ProductStore();
void putProduct(T item) {
data[item.productId] = item;
}
void putAllProducts(List<T> items) {
for (var item in items) {
putProduct(item);
}
}
List<T> getProducts(List<ProductId> ids) {
final result = <T>[];
for (var id in ids) {
final item = getProduct(id);
if (item != null) {
result.add(item);
}
}
return result;
}
T? getProduct(ProductId productId) {
return productId.isValid() ? data[productId] : null;
}
bool existsProduct(ProductId productId) {
return productId.isValid() == true && data.containsKey(productId);
}
bool existsProducts(List<ProductId> productIds) {
for (var productId in productIds) {
if (existsProduct(productId)) {
return true;
}
}
return false;
}
T? getFirstProduct(List<ProductId> productIds) {
for (var productId in productIds) {
final product = getProduct(productId);
if (product != null) {
return product;
}
}
return null;
}
// Map<IapType, List<String>> filterUncertaintyProductIds(List<ProductId> productIds) {
// final uncertaintyIds = <IapType, List<String>>{};
//
// for (ProductId productId in productIds) {
// final id = productId.platformProductId;
// if (!data.containsKey(id)) {
// IapType iapType = IapType.Product;
// if (PurchaseUtils.isSubscriptionProductId(id)) {
// iapType = IapType.Subscription;
// }
// List<String> ids = uncertaintyIds[iapType];
// if (ids == null) {
// ids = <String>[];
// uncertaintyIds[iapType] = ids;
// }
// ids.add(id);
// }
// }
// return uncertaintyIds;
// }
ProductStore<T> clone() {
return ProductStore()..data.addAll(data);
}
}

View File

@ -0,0 +1,73 @@
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/manifest/manifest_manager.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_app/financial/reward/reward_model.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_utils/extensions/extensions.dart';
/// Created by Haoyi on 2023/2/13
class RewardManager {
static final RewardManager _instance = RewardManager._();
static RewardManager get instance => _instance;
RewardManager._();
final BehaviorSubject<AssetsStore<Asset>> _assetsStoreSubject =
BehaviorSubject.seeded(AssetsStore<Asset>.inactive());
AssetsStore<Asset> get rewardedStore => _assetsStoreSubject.value;
Stream<AssetsStore<Asset>> get observableAssetStore => _assetsStoreSubject.stream;
Future init() async {
await reloadAssets();
}
Future switchSession() async {
reloadAssets();
}
Future reloadAssets() async {
final transactions = await GuruDB.instance
.selectOrders(method: TransactionMethod.reward, attrs: [TransactionAttributes.asset]);
final newAssetsStore = AssetsStore<Asset>();
for (var transaction in transactions) {
final productId = transaction.productId;
Log.v("init [Rewards] transaction:${transaction.sku} $productId");
newAssetsStore.addAsset(Asset(productId, transaction));
}
_assetsStoreSubject.addEx(newAssetsStore);
}
Future<RewardProduct> buildRewardProduct(TransactionIntent intent) async {
final manifest = await ManifestManager.instance.createManifest(intent);
return RewardProduct(intent.productId, manifest);
}
Future<bool> claim(RewardProduct product, {String from = ""}) async {
Log.v("rewarded");
// Joker
if (product.productId.isConsumable) {
ManifestManager.instance.deliver(product.manifest, TransactionMethod.reward);
return true;
}
final order = product.createOrder();
final result = await GuruDB.instance.upsertOrder(order: order).catchError((error, stacktrace) {
Log.v("refreshTransaction error!$error $stacktrace");
return false;
});
if (result) {
final newAssetsStore = rewardedStore.clone();
newAssetsStore.addAsset(Asset(product.productId, order));
_assetsStoreSubject.addEx(newAssetsStore);
}
ManifestManager.instance.deliver(product.manifest, TransactionMethod.reward);
return result;
}
}

View File

@ -0,0 +1,44 @@
import 'package:guru_app/financial/asset/assets_model.dart';
import 'package:guru_app/financial/data/db/order_database.dart';
import 'package:guru_app/financial/manifest/manifest.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/id/id_utils.dart';
/// Created by Haoyi on 2023/2/13
class RewardProduct implements Product {
@override
final ProductId productId;
@override
final Manifest manifest;
String get sku => productId.sku;
RewardProduct(this.productId, this.manifest);
bool isConsumable() {
return productId.isConsumable;
}
@override
String toString() {
return 'IapProduct{productId: $productId}';
}
@override
OrderEntity createOrder() {
return OrderEntity(
orderId: IdUtils.uuidV4(),
sku: productId.sku,
state: TransactionState.success,
attr: productId.attr,
method: TransactionMethod.reward.index,
currency: TransactionCurrency.reward,
cost: 0,
category: manifest.category,
timestamp: DateTimeUtils.currentTimeInMillis(),
manifest: manifest);
}
}

View File

@ -0,0 +1,65 @@
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:flutter/services.dart';
import 'package:guru_app/guru_app.dart';
import 'package:guru_navigator/guru_navigator.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/router/router.dart';
import 'package:guru_utils/uri/uri_utils.dart';
/// Created by Haoyi on 5/20/21
class DxLinkManager {
static final DxLinkManager instance = DxLinkManager._();
DxLinkManager._();
void initDynamicLinks() async {
FirebaseDynamicLinks.instance.onLink.listen((PendingDynamicLinkData? dynamicLink) async {
final Uri? deepLink = dynamicLink?.link;
Log.d("### onDynamicLink $deepLink");
if (deepLink != null) {
_openLink(deepLink);
}
}, onError: (e) async {
Log.w('onLinkError ${e.message} ${e.stacktrace}', error: e);
});
final PendingDynamicLinkData? data =
await FirebaseDynamicLinks.instance.getInitialLink().catchError((error, stacktrace) {
Log.d("getInitialLink error:$error $stacktrace");
});
final Uri? deepLink = data?.link;
Log.d("initDynamicLinks: $deepLink");
if (deepLink != null) {
Future.delayed(const Duration(seconds: 2), () => _openLink(deepLink));
}
}
Future<dynamic> _handleDeeplink(MethodCall call) async {
Log.d("call.method: ${call.method} arguments:${call.arguments}");
switch (call.method) {
case "navigate":
final uri = Uri.parse(call.arguments["uri"]);
if ((uri.authority.isNotEmpty != true) ||
uri.toString().contains(GuruApp.instance.appSpec.details.authority)) {
_openLink(uri);
} else {
UriUtils.launchURL(uri);
}
}
return true;
}
void initDeeplink() async {
GuruNavigator.init(_handleDeeplink);
}
void init() async {
initDeeplink();
initDynamicLinks();
}
void _openLink(Uri uri) {
RouteCenter.instance.dispatchUri(uri);
}
}

View File

@ -0,0 +1,7 @@
library guru_firebase;
/// Created by Haoyi on 2022/8/31
export 'remoteconfig/remote_config_manager.dart';
export 'messaging/remote_messaging_manager.dart';
export 'package:firebase_messaging/firebase_messaging.dart';

View File

@ -0,0 +1,31 @@
/// Created by Haoyi on 2021/7/28
part of '../firestore_manager.dart';
extension AccountExtension on FirestoreManager {
String get userCollection {
if (kReleaseMode) {
return "users";
} else {
return "test_users";
}
}
Future<AccountProfile?> modifyProfile(Map<String, dynamic> modifyJson) async {
final uid = AccountDataStore.instance.uid;
if (uid == null || modifyJson.isEmpty) {
Log.i("modifyProfile error! uid is null!");
return null;
}
modifyJson.remove(AccountProfile.dirtyField);
// if (Settings.instance.debugMode.get() != true) {
await FirebaseFirestore.instance
.collection(userCollection)
.doc(uid)
.set(modifyJson, SetOptions(merge: true /*mergeFields: modifyJson.keys.toList()*/));
// }
modifyJson[AccountProfile.dirtyField] = false;
return AccountDataStore.instance.accountProfile?.merge(modifyJson) ??
AccountProfile.fromJson(modifyJson);
}
}

View File

@ -0,0 +1,15 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/account/model/account_profile.dart';
import 'package:guru_utils/log/log.dart';
/// Created by Haoyi on 2022/9/1
part 'account/account_extension.dart';
class FirestoreManager {
static final FirestoreManager instance = FirestoreManager._();
FirestoreManager._();
}

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