parent
							
								
									33a9582292
								
							
						
					
					
						commit
						59b48f342c
					
				|  | @ -0,0 +1,9 @@ | |||
| # .github/release.yml | ||||
| changelog: | ||||
|   categories: | ||||
|     - title: 🟢 Features | ||||
|       labels: | ||||
|         - Feature | ||||
|     - title: 🟠 Optimize | ||||
|       labels: | ||||
|         - optimize | ||||
|  | @ -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' | ||||
|  | @ -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/** | ||||
|  | @ -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或false,true表示在IapManager将自动进行restore操作,false反之 | ||||
|   - deployment解析器添加`enable_analytics_statistic`的支持,取值范围为true或false,true表示在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),该库没有引用任何GP,ADS, Firebase相关库,因此老项目可以正常引入 | ||||
| - **`[guru_spec]`** | ||||
|     该packages是一个方便生成APP基础信息的一个生成器,这样在配置文件中生成后,将可以将信息生成到代码中,支持flavors,该库依赖GP,ADS, Firebase,因此需要引入GuruApp库 | ||||
| - **`[guru_platform_data]`** | ||||
|     该库封装了一些平台相关的原生操作,该库弥补pub.dev上未实现的原生特殊功能 | ||||
| - **`[soundpool]`** | ||||
|     该库移植自原有soundpool但由于该库长期不更新,并内部依赖有错误,因此单独抽出来进行适配。 | ||||
|  | @ -0,0 +1 @@ | |||
| TODO: Add your license here. | ||||
|  | @ -0,0 +1,39 @@ | |||
| <!--  | ||||
| This README describes the package. If you publish this package to pub.dev, | ||||
| this README's contents appear on the landing page for your package. | ||||
| 
 | ||||
| For information about how to write a good package README, see the guide for | ||||
| [writing package pages](https://dart.dev/guides/libraries/writing-package-pages).  | ||||
| 
 | ||||
| For general information about developing packages, see the Dart guide for | ||||
| [creating packages](https://dart.dev/guides/libraries/create-library-packages) | ||||
| and the Flutter guide for | ||||
| [developing packages and plugins](https://flutter.dev/developing-packages).  | ||||
| --> | ||||
| 
 | ||||
| TODO: Put a short description of the package here that helps potential users | ||||
| know whether this package might be useful for them. | ||||
| 
 | ||||
| ## Features | ||||
| 
 | ||||
| TODO: List what your package can do. Maybe include images, gifs, or videos. | ||||
| 
 | ||||
| ## Getting started | ||||
| 
 | ||||
| TODO: List prerequisites and provide or point to information on how to | ||||
| start using the package. | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| TODO: Include short and useful examples for package users. Add longer examples | ||||
| to `/example` folder.  | ||||
| 
 | ||||
| ```dart | ||||
| const like = 'sample'; | ||||
| ``` | ||||
| 
 | ||||
| ## Additional information | ||||
| 
 | ||||
| TODO: Tell users more about the package: where to find more information, how to  | ||||
| contribute to the package, how to file issues, what response they can expect  | ||||
| from the package authors, and more. | ||||
|  | @ -0,0 +1,4 @@ | |||
| include: package:flutter_lints/flutter.yaml | ||||
| 
 | ||||
| # Additional information about this file can be found at | ||||
| # https://dart.dev/guides/language/analysis-options | ||||
|  | @ -0,0 +1,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 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -0,0 +1,431 @@ | |||
| app_name: GuruApp | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| # 广告配置 | ||||
| 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}' | ||||
| 
 | ||||
| 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: | ||||
|         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 | ||||
| 
 | ||||
|  | @ -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 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -0,0 +1,51 @@ | |||
| /// Created by Haoyi on 2021/7/26 | ||||
| 
 | ||||
| part of "account_manager.dart"; | ||||
| 
 | ||||
| extension AccountAuthExtension on AccountManager { | ||||
|   Future<AccountAuth> _authenticate(SaasUser saasUser, | ||||
|       {bool canRefreshFirebaseToken = true}) async { | ||||
|     User? firebaseUser; | ||||
|     SaasUser newSaasUser = saasUser; | ||||
|     firebaseUser = await _authenticateFirebase(saasUser).catchError((error) { | ||||
|       Log.e("_authenticateFirebase error! $error", tag: "Account"); | ||||
|       return null; | ||||
|     }); | ||||
|     if (firebaseUser == null && canRefreshFirebaseToken) { | ||||
|       try { | ||||
|         newSaasUser = await _refreshFirebaseToken(saasUser); | ||||
|         return _authenticate(newSaasUser, canRefreshFirebaseToken: false); | ||||
|       } catch (error, stacktrace) { | ||||
|         return AccountAuth(saasUser, null); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return AccountAuth(newSaasUser, firebaseUser); | ||||
|   } | ||||
| 
 | ||||
|   Future<SaasUser> _refreshFirebaseToken(SaasUser oldSaasUser) async { | ||||
|     return await GuruApi.instance | ||||
|         .renewFirebaseToken() | ||||
|         .then((tokenData) => oldSaasUser.copyWith(firebaseToken: tokenData.firebaseToken)); | ||||
|   } | ||||
| 
 | ||||
|   Future<User?> _authenticateFirebase(SaasUser saasUser) async { | ||||
|     int retry = 0; | ||||
|     dynamic lastError; | ||||
|     while (retry < 1) { | ||||
|       try { | ||||
|         Log.i("[$retry] _authenticateFirebase:${saasUser.firebaseToken}", tag: "Account"); | ||||
| 
 | ||||
|         return await FirebaseAuth.instance | ||||
|             .signInWithCustomToken(saasUser.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!"); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,124 @@ | |||
| import 'dart:convert'; | ||||
| import 'package:firebase_auth/firebase_auth.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/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<SaasUser?> _saasUserSubject = 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); | ||||
|   int initRetryCount = 0; | ||||
| 
 | ||||
|   Stream<AccountProfile?> get observableAccountProfile => _accountProfile.stream; | ||||
| 
 | ||||
|   // final EnvConfig envConfig; | ||||
| 
 | ||||
|   AccountDataStore._(); | ||||
| 
 | ||||
|   String? get saasToken => _saasUserSubject.value?.token; | ||||
| 
 | ||||
|   String? get uid => _saasUserSubject.value?.uid; | ||||
| 
 | ||||
|   AccountProfile? get accountProfile => _accountProfile.value; | ||||
| 
 | ||||
|   String? get nickname => _accountProfile.value?.nickname; | ||||
| 
 | ||||
|   String? get countryCode => _accountProfile.value?.countryCode; | ||||
| 
 | ||||
|   SaasUser? get user => _saasUserSubject.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); | ||||
| 
 | ||||
|   Stream<SaasUser?> get observableSaasUser => _saasUserSubject.stream; | ||||
| 
 | ||||
|   void dispose() { | ||||
|     _deviceInfoSubject.close(); | ||||
|     _saasUserSubject.close(); | ||||
|     _firebaseUser.close(); | ||||
|     _accountProfile.close(); | ||||
|   } | ||||
| 
 | ||||
|   Future<SaasUser?> signInAnonymousInLocked() async { | ||||
|     // 这里需要使用原始的http post请求。否则这里将会死锁DIO所有请求 | ||||
|     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 SaasUser.fromJson(result["data"]); | ||||
|       } | ||||
|     } catch (error, stacktrace) { | ||||
|       Log.v("signInAnonymousInLocked error:$error", tag: "Account"); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   Future refreshAuth() async { | ||||
|     final saasUser = await signInAnonymousInLocked(); | ||||
|     if (saasUser != null) { | ||||
|       updateSaasUser(saasUser); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void updateDeviceInfo(DeviceInfo deviceInfo) { | ||||
|     _deviceInfoSubject.addEx(deviceInfo); | ||||
|   } | ||||
| 
 | ||||
|   void updateSaasUser(SaasUser saasUser) { | ||||
|     _saasUserSubject.addEx(saasUser); | ||||
| 
 | ||||
|     if (saasUser.createAtTimestamp > 0) { | ||||
|       GuruAnalytics.instance | ||||
|           .setUserProperty("user_created_timestamp", saasUser.createAtTimestamp.toString()); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void updateFirebaseUser(User user) { | ||||
|     _firebaseUser.addEx(user); | ||||
|   } | ||||
| 
 | ||||
|   void updateAccountProfile(AccountProfile profile) { | ||||
|     _accountProfile.addEx(profile); | ||||
|   } | ||||
| 
 | ||||
|   bool transitionTo(AccountDataStatus status, {AccountDataStatus? expectStatus}) { | ||||
|     return _accountDataStatus.addIfChanged(status); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,161 @@ | |||
| import 'dart:async'; | ||||
| 
 | ||||
| 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/property/app_property.dart'; | ||||
| import 'package:guru_app/property/settings/guru_settings.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'; | ||||
| 
 | ||||
| /// Created by Haoyi on 6/3/21 | ||||
| /// | ||||
| /// | ||||
| part "account_service_extension.dart"; | ||||
| 
 | ||||
| part "account_auth_extension.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; | ||||
| 
 | ||||
|   // final FirestoreService firestoreService; | ||||
| 
 | ||||
|   Timer? retryTimer; | ||||
| 
 | ||||
|   static AccountManager instance = AccountManager(); | ||||
| 
 | ||||
|   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); | ||||
|   } | ||||
| 
 | ||||
|   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); | ||||
| 
 | ||||
|     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 authenticate().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; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,151 @@ | |||
| /// Created by Haoyi on 6/3/21 | ||||
| 
 | ||||
| part of "account_manager.dart"; | ||||
| 
 | ||||
| extension AccountServiceExtension on AccountManager { | ||||
|   Future<bool> _restoreAccount(Account account) async { | ||||
|     SaasUser? saasUser = account.saasUser; | ||||
|     Log.d("restoreAccount $saasUser", tag: "Account"); | ||||
|     saasUser ??= await signInWithAnonymous().catchError((error, stacktrace) { | ||||
|       Log.v("signInWithAnonymous error:$error, $stacktrace"); | ||||
|       return null; | ||||
|     }); | ||||
| 
 | ||||
|     Log.v("_restoreAccount saasUser:$saasUser", tag: "Account"); | ||||
|     final device = account.device; | ||||
|     if (device != null) { | ||||
|       _updateDevice(device); | ||||
|     } | ||||
| 
 | ||||
|     final accountProfile = account.accountProfile; | ||||
|     if (accountProfile != null) { | ||||
|       _updateAccountProfile(accountProfile); | ||||
|     } | ||||
| 
 | ||||
|     if (saasUser != null) { | ||||
|       _updateSaasUser(saasUser); | ||||
|       await _verifyOrReportAuthDevice(saasUser); | ||||
|       final auth = await authenticate(); | ||||
|       if (auth == null) { | ||||
|         return false; | ||||
|       } | ||||
|       if (accountProfile != null) { | ||||
|         await _checkOrUploadAccountProfile(accountProfile); | ||||
|       } | ||||
|       return true; | ||||
|     } else { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<AccountAuth?> authenticate() async { | ||||
|     final saasUser = accountDataStore.user; | ||||
|     if (saasUser == null) { | ||||
|       return null; | ||||
|     } | ||||
|     try { | ||||
|       final auth = await _authenticate(saasUser); | ||||
|       final newSaasUser = auth.user; | ||||
|       if (newSaasUser != null && !saasUser.isSame(newSaasUser)) { | ||||
|         _updateSaasUser(newSaasUser); | ||||
|       } | ||||
|       if (auth.firebaseUser != null) { | ||||
|         _updateFirebaseUser(auth.firebaseUser!); | ||||
|         Log.i("_updateFirebaseUser success!", tag: "Account"); | ||||
|       } | ||||
|       return auth; | ||||
|     } catch (error, stacktrace) { | ||||
|       GuruAnalytics.instance.logException(error, stacktrace: stacktrace); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   Future<DeviceTrack> _buildDevice(SaasUser 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<SaasUser?> signInWithAnonymous() async { | ||||
|     final anonymousSecretKey = await AppProperty.getInstance().getAnonymousSecretKey(); | ||||
|     return GuruApi.instance.signInWithAnonymous(secret: anonymousSecretKey); | ||||
|   } | ||||
| 
 | ||||
|   Future _verifyOrReportAuthDevice(SaasUser saasUser) async { | ||||
|     final deviceTrack = await _buildDevice(saasUser); | ||||
|     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 && saasUser.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); | ||||
|   } | ||||
| 
 | ||||
|   void _updateSaasUser(SaasUser saasUser) { | ||||
|     accountDataStore.updateSaasUser(saasUser); | ||||
|     AppProperty.getInstance().setAccountSaasUser(saasUser); | ||||
|     GuruAnalytics.instance.setUserId(saasUser.uid); | ||||
|   } | ||||
| 
 | ||||
|   void _updateFirebaseUser(User user) { | ||||
|     accountDataStore.updateFirebaseUser(user); | ||||
|   } | ||||
| 
 | ||||
|   void _updateAccountProfile(AccountProfile accountProfile) { | ||||
|     accountDataStore.updateAccountProfile(accountProfile); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,40 @@ | |||
| import 'package:firebase_auth/firebase_auth.dart'; | ||||
| import 'package:guru_app/account/model/account_profile.dart'; | ||||
| import 'package:guru_app/account/model/user.dart'; | ||||
| import 'package:guru_utils/device/device_info.dart'; | ||||
| 
 | ||||
| 
 | ||||
| /// Created by Haoyi on 6/3/21 | ||||
| 
 | ||||
| class Account { | ||||
|   final SaasUser? saasUser; | ||||
|   final DeviceInfo? device; | ||||
|   final AccountProfile? accountProfile; | ||||
|   final User? firebaseUser; | ||||
| 
 | ||||
|   String? get uid => saasUser?.uid; | ||||
| 
 | ||||
|   String? get nickname => accountProfile?.nickname; | ||||
| 
 | ||||
|   Account.restore({this.saasUser, this.device, this.accountProfile, this.firebaseUser}); | ||||
| } | ||||
| 
 | ||||
| class AccountAuth { | ||||
|   final SaasUser? user; | ||||
|   final User? firebaseUser; | ||||
| 
 | ||||
|   AccountAuth(this.user, this.firebaseUser); | ||||
| 
 | ||||
|   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, firebaseUser: $firebaseUser}'; | ||||
|   } | ||||
| } | ||||
|  | @ -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}'; | ||||
|   } | ||||
| } | ||||
|  | @ -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, | ||||
|     }; | ||||
|  | @ -0,0 +1,99 @@ | |||
| import 'package:json_annotation/json_annotation.dart'; | ||||
| 
 | ||||
| /// Created by Haoyi on 6/3/21 | ||||
| 
 | ||||
| part 'user.g.dart'; | ||||
| 
 | ||||
| @JsonSerializable() | ||||
| class SaasUser { | ||||
|   @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); | ||||
| 
 | ||||
|   SaasUser( | ||||
|       {required this.uid, | ||||
|       required this.token, | ||||
|       required this.firebaseToken, | ||||
|       this.createAtTimestamp = 0}); | ||||
| 
 | ||||
|   factory SaasUser.fromJson(Map<String, dynamic> json) => _$SaasUserFromJson(json); | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() => _$SaasUserToJson(this); | ||||
| 
 | ||||
|   SaasUser copyWith({String? firebaseToken, String? token}) { | ||||
|     return SaasUser( | ||||
|         uid: uid, token: token ?? this.token, firebaseToken: firebaseToken ?? this.firebaseToken); | ||||
|   } | ||||
| 
 | ||||
|   bool isSame(SaasUser? 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 FirebaseTokenData { | ||||
|   @JsonKey(name: 'uid', defaultValue: "") | ||||
|   final String uid; | ||||
| 
 | ||||
|   @JsonKey(name: 'firebaseToken', defaultValue: "") | ||||
|   final String firebaseToken; | ||||
| 
 | ||||
|   FirebaseTokenData({required this.uid, required 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}'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class UserAttr { | ||||
|   static const real = 0; | ||||
|   static const tester = 10; | ||||
|   static const machine = 100; | ||||
| } | ||||
|  | @ -0,0 +1,45 @@ | |||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'user.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| SaasUser _$SaasUserFromJson(Map<String, dynamic> json) => SaasUser( | ||||
|       uid: json['uid'] as String? ?? '', | ||||
|       token: json['token'] as String? ?? '', | ||||
|       firebaseToken: json['firebaseToken'] as String? ?? '', | ||||
|       createAtTimestamp: json['createdAtTimestamp'] as int? ?? 0, | ||||
|     ); | ||||
| 
 | ||||
| Map<String, dynamic> _$SaasUserToJson(SaasUser instance) => <String, dynamic>{ | ||||
|       '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, | ||||
|     }; | ||||
| 
 | ||||
| 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, | ||||
|     }; | ||||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,636 @@ | |||
| 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/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<ConnectivityResult> connectivityStatusSubject = | ||||
|       BehaviorSubject.seeded(ConnectivityResult.none); | ||||
| 
 | ||||
|   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; | ||||
| 
 | ||||
|   Stream<ConnectivityResult> get observableConnectivityStatus => connectivityStatusSubject.stream; | ||||
| 
 | ||||
|   @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; | ||||
| 
 | ||||
|   ConnectivityResult get connectivityStatus => connectivityStatusSubject.value; | ||||
| 
 | ||||
|   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); | ||||
|         } | ||||
|       } | ||||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   Future initialize({SaasUser? saasUser}) async { | ||||
|     _adsProfileSubject.addEx(GuruApp.instance.adsProfile); | ||||
|     await initEnv(); | ||||
|     await initSdk( | ||||
|         saasUser: saasUser, | ||||
|         onInitialized: () { | ||||
|           // loadAds(); | ||||
|           adImpressionController.init(); | ||||
|           initLifecycleConnectivity(); | ||||
|           checkAndPreload(); | ||||
|           // GuruSettings.instance.totalLevelUp | ||||
|           //     .observe() | ||||
|           //     .throttleTime(const Duration(seconds: 1)) | ||||
|           //     .listen((count) { | ||||
|           //   checkAndPreload(); | ||||
|           // }); | ||||
| 
 | ||||
|           Log.i("ADS Initialized", tag: "Ads", syncFirebase: true); | ||||
|         }); | ||||
| 
 | ||||
|     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}) { | ||||
|     Log.d("requestGdpr! debugGeography:$debugGeography testDeviceId:$testDeviceId", tag: "Ads"); | ||||
|     // adb logcat -s UserMessagingPlatform | ||||
|     // Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("xxxx") to set this as a debug device. | ||||
|     return GuruApplovinFlutter.instance | ||||
|         .requestGdpr(debugGeography: debugGeography, testDeviceId: testDeviceId); | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> resetGdpr() { | ||||
|     return GuruApplovinFlutter.instance.resetGdpr(); | ||||
|   } | ||||
| 
 | ||||
|   @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 _adsProfile = adsProfile; | ||||
|     Ads? ad = interstitialAds[_adsProfile.interstitialId]; | ||||
|     int hiddenAt = 0; | ||||
|     if (ad is AdsAudit) { | ||||
|       hiddenAt = ad.latestHiddenAt; | ||||
|     } | ||||
| 
 | ||||
|     final now = DateTimeUtils.currentTimeInMillis(); | ||||
|     final impGapInMillis = | ||||
|         AdsManager.instance.adsConfig.interstitialConfig.getSceneImpGapInSeconds(scene) * 1000; | ||||
|     Log.d( | ||||
|         "canShowInterstitial($scene): now:$now latestFullscreenAdsHiddenTimestamps:$latestFullscreenAdsHiddenTimestamps hiddenAt:$hiddenAt impGapInMillis:$impGapInMillis", | ||||
|         tag: "Ads"); | ||||
|     if ((now - AdsManager.instance.latestFullscreenAdsHiddenTimestamps) < 60000 || | ||||
|         ((now - hiddenAt) < impGapInMillis)) { | ||||
|       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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -0,0 +1,481 @@ | |||
| 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: {"new_block": 120, "reset_scs": 120}) | ||||
|   @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", defaultValue: 120) | ||||
|   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; | ||||
|   } | ||||
| 
 | ||||
|   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); | ||||
| } | ||||
|  | @ -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 | ||||
|           ? {'new_block': 120, 'reset_scs': 120} | ||||
|           : configStringIntMapStringConvert | ||||
|               .fromJson(json['sp_scene'] as String), | ||||
|       impGapInSeconds: json['imp_gap_s'] as int? ?? 120, | ||||
|     ); | ||||
| 
 | ||||
| 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, | ||||
|     }; | ||||
|  | @ -0,0 +1,189 @@ | |||
| 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/property/app_property.dart'; | ||||
| import 'package:guru_app/property/property_keys.dart'; | ||||
| import 'package:guru_utils/collection/collectionutils.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); | ||||
|             // 判断是不是facebook的network | ||||
|             // if (impressionData.networkName == "undisclosed") { | ||||
|             //   final calibrationCpm = await facebookCalibrator.getCpm(impressionData.unitFormat, impressionData.country); | ||||
|             //   if (calibrationCpm > 0) { | ||||
|             //     final newImpressionData = impressionData.derive(newPublisherRevenue: calibrationCpm); | ||||
|             //     if (facebookCalibrator.config?.fbIrldReport == true) { | ||||
|             //       AnalyticsUtils.logEventEx("tch_fb_ad_rev", value: calibrationCpm, parameters: { | ||||
|             //         FirebaseEventsParams.AD_FORMAT: impressionData.unitFormat, | ||||
|             //         FirebaseEventsParams.AD_UNIT_NAME: impressionData.unitName, | ||||
|             //         FirebaseEventsParams.CURRENCY: impressionData.currency, | ||||
|             //         "country": impressionData.country, | ||||
|             //         "mopub_rev": impressionData.publisherRevenue | ||||
|             //       }); | ||||
|             //     } | ||||
|             // | ||||
|             //     impressionData = newImpressionData; | ||||
|             //   } | ||||
|             // } | ||||
|             await refreshLtv(impressionData); | ||||
|             // _reportAdImpression(arguments); | ||||
| 
 | ||||
|             final jsonPayload = jsonEncode(impressionData.payload); | ||||
|             latestImpressionPayload = jsonPayload; | ||||
|             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); | ||||
|       // _logAdLtv(revenue: revenue, adPlatform: adPlatform, currency: currency); | ||||
|     } | ||||
|     Log.d("refreshLtv payload:${impressionData.payload}"); | ||||
|   } | ||||
| 
 | ||||
|   // _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); | ||||
|       GuruAnalytics.instance.logPurchase(totalRevenue, | ||||
|           currency: data.currency, contentId: "MAX", adPlatform: "MAX"); | ||||
| 
 | ||||
|       totalRevenue = .0; | ||||
|     } | ||||
|     appProperty.setDouble(PropertyKeys.totalRevenue, totalRevenue); | ||||
|   } | ||||
| } | ||||
|  | @ -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()); | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -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}'; | ||||
|   } | ||||
| } | ||||
|  | @ -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(); | ||||
|   } | ||||
| } | ||||
|  | @ -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"); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -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()); | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -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()); | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -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)); | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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"}]}}'; | ||||
| } | ||||
|  | @ -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}'; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,2 @@ | |||
| import "package:guru_utils/aigc/bi/ai_bi.dart"; | ||||
| export "package:guru_utils/aigc/bi/ai_bi.dart"; | ||||
|  | @ -0,0 +1,74 @@ | |||
| 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 { | ||||
|   @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; | ||||
| 
 | ||||
|   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); | ||||
|   } | ||||
| 
 | ||||
|   AnalyticsConfig(this.capabilities, this.delayedInSeconds, this.expiredInDays, this.strategy, | ||||
|       this.enabledStrategy); | ||||
| 
 | ||||
|   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}'; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,43 @@ | |||
| // 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, | ||||
|     ); | ||||
| 
 | ||||
| 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, | ||||
|     }; | ||||
| 
 | ||||
| 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, | ||||
|     }; | ||||
|  | @ -0,0 +1,599 @@ | |||
| /// Created by Haoyi on 2022/8/24 | ||||
| 
 | ||||
| import 'dart:collection'; | ||||
| import 'dart:core'; | ||||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:firebase_crashlytics/firebase_crashlytics.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/data/analytics_model.dart'; | ||||
| import 'package:guru_app/analytics/strategy/guru_analytics_strategy.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_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 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); | ||||
| 
 | ||||
|   Stream<GuruStatistic> get observableGuruEventStatistic => guruEventStatistic.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]; | ||||
|   } | ||||
| 
 | ||||
|   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(); | ||||
|         Log.d("register transmitter"); | ||||
|       }); | ||||
|       initialized = true; | ||||
|       // if (Platform.isAndroid) { | ||||
|       //   _logPeerApps(); | ||||
|       // } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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!); | ||||
|     } | ||||
| 
 | ||||
|     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()); | ||||
|   } | ||||
| 
 | ||||
|   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); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void setUserId(String userId) { | ||||
|     Log.d("setUserId: $userId"); | ||||
|     recordEvents("setUserId", {"userId": userId}); | ||||
|     recordProperty("userId", userId); | ||||
|     if (userId.isNotEmpty) { | ||||
|       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) { | ||||
|       // 自打点中。idfa变是adId | ||||
|       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>{}}) { | ||||
|     EventLogger.logFbPurchase(amount, | ||||
|         currency: currency, | ||||
|         contentId: contentId, | ||||
|         adPlatform: adPlatform, | ||||
|         additionParameters: parameters); | ||||
|   } | ||||
| 
 | ||||
|   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 = ''}) { | ||||
|     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 | ||||
|       }; | ||||
|       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, | ||||
|   }) async { | ||||
|     logEvent("earn_virtual_currency", <String, dynamic>{ | ||||
|       "virtual_currency_name": virtualCurrencyName, | ||||
|       "item_category": method, | ||||
|       "value": value, | ||||
|       "balance": balance | ||||
|     }); | ||||
|     AiBi.instance.earnVirtualCurrency(balance, value.toDouble(), method); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> setGuruUserProperty(String key, String value) async { | ||||
|     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); | ||||
|   } | ||||
| } | ||||
|  | @ -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"); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // 接入Adjust后trackAdRevenueNew要放开 | ||||
|   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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,61 @@ | |||
| // /// 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) { | ||||
|     // logEventEx(name, itemCategory: scene, itemName: adName); | ||||
|     if (release) { | ||||
|       EventLogger.logAdRevenue(adRevenue, adPlatform, currency); | ||||
|     } else { | ||||
|       Log.d("[firebase] logAdRevenue ${<String, dynamic>{ | ||||
|         "adRevenue": adRevenue, | ||||
|         "adPlatform": adPlatform, | ||||
|         "currency": currency | ||||
|       }}"); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
| 
 | ||||
|     // 如果excluded不为空,证明存在排除选项,该validate将只判断所有excluded中的逻辑, | ||||
|     // 将不会在判断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; | ||||
|       // 如果remoteAnalyticsStrategy非空表示云控配置了strategy | ||||
|       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做回滚的原因, | ||||
|             // 主要是考虑到上一次的云端strategy可能没有本地的strategy可靠, | ||||
|             // SDK假设本地的strategy比Firebase Storage中配置的strategy更可靠 | ||||
|             // 因此这里在出现下载异常的情况下,会回滚到本地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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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()); | ||||
| } | ||||
|  | @ -0,0 +1,138 @@ | |||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:guru_app/analytics/data/analytics_model.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; | ||||
| 
 | ||||
|   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}); | ||||
| 
 | ||||
|   @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"); | ||||
|     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}'; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,71 @@ | |||
| // 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?, | ||||
|     ); | ||||
| 
 | ||||
| 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, | ||||
|     }; | ||||
| 
 | ||||
| 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, | ||||
|     }; | ||||
|  | @ -0,0 +1,158 @@ | |||
| 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/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<SaasUser> signInWithAnonymous(@Body() AnonymousLoginReqBody body); | ||||
| 
 | ||||
|   @POST("/auth/api/v1/renewals/token") | ||||
|   Future<SaasUser> 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); | ||||
| } | ||||
|  | @ -0,0 +1,215 @@ | |||
| // 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<SaasUser> signInWithAnonymous(AnonymousLoginReqBody body) async { | ||||
|     const _extra = <String, dynamic>{}; | ||||
|     final queryParameters = <String, dynamic>{}; | ||||
|     final _headers = <String, dynamic>{}; | ||||
|     final _data = <String, dynamic>{}; | ||||
|     _data.addAll(body.toJson()); | ||||
|     final _result = | ||||
|         await _dio.fetch<Map<String, dynamic>>(_setStreamType<SaasUser>(Options( | ||||
|       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 = SaasUser.fromJson(_result.data!); | ||||
|     return value; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Future<SaasUser> refreshSaasToken() async { | ||||
|     const _extra = <String, dynamic>{}; | ||||
|     final queryParameters = <String, dynamic>{}; | ||||
|     final _headers = <String, dynamic>{}; | ||||
|     final Map<String, dynamic>? _data = null; | ||||
|     final _result = | ||||
|         await _dio.fetch<Map<String, dynamic>>(_setStreamType<SaasUser>(Options( | ||||
|       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 = SaasUser.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(); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,25 @@ | |||
| /// Created by Haoyi on 6/4/21 | ||||
| /// | ||||
| part of "../guru_api.dart"; | ||||
| 
 | ||||
| extension GuruApiExtension on GuruApi { | ||||
|   Future<SaasUser> signInWithAnonymous({required String secret}) async { | ||||
|     return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: secret)); | ||||
|   } | ||||
| 
 | ||||
|   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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,184 @@ | |||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:guru_app/firebase/messaging/remote_messaging_manager.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; | ||||
| 
 | ||||
|   @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; | ||||
| 
 | ||||
|   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}); | ||||
| 
 | ||||
|   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; | ||||
| 
 | ||||
|   RemoteDeployment({this.keepScreenOnDuration = 0}); | ||||
| 
 | ||||
|   factory RemoteDeployment.fromJson(Map<String, dynamic> json) => _$RemoteDeploymentFromJson(json); | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() => _$RemoteDeploymentToJson(this); | ||||
| } | ||||
|  | @ -0,0 +1,131 @@ | |||
| // 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, | ||||
|     ); | ||||
| 
 | ||||
| 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, | ||||
|     }; | ||||
| 
 | ||||
| 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, | ||||
|     ); | ||||
| 
 | ||||
| Map<String, dynamic> _$RemoteDeploymentToJson(RemoteDeployment instance) => | ||||
|     <String, dynamic>{ | ||||
|       'keep_screen_on_duration_m': instance.keepScreenOnDuration, | ||||
|     }; | ||||
|  | @ -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)); | ||||
| //   // } | ||||
| // } | ||||
|  | @ -0,0 +1,87 @@ | |||
| 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/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/test/test_guru_app_creator.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; | ||||
| 
 | ||||
|   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<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 { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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) { | ||||
| //   } | ||||
| // } | ||||
|  | @ -0,0 +1,13 @@ | |||
| import 'package:guru_app/financial/data/db/order_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]; | ||||
| 
 | ||||
| class Creators { | ||||
|   static final List<TableCreator> creators = [..._creatorV1, ..._creatorV2]; | ||||
| } | ||||
|  | @ -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 => 3; | ||||
| } | ||||
|  | @ -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(); | ||||
|  | @ -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 EXISTS,所以这里用try 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(); | ||||
|  | @ -0,0 +1,13 @@ | |||
| import 'package:guru_app/financial/data/db/order_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'; | ||||
| 
 | ||||
| /// Created by @Haoyi on 2020/5/22 | ||||
| /// | ||||
| 
 | ||||
| class Migrations { | ||||
|   static final migrations = [migration1to2, migration2to3]; | ||||
| } | ||||
|  | @ -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); | ||||
| } | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|  | @ -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}'; | ||||
|   } | ||||
| } | ||||
|  | @ -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, | ||||
|     }; | ||||
|  | @ -0,0 +1,59 @@ | |||
| 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/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(); | ||||
|     RewardManager.instance.init(); | ||||
|   } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -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 => ; | ||||
| } | ||||
|  | @ -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); | ||||
| } | ||||
|  | @ -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), | ||||
|     }; | ||||
|  | @ -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); | ||||
| //     } | ||||
| //   } | ||||
| // } | ||||
| } | ||||
|  | @ -0,0 +1,166 @@ | |||
| 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 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(); | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  | @ -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]; | ||||
| // } | ||||
|  | @ -0,0 +1,77 @@ | |||
| import 'dart:async'; | ||||
| 
 | ||||
| import 'package:guru_app/financial/igc/igc_manager.dart'; | ||||
| import 'package:guru_app/financial/product/product_model.dart'; | ||||
| import 'manifest.dart'; | ||||
| 
 | ||||
| /// Created by Haoyi on 2022/8/21 | ||||
| 
 | ||||
| typedef DetailsDistributor = Future<bool> 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<bool> _deliverIgcDetails( | ||||
|       Details details, TransactionMethod method, String scene) async { | ||||
|     if (details.amount > 0) { | ||||
|       IgcManager.instance.accumulate(details.amount, method, scene: scene); | ||||
|       return true; | ||||
|     } | ||||
|     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<bool> deliver(Manifest manifest, TransactionMethod method) async { | ||||
|     bool result = false; | ||||
|     for (var details in manifest.details) { | ||||
|       result |= await distributors[details.type]?.call(details, method, manifest.scene) ?? false; | ||||
|     } | ||||
|     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); | ||||
|   } | ||||
| } | ||||
|  | @ -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: ""); | ||||
| // } | ||||
|  | @ -0,0 +1,259 @@ | |||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:guru_app/financial/data/db/order_database.dart'; | ||||
| import 'package:guru_app/financial/manifest/manifest.dart'; | ||||
| import 'package:guru_app/financial/manifest/manifest_manager.dart'; | ||||
| import 'package:guru_app/guru_app.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 = 1; | ||||
|   static const asset = 1; | ||||
|   static const consumable = 2; | ||||
| 
 | ||||
|   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 CUSTOMER,但是新用户的判断是以APP的生命周期为标准, | ||||
|   // 没有购买过此商品组的新用户(默认选项) | ||||
|   newCustomerNeverHadSubscribedThisGroup, | ||||
| 
 | ||||
|   // 这里对应的是 GP 上的 NEW CUSTOMER,但是新用户的判断是以APP的生命周期为标准, | ||||
|   // 没有购买过此商品的新用户 | ||||
|   newCustomerNeverHadThisSubscription, | ||||
| 
 | ||||
|   // 这里对应的是 GP 上的 NEW CUSTOMER,但是新用户的判断是以APP的生命周期为标准, | ||||
|   // 没有购买过任何订阅商品的新用户 | ||||
|   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, | ||||
|       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); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 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.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 购买 | ||||
|   reward, // 奖励获得 | ||||
|   none | ||||
| } | ||||
| 
 | ||||
| String convertTransactionMethodName(TransactionMethod method) { | ||||
|   switch (method) { | ||||
|     case TransactionMethod.iap: | ||||
|       return "iap_buy"; | ||||
|     case TransactionMethod.igc: | ||||
|       return "igc"; | ||||
|     case TransactionMethod.reward: | ||||
|       return "reward"; | ||||
|     default: | ||||
|       return "none"; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class TransactionIntent { | ||||
|   final ProductId productId; | ||||
|   final int igcCost; | ||||
|   final String scene; // 购买场景(最终会用到logEarnVirtualCurrency的item_category上) | ||||
|   final bool sales; // 是否为促销商品 | ||||
|   final double rate; // 默认1.0 指收益率,如:打折促销时设成1.2,最终的收益将是1.2倍 | ||||
|   final EligibilityCriteria eligibilityCriteria; | ||||
| 
 | ||||
|   TransactionIntent(this.productId, this.scene, | ||||
|       {this.igcCost = 0, | ||||
|       this.sales = false, | ||||
|       this.rate = 1.0, | ||||
|       this.eligibilityCriteria = EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup}); | ||||
| } | ||||
|  | @ -0,0 +1,168 @@ | |||
| /// 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 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.none: | ||||
|         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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,69 @@ | |||
| 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 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; | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  | @ -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'; | ||||
| 
 | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  | @ -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._(); | ||||
| } | ||||
|  | @ -0,0 +1,453 @@ | |||
| import 'dart:async'; | ||||
| import 'dart:io'; | ||||
| import 'dart:ui'; | ||||
| 
 | ||||
| import 'package:firebase_core/firebase_core.dart'; | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:guru_app/account/account_manager.dart'; | ||||
| import 'package:guru_app/analytics/guru_analytics.dart'; | ||||
| import 'package:guru_app/guru_app.dart'; | ||||
| import 'package:guru_app/lifecycle/lifecycle_model.dart'; | ||||
| import 'package:guru_app/property/app_property.dart'; | ||||
| import 'package:guru_app/property/property_keys.dart'; | ||||
| import 'package:guru_utils/controller/lifecycle_controller.dart'; | ||||
| import 'package:guru_utils/lifecycle/lifecycle_manager.dart'; | ||||
| import 'package:guru_utils/router/router.dart'; | ||||
| import 'package:guru_utils/extensions/extensions.dart'; | ||||
| import 'package:guru_utils/log/log.dart'; | ||||
| import 'package:guru_utils/math/math_utils.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:json_annotation/json_annotation.dart'; | ||||
| 
 | ||||
| /// Created by Haoyi on 5/14/21 | ||||
| // | ||||
| Future<dynamic> _backgroundMessageHandler(RemoteMessage message) async { | ||||
|   Log.d("_backgroundMessageHandler:${message.data} ${message.data["uri"]}"); | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| enum RationaleResult { skip, allow } | ||||
| 
 | ||||
| enum PromptTrigger { | ||||
|   @JsonValue(0) | ||||
|   rationale, // 依赖Android原生的shouldShowRequestRationale返回值来展示对应的Rationale页面 | ||||
|   @JsonValue(1) | ||||
|   request // 依赖请求的次数来展示对应的Rationale页面 | ||||
| } | ||||
| 
 | ||||
| class RemoteMessagingManager { | ||||
|   static RemoteMessagingManager instance = RemoteMessagingManager._(); | ||||
| 
 | ||||
|   late FirebaseMessaging _firebaseMessaging; | ||||
| 
 | ||||
|   final BehaviorSubject<String?> fcmToken = BehaviorSubject.seeded(null); | ||||
| 
 | ||||
|   Stream<String?> get observableFCMToken => fcmToken.stream; | ||||
| 
 | ||||
|   RemoteMessagingManager._(); | ||||
| 
 | ||||
|   int _retryFetchTokenCount = 0; | ||||
| 
 | ||||
|   final _statusMap = { | ||||
|     AuthorizationStatus.authorized: "granted", | ||||
|     AuthorizationStatus.denied: "denied", | ||||
|     AuthorizationStatus.provisional: "provisional", | ||||
|     AuthorizationStatus.notDetermined: "not_determined" | ||||
|   }; | ||||
| 
 | ||||
|   String? _getUriLastSegment(String? uri) { | ||||
|     try { | ||||
|       return Uri.parse(uri ?? "").pathSegments.last; | ||||
|     } catch (error) { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void fetchToken({Completer<String>? completer}) async { | ||||
|     Log.d("Fetch FCMToken!!"); | ||||
|     String? token; | ||||
|     try { | ||||
|       token = await _firebaseMessaging.getToken(); | ||||
|       if (token != null) { | ||||
|         fcmToken.addEx(token); | ||||
|         // RuntimeProperty.instance.setString("firebase_push_token", token); | ||||
|         Log.d("### FCMToken :$token"); | ||||
|       } | ||||
|     } catch (error, stacktrace) { | ||||
|       Log.d("fetchToken error!", error: error, stackTrace: stacktrace); | ||||
|     } | ||||
|     if (token == null || token == '') { | ||||
|       final intervalSeconds = | ||||
|           (MathUtils.fibonacci(_retryFetchTokenCount) * 8).clamp(8, 600); | ||||
|       Future.delayed(Duration(seconds: intervalSeconds), () { | ||||
|         fetchToken(); | ||||
|       }); | ||||
|       _retryFetchTokenCount++; | ||||
|     } else { | ||||
|       _retryFetchTokenCount = 0; | ||||
|       completer?.complete(Future.value(token)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<String?> getToken() async { | ||||
|     final result = fcmToken.value ?? (await _firebaseMessaging.getToken()); | ||||
|     if (result != null && fcmToken.value == null) { | ||||
|       fcmToken.addEx(result); | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   void init() async { | ||||
|     _firebaseMessaging = FirebaseMessaging.instance; | ||||
|     final granted = await checkNotificationPermission(); | ||||
|     if (!granted) { | ||||
|       Future.delayed(const Duration(seconds: 8), () async { | ||||
|         if (GuruApp | ||||
|             .instance.appSpec.deployment.autoRequestNotificationPermission) { | ||||
|           Log.d("guru_app auto request notification permissions!"); | ||||
|           requestNotificationPermission(); | ||||
|         } else { | ||||
|           Log.d("guru_app check notification permissions!"); | ||||
|           final shouldShowRequestRationale = | ||||
|               await Permission.notification.shouldShowRequestRationale; | ||||
|           Log.d( | ||||
|               "guru_app post request notification permission event! shouldShowRequestRationale:$shouldShowRequestRationale"); | ||||
|           LifecycleManager.instance.postEvent( | ||||
|               RequestNotificationPermissionEvent( | ||||
|                   rationale: shouldShowRequestRationale)); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     FirebaseMessaging.instance.getInitialMessage().then((message) { | ||||
|       if (message != null) { | ||||
|         final uri = message.data["uri"]; | ||||
|         if (uri != null && uri is String && uri.isNotEmpty) { | ||||
|           Log.d("getInitialMessage:${message.data} $uri"); | ||||
|           RouteCenter.instance.dispatchUri(Uri.parse(uri)); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|     FirebaseMessaging.onMessage.listen((message) { | ||||
|       Log.d("onMessage:${message.data}"); | ||||
|       final data = message.data; | ||||
| 
 | ||||
|       // final notification = message.notification; | ||||
|       // if (Platform.isAndroid && notification != null) { | ||||
|       //   NotificationChannel.showNotification( | ||||
|       //       NotificationChannel.pushType, {"title": notification.title, "body": notification.body, "cmd": data["cmd"], "uri": data["uri"]}); | ||||
|       // } | ||||
|       GuruAnalytics.instance.logEventEx("push_receive", | ||||
|           itemCategory: data["cmd"], itemName: _getUriLastSegment(data["uri"])); | ||||
|     }); | ||||
| 
 | ||||
|     FirebaseMessaging.onMessageOpenedApp.listen((message) { | ||||
|       final uri = message.data["uri"]; | ||||
|       if (uri != null && uri is String && uri.isNotEmpty) { | ||||
|         Log.d("onMessageOpenApp:${message.data} ${message.data["uri"]}"); | ||||
|         RouteCenter.instance.dispatchUri(Uri.parse(message.data["uri"])); | ||||
|       } | ||||
|     }); | ||||
|     _firebaseMessaging.onTokenRefresh.listen((event) { | ||||
|       Log.d("onTokenRefresh $event"); | ||||
|       AccountManager.instance.refreshFcmToken(); | ||||
|       // Injector.provide<AccountService>().refreshFcmToken(); | ||||
|     }); | ||||
|     fetchToken(); | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> checkNotificationPermission() async { | ||||
|     final _map = { | ||||
|       AuthorizationStatus.authorized: "granted", | ||||
|       AuthorizationStatus.denied: "denied", | ||||
|       AuthorizationStatus.provisional: "provisional", | ||||
|       AuthorizationStatus.notDetermined: "not_determined" | ||||
|     }; | ||||
|     final notificationSettings = | ||||
|         await _firebaseMessaging.getNotificationSettings(); | ||||
|     final property = _map[notificationSettings.authorizationStatus]; | ||||
|     if (property != null) { | ||||
|       GuruAnalytics.instance.setUserProperty("noti_perm", property); | ||||
|     } else { | ||||
|       GuruAnalytics.instance.setUserProperty("noti_perm", "not_determined"); | ||||
|     } | ||||
|     return notificationSettings.authorizationStatus == | ||||
|         AuthorizationStatus.authorized; | ||||
|   } | ||||
| 
 | ||||
|   Future<AuthorizationStatus> getNotificationAuthorizationStatus() async { | ||||
|     final notificationSettings = | ||||
|         await _firebaseMessaging.getNotificationSettings(); | ||||
|     return notificationSettings.authorizationStatus; | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> isShouldShowRequestRationale() async { | ||||
|     if (GuruApp | ||||
|             .instance.appSpec.deployment.notificationPermissionPromptTrigger == | ||||
|         PromptTrigger.rationale) { | ||||
|       return await Permission.notification.shouldShowRequestRationale; | ||||
|     } | ||||
| 
 | ||||
|     if (await Permission.notification.isGranted) { | ||||
|       return false; | ||||
|     } | ||||
|     final permanentlyDenied = await Permission.notification.isPermanentlyDenied; | ||||
|     if (permanentlyDenied) { | ||||
|       return false; | ||||
|     } | ||||
|     int deniedTimes = await AppProperty.getInstance() | ||||
|         .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); | ||||
|     if (deniedTimes >= 2) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     final requestTimes = await AppProperty.getInstance() | ||||
|         .getInt(PropertyKeys.requestNotificationPermissionTimes, defValue: 0); | ||||
|     return requestTimes >= 1; | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> _requestNotificationPermissionForAndroid( | ||||
|       {String style = "default", | ||||
|       String scene = "", | ||||
|       Completer<RationaleResult> Function()? showRationale}) async { | ||||
|     final PromptTrigger promptTrigger = | ||||
|         GuruApp.instance.appSpec.deployment.notificationPermissionPromptTrigger; | ||||
| 
 | ||||
|     if (await Permission.notification.isGranted) { | ||||
|       GuruAnalytics.instance.setUserProperty("noti_perm", "granted"); | ||||
|       return true; | ||||
|     } else { | ||||
|       final permanentlyDenied = | ||||
|           await Permission.notification.isPermanentlyDenied; | ||||
|       if (permanentlyDenied) { | ||||
|         GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); | ||||
|         return false; | ||||
|       } | ||||
|       int deniedTimes = await AppProperty.getInstance() | ||||
|           .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); | ||||
|       if (deniedTimes >= 2) { | ||||
|         GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|       final promptTriggerValue = | ||||
|           promptTrigger == PromptTrigger.rationale ? "a" : "b"; | ||||
| 
 | ||||
|       final requestTimes = await AppProperty.getInstance().increaseAndGet( | ||||
|           PropertyKeys.requestNotificationPermissionTimes, | ||||
|           defValue: 0); | ||||
| 
 | ||||
|       final trackingNotificationPermissionPass = GuruApp | ||||
|               .instance.appSpec.deployment.trackingNotificationPermissionPass && | ||||
|           requestTimes < | ||||
|               (GuruApp.instance.appSpec.deployment | ||||
|                   .trackingNotificationPermissionPassLimitTimes); | ||||
| 
 | ||||
|       if (trackingNotificationPermissionPass) { | ||||
|         GuruAnalytics.instance.logEventEx("noti_perm_req_$requestTimes", | ||||
|             itemCategory: style, | ||||
|             itemName: scene, | ||||
|             parameters: { | ||||
|               "request_times": requestTimes, | ||||
|               "denied_times": deniedTimes, | ||||
|               "prompt_trigger": promptTriggerValue | ||||
|             }); | ||||
|       } | ||||
| 
 | ||||
|       final shouldShowRequestRationale = | ||||
|           await Permission.notification.shouldShowRequestRationale || | ||||
|               (promptTrigger == PromptTrigger.request && requestTimes > 1); | ||||
| 
 | ||||
|       Log.d( | ||||
|           "_requestNotificationPermission requestTimes:$requestTimes deniedTimes:$deniedTimes trackingNotificationPermissionPass:$trackingNotificationPermissionPass promptTrigger:$promptTrigger shouldShowRequestRationale:$shouldShowRequestRationale "); | ||||
| 
 | ||||
|       if (shouldShowRequestRationale && showRationale != null) { | ||||
|         GuruAnalytics.instance.logEventEx("noti_perm_rationale_imp", | ||||
|             itemCategory: style, itemName: scene); | ||||
|         RationaleResult rationaleResult = RationaleResult.skip; | ||||
|         try { | ||||
|           final completer = showRationale(); | ||||
|           rationaleResult = await completer.future; | ||||
|         } catch (error, stacktrace) { | ||||
|           Log.d("showRationale error!", error: error, stackTrace: stacktrace); | ||||
|         } | ||||
|         GuruAnalytics.instance.logEventEx("noti_perm_rationale_result", | ||||
|             itemCategory: style, | ||||
|             itemName: scene, | ||||
|             parameters: { | ||||
|               "result": | ||||
|                   rationaleResult == RationaleResult.allow ? "allow" : "skip", | ||||
|             }); | ||||
|         if (rationaleResult == RationaleResult.skip) { | ||||
|           return false; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       final showTimes = await AppProperty.getInstance().increaseAndGet( | ||||
|           PropertyKeys.showNotificationPermissionTimes, | ||||
|           defValue: 0); | ||||
| 
 | ||||
|       GuruAnalytics.instance.logEventEx("noti_perm_imp", | ||||
|           itemCategory: style, | ||||
|           itemName: scene, | ||||
|           parameters: { | ||||
|             "show_times": showTimes, | ||||
|             "request_times": requestTimes, | ||||
|             "denied_times": deniedTimes, | ||||
|             "prompt_trigger": promptTriggerValue | ||||
|           }); | ||||
|       final requestSettings = await _firebaseMessaging.requestPermission(); | ||||
|       final result = | ||||
|           _statusMap[requestSettings.authorizationStatus] ?? "not_determined"; | ||||
|       await GuruAnalytics.instance.setUserProperty("noti_perm", result); | ||||
| 
 | ||||
|       if (requestSettings.authorizationStatus != | ||||
|           AuthorizationStatus.authorized) { | ||||
|         final shouldShowRequestRationale2 = | ||||
|             await Permission.notification.shouldShowRequestRationale; | ||||
|         if (deniedTimes == 0 && shouldShowRequestRationale2) { | ||||
|           deniedTimes = 1; | ||||
|           await AppProperty.getInstance() | ||||
|               .setInt(PropertyKeys.deniedNotificationPermissionTimes, 1); | ||||
|         } else if (deniedTimes == 1 && | ||||
|             shouldShowRequestRationale != shouldShowRequestRationale2) { | ||||
|           deniedTimes = 2; | ||||
|           await AppProperty.getInstance() | ||||
|               .setInt(PropertyKeys.deniedNotificationPermissionTimes, 2); | ||||
|         } | ||||
|       } else { | ||||
|         if (trackingNotificationPermissionPass) { | ||||
|           GuruAnalytics.instance.logEventEx("noti_perm_pass_$requestTimes", | ||||
|               itemCategory: style, | ||||
|               itemName: scene, | ||||
|               parameters: { | ||||
|                 "show_times": showTimes, | ||||
|                 "request_times": requestTimes, | ||||
|                 "denied_times": deniedTimes, | ||||
|                 "prompt_trigger": promptTriggerValue | ||||
|               }); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       GuruAnalytics.instance.logEventEx("noti_perm_result", | ||||
|           itemCategory: style, | ||||
|           itemName: scene, | ||||
|           parameters: { | ||||
|             "result": result, | ||||
|             "show_times": showTimes, | ||||
|             "request_times": requestTimes, | ||||
|             "denied_times": deniedTimes, | ||||
|             "prompt_trigger": promptTriggerValue | ||||
|           }); | ||||
| 
 | ||||
|       Log.d( | ||||
|           "notificationSettings.authorizationStatus:${requestSettings.authorizationStatus} showTimes:$requestTimes deniedTimes:$deniedTimes promptTrigger: $promptTrigger"); | ||||
|       return requestSettings.authorizationStatus == | ||||
|           AuthorizationStatus.authorized; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> _requestNotificationPermissionForIOS( | ||||
|       {String style = "default", String scene = ""}) async { | ||||
|     final status = await getNotificationAuthorizationStatus(); | ||||
|     switch (status) { | ||||
|       case AuthorizationStatus.authorized: | ||||
|         GuruAnalytics.instance.setUserProperty("noti_perm", "granted"); | ||||
|         return true; | ||||
|       case AuthorizationStatus.provisional: | ||||
|         GuruAnalytics.instance.setUserProperty("noti_perm", "provisional"); | ||||
|         return true; | ||||
|       case AuthorizationStatus.denied: | ||||
|         GuruAnalytics.instance.setUserProperty("noti_perm", "denied"); | ||||
|         return false; | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     final trackingNotificationPermissionPass = | ||||
|         GuruApp.instance.appSpec.deployment.trackingNotificationPermissionPass; | ||||
| 
 | ||||
|     int deniedTimes = await AppProperty.getInstance() | ||||
|         .getInt(PropertyKeys.deniedNotificationPermissionTimes, defValue: 0); | ||||
| 
 | ||||
|     final requestTimes = await AppProperty.getInstance().increaseAndGet( | ||||
|         PropertyKeys.requestNotificationPermissionTimes, | ||||
|         defValue: 0); | ||||
|     final showTimes = await AppProperty.getInstance().increaseAndGet( | ||||
|         PropertyKeys.showNotificationPermissionTimes, | ||||
|         defValue: 0); | ||||
| 
 | ||||
|     if (trackingNotificationPermissionPass) { | ||||
|       GuruAnalytics.instance.logEventEx("noti_perm_req_$requestTimes", | ||||
|           itemCategory: style, | ||||
|           itemName: scene, | ||||
|           parameters: { | ||||
|             "request_times": requestTimes, | ||||
|             "denied_times": deniedTimes, | ||||
|             "prompt_trigger": "a" | ||||
|           }); | ||||
|     } | ||||
| 
 | ||||
|     final requestSettings = await _firebaseMessaging.requestPermission(); | ||||
|     final result = | ||||
|         _statusMap[requestSettings.authorizationStatus] ?? "not_determined"; | ||||
|     await GuruAnalytics.instance.setUserProperty("noti_perm", result); | ||||
| 
 | ||||
|     if (requestSettings.authorizationStatus != AuthorizationStatus.authorized) { | ||||
|       deniedTimes += 1; | ||||
|     } else { | ||||
|       if (trackingNotificationPermissionPass) { | ||||
|         GuruAnalytics.instance.logEventEx("noti_perm_pass_$requestTimes", | ||||
|             itemCategory: style, | ||||
|             itemName: scene, | ||||
|             parameters: { | ||||
|               "show_times": showTimes, | ||||
|               "request_times": requestTimes, | ||||
|               "denied_times": deniedTimes, | ||||
|               "prompt_trigger": "a" | ||||
|             }); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     GuruAnalytics.instance.logEventEx("noti_perm_result", | ||||
|         itemCategory: style, | ||||
|         itemName: scene, | ||||
|         parameters: { | ||||
|           "result": result, | ||||
|           "show_times": showTimes, | ||||
|           "request_times": requestTimes, | ||||
|           "denied_times": deniedTimes, | ||||
|           "prompt_trigger": "a" | ||||
|         }); | ||||
|     Log.d( | ||||
|         "notificationSettings.authorizationStatus:${requestSettings.authorizationStatus} showTimes:$requestTimes deniedTimes:$deniedTimes"); | ||||
|     return requestSettings.authorizationStatus == | ||||
|         AuthorizationStatus.authorized; | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> requestNotificationPermission( | ||||
|       {String style = "default", | ||||
|       String scene = "", | ||||
|       Completer<RationaleResult> Function()? showRationale}) async { | ||||
|     if (Platform.isAndroid) { | ||||
|       return _requestNotificationPermissionForAndroid( | ||||
|           style: style, scene: scene, showRationale: showRationale); | ||||
|     } else if (Platform.isIOS) { | ||||
|       return _requestNotificationPermissionForIOS(style: style, scene: scene); | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   void saveTokenToClipboard() { | ||||
|     final token = fcmToken.value; | ||||
|     if (token != null) { | ||||
|       Clipboard.setData(ClipboardData(text: token)); | ||||
|       Log.d("saveTokenToClipboard:$token"); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void dispose() { | ||||
|     fcmToken.close(); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,112 @@ | |||
| /// Created by Haoyi on 2022/2/26 | ||||
| 
 | ||||
| part of "remote_config_manager.dart"; | ||||
| 
 | ||||
| extension RemoteConfigInterface on RemoteConfigManager { | ||||
|   bool isIOSReview() { | ||||
|     if (Platform.isIOS) { | ||||
|       return getBool(RemoteConfigReservedConstants.iosReviewVersion, defaultValue: true) ?? true; | ||||
|     } else { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   CdnConfig getCdnConfig() { | ||||
|     final cdnConfigStr = getString(RemoteConfigReservedConstants.cdnConfig); | ||||
|     if (cdnConfigStr != null && cdnConfigStr.isNotEmpty) { | ||||
|       return CdnConfig.fromJson( | ||||
|         jsonDecode(cdnConfigStr), | ||||
|         defaultStoragePrefix: GuruApp.instance.details.storagePrefix, | ||||
|         defaultCdnPrefix: GuruApp.instance.details.defaultCdnPrefix, | ||||
|       ); | ||||
|     } | ||||
|     return CdnConfig.fromJson( | ||||
|       {}, | ||||
|       defaultStoragePrefix: GuruApp.instance.details.storagePrefix, | ||||
|       defaultCdnPrefix: GuruApp.instance.details.defaultCdnPrefix, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   TaichiConfig? getTaichiConfig() { | ||||
|     final taichiStr = | ||||
|         RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.taichiConfig); | ||||
|     if (taichiStr != null && taichiStr.isNotEmpty) { | ||||
|       return TaichiConfig.fromJson(jsonDecode(taichiStr)); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   AdInterstitialConfig getIadsConfig() { | ||||
|     final iadsConfigStr = | ||||
|         RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.iadsConfig); | ||||
|     if (iadsConfigStr != null && iadsConfigStr.isNotEmpty) { | ||||
|       return AdInterstitialConfig.fromJson(jsonDecode(iadsConfigStr)); | ||||
|     } | ||||
|     return AdInterstitialConfig.fromJson({}); | ||||
|   } | ||||
| 
 | ||||
|   AdRewardedConfig getRadsConfig() { | ||||
|     final radsConfigStr = | ||||
|         RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.radsConfig); | ||||
|     if (radsConfigStr != null && radsConfigStr.isNotEmpty) { | ||||
|       return AdRewardedConfig.fromJson(jsonDecode(radsConfigStr)); | ||||
|     } | ||||
|     return AdRewardedConfig.fromJson({}); | ||||
|   } | ||||
| 
 | ||||
|   AdBannerConfig getBadsConfig() { | ||||
|     final badsConfigStr = | ||||
|         RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.badsConfig); | ||||
|     if (badsConfigStr != null && badsConfigStr.isNotEmpty) { | ||||
|       return AdBannerConfig.fromJson(jsonDecode(badsConfigStr)); | ||||
|     } | ||||
|     return AdBannerConfig.fromJson({}); | ||||
|   } | ||||
| 
 | ||||
|   StrategyAdsConfig getStrategyAdsConfig() { | ||||
|     final sadsConfigStr = | ||||
|         RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.sadsConfig); | ||||
|     if (sadsConfigStr != null && sadsConfigStr.isNotEmpty) { | ||||
|       return StrategyAdsConfig.fromJson(jsonDecode(sadsConfigStr)); | ||||
|     } | ||||
|     return StrategyAdsConfig.fromJson({}); | ||||
|   } | ||||
| 
 | ||||
|   IOSAttConfig getIOSAttConfig() { | ||||
|     final iosAttConfigString = | ||||
|         RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.iosAttConfig); | ||||
| 
 | ||||
|     if (iosAttConfigString != null && iosAttConfigString.isNotEmpty) { | ||||
|       return IOSAttConfig.fromJson(jsonDecode(iosAttConfigString)); | ||||
|     } | ||||
|     return IOSAttConfig.fromJson({}); | ||||
|   } | ||||
| 
 | ||||
|   CommonAdsConfig getCommonAdsConfig() { | ||||
|     final commonAdsConfigString = | ||||
|         RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.commonAdsConfig); | ||||
| 
 | ||||
|     if (commonAdsConfigString != null && commonAdsConfigString.isNotEmpty) { | ||||
|       return CommonAdsConfig.fromJson(jsonDecode(commonAdsConfigString)); | ||||
|     } | ||||
|     return CommonAdsConfig.fromJson({}); | ||||
|   } | ||||
| 
 | ||||
|   AnalyticsConfig getAnalyticsConfig() { | ||||
|     final analyticsConfigStr = | ||||
|         RemoteConfigUtils.instance.getString(RemoteConfigReservedConstants.analyticsConfig); | ||||
|     if (analyticsConfigStr != null && analyticsConfigStr.isNotEmpty) { | ||||
|       return AnalyticsConfig.fromJson(jsonDecode(analyticsConfigStr)); | ||||
|     } | ||||
|     return AnalyticsConfig.fromJson({}); | ||||
|   } | ||||
| 
 | ||||
|   RemoteDeployment getRemoteDeployment() { | ||||
|     final deploymentConfigStr = | ||||
|         RemoteConfigUtils.instance.getString(RemoteConfigReservedConstants.deploymentConfig); | ||||
|     if (deploymentConfigStr != null && deploymentConfigStr.isNotEmpty) { | ||||
|       return RemoteDeployment.fromJson(jsonDecode(deploymentConfigStr)); | ||||
|     } | ||||
|     return RemoteDeployment.fromJson({}); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,209 @@ | |||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:firebase_remote_config/firebase_remote_config.dart'; | ||||
| import 'package:guru_app/ads/core/ads_config.dart'; | ||||
| import 'package:guru_app/analytics/data/analytics_model.dart'; | ||||
| import 'package:guru_app/firebase/remoteconfig/reserved_remote_config_models.dart'; | ||||
| import 'package:guru_app/guru_app.dart'; | ||||
| import 'package:guru_utils/http/http_model.dart'; | ||||
| import 'package:guru_utils/log/log.dart'; | ||||
| import 'package:rxdart/rxdart.dart'; | ||||
| import 'package:guru_utils/remote/remote_config.dart'; | ||||
| 
 | ||||
| part "remote_config_interface.dart"; | ||||
| 
 | ||||
| /// Created by Haoyi on 2020/4/22 | ||||
| /// | ||||
| part "remote_config_reserved_constants.dart"; | ||||
| 
 | ||||
| class RemoteConfigManager extends IRemoteConfig { | ||||
|   final BehaviorSubject<FirebaseRemoteConfig?> _subject = | ||||
|       BehaviorSubject.seeded(null); | ||||
|   static RemoteConfigManager? _instance; | ||||
| 
 | ||||
|   static RemoteConfigManager _getInstance() { | ||||
|     _instance ??= RemoteConfigManager._internal(); | ||||
|     return _instance!; | ||||
|   } | ||||
| 
 | ||||
|   factory RemoteConfigManager() => _getInstance(); | ||||
| 
 | ||||
|   static RemoteConfigManager get instance => _getInstance(); | ||||
| 
 | ||||
|   static final RegExp _invalidABKey = RegExp('[^a-zA-Z0-9_-]'); | ||||
| 
 | ||||
|   RemoteConfigManager._internal(); | ||||
| 
 | ||||
|   Future<void> init(Map<String, dynamic> defaultConfigs) async { | ||||
|     final remoteConfig = FirebaseRemoteConfig.instance; | ||||
|     await remoteConfig.setConfigSettings(RemoteConfigSettings( | ||||
|       fetchTimeout: const Duration(seconds: 15), | ||||
|       minimumFetchInterval: const Duration(hours: 2), | ||||
|     )); | ||||
| 
 | ||||
|     _subject.add(remoteConfig); | ||||
| 
 | ||||
|     try { | ||||
|       await remoteConfig.setDefaults(defaultConfigs); | ||||
|       await remoteConfig.activate(); | ||||
|     } catch (exception) { | ||||
|       Log.d( | ||||
|           "Unable to fetch remote config. Cached or default values will be used!", | ||||
|           error: exception); | ||||
|     } finally { | ||||
|       _subject.add(remoteConfig); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future fetchAndActivate() async { | ||||
|     final remoteConfig = FirebaseRemoteConfig.instance; | ||||
|     try { | ||||
|       await remoteConfig.fetchAndActivate(); | ||||
|     } catch (exception) { | ||||
|       Log.d( | ||||
|           "Unable to fetch remote config. Cached or default values will be used!", | ||||
|           error: exception); | ||||
|     } finally { | ||||
|       _subject.add(remoteConfig); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<String> dumpString() async { | ||||
|     final config = FirebaseRemoteConfig.instance; | ||||
|     final data = config.getAll(); | ||||
|     String result = ""; | ||||
|     for (var entry in data.entries) { | ||||
|       result += "[${entry.key}] ==> (${entry.value.asString()})\n"; | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   static String valueSourceToString(ValueSource source) { | ||||
|     switch (source) { | ||||
|       case ValueSource.valueRemote: | ||||
|         return "Remote"; | ||||
|       case ValueSource.valueStatic: | ||||
|         return "Static"; | ||||
|       default: | ||||
|         return "Default"; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Map<String, String> allData() { | ||||
|     final config = FirebaseRemoteConfig.instance; | ||||
|     final data = config.getAll(); | ||||
|     final result = { | ||||
|       for (var entry in data.entries) | ||||
|         "${entry.key} [${valueSourceToString(entry.value.source)}]": | ||||
|             entry.value.asString() | ||||
|     }; | ||||
|     result["last_fetch_remote_config_time"] = config.lastFetchTime.toString(); | ||||
|     result["last_fetch_remote_config_status"] = | ||||
|         config.lastFetchStatus.toString(); | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   Future forceFetch({bool debug = false}) async { | ||||
|     final remoteConfig = FirebaseRemoteConfig.instance; | ||||
|     try { | ||||
|       await remoteConfig.setConfigSettings(RemoteConfigSettings( | ||||
|         fetchTimeout: const Duration(seconds: 15), | ||||
|         minimumFetchInterval: const Duration(seconds: 0), | ||||
|       )); | ||||
|       await remoteConfig.fetchAndActivate(); | ||||
|     } catch (exception) { | ||||
|       Log.d( | ||||
|           "Unable to fetch remote config. Cached or default values will be used $exception", | ||||
|           error: exception); | ||||
|       if (debug) { | ||||
|         rethrow; | ||||
|       } | ||||
|     } finally { | ||||
|       _subject.add(remoteConfig); | ||||
|       await remoteConfig.setConfigSettings(RemoteConfigSettings( | ||||
|         fetchTimeout: const Duration(seconds: 15), | ||||
|         minimumFetchInterval: const Duration(hours: 2), | ||||
|       )); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Map<String, String> getABProperties() { | ||||
|     final config = FirebaseRemoteConfig.instance; | ||||
|     final data = config.getAll(); | ||||
|     final result = <String, String>{}; | ||||
|     final invalidABKeys = <String>{}; | ||||
|     dynamic cause; | ||||
|     for (var entry in data.entries) { | ||||
|       final valueStr = entry.value.asString(); | ||||
|       if (valueStr.contains("guru_ab_")) { | ||||
|         try { | ||||
|           final jsonValue = json.decode(valueStr); | ||||
|           if (jsonValue is Map<String, dynamic>) { | ||||
|             for (var jsonEntry in jsonValue.entries) { | ||||
|               if (jsonEntry.key.contains("guru_ab_")) { | ||||
|                 String abName = jsonEntry.key.replaceFirst("guru_ab_", ""); | ||||
|                 if (abName.contains(_invalidABKey)) { | ||||
|                   Log.w("abName($abName) length is invalid! $abName"); | ||||
|                   invalidABKeys.add(abName); | ||||
|                 } else { | ||||
|                   if (abName.length > 20) { | ||||
|                     invalidABKeys.add(abName); | ||||
|                     abName = abName.substring(0, 20); | ||||
|                   } | ||||
|                   result["ab_$abName"] = jsonEntry.value.toString(); | ||||
|                   Log.i("abName:ab_$abName value:${jsonEntry.value}"); | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } catch (error, stacktrace) { | ||||
|           Log.w("decode json error! $error"); | ||||
|           cause = error; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (invalidABKeys.isNotEmpty) { | ||||
|       GuruAnalytics.instance.logException( | ||||
|           InvalidABPropertyKeysException(invalidABKeys, cause: cause)); | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool? getBool(String name, {bool? defaultValue}) => | ||||
|       _subject.value?.getBool(name) ?? defaultValue; | ||||
| 
 | ||||
|   @override | ||||
|   String? getString(String name, {String? defaultValue}) => | ||||
|       _subject.value?.getString(name) ?? | ||||
|       defaultValue ?? | ||||
|       RemoteConfigReservedConstants.getDefaultConfigString(name); | ||||
| 
 | ||||
|   @override | ||||
|   double? getDouble(String name, {double? defaultValue}) => | ||||
|       _subject.value?.getDouble(name) ?? defaultValue; | ||||
| 
 | ||||
|   @override | ||||
|   int? getInt(String name, {int? defaultValue}) => | ||||
|       _subject.value?.getInt(name) ?? defaultValue; | ||||
| 
 | ||||
|   Stream<FirebaseRemoteConfig> observeConfig() => | ||||
|       _subject.stream.map((config) => config ?? FirebaseRemoteConfig.instance); | ||||
| 
 | ||||
|   @override | ||||
|   Stream<bool?> observeBool(String name, {bool? defaultValue}) => | ||||
|       observeConfig().map((config) => config.getBool(name)); | ||||
| 
 | ||||
|   @override | ||||
|   Stream<String?> observeString(String name, {String? defaultValue}) => | ||||
|       observeConfig().map((config) => config.getString(name)); | ||||
| 
 | ||||
|   @override | ||||
|   Stream<double?> observeDouble(String name, {double? defaultValue}) => | ||||
|       observeConfig().map((config) => config.getDouble(name)); | ||||
| 
 | ||||
|   @override | ||||
|   Stream<int?> observeInt(String name, {int? defaultValue}) => | ||||
|       observeConfig().map((config) => config.getInt(name)); | ||||
| } | ||||
|  | @ -0,0 +1,46 @@ | |||
| /// Created by Haoyi on 2022/2/26 | ||||
| 
 | ||||
| part of "remote_config_manager.dart"; | ||||
| 
 | ||||
| extension RemoteConfigReservedConstants on RemoteConfigManager { | ||||
|   static const fbEventMapping = "fb_event_mapping"; | ||||
|   static const iosReviewVersion = "ios_review_version"; | ||||
|   static const taichiConfig = "taichi_config"; | ||||
| 
 | ||||
|   // ads | ||||
|   static const iadsConfig = "iads_config"; // Interstitial Ads Config | ||||
|   static const badsConfig = "bads_config"; // Banner Ads Config | ||||
|   static const radsConfig = "rads_config"; // Rewarded Ads Config | ||||
|   static const oadsConfig = "oads_config"; // Open Ads Config | ||||
|   static const sadsConfig = "sads_config"; // Strategy Ads Config | ||||
|   static const iosAttConfig = "ios_att_config"; // iOS ATT Config | ||||
|   static const commonAdsConfig = "common_ads_config"; // Common Ads Config | ||||
| 
 | ||||
|   // rater | ||||
|   static const appRater = "app_rater"; | ||||
| 
 | ||||
|   static const cdnConfig = "cdn_config"; | ||||
| 
 | ||||
|   static const analyticsConfig = "analytics_config"; | ||||
| 
 | ||||
|   static const deploymentConfig = "deployment_config"; | ||||
| 
 | ||||
|   static const Set<String> _reservedRemoteConfigNames = <String>{ | ||||
|     fbEventMapping, | ||||
|     iosReviewVersion, | ||||
|     taichiConfig, | ||||
|     iadsConfig, | ||||
|     badsConfig, | ||||
|     radsConfig, | ||||
|     oadsConfig, | ||||
|     iosAttConfig, | ||||
|     appRater, | ||||
|     cdnConfig, | ||||
|     analyticsConfig, | ||||
|     deploymentConfig | ||||
|   }; | ||||
| 
 | ||||
|   static String? getDefaultConfigString(String key) { | ||||
|     return GuruApp.instance.defaultRemoteConfig[key]; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,131 @@ | |||
| import 'package:json_annotation/json_annotation.dart'; | ||||
| import 'package:guru_app/ads/core/ads_config.dart'; | ||||
| export 'package:guru_app/ads/core/ads_config.dart'; | ||||
| /// Created by Haoyi on 2022/8/25 | ||||
| /// | ||||
| part 'reserved_remote_config_models.g.dart'; | ||||
| 
 | ||||
| // @JsonSerializable() | ||||
| // class TaichiConfig { | ||||
| //   @JsonKey(name: "enable", defaultValue: false) | ||||
| //   final bool enable; | ||||
| // | ||||
| //   @JsonKey(name: "threshold", defaultValue: "") | ||||
| //   final String threshold; | ||||
| // | ||||
| //   @JsonKey(name: "abnormal_threshold", defaultValue: 0.1) | ||||
| //   final double abnormalThreshold; | ||||
| // | ||||
| //   TaichiConfig( | ||||
| //       {this.enable = false, this.threshold = "", this.abnormalThreshold = 0.1}); | ||||
| // | ||||
| //   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); | ||||
| } | ||||
| 
 | ||||
| class InvalidABPropertyKeysException implements Exception { | ||||
|   final Set<String> invalidKeys; | ||||
|   final dynamic cause; | ||||
| 
 | ||||
|   InvalidABPropertyKeysException(this.invalidKeys, {this.cause}); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return "InvalidABPropertyKeysException: $invalidKeys cause:$cause"; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,47 @@ | |||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'reserved_remote_config_models.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| 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, | ||||
|     }; | ||||
|  | @ -0,0 +1,63 @@ | |||
| // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart | ||||
| // This is a library that looks up messages for specific locales by | ||||
| // delegating to the appropriate library. | ||||
| 
 | ||||
| // Ignore issues from commonly used lints in this file. | ||||
| // ignore_for_file:implementation_imports, file_names, unnecessary_new | ||||
| // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering | ||||
| // ignore_for_file:argument_type_not_assignable, invalid_assignment | ||||
| // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases | ||||
| // ignore_for_file:comment_references | ||||
| 
 | ||||
| import 'dart:async'; | ||||
| 
 | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:intl/message_lookup_by_library.dart'; | ||||
| import 'package:intl/src/intl_helpers.dart'; | ||||
| 
 | ||||
| import 'messages_en.dart' as messages_en; | ||||
| 
 | ||||
| typedef Future<dynamic> LibraryLoader(); | ||||
| Map<String, LibraryLoader> _deferredLibraries = { | ||||
|   'en': () => new SynchronousFuture(null), | ||||
| }; | ||||
| 
 | ||||
| MessageLookupByLibrary? _findExact(String localeName) { | ||||
|   switch (localeName) { | ||||
|     case 'en': | ||||
|       return messages_en.messages; | ||||
|     default: | ||||
|       return null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// User programs should call this before using [localeName] for messages. | ||||
| Future<bool> initializeMessages(String localeName) { | ||||
|   var availableLocale = Intl.verifiedLocale( | ||||
|       localeName, (locale) => _deferredLibraries[locale] != null, | ||||
|       onFailure: (_) => null); | ||||
|   if (availableLocale == null) { | ||||
|     return new SynchronousFuture(false); | ||||
|   } | ||||
|   var lib = _deferredLibraries[availableLocale]; | ||||
|   lib == null ? new SynchronousFuture(false) : lib(); | ||||
|   initializeInternalMessageLookup(() => new CompositeMessageLookup()); | ||||
|   messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); | ||||
|   return new SynchronousFuture(true); | ||||
| } | ||||
| 
 | ||||
| bool _messagesExistFor(String locale) { | ||||
|   try { | ||||
|     return _findExact(locale) != null; | ||||
|   } catch (e) { | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { | ||||
|   var actualLocale = | ||||
|       Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); | ||||
|   if (actualLocale == null) return null; | ||||
|   return _findExact(actualLocale); | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue