From ea55fd4551563866b3425834a0049c5cbd55538f Mon Sep 17 00:00:00 2001 From: Haoyi Date: Thu, 7 Mar 2024 11:46:50 +0800 Subject: [PATCH] update guru_app/guru_ui Signed-off-by: Haoyi --- guru_app/guru/guru_spec.yaml | 94 +- .../lib/account/account_auth_extension.dart | 44 +- .../lib/account/account_auth_invoker.dart | 26 + guru_app/lib/account/account_data_store.dart | 93 +- guru_app/lib/account/account_manager.dart | 134 ++- .../account/account_service_extension.dart | 209 +++- guru_app/lib/account/model/account.dart | 88 +- guru_app/lib/account/model/credential.dart | 37 + guru_app/lib/account/model/user.dart | 114 +- guru_app/lib/account/model/user.g.dart | 62 +- guru_app/lib/ads/ads_manager.dart | 23 +- guru_app/lib/ads/core/ads_config.dart | 11 +- guru_app/lib/ads/core/ads_config.g.dart | 4 +- guru_app/lib/ads/core/ads_impression.dart | 44 +- .../lib/analytics/abtest/abtest_model.dart | 388 ++++++ .../lib/analytics/abtest/abtest_model.g.dart | 109 ++ .../lib/analytics/data/analytics_model.dart | 27 +- .../lib/analytics/data/analytics_model.g.dart | 8 + guru_app/lib/analytics/guru_analytics.dart | 255 +++- .../lib/analytics/modules/ads_analytics.dart | 35 +- .../lib/api/data/orders/orders_model.dart | 21 +- .../lib/api/data/orders/orders_model.g.dart | 4 + guru_app/lib/api/guru_api.dart | 23 +- guru_app/lib/api/guru_api.g.dart | 180 ++- .../lib/api/modules/guru_api_extension.dart | 32 +- guru_app/lib/app/app_models.dart | 32 +- guru_app/lib/app/app_models.g.dart | 15 + guru_app/lib/controller/assets_aware.dart | 58 +- guru_app/lib/database/creators/creators.dart | 5 +- guru_app/lib/database/guru_db.dart | 2 +- .../migrations/migration_v3_to_v4.dart | 16 + .../lib/database/migrations/migrations.dart | 4 +- guru_app/lib/financial/financial_manager.dart | 8 + guru_app/lib/financial/iap/iap_manager.dart | 480 ++++---- guru_app/lib/financial/igb/igb_manager.dart | 53 + guru_app/lib/financial/igb/igb_product.dart | 46 + guru_app/lib/financial/igc/igc_manager.dart | 4 + .../financial/manifest/manifest_manager.dart | 84 +- .../lib/financial/product/product_model.dart | 44 +- .../financial/product/product_profile.dart | 42 +- .../lib/financial/reward/reward_manager.dart | 8 +- .../remoteconfig/remote_config_manager.dart | 67 +- .../remote_config_reserved_constants.dart | 2 +- guru_app/lib/guru_app.dart | 190 ++- .../lib/inventory/db/inventory_database.dart | 408 +++++++ .../inventory/db/inventory_database.g.dart | 96 ++ guru_app/lib/inventory/inventory_manager.dart | 193 +++ guru_app/lib/property/app_property.dart | 3 + .../modules/account_property_extension.dart | 77 +- .../modules/analytics_property_extension.dart | 69 ++ .../modules/default_property_extension.dart | 4 +- .../modules/iap_property_extension.dart | 10 + guru_app/lib/property/property_keys.dart | 27 +- guru_app/lib/property/property_tags.dart | 1 + .../property/settings/global_settings.dart | 2 +- guru_app/lib/test/test_guru_app_creator.dart | 5 +- .../lib/test/test_guru_app_creator.g.dart | 184 +-- .../packages/firebase/guru_fiam/.gitignore | 29 + .../packages/firebase/guru_fiam/CHANGELOG.md | 3 + guru_app/packages/firebase/guru_fiam/LICENSE | 1 + .../packages/firebase/guru_fiam/README.md | 39 + .../firebase/guru_fiam/analysis_options.yaml | 4 + .../firebase/guru_fiam/lib/guru_fiam.dart | 7 + .../packages/firebase/guru_fiam/pubspec.yaml | 54 + .../guru_fiam/test/guru_fiam_test.dart | 12 + .../example/lib/data/initializer.dart | 34 +- .../example/lib/data/initializer.g.dart | 4 +- .../guru_assistant/example/pubspec.lock | 138 ++- .../guru_assistant/example/pubspec.yaml | 10 + guru_app/packages/guru_assistant/pubspec.lock | 11 +- guru_app/packages/guru_fb_game/.gitignore | 29 + guru_app/packages/guru_fb_game/CHANGELOG.md | 3 + guru_app/packages/guru_fb_game/LICENSE | 1 + guru_app/packages/guru_fb_game/README.md | 39 + .../guru_fb_game/analysis_options.yaml | 4 + .../packages/guru_fb_game/example/.gitignore | 43 + .../packages/guru_fb_game/example/README.md | 16 + .../example/analysis_options.yaml | 28 + .../guru_fb_game/example/lib/main.dart | 125 ++ .../guru_fb_game/example/pubspec.lock | 188 +++ .../guru_fb_game/example/pubspec.yaml | 90 ++ .../example/test/widget_test.dart | 30 + .../guru_fb_game/example/web/event-logger.js | 102 ++ .../guru_fb_game/example/web/favicon.png | Bin 0 -> 917 bytes .../guru_fb_game/example/web/fb-function.js | 331 ++++++ .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../guru_fb_game/example/web/index.html | 187 +++ .../guru_fb_game/example/web/index.js | 68 ++ .../guru_fb_game/example/web/manifest.json | 35 + .../guru_fb_game/example/web/pako.min.js | 1 + .../guru_fb_game/lib/guru_fb_game.dart | 59 + .../guru_fb_game/lib/model/model.dart | 232 ++++ .../guru_fb_game/lib/model/model.g.dart | 199 ++++ guru_app/packages/guru_fb_game/lib/utils.dart | 312 +++++ guru_app/packages/guru_fb_game/pubspec.yaml | 56 + .../guru_fb_game/test/guru_fb_game_test.dart | 9 + guru_app/packages/guru_login/.gitignore | 30 + guru_app/packages/guru_login/CHANGELOG.md | 3 + guru_app/packages/guru_login/LICENSE | 1 + guru_app/packages/guru_login/README.md | 39 + .../packages/guru_login/analysis_options.yaml | 4 + .../lib/data/account_credentials.dart | 383 ++++++ .../lib/data/account_credentials.g.dart | 130 ++ .../guru_login/lib/data/account_model.dart | 42 + .../packages/guru_login/lib/guru_login.dart | 154 +++ guru_app/packages/guru_login/pubspec.yaml | 69 ++ .../guru_login/test/guru_login_test.dart | 7 + .../lib/src/guru_spec_generator.dart | 276 ++++- .../lib/auth/auth_credential_manager.dart | 108 ++ .../lib/controller/ads_controller.dart | 5 +- .../controller/aware/ads/banner_aware.dart | 7 +- .../controller/aware/ads/rewarded_aware.dart | 2 +- .../aware/keep_screen_on_aware.dart | 8 +- .../lib/database/batch/batch_aware.dart | 2 + .../guru_utils/lib/database/database.dart | 52 +- .../guru_utils/lib/device/device_utils.dart | 67 +- .../lib/extensions/async_extension.dart | 9 + .../lib/extensions/list_extension.dart | 41 +- .../packages/guru_utils/lib/guru_utils.dart | 2 + .../packages/guru_utils/lib/http/http_ex.dart | 1 + .../guru_utils/lib/http/http_model.dart | 6 + .../guru_utils/lib/image/image_utils.dart | 3 +- guru_app/packages/guru_utils/lib/log/log.dart | 11 +- .../guru_utils/lib/manifest/manifest.dart | 47 +- .../guru_utils/lib/packages/guru_package.dart | 2 + .../guru_utils/lib/property/app_property.dart | 5 + .../guru_utils/lib/router/route_path.dart | 6 +- .../guru_utils/lib/settings/settings.dart | 2 +- .../lib/settings/utils_settings.dart | 62 +- .../GuruAnalyticsConstants.kt | 8 + .../GuruAnalyticsFlutterPlugin.kt | 31 + .../ios/Classes/GuruAnalyticsConstants.swift | 9 + .../SwiftGuruAnalyticsFlutterPlugin.swift | 70 +- .../lib/event_logger.dart | 26 +- .../lib/events_constants.dart | 1 + .../lib/guru/guru_event_logger.dart | 87 +- .../android/build.gradle | 6 +- .../guru/guru_applovin_flutter/AdStatus.kt | 8 + .../guru/guru_applovin_flutter/BannerAd.kt | 59 +- .../GuruApplovinFlutterPlugin.kt | 18 +- .../guru_applovin_flutter/InterstitialAd.kt | 38 +- .../guru_applovin_flutter/RewardedVideoAd.kt | 36 +- .../example/pubspec.lock | 58 +- .../ios/Classes/BannerAd.swift | 77 +- .../SwiftGuruApplovinFlutterPlugin.swift | 14 + .../ios/guru_applovin_flutter.podspec | 3 +- .../lib/guru_applovin_flutter.dart | 9 + .../guru_platform_data/android/build.gradle | 1 + .../guru_platform_data/GuruPlatformData.kt | 21 + .../Classes/SwiftGuruPlatformDataPlugin.swift | 5 + .../lib/guru_platform_data.dart | 29 +- guru_app/pubspec.lock | 202 ++-- guru_app/pubspec.yaml | 3 +- guru_app/test/analytics/dma_test.dart | 122 ++ guru_app/test/analytics/dma_test.mocks.dart | 1057 +++++++++++++++++ guru_app/tools/bin/pulish_gitea.py | 2 +- guru_ui/assets/images/ic_purchase_failed.png | Bin 0 -> 2405 bytes .../lib/pages/button/button_controller.dart | 2 + .../lib/pages/button/button_design_model.dart | 24 + .../pages/button/button_design_model.g.dart | 72 ++ .../example/lib/pages/button/button_view.dart | 257 ++-- .../lib/pages/root/root_controller.dart | 6 +- guru_ui/example/lib/pages/root/root_view.dart | 4 +- .../lib/pages/settings/settings_view.dart | 2 + .../lib/pages/store/store_controller.dart | 30 +- .../example/lib/pages/store/store_page.dart | 48 +- guru_ui/example/lib/theme/example_theme.dart | 2 +- guru_ui/lib/pages/store/bundle_card.dart | 28 +- guru_ui/lib/pages/store/bundle_card.g.dart | 9 +- guru_ui/lib/pages/store/purchase_banner.dart | 42 +- .../lib/pages/store/purchase_banner.g.dart | 12 +- guru_ui/lib/pages/store/purchase_card.dart | 4 +- guru_ui/lib/pages/store/purchase_card.g.dart | 19 +- guru_ui/lib/pages/store/store_controller.dart | 88 +- .../lib/pages/store/store_design_spec.dart | 18 +- .../lib/pages/store/store_design_spec.g.dart | 62 +- guru_ui/lib/pages/store/store_page.dart | 33 +- .../pages/subscription/subscription_card.dart | 632 ++++++++++ .../subscription/subscription_card.g.dart | 397 +++++++ .../subscription/subscription_controller.dart | 84 ++ .../pages/subscription/subscription_page.dart | 413 +++++++ .../subscription/subscription_page.g.dart | 152 +++ .../lib/daily_challenge_package.dart | 2 + .../challenge_calendar_controller.dart | 82 +- .../calendar/challenge_calendar_page.dart | 4 +- guru_ui/packages/design/lib/design.dart | 3 +- guru_ui/packages/design/lib/design_field.dart | 31 +- .../packages/design/lib/design_metrics.dart | 29 +- .../design_generator/lib/src/generator.dart | 11 +- .../design_spec/lib/design_annotations.dart | 4 +- .../guru_popup/lib/dialog/dialog_aware.dart | 15 + .../packages/guru_popup/lib/guru_popup.dart | 2 + .../guru_popup/lib/overlay/overlay_aware.dart | 6 +- .../guru_widgets/lib/appbar/guru_app_bar.dart | 7 +- .../lib/appbar/guru_app_bar.g.dart | 20 +- .../lib/assetbar/guru_asset_bar.dart | 38 +- .../lib/assetbar/guru_asset_bar.g.dart | 8 +- .../lib/banner/purchase_banner.g.dart | 1 + .../guru_widgets/lib/button/guru_button.dart | 61 +- .../lib/button/guru_button.g.dart | 44 +- .../lib/dialog/guru_dialog.g.dart | 70 +- .../navigationbar/guru_navigation_bar.g.dart | 1 + .../lib/overlay/asset/asset_background.dart | 29 +- .../lib/overlay/asset/asset_reward.dart | 17 +- .../lib/overlay/guru_asset_controller.dart | 55 +- .../lib/overlay/guru_loading.dart | 6 +- .../lib/overlay/guru_loading.g.dart | 16 +- .../lib/pages/awards/guru_awards_page.dart | 5 +- .../lib/pages/awards/guru_awards_page.g.dart | 5 +- .../navigation/guru_navigation_page.g.dart | 1 + .../pages/settings/guru_settings_page.dart | 2 +- .../pages/settings/guru_settings_page.g.dart | 1 + .../lib/sliderbar/guru_slider_bar.g.dart | 1 + .../lib/tabbar/guru_tab_bar.g.dart | 1 + .../guru_widgets/lib/theme/guru_theme.dart | 59 +- .../guru_widgets/lib/theme/guru_theme.g.dart | 1 + .../lib/tile/guru_list_tile.g.dart | 16 +- guru_ui/pubspec.lock | 8 +- 221 files changed, 12047 insertions(+), 1478 deletions(-) create mode 100644 guru_app/lib/account/account_auth_invoker.dart create mode 100644 guru_app/lib/account/model/credential.dart create mode 100644 guru_app/lib/analytics/abtest/abtest_model.dart create mode 100644 guru_app/lib/analytics/abtest/abtest_model.g.dart create mode 100644 guru_app/lib/database/migrations/migration_v3_to_v4.dart create mode 100644 guru_app/lib/financial/igb/igb_manager.dart create mode 100644 guru_app/lib/financial/igb/igb_product.dart create mode 100644 guru_app/lib/inventory/db/inventory_database.dart create mode 100644 guru_app/lib/inventory/db/inventory_database.g.dart create mode 100644 guru_app/lib/inventory/inventory_manager.dart create mode 100644 guru_app/packages/firebase/guru_fiam/.gitignore create mode 100644 guru_app/packages/firebase/guru_fiam/CHANGELOG.md create mode 100644 guru_app/packages/firebase/guru_fiam/LICENSE create mode 100644 guru_app/packages/firebase/guru_fiam/README.md create mode 100644 guru_app/packages/firebase/guru_fiam/analysis_options.yaml create mode 100644 guru_app/packages/firebase/guru_fiam/lib/guru_fiam.dart create mode 100644 guru_app/packages/firebase/guru_fiam/pubspec.yaml create mode 100644 guru_app/packages/firebase/guru_fiam/test/guru_fiam_test.dart create mode 100644 guru_app/packages/guru_fb_game/.gitignore create mode 100644 guru_app/packages/guru_fb_game/CHANGELOG.md create mode 100644 guru_app/packages/guru_fb_game/LICENSE create mode 100644 guru_app/packages/guru_fb_game/README.md create mode 100644 guru_app/packages/guru_fb_game/analysis_options.yaml create mode 100644 guru_app/packages/guru_fb_game/example/.gitignore create mode 100644 guru_app/packages/guru_fb_game/example/README.md create mode 100644 guru_app/packages/guru_fb_game/example/analysis_options.yaml create mode 100644 guru_app/packages/guru_fb_game/example/lib/main.dart create mode 100644 guru_app/packages/guru_fb_game/example/pubspec.lock create mode 100644 guru_app/packages/guru_fb_game/example/pubspec.yaml create mode 100644 guru_app/packages/guru_fb_game/example/test/widget_test.dart create mode 100644 guru_app/packages/guru_fb_game/example/web/event-logger.js create mode 100644 guru_app/packages/guru_fb_game/example/web/favicon.png create mode 100644 guru_app/packages/guru_fb_game/example/web/fb-function.js create mode 100644 guru_app/packages/guru_fb_game/example/web/icons/Icon-192.png create mode 100644 guru_app/packages/guru_fb_game/example/web/icons/Icon-512.png create mode 100644 guru_app/packages/guru_fb_game/example/web/icons/Icon-maskable-192.png create mode 100644 guru_app/packages/guru_fb_game/example/web/icons/Icon-maskable-512.png create mode 100644 guru_app/packages/guru_fb_game/example/web/index.html create mode 100644 guru_app/packages/guru_fb_game/example/web/index.js create mode 100644 guru_app/packages/guru_fb_game/example/web/manifest.json create mode 100644 guru_app/packages/guru_fb_game/example/web/pako.min.js create mode 100644 guru_app/packages/guru_fb_game/lib/guru_fb_game.dart create mode 100644 guru_app/packages/guru_fb_game/lib/model/model.dart create mode 100644 guru_app/packages/guru_fb_game/lib/model/model.g.dart create mode 100644 guru_app/packages/guru_fb_game/lib/utils.dart create mode 100644 guru_app/packages/guru_fb_game/pubspec.yaml create mode 100644 guru_app/packages/guru_fb_game/test/guru_fb_game_test.dart create mode 100644 guru_app/packages/guru_login/.gitignore create mode 100644 guru_app/packages/guru_login/CHANGELOG.md create mode 100644 guru_app/packages/guru_login/LICENSE create mode 100644 guru_app/packages/guru_login/README.md create mode 100644 guru_app/packages/guru_login/analysis_options.yaml create mode 100644 guru_app/packages/guru_login/lib/data/account_credentials.dart create mode 100644 guru_app/packages/guru_login/lib/data/account_credentials.g.dart create mode 100644 guru_app/packages/guru_login/lib/data/account_model.dart create mode 100644 guru_app/packages/guru_login/lib/guru_login.dart create mode 100644 guru_app/packages/guru_login/pubspec.yaml create mode 100644 guru_app/packages/guru_login/test/guru_login_test.dart create mode 100644 guru_app/packages/guru_utils/lib/auth/auth_credential_manager.dart create mode 100644 guru_app/test/analytics/dma_test.dart create mode 100644 guru_app/test/analytics/dma_test.mocks.dart create mode 100644 guru_ui/assets/images/ic_purchase_failed.png create mode 100644 guru_ui/example/lib/pages/button/button_design_model.dart create mode 100644 guru_ui/example/lib/pages/button/button_design_model.g.dart create mode 100644 guru_ui/lib/pages/subscription/subscription_card.dart create mode 100644 guru_ui/lib/pages/subscription/subscription_card.g.dart create mode 100644 guru_ui/lib/pages/subscription/subscription_controller.dart create mode 100644 guru_ui/lib/pages/subscription/subscription_page.dart create mode 100644 guru_ui/lib/pages/subscription/subscription_page.g.dart diff --git a/guru_app/guru/guru_spec.yaml b/guru_app/guru/guru_spec.yaml index 4f7092c..2882133 100644 --- a/guru_app/guru/guru_spec.yaml +++ b/guru_app/guru/guru_spec.yaml @@ -1,5 +1,7 @@ app_name: GuruApp +app_category: app + flavor: "guru_test" # App接入GuruApp的基础信息(下面内容必填) @@ -81,7 +83,6 @@ deployment: # ios 验证服务器的密码 ios_validate_receipt_password: aa998877665544332211bb00cc - # 被标注的conversion点,在自打点库中将被以Emergency的优先级进行发送 conversion_events: - first_rads_rewarded @@ -166,6 +167,30 @@ deployment: # 因此它的优先级永远不会大于正常的 Banner 广告, 默认值是 false show_internal_ads_when_banner_unavailable: true + # 由于订阅订单比较重要,而从用户反馈的日志上来看,会存在接口返回异常的问题 + # 因此针对这种情况,添加订阅的恢复宽限次数,默认为 3 次 + # 当订阅订单恢复失败次数超过该次数,才会真正删除 + subscription_restore_grace_count: 3 + + # 插屏在展示广告前,为了保证用户的体验,会有一个广告的保护时间, + # 即:距上一次全屏广告(插屏广告和激励广告)的结束间隔时间, + # 默认的间隔保护时间为 1 分钟(60 秒)单位为秒 + fullscreen_ads_min_interval: 60 + + # 是否打开中台的 AccountProfile 同步机制 + # 打开后,在登陆后(包括匿名登陆) 会启动向 Firestore 进行同步AccountProfile的机制 + # Firestore 针对 AccountProfile的存储位置默认放在 users 表中 + enabled_sync_account_profile: false + + # 根据 BI 的需求,对应的 Purchase事件只能报太极的 001 或 020的其中一个 + # 因此添加 Purchase Event 的 trigger, 默认值为 1 + # 1: 表示在发生购买时打 tch_ad_rev_roas_001 + # 2: 表示在发生购买时打 tch_ad_rev_roas_020 + # 在广告展示时也会依据该 trigger 的值,在不同的时机打对应的 purchase事件 + purchase_event_trigger: 1 + +# tracking_notification_permission_pass_analytics_type : guru|firebase + # 广告配置 ads_profile: # Banner广告ID(变现提供) @@ -202,7 +227,6 @@ ads_profile: android: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ios: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - remote_config: # 保留配置,插屏广告相关配置 iads_config: '{"free_s":600,"win_count":4,"scene":"game_start","sp_scene":"new_block:120;reset_scs:120","retry_min_s":10,"retry_max_s":600,"amazon_enable":false,"imp_gap_s":120}' @@ -215,6 +239,10 @@ remote_config: # 保留配置,打点相关配置 analytics_config: '{"cap":"firebase|facebook|guru", "init_delay_s": 10}' +# +# _mapping: +# cdn_config: "cdn2_config" + products: # sku @@ -311,6 +339,7 @@ products: manifest: category: "prop" details: + sku: "{1}_{2}" type: "prop" amount: 1 theme_id: "{1}" @@ -389,15 +418,15 @@ products: details: type: "igc" amount: 16000 - - theme_mul: - sku: "theme_{category}_{theme_id}" - attr: possessive - method: igc - manifest: - category: "{1}" - theme_id: "{2}" - cate: "{1}" +# +# theme_mul: +# sku: "theme_{category}_{theme_id}" +# attr: possessive +# method: igc +# manifest: +# category: "{1}" +# theme_id: "{2}" +# cate: "{1}" # adjust 相关配置 adjust_profile: @@ -429,3 +458,46 @@ adjust_profile: android: 95fu7q ios: 1p8z5t +experiments: + + test: + start: 20240129T000000 + end: 20240129T000000 + audience: + filters: + - version: + opt: lt + mmp: 2.3.0 + - country: + included: "" + excluded: "us,cn,en" + - platform: + android: + opt: gte + ver: 33 + ios: + opt: gte + ver: 14 + variant: 2 + test2: + start: 20240129T000000 + end: 20240129T000000 + audience: + filters: + - version: + opt: lt + mmp: 2.3.0 + - country: + included: "cn" + excluded: "us" + - platform: + android: + opt: lt + ver: 24 + ios: + opt: gte + ver: 14 + - new_user: true + variant: 5 + + diff --git a/guru_app/lib/account/account_auth_extension.dart b/guru_app/lib/account/account_auth_extension.dart index 06e5155..0de314a 100644 --- a/guru_app/lib/account/account_auth_extension.dart +++ b/guru_app/lib/account/account_auth_extension.dart @@ -3,41 +3,41 @@ part of "account_manager.dart"; extension AccountAuthExtension on AccountManager { - Future _authenticate(SaasUser saasUser, + Future _loginFirebase(GuruUser guruUser, {bool canRefreshFirebaseToken = true}) async { User? firebaseUser; - SaasUser newSaasUser = saasUser; - firebaseUser = await _authenticateFirebase(saasUser).catchError((error) { + GuruUser newGuruUser = guruUser; + firebaseUser = await _authenticateFirebase(guruUser).catchError((error) { Log.e("_authenticateFirebase error! $error", tag: "Account"); return null; }); if (firebaseUser == null && canRefreshFirebaseToken) { try { - newSaasUser = await _refreshFirebaseToken(saasUser); - return _authenticate(newSaasUser, canRefreshFirebaseToken: false); + newGuruUser = await _refreshFirebaseToken(guruUser); + return _loginFirebase(newGuruUser, canRefreshFirebaseToken: false); } catch (error, stacktrace) { - return AccountAuth(saasUser, null); + return FirebaseAccountAuth(guruUser, firebaseUser: null); } } - return AccountAuth(newSaasUser, firebaseUser); + return FirebaseAccountAuth(newGuruUser, firebaseUser: firebaseUser); } - Future _refreshFirebaseToken(SaasUser oldSaasUser) async { + Future _refreshFirebaseToken(GuruUser oldSaasUser) async { return await GuruApi.instance .renewFirebaseToken() .then((tokenData) => oldSaasUser.copyWith(firebaseToken: tokenData.firebaseToken)); } - Future _authenticateFirebase(SaasUser saasUser) async { + Future _authenticateFirebase(GuruUser guruUser) async { int retry = 0; dynamic lastError; while (retry < 1) { try { - Log.i("[$retry] _authenticateFirebase:${saasUser.firebaseToken}", tag: "Account"); + Log.i("[$retry] _authenticateFirebase:${guruUser.firebaseToken}", tag: "Account"); return await FirebaseAuth.instance - .signInWithCustomToken(saasUser.firebaseToken) + .signInWithCustomToken(guruUser.firebaseToken) .then((result) => result.user); } catch (error, stacktrace) { await Future.delayed(const Duration(milliseconds: 600)); @@ -48,4 +48,26 @@ extension AccountAuthExtension on AccountManager { } throw lastError ?? ("_authenticateFirebase error!"); } + + Future authenticateFirebase() async { + final guruUser = accountDataStore.user; + if (guruUser == null) { + return false; + } + try { + final auth = await _loginFirebase(guruUser); + final newGuruUser = auth.user; + if (!guruUser.isSame(newGuruUser)) { + _updateGuruUser(newGuruUser); + } + if (auth.firebaseUser != null) { + _updateFirebaseUser(auth.firebaseUser!); + Log.i("_updateFirebaseUser success!", tag: "Account"); + } + return true; + } catch (error, stacktrace) { + GuruAnalytics.instance.logException(error, stacktrace: stacktrace); + } + return false; + } } diff --git a/guru_app/lib/account/account_auth_invoker.dart b/guru_app/lib/account/account_auth_invoker.dart new file mode 100644 index 0000000..02acc39 --- /dev/null +++ b/guru_app/lib/account/account_auth_invoker.dart @@ -0,0 +1,26 @@ +part of "account_manager.dart"; + +extension AccountAuthInvoker on AccountManager { + Future _invokeLogin(GuruUser loginUser, Credential credential) async { + return await GuruApp.instance.protocol.accountAuthDelegate?.onLogin(loginUser, credential) ?? + true; + } + + Future _invokeLogout(GuruUser logoutUser) async { + return await GuruApp.instance.protocol.accountAuthDelegate?.onLogout(logoutUser) ?? true; + } + + Future _invokeAnonymousLogout(GuruUser logoutUser) async { + return await GuruApp.instance.protocol.accountAuthDelegate?.onAnonymousLogout(logoutUser) ?? + true; + } + + Future _invokeAnonymousLogin(GuruUser loginUser, Credential credential) async { + return await GuruApp.instance.protocol.accountAuthDelegate?.onAnonymousLogin(loginUser, credential) ?? + true; + } + + Future _invokeConflict() async { + return await GuruApp.instance.protocol.accountAuthDelegate?.onConflict() ?? false; + } +} diff --git a/guru_app/lib/account/account_data_store.dart b/guru_app/lib/account/account_data_store.dart index 53fc9bd..a3c47f1 100644 --- a/guru_app/lib/account/account_data_store.dart +++ b/guru_app/lib/account/account_data_store.dart @@ -1,11 +1,13 @@ import 'dart:convert'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:guru_app/account/model/account.dart'; import 'package:guru_app/account/model/account_profile.dart'; import 'package:guru_app/account/model/user.dart'; import 'package:guru_app/analytics/guru_analytics.dart'; import 'package:guru_app/api/guru_api.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; import 'package:guru_utils/device/device_info.dart'; import 'package:guru_utils/extensions/extensions.dart'; @@ -19,11 +21,15 @@ class AccountDataStore { static final AccountDataStore instance = AccountDataStore._(); final BehaviorSubject _deviceInfoSubject = BehaviorSubject.seeded(null); - final BehaviorSubject _saasUserSubject = BehaviorSubject.seeded(null); + final BehaviorSubject _guruUserSubject = BehaviorSubject.seeded(null); final BehaviorSubject _firebaseUser = BehaviorSubject.seeded(null); final BehaviorSubject _accountProfile = BehaviorSubject.seeded(null); final BehaviorSubject _accountDataStatus = BehaviorSubject.seeded(AccountDataStatus.idle); + + final BehaviorSubject> _credentials = + BehaviorSubject.seeded({}); + int initRetryCount = 0; Stream get observableAccountProfile => _accountProfile.stream; @@ -32,9 +38,12 @@ class AccountDataStore { AccountDataStore._(); - String? get saasToken => _saasUserSubject.value?.token; + @Deprecated("use guruToken instead") + String? get saasToken => _guruUserSubject.value?.token; - String? get uid => _saasUserSubject.value?.uid; + String? get guruToken => _guruUserSubject.value?.token; + + String? get uid => _guruUserSubject.value?.uid; AccountProfile? get accountProfile => _accountProfile.value; @@ -42,7 +51,7 @@ class AccountDataStore { String? get countryCode => _accountProfile.value?.countryCode; - SaasUser? get user => _saasUserSubject.value; + GuruUser? get user => _guruUserSubject.value; String? get avatar => _accountProfile.value?.avatar; @@ -55,16 +64,42 @@ class AccountDataStore { Stream get observableInitialized => _accountDataStatus.stream.map((status) => status == AccountDataStatus.initialized); - Stream get observableSaasUser => _saasUserSubject.stream; + bool get hasUid => uid?.isNotEmpty == true; + + bool get isAnonymous => + (uid?.isNotEmpty != true) || + (_credentials.value.containsKey(AuthType.anonymous) && _credentials.value.length == 1); + + Stream get observableSaasUser => _guruUserSubject.stream; + + Map get credentials => _credentials.value; + + Account get account => Account.restore( + guruUser: user, + device: currentDevice, + accountProfile: accountProfile, + firebaseUser: _firebaseUser.value, + credentials: credentials); + + Stream get observableAccount => Rx.combineLatestList([ + _guruUserSubject.stream, + _deviceInfoSubject.stream, + _accountProfile.stream, + _firebaseUser.stream, + _credentials.stream + ]).debounceTime(const Duration(milliseconds: 100)).map((_) => account); + + bool get isSocialLogged => (uid?.isNotEmpty == true) && credentials.isNotEmpty; void dispose() { _deviceInfoSubject.close(); - _saasUserSubject.close(); + _guruUserSubject.close(); _firebaseUser.close(); _accountProfile.close(); + _credentials.close(); } - Future signInAnonymousInLocked() async { + Future signInAnonymousInLocked() async { // 这里需要使用原始的http post请求。否则这里将会死锁DIO所有请求 final secret = await AppProperty.getInstance().getAnonymousSecretKey(); final headers = { @@ -82,7 +117,7 @@ class AccountDataStore { final data = const Utf8Decoder().convert(response.bodyBytes); if (data.isNotEmpty) { final result = json.decode(data); - return SaasUser.fromJson(result["data"]); + return GuruUser.fromJson(result["data"]); } } catch (error, stacktrace) { Log.v("signInAnonymousInLocked error:$error", tag: "Account"); @@ -91,9 +126,9 @@ class AccountDataStore { } Future refreshAuth() async { - final saasUser = await signInAnonymousInLocked(); - if (saasUser != null) { - updateSaasUser(saasUser); + final guruUser = await signInAnonymousInLocked(); + if (guruUser != null) { + updateGuruUser(guruUser); } } @@ -101,12 +136,16 @@ class AccountDataStore { _deviceInfoSubject.addEx(deviceInfo); } - void updateSaasUser(SaasUser saasUser) { - _saasUserSubject.addEx(saasUser); + @Deprecated("use updateGuruUser instead") + void updateSaasUser(GuruUser saasUser) { + updateGuruUser(saasUser); + } - if (saasUser.createAtTimestamp > 0) { + void updateGuruUser(GuruUser guruUser) { + _guruUserSubject.addEx(guruUser); + if (guruUser.createAtTimestamp > 0) { GuruAnalytics.instance - .setUserProperty("user_created_timestamp", saasUser.createAtTimestamp.toString()); + .setUserProperty("user_created_timestamp", guruUser.createAtTimestamp.toString()); } } @@ -118,7 +157,31 @@ class AccountDataStore { _accountProfile.addEx(profile); } + void bindCredential(Credential credential) { + final newCredentials = Map.of(_credentials.value); + newCredentials[credential.authType] = credential; + _credentials.addEx(newCredentials); + } + + void unbindCredential(AuthType authType) { + final newCredentials = Map.of(_credentials.value); + newCredentials.remove(authType); + _credentials.addEx(newCredentials); + } + + void updateCredentials(Map credentials) { + _credentials.addEx(Map.of(credentials)); + } + bool transitionTo(AccountDataStatus status, {AccountDataStatus? expectStatus}) { return _accountDataStatus.addIfChanged(status); } + + logout() { + _guruUserSubject.addEx(null); + _firebaseUser.addEx(null); + _deviceInfoSubject.addEx(null); + _accountProfile.addEx(null); + _accountDataStatus.addIfChanged(AccountDataStatus.idle); + } } diff --git a/guru_app/lib/account/account_manager.dart b/guru_app/lib/account/account_manager.dart index 1527813..41578ba 100644 --- a/guru_app/lib/account/account_manager.dart +++ b/guru_app/lib/account/account_manager.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:dio/dio.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:guru_app/account/account_data_store.dart'; import 'package:guru_app/account/model/account.dart'; @@ -9,8 +10,10 @@ import 'package:guru_app/analytics/guru_analytics.dart'; import 'package:guru_app/api/guru_api.dart'; import 'package:guru_app/firebase/firebase.dart'; import 'package:guru_app/firebase/firestore/firestore_manager.dart'; +import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; import 'package:guru_utils/collection/collectionutils.dart'; import 'package:guru_utils/core/ext.dart'; import 'package:guru_utils/datetime/datetime_utils.dart'; @@ -19,6 +22,8 @@ import 'package:guru_utils/device/device_utils.dart'; import 'package:guru_utils/log/log.dart'; import 'package:guru_utils/network/network_utils.dart'; +import 'model/credential.dart'; + /// Created by Haoyi on 6/3/21 /// /// @@ -26,6 +31,8 @@ part "account_service_extension.dart"; part "account_auth_extension.dart"; +part "account_auth_invoker.dart"; + class ModifyNicknameException implements Exception { final String? message; final dynamic cause; @@ -53,11 +60,13 @@ class ModifyLevelException implements Exception { class AccountManager { final AccountDataStore accountDataStore; - // final FirestoreService firestoreService; - Timer? retryTimer; - static AccountManager instance = AccountManager(); + static final AccountManager instance = AccountManager(); + + static const List defaultSupportedAuthCredentialDelegates = [ + AnonymousCredentialDelegate() + ]; AccountManager() : accountDataStore = AccountDataStore.instance; @@ -109,6 +118,117 @@ class AccountManager { accountDataStore.updateAccountProfile(dirtyAccountProfile); } + /// 登录 + /// + /// [authType] 登录类型 + /// [onConflict] 登录冲突处理 + /// [onLogin] 登录成功处理 + /// + Future loginWith(AuthType authType) async { + late final Credential? credential; + try { + final result = await AuthCredentialManager.instance.loginWith(authType); + credential = result.credential; + if (!result.isSuccess || credential == null) { + Log.w("loginWith $authType error! credential: [$credential]", tag: "Account"); + return false; + } + } catch (error, stacktrace) { + Log.e("loginWith $authType error:$error, $stacktrace"); + return false; + } + + try { + /// 如果冲突将会报 409 的错 + final guruUser = await _requestGuruUser(credential); + await processLogin(guruUser, credential); + return true; + } catch (error, stacktrace) { + Log.w("loginWith $authType error:$error, $stacktrace"); + if (error is DioError && error.response?.statusCode == 409) { + return await _processConflict(credential); + } else { + return false; + } + } + } + + Future processLogin(GuruUser user, Credential credential) async { + await _updateGuruUser(user); + await _bindCredential(credential); + try { + await _verifyOrReportAuthDevice(user); + authenticateFirebase(); + } catch (error, stacktrace) { + Log.e("_verifyOrReportAuthDevice error!$error $stacktrace"); + } + if (credential.isAnonymous) { + return await _invokeAnonymousLogin(user, credential); + } else { + return await _invokeLogin(user, credential); + } + } + + /// + /// 登出操作,会把所有的第三方登陆都登出,如果当前有对应的匿名登陆的 credential 那么保留到匿名登陆的状态 + /// 如果当前没有匿名登录的 credential 那么这里将会重新创建一个匿名登陆,并且这里不会进行数据迁移 + /// + /// 如果在调用 logout 时,明确指定了要登出哪些 AuthType, + /// 那么 logout 方法将尝试用 unbind 的方法去处理用户信息。 + /// 当尝试解绑掉指定的 authTypes 时,只要满足下面两种情况的其中一种, + /// 都不能以 unbind 形式进行处理,都会认定为是真正的 logout + /// 1. 如果解绑掉所有指定的凭证后,当前凭证信息只保留了一个匿名凭证 + /// 2. 如果解绑掉所有指定的凭证后,当前没有任何凭证信息 + /// 如果上面两个条件都不满足,那么将以 unbind方法进行 logout + /// 以 unbind 形式进行 logout时,将不会通知应用 onLogout方法 + /// + /// 因此这里需要注意,就算明确指定了登出的 AuthType,依然存在调用 onLogout 的情况 + /// + Future logout({bool switching = false, Set? authTypes}) async { + bool isUnbind = false; + + if (authTypes != null && authTypes.isNotEmpty) { + final currentCredentials = accountDataStore.credentials.keys.toSet(); + currentCredentials.removeAll(authTypes); + currentCredentials.remove(AuthType.anonymous); + isUnbind = currentCredentials.isNotEmpty; + } + + final logoutUser = accountDataStore.user?.copyWith(); + try { + if (!isUnbind && logoutUser != null) { + final result = await _invokeLogout(logoutUser); + if (!result) { + Log.w("logout error! ignore!"); + return null; + } + } + } catch (error, stacktrace) { + Log.w("invokeLogout error! $error!"); + return null; + } + for (var authType in accountDataStore.credentials.keys) { + /// 默认的登出只是 unbind 掉三方的 credentials ,不会真正的登出 + /// 如果当前没有匿名登陆,那么就会真正的登出,并会重新登陆匿名,但是数据不会清除 + /// 如果 authTypes传的是 null,这里会返回空,依然满足不等于 False, + /// 这里只要是空或是真正的包含才会进行真正的解绑 + if (authTypes?.contains(authType) != false && authType != AuthType.anonymous) { + await AuthCredentialManager.instance.logout(authType); + _unbindCredential(authType); + } + } + + /// 如果当前连匿名登陆也没有了,那么就会重新登陆匿名帐号 + /// 如果是正在切换帐号的话,这里不需要登录一个新的匿名帐号 + if (!switching && accountDataStore.credentials.isEmpty) { + final auth = await _retrieveAnonymous(); + if (auth != null) { + await processLogin(auth.user, auth.credential!); + } + } + return logoutUser; + } + Future modifyProfile( {String? nickname, String? avatar, @@ -134,6 +254,10 @@ class AccountManager { }); await updateLocalProfile(modifiedJson); + /// 如果本地部署没有打开同步 AccountProfile 机制,这里直接返回 true + if (!GuruApp.instance.appSpec.deployment.enabledSyncAccountProfile) { + return true; + } while ((await NetworkUtils.isNetworkConnected()) && retryCount-- > 0) { final accountProfile = await FirestoreManager.instance.modifyProfile(modifiedJson).onError((error, stackTrace) { @@ -149,7 +273,9 @@ class AccountManager { return true; } else { Log.i("[$retryCount] modify profile error!", tag: "Account"); - await authenticate().timeout(const Duration(seconds: 15)).catchError((error, stackTrace) { + await authenticateFirebase() + .timeout(const Duration(seconds: 15)) + .catchError((error, stackTrace) { Log.i("re-authenticate error:$error", stackTrace: stackTrace, tag: "Account"); }); await Future.delayed(const Duration(seconds: 1)); diff --git a/guru_app/lib/account/account_service_extension.dart b/guru_app/lib/account/account_service_extension.dart index 4fd7bfa..70df4c6 100644 --- a/guru_app/lib/account/account_service_extension.dart +++ b/guru_app/lib/account/account_service_extension.dart @@ -1,17 +1,21 @@ /// Created by Haoyi on 6/3/21 - part of "account_manager.dart"; extension AccountServiceExtension on AccountManager { Future _restoreAccount(Account account) async { - SaasUser? saasUser = account.saasUser; - Log.d("restoreAccount $saasUser", tag: "Account"); - saasUser ??= await signInWithAnonymous().catchError((error, stacktrace) { - Log.v("signInWithAnonymous error:$error, $stacktrace"); - return null; - }); + AccountAuth? anonymousAuth; + GuruUser? guruUser = account.guruUser; + Log.d("restoreAccount $guruUser", tag: "Account"); + try { + if (guruUser == null) { + anonymousAuth = await _retrieveAnonymous(); + guruUser = anonymousAuth?.user; + } + } catch (error, stacktrace) { + Log.w("loginWith Anonymous error:$error, $stacktrace"); + } - Log.v("_restoreAccount saasUser:$saasUser", tag: "Account"); + Log.v("_restoreAccount saasUser:$guruUser", tag: "Account"); final device = account.device; if (device != null) { _updateDevice(device); @@ -22,45 +26,116 @@ extension AccountServiceExtension on AccountManager { _updateAccountProfile(accountProfile); } - if (saasUser != null) { - _updateSaasUser(saasUser); - await _verifyOrReportAuthDevice(saasUser); - final auth = await authenticate(); - if (auth == null) { - return false; - } + final credentials = account.credentials; + if (credentials.isNotEmpty) { + _restoreCredentials(credentials); + } + + if (guruUser != null) { + await _updateGuruUser(guruUser); + await _verifyOrReportAuthDevice(guruUser); + await authenticateFirebase(); if (accountProfile != null) { await _checkOrUploadAccountProfile(accountProfile); } + if (anonymousAuth != null) { + final anonymousCredential = anonymousAuth.credential; + if (anonymousCredential != null) { + _bindCredential(anonymousCredential); + return await _invokeAnonymousLogin(anonymousAuth.user, anonymousCredential); + } + } return true; } else { return false; } } - Future authenticate() async { - final saasUser = accountDataStore.user; - if (saasUser == null) { - return null; - } + Future switchUser(GuruUser newUser) async { + /// 更新 login 的用户信息 + _updateGuruUser(newUser); try { - final auth = await _authenticate(saasUser); - final newSaasUser = auth.user; - if (newSaasUser != null && !saasUser.isSame(newSaasUser)) { - _updateSaasUser(newSaasUser); - } - if (auth.firebaseUser != null) { - _updateFirebaseUser(auth.firebaseUser!); - Log.i("_updateFirebaseUser success!", tag: "Account"); - } - return auth; + await _verifyOrReportAuthDevice(newUser); + // 登陆 firebase 不需要同步等待 + authenticateFirebase(); } catch (error, stacktrace) { - GuruAnalytics.instance.logException(error, stacktrace: stacktrace); + Log.w("loginWithCredential error:$error, $stacktrace"); } - return null; } - Future _buildDevice(SaasUser saasUser) async { + Future _switchAccount(Credential credential) async { + GuruUser? loginUser; + GuruUser? logoutUser; + + /// 这里只调用接口获取对应的新用户信息,还没有做对应的绑定操作 + try { + loginUser = await _loginGuruWithCredential(credential); + } catch (error, stacktrace) { + Log.w("loginWithCredential[${credential.authType}] error:$error, $stacktrace"); + return false; + } + if (loginUser.isSame(accountDataStore.user)) { + Log.w("loginWithCredential same user!", tag: "Account"); + _bindCredential(credential); + return false; + } + bool result = false; + + /// logout 内部进行了拦截,因此这里总是会返回一个 logoutUser + /// logout传入 switch参数,表示是一个切换帐号,不需要真正的登出, + /// 因为在下面的 SwitchAccount方法中会完成后续的过程 + logoutUser = await logout(switching: true); + + /// 如果这里没有返回出对应的退出用户,将认为退出失败 + /// 因为进到 switchAccount 里肯定是非匿名登陆的帐号做登出操作 + if (logoutUser != null) { + result = await GuruApp.instance.switchAccount(loginUser, credential, oldUser: logoutUser); + } + return result; + } + + Future _processConflict(Credential credential) async { + final historicalSocialAuths = await AppProperty.getInstance().getHistoricalSocialAuths(); + + /// 如果是匿名登录,并且在这个设备上同样的用户没有绑定过其它的三方登陆凭证 + /// 这种情况下,认定为新用户,中台会静默解决冲突,并且对应的数据库不会发生迁移 + if (accountDataStore.isAnonymous && historicalSocialAuths.isEmpty) { + Log.d("associate conflict: _loginGuruWithCredential!"); + final user = accountDataStore.user; + final oldUid = user?.uid ?? ""; + if (user != null) { + await _invokeAnonymousLogout(user); + } + + /// 因为这里是匿名登陆,因此在冲突的时候通过静默的方法切换账户 + final guruUser = await _loginGuruWithCredential(credential); + + /// 由于是冲突处理,此时的匿名帐号已经和当前新登陆的用户不能配对 + /// 因此在新用户登陆成功后,这里需要将匿名帐户的凭证解绑,并清除匿名的密钥 + /// 这样做的目的是为了在该帐号退出时,判断匿名帐号是否存在, + /// 如果不存在会创建一个新的匿名帐号,确保数据不被污染 + await _unbindCredential(AuthType.anonymous); + + /// 将新的用户进行关联,此时当前设备上只有一个登陆凭证 + await processLogin(guruUser, credential); + + GuruAnalytics.instance.logGuruEvent("switch_account", { + "auth": getAuthName(credential.authType), + "old_uid": oldUid, + "new_uid": guruUser.uid, + "silent": true + }); + return true; + } else { + final canSwitch = await _invokeConflict(); + if (canSwitch) { + return await _switchAccount(credential); + } + } + return false; + } + + Future _buildDevice(GuruUser saasUser) async { final DeviceInfo? deviceInfo = await AppProperty.getInstance().getAccountDevice(); final firebasePushToken = await RemoteMessagingManager.instance.getToken(); @@ -73,13 +148,39 @@ extension AccountServiceExtension on AccountManager { return DeviceTrack(null, deviceInfo); } - Future signInWithAnonymous() async { - final anonymousSecretKey = await AppProperty.getInstance().getAnonymousSecretKey(); - return GuruApi.instance.signInWithAnonymous(secret: anonymousSecretKey); + Future _retrieveAnonymous() async { + final result = await AuthCredentialManager.instance.loginWith(AuthType.anonymous); + final credential = result.credential; + if (!result.isSuccess || credential == null) { + Log.w("_retrieveAnonymous error!", tag: "Account"); + return null; + } + final user = await _requestGuruUser(credential); + return AccountAuth(user, credential: credential); } - Future _verifyOrReportAuthDevice(SaasUser saasUser) async { - final deviceTrack = await _buildDevice(saasUser); + Future _loginGuruWithCredential(Credential credential) async { + return await GuruApi.instance.loginGuruWithCredential(credential: credential); + } + + Future _associateCredential(Credential credential) async { + return await GuruApi.instance.associateCredential(credential: credential); + } + + Future _requestGuruUser(Credential credential) async { + //MetaData是匿名请求,或者当前没有任何 GuruUser Id,走signIn接口 + if (!accountDataStore.hasUid || credential.isAnonymous) { + Log.d("_loginGuruWithCredential!", tag: "Account"); + return await _loginGuruWithCredential(credential); + } else { + Log.d("_associateCredential!"); + //当前有 GuruUser id,并且MetaData是三方登录Token,走associate接口(不管已有的SaasUser是不是三方登录) + return await _associateCredential(credential); + } + } + + Future _verifyOrReportAuthDevice(GuruUser guruUser) async { + final deviceTrack = await _buildDevice(guruUser); final latestReportDeviceTimestamp = await AppProperty.getInstance().getLatestReportDeviceTimestamp(); final elapsedInterval = DateTimeUtils.currentTimeInMillis() - latestReportDeviceTimestamp; @@ -89,7 +190,7 @@ extension AccountServiceExtension on AccountManager { if (deviceId.isNotEmpty) { GuruAnalytics.instance.setDeviceId(deviceId); } - if (isChanged && reportDevice?.isValid == true && saasUser.isValid == true) { + if (isChanged && reportDevice?.isValid == true && guruUser.isValid == true) { final result = await GuruApi.instance.reportDevice(reportDevice!).then((_) { return true; }).catchError((error) { @@ -135,10 +236,32 @@ extension AccountServiceExtension on AccountManager { accountDataStore.updateDeviceInfo(device); } - void _updateSaasUser(SaasUser saasUser) { - accountDataStore.updateSaasUser(saasUser); - AppProperty.getInstance().setAccountSaasUser(saasUser); - GuruAnalytics.instance.setUserId(saasUser.uid); + Future _bindCredential(Credential credential) async { + accountDataStore.bindCredential(credential); + + /// 这里匿名帐号是不会保存凭证的,因为匿名帐号的登陆凭证是自生成的 + if (credential.authType != AuthType.anonymous) { + await AppProperty.getInstance().saveCredential(credential); + } + } + + Future _unbindCredential(AuthType authType) async { + accountDataStore.unbindCredential(authType); + if (authType != AuthType.anonymous) { + await AppProperty.getInstance().deleteCredential(authType); + } else { + await AppProperty.getInstance().clearAnonymousSecretKey(); + } + } + + void _restoreCredentials(Map credentials) { + accountDataStore.updateCredentials(credentials); + } + + Future _updateGuruUser(GuruUser guruUser) async { + accountDataStore.updateGuruUser(guruUser); + await AppProperty.getInstance().setAccountGuruUser(guruUser); + await GuruAnalytics.instance.setUserId(guruUser.uid); } void _updateFirebaseUser(User user) { diff --git a/guru_app/lib/account/model/account.dart b/guru_app/lib/account/model/account.dart index 594be12..8061d7b 100644 --- a/guru_app/lib/account/model/account.dart +++ b/guru_app/lib/account/model/account.dart @@ -1,40 +1,108 @@ import 'package:firebase_auth/firebase_auth.dart'; +import 'package:guru_app/account/account_manager.dart'; import 'package:guru_app/account/model/account_profile.dart'; import 'package:guru_app/account/model/user.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; import 'package:guru_utils/device/device_info.dart'; - +import 'package:guru_utils/property/app_property.dart'; /// Created by Haoyi on 6/3/21 class Account { - final SaasUser? saasUser; + final GuruUser? guruUser; final DeviceInfo? device; final AccountProfile? accountProfile; final User? firebaseUser; + final Map credentials; // facebook, google, apple, anonymous - String? get uid => saasUser?.uid; + @Deprecated("use guruUser instead") + SaasUser? get saasUser => guruUser; + + String? get uid => guruUser?.uid; String? get nickname => accountProfile?.nickname; - Account.restore({this.saasUser, this.device, this.accountProfile, this.firebaseUser}); + Account.restore( + {this.guruUser, + this.device, + this.accountProfile, + this.firebaseUser, + this.credentials = const {}}); } class AccountAuth { - final SaasUser? user; - final User? firebaseUser; + final GuruUser user; + final Credential? credential; - AccountAuth(this.user, this.firebaseUser); + AccountAuth(this.user, {this.credential}); bool get isValid => uid != null && uid != ""; - String? get saasToken => user?.token; + String? get saasToken => user.token; - String? get uid => user?.uid; + String? get uid => user.uid; + + // bool get existsFirebaseUser => firebaseUser != null; + + @override + String toString() { + return 'AccountAuth{user: $user}'; + } +} + +class FirebaseAccountAuth { + final GuruUser user; + final User? firebaseUser; + + FirebaseAccountAuth(this.user, {this.firebaseUser}); + + bool get isValid => uid != null && uid != ""; + + String? get guruToken => user.token; + + String? get uid => user.uid; bool get existsFirebaseUser => firebaseUser != null; @override String toString() { - return 'AccountAuth{user: $user, firebaseUser: $firebaseUser}'; + return 'AccountAuth{user: $user}'; } } + +abstract class IAccountAuthDelegate { + /// 支持登陆的代理,这块不管返回不返回都会支持匿名登陆 + List get supportedAuthCredentialDelegates => + AccountManager.defaultSupportedAuthCredentialDelegates; + + /// 返回设备共享的用户属性,在帐号切换的时候会保证这些 KEY,会保留下来 + /// 注意,这里尽量不要把用户相关的属性设到这里面,否则会出现不必要的问题 + Set get deviceSharedProperties => {}; + + /// 这个方法调用时,中台会确保当前的数据系统是切换后的数据系统 + /// 因此可以放心使用模板的数据,当 processor 返回后,对应的数据系统将会被切换 + /// 因此确保 processor 返回后,数据系统已经切换到新用户的数据系统 + Future onLogin(GuruUser loginUser, Credential credential); + + /// 这个方法调用时,中台会确保当前的数据系统是切换前的数据系统 + /// 因此可以放心使用模板的数据,当 processor返回后,对应的数据系统将会被切换 + /// 因此确保 processor 返回前,数据系统已经完成老用户数据的迁移 + Future onLogout(GuruUser logoutUser); + + /// 当出现登陆冲突时,有可能当前是匿名帐号,而这时,中台模板会静默登入到新的帐号中 + /// 与此同时,中台会调用 onAnonymousLogout + Future onAnonymousLogout(GuruUser logoutUser) async { + return true; + } + + /// 当 APP首次匿名登陆时,或当登陆一个没有绑定匿名Credential的帐号时,会在登出时重新登陆一个新的匿名帐号 + /// 中台在这种情况下会调用 onAnonymousLogin + Future onAnonymousLogin(GuruUser loginUser, Credential credential) async { + return true; + } + + /// 当登陆时,有可能会出现帐号冲突,因此针对冲突可以选择切换帐号或者忽略 + /// 如果你选择切换帐号,那么你需要提供 onLogout 方法,用于处理老用户的数据迁移 + /// 返回值为是否继续 + Future onConflict(); +} diff --git a/guru_app/lib/account/model/credential.dart b/guru_app/lib/account/model/credential.dart new file mode 100644 index 0000000..f0258dc --- /dev/null +++ b/guru_app/lib/account/model/credential.dart @@ -0,0 +1,37 @@ +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; + +class AnonymousCredential extends Credential { + final String secretKey; + + AnonymousCredential(this.secretKey); + + @override + String get token => secretKey; + + @override + AuthType get authType => AuthType.anonymous; + + String toJson() => secretKey; +} + +class AnonymousCredentialDelegate extends AuthCredentialDelegate { + const AnonymousCredentialDelegate(); + + @override + AuthType get authType => AuthType.anonymous; + + @override + Future login() async { + final secretKey = await AppProperty.getInstance().getAnonymousSecretKey(); + return AuthResult.success(AnonymousCredential(secretKey)); + } + + @override + Future logout() async {} + + @override + Credential deserializeCredential(String data) { + return AnonymousCredential(data); + } +} diff --git a/guru_app/lib/account/model/user.dart b/guru_app/lib/account/model/user.dart index af8844b..b850c3b 100644 --- a/guru_app/lib/account/model/user.dart +++ b/guru_app/lib/account/model/user.dart @@ -4,8 +4,11 @@ import 'package:json_annotation/json_annotation.dart'; part 'user.g.dart'; +@Deprecated("Use Guru User instead") +typedef SaasUser = GuruUser; + @JsonSerializable() -class SaasUser { +class GuruUser { @JsonKey(name: 'uid', defaultValue: "") final String uid; @@ -23,22 +26,22 @@ class SaasUser { bool get isValid => (uid != "") && (token.isNotEmpty == true) && (firebaseToken.isNotEmpty == true); - SaasUser( + GuruUser( {required this.uid, required this.token, required this.firebaseToken, this.createAtTimestamp = 0}); - factory SaasUser.fromJson(Map json) => _$SaasUserFromJson(json); + factory GuruUser.fromJson(Map json) => _$GuruUserFromJson(json); - Map toJson() => _$SaasUserToJson(this); + Map toJson() => _$GuruUserToJson(this); - SaasUser copyWith({String? firebaseToken, String? token}) { - return SaasUser( + GuruUser copyWith({String? firebaseToken, String? token}) { + return GuruUser( uid: uid, token: token ?? this.token, firebaseToken: firebaseToken ?? this.firebaseToken); } - bool isSame(SaasUser? user) { + bool isSame(GuruUser? user) { return uid == user?.uid && token == user?.token && firebaseToken == user?.firebaseToken && @@ -70,6 +73,63 @@ class AnonymousLoginReqBody { Map toJson() => _$AnonymousLoginReqBodyToJson(this); } +@JsonSerializable() +class FacebookLoginReqBody { + @JsonKey(name: 'accessToken', defaultValue: "") + final String? accessToken; + + FacebookLoginReqBody({this.accessToken}); + + @override + String toString() { + return 'FacebookLoginReqBody{accessToken: $accessToken}'; + } + + factory FacebookLoginReqBody.fromJson(Map json) => + _$FacebookLoginReqBodyFromJson(json); + + Map toJson() => _$FacebookLoginReqBodyToJson(this); +} + +@JsonSerializable() +class GoogleLoginReqBody { + @JsonKey(name: 'idToken', defaultValue: "") + final String? idToken; + + GoogleLoginReqBody({this.idToken}); + + @override + String toString() { + return 'GoogleLoginReqBody{idToken: $idToken}'; + } + + factory GoogleLoginReqBody.fromJson(Map json) => + _$GoogleLoginReqBodyFromJson(json); + + Map toJson() => _$GoogleLoginReqBodyToJson(this); +} + +@JsonSerializable() +class AppleLoginReqBody { + @JsonKey(name: 'token', defaultValue: "") + final String? token; + + @JsonKey(name: 'clientType', defaultValue: "ios") + final String clientType; + + AppleLoginReqBody({this.token, this.clientType = "ios"}); + + @override + String toString() { + return 'AppleLoginReqBody{token: $token, clientType: $clientType}'; + } + + factory AppleLoginReqBody.fromJson(Map json) => + _$AppleLoginReqBodyFromJson(json); + + Map toJson() => _$AppleLoginReqBodyToJson(this); +} + @JsonSerializable() class FirebaseTokenData { @JsonKey(name: 'uid', defaultValue: "") @@ -78,7 +138,7 @@ class FirebaseTokenData { @JsonKey(name: 'firebaseToken', defaultValue: "") final String firebaseToken; - FirebaseTokenData({required this.uid, required this.firebaseToken}); + FirebaseTokenData({this.uid = "", this.firebaseToken = ""}); factory FirebaseTokenData.fromJson(Map json) => _$FirebaseTokenDataFromJson(json); @@ -91,9 +151,45 @@ class FirebaseTokenData { } } +@JsonSerializable() +class UserAuthInfo { + @JsonKey(name: "secret", defaultValue: "") + final String secret; + + @JsonKey(name: 'providerList', defaultValue: const []) + final List providerList; + + factory UserAuthInfo.fromJson(Map json) => _$UserAuthInfoFromJson(json); + + Map toJson() => _$UserAuthInfoToJson(this); + + UserAuthInfo({this.secret = "", this.providerList = const []}); + + @override + String toString() { + return 'UserAuthList{providerList: $providerList}'; + } +} + +@JsonSerializable() +class UnbindReqBody { + @JsonKey(name: 'provider', defaultValue: "") + final String provider; + + UnbindReqBody({this.provider = ""}); + + @override + String toString() { + return 'UnbindReqBody{provider: $provider}'; + } + + factory UnbindReqBody.fromJson(Map json) => _$UnbindReqBodyFromJson(json); + + Map toJson() => _$UnbindReqBodyToJson(this); +} class UserAttr { static const real = 0; static const tester = 10; static const machine = 100; -} \ No newline at end of file +} diff --git a/guru_app/lib/account/model/user.g.dart b/guru_app/lib/account/model/user.g.dart index ff09843..a4ee25b 100644 --- a/guru_app/lib/account/model/user.g.dart +++ b/guru_app/lib/account/model/user.g.dart @@ -6,14 +6,14 @@ part of 'user.dart'; // JsonSerializableGenerator // ************************************************************************** -SaasUser _$SaasUserFromJson(Map json) => SaasUser( +GuruUser _$GuruUserFromJson(Map json) => GuruUser( uid: json['uid'] as String? ?? '', token: json['token'] as String? ?? '', firebaseToken: json['firebaseToken'] as String? ?? '', createAtTimestamp: json['createdAtTimestamp'] as int? ?? 0, ); -Map _$SaasUserToJson(SaasUser instance) => { +Map _$GuruUserToJson(GuruUser instance) => { 'uid': instance.uid, 'token': instance.token, 'firebaseToken': instance.firebaseToken, @@ -32,6 +32,40 @@ Map _$AnonymousLoginReqBodyToJson( 'secret': instance.secret, }; +FacebookLoginReqBody _$FacebookLoginReqBodyFromJson( + Map json) => + FacebookLoginReqBody( + accessToken: json['accessToken'] as String? ?? '', + ); + +Map _$FacebookLoginReqBodyToJson( + FacebookLoginReqBody instance) => + { + 'accessToken': instance.accessToken, + }; + +GoogleLoginReqBody _$GoogleLoginReqBodyFromJson(Map json) => + GoogleLoginReqBody( + idToken: json['idToken'] as String? ?? '', + ); + +Map _$GoogleLoginReqBodyToJson(GoogleLoginReqBody instance) => + { + 'idToken': instance.idToken, + }; + +AppleLoginReqBody _$AppleLoginReqBodyFromJson(Map json) => + AppleLoginReqBody( + token: json['token'] as String? ?? '', + clientType: json['clientType'] as String? ?? 'ios', + ); + +Map _$AppleLoginReqBodyToJson(AppleLoginReqBody instance) => + { + 'token': instance.token, + 'clientType': instance.clientType, + }; + FirebaseTokenData _$FirebaseTokenDataFromJson(Map json) => FirebaseTokenData( uid: json['uid'] as String? ?? '', @@ -43,3 +77,27 @@ Map _$FirebaseTokenDataToJson(FirebaseTokenData instance) => 'uid': instance.uid, 'firebaseToken': instance.firebaseToken, }; + +UserAuthInfo _$UserAuthInfoFromJson(Map json) => UserAuthInfo( + secret: json['secret'] as String? ?? '', + providerList: (json['providerList'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], + ); + +Map _$UserAuthInfoToJson(UserAuthInfo instance) => + { + 'secret': instance.secret, + 'providerList': instance.providerList, + }; + +UnbindReqBody _$UnbindReqBodyFromJson(Map json) => + UnbindReqBody( + provider: json['provider'] as String? ?? '', + ); + +Map _$UnbindReqBodyToJson(UnbindReqBody instance) => + { + 'provider': instance.provider, + }; diff --git a/guru_app/lib/ads/ads_manager.dart b/guru_app/lib/ads/ads_manager.dart index e02c4a6..84322e6 100644 --- a/guru_app/lib/ads/ads_manager.dart +++ b/guru_app/lib/ads/ads_manager.dart @@ -560,18 +560,26 @@ class AdsManager extends AdsManagerDelegate { return ad; } - Future requestGdpr({int? debugGeography, String? testDeviceId}) { + Future requestGdpr({int? debugGeography, String? testDeviceId}) async { Log.d("requestGdpr! debugGeography:$debugGeography testDeviceId:$testDeviceId", tag: "Ads"); // adb logcat -s UserMessagingPlatform // Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("xxxx") to set this as a debug device. - return GuruApplovinFlutter.instance + final result = await GuruApplovinFlutter.instance .requestGdpr(debugGeography: debugGeography, testDeviceId: testDeviceId); + final consentResult = await GuruAnalytics.instance.refreshConsents(); + Log.d("requestGdpr result:$result consentResult:$consentResult"); + return result; } Future resetGdpr() { return GuruApplovinFlutter.instance.resetGdpr(); } + Future updateOrientation(int orientation) async { + final result = await GuruApplovinFlutter.instance.updateOrientation(orientation); + return result == true; + } + @override Future createBannerAds({String? scene, AdsLifecycleObserver? observer}) async { final _adsProfile = adsProfile; @@ -583,21 +591,14 @@ class AdsManager extends AdsManagerDelegate { if (isPurchasedNoAd) { return AdCause.noAds; } - final _adsProfile = adsProfile; - Ads? ad = interstitialAds[_adsProfile.interstitialId]; - int hiddenAt = 0; - if (ad is AdsAudit) { - hiddenAt = ad.latestHiddenAt; - } - + final hiddenAt = AdsManager.instance.latestFullscreenAdsHiddenTimestamps; final now = DateTimeUtils.currentTimeInMillis(); final impGapInMillis = AdsManager.instance.adsConfig.interstitialConfig.getSceneImpGapInSeconds(scene) * 1000; Log.d( "canShowInterstitial($scene): now:$now latestFullscreenAdsHiddenTimestamps:$latestFullscreenAdsHiddenTimestamps hiddenAt:$hiddenAt impGapInMillis:$impGapInMillis", tag: "Ads"); - if ((now - AdsManager.instance.latestFullscreenAdsHiddenTimestamps) < 60000 || - ((now - hiddenAt) < impGapInMillis)) { + if ((now - hiddenAt) < impGapInMillis) { Log.d("show ads too frequency", syncFirebase: true); return AdCause.tooFrequent; } diff --git a/guru_app/lib/ads/core/ads_config.dart b/guru_app/lib/ads/core/ads_config.dart index 9709d56..8b92ecd 100644 --- a/guru_app/lib/ads/core/ads_config.dart +++ b/guru_app/lib/ads/core/ads_config.dart @@ -316,7 +316,7 @@ class AdInterstitialConfig { @joinedStringConvert final List scenes; - @JsonKey(name: "sp_scene", defaultValue: {"new_block": 120, "reset_scs": 120}) + @JsonKey(name: "sp_scene", defaultValue: {}) @configStringIntMapStringConvert final Map specialScenes; @@ -329,8 +329,8 @@ class AdInterstitialConfig { @JsonKey(name: "amazon_enable", defaultValue: false) final bool amazonEnable; - @JsonKey(name: "imp_gap_s", defaultValue: 120) - final int impGapInSeconds; + @JsonKey(name: "imp_gap_s") + final int? impGapInSeconds; AdInterstitialConfig(this.freeInSecond, this.validation, this.scenes, this.retryMinTimeInSecond, this.retryMaxTimeInSecond, @@ -346,7 +346,10 @@ class AdInterstitialConfig { } int getSceneImpGapInSeconds(String scene) { - return specialScenes[scene] ?? impGapInSeconds; + return (specialScenes[scene] ?? + impGapInSeconds ?? + GuruApp.instance.appSpec.deployment.fullscreenAdsMinInterval) + .clamp(5, 600); } Future checkFreeTime() async { diff --git a/guru_app/lib/ads/core/ads_config.g.dart b/guru_app/lib/ads/core/ads_config.g.dart index b8be8a1..e472f82 100644 --- a/guru_app/lib/ads/core/ads_config.g.dart +++ b/guru_app/lib/ads/core/ads_config.g.dart @@ -132,10 +132,10 @@ AdInterstitialConfig _$AdInterstitialConfigFromJson( json['retry_max_s'] as int? ?? 600, amazonEnable: json['amazon_enable'] as bool? ?? false, specialScenes: json['sp_scene'] == null - ? {'new_block': 120, 'reset_scs': 120} + ? {} : configStringIntMapStringConvert .fromJson(json['sp_scene'] as String), - impGapInSeconds: json['imp_gap_s'] as int? ?? 120, + impGapInSeconds: json['imp_gap_s'] as int?, ); Map _$AdInterstitialConfigToJson( diff --git a/guru_app/lib/ads/core/ads_impression.dart b/guru_app/lib/ads/core/ads_impression.dart index f3abef7..dea1260 100644 --- a/guru_app/lib/ads/core/ads_impression.dart +++ b/guru_app/lib/ads/core/ads_impression.dart @@ -3,9 +3,11 @@ import 'dart:io'; import 'package:guru_app/analytics/guru_analytics.dart'; import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart'; +import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; import 'package:guru_app/property/property_keys.dart'; import 'package:guru_utils/collection/collectionutils.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:guru_utils/log/log.dart'; import 'package:guru_applovin_flutter/ad_impression.dart'; @@ -65,26 +67,7 @@ class AdImpressionController { } final payloadMap = json.decode(payload); ImpressionData impressionData = ImpressionData.fromJson(payloadMap); - // 判断是不是facebook的network - // if (impressionData.networkName == "undisclosed") { - // final calibrationCpm = await facebookCalibrator.getCpm(impressionData.unitFormat, impressionData.country); - // if (calibrationCpm > 0) { - // final newImpressionData = impressionData.derive(newPublisherRevenue: calibrationCpm); - // if (facebookCalibrator.config?.fbIrldReport == true) { - // AnalyticsUtils.logEventEx("tch_fb_ad_rev", value: calibrationCpm, parameters: { - // FirebaseEventsParams.AD_FORMAT: impressionData.unitFormat, - // FirebaseEventsParams.AD_UNIT_NAME: impressionData.unitName, - // FirebaseEventsParams.CURRENCY: impressionData.currency, - // "country": impressionData.country, - // "mopub_rev": impressionData.publisherRevenue - // }); - // } - // - // impressionData = newImpressionData; - // } - // } await refreshLtv(impressionData); - // _reportAdImpression(arguments); final jsonPayload = jsonEncode(impressionData.payload); latestImpressionPayload = jsonPayload; @@ -119,12 +102,13 @@ class AdImpressionController { final currency = impressionData.currency; if (revenue != -1) { _logAdRevenue(impressionData); + // if () // _logAdLtv(revenue: revenue, adPlatform: adPlatform, currency: currency); } Log.d("refreshLtv payload:${impressionData.payload}"); } - // _logAdLtv({double revenue = 0.0, String adPlatform = "MAX", String currency = ""}) async { + // Future _logAdLtv({double revenue = 0.0, String adPlatform = "MAX", String currency = ""}) async { // final nowDate = DateTimeUtils.yyyyMMddUtcNum; // final appProperty = AppProperty.getInstance(); // final totalLtv = await appProperty.getDouble(PropertyKeys.totalLtv, defValue: 0.0); @@ -179,11 +163,25 @@ class AdImpressionController { totalRevenue += data.publisherRevenue; if (totalRevenue >= 0.01) { GuruAnalytics.instance.logAdRevenue(totalRevenue, data.platform, data.currency); - GuruAnalytics.instance.logPurchase(totalRevenue, - currency: data.currency, contentId: "MAX", adPlatform: "MAX"); - + if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 1) { + GuruAnalytics.instance.logPurchase(totalRevenue, + currency: data.currency, contentId: "MAX", adPlatform: "MAX"); + } totalRevenue = .0; } appProperty.setDouble(PropertyKeys.totalRevenue, totalRevenue); + + double totalRevenue020 = + await appProperty.getDouble(PropertyKeys.totalRevenue020, defValue: 0.0); + totalRevenue020 += data.publisherRevenue; + if (totalRevenue020 >= 0.2) { + GuruAnalytics.instance.logAdRevenue020(totalRevenue020, data.platform, data.currency); + if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 2) { + GuruAnalytics.instance.logPurchase(totalRevenue020, + currency: data.currency, contentId: "MAX", adPlatform: "MAX"); + } + totalRevenue020 = .0; + } + appProperty.setDouble(PropertyKeys.totalRevenue020, totalRevenue020); } } diff --git a/guru_app/lib/analytics/abtest/abtest_model.dart b/guru_app/lib/analytics/abtest/abtest_model.dart new file mode 100644 index 0000000..434d8d2 --- /dev/null +++ b/guru_app/lib/analytics/abtest/abtest_model.dart @@ -0,0 +1,388 @@ +import 'dart:io'; + +import 'package:guru_app/account/account_data_store.dart'; +import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_utils/device/device_utils.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/random/random_utils.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'abtest_model.g.dart'; + +class ConditionOpt { + static const equals = "eq"; + static const greaterThan = "gt"; + static const greaterThanOrEquals = "gte"; + static const lessThan = "lt"; + static const lessThanOrEquals = "lte"; + static const notEquals = "ne"; + + static bool evaluate(T value, T target, String opt) { + final result = value.compareTo(target); + switch (opt) { + case ConditionOpt.equals: + return result == 0; + case ConditionOpt.greaterThan: + return result > 0; + case ConditionOpt.greaterThanOrEquals: + return result >= 0; + case ConditionOpt.lessThan: + return result < 0; + case ConditionOpt.lessThanOrEquals: + return result <= 0; + case ConditionOpt.notEquals: + return result != 0; + default: + return false; + } + } +} + +abstract class ABTestFilter { + static const platform = 1; + static const version = 2; + static const country = 3; + static const newUser = 4; + + final int type; + + const ABTestFilter(this.type); + + bool filter(); + + factory ABTestFilter.fromJson(Map json) { + final type = json["type"] = json["type"] ?? 0; + switch (type) { + case ABTestFilter.platform: + return PlatformFilter.fromJson(json); + case ABTestFilter.country: + return CountryFilter.fromJson(json); + case ABTestFilter.version: + return VersionFilter.fromJson(json); + case ABTestFilter.newUser: + return NewUserFilter.fromJson(json); + default: + throw UnimplementedError("Unknown ABTestFilter type: $type"); + } + } + + Map toJson() => toJson()..addAll({"type": type}); +} + +abstract class ABTestCondition { + bool validate(); +} + +/// 为了后面可以做一些定制,因此这里按平台进行区分 +@JsonSerializable() +class AndroidCondition extends ABTestCondition { + @JsonKey(name: "opt") + final String? opt; + + @JsonKey(name: "sdk") + final int? sdkInt; + + AndroidCondition({this.opt, this.sdkInt}); + + factory AndroidCondition.fromJson(Map json) => _$AndroidConditionFromJson(json); + + Map toJson() => _$AndroidConditionToJson(this); + + @override + bool validate() { + final versionOpt = opt; + final targetVersion = sdkInt; + if (versionOpt != null && targetVersion != null) { + final versionCode = DeviceUtils.peekOSVersion(); + // 操作系统版本号获取失败,直接返回false + if (versionCode == -1) { + return false; + } + if (!ConditionOpt.evaluate(versionCode, targetVersion, versionOpt)) { + return false; + } + } + // 方便后面扩展其它字段 + return true; + } +} + +@JsonSerializable() +class IosCondition extends ABTestCondition { + @JsonKey(name: "opt") + final String? opt; + + @JsonKey(name: "ver") + final int? version; // 这里只记录大版本号 + + IosCondition({this.opt, this.version}); + + factory IosCondition.fromJson(Map json) => _$IosConditionFromJson(json); + + Map toJson() => _$IosConditionToJson(this); + + @override + bool validate() { + final versionOpt = opt; + final targetVersion = version; + if (versionOpt != null && targetVersion != null) { + final versionCode = DeviceUtils.peekOSVersion(); + // 操作系统版本号获取失败,直接返回false + if (versionCode == -1) { + return false; + } + if (!ConditionOpt.evaluate(versionCode, targetVersion, versionOpt)) { + return false; + } + } + // 方便后面扩展其它字段 + return true; + } +} + +@JsonSerializable() +class PlatformFilter extends ABTestFilter { + @JsonKey(name: "ac") + final AndroidCondition? androidCondition; + + @JsonKey(name: "ic") + final IosCondition? iosCondition; + + PlatformFilter({this.androidCondition, this.iosCondition}) : super(ABTestFilter.platform); + + @override + bool filter() { + // 如果配了 Platform Filter, 如果指定平台没有 condition, 则默认为true + if (Platform.isAndroid) { + return androidCondition?.validate() != false; + } else if (Platform.isIOS) { + return iosCondition?.validate() != false; + } + return false; + } + + factory PlatformFilter.fromJson(Map json) => _$PlatformFilterFromJson(json); + + @override + Map toJson() => _$PlatformFilterToJson(this); +} + +@JsonSerializable(constructor: "_") +class VersionFilter extends ABTestFilter { + @JsonKey(name: "opt") + final String opt; + + @JsonKey(name: "mmp") + final String mmp; // major.minor.patch + + VersionFilter._(this.opt, this.mmp) : super(ABTestFilter.version); + + VersionFilter.equals(this.mmp) + : opt = ConditionOpt.equals, + super(ABTestFilter.version); + + VersionFilter.greaterThan(this.mmp) + : opt = ConditionOpt.greaterThan, + super(ABTestFilter.version); + + VersionFilter.greaterThanOrEquals(this.mmp) + : opt = ConditionOpt.greaterThanOrEquals, + super(ABTestFilter.version); + + VersionFilter.lessThan(this.mmp) + : opt = ConditionOpt.lessThan, + super(ABTestFilter.version); + + VersionFilter.lessThanOrEquals(this.mmp) + : opt = ConditionOpt.lessThanOrEquals, + super(ABTestFilter.version); + + VersionFilter.notEquals(this.mmp) + : opt = ConditionOpt.notEquals, + super(ABTestFilter.version); + + @override + bool filter() { + final version = GuruSettings.instance.version.get(); + Log.d("[$runtimeType] $version $opt $mmp"); + return ConditionOpt.evaluate(version, mmp, opt); + } + + @override + String toString() { + return 'VersionValidator{opt: $opt, mmp: $mmp}'; + } + + factory VersionFilter.fromJson(Map json) => _$VersionFilterFromJson(json); + + @override + Map toJson() => _$VersionFilterToJson(this); +} + +@JsonSerializable(constructor: "_") +class CountryFilter extends ABTestFilter { + @JsonKey(name: "included", defaultValue: {}) + final Set included; + + @JsonKey(name: "excluded", defaultValue: {}) + final Set excluded; + + CountryFilter._(this.included, this.excluded) : super(ABTestFilter.country); + + CountryFilter.included(this.included) + : excluded = {}, + super(ABTestFilter.country); + + CountryFilter.excluded(this.excluded) + : included = {}, + super(ABTestFilter.country); + + @override + bool filter() { + final String countryCode = Platform.localeName.split('_').safeLast?.toLowerCase() ?? ""; + Log.d("[$runtimeType] $countryCode included: $included excluded: $excluded"); + if (countryCode.isEmpty) { + return false; + } + +// 如果excluded不为空,证明存在排除选项,该validate将只判断所有excluded中的逻辑, +// 将不会在判断included中的逻辑 + if (excluded.isNotEmpty) { + return !excluded.contains(countryCode); + } + + if (included.contains(countryCode)) { + return true; + } + return false; + } + + factory CountryFilter.fromJson(Map json) => _$CountryFilterFromJson(json); + + @override + Map toJson() => _$CountryFilterToJson(this); +} + +@JsonSerializable() +class NewUserFilter extends ABTestFilter { + NewUserFilter() : super(ABTestFilter.newUser); + + @override + bool filter() { + final version = GuruSettings.instance.version.get(); + final fiv = GuruSettings.instance.firstInstallVersion.get(); + return fiv.startsWith(version); + } + + factory NewUserFilter.fromJson(Map json) => _$NewUserFilterFromJson(json); + + @override + Map toJson() => _$NewUserFilterToJson(this); +} + +@JsonSerializable() +class ABTestAudience { + @JsonKey(name: "filters") + final List filters; + + @JsonKey(name: "variant", defaultValue: 2) + final int variant; + + ABTestAudience({required this.filters, this.variant = 2}); + + factory ABTestAudience.fromJson(Map json) => _$ABTestAudienceFromJson(json); + + Map toJson() => _$ABTestAudienceToJson(this); + + @override + String toString() { + return 'ABTestAudience{filters: $filters, variant: $variant}'; + } + + bool validate() { + for (var filter in filters) { + if (!filter.filter()) { + return false; + } + } + return true; + } +} + +@JsonSerializable() +class ABTestExperiment { + @JsonKey(name: "name") + final String name; + + @JsonKey(name: "start_ts", defaultValue: 0) + final int startTs; + + @JsonKey(name: "end_ts", defaultValue: 0) + final int endTs; + + @JsonKey(name: "audience") + final ABTestAudience audience; + + ABTestExperiment( + {required String name, required this.startTs, required this.endTs, required this.audience}) + : name = _validExperimentName(name); + + @override + String toString() { + return 'ABTestExperiment{name: $name, startTs: $startTs, endTs: $endTs, audience: $audience}'; + } + + static String _validExperimentName(String experimentName) { + if (experimentName.contains(RemoteConfigManager.invalidABKeyRegExp)) { + Log.w("abName($experimentName) use invalid key! $experimentName! replace invalid char to _"); + experimentName = experimentName.replaceAll(RemoteConfigManager.invalidABKeyRegExp, "_"); + } else { + if (experimentName.length > 20) { + experimentName = experimentName.substring(0, 20); + } + } + return experimentName; + } + + factory ABTestExperiment.fromJson(Map json) => _$ABTestExperimentFromJson(json); + + Map toJson() => _$ABTestExperimentToJson(this); + + bool isExpired() { + final now = DateTime.now().millisecondsSinceEpoch; + return now < startTs || now > endTs; + } + + bool isMatchAudience() { + return audience.validate(); + } + + @JsonKey(includeToJson: false) + String? _variantName; + + String get variantName => + (_variantName ??= _toVariantName(RandomUtils.nextInt(audience.variant))); + + static const _originalVariant = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + String _toVariantName(int value) { + String codes = ""; + int nv = value; + while (true) { + final nextNum = nv ~/ _originalVariant.length; + if (nextNum <= 0) { + break; + } + codes = "${_originalVariant[nv % _originalVariant.length]}$codes"; + nv = nextNum; + } + + final tailIndex = nv % _originalVariant.length; + if (tailIndex >= 0) { + codes = "${_originalVariant[tailIndex]}$codes"; + } + return codes.toString(); + } +} diff --git a/guru_app/lib/analytics/abtest/abtest_model.g.dart b/guru_app/lib/analytics/abtest/abtest_model.g.dart new file mode 100644 index 0000000..8163bfa --- /dev/null +++ b/guru_app/lib/analytics/abtest/abtest_model.g.dart @@ -0,0 +1,109 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'abtest_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AndroidCondition _$AndroidConditionFromJson(Map json) => + AndroidCondition( + opt: json['opt'] as String?, + sdkInt: json['sdk'] as int?, + ); + +Map _$AndroidConditionToJson(AndroidCondition instance) => + { + 'opt': instance.opt, + 'sdk': instance.sdkInt, + }; + +IosCondition _$IosConditionFromJson(Map json) => IosCondition( + opt: json['opt'] as String?, + version: json['ver'] as int?, + ); + +Map _$IosConditionToJson(IosCondition instance) => + { + 'opt': instance.opt, + 'ver': instance.version, + }; + +PlatformFilter _$PlatformFilterFromJson(Map json) => + PlatformFilter( + androidCondition: json['ac'] == null + ? null + : AndroidCondition.fromJson(json['ac'] as Map), + iosCondition: json['ic'] == null + ? null + : IosCondition.fromJson(json['ic'] as Map), + ); + +Map _$PlatformFilterToJson(PlatformFilter instance) => + { + 'ac': instance.androidCondition, + 'ic': instance.iosCondition, + }; + +VersionFilter _$VersionFilterFromJson(Map json) => + VersionFilter._( + json['opt'] as String, + json['mmp'] as String, + ); + +Map _$VersionFilterToJson(VersionFilter instance) => + { + 'opt': instance.opt, + 'mmp': instance.mmp, + }; + +CountryFilter _$CountryFilterFromJson(Map json) => + CountryFilter._( + (json['included'] as List?)?.map((e) => e as String).toSet() ?? + {}, + (json['excluded'] as List?)?.map((e) => e as String).toSet() ?? + {}, + ); + +Map _$CountryFilterToJson(CountryFilter instance) => + { + 'included': instance.included.toList(), + 'excluded': instance.excluded.toList(), + }; + +NewUserFilter _$NewUserFilterFromJson(Map json) => + NewUserFilter(); + +Map _$NewUserFilterToJson(NewUserFilter instance) => + {}; + +ABTestAudience _$ABTestAudienceFromJson(Map json) => + ABTestAudience( + filters: (json['filters'] as List) + .map((e) => ABTestFilter.fromJson(e as Map)) + .toList(), + variant: json['variant'] as int? ?? 2, + ); + +Map _$ABTestAudienceToJson(ABTestAudience instance) => + { + 'filters': instance.filters, + 'variant': instance.variant, + }; + +ABTestExperiment _$ABTestExperimentFromJson(Map json) => + ABTestExperiment( + name: json['name'] as String, + startTs: json['start_ts'] as int? ?? 0, + endTs: json['end_ts'] as int? ?? 0, + audience: + ABTestAudience.fromJson(json['audience'] as Map), + ); + +Map _$ABTestExperimentToJson(ABTestExperiment instance) => + { + 'name': instance.name, + 'start_ts': instance.startTs, + 'end_ts': instance.endTs, + 'audience': instance.audience, + }; diff --git a/guru_app/lib/analytics/data/analytics_model.dart b/guru_app/lib/analytics/data/analytics_model.dart index 64dd11a..96437aa 100644 --- a/guru_app/lib/analytics/data/analytics_model.dart +++ b/guru_app/lib/analytics/data/analytics_model.dart @@ -8,6 +8,8 @@ part 'analytics_model.g.dart'; @JsonSerializable() class AnalyticsConfig { + static const _defaultGoogleDma = [1, 0, 12, 65]; + static const _defaultDmaCountry = []; @JsonKey(name: "cap", defaultValue: ["firebase", "facebook", "guru"]) @joinedStringConvert final List capabilities; @@ -24,6 +26,13 @@ class AnalyticsConfig { @JsonKey(name: "enabled_strategy", defaultValue: false) final bool enabledStrategy; + /// ad_storage,analytics_storage,personalization,user_data + @JsonKey(name: "google_dma", defaultValue: _defaultGoogleDma) + final List googleDmaMask; + + @JsonKey(name: "dma_country", defaultValue: _defaultDmaCountry) + final List dmaCountry; + AppEventCapabilities toAppEventCapabilities() { int capValue = 0; if (capabilities.contains("firebase")) { @@ -38,8 +47,15 @@ class AnalyticsConfig { return AppEventCapabilities(capValue); } + bool googleDmaGranted(ConsentType type, int flags) { + if (type.index < googleDmaMask.length) { + return (googleDmaMask[type.index] & flags) == googleDmaMask[type.index]; + } + return _defaultGoogleDma[type.index] & flags == _defaultGoogleDma[type.index]; + } + AnalyticsConfig(this.capabilities, this.delayedInSeconds, this.expiredInDays, this.strategy, - this.enabledStrategy); + this.enabledStrategy, this.googleDmaMask, this.dmaCountry); factory AnalyticsConfig.fromJson(Map json) => _$AnalyticsConfigFromJson(json); @@ -72,3 +88,12 @@ class UserIdentification { return 'UserIdentification{firebaseAppInstanceId: $firebaseAppInstanceId, idfa: $idfa, adId: $adId, gpsAdId: $gpsAdId}'; } } + +class ConsentFieldName { + static const adStorage = "ad_storage"; + static const analyticsStorage = "analytics_storage"; + static const adPersonalization = "ad_personalization"; + static const adUserData = "ad_user_data"; +} + +enum ConsentType { adStorage, analyticsStorage, adPersonalization, adUserData } diff --git a/guru_app/lib/analytics/data/analytics_model.g.dart b/guru_app/lib/analytics/data/analytics_model.g.dart index 47a34ce..7f7280d 100644 --- a/guru_app/lib/analytics/data/analytics_model.g.dart +++ b/guru_app/lib/analytics/data/analytics_model.g.dart @@ -15,6 +15,12 @@ AnalyticsConfig _$AnalyticsConfigFromJson(Map json) => json['expired_d'] as int? ?? 7, json['strategy'] as String? ?? '', json['enabled_strategy'] as bool? ?? false, + (json['google_dma'] as List?)?.map((e) => e as int).toList() ?? + [1, 0, 12, 65], + (json['dma_country'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], ); Map _$AnalyticsConfigToJson(AnalyticsConfig instance) => @@ -24,6 +30,8 @@ Map _$AnalyticsConfigToJson(AnalyticsConfig instance) => 'expired_d': instance.expiredInDays, 'strategy': instance.strategy, 'enabled_strategy': instance.enabledStrategy, + 'google_dma': instance.googleDmaMask, + 'dma_country': instance.dmaCountry, }; UserIdentification _$UserIdentificationFromJson(Map json) => diff --git a/guru_app/lib/analytics/guru_analytics.dart b/guru_app/lib/analytics/guru_analytics.dart index 4cb9aaa..e21360d 100644 --- a/guru_app/lib/analytics/guru_analytics.dart +++ b/guru_app/lib/analytics/guru_analytics.dart @@ -4,7 +4,9 @@ import 'dart:collection'; import 'dart:core'; import 'dart:io'; +import 'package:adjust_sdk/adjust_third_party_sharing.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/foundation.dart'; import 'package:guru_analytics_flutter/event_logger.dart'; import 'package:guru_analytics_flutter/event_logger_common.dart'; @@ -15,13 +17,18 @@ import 'package:guru_app/account/account_data_store.dart'; import 'package:guru_app/ads/ads_manager.dart'; import 'package:guru_app/ads/core/ads_config.dart'; import 'package:guru_app/aigc/bi/ai_bi.dart'; +import 'package:guru_app/analytics/abtest/abtest_model.dart'; import 'package:guru_app/analytics/data/analytics_model.dart'; import 'package:guru_app/analytics/strategy/guru_analytics_strategy.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; import 'package:guru_app/property/property_keys.dart'; import 'package:guru_app/property/runtime_property.dart'; +import 'package:guru_app/property/settings/guru_settings.dart'; +import 'package:guru_platform_data/guru_platform_data.dart'; +import 'package:guru_utils/collection/collectionutils.dart'; import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:guru_utils/device/device_info.dart'; import 'package:guru_utils/device/device_utils.dart'; @@ -38,7 +45,7 @@ part 'modules/ads_analytics.dart'; part 'modules/adjust_aware.dart'; class GuruAnalytics extends Analytics with AdjustAware { - bool get release => !_mock && _enabledAnalytics && kReleaseMode; + bool get release => !_mock && (_enabledAnalytics || kReleaseMode); String appInstanceId = ""; @@ -55,6 +62,10 @@ class GuruAnalytics extends Analytics with AdjustAware { static String currentScreen = ""; + static final RegExp _consentPurposeRegExp = RegExp(r"^[01]+$"); + + static String? mockCountryCode; + static const errorEventCodes = { 14, // 上报事件失败 22, // 网络状态不可用 @@ -71,8 +82,13 @@ class GuruAnalytics extends Analytics with AdjustAware { final BehaviorSubject guruEventStatistic = BehaviorSubject.seeded(GuruStatistic.invalid); + final BehaviorSubject> abTestExperimentVariant = BehaviorSubject.seeded({}); + Stream get observableGuruEventStatistic => guruEventStatistic.stream; + Stream> get observableABTestExperimentVariant => + abTestExperimentVariant.stream; + final BehaviorSubject userIdentificationSubject = BehaviorSubject.seeded(UserIdentification()); @@ -98,6 +114,19 @@ class GuruAnalytics extends Analytics with AdjustAware { return Analytics.userProperties[key]; } + Future prepare() async { + if (GuruApp.instance.appSpec.localABTestExperiments.isNotEmpty) { + await initLocalExperiments(); + } + RemoteConfigManager.instance.observeConfig().listen((config) { + Log.i( + "GuruAnalytics observeConfig changed: ${config.lastFetchStatus} ${config.lastFetchTime}"); + if (config.lastFetchStatus == RemoteConfigFetchStatus.success) { + refreshABProperties(); + } + }); + } + void init() async { Log.d( "AnalyticsUtil init### Platform.localeName :${Platform.localeName} ${Intl.getCurrentLocale()}"); @@ -142,6 +171,7 @@ class GuruAnalytics extends Analytics with AdjustAware { Future.delayed(const Duration(seconds: 1), () { initAdjust(); initFbEventMapping(); + refreshConsents(); Log.d("register transmitter"); }); initialized = true; @@ -151,6 +181,100 @@ class GuruAnalytics extends Analytics with AdjustAware { } } + Future switchSession(String oldToken, String newToken) async { + _initEnvProperties(); + _logLocale(); + _logDeviceType(); + } + + Future initLocalExperiments() async { + final runningExperiments = await AppProperty.getInstance().loadRunningExperiments(); + final experiments = GuruApp.instance.appSpec.localABTestExperiments; + final validRunningExperimentKeys = + runningExperiments.keys.toSet().intersection(experiments.keys.toSet()); + for (var experiment in experiments.values) { + // 如果在已经开始的实验中,但是不在当前的实验列表中,需要删除 + final needRemove = runningExperiments.containsKey(experiment.name) && + !validRunningExperimentKeys.contains(experiment.name); + if (needRemove) { + await removeExperiment(experiment.name); + } else { + await _applyExperiment(experiment); + } + } + } + + Future refreshConsents({AnalyticsConfig? analyticsConfig}) async { + final config = analyticsConfig ?? RemoteConfigManager.instance.getAnalyticsConfig(); + final purposeConsents = await GuruPlatformData.getPurposeConsents(); + Log.i("refreshConsents: '$purposeConsents'"); + if (purposeConsents.isEmpty) { + return ""; + } + + /// 如果他不是完全使用 1,0 组成的字符串 + if (!_consentPurposeRegExp.hasMatch(purposeConsents)) { + Log.i("invalid consents $purposeConsents"); + return ""; + } + + /// 获取当前的 countryCode, 判断是否在 dma country的范围内 + if (config.dmaCountry.isNotEmpty) { + final countryCode = getCountryCode(); + if (!config.dmaCountry.contains(countryCode)) { + Log.i("invalid country $countryCode"); + return ""; + } + } + + final length = min(purposeConsents.length, 32); + int flags = 0; + for (var i = 0; i < length; i++) { + flags |= (((purposeConsents[i] == "1") ? 1 : 0) << i); + } + + final consentsData = { + ConsentFieldName.adStorage: config.googleDmaGranted(ConsentType.adStorage, flags), + ConsentFieldName.analyticsStorage: + config.googleDmaGranted(ConsentType.analyticsStorage, flags), + ConsentFieldName.adPersonalization: + config.googleDmaGranted(ConsentType.adPersonalization, flags), + ConsentFieldName.adUserData: config.googleDmaGranted(ConsentType.adUserData, flags), + }; + + String _flag(String key) { + return consentsData[key] == true ? "1" : "0"; + } + + Log.d("setConsents consentsData: $consentsData"); + + try { + final result = await EventLogger.guru.setConsents(consentsData); + Log.d("setConsents result: $result"); + } catch (error, stacktrace) { + Log.e("setConsents error! $error, $stacktrace"); + } + + if (enabledAdjust) { + AdjustThirdPartySharing adjustThirdPartySharing = AdjustThirdPartySharing(null); + adjustThirdPartySharing.addGranularOption("google_dma", "eea", "1"); + adjustThirdPartySharing.addGranularOption( + "google_dma", "ad_personalization", _flag(ConsentFieldName.adPersonalization)); + adjustThirdPartySharing.addGranularOption( + "google_dma", "ad_user_data", _flag(ConsentFieldName.adUserData)); + Adjust.trackThirdPartySharing(adjustThirdPartySharing); + Log.d("setAdjust complete!"); + } + + final result = + "${_flag(ConsentFieldName.adStorage)}${_flag(ConsentFieldName.analyticsStorage)}${_flag(ConsentFieldName.adPersonalization)}${_flag(ConsentFieldName.adUserData)}"; + final changed = await AppProperty.getInstance().refreshGoogleDma(result); + if (changed || GuruSettings.instance.debugMode.get()) { + logEventEx("dma_gg", parameters: {"purpose": purposeConsents, "result": result}); + } + return result; + } + void processAnalyticsCallback(int code, String? errorInfo) { if (!errorEventCodes.contains(code)) { return; @@ -269,7 +393,10 @@ class GuruAnalytics extends Analytics with AdjustAware { if (firebaseId?.isNotEmpty == true) { setFirebaseId(firebaseId!); } + refreshABProperties(); + } + void refreshABProperties() { final abProperties = RemoteConfigManager.instance.getABProperties(); final PropertyBundle propertyBundle = PropertyBundle(); @@ -293,6 +420,17 @@ class GuruAnalytics extends Analytics with AdjustAware { setUserProperty("first_open_time", firstInstallTime.toString()); } + String getCountryCode() { + if (mockCountryCode != null) { + return mockCountryCode!; + } + final currentLocale = Platform.localeName.split('_'); + if (currentLocale.length > 1) { + return currentLocale.last.toLowerCase(); + } + return ""; + } + void _logLocale() { if (Platform.localeName.isNotEmpty == true) { String lanCode = ""; @@ -335,6 +473,60 @@ class GuruAnalytics extends Analytics with AdjustAware { } } + static String buildVariantKey(String experimentName) { + return "ab_$experimentName"; + } + + String getExperimentVariant(String experimentName) { + return abTestExperimentVariant.value[experimentName] ?? "BASELINE"; + } + + Future setLocalABTest(ABTestExperiment experiment, {PropertyBundle? bundle}) async { + Log.d("setLocalABTest: $experiment"); + String experimentName = experiment.name; + final exp = await AppProperty.getInstance().getExperiment(experimentName, bundle: bundle); + if (exp != null) { + Log.w("Experiment already exists!"); + experiment = exp; + } + + return await _applyExperiment(experiment); + } + + Future removeExperiment(String experimentName) async { + await AppProperty.getInstance().removeExperiment(experimentName); + final data = Map.of(abTestExperimentVariant.value); + data.remove(experimentName); + abTestExperimentVariant.addIfChanged(data); + } + + Future _applyExperiment(ABTestExperiment experiment) async { + final experimentName = experiment.name; + if (experiment.isExpired()) { + Log.w("Experiment($experimentName) is expired"); + await removeExperiment(experimentName); + return false; + } + + if (!experiment.isMatchAudience()) { + Log.i("NOT match audience! $experiment! INTO BASELINE"); + return false; + } + + String variantName = await AppProperty.getInstance().getExperimentVariant(experimentName); + if (variantName.isEmpty) { + variantName = await AppProperty.getInstance().setExperiment(experiment); + } + + await setGuruUserProperty(buildVariantKey(experimentName), variantName); + Log.i("==> Setup Local Experiment($experimentName) variantName: $variantName"); + + final data = Map.of(abTestExperimentVariant.value); + data[experimentName] = variantName; + abTestExperimentVariant.addIfChanged(data); + return true; + } + void setDeviceId(String deviceId) { Log.d("setDeviceId: $deviceId"); recordEvents("setDeviceId", {"userId": deviceId}); @@ -348,12 +540,12 @@ class GuruAnalytics extends Analytics with AdjustAware { } } - void setUserId(String userId) { + Future setUserId(String userId) async { Log.d("setUserId: $userId"); recordEvents("setUserId", {"userId": userId}); recordProperty("userId", userId); if (userId.isNotEmpty) { - AppProperty.getInstance().setUserId(userId); + await AppProperty.getInstance().setUserId(userId); if (release) { EventLogger.setUserId(userId); FirebaseCrashlytics.instance.setUserIdentifier(userId); @@ -528,12 +720,17 @@ class GuruAnalytics extends Analytics with AdjustAware { {String currency = "", String contentId = "", String adPlatform = "", - Map parameters = const {}}) { - EventLogger.logFbPurchase(amount, - currency: currency, - contentId: contentId, - adPlatform: adPlatform, - additionParameters: parameters); + Map parameters = const {}}) async { + Log.i("logPurchase:$amount, $currency, $contentId, $adPlatform, $parameters"); + try { + await EventLogger.logFbPurchase(amount, + currency: currency, + contentId: contentId, + adPlatform: adPlatform, + additionParameters: parameters); + } catch (error, stacktrace) { + Log.w("logFbPurchase error$error, $stacktrace"); + } } void logEventShare({String? itemCategory, String? itemName}) { @@ -547,6 +744,7 @@ class GuruAnalytics extends Analytics with AdjustAware { void logSpendCredits(String contentId, String contentType, int price, {required String virtualCurrencyName, required int balance, String scene = ''}) { + final levelName = GuruApp.instance.protocol.getLevelName(); if (release) { EventLogger.logSpendCredits(contentId, contentType, price, virtualCurrencyName: virtualCurrencyName, balance: balance, scene: scene); @@ -557,7 +755,8 @@ class GuruAnalytics extends Analytics with AdjustAware { "virtual_currency_name": virtualCurrencyName, "value": price, "balance": balance, - "scene": scene + "scene": scene, + "level_name": levelName }; Log.d("logEvent: spend_virtual_currency $parameters"); EventLogger.transmit("spend_virtual_currency", parameters); @@ -565,22 +764,34 @@ class GuruAnalytics extends Analytics with AdjustAware { AiBi.instance.spendVirtualCurrency(balance, price.toDouble(), contentType); } - Future logEarnVirtualCurrency({ - required String virtualCurrencyName, - required String method, - required int balance, - required int value, - }) async { - logEvent("earn_virtual_currency", { - "virtual_currency_name": virtualCurrencyName, - "item_category": method, - "value": value, - "balance": balance - }); + Future logEarnVirtualCurrency( + {required String virtualCurrencyName, + required String method, + required int balance, + required int value, + String? specific, + String? scene}) async { + final levelName = GuruApp.instance.protocol.getLevelName(); + logEvent( + "earn_virtual_currency", + filterOutNulls({ + "virtual_currency_name": virtualCurrencyName, + "item_category": method, + "item_name": specific, + "value": value, + "balance": balance, + "level_name": levelName, + "scene": scene + })); AiBi.instance.earnVirtualCurrency(balance, value.toDouble(), method); } + String? peekUserProperty(String key) { + return Analytics.userProperties[key]; + } + Future setGuruUserProperty(String key, String value) async { + recordProperty(key, value); return await EventLogger.setGuruUserProperty(key, value); } diff --git a/guru_app/lib/analytics/modules/ads_analytics.dart b/guru_app/lib/analytics/modules/ads_analytics.dart index 81ae8c0..fc213fa 100644 --- a/guru_app/lib/analytics/modules/ads_analytics.dart +++ b/guru_app/lib/analytics/modules/ads_analytics.dart @@ -7,15 +7,44 @@ part of "../guru_analytics.dart"; extension AdsAnalytics on GuruAnalytics { - void logAdRevenue(double adRevenue, String adPlatform, String currency) { + void logAdRevenue(double adRevenue, String adPlatform, String currency, + {String? orderType, String? orderId, String? productId, int? transactionDate}) { // logEventEx(name, itemCategory: scene, itemName: adName); + final orderExtras = CollectionUtils.filterOutNulls({ + "order_type": orderType, + "order_id": orderId, + "product_id": productId, + "trans_ts": transactionDate + }); if (release) { - EventLogger.logAdRevenue(adRevenue, adPlatform, currency); + EventLogger.logAdRevenue(adRevenue, adPlatform, currency, extras: orderExtras); } else { Log.d("[firebase] logAdRevenue ${{ "adRevenue": adRevenue, "adPlatform": adPlatform, - "currency": currency + "currency": currency, + ...orderExtras + }}"); + } + } + + void logAdRevenue020(double adRevenue, String adPlatform, String currency, + {String? orderType, String? orderId, String? productId, int? transactionDate}) { + // logEventEx(name, itemCategory: scene, itemName: adName); + final orderExtras = CollectionUtils.filterOutNulls({ + "order_type": orderType, + "order_id": orderId, + "product_id": productId, + "trans_ts": transactionDate + }); + if (release) { + EventLogger.logAdRevenue020(adRevenue, adPlatform, currency, extras: orderExtras); + } else { + Log.d("[firebase] logAdRevenue020 ${{ + "adRevenue": adRevenue, + "adPlatform": adPlatform, + "currency": currency, + ...orderExtras }}"); } } diff --git a/guru_app/lib/api/data/orders/orders_model.dart b/guru_app/lib/api/data/orders/orders_model.dart index 5ea9603..c6445c5 100644 --- a/guru_app/lib/api/data/orders/orders_model.dart +++ b/guru_app/lib/api/data/orders/orders_model.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:guru_app/analytics/data/analytics_model.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:json_annotation/json_annotation.dart'; part 'orders_model.g.dart'; @@ -14,8 +15,7 @@ class OrderUserInfo { OrderUserInfo(this.level); - factory OrderUserInfo.fromJson(Map json) => - _$OrderUserInfoFromJson(json); + factory OrderUserInfo.fromJson(Map json) => _$OrderUserInfoFromJson(json); Map toJson() => _$OrderUserInfoToJson(this); } @@ -66,6 +66,12 @@ class OrdersReport { @JsonKey(name: "eventConfig") UserIdentification? userIdentification; + @JsonKey(name: "orderId") + String? orderId; + + @JsonKey(name: "transactionDate") + int? transactionDate; + OrdersReport( {this.orderType, this.token, @@ -81,7 +87,9 @@ class OrdersReport { this.orderUserInfo, this.userIdentification, this.offerId, - this.basePlanId}); + this.basePlanId, + this.orderId, + this.transactionDate}); @override String toString() { @@ -91,6 +99,7 @@ class OrdersReport { sb.writeln(" price: $price"); sb.writeln(" currency: $currency"); sb.writeln(" userIdentification: $userIdentification"); + sb.writeln(" orderId: $orderId"); if (Platform.isAndroid) { sb.writeln(" orderType: $orderType"); sb.writeln(" packageName: $packageName"); @@ -108,8 +117,7 @@ class OrdersReport { .toString(); //'OrdersReport{orderType: $orderType, packageName: $packageName, productId: $productId, subscriptionId: $subscriptionId, token: $token, bundleId: $bundleId, receipt: $receipt, price: $price, currency: $currency, introductoryPrice: $introductoryPrice}'; } - factory OrdersReport.fromJson(Map json) => - _$OrdersReportFromJson(json); + factory OrdersReport.fromJson(Map json) => _$OrdersReportFromJson(json); Map toJson() => _$OrdersReportToJson(this); } @@ -126,8 +134,7 @@ class OrdersResponse { OrdersResponse(this.usdPrice, this.test); - factory OrdersResponse.fromJson(Map json) => - _$OrdersResponseFromJson(json); + factory OrdersResponse.fromJson(Map json) => _$OrdersResponseFromJson(json); Map toJson() => _$OrdersResponseToJson(this); diff --git a/guru_app/lib/api/data/orders/orders_model.g.dart b/guru_app/lib/api/data/orders/orders_model.g.dart index 2f08f47..658e838 100644 --- a/guru_app/lib/api/data/orders/orders_model.g.dart +++ b/guru_app/lib/api/data/orders/orders_model.g.dart @@ -37,6 +37,8 @@ OrdersReport _$OrdersReportFromJson(Map json) => OrdersReport( json['eventConfig'] as Map), offerId: json['offerId'] as String?, basePlanId: json['basePlanId'] as String?, + orderId: json['orderId'] as String?, + transactionDate: json['transactionDate'] as int?, ); Map _$OrdersReportToJson(OrdersReport instance) => @@ -56,6 +58,8 @@ Map _$OrdersReportToJson(OrdersReport instance) => 'currency': instance.currency, 'userInfo': instance.orderUserInfo, 'eventConfig': instance.userIdentification, + 'orderId': instance.orderId, + 'transactionDate': instance.transactionDate, }; OrdersResponse _$OrdersResponseFromJson(Map json) => diff --git a/guru_app/lib/api/guru_api.dart b/guru_app/lib/api/guru_api.dart index a1a93fe..381cc2f 100644 --- a/guru_app/lib/api/guru_api.dart +++ b/guru_app/lib/api/guru_api.dart @@ -7,6 +7,7 @@ import 'package:guru_app/account/account_data_store.dart'; import 'package:guru_app/account/model/user.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; import 'package:guru_utils/device/device_info.dart'; import 'package:guru_utils/device/device_utils.dart'; import 'package:retrofit/retrofit.dart'; @@ -120,10 +121,28 @@ abstract class GuruApiMethods { // Auth @POST("/auth/api/v1/tokens/provider/secret") - Future signInWithAnonymous(@Body() AnonymousLoginReqBody body); + Future signInWithAnonymous(@Body() AnonymousLoginReqBody body); + + @POST("/auth/api/v1/tokens/provider/facebook-gaming") + Future signInWithFacebook(@Body() FacebookLoginReqBody body); + + @POST("/auth/api/v1/tokens/provider/google") + Future signInWithGoogle(@Body() GoogleLoginReqBody body); + + @POST("/auth/api/v1/tokens/provider/apple") + Future signInWithApple(@Body() AppleLoginReqBody body); + + @POST("/auth/api/v1/bindings/provider/facebook-gaming") + Future associateWithFacebook(@Body() FacebookLoginReqBody body); + + @POST("/auth/api/v1/bindings/provider/google") + Future associateWithGoogle(@Body() GoogleLoginReqBody body); + + @POST("/auth/api/v1/bindings/provider/apple") + Future associateWithApple(@Body() AppleLoginReqBody body); @POST("/auth/api/v1/renewals/token") - Future refreshSaasToken(); + Future refreshSaasToken(); @POST("/auth/api/v1/renewals/firebase") Future renewFirebaseToken(); diff --git a/guru_app/lib/api/guru_api.g.dart b/guru_app/lib/api/guru_api.g.dart index dc68f39..b055b9c 100644 --- a/guru_app/lib/api/guru_api.g.dart +++ b/guru_app/lib/api/guru_api.g.dart @@ -46,14 +46,14 @@ class _GuruApiMethods implements GuruApiMethods { } @override - Future signInWithAnonymous(AnonymousLoginReqBody body) async { + Future signInWithAnonymous(AnonymousLoginReqBody body) async { const _extra = {}; final queryParameters = {}; final _headers = {}; final _data = {}; _data.addAll(body.toJson()); final _result = - await _dio.fetch>(_setStreamType(Options( + await _dio.fetch>(_setStreamType(Options( method: 'POST', headers: _headers, extra: _extra, @@ -69,18 +69,186 @@ class _GuruApiMethods implements GuruApiMethods { _dio.options.baseUrl, baseUrl, )))); - final value = SaasUser.fromJson(_result.data!); + final value = GuruUser.fromJson(_result.data!); return value; } @override - Future refreshSaasToken() async { + Future signInWithFacebook(FacebookLoginReqBody body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/tokens/provider/facebook-gaming', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = GuruUser.fromJson(_result.data!); + return value; + } + + @override + Future signInWithGoogle(GoogleLoginReqBody body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/tokens/provider/google', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = GuruUser.fromJson(_result.data!); + return value; + } + + @override + Future signInWithApple(AppleLoginReqBody body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/tokens/provider/apple', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = GuruUser.fromJson(_result.data!); + return value; + } + + @override + Future associateWithFacebook(FacebookLoginReqBody body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/bindings/provider/facebook-gaming', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = GuruUser.fromJson(_result.data!); + return value; + } + + @override + Future associateWithGoogle(GoogleLoginReqBody body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/bindings/provider/google', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = GuruUser.fromJson(_result.data!); + return value; + } + + @override + Future associateWithApple(AppleLoginReqBody body) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(body.toJson()); + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/auth/api/v1/bindings/provider/apple', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = GuruUser.fromJson(_result.data!); + return value; + } + + @override + Future refreshSaasToken() async { const _extra = {}; final queryParameters = {}; final _headers = {}; final Map? _data = null; final _result = - await _dio.fetch>(_setStreamType(Options( + await _dio.fetch>(_setStreamType(Options( method: 'POST', headers: _headers, extra: _extra, @@ -96,7 +264,7 @@ class _GuruApiMethods implements GuruApiMethods { _dio.options.baseUrl, baseUrl, )))); - final value = SaasUser.fromJson(_result.data!); + final value = GuruUser.fromJson(_result.data!); return value; } diff --git a/guru_app/lib/api/modules/guru_api_extension.dart b/guru_app/lib/api/modules/guru_api_extension.dart index 039212a..7b5a5ba 100644 --- a/guru_app/lib/api/modules/guru_api_extension.dart +++ b/guru_app/lib/api/modules/guru_api_extension.dart @@ -3,8 +3,36 @@ part of "../guru_api.dart"; extension GuruApiExtension on GuruApi { - Future signInWithAnonymous({required String secret}) async { - return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: secret)); + // Future signInWithAnonymous({required String secret}) async { + // return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: secret)); + // } + + Future loginGuruWithCredential({required Credential credential}) async { + switch (credential.authType) { + case AuthType.facebook: + return await methods + .signInWithFacebook(FacebookLoginReqBody(accessToken: credential.token)); + case AuthType.google: + return await methods.signInWithGoogle(GoogleLoginReqBody(idToken: credential.token)); + case AuthType.apple: + return await methods.signInWithApple(AppleLoginReqBody(token: credential.token)); + case AuthType.anonymous: + return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: credential.token)); + } + } + + Future associateCredential({required Credential credential}) async { + switch (credential.authType) { + case AuthType.facebook: + return await methods + .associateWithFacebook(FacebookLoginReqBody(accessToken: credential.token)); + case AuthType.google: + return await methods.associateWithGoogle(GoogleLoginReqBody(idToken: credential.token)); + case AuthType.apple: + return await methods.associateWithApple(AppleLoginReqBody(token: credential.token)); + case AuthType.anonymous: + return await methods.signInWithAnonymous(AnonymousLoginReqBody(secret: credential.token)); + } } Future reportDevice(DeviceInfo deviceInfo) async { diff --git a/guru_app/lib/app/app_models.dart b/guru_app/lib/app/app_models.dart index 5a69bf6..64f0a04 100644 --- a/guru_app/lib/app/app_models.dart +++ b/guru_app/lib/app/app_models.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:guru_app/firebase/messaging/remote_messaging_manager.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:json_annotation/json_annotation.dart'; /// Created by Haoyi on 2022/8/29 @@ -64,6 +65,9 @@ class Deployment { static const int defaultApiTimeout = 15000; // 15s static const int defaultIosSandboxSubsRenewalSpeed = 2; static const int defaultTrackingNotificationPermissionPassLimitTimes = 10; + static const int defaultSubscriptionRestoreGraceCount = 3; + static const int defaultFullscreenMinInterval = 60; + static const int defaultSubscriptionGraceDays = DateTimeUtils.dayInMillis; @JsonKey(name: "property_cache_size", defaultValue: 256) final int propertyCacheSize; @@ -139,6 +143,21 @@ class Deployment { @JsonKey(name: "show_internal_ads_when_banner_unavailable", defaultValue: false) final bool showInternalAdsWhenBannerUnavailable; + @JsonKey(name: "subscription_restore_grace_count", defaultValue: defaultSubscriptionRestoreGraceCount) + final int subscriptionRestoreGraceCount; + + @JsonKey(name: "fullscreen_ads_min_interval", defaultValue: defaultFullscreenMinInterval) + final int fullscreenAdsMinInterval; + + @JsonKey(name: "subscription_grace_period", defaultValue: defaultSubscriptionGraceDays) + final int subscriptionGraceDays; + + @JsonKey(name: "enabled_sync_account_profile", defaultValue: false) + final bool enabledSyncAccountProfile; + + @JsonKey(name: "purchase_event_trigger", defaultValue: 1) + final int purchaseEventTrigger; + Deployment( {this.propertyCacheSize = 256, this.enableDithering = true, @@ -164,7 +183,12 @@ class Deployment { defaultTrackingNotificationPermissionPassLimitTimes, this.enabledGuruAnalyticsStrategy = false, this.allowInterstitialAsAlternativeReward = false, - this.showInternalAdsWhenBannerUnavailable = false}); + this.showInternalAdsWhenBannerUnavailable = false, + this.subscriptionRestoreGraceCount = defaultSubscriptionRestoreGraceCount, + this.fullscreenAdsMinInterval = defaultFullscreenMinInterval, + this.subscriptionGraceDays = defaultSubscriptionGraceDays, + this.enabledSyncAccountProfile = false, + this.purchaseEventTrigger = 1}); factory Deployment.fromJson(Map json) => _$DeploymentFromJson(json); @@ -176,7 +200,11 @@ class RemoteDeployment { @JsonKey(name: "keep_screen_on_duration_m", defaultValue: 0) final int keepScreenOnDuration; - RemoteDeployment({this.keepScreenOnDuration = 0}); + @JsonKey(name: "subscriptionGraceDays") + final int? subscriptionGraceDays; + + RemoteDeployment( + {this.keepScreenOnDuration = 0, this.subscriptionGraceDays}); factory RemoteDeployment.fromJson(Map json) => _$RemoteDeploymentFromJson(json); diff --git a/guru_app/lib/app/app_models.g.dart b/guru_app/lib/app/app_models.g.dart index 14d032d..c245fe4 100644 --- a/guru_app/lib/app/app_models.g.dart +++ b/guru_app/lib/app/app_models.g.dart @@ -79,6 +79,14 @@ Deployment _$DeploymentFromJson(Map json) => Deployment( json['allow_interstitial_as_alternative_reward'] as bool? ?? false, showInternalAdsWhenBannerUnavailable: json['show_internal_ads_when_banner_unavailable'] as bool? ?? false, + subscriptionRestoreGraceCount: + json['subscription_restore_grace_count'] as int? ?? 3, + fullscreenAdsMinInterval: + json['fullscreen_ads_min_interval'] as int? ?? 60, + subscriptionGraceDays: + json['subscription_grace_period'] as int? ?? 86400000, + enabledSyncAccountProfile: + json['enabled_sync_account_profile'] as bool? ?? false, ); Map _$DeploymentToJson(Deployment instance) => @@ -113,6 +121,11 @@ Map _$DeploymentToJson(Deployment instance) => instance.allowInterstitialAsAlternativeReward, 'show_internal_ads_when_banner_unavailable': instance.showInternalAdsWhenBannerUnavailable, + 'subscription_restore_grace_count': + instance.subscriptionRestoreGraceCount, + 'fullscreen_ads_min_interval': instance.fullscreenAdsMinInterval, + 'subscription_grace_period': instance.subscriptionGraceDays, + 'enabled_sync_account_profile': instance.enabledSyncAccountProfile, }; const _$PromptTriggerEnumMap = { @@ -123,9 +136,11 @@ const _$PromptTriggerEnumMap = { RemoteDeployment _$RemoteDeploymentFromJson(Map json) => RemoteDeployment( keepScreenOnDuration: json['keep_screen_on_duration_m'] as int? ?? 0, + subscriptionGraceDays: json['subscriptionGraceDays'] as int?, ); Map _$RemoteDeploymentToJson(RemoteDeployment instance) => { 'keep_screen_on_duration_m': instance.keepScreenOnDuration, + 'subscriptionGraceDays': instance.subscriptionGraceDays, }; diff --git a/guru_app/lib/controller/assets_aware.dart b/guru_app/lib/controller/assets_aware.dart index bfb9a1c..3a218fe 100644 --- a/guru_app/lib/controller/assets_aware.dart +++ b/guru_app/lib/controller/assets_aware.dart @@ -3,13 +3,18 @@ import 'package:guru_app/financial/asset/assets_store.dart'; import 'package:guru_app/financial/financial_manager.dart'; import 'package:guru_app/financial/iap/iap_manager.dart'; import 'package:guru_app/financial/iap/iap_model.dart'; +import 'package:guru_app/financial/igb/igb_manager.dart'; +import 'package:guru_app/financial/igb/igb_product.dart'; import 'package:guru_app/financial/igc/igc_manager.dart'; import 'package:guru_app/financial/igc/igc_model.dart'; import 'package:guru_app/financial/product/product_store.dart'; import 'package:guru_app/financial/reward/reward_manager.dart'; import 'package:guru_app/financial/reward/reward_model.dart'; import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/inventory/db/inventory_database.dart'; +import 'package:guru_app/inventory/inventory_manager.dart'; import 'package:guru_app/test/test_guru_app_creator.dart'; +import 'package:guru_utils/collection/collectionutils.dart'; import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:guru_utils/extensions/extensions.dart'; import 'package:guru_utils/controller/controller.dart'; @@ -18,7 +23,7 @@ import 'package:guru_utils/controller/controller.dart'; mixin AssetsAware on LifecycleController { final BehaviorSubject> _productStoreSubject = - BehaviorSubject.seeded(ProductStore()); + BehaviorSubject.seeded(ProductStore()); ProductStore get currentProductStore => _productStoreSubject.value; @@ -44,6 +49,9 @@ mixin AssetsAware on LifecycleController { Stream get observableIgcBalance => IgcManager.instance.observableCurrentBalance; + Stream> get observableInventoryItems => + InventoryManager.instance.observableData; + Future restorePurchases() async { return await IapManager.instance.restorePurchases(); } @@ -73,6 +81,52 @@ mixin AssetsAware on LifecycleController { return RewardManager.instance.buildRewardProduct(intent); } + Future buildIgbProduct(TransactionIntent intent) { + return IgbManager.instance.buildIgbProduct(intent); + } + + int getInventoryBalance(String sku) { + return InventoryManager.instance.getData(sku)?.balance ?? 0; + } + + TimeSensitiveData getInventoryTimeSensitiveData(String sku) { + return InventoryManager.instance.getData(sku)?.timeSensitive ?? const TimeSensitiveData(); + } + + /// 使用指定[sku]的道具,[amount]为使用数量,[action]的行为,[scene]为使用场景 + /// useProp最终会得到一个行为上的收益,因此这里为了方便针对道具使用进行统一的行为分析 + /// 这里的 [action]和[scene]最终会通过 spend_virtual_currency 事件进行统一的统计 + /// propCategory可以参照 [PropCategory] 中的定义 + /// 具体参数对照如下: + /// - **`item_name`**: [intent] 如果这里指定了使用道具的意图,那么这里就会使用这个意图,否则使用场景, + /// - **`item_category`**: [category], + /// - **`virtual_currency_name`**: [propSku], + /// - **`value`**: [amount], + /// - **`balance`**: balance, + /// - **`scene`**: [scene], + /// - **`level_name`**: levelName + /// + /// 返回值为是否成功使用道具,false表示道具不足,true表示使用成功 + /// + Future useProp( + String propSku, + String scene, { + int amount = 1, + String? intent, + String category = PropCategory.boosts, + bool timeSensitiveOnly = false, + int? transactionTs, + }) async { + final manifest = Manifest.action(category, scene, + extras: CollectionUtils.filterOutNulls({ + ExtraReservedField.contentId: intent ?? scene, // 如果这里指定了使用道具的意图,那么这里就会使用这个意图,否则使用场景 + ExtraReservedField.transactionTs: transactionTs // 如果这里指定了交易时间,那么就会使用这个时间,否则使用当前时间 + })); + return await InventoryManager.instance.consume( + [StockItem.consumable(propSku, amount)], manifest, + timeSensitiveOnly: timeSensitiveOnly); + } + Future requestProduct(Product product, {String from = ""}) async { if (product is IapProduct) { return await IapManager.instance.buy(product); @@ -80,6 +134,8 @@ mixin AssetsAware on LifecycleController { return await IgcManager.instance.purchase(product); } else if (product is RewardProduct) { return await RewardManager.instance.claim(product); + } else if (product is IgbProduct) { + return await IgbManager.instance.redeem(product); } else { return false; } diff --git a/guru_app/lib/database/creators/creators.dart b/guru_app/lib/database/creators/creators.dart index 53b329b..8c58619 100644 --- a/guru_app/lib/database/creators/creators.dart +++ b/guru_app/lib/database/creators/creators.dart @@ -1,4 +1,5 @@ import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/inventory/db/inventory_database.dart'; import 'package:guru_utils/database/database.dart'; import 'package:guru_utils/property/storage/db/property_database.dart'; @@ -8,6 +9,8 @@ final List _creatorV1 = [PropertyEntity.createTable]; final List _creatorV2 = [OrderEntity.createTable]; +final List _creatorV4 = [InventoryTable.createTable]; + class Creators { - static final List creators = [..._creatorV1, ..._creatorV2]; + static final List creators = [..._creatorV1, ..._creatorV2, ..._creatorV4]; } diff --git a/guru_app/lib/database/guru_db.dart b/guru_app/lib/database/guru_db.dart index f4fb968..6d98ec2 100644 --- a/guru_app/lib/database/guru_db.dart +++ b/guru_app/lib/database/guru_db.dart @@ -25,5 +25,5 @@ class GuruDB extends _GuruDB with PropertyDatabase { List get tableCreators => Creators.creators; @override - int get version => 3; + int get version => 4; } diff --git a/guru_app/lib/database/migrations/migration_v3_to_v4.dart b/guru_app/lib/database/migrations/migration_v3_to_v4.dart new file mode 100644 index 0000000..a3d2112 --- /dev/null +++ b/guru_app/lib/database/migrations/migration_v3_to_v4.dart @@ -0,0 +1,16 @@ +part of "migrations.dart"; + +class _MigrationV3toV4 implements Migration { + @override + Future migrate(Transaction transaction) async { + // 由于这里无法保证所在平台是否支持IF NOT EXISTS,所以这里用try catch来处理 + try { + await InventoryTable.createTable(transaction); + } catch (error, stacktrace) { + Log.w("ignore alter cmd!"); + } + return MigrateResult.success; + } +} + +final migration3to4 = _MigrationV3toV4(); diff --git a/guru_app/lib/database/migrations/migrations.dart b/guru_app/lib/database/migrations/migrations.dart index 0e4500d..da719ba 100644 --- a/guru_app/lib/database/migrations/migrations.dart +++ b/guru_app/lib/database/migrations/migrations.dart @@ -1,13 +1,15 @@ import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/inventory/db/inventory_database.dart'; import 'package:guru_utils/database/database.dart'; import 'package:guru_utils/log/log.dart'; part "migration_v1_to_v2.dart"; part 'migration_v2_to_v3.dart'; +part 'migration_v3_to_v4.dart'; /// Created by @Haoyi on 2020/5/22 /// class Migrations { - static final migrations = [migration1to2, migration2to3]; + static final migrations = [migration1to2, migration2to3, migration3to4]; } diff --git a/guru_app/lib/financial/financial_manager.dart b/guru_app/lib/financial/financial_manager.dart index 9c40cf0..ec516e6 100644 --- a/guru_app/lib/financial/financial_manager.dart +++ b/guru_app/lib/financial/financial_manager.dart @@ -2,6 +2,7 @@ import 'package:guru_app/financial/asset/assets_model.dart'; import 'package:guru_app/financial/asset/assets_store.dart'; import 'package:guru_app/financial/iap/iap_manager.dart'; import 'package:guru_app/financial/iap/iap_model.dart'; +import 'package:guru_app/financial/igb/igb_manager.dart'; import 'package:guru_app/financial/igc/igc_manager.dart'; import 'package:guru_app/financial/reward/reward_manager.dart'; import 'package:guru_utils/extensions/extensions.dart'; @@ -54,6 +55,13 @@ class FinancialManager { void init() { IapManager.instance.init(); IgcManager.instance.init(); + IgbManager.instance.init(); RewardManager.instance.init(); } + + void switchSession(String fromUid, String toUid) { + IapManager.instance.switchSession(); + IgcManager.instance.switchSession(); + RewardManager.instance.switchSession(); + } } diff --git a/guru_app/lib/financial/iap/iap_manager.dart b/guru_app/lib/financial/iap/iap_manager.dart index 475327d..3229bf6 100644 --- a/guru_app/lib/financial/iap/iap_manager.dart +++ b/guru_app/lib/financial/iap/iap_manager.dart @@ -18,6 +18,7 @@ import 'package:guru_app/financial/manifest/manifest.dart'; import 'package:guru_app/financial/manifest/manifest_manager.dart'; import 'package:guru_app/financial/product/product_model.dart'; import 'package:guru_app/financial/product/product_store.dart'; +import 'package:guru_app/firebase/firebase.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; import 'package:guru_app/property/property_keys.dart'; @@ -50,17 +51,14 @@ class IapManager { final BehaviorSubject> _iapStoreSubject = BehaviorSubject.seeded(AssetsStore.inactive()); - final Map iapRequestMap = - HashMap(); + final Map iapRequestMap = HashMap(); Stream> get observableProductDetails => _productDetailsSubject.stream; - Stream> get observableAssetStore => - _iapStoreSubject.stream; + Stream> get observableAssetStore => _iapStoreSubject.stream; - Map get loadedProductDetails => - _productDetailsSubject.value; + Map get loadedProductDetails => _productDetailsSubject.value; AssetsStore get purchasedStore => _iapStoreSubject.value; @@ -83,8 +81,8 @@ class IapManager { bool _restorePurchase = false; final iapRevenueAppEventOptions = AppEventOptions( - capabilities: const AppEventCapabilities( - AppEventCapabilities.firebase | AppEventCapabilities.guru), + capabilities: + const AppEventCapabilities(AppEventCapabilities.firebase | AppEventCapabilities.guru), firebaseParamsConvertor: _iapRevenueToValue, guruParamsConvertor: _iapRevenueToValue); @@ -100,8 +98,7 @@ class IapManager { void init() async { final iapCount = await AppProperty.getInstance().getIapCount(); if (iapCount > 0) { - GuruAnalytics.instance - .setUserProperty("purchase_count", iapCount.toString()); + GuruAnalytics.instance.setUserProperty("purchase_count", iapCount.toString()); GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); } else { GuruAnalytics.instance.setUserProperty("is_iap_user", "false"); @@ -113,8 +110,7 @@ class IapManager { stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); } if (subscription == null) { - final Stream> purchaseUpdated = - _inAppPurchase.purchaseStream; + final Stream> purchaseUpdated = _inAppPurchase.purchaseStream; subscription = purchaseUpdated.listen( (List purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); @@ -141,13 +137,27 @@ class IapManager { } finally {} } + Future switchSession() async { + final iapCount = await AppProperty.getInstance().getIapCount(); + if (iapCount > 0) { + GuruAnalytics.instance.setUserProperty("purchase_count", iapCount.toString()); + GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); + } else { + GuruAnalytics.instance.setUserProperty("is_iap_user", "false"); + } + try { + await reloadOrders(); + } catch (error, stacktrace) { + Log.w("reloadOrders error! $error", + stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); + } + _checkAndLoad(); + } + Future reloadOrders() async { final transactions = await GuruDB.instance.selectOrders( method: TransactionMethod.iap, - attrs: [ - TransactionAttributes.asset, - TransactionAttributes.subscriptions - ]); + attrs: [TransactionAttributes.asset, TransactionAttributes.subscriptions]); final newAssetStore = AssetsStore(); Log.d("reloadOrders ${transactions.length}"); for (var transaction in transactions) { @@ -165,15 +175,14 @@ class IapManager { do { final seconds = min(MathUtils.fibonacci(retry, offset: 2), 900); await Future.delayed(Duration(seconds: seconds)); - available = - await _inAppPurchase.isAvailable().catchError((error, stacktrace) { + available = await _inAppPurchase.isAvailable().catchError((error, stacktrace) { Log.w("isAvailable error:$error", stackTrace: stacktrace); return false; }); Log.d("_checkAndLoad:$retry available:$available"); retry++; } while (!available); - availableSubject.addEx(true); + availableSubject.addIfChanged(true); try { await refreshProducts(); if (GuruApp.instance.appSpec.deployment.autoRestoreIap || @@ -196,12 +205,9 @@ class IapManager { iapRequest.response(false); final iapErrorMsg = "_processIapError:${iapRequest.productId}"; Log.w(iapErrorMsg, - error: PurchaseError(iapErrorMsg), - syncFirebase: true, - syncCrashlytics: true); + error: PurchaseError(iapErrorMsg), syncFirebase: true, syncCrashlytics: true); try { - await GuruDB.instance - .upsertOrder(order: iapRequest.order.error(iapErrorMsg)); + await GuruDB.instance.upsertOrder(order: iapRequest.order.error(iapErrorMsg)); } catch (error, stacktrace) { Log.w("_processIapError upsert error! $error", syncFirebase: true); } @@ -218,8 +224,7 @@ class IapManager { try { await GuruDB.instance.deleteOrder(order: order); } catch (error, stacktrace) { - Log.w("_processIapCancel deleteOrder error! $error", - syncFirebase: true); + Log.w("_processIapCancel deleteOrder error! $error", syncFirebase: true); } } iapRequestMap.clear(); @@ -248,22 +253,18 @@ class IapManager { // }); // } - String dumpProductAndPurchased( - ProductDetails details, PurchaseDetails purchaseDetails) { + String dumpProductAndPurchased(ProductDetails details, PurchaseDetails purchaseDetails) { final StringBuffer sb = StringBuffer(); if (Platform.isAndroid) { try { - GooglePlayPurchaseDetails googlePlayDetails = - purchaseDetails as GooglePlayPurchaseDetails; - GooglePlayProductDetails googlePlayProduct = - details as GooglePlayProductDetails; + GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails; + GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails; Log.d( "Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}"); } catch (error, stacktrace) {} } else if (Platform.isIOS) { - AppStorePurchaseDetails appleDetails = - purchaseDetails as AppStorePurchaseDetails; + AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails; AppStoreProductDetails appleProduct = details as AppStoreProductDetails; sb.writeln("#### purchase ####"); sb.writeln("productID: ${appleDetails.productID}"); @@ -274,23 +275,18 @@ class IapManager { sb.writeln("skPaymentTransaction:"); sb.writeln( " =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}"); - sb.writeln( - " =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}"); + sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}"); sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:"); - sb.writeln( - " =>${appleDetails.skPaymentTransaction.transactionIdentifier}:"); + sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:"); sb.writeln("\n#### product ####"); sb.writeln("currencyCode: ${appleProduct.currencyCode}"); sb.writeln("rawPrice: ${appleProduct.rawPrice}"); sb.writeln("currencyCode: ${appleProduct.currencyCode}"); sb.writeln("currencyCode skProduct"); - sb.writeln( - " =>localizedTitle: ${appleProduct.skProduct.localizedTitle}"); - sb.writeln( - " =>localizedDescription: ${appleProduct.skProduct.localizedDescription}"); + sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}"); + sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}"); sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}"); - sb.writeln( - " =>productIdentifier: ${appleProduct.skProduct.productIdentifier}"); + sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}"); sb.writeln( " =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}"); sb.writeln(" =>appleProduct.skProduct.priceLocale"); @@ -319,9 +315,8 @@ class IapManager { int getIOSPeriodInterval(int numberOfUnits, SKSubscriptionPeriodUnit unit) { if (GuruSettings.instance.debugMode.get()) { - final renewalSpeed = GuruApp - .instance.appSpec.deployment.iosSandboxSubsRenewalSpeed - .clamp(1, 5); + final renewalSpeed = + GuruApp.instance.appSpec.deployment.iosSandboxSubsRenewalSpeed.clamp(1, 5); switch (unit) { case SKSubscriptionPeriodUnit.day: return numberOfUnits * weekRenewalDurations[renewalSpeed - 1] ~/ 7; @@ -346,8 +341,52 @@ class IapManager { } } - Future processRestoredSubscription( - List subscriptionPurchased) async { + bool checkSubscriptionPeriod(PurchaseDetails purchaseDetails, ProductDetails productDetails) { + bool validOrder = false; + if (Platform.isAndroid) { + validOrder = true; + } else if (Platform.isIOS) { + final appleProduct = productDetails as AppStoreProductDetails; + SKProductSubscriptionPeriodWrapper? period = appleProduct.skProduct.subscriptionPeriod; + + final appleDetails = purchaseDetails as AppStorePurchaseDetails; + final paymentDiscount = appleDetails.skPaymentTransaction.payment.paymentDiscount; + if (paymentDiscount != null) { + Log.i("paymentDiscount: ${paymentDiscount.identifier} ${paymentDiscount.timestamp}"); + for (var discount in appleProduct.skProduct.discounts) { + final discountSubPeriod = discount.subscriptionPeriod; + Log.i( + "check discount(${paymentDiscount.identifier}) product [ ${discount.identifier} ${discountSubPeriod.unit} ${discountSubPeriod.numberOfUnits} ]"); + if (discount.identifier == paymentDiscount.identifier) { + period = discount.subscriptionPeriod; + break; + } + } + } + + Log.i("checkSubscriptionPeriod: ${appleProduct.skProduct.productIdentifier} ${period?.unit} ${period?.numberOfUnits} ${appleDetails.transactionDate}"); + + if (period != null) { + final numberOfUnits = period.numberOfUnits; + final unit = period.unit; + final int validInterval = getIOSPeriodInterval(numberOfUnits, unit); + final transactionTs = int.tryParse(purchaseDetails.transactionDate ?? "") ?? 0; + final now = DateTimeUtils.currentTimeInMillis(); + final gracePeriod = GuruApp.instance.remoteDeployment.subscriptionGraceDays ?? + GuruApp.instance.appSpec.deployment.subscriptionGraceDays; + /// 过期时间 = 订单的最后一次刷新时间 + 订阅周期(优惠周期) + 宽限期 + final expiredTs = transactionTs + validInterval + gracePeriod; + /// 如果当前的时间小于过期时间,那么这个订单是有效的 + validOrder = now < expiredTs; + Log.d( + "productID: ${purchaseDetails.productID}) purchaseID: ${purchaseDetails.purchaseID}[$numberOfUnits][$unit] [$now < $transactionTs + $validInterval]($validOrder) ", + tag: PropertyTags.iap); + } + } + return validOrder; + } + + Future processRestoredSubscription(List subscriptionPurchased) async { List purchasedDetails = subscriptionPurchased; if (Platform.isIOS) { purchasedDetails = buildLatestPurchasedPlanForIos(subscriptionPurchased); @@ -359,8 +398,7 @@ class IapManager { if (Platform.isAndroid) { final purchasedSkus = purchasedDetails.map((e) => e.productID).toSet(); newPurchasedStore.removeWhere((productId, asset) { - final expired = - productId.isSubscription && !purchasedSkus.contains(productId.sku); + final expired = productId.isSubscription && !purchasedSkus.contains(productId.sku); Log.i("remove expired subscription[$productId] expired:$expired"); if (expired) { expiredSkus.add(asset.productId.sku); @@ -370,8 +408,7 @@ class IapManager { } for (var purchased in purchasedDetails) { - final productId = - GuruApp.instance.findProductId(sku: purchased.productID); + final productId = GuruApp.instance.findProductId(sku: purchased.productID); if (productId == null) { Log.w("productId is null! ${purchased.productID}"); continue; @@ -381,26 +418,7 @@ class IapManager { Log.w("product is null! ${purchased.productID}"); continue; } - purchased.transactionDate; - bool validPurchase = false; - if (Platform.isAndroid) { - validPurchase = true; - } else if (Platform.isIOS) { - final appleProduct = productDetails as AppStoreProductDetails; - final period = appleProduct.skProduct.subscriptionPeriod; - if (period != null) { - final numberOfUnits = period.numberOfUnits; - final unit = period.unit; - final int validInterval = getIOSPeriodInterval(numberOfUnits, unit); - final transactionTs = - int.tryParse(purchased.transactionDate ?? "") ?? 0; - final now = DateTimeUtils.currentTimeInMillis(); - validPurchase = transactionTs + validInterval < now; - Log.d( - "productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}[$numberOfUnits][$unit] $transactionTs + $validInterval < $now ($validPurchase)", - tag: PropertyTags.iap); - } - } + final bool validPurchase = checkSubscriptionPeriod(purchased, productDetails); if (validPurchase) { Log.d( "[Restored Subscription] productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}", @@ -408,8 +426,8 @@ class IapManager { final asset = newPurchasedStore.getAsset(productId); late OrderEntity newOrder; if (asset == null) { - final product = await _createProduct( - productId.createIntent(scene: "restore"), productDetails); + final product = + await _createProduct(productId.createIntent(scene: "restore"), productDetails); newOrder = product.createOrder().success(); } else { newOrder = asset.order.success(); @@ -417,8 +435,7 @@ class IapManager { try { await GuruDB.instance.replaceOrderBySku(order: newOrder); } catch (error, stacktrace) { - Log.w("Failed to upsert order: $error $stacktrace", - tag: PropertyTags.iap); + Log.w("Failed to upsert order: $error $stacktrace", tag: PropertyTags.iap); } final newAsset = Asset(productId, newOrder); newPurchasedStore.addAsset(newAsset); @@ -431,21 +448,30 @@ class IapManager { } if (expiredSkus.isNotEmpty) { - Log.i("expired orders:${expiredSkus.length}}"); - try { - await GuruDB.instance.deleteOrdersBySkus(expiredSkus); - } catch (error, stacktrace) { - Log.w("Failed to upsert order: $error $stacktrace", - tag: PropertyTags.iap); + final graceCount = await AppProperty.getInstance().increaseGraceCount(); + Log.i("expired orders:${expiredSkus.length}} grace count: $graceCount"); + if (graceCount > GuruApp.instance.appSpec.deployment.subscriptionRestoreGraceCount) { + try { + await GuruDB.instance.deleteOrdersBySkus(expiredSkus); + } catch (error, stacktrace) { + Log.w("Failed to upsert order: $error $stacktrace", tag: PropertyTags.iap); + } + await AppProperty.getInstance().resetGraceCount(); + GuruAnalytics.instance.logGuruEvent('dev_iap_audit', { + "item_category": "expired", + "item_name": "sub", + "platform": Platform.isAndroid ? "Android" : "iOS", + "value": graceCount + }); } + } else { + await AppProperty.getInstance().resetGraceCount(); } _iapStoreSubject.addEx(newPurchasedStore); - Log.d( - "[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}"); + Log.d("[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}"); } - List buildLatestPurchasedPlanForIos( - List purchaseDetails) { + List buildLatestPurchasedPlanForIos(List purchaseDetails) { if (purchaseDetails.isEmpty) { return []; } @@ -459,15 +485,14 @@ class IapManager { .toSet(); Log.d("rawTransactionIds:$rawTransactionIds"); final sortedPurchaseDetails = purchaseDetails.toList(); - sortedPurchaseDetails.sort((a, b) => - (int.tryParse(b.transactionDate ?? '') ?? 0) - .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0)); + sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0) + .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0)); sortedPurchaseDetails.retainWhere((details) { var detail = details as AppStorePurchaseDetails; Log.d( "checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}"); - return rawTransactionIds.remove(detail - .skPaymentTransaction.originalTransaction?.transactionIdentifier); + return rawTransactionIds + .remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier); }); return sortedPurchaseDetails; @@ -487,15 +512,14 @@ class IapManager { .toSet(); Log.d("rawTransactionIds:$rawTransactionIds"); final sortedPurchaseDetails = purchaseDetails.toList(); - sortedPurchaseDetails.sort((a, b) => - (int.tryParse(b.transactionDate ?? '') ?? 0) - .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0)); + sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0) + .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0)); sortedPurchaseDetails.retainWhere((details) { var detail = details as AppStorePurchaseDetails; Log.d( "checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}"); - return rawTransactionIds.remove(detail - .skPaymentTransaction.originalTransaction?.transactionIdentifier); + return rawTransactionIds + .remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier); }); for (var details in sortedPurchaseDetails) { @@ -509,8 +533,7 @@ class IapManager { } } - void _listenToPurchaseUpdated( - List purchaseDetailsList) async { + void _listenToPurchaseUpdated(List purchaseDetailsList) async { final List> restoredIapPurchases = []; final List> pendingCompletePurchase = []; final List subscriptionPurchases = []; @@ -532,9 +555,7 @@ class IapManager { return; } for (var details in purchaseDetailsList) { - final productId = - GuruApp.instance.findProductId(sku: details.productID) ?? - ProductId.invalid; + final productId = GuruApp.instance.findProductId(sku: details.productID) ?? ProductId.invalid; Log.d( "[details]: $productId [${details.productID}] ${details.purchaseID} ${details.status} ${details.pendingCompletePurchase}"); GuruAnalytics.instance.logGuruEvent('dev_iap_update', { @@ -557,7 +578,13 @@ class IapManager { final productDetails = loadedProductDetails[productId]; if (productDetails != null) { - await _completePurchase(productId, productDetails, details); + /// 如果是 IOS的 purchased订单,并且是订阅的订单,他又没在当前请求的列表中,证明他是一个恢复的订单 + if (Platform.isIOS && productId.isSubscription && !iapRequestMap.containsKey(productId)) { + subscriptionPurchases.add(details); + existsRestored = true; + } else { + await _completePurchase(productId, productDetails, details); + } } Log.d("completePurchase ${details.productID} ${details.purchaseID}"); @@ -574,9 +601,8 @@ class IapManager { } // 如果是未完成的商品或是恢复出了消耗品,都需要手动完成 if (Platform.isAndroid) { - final originPurchaseState = (details as GooglePlayPurchaseDetails) - .billingClientPurchase - .purchaseState; + final originPurchaseState = + (details as GooglePlayPurchaseDetails).billingClientPurchase.purchaseState; Log.d( "restore android ${details.pendingCompletePurchase} $productId $originPurchaseState"); if (originPurchaseState == PurchaseStateWrapper.purchased) { @@ -614,8 +640,7 @@ class IapManager { if (existsRestored) { if (pendingCompletePurchase.isNotEmpty) { await completeAllPurchases(pendingCompletePurchase); - Log.d("manual complete/consume all purchases!", - syncFirebase: true, syncCrashlytics: true); + Log.d("manual complete/consume all purchases!", syncFirebase: true, syncCrashlytics: true); } if (restoredIapPurchases.isNotEmpty) { @@ -659,8 +684,8 @@ class IapManager { upsertOrders.add(newOrder); } } else if (productDetails != null) { - final product = await _createProduct( - productId.createIntent(scene: "restore"), productDetails); + final product = + await _createProduct(productId.createIntent(scene: "restore"), productDetails); final newOrder = product.createOrder().success(); upsertOrders.add(newOrder); } @@ -671,20 +696,17 @@ class IapManager { await GuruDB.instance.upsertOrders(upsertOrders); updatedOrder.addAll(upsertOrders); } catch (error, stacktrace) { - Log.w("upsertOrders error:$error $stacktrace", - syncCrashlytics: true, syncFirebase: true); + Log.w("upsertOrders error:$error $stacktrace", syncCrashlytics: true, syncFirebase: true); for (var order in upsertOrders) { try { await GuruDB.instance.upsertOrder(order: order); updatedOrder.add(order); } catch (error1, stacktrace1) { - Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1", - syncFirebase: true); + Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1", syncFirebase: true); } } } - final assets = - updatedOrder.map((order) => Asset(order.productId, order)).toList(); + final assets = updatedOrder.map((order) => Asset(order.productId, order)).toList(); newPurchased.addAllAssets(assets); } _iapStoreSubject.addEx(newPurchased); @@ -692,16 +714,13 @@ class IapManager { } Future reportFailedOrders() async { - final failedIapOrders = - await AppProperty.getInstance().loadAllFailedIapOrders(); + final failedIapOrders = await AppProperty.getInstance().loadAllFailedIapOrders(); failedIapOrders.forEach((key, value) async { try { final order = OrdersReport.fromJson(json.decode(value)); final result = await GuruApi.instance.reportOrders(order); - if (result.usdPrice > 0) { - logRevenue( - result.usdPrice, order.productId ?? order.subscriptionId); - } + // 这里不管返回什么值,都认为是成功的, 只有崩溃才会返回失败 + await logRevenue(order, result); AppProperty.getInstance().removeReportSuccessOrder(key); } catch (error, stacktrace) {} }); @@ -709,8 +728,7 @@ class IapManager { } String buildGooglePlayDetailsString( - GooglePlayProductDetails googlePlayProduct, - GooglePlayPurchaseDetails googlePlayDetails) { + GooglePlayProductDetails googlePlayProduct, GooglePlayPurchaseDetails googlePlayDetails) { final StringBuffer sb = StringBuffer(); sb.writeln("#### purchase ####"); @@ -742,15 +760,12 @@ class IapManager { if (oneTimeDetails != null) { sb.writeln(" => oneTimeDetails:"); sb.writeln(" - formattedPrice: ${oneTimeDetails.formattedPrice}"); - sb.writeln( - " - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}"); - sb.writeln( - " - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}"); + sb.writeln(" - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}"); + sb.writeln(" - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}"); } final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; - if (subscriptionOfferDetails != null && - subscriptionOfferDetails.isNotEmpty) { + if (subscriptionOfferDetails != null && subscriptionOfferDetails.isNotEmpty) { for (var offer in subscriptionOfferDetails) { sb.writeln(" => sub offer: ${offer.offerId}"); sb.writeln(" - basePlanId: ${offer.basePlanId}"); @@ -773,13 +788,12 @@ class IapManager { return sb.toString(); } - Future reportOrders(ProductId productId, ProductDetails details, - PurchaseDetails purchaseDetails, OrderEntity? order) async { + Future reportOrders(ProductId productId, ProductDetails details, PurchaseDetails purchaseDetails, + OrderEntity? order) async { final OrdersReport ordersReport = OrdersReport(); if (Platform.isAndroid) { - ordersReport.token = - purchaseDetails.verificationData.serverVerificationData; + ordersReport.token = purchaseDetails.verificationData.serverVerificationData; ordersReport.packageName = GuruApp.instance.details.packageName; final manifest = order?.manifest; final basePlanId = manifest?.basePlanId; @@ -789,16 +803,13 @@ class IapManager { ordersReport.offerId = offerId; } try { - GooglePlayPurchaseDetails googlePlayDetails = - purchaseDetails as GooglePlayPurchaseDetails; - GooglePlayProductDetails googlePlayProduct = - details as GooglePlayProductDetails; + GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails; + GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails; Log.d( "Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}"); } catch (error, stacktrace) {} } else if (Platform.isIOS) { - AppStorePurchaseDetails appleDetails = - purchaseDetails as AppStorePurchaseDetails; + AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails; AppStoreProductDetails appleProduct = details as AppStoreProductDetails; final StringBuffer sb = StringBuffer(); sb.writeln("#### purchase ####"); @@ -810,31 +821,25 @@ class IapManager { sb.writeln("skPaymentTransaction:"); sb.writeln( " =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}"); - sb.writeln( - " =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}"); + sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}"); sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:"); - sb.writeln( - " =>${appleDetails.skPaymentTransaction.transactionIdentifier}:"); + sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:"); sb.writeln("\n#### product ####"); sb.writeln("currencyCode: ${appleProduct.currencyCode}"); sb.writeln("rawPrice: ${appleProduct.rawPrice}"); sb.writeln("currencyCode: ${appleProduct.currencyCode}"); sb.writeln("currencyCode skProduct"); - sb.writeln( - " =>localizedTitle: ${appleProduct.skProduct.localizedTitle}"); - sb.writeln( - " =>localizedDescription: ${appleProduct.skProduct.localizedDescription}"); + sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}"); + sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}"); sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}"); - sb.writeln( - " =>productIdentifier: ${appleProduct.skProduct.productIdentifier}"); + sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}"); sb.writeln( " =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}"); sb.writeln(" =>appleProduct.skProduct.priceLocale"); sb.writeln(" ->{appleProduct.skProduct.priceLocale}"); ordersReport.bundleId = GuruApp.instance.appSpec.details.bundleId; - ordersReport.receipt = - purchaseDetails.verificationData.serverVerificationData; + ordersReport.receipt = purchaseDetails.verificationData.serverVerificationData; ordersReport.sku = appleDetails.productID; ordersReport.countryCode = appleProduct.skProduct.priceLocale.countryCode; Log.d("IOS Product/Purchase ${sb.toString()}"); @@ -847,40 +852,66 @@ class IapManager { ordersReport.orderType = OrderType.inapp; ordersReport.productId = details.id; } + ordersReport.orderId = purchaseDetails.purchaseID; ordersReport.price = details.rawPrice.toString(); ordersReport.currency = details.currencyCode; - - ordersReport.orderUserInfo = - OrderUserInfo(GuruSettings.instance.bestLevel.get().toString()); + ordersReport.orderUserInfo = OrderUserInfo(GuruSettings.instance.bestLevel.get().toString()); ordersReport.userIdentification = GuruAnalytics.instance.userIdentification; + final transactionDate = purchaseDetails.transactionDate; + if (transactionDate != null && transactionDate.isNotEmpty) { + try { + final ts = int.tryParse(transactionDate) ?? DateTimeUtils.currentTimeInMillis(); + ordersReport.transactionDate = ts; + } catch (error, stacktrace) { + Log.w("parse transactionDate error! $error", stackTrace: stacktrace); + } + } + Log.d("orderReport:$ordersReport", tag: "Iap"); try { final result = await GuruApi.instance.reportOrders(ordersReport); - if ((result.usdPrice > 0) || - (result.usdPrice == 0 && result.isTestOrder)) { - logRevenue(result.usdPrice, purchaseDetails.productID); - Log.i("reportOrders success! $result"); - return; - } - Log.i("ignoreInvalidResult $result", tag: "Iap"); + // 这里不管返回什么值,都认为是成功的 + await logRevenue(ordersReport, result); + return; } catch (error, stacktrace) { Log.i("reportOrders error!", error: error, stackTrace: stacktrace); } AppProperty.getInstance().saveFailedIapOrders(ordersReport); } - Future logRevenue(double usdPrice, String? sku) async { + Future logRevenue(OrdersReport order, OrdersResponse result) async { + final isSubscription = order.orderType == OrderType.subs; + final sku = (isSubscription ? order.subscriptionId : order.productId) ?? + (order.productId ?? order.subscriptionId ?? order.sku); + final usdPrice = result.usdPrice; if (sku == null || sku.isEmpty) { - return; + return false; } + + if (!result.isTestOrder && usdPrice <= 0) { + Log.i("ignoreInvalidResult $result", tag: "Iap"); + return false; + } + Log.i("prepare logRevenue! $result $sku"); + final platform = Platform.isIOS ? "appstore" : "google_play"; - GuruAnalytics.instance.logAdRevenue(usdPrice, platform, "USD"); - final productId = - GuruApp.instance.findProductId(sku: sku) ?? ProductId.invalid; - GuruAnalytics.instance.logPurchase(usdPrice, - currency: 'USD', contentId: sku, adPlatform: platform); - if (productId.isSubscription) { + if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 2) { + GuruAnalytics.instance.logAdRevenue020(usdPrice, platform, "USD", + orderId: order.orderId, + orderType: isSubscription ? "SUB" : "IAP", + productId: sku, + transactionDate: order.transactionDate); + } else { + GuruAnalytics.instance.logAdRevenue(usdPrice, platform, "USD", + orderId: order.orderId, + orderType: isSubscription ? "SUB" : "IAP", + productId: sku, + transactionDate: order.transactionDate); + } + GuruAnalytics.instance + .logPurchase(usdPrice, currency: 'USD', contentId: sku, adPlatform: platform); + if (isSubscription) { GuruAnalytics.instance.logEvent( "sub_purchase", { @@ -888,6 +919,9 @@ class IapManager { "currency": "USD", "revenue": usdPrice, "product_id": sku, + "order_type": "SUB", + "order_id": order.orderId, + "trans_ts": order.transactionDate }, options: iapRevenueAppEventOptions); } else { @@ -898,11 +932,16 @@ class IapManager { "currency": "USD", "revenue": usdPrice, "product_id": sku, + "order_type": "IAP", + "order_id": order.orderId, + "trans_ts": order.transactionDate }, options: iapRevenueAppEventOptions); } - GuruAnalytics.instance.logGuruEvent("dev_iap_action", - {"item_category": "reported", "item_name": sku, "result": "true"}); + GuruAnalytics.instance.logGuruEvent( + "dev_iap_action", {"item_category": "reported", "item_name": sku, "result": "true"}); + Log.i("reportOrders completed! logRevenue success! $result $sku"); + return true; } Future _deliverManifest(ProductId productId, Manifest manifest) async { @@ -912,8 +951,7 @@ class IapManager { result = await ManifestManager.instance .deliver(manifest, TransactionMethod.iap) .catchError((error) { - Log.w("applyManifest error:$error", - syncCrashlytics: true, syncFirebase: true); + Log.w("applyManifest error:$error", syncCrashlytics: true, syncFirebase: true); }); } catch (error, stacktrace) { cause = error.toString(); @@ -991,8 +1029,7 @@ class IapManager { await _inAppPurchase.completePurchase(details); final count = await AppProperty.getInstance().increaseAndGetIapCount(); GuruAnalytics.instance.setUserProperty("purchase_count", count.toString()); - Log.d("_completePurchase $productId ${details.pendingCompletePurchase}", - tag: "Iap"); + Log.d("_completePurchase $productId ${details.pendingCompletePurchase}", tag: "Iap"); OrderEntity? resultOrder; IapRequest? iapRequest = iapRequestMap.remove(productId); @@ -1075,27 +1112,24 @@ class IapManager { await appProperty.getAndIncrease(PropertyKeys.subscriptionCount); if (group != null) { - await appProperty - .getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group)); + await appProperty.getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group)); } - await appProperty - .getAndIncrease(PropertyKeys.buildSubscriptionCount(productId)); + await appProperty.getAndIncrease(PropertyKeys.buildSubscriptionCount(productId)); } Future createPurchaseManifest(TransactionIntent intent) { return ManifestManager.instance.createManifest(intent); } - Future checkAndDistributeOfferDetails(ProductId productId, - ProductDetails? details, EligibilityCriteria eligibilityCriteria) async { + Future checkAndDistributeOfferDetails( + ProductId productId, ProductDetails? details, EligibilityCriteria eligibilityCriteria) async { Log.d("checkAndDistributeOfferDetails $productId $eligibilityCriteria"); switch (eligibilityCriteria) { case EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup: final group = GuruApp.instance.appSpec.productProfile.group(productId); if (group != null) { final key = PropertyKeys.buildGroupSubscriptionCount(group); - final count = - await AppProperty.getInstance().getInt(key, defValue: 0); + final count = await AppProperty.getInstance().getInt(key, defValue: 0); Log.d(" ==> $key $count"); return count > 0 ? null : details; } @@ -1107,8 +1141,8 @@ class IapManager { Log.d(" ==> $key $count"); return count > 0 ? null : details; case EligibilityCriteria.newCustomerNeverHadAnySubscription: - final count = await AppProperty.getInstance() - .getInt(PropertyKeys.subscriptionCount, defValue: 0); + final count = + await AppProperty.getInstance().getInt(PropertyKeys.subscriptionCount, defValue: 0); Log.d(" ==> subscriptionCount $count"); return count > 0 ? null : details; default: @@ -1117,21 +1151,17 @@ class IapManager { return null; } - Future _createProduct( - TransactionIntent intent, ProductDetails details) async { + Future _createProduct(TransactionIntent intent, ProductDetails details) async { final productId = intent.productId; Manifest manifest = await ManifestManager.instance.createManifest(intent); Log.d("createProduct ${productId.sku} ${productId.hasBasePlan}"); ProductDetails baseDetails = details; ProductDetails? offerDetails; - if (Platform.isAndroid && - productId.isSubscription && - productId.hasBasePlan) { + if (Platform.isAndroid && productId.isSubscription && productId.hasBasePlan) { final googlePlayProductDetails = details as GooglePlayProductDetails; final productDetails = googlePlayProductDetails.productDetails; final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; - final offerProductDetails = - GooglePlayProductDetails.fromProductDetails(productDetails); + final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails); final expectBasePlan = productId.basePlan; final expectOfferId = productId.offerId; Log.d( @@ -1161,12 +1191,10 @@ class IapManager { } } } - return Product.iap(productId, baseDetails, manifest, - offerDetails: offerDetails) as IapProduct; + return Product.iap(productId, baseDetails, manifest, offerDetails: offerDetails) as IapProduct; } - Future> buildProducts( - Set intents) async { + Future> buildProducts(Set intents) async { ProductStore iapStore = ProductStore(); final _productDetails = loadedProductDetails; for (var intent in intents) { @@ -1180,8 +1208,8 @@ class IapManager { final product = await _createProduct(intent, details); iapStore.putProduct(product); if (intent.productId.hasOffer && !iapStore.existsProduct(productId)) { - final originProduct = await _createProduct( - productId.createIntent(scene: intent.scene), details); + final originProduct = + await _createProduct(productId.createIntent(scene: intent.scene), details); iapStore.putProduct(originProduct); } } @@ -1222,8 +1250,7 @@ class IapManager { result = await _inAppPurchase.buyNonConsumable(purchaseParam: param); } if (!result) { - Log.d( - "_requestPurchases error! ${product.productId} ${product.details.price}", + Log.d("_requestPurchases error! ${product.productId} ${product.details.price}", syncFirebase: true); GuruAnalytics.instance.logGuruEvent("dev_iap_action", { "item_category": "request", @@ -1252,13 +1279,12 @@ class IapManager { if (!Platform.isAndroid) { return; } - final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase - .getPlatformAddition(); + final InAppPurchaseAndroidPlatformAddition androidAddition = + _inAppPurchase.getPlatformAddition(); final response = await androidAddition.queryPastPurchases(); for (var purchase in response.pastPurchases) { androidAddition.consumePurchase(purchase); - Log.w( - "[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}"); + Log.w("[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}"); _inAppPurchase.completePurchase(purchase); } @@ -1270,30 +1296,26 @@ class IapManager { Future manualConsumePurchase(PurchaseDetails purchase) async { if (Platform.isAndroid) { final InAppPurchaseAndroidPlatformAddition androidAddition = - _inAppPurchase - .getPlatformAddition(); + _inAppPurchase.getPlatformAddition(); await androidAddition.consumePurchase(purchase); _inAppPurchase.completePurchase(purchase); await GuruDB.instance.deletePendingOrderBySku(sku: purchase.productID); } } - Future manualConsumeAllPurchases( - List> tuples) async { + Future manualConsumeAllPurchases(List> tuples) async { for (var tuple in tuples) { try { final productId = tuple.item1; final purchase = tuple.item2; await manualConsumePurchase(purchase); } catch (error, stacktrace) { - Log.w("consumePurchase error! $error", - stackTrace: stacktrace, syncFirebase: true); + Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true); } } } - Future completeAllPurchases( - List> tuples) async { + Future completeAllPurchases(List> tuples) async { for (var tuple in tuples) { try { final productId = tuple.item1; @@ -1306,8 +1328,7 @@ class IapManager { "item_name": productId.sku, "result": "true", }); - final order = - await _completePurchase(productId, productDetails, details); + final order = await _completePurchase(productId, productDetails, details); } else { GuruAnalytics.instance.logGuruEvent("dev_iap_action", { "item_category": "pending_consume", @@ -1325,8 +1346,7 @@ class IapManager { } } } catch (error, stacktrace) { - Log.w("consumePurchase error! $error", - stackTrace: stacktrace, syncFirebase: true); + Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true); } } } @@ -1339,13 +1359,10 @@ class IapManager { // } Map _filterProductSkus( - {required Set ids, - required Set attrs, - Set? validIds}) { + {required Set ids, required Set attrs, Set? validIds}) { final List> entries = ids .where((productId) => - (validIds?.contains(productId) != false) && - attrs.contains(productId.attr)) + (validIds?.contains(productId) != false) && attrs.contains(productId.attr)) .map((productId) => MapEntry(productId.sku, productId)) .toList(); return Map.fromEntries(entries); @@ -1393,16 +1410,13 @@ class IapManager { } final queryProductIds = queryOneOffChargeSkuMap.keys.toSet(); - queryProductIds.addAll( - GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku)); + queryProductIds.addAll(GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku)); Log.d("refresh product:", tag: "IAP"); for (var productId in queryProductIds) { Log.d(" => $productId", tag: "IAP"); } - final response = - await _queryProducts(queryProductIds).catchError((error, stacktrace) { - Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace", - tag: "IAP"); + final response = await _queryProducts(queryProductIds).catchError((error, stacktrace) { + Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace", tag: "IAP"); return _emptyResponse; }); Log.i("refreshProduct COMPLETED:", tag: "IAP"); @@ -1419,8 +1433,8 @@ class IapManager { detailsMap.addAll(extractProducts(details)); } - GuruAnalytics.instance.logGuruEvent( - "dev_iap_action", {"item_category": "load", "result": "true"}); + GuruAnalytics.instance + .logGuruEvent("dev_iap_action", {"item_category": "load", "result": "true"}); final newProductDetails = Map.of(loadedProductDetails); newProductDetails.addAll(detailsMap); _productDetailsSubject.addEx(newProductDetails); @@ -1440,8 +1454,7 @@ class IapManager { final googlePlayProductDetails = details as GooglePlayProductDetails; final productDetails = googlePlayProductDetails.productDetails; final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; - final offerProductDetails = - GooglePlayProductDetails.fromProductDetails(productDetails); + final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails); for (var id in ids) { final expectBasePlan = id.basePlan; final expectOfferId = id.offerId; @@ -1452,8 +1465,7 @@ class IapManager { final offer = subscriptionOfferDetails[i]; Log.d( "$i expectOfferId:$expectOfferId offerId:${offer.offerId} expectBasePlan:$expectBasePlan basePlanId:${offer.basePlanId}"); - if (expectBasePlan != offer.basePlanId || - expectOfferId != offer.offerId) { + if (expectBasePlan != offer.basePlanId || expectOfferId != offer.offerId) { continue; } detailsMap[id] = offerProductDetails[i]; diff --git a/guru_app/lib/financial/igb/igb_manager.dart b/guru_app/lib/financial/igb/igb_manager.dart new file mode 100644 index 0000000..79bdf34 --- /dev/null +++ b/guru_app/lib/financial/igb/igb_manager.dart @@ -0,0 +1,53 @@ +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/database/guru_db.dart'; +import 'package:guru_app/financial/asset/assets_model.dart'; +import 'package:guru_app/financial/asset/assets_store.dart'; +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/igb/igb_product.dart'; +import 'package:guru_app/financial/igc/igc_model.dart'; +import 'package:guru_app/financial/manifest/manifest_manager.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/inventory/inventory_manager.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/extensions/extensions.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/app_property.dart'; + +/// Created by Haoyi on 2023/2/18 + +class IgbManager { + static final IgbManager _instance = IgbManager._(); + + static IgbManager get instance => _instance; + + IgbManager._(); + + Future init() async { + await InventoryManager.instance.init(); + } + + Future reloadAssets() async {} + + Future accumulate(int igc, TransactionMethod method, {String? scene}) async { + return false; + } + + Future clear() async {} + + Future buildIgbProduct(TransactionIntent intent) async { + final manifest = await ManifestManager.instance.createManifest(intent); + return IgbProduct(intent.productId, manifest, intent.igbCost); + } + + Future redeem(IgbProduct product) async { + Log.v("Igb buy"); + final result = await InventoryManager.instance.consume(product.cost, product.manifest); + if (result) { + await ManifestManager.instance.deliver(product.manifest, TransactionMethod.igb); + } + return true; + } + + void dispose() {} +} diff --git a/guru_app/lib/financial/igb/igb_product.dart b/guru_app/lib/financial/igb/igb_product.dart new file mode 100644 index 0000000..03516c2 --- /dev/null +++ b/guru_app/lib/financial/igb/igb_product.dart @@ -0,0 +1,46 @@ +import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/inventory/db/inventory_database.dart'; +import 'package:guru_app/inventory/inventory_manager.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/id/id_utils.dart'; +import 'package:guru_utils/manifest/manifest.dart'; + +// In-game barter Product +class IgbProduct implements Product { + @override + final ProductId productId; + + @override + final Manifest manifest; + + final List cost; + + String get sku => productId.sku; + + IgbProduct(this.productId, this.manifest, this.cost); + + bool isConsumable() { + return productId.isConsumable; + } + + @override + String toString() { + return 'StockProduct{productId: $productId}'; + } + + @override + OrderEntity createOrder() { + return OrderEntity( + orderId: IdUtils.uuidV4(), + sku: productId.sku, + state: TransactionState.success, + attr: productId.attr, + method: TransactionMethod.igc.index, + currency: TransactionCurrency.igc, + cost: 1, + category: manifest.category, + timestamp: DateTimeUtils.currentTimeInMillis(), + manifest: manifest); + } +} diff --git a/guru_app/lib/financial/igc/igc_manager.dart b/guru_app/lib/financial/igc/igc_manager.dart index 13dbcab..9dbac1b 100644 --- a/guru_app/lib/financial/igc/igc_manager.dart +++ b/guru_app/lib/financial/igc/igc_manager.dart @@ -52,6 +52,10 @@ class IgcManager { await reloadAssets(); } + Future switchSession() async { + init(); + } + Future reloadAssets() async { final orders = await GuruDB.instance .selectOrders(method: TransactionMethod.igc, attrs: [TransactionAttributes.asset]); diff --git a/guru_app/lib/financial/manifest/manifest_manager.dart b/guru_app/lib/financial/manifest/manifest_manager.dart index 935344d..f90dc97 100644 --- a/guru_app/lib/financial/manifest/manifest_manager.dart +++ b/guru_app/lib/financial/manifest/manifest_manager.dart @@ -2,11 +2,15 @@ import 'dart:async'; import 'package:guru_app/financial/igc/igc_manager.dart'; import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/inventory/inventory_manager.dart'; import 'manifest.dart'; /// Created by Haoyi on 2022/8/21 -typedef DetailsDistributor = Future Function(Details, TransactionMethod, String scene); +// typedef DetailsDistributor = Future Function(Details, TransactionMethod, String scene); + +typedef DetailsDistributor = Future> Function( + Details, TransactionMethod, String scene); typedef ManifestBuilder = Future Function(TransactionIntent); @@ -27,15 +31,31 @@ class ManifestManager { final List builders = []; - static Future _deliverIgcDetails( + static Future> _deliverIgcDetails( Details details, TransactionMethod method, String scene) async { if (details.amount > 0) { - IgcManager.instance.accumulate(details.amount, method, scene: scene); - return true; + await IgcManager.instance.accumulate(details.amount, method, scene: scene); } - return false; + return []; } + static Future> _deliverDefaultDetails( + Details details, TransactionMethod method, String scene) async { + if (details.amount > 0) { + return [StockItem.fromDetails(details)]; + } + return []; + } + + // static Future _deliverDetails( + // Details details, TransactionMethod method, String scene) async { + // final stock = StockItem.fromDetails(details); + // if (stock.amount > 0) { + // await InventoryManager.instance.acquire([stock], method, scene); + // } + // return false; + // } + void addDistributor(String type, DetailsDistributor distributor) { distributors[type] = distributor; } @@ -48,11 +68,55 @@ class ManifestManager { this.builders.addAll(builders); } + Future _acquire(List items, TransactionMethod method, Manifest manifest) async { + if (items.isNotEmpty) { + String specific = ""; + switch (method) { + case TransactionMethod.iap: + specific = manifest.contentId; + break; + case TransactionMethod.igc: + specific = "coin"; + break; + case TransactionMethod.igb: + specific = manifest.barterId; + break; + default: + specific = manifest.scene; + break; + } + await InventoryManager.instance.acquire(items, method, specific, scene: manifest.scene); + } + } + + Future> deliverStockItems( + List items, Manifest manifest, TransactionMethod method) async { + final List unsold = []; + for (var item in items) { + if (item.sku == DetailsReservedType.igc) { + await distributors[DetailsReservedType.igc] + ?.call(Details.define(DetailsReservedType.igc, item.amount), method, manifest.scene); + continue; + } + unsold.add(item); + } + return unsold; + } + Future deliver(Manifest manifest, TransactionMethod method) async { bool result = false; + final List unsold = []; for (var details in manifest.details) { - result |= await distributors[details.type]?.call(details, method, manifest.scene) ?? false; + final items = await distributors[details.type]?.call(details, method, manifest.scene) ?? + await _deliverDefaultDetails(details, method, manifest.scene); + final unsoldItems = await deliverStockItems(items, manifest, method); + unsold.addAll(unsoldItems); + result |= unsoldItems.isEmpty; } + if (unsold.isNotEmpty) { + await _acquire(unsold, method, manifest); + } + deliveredManifestStream.add(manifest); return result; } @@ -74,4 +138,12 @@ class ManifestManager { final extras = {ExtraReservedField.scene: scene}; return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details); } + +// Manifest createIgbManifest(int igc, {String? category, String scene = ""}) { +// final details =
[]; +// details.add(Details.define(DetailsReservedType.igc, igc)); +// +// final extras = {ExtraReservedField.scene: scene}; +// return Manifest(category ?? DetailsReservedType.igc, extras: extras, details: details); +// } } diff --git a/guru_app/lib/financial/product/product_model.dart b/guru_app/lib/financial/product/product_model.dart index 560aebf..1d91c1c 100644 --- a/guru_app/lib/financial/product/product_model.dart +++ b/guru_app/lib/financial/product/product_model.dart @@ -1,9 +1,11 @@ import 'dart:io'; import 'package:guru_app/financial/data/db/order_database.dart'; +import 'package:guru_app/financial/igb/igb_product.dart'; import 'package:guru_app/financial/manifest/manifest.dart'; import 'package:guru_app/financial/manifest/manifest_manager.dart'; import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/inventory/inventory_manager.dart'; import 'package:guru_utils/hash/hash.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:guru_app/financial/iap/iap_model.dart'; @@ -51,9 +53,9 @@ class TransactionAttributes { // Offer products for sale in your app for a one-off charge @deprecated - static const possessive = 1; - static const asset = 1; - static const consumable = 2; + static const possessive = DetailsAttr.permanent; + static const asset = DetailsAttr.permanent; + static const consumable = DetailsAttr.consumable; static const Set oneOffChargeAttributes = {asset, consumable}; @@ -174,6 +176,7 @@ class ProductId { TransactionIntent createIntent( {required String scene, int igcCost = 0, + List igbCost = const [], bool sales = false, double rate = 1.0, EligibilityCriteria eligibilityCriteria = @@ -193,6 +196,13 @@ class ProductId { final manifest = await ManifestManager.instance.createManifest(intent); return IgcProduct(this, manifest, igcCost); } + + Future createIgbProduct(List igbCost, String scene, + {Manifest? specified}) async { + final manifest = specified ?? + await ManifestManager.instance.createManifest(createIntent(scene: scene, igbCost: igbCost)); + return IgbProduct(this, manifest, igbCost); + } } abstract class Product { @@ -207,12 +217,14 @@ abstract class Product { factory Product.reward(ProductId productId, Manifest manifest) = RewardProduct; + factory Product.igb(ProductId productId, Manifest manifest, List cost) = IgbProduct; + // // factory Product.gems(ProductId productId, int price, Manifest manifest) = GemProduct; // // factory Product.reward(Reward reward) = RewardProduct; // - OrderEntity createOrder(); +// OrderEntity createOrder(); } class TransactionState { @@ -223,11 +235,17 @@ class TransactionState { static const expired = -3; } +// 交易方式 enum TransactionMethod { iap, // IAP购买 - igc, // In-game currency 购买 + igc, // In-game currency 购买(coin/gems..) reward, // 奖励获得 - none + + bonus, // 优惠 + igb, // In-game barter + free, + migrate, + unknown } String convertTransactionMethodName(TransactionMethod method) { @@ -238,14 +256,23 @@ String convertTransactionMethodName(TransactionMethod method) { return "igc"; case TransactionMethod.reward: return "reward"; + case TransactionMethod.bonus: + return "bonus"; + case TransactionMethod.igb: + return "igb"; + case TransactionMethod.free: + return "prop"; + case TransactionMethod.migrate: + return "migrate"; default: - return "none"; + return "unknown"; } } class TransactionIntent { final ProductId productId; - final int igcCost; + final int igcCost; // In-game currency cost + final List igbCost; // In-game barter cost final String scene; // 购买场景(最终会用到logEarnVirtualCurrency的item_category上) final bool sales; // 是否为促销商品 final double rate; // 默认1.0 指收益率,如:打折促销时设成1.2,最终的收益将是1.2倍 @@ -253,6 +280,7 @@ class TransactionIntent { TransactionIntent(this.productId, this.scene, {this.igcCost = 0, + this.igbCost = const [], this.sales = false, this.rate = 1.0, this.eligibilityCriteria = EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup}); diff --git a/guru_app/lib/financial/product/product_profile.dart b/guru_app/lib/financial/product/product_profile.dart index 24cb77e..246ac06 100644 --- a/guru_app/lib/financial/product/product_profile.dart +++ b/guru_app/lib/financial/product/product_profile.dart @@ -11,6 +11,7 @@ class ProductProfile { final Set iapIds = {}; final Set igcIds = {}; final Set rewardIds = {}; + final Set igbIds = {}; final Map groupMap; @@ -19,16 +20,17 @@ class ProductProfile { final Map> _offerIds = {}; final List> _idsMap = - List.generate(TransactionAttributes.count, (index) => {}); + List.generate(TransactionAttributes.count, (index) => {}); - ProductProfile({required Set oneOffChargeIapIds, - required Set subscriptionsIapIds, - Set pointsIapIds = const {}, - Set igcIds = const {}, - Set rewardIds = const {}, - this.groupMap = const {}, - List manifestBuilders = const [], - this.noAdsCapIds = const {}}) { + ProductProfile( + {required Set oneOffChargeIapIds, + required Set subscriptionsIapIds, + Set pointsIapIds = const {}, + Set igcIds = const {}, + Set rewardIds = const {}, + this.groupMap = const {}, + List manifestBuilders = const [], + this.noAdsCapIds = const {}}) { for (var productId in oneOffChargeIapIds) { _define(productId, TransactionMethod.iap); } @@ -70,7 +72,16 @@ class ProductProfile { case TransactionMethod.reward: rewardIds.add(definedProductId); break; - case TransactionMethod.none: + case TransactionMethod.bonus: + break; + case TransactionMethod.igb: + igbIds.add(definedProductId); + break; + case TransactionMethod.free: + break; + case TransactionMethod.migrate: + break; + case TransactionMethod.unknown: break; } _idsMap[productId.attr][productId.sku] = definedProductId; @@ -114,11 +125,12 @@ class IapProfile { final List subscriptionsIapIds = []; final List noAdsCapIds; final List> _idsMap = - List.generate(TransactionAttributes.count, (index) => {}); + List.generate(TransactionAttributes.count, (index) => {}); - IapProfile({required List oneOffChargeIapIds, - required List subscriptionsIapIds, - this.noAdsCapIds = const []}) { + IapProfile( + {required List oneOffChargeIapIds, + required List subscriptionsIapIds, + this.noAdsCapIds = const []}) { for (var productId in oneOffChargeIapIds) { _define(productId); } @@ -130,7 +142,7 @@ class IapProfile { bool hasIap() => oneOffChargeIapIds.isEmpty && subscriptionsIapIds.isEmpty; static final IapProfile invalid = - IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []); + IapProfile(oneOffChargeIapIds: [], subscriptionsIapIds: [], noAdsCapIds: []); ProductId _define(ProductId productId) { if (productId.isOneOffCharge) { diff --git a/guru_app/lib/financial/reward/reward_manager.dart b/guru_app/lib/financial/reward/reward_manager.dart index 85a5347..769077c 100644 --- a/guru_app/lib/financial/reward/reward_manager.dart +++ b/guru_app/lib/financial/reward/reward_manager.dart @@ -28,9 +28,13 @@ class RewardManager { await reloadAssets(); } + Future switchSession() async { + reloadAssets(); + } + Future reloadAssets() async { - final transactions = await GuruDB.instance.selectOrders( - method: TransactionMethod.reward, attrs: [TransactionAttributes.asset]); + final transactions = await GuruDB.instance + .selectOrders(method: TransactionMethod.reward, attrs: [TransactionAttributes.asset]); final newAssetsStore = AssetsStore(); for (var transaction in transactions) { final productId = transaction.productId; diff --git a/guru_app/lib/firebase/remoteconfig/remote_config_manager.dart b/guru_app/lib/firebase/remoteconfig/remote_config_manager.dart index f4d2db9..fb34c22 100644 --- a/guru_app/lib/firebase/remoteconfig/remote_config_manager.dart +++ b/guru_app/lib/firebase/remoteconfig/remote_config_manager.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:guru_app/ads/core/ads_config.dart'; import 'package:guru_app/analytics/data/analytics_model.dart'; +import 'package:guru_app/firebase/firebase.dart'; import 'package:guru_app/firebase/remoteconfig/reserved_remote_config_models.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_utils/http/http_model.dart'; @@ -18,8 +19,7 @@ part "remote_config_interface.dart"; part "remote_config_reserved_constants.dart"; class RemoteConfigManager extends IRemoteConfig { - final BehaviorSubject _subject = - BehaviorSubject.seeded(null); + final BehaviorSubject _subject = BehaviorSubject.seeded(null); static RemoteConfigManager? _instance; static RemoteConfigManager _getInstance() { @@ -31,7 +31,7 @@ class RemoteConfigManager extends IRemoteConfig { static RemoteConfigManager get instance => _getInstance(); - static final RegExp _invalidABKey = RegExp('[^a-zA-Z0-9_-]'); + static final RegExp invalidABKeyRegExp = RegExp('[^a-zA-Z0-9_-]'); RemoteConfigManager._internal(); @@ -42,17 +42,16 @@ class RemoteConfigManager extends IRemoteConfig { minimumFetchInterval: const Duration(hours: 2), )); - _subject.add(remoteConfig); + _subject.add(FirebaseRemoteConfigWrapper._(remoteConfig)); try { await remoteConfig.setDefaults(defaultConfigs); await remoteConfig.activate(); } catch (exception) { - Log.d( - "Unable to fetch remote config. Cached or default values will be used!", + Log.d("Unable to fetch remote config. Cached or default values will be used!", error: exception); } finally { - _subject.add(remoteConfig); + _subject.add(FirebaseRemoteConfigWrapper._(remoteConfig)); } } @@ -61,11 +60,10 @@ class RemoteConfigManager extends IRemoteConfig { try { await remoteConfig.fetchAndActivate(); } catch (exception) { - Log.d( - "Unable to fetch remote config. Cached or default values will be used!", + Log.d("Unable to fetch remote config. Cached or default values will be used!", error: exception); } finally { - _subject.add(remoteConfig); + _subject.add(FirebaseRemoteConfigWrapper._(remoteConfig)); } } @@ -95,12 +93,10 @@ class RemoteConfigManager extends IRemoteConfig { final data = config.getAll(); final result = { for (var entry in data.entries) - "${entry.key} [${valueSourceToString(entry.value.source)}]": - entry.value.asString() + "${entry.key} [${valueSourceToString(entry.value.source)}]": entry.value.asString() }; result["last_fetch_remote_config_time"] = config.lastFetchTime.toString(); - result["last_fetch_remote_config_status"] = - config.lastFetchStatus.toString(); + result["last_fetch_remote_config_status"] = config.lastFetchStatus.toString(); return result; } @@ -113,14 +109,13 @@ class RemoteConfigManager extends IRemoteConfig { )); await remoteConfig.fetchAndActivate(); } catch (exception) { - Log.d( - "Unable to fetch remote config. Cached or default values will be used $exception", + Log.d("Unable to fetch remote config. Cached or default values will be used $exception", error: exception); if (debug) { rethrow; } } finally { - _subject.add(remoteConfig); + _subject.add(FirebaseRemoteConfigWrapper._(remoteConfig)); await remoteConfig.setConfigSettings(RemoteConfigSettings( fetchTimeout: const Duration(seconds: 15), minimumFetchInterval: const Duration(hours: 2), @@ -143,7 +138,7 @@ class RemoteConfigManager extends IRemoteConfig { for (var jsonEntry in jsonValue.entries) { if (jsonEntry.key.contains("guru_ab_")) { String abName = jsonEntry.key.replaceFirst("guru_ab_", ""); - if (abName.contains(_invalidABKey)) { + if (abName.contains(invalidABKeyRegExp)) { Log.w("abName($abName) length is invalid! $abName"); invalidABKeys.add(abName); } else { @@ -151,7 +146,7 @@ class RemoteConfigManager extends IRemoteConfig { invalidABKeys.add(abName); abName = abName.substring(0, 20); } - result["ab_$abName"] = jsonEntry.value.toString(); + result[GuruAnalytics.buildVariantKey(abName)] = jsonEntry.value.toString(); Log.i("abName:ab_$abName value:${jsonEntry.value}"); } } @@ -164,15 +159,14 @@ class RemoteConfigManager extends IRemoteConfig { } } if (invalidABKeys.isNotEmpty) { - GuruAnalytics.instance.logException( - InvalidABPropertyKeysException(invalidABKeys, cause: cause)); + GuruAnalytics.instance + .logException(InvalidABPropertyKeysException(invalidABKeys, cause: cause)); } return result; } @override - bool? getBool(String name, {bool? defaultValue}) => - _subject.value?.getBool(name) ?? defaultValue; + bool? getBool(String name, {bool? defaultValue}) => _subject.value?.getBool(name) ?? defaultValue; @override String? getString(String name, {String? defaultValue}) => @@ -185,11 +179,10 @@ class RemoteConfigManager extends IRemoteConfig { _subject.value?.getDouble(name) ?? defaultValue; @override - int? getInt(String name, {int? defaultValue}) => - _subject.value?.getInt(name) ?? defaultValue; + int? getInt(String name, {int? defaultValue}) => _subject.value?.getInt(name) ?? defaultValue; - Stream observeConfig() => - _subject.stream.map((config) => config ?? FirebaseRemoteConfig.instance); + Stream observeConfig() => _subject.stream + .map((config) => config ?? FirebaseRemoteConfigWrapper._(FirebaseRemoteConfig.instance)); @override Stream observeBool(String name, {bool? defaultValue}) => @@ -207,3 +200,23 @@ class RemoteConfigManager extends IRemoteConfig { Stream observeInt(String name, {int? defaultValue}) => observeConfig().map((config) => config.getInt(name)); } + +class FirebaseRemoteConfigWrapper { + final FirebaseRemoteConfig config; + + const FirebaseRemoteConfigWrapper._(this.config); + + DateTime get lastFetchTime => config.lastFetchTime; + + RemoteConfigFetchStatus get lastFetchStatus => config.lastFetchStatus; + + static String _key(String key) => GuruApp.instance.getRemoteConfigKey(key); + + String getString(String key) => config.getString(_key(key)); + + bool getBool(String key) => config.getBool(_key(key)); + + double getDouble(String key) => config.getDouble(_key(key)); + + int getInt(String key) => config.getInt(_key(key)); +} diff --git a/guru_app/lib/firebase/remoteconfig/remote_config_reserved_constants.dart b/guru_app/lib/firebase/remoteconfig/remote_config_reserved_constants.dart index 97f94f4..7230e24 100644 --- a/guru_app/lib/firebase/remoteconfig/remote_config_reserved_constants.dart +++ b/guru_app/lib/firebase/remoteconfig/remote_config_reserved_constants.dart @@ -41,6 +41,6 @@ extension RemoteConfigReservedConstants on RemoteConfigManager { }; static String? getDefaultConfigString(String key) { - return GuruApp.instance.defaultRemoteConfig[key]; + return GuruApp.instance.defaultRemoteConfig[GuruApp.instance.getRemoteConfigKey(key)]; } } diff --git a/guru_app/lib/guru_app.dart b/guru_app/lib/guru_app.dart index dd0bcf7..9cb4ad5 100644 --- a/guru_app/lib/guru_app.dart +++ b/guru_app/lib/guru_app.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:io'; import 'package:adjust_sdk/adjust_event.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; @@ -7,7 +6,11 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:guru_app/account/account_data_store.dart'; import 'package:guru_app/account/account_manager.dart'; +import 'package:guru_app/account/model/account.dart'; +import 'package:guru_app/account/model/credential.dart'; +import 'package:guru_app/account/model/user.dart'; import 'package:guru_app/ads/ads_manager.dart'; +import 'package:guru_app/analytics/abtest/abtest_model.dart'; import 'package:guru_app/analytics/guru_analytics.dart'; import 'package:guru_app/app/app_models.dart'; import 'package:guru_app/database/guru_db.dart'; @@ -18,10 +21,15 @@ import 'package:guru_app/financial/manifest/manifest_manager.dart'; import 'package:guru_app/financial/product/product_model.dart'; import 'package:guru_app/financial/reward/reward_manager.dart'; import 'package:guru_app/firebase/dxlinks/dxlink_manager.dart'; +import 'package:guru_app/inventory/inventory_manager.dart'; import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; +import 'package:guru_app/property/app_property.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; import 'package:guru_utils/collection/collectionutils.dart'; import 'package:guru_utils/controller/aware/ads/overlay/ads_overlay.dart'; import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/device/device_utils.dart'; import 'package:guru_utils/lifecycle/lifecycle_manager.dart'; import 'package:guru_utils/network/network_utils.dart'; import 'package:guru_utils/property/app_property.dart'; @@ -32,10 +40,13 @@ import 'package:guru_utils/log/log.dart'; import 'package:guru_utils/packages/guru_package.dart'; import 'package:guru_utils/ads/ads.dart'; import 'package:guru_utils/guru_utils.dart'; +import 'package:guru_utils/property/property_model.dart'; import 'package:logger/logger.dart' as Logger; import 'package:guru_utils/aigc/bi/ai_bi.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_app/analytics/abtest/abtest_model.dart'; + export 'package:firebase_core/firebase_core.dart'; export 'package:guru_app/app/app_models.dart'; export 'package:guru_utils/log/log.dart'; @@ -49,13 +60,15 @@ export 'dart:io'; export 'dart:math'; export 'package:guru_app/financial/manifest/manifest.dart'; export 'package:guru_app/firebase/messaging/remote_messaging_manager.dart'; - +export 'package:guru_app/analytics/abtest/abtest_model.dart'; /// Created by Haoyi on 2022/8/25 abstract class AppSpec { String get appName; + AppCategory get appCategory; + String get flavor; AppDetails get details; @@ -69,6 +82,10 @@ abstract class AppSpec { Deployment get deployment; Map get defaultRemoteConfig; + + Map get localABTestExperiments; + + String getRemoteConfigKey(String key); } class NotImplementationAppSpecCreatorException implements Exception { @@ -83,14 +100,12 @@ class NotImplementationAppSpecCreatorException implements Exception { class AppEnv { final AppSpec spec; final RootPackage package; - final BackgroundMessageHandler? backgroundMessageHandler; - final ToastDelegate? toastDelegate; + final IGuruSdkProtocol protocol; AppEnv( {required this.spec, required this.package, - this.backgroundMessageHandler, - this.toastDelegate}); + required this.protocol}); } extension _GuruPackageExtension on GuruPackage { @@ -130,8 +145,80 @@ extension _GuruPackageExtension on GuruPackage { child._dispatchInitializeAsync(); } } + + Future _dispatchSwitchSession(String oldToken, String newToken) async { + await switchSession(oldToken, newToken); + children.sort((p1, p2) { + return p2.priority.compareTo(p1.priority); + }); + for (var child in children) { + if (flattenChildrenAsyncInit) { + child._dispatchSwitchSession(oldToken, newToken); + } else { + await child._dispatchSwitchSession(oldToken, newToken); + } + } + } } +enum AppCategory { game, app } + +abstract class IGuruSdkProtocol { + static void _unimplementedError(String name) { + Log.e( + "[$name] It is critically important that the GuruSDK protocol be implemented with precision. \n" + "Failure to adhere to its standards will result in inaccuracies within our analytics data,\n" + "thereby severely compromising our marketing strategies and the effectiveness of our user acquisition efforts.\n" + "It is essential to understand that non-compliance is not an option,\n" + "as it poses significant risks to our operational success and strategic objectives."); + throw UnimplementedError("Please fully implement the IGuruSdkProtocol!"); + } + + InventoryDelegate? get inventoryDelegate => null; + + BackgroundMessageHandler? get backgroundMessageHandler => null; + + ToastDelegate? get toastDelegate => null; + + IAccountAuthDelegate? get accountAuthDelegate => null; + + String getLevelName() { + if (GuruApp.instance.appSpec.appCategory == AppCategory.game) { + _unimplementedError("getLevelName"); + } + return "app"; + } + + int getCurrentLevel() { + if (GuruApp.instance.appSpec.appCategory == AppCategory.game) { + _unimplementedError("getCurrentLevel"); + } + return 1; + } +} + +final Set _deviceSharedProperties = { + PropertyKeys.deviceId, + PropertyKeys.version, + PropertyKeys.buildNumber, + PropertyKeys.firstInstallTime, + PropertyKeys.firstInstallVersion, + PropertyKeys.prevInstallVersion, + PropertyKeys.latestInstallVersion, + PropertyKeys.previousInstalledVersion, + PropertyKeys.latestLtDate, + PropertyKeys.ltDays, + PropertyKeys.appInstanceId, + PropertyKeys.debugMode, + PropertyKeys.keepOnScreenDuration, + PropertyKeys.analyticsAdId, + PropertyKeys.analyticsAdjustId, + PropertyKeys.analyticsDeviceId, + PropertyKeys.analyticsIdfa, + PropertyKeys.analyticsFirebaseId, + PropertyKeys.latestAnalyticsStrategy +}; + class GuruApp { static late GuruApp _instance; @@ -141,6 +228,12 @@ class GuruApp { final AppSpec appSpec; + final IGuruSdkProtocol protocol; + RemoteDeployment? _remoteDeployment; + + RemoteDeployment get remoteDeployment => + _remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment(); + String get appName => appSpec.appName; String get flavor => appSpec.flavor; @@ -157,8 +250,12 @@ class GuruApp { Set get conversionEvents => appSpec.deployment.conversionEvents; - GuruApp._({required this.appSpec, required this.rootPackage, ToastDelegate? toastDelegate}) { - GuruUtils.toastDelegate = toastDelegate; + GuruApp._( + {required this.appSpec, + required this.rootPackage, + required this.protocol, + }) { + GuruUtils.toastDelegate = protocol.toastDelegate; AdsOverlay.bind(showBanner: GuruPopup.instance.showAdsBanner); } @@ -167,8 +264,15 @@ class GuruApp { Iterable> get localizationsDelegates => rootPackage._mergeLocalizationsDelegates(); + String getRemoteConfigKey(String key) => appSpec.getRemoteConfigKey(key); + bool? _check; + @visibleForTesting + static void setMockInstance(GuruApp app) { + _instance = app; + } + Future _initialize() async { try { await GuruDB.instance.initDatabase(); @@ -182,6 +286,57 @@ class GuruApp { } } + Future _migrateDeviceSharedData(PropertyBundle latestData) async { + final PropertyBundle bundle = PropertyBundle(); + final keys = { + ..._deviceSharedProperties, + ...(protocol.accountAuthDelegate?.deviceSharedProperties ?? {}) + }; + for (var key in keys) { + final value = latestData.getString(key); + if (value != null) { + bundle.setString(key, value); + } + } + await AppProperty.getInstance().setProperties(bundle); + } + + RemoteDeployment refreshRemoteDeployment() { + try { + return _remoteDeployment ??= RemoteConfigManager.instance.getRemoteDeployment(); + } catch (error, stacktrace) { + Log.w("refreshRemoteDeployment error:$error, $stacktrace"); + } + return RemoteDeployment.fromJson({}); + } + + Future switchAccount(GuruUser user, Credential credential, {GuruUser? oldUser}) async { + final String oldToken = oldUser?.uid ?? ""; + final String newToken = user.uid; + try { + final previousUserProperties = + PropertyBundle(map: await AppProperty.getInstance().loadAllValues()); + + final result = await GuruDB.instance.switchSession(oldToken, newToken); + if (!result) { + Log.w("switchSession failed"); + return false; + } + AppProperty.reload(cacheSize: appSpec.deployment.propertyCacheSize); + await _migrateDeviceSharedData(previousUserProperties); + await GuruSettings.instance.refresh(); + await DeviceUtils.reload(); + FinancialManager.instance.switchSession(oldToken, newToken); + GuruAnalytics.instance.switchSession(oldToken, newToken); + await AccountManager.instance.processLogin(user, credential); + await rootPackage._dispatchSwitchSession(oldToken, newToken); + return true; + } catch (error, stacktrace) { + Log.w("switchSession error:$error, $stacktrace"); + return false; + } + } + Future _checkApp() async { try { final pkgName = (await PackageInfo.fromPlatform()).appName; @@ -210,6 +365,9 @@ class GuruApp { Future _dispatchInitializeSync() async { await RemoteConfigManager.instance.init(appSpec.defaultRemoteConfig); + refreshRemoteDeployment(); + await DeviceUtils.initialize(); + await GuruAnalytics.instance.prepare(); await rootPackage._dispatchInitialize(); try { GuruUtils.isTablet = (await GuruApplovinFlutter.instance.isTablet()) ?? false; @@ -237,7 +395,7 @@ class GuruApp { } static Future initialize({required AppEnv appEnv}) async { - final backgroundMessageHandler = appEnv.backgroundMessageHandler; + final backgroundMessageHandler = appEnv.protocol.backgroundMessageHandler; if (backgroundMessageHandler != null) { FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler); } @@ -248,9 +406,18 @@ class GuruApp { Log.e("Firebase.initializeApp() error!", error: error, stackTrace: stacktrace); } GuruUtils.flavor = appEnv.spec.flavor; + + /// 这里不用担心重复初始化,因为initialize会把对应的 AuthType 重新赋值 + /// 如果传入的 AuthType 有重复,会覆盖之前的 AuthType + AuthCredentialManager.initialize([ + ...AccountManager.defaultSupportedAuthCredentialDelegates, + ...appEnv.protocol.accountAuthDelegate?.supportedAuthCredentialDelegates ?? [] + ]); try { _instance = GuruApp._( - appSpec: appEnv.spec, rootPackage: appEnv.package, toastDelegate: appEnv.toastDelegate); + appSpec: appEnv.spec, + rootPackage: appEnv.package, + protocol: appEnv.protocol); Log.init(_instance.appName, persistentLogFileSize: appEnv.spec.deployment.logFileSizeLimit, persistentLogCount: appEnv.spec.deployment.logFileCount, @@ -279,8 +446,7 @@ extension GuruAppInitializerExt on GuruApp { await RemoteConfigManager.instance.fetchAndActivate(); final cdnConfig = RemoteConfigManager.instance.getCdnConfig(); HttpEx.init(cdnConfig, GuruApp.instance.appSpec.details.storagePrefix); - - final remoteDeployment = RemoteConfigManager.instance.getRemoteDeployment(); + refreshRemoteDeployment(); Settings.get() .keepOnScreenDuration .set(remoteDeployment.keepScreenOnDuration * DateTimeUtils.minuteInMillis); diff --git a/guru_app/lib/inventory/db/inventory_database.dart b/guru_app/lib/inventory/db/inventory_database.dart new file mode 100644 index 0000000..ce8c943 --- /dev/null +++ b/guru_app/lib/inventory/db/inventory_database.dart @@ -0,0 +1,408 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:guru_app/database/guru_db.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_utils/database/batch/batch_data.dart'; +import 'package:guru_utils/datetime/datetime_utils.dart'; +import 'package:guru_utils/id/identifiable.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/manifest/manifest.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'inventory_database.g.dart'; + +@JsonSerializable() +class LimitedBalance { + @JsonKey(name: "a", defaultValue: 0) + final int amount; + + @JsonKey(name: "e", defaultValue: -1) + final int expireAt; + + @JsonKey(name: "r", defaultValue: false) + final bool recycle; + + LimitedBalance(this.amount, this.expireAt, {this.recycle = false}); + + factory LimitedBalance.fromJson(Map json) => _$LimitedBalanceFromJson(json); + + Map toJson() => _$LimitedBalanceToJson(this); +} + +@JsonSerializable() +class TimeSensitiveData { + @JsonKey(name: "v", defaultValue: []) + final List valid; + + @JsonKey(name: "e", defaultValue: []) + final List expired; + + int get validBalance => + valid.isEmpty ? 0 : valid.map((e) => e.amount).reduce((total, amount) => total + amount); + + int get expiredBalance => + expired.isEmpty ? 0 : expired.map((e) => e.amount).reduce((total, amount) => total + amount); + + const TimeSensitiveData( + {this.valid = const [], this.expired = const []}); + + TimeSensitiveData.create({LimitedBalance? balance}) + : valid = ((balance?.expireAt ?? 0) > DateTimeUtils.currentTimeInMillis()) ? [balance!] : [], + expired = []; + + factory TimeSensitiveData.fromJson(Map json) => + _$TimeSensitiveDataFromJson(json); + + Map toJson() => _$TimeSensitiveDataToJson(this); + + TimeSensitiveData attachLimitedBalance(LimitedBalance balance) { + if (balance.expireAt > DateTimeUtils.currentTimeInMillis()) { + return TimeSensitiveData(valid: List.of(valid)..add(balance), expired: List.of(expired)); + } + return this; + } + + /// + TimeSensitiveData consume(int balance) { + final now = DateTimeUtils.currentTimeInMillis(); + final newValid = []; + for (var item in valid) { + int amount = item.amount; + if (now < item.expireAt) { + if (balance >= item.amount) { + balance -= item.amount; + continue; + } else { + amount -= balance; + balance = 0; + } + } + // 这里只针对未过期的道具进行处理 + newValid.add(LimitedBalance(amount, item.expireAt)); + } + newValid.sort((a, b) => b.expireAt.compareTo(a.expireAt)); + return TimeSensitiveData(valid: newValid, expired: expired); + } + + RecycleResult recycleExpiredBalance({int? transactionTs}) { + final now = transactionTs ?? DateTimeUtils.currentTimeInMillis(); + final newValid = []; + final newExpired = []; + int expiredBalance = 0; + for (var item in valid) { + if (now >= item.expireAt) { + expiredBalance += item.amount; + if (item.recycle) { + newExpired.add(item); + } + } else { + newValid.add(item); + } + } + newValid.sort((a, b) => b.expireAt.compareTo(a.expireAt)); + return RecycleResult(expiredBalance, TimeSensitiveData(valid: newValid, expired: newExpired)); + } +} + +class RecycleResult { + final int expiredBalance; + final TimeSensitiveData timeSensitiveData; + + RecycleResult(this.expiredBalance, this.timeSensitiveData); +} + +class ConsumeResult { + final bool consumed; + final InventoryItem item; + + ConsumeResult.success(this.item) : consumed = true; + + ConsumeResult.error(this.item) : consumed = false; +} + +@JsonSerializable(constructor: "_") +class InventoryDetails { + @JsonKey(name: "a", defaultValue: {}) + final Map acquired; + + @JsonKey(name: "c", defaultValue: {}) + final Map consumed; + + @JsonKey(name: "d") + final Map data; + + const InventoryDetails._(this.acquired, this.consumed, this.data); + + InventoryDetails.empty() + : acquired = const {}, + consumed = const {}, + data = const {}; + + InventoryDetails.create(int amount) + : acquired = (amount == 0) + ? const {} + : {TransactionMethod.unknown: amount}, + consumed = const {}, + data = {}; + + // 通过什么方式获得了多少 + InventoryDetails acquire(TransactionMethod method, int amount) { + return InventoryDetails._( + Map.from(acquired)..[method] = (acquired[method] ?? 0) + amount, + Map.from(consumed), + Map.from(data)); + } + + // 在什么场景消耗了多少 + InventoryDetails consume(String scene, int amount) { + return InventoryDetails._( + Map.from(acquired), + Map.from(consumed)..[scene] = (consumed[scene] ?? 0) + amount, + Map.from(data)); + } + + factory InventoryDetails.fromJson(Map json) => _$InventoryDetailsFromJson(json); + + Map toJson() => _$InventoryDetailsToJson(this); + + void setInt(String key, int value) { + data[key] = value; + } + + void setDouble(String key, double value) { + data[key] = value; + } + + void setString(String key, String value) { + data[key] = value; + } + + void setBool(String key, bool value) { + data[key] = value; + } + + int? getInt(String key) { + return data[key]; + } + + double? getDouble(String key) { + return data[key]; + } + + String? getString(String key) { + return data[key]; + } + + bool? getBool(String key) { + return data[key]; + } +} + +class InventoryDetailsStringConvert implements JsonConverter { + const InventoryDetailsStringConvert(); + + @override + InventoryDetails fromJson(String json) { + if (json.isEmpty) { + return InventoryDetails.empty(); + } + final result = jsonDecode(json); + return InventoryDetails.fromJson(result); + } + + @override + String toJson(InventoryDetails transaction) { + return jsonEncode(transaction); + } +} + +class TimeSensitiveDataStringConvert implements JsonConverter { + const TimeSensitiveDataStringConvert(); + + @override + TimeSensitiveData fromJson(String json) { + if (json.isEmpty) { + return const TimeSensitiveData(); + } + final result = jsonDecode(json); + return TimeSensitiveData.fromJson(result); + } + + @override + String toJson(TimeSensitiveData transaction) { + return jsonEncode(transaction); + } +} + +const InventoryDetailsStringConvert inventoryDetailsStringConvert = InventoryDetailsStringConvert(); +const TimeSensitiveDataStringConvert timeSensitiveDataStringConvert = + TimeSensitiveDataStringConvert(); + +class InventoryTable { + static const tbName = "inventory"; // Product Transaction Table + static const dbSku = "sku"; + static const dbBalance = "balance"; + static const dbCategory = "cat"; + static const dbAttr = "attr"; + static const dbDetails = "details"; + static const dbTimeSensitive = "tsv"; + static const dbUpdateAt = "update_at"; + static const dbCreateAt = "create_at"; + + static Future createTable(Transaction delegate) async { + const v1Fields = "${InventoryTable.dbSku} TEXT PRIMARY KEY," + "${InventoryTable.dbBalance} INTEGER NOT NULL DEFAULT 0," + "${InventoryTable.dbCategory} TEXT NOT NULL DEFAULT ''," + "${InventoryTable.dbAttr} INTEGER NOT NULL DEFAULT ${DetailsAttr.consumable}," + "${InventoryTable.dbDetails} TEXT NOT NULL DEFAULT ''," + "${InventoryTable.dbTimeSensitive} TEXT NOT NULL DEFAULT ''," + "${InventoryTable.dbCreateAt} INTEGER NOT NULL DEFAULT 0," + "${InventoryTable.dbUpdateAt} INTEGER NOT NULL DEFAULT 0"; + + const cmd = "CREATE TABLE ${InventoryTable.tbName} (" + "$v1Fields" + ");"; + + Log.v("#### cmd: $cmd"); + + await delegate.execute(cmd); + await delegate.execute( + "CREATE INDEX inventory_item_idx ON ${InventoryTable.tbName} (${InventoryTable.dbSku});"); + await delegate.execute( + "CREATE INDEX inventory_item_category_idx ON ${InventoryTable.tbName} (${InventoryTable.dbCategory});"); + } +} + +@JsonSerializable(constructor: "_") +class InventoryItem implements Identifiable { + @JsonKey(name: InventoryTable.dbSku) + final String sku; + + @JsonKey(name: InventoryTable.dbBalance) + final int balance; // 永久的+时效 + + @JsonKey(name: InventoryTable.dbCategory) + final String category; + + @JsonKey(name: InventoryTable.dbAttr) + final int attr; + + @JsonKey(name: InventoryTable.dbTimeSensitive) + @timeSensitiveDataStringConvert + final TimeSensitiveData timeSensitive; + + @JsonKey(name: InventoryTable.dbUpdateAt) + final int updateAt; + + @JsonKey(name: InventoryTable.dbCreateAt) + final int createAt; + + @JsonKey(name: InventoryTable.dbDetails) + @inventoryDetailsStringConvert + final InventoryDetails details; + + InventoryItem._(this.sku, this.balance, this.category, this.attr, this.details, + this.timeSensitive, this.createAt, this.updateAt); + + InventoryItem.create(this.sku, this.category, this.attr, + {int expireAt = -1, this.balance = 0, TransactionMethod method = TransactionMethod.unknown}) + : updateAt = DateTimeUtils.currentTimeInMillis(), + createAt = DateTimeUtils.currentTimeInMillis(), + details = InventoryDetails.create(balance), + timeSensitive = TimeSensitiveData.create(balance: LimitedBalance(balance, expireAt)); + + InventoryItem acquire(TransactionMethod method, int amount, {int? expiredAt}) { + final now = DateTimeUtils.currentTimeInMillis(); + if (expiredAt != null && expiredAt > now) { + timeSensitive.valid.add(LimitedBalance(amount, expiredAt)); + } + final recycled = timeSensitive.recycleExpiredBalance(); + final target = balance + amount; + return InventoryItem._(sku, (target - recycled.expiredBalance).clamp(0, target), category, attr, + details.acquire(method, amount), recycled.timeSensitiveData, createAt, now); + } + + ConsumeResult consume(int amount, {String scene = ""}) { + final now = DateTimeUtils.currentTimeInMillis(); + final recycled = timeSensitive.recycleExpiredBalance(); + int remainBalance = (balance - recycled.expiredBalance).clamp(0, balance); + if (remainBalance >= amount && attr == DetailsAttr.consumable) { + remainBalance -= amount; + return ConsumeResult.success(InventoryItem._( + sku, + remainBalance, + category, + attr, + details.consume(scene, amount), + recycled.timeSensitiveData.consume(amount), + createAt, + now)); + } + return ConsumeResult.error(InventoryItem._( + sku, remainBalance, category, attr, details, recycled.timeSensitiveData, createAt, now)); + } + + ConsumeResult consumeTimeSensitiveOnly(int amount, {String scene = "", int? transactionTs}) { + final now = transactionTs ?? DateTimeUtils.currentTimeInMillis(); + final recycled = timeSensitive.recycleExpiredBalance(transactionTs: transactionTs); + final validBalance = recycled.timeSensitiveData.validBalance; + int remainBalance = (balance - recycled.expiredBalance).clamp(0, balance); + + /// 这里只是针对有时效性的道具进行消耗,如果时效性道具不足,将返回错误 + if (validBalance >= amount && attr == DetailsAttr.consumable) { + /// 虽然这里只是针对时效性道具进行消耗,但是这里的balance还是会进行更新 + remainBalance = (remainBalance - amount).clamp(0, remainBalance); + return ConsumeResult.success(InventoryItem._( + sku, + remainBalance, + category, + attr, + details.consume(scene, amount), + recycled.timeSensitiveData.consume(amount), + createAt, + now)); + } + return ConsumeResult.error(InventoryItem._( + sku, remainBalance, category, attr, details, recycled.timeSensitiveData, createAt, now)); + } + + factory InventoryItem.fromJson(Map json) => _$InventoryItemFromJson(json); + + Map toJson() => _$InventoryItemToJson(this); + + @override + String get id => sku; +} + +extension InventoryDatabase on GuruDB { + Future> loadInventoryItems() async { + final db = getDb(); + final result = await db.rawQuery("SELECT * FROM ${InventoryTable.tbName}"); + final batchData = BatchData.empty(); + if (result.isNotEmpty) { + batchData.queryAll([for (var item in result) InventoryItem.fromJson(item)]); + } + return batchData; + } + + Future> updateInventoryItem(InventoryItem item) async { + final db = getDb(); + await db.insert(InventoryTable.tbName, item.toJson(), + conflictAlgorithm: ConflictAlgorithm.replace); + return BatchData.singleSuccess(BatchMethod.update, item); + } + + Future> updateInventoryItems(List items) async { + final BatchData batchData = BatchData.empty(); + return runInTransaction((txn) async { + for (var item in items) { + await txn.insert(InventoryTable.tbName, item.toJson(), + conflictAlgorithm: ConflictAlgorithm.replace); + batchData.update(item); + } + return batchData; + }); + } +} diff --git a/guru_app/lib/inventory/db/inventory_database.g.dart b/guru_app/lib/inventory/db/inventory_database.g.dart new file mode 100644 index 0000000..1a6334c --- /dev/null +++ b/guru_app/lib/inventory/db/inventory_database.g.dart @@ -0,0 +1,96 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'inventory_database.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LimitedBalance _$LimitedBalanceFromJson(Map json) => + LimitedBalance( + json['a'] as int? ?? 0, + json['e'] as int? ?? -1, + recycle: json['r'] as bool? ?? false, + ); + +Map _$LimitedBalanceToJson(LimitedBalance instance) => + { + 'a': instance.amount, + 'e': instance.expireAt, + 'r': instance.recycle, + }; + +TimeSensitiveData _$TimeSensitiveDataFromJson(Map json) => + TimeSensitiveData( + valid: (json['v'] as List?) + ?.map((e) => LimitedBalance.fromJson(e as Map)) + .toList() ?? + [], + expired: (json['e'] as List?) + ?.map((e) => LimitedBalance.fromJson(e as Map)) + .toList() ?? + [], + ); + +Map _$TimeSensitiveDataToJson(TimeSensitiveData instance) => + { + 'v': instance.valid, + 'e': instance.expired, + }; + +InventoryDetails _$InventoryDetailsFromJson(Map json) => + InventoryDetails._( + (json['a'] as Map?)?.map( + (k, e) => + MapEntry($enumDecode(_$TransactionMethodEnumMap, k), e as int), + ) ?? + {}, + (json['c'] as Map?)?.map( + (k, e) => MapEntry(k, e as int), + ) ?? + {}, + json['d'] as Map, + ); + +Map _$InventoryDetailsToJson(InventoryDetails instance) => + { + 'a': instance.acquired + .map((k, e) => MapEntry(_$TransactionMethodEnumMap[k]!, e)), + 'c': instance.consumed, + 'd': instance.data, + }; + +const _$TransactionMethodEnumMap = { + TransactionMethod.iap: 'iap', + TransactionMethod.igc: 'igc', + TransactionMethod.reward: 'reward', + TransactionMethod.bonus: 'bonus', + TransactionMethod.igb: 'igb', + TransactionMethod.free: 'free', + TransactionMethod.migrate: 'migrate', + TransactionMethod.unknown: 'unknown', +}; + +InventoryItem _$InventoryItemFromJson(Map json) => + InventoryItem._( + json['sku'] as String, + json['balance'] as int, + json['cat'] as String, + json['attr'] as int, + inventoryDetailsStringConvert.fromJson(json['details'] as String), + timeSensitiveDataStringConvert.fromJson(json['tsv'] as String), + json['create_at'] as int, + json['update_at'] as int, + ); + +Map _$InventoryItemToJson(InventoryItem instance) => + { + 'sku': instance.sku, + 'balance': instance.balance, + 'cat': instance.category, + 'attr': instance.attr, + 'tsv': timeSensitiveDataStringConvert.toJson(instance.timeSensitive), + 'update_at': instance.updateAt, + 'create_at': instance.createAt, + 'details': inventoryDetailsStringConvert.toJson(instance.details), + }; diff --git a/guru_app/lib/inventory/inventory_manager.dart b/guru_app/lib/inventory/inventory_manager.dart new file mode 100644 index 0000000..9312483 --- /dev/null +++ b/guru_app/lib/inventory/inventory_manager.dart @@ -0,0 +1,193 @@ +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/database/guru_db.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/inventory/db/inventory_database.dart'; +import 'package:guru_utils/database/batch/batch_aware.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/manifest/manifest.dart'; + +class InventoryCategory { + static const String prop = "prop"; // 道具类 +} + +class PropCategory { + // 增益类:提供临时或永久的能力提升,如速度增加、力量提升、生命值恢复等。 + static const String boosts = 'Boosts'; + + // 减益类:施加在对手或玩家身上,造成能力降低、速度减慢、伤害增加等负面效果。 + static const String debuffs = 'Debuffs'; + + // 治疗类:恢复生命值或状态,解除负面效果。 + static const String healing = 'Healing'; + + // 防御类:提供防护,减少受到的伤害或完全避免某些类型的伤害。 + static const String defensive = 'Defensive'; + + // 攻击类:用于对敌人造成伤害或施加减益效果,如武器、陷阱、魔法等。 + static const String offensive = 'Offensive'; + + // 辅助类:提供团队增益、增强队友能力、提供战术优势等非直接攻击手段的道具。 + static const String supportive = 'Supportive'; + + // 探索类:用于揭示地图、发现隐藏物品、解锁新区域或提供关键信息的道具。 + static const String exploratory = 'Exploratory'; + + // 交互类:与游戏世界中的其他元素或玩家进行交互的道具,如钥匙、开关、通信设备等。 + static const String interactive = 'Interactive'; + + // 资源类:用于制造、升级或交易的材料和货币类型道具。 + static const String resources = 'Resources'; + + // 限定类:只能使用一定次数的道具,使用后消失或需要充能。 + static const String limitedUse = 'LimitedUse'; +} + +mixin InventoryDelegate { + // 返回指定id的库存类别 + // 如 hint,zoom这些道具统一返回 + String getInventoryCategory(String id) { + return InventoryCategory.prop; + } + + Future> getMigrateStockItems() async { + return []; + } +} + +class StockItem { + final String sku; + final int amount; + final int attr; + final DateTime? expired; + + const StockItem.consumable(this.sku, this.amount, {this.expired}) : attr = DetailsAttr.consumable; + + const StockItem.permanent(this.sku, this.amount, {this.expired}) : attr = DetailsAttr.permanent; + + const StockItem.igc(this.amount, {this.sku = "igc"}) + : attr = DetailsAttr.consumable, + expired = null; + + StockItem.fromDetails(Details details) + : sku = details.sku, + attr = details.attr, + amount = details.amount, + expired = null; +} + +class InventoryManager with BatchAware { + static final InventoryManager instance = InventoryManager._(); + + InventoryManager._(); + + String getInventoryCategory(String id) { + return GuruApp.instance.protocol.inventoryDelegate?.getInventoryCategory(id) ?? + InventoryCategory.prop; + } + + Future init() async { + final batch = await GuruDB.instance.loadInventoryItems(); + processBatchData(batch); + await _migrate(); + } + + Future _migrate() async { + final migrateStockItems = + await GuruApp.instance.protocol.inventoryDelegate?.getMigrateStockItems() ?? []; + + if (migrateStockItems.isNotEmpty) { + final needMigrateItems = migrateStockItems.where((item) => !exists(item.sku)).toList(); + await acquire(needMigrateItems, TransactionMethod.migrate, "migrate"); + } + } + + /// + /// 通过[method]中的的特定[specific]方式 获得了指定的 [items] + /// * method: iap -> specific: sku + /// * method: igc -> specific: coin/gems... + /// * method: reward -> specific: ads/lottery/daily/... + /// * method: bonus -> specific: ads/other/... + /// * method: igb -> specific: hint/hammer/swap/magic/.. + /// + /// method 最终会在 earnVirtualCurrency 中成为 item_category + /// specific 最终会在 earnVirtualCurrency 中成为 item_name + /// + Future acquire(List items, TransactionMethod method, String specific, + {String? scene}) async { + final acquired = []; + for (var item in items) { + final category = getInventoryCategory(item.sku); + final invItem = getData(item.sku) + ?.acquire(method, item.amount, expiredAt: item.expired?.millisecondsSinceEpoch) ?? + InventoryItem.create(item.sku, category, item.attr, + balance: item.amount, + method: method, + expireAt: item.expired?.millisecondsSinceEpoch ?? -1); + acquired.add(invItem); + + GuruAnalytics.instance.logEarnVirtualCurrency( + virtualCurrencyName: item.sku, + method: convertTransactionMethodName(method), + specific: specific, + balance: invItem.balance, + value: item.amount, + scene: scene); + } + final batchData = await GuruDB.instance.updateInventoryItems(acquired); + processBatchData(batchData); + } + + ConsumeResult? _consumeItem(StockItem item, Manifest redeemed, {bool timeSensitiveOnly = false}) { + final inventoryItem = getData(item.sku); + return timeSensitiveOnly + ? inventoryItem?.consumeTimeSensitiveOnly(item.amount, + scene: redeemed.scene, transactionTs: redeemed.transactionTs) + : inventoryItem?.consume(item.amount, scene: redeemed.scene); + } + + /// 消耗指定的道具[items],如果可以成功消费,便可得到相应的[redeemed]清单 + Future consume(List items, Manifest redeemed, + {bool timeSensitiveOnly = false}) async { + final consumed = []; + bool isConsumed = true; + for (var item in items) { + /// 这里只是针对对应的 SKU 商品进行 consume标记,真正的消耗在下面的updateInventoryItems中 + final result = _consumeItem(item, redeemed, timeSensitiveOnly: timeSensitiveOnly); + if (result == null) { + Log.e("consume failed! Not Found inventory item: ${item.sku}"); + return false; + } + consumed.add(result.item); + // 这里如果没有消耗掉,将跳出 + if (!result.consumed) { + Log.w("consume failed: ${item.sku} ${item.amount}"); + isConsumed = false; + break; + } + } + if (consumed.isEmpty) { + return false; + } else { + if (isConsumed && consumed.length == items.length) { + for (int i = 0; i < items.length; ++i) { + GuruAnalytics.instance.logSpendCredits( + redeemed.contentId, redeemed.category, items[i].amount, + virtualCurrencyName: consumed[i].sku, + balance: consumed[i].balance, + scene: redeemed.scene); + } + } + } + + /// 这里会更新消耗掉的道具 + final batchData = await GuruDB.instance.updateInventoryItems(consumed); + processBatchData(batchData); + return isConsumed; + } + + bool canAfford(String id, int amount) { + final item = getData(id); + return item != null && item.balance >= amount; + } +} diff --git a/guru_app/lib/property/app_property.dart b/guru_app/lib/property/app_property.dart index e875fd6..aeaac0c 100644 --- a/guru_app/lib/property/app_property.dart +++ b/guru_app/lib/property/app_property.dart @@ -3,10 +3,13 @@ import 'dart:convert'; import 'package:guru_app/account/model/account.dart'; import 'package:guru_app/account/model/account_profile.dart'; +import 'package:guru_app/account/model/credential.dart'; import 'package:guru_app/account/model/user.dart'; +import 'package:guru_app/analytics/abtest/abtest_model.dart'; import 'package:guru_app/api/data/orders/orders_model.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; import 'package:guru_utils/property/property_model.dart'; import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:guru_utils/device/device_info.dart'; diff --git a/guru_app/lib/property/modules/account_property_extension.dart b/guru_app/lib/property/modules/account_property_extension.dart index 9bf0e81..4add96d 100644 --- a/guru_app/lib/property/modules/account_property_extension.dart +++ b/guru_app/lib/property/modules/account_property_extension.dart @@ -2,25 +2,25 @@ part of "../app_property.dart"; extension AccountPropertyExtension on AppProperty { - void setAccountSaasUser(SaasUser saasUser) { - final data = jsonEncode(saasUser); - setString(PropertyKeys.accountSaasUser, data); + Future setAccountGuruUser(GuruUser guruUser) async { + final data = jsonEncode(guruUser); + await setString(PropertyKeys.accountGuruUser, data); } - void setAccountDevice(DeviceInfo deviceInfo) { + Future setAccountDevice(DeviceInfo deviceInfo) async { final data = jsonEncode(deviceInfo); - setString(PropertyKeys.accountDevice, data); + await setString(PropertyKeys.accountDevice, data); } - void setAccountProfile(AccountProfile accountProfile) { + Future setAccountProfile(AccountProfile accountProfile) async { final data = jsonEncode(accountProfile); - setString(PropertyKeys.accountProfile, data); + await setString(PropertyKeys.accountProfile, data); } // refer updateLocalProfile - void setDirtyAccountProfile(AccountProfile accountProfile) { + Future setDirtyAccountProfile(AccountProfile accountProfile) async { final data = jsonEncode(accountProfile.copyWith(dirty: true)); - setString(PropertyKeys.accountProfile, data); + await setString(PropertyKeys.accountProfile, data); } Future getAccountDevice() async { @@ -37,14 +37,14 @@ extension AccountPropertyExtension on AppProperty { Log.v("loadValuesByTag is empty, $error"); return PropertyBundle.empty(); }); - SaasUser? saasUser; + GuruUser? guruUser; DeviceInfo? device; AccountProfile? accountProfile; - final saasUserString = accountBundle.getString(PropertyKeys.accountSaasUser); + final saasUserString = accountBundle.getString(PropertyKeys.accountGuruUser); if (DartExt.isNotBlank(saasUserString)) { final map = jsonDecode(saasUserString!); - saasUser = SaasUser.fromJson(map); + guruUser = GuruUser.fromJson(map); } final accountDeviceString = accountBundle.getString(PropertyKeys.accountDevice); @@ -59,7 +59,28 @@ extension AccountPropertyExtension on AppProperty { accountProfile = AccountProfile.fromJson(map); } - return Account.restore(saasUser: saasUser, device: device, accountProfile: accountProfile); + final Map credentials = {}; + for (final authType in AuthCredentialManager.instance.supportedAuthType) { + final credentialString = + accountBundle.getString(PropertyKeys.buildAuthCredentialKey(authType)); + if (DartExt.isNotBlank(credentialString)) { + final credential = + AuthCredentialManager.instance.deserializeCredential(authType, credentialString!); + if (credential != null) { + credentials[authType] = credential; + } + } + } + final anonymousSecretKey = accountBundle.getString(PropertyKeys.anonymousSecretKey); + if (DartExt.isNotBlank(anonymousSecretKey)) { + credentials[AuthType.anonymous] = AnonymousCredential(anonymousSecretKey!); + } + + return Account.restore( + guruUser: guruUser, + device: device, + accountProfile: accountProfile, + credentials: credentials); } Future getLatestReportDeviceTimestamp() async { @@ -78,4 +99,34 @@ extension AccountPropertyExtension on AppProperty { } return secret; } + + Future clearAnonymousSecretKey() async { + await remove(PropertyKeys.anonymousSecretKey); + } + + Future saveCredential(Credential credential) async { + final data = jsonEncode(credential); + await setString(PropertyKeys.buildAuthCredentialKey(credential.authType), data); + final historicalSocialAuths = await getHistoricalSocialAuths(); + final authName = getAuthName(credential.authType); + if (!historicalSocialAuths.contains(authName)) { + historicalSocialAuths.add(authName); + setHistoricalSocialAuths(historicalSocialAuths); + } + } + + Future deleteCredential(AuthType authType) async { + await remove(PropertyKeys.buildAuthCredentialKey(authType)); + } + + Future> getHistoricalSocialAuths() async { + final data = await getString(PropertyKeys.historicalSocialAuths, defValue: ""); + return data.isNotEmpty ? (data.split("|").toSet()..remove("")) : {}; + } + + Future setHistoricalSocialAuths(Set historicalSocialAuths) async { + historicalSocialAuths.remove(""); + await setString(PropertyKeys.historicalSocialAuths, historicalSocialAuths.join("|")); + return true; + } } diff --git a/guru_app/lib/property/modules/analytics_property_extension.dart b/guru_app/lib/property/modules/analytics_property_extension.dart index 41b2ac1..9aa8647 100644 --- a/guru_app/lib/property/modules/analytics_property_extension.dart +++ b/guru_app/lib/property/modules/analytics_property_extension.dart @@ -38,4 +38,73 @@ extension AnalyticsPropertyExtension on AppProperty { await setString(PropertyKeys.analyticsIdfa, idfa); } } + + Future> loadRunningExperiments() async { + final bundle = await AppProperty.getInstance().loadValuesByTag(PropertyTags.guruExperiment); + final result = {}; + bundle.forEach((key, value) { + try { + if (value.isNotEmpty) { + final map = json.decode(value); + final experiment = ABTestExperiment.fromJson(map); + result[key.name] = experiment; + Log.d("loadRunningExperiments: ${key.name} => $experiment"); + } + } catch (error, stacktrace) { + Log.w("getExperiment error! $error"); + } + }); + return result; + } + + Future getExperiment(String experimentName, {PropertyBundle? bundle}) async { + final experimentKey = PropertyKeys.buildExperimentProperty(experimentName); + final result = bundle?.getString(experimentKey) ?? await getString(experimentKey, defValue: ""); + try { + if (result.isNotEmpty) { + final map = json.decode(result); + return ABTestExperiment.fromJson(map); + } + } catch (error, stacktrace) { + Log.w("getExperiment error! $error"); + } + return null; + } + + Future getExperimentVariant(String experimentName) async { + final variantKey = + PropertyKeys.buildABTestProperty(GuruAnalytics.buildVariantKey(experimentName)); + return await getString(variantKey, defValue: ""); + } + + Future setExperiment(ABTestExperiment experiment) async { + PropertyBundle propertyBundle = PropertyBundle(); + + final variantKey = + PropertyKeys.buildABTestProperty(GuruAnalytics.buildVariantKey(experiment.name)); + final variantName = experiment.variantName; + propertyBundle.setString(variantKey, variantName); + + final experimentKey = PropertyKeys.buildExperimentProperty(experiment.name); + propertyBundle.setString(experimentKey, json.encode(experiment)); + await setProperties(propertyBundle); + return variantName; + } + + removeExperiment(String experimentName) async { + final experimentKey = PropertyKeys.buildExperimentProperty(experimentName); + await remove(experimentKey); + final variantKey = PropertyKeys.buildABTestProperty(experimentName); + await remove(variantKey); + } + + Future refreshGoogleDma(String googleDma) async { + final oldGoogleDma = + await AppProperty.getInstance().getString(PropertyKeys.googleDma, defValue: ""); + if (googleDma != oldGoogleDma) { + await AppProperty.getInstance().setString(PropertyKeys.googleDma, googleDma); + return true; + } + return false; + } } diff --git a/guru_app/lib/property/modules/default_property_extension.dart b/guru_app/lib/property/modules/default_property_extension.dart index ca306f0..54f76be 100644 --- a/guru_app/lib/property/modules/default_property_extension.dart +++ b/guru_app/lib/property/modules/default_property_extension.dart @@ -3,8 +3,8 @@ part of "../app_property.dart"; /// Created by @Haoyi on 5/14/21 extension DefaultPropertyExtension on AppProperty { - Future getDeviceId() async { - return getOrCreateString(PropertyKeys.deviceId, IdUtils.uuidV4()); + Future getDeviceId({String? forceDeviceId}) async { + return getOrCreateString(PropertyKeys.deviceId, forceDeviceId ?? IdUtils.uuidV4()); } Future getFirstInstallTime() async { diff --git a/guru_app/lib/property/modules/iap_property_extension.dart b/guru_app/lib/property/modules/iap_property_extension.dart index 54c6322..238d433 100644 --- a/guru_app/lib/property/modules/iap_property_extension.dart +++ b/guru_app/lib/property/modules/iap_property_extension.dart @@ -18,4 +18,14 @@ extension IapPropertyExtension on AppProperty { Future removeReportSuccessOrder(PropertyKey key) async { remove(key); } + + Future increaseGraceCount() async { + final count = await getInt(PropertyKeys.subscriptionGraceCount, defValue: 0); + await setInt(PropertyKeys.subscriptionGraceCount, count + 1); + return count + 1; + } + + Future resetGraceCount() async { + await setInt(PropertyKeys.subscriptionGraceCount, 0); + } } diff --git a/guru_app/lib/property/property_keys.dart b/guru_app/lib/property/property_keys.dart index 79bf542..8b17556 100644 --- a/guru_app/lib/property/property_keys.dart +++ b/guru_app/lib/property/property_keys.dart @@ -1,5 +1,6 @@ import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; import 'package:guru_utils/id/id_utils.dart'; import 'package:guru_utils/property/property_model.dart'; import 'package:guru_utils/settings/settings.dart'; @@ -21,9 +22,12 @@ class PropertyKeys { static const PropertyKey debugMode = UtilsSettingsKeys.debugMode; static const PropertyKey latestLtDate = UtilsSettingsKeys.latestLtDate; static const PropertyKey ltDays = UtilsSettingsKeys.ltDays; + static const PropertyKey keepOnScreenDuration = UtilsSettingsKeys.keepOnScreenDuration; - static const PropertyKey accountSaasUser = + static const PropertyKey accountGuruUser = PropertyKey.general("account_saas_user", tag: PropertyTags.account); + @Deprecated("use accountGuruUser instead") + static const PropertyKey accountSaasUser = accountGuruUser; static const PropertyKey accountDevice = PropertyKey.general("account_device", tag: PropertyTags.account); static const PropertyKey accountProfile = @@ -33,11 +37,18 @@ class PropertyKeys { static const PropertyKey anonymousSecretKey = PropertyKey.general("anonymous_secret_key", tag: PropertyTags.account); + static const PropertyKey historicalSocialAuths = + PropertyKey.general("historical_social_auths", tag: PropertyTags.account); + static const PropertyKey isNoAds = PropertyKey.setting("no_ads", tag: PropertyTags.ads); static const PropertyKey totalRevenue = PropertyKey.general("total_revenue", tag: PropertyTags.financial); + /// 020的 revenue + static const PropertyKey totalRevenue020 = + PropertyKey.general("total_revenue_020", tag: PropertyTags.financial); + static const PropertyKey userRewardedCount = PropertyKey.general("user_rewarded_count", tag: PropertyTags.ads); @@ -49,6 +60,8 @@ class PropertyKeys { static const PropertyKey iapIgc = PropertyKey.general("iap_igc", tag: PropertyTags.iap); static const PropertyKey noIapIgc = PropertyKey.general("no_iap_igc", tag: PropertyTags.iap); + static const PropertyKey subscriptionGraceCount = + PropertyKey.general("subscription_grace_count", tag: PropertyTags.iap); static const PropertyKey admobConsentTestDeviceId = PropertyKey.general("admob_consent_test_device_id", tag: PropertyTags.ads); static const PropertyKey admobConsentDebugGeography = @@ -97,6 +110,9 @@ class PropertyKeys { static const PropertyKey analyticsIdfa = PropertyKey.general("analytics_idfa", tag: PropertyTags.analytics); + static const PropertyKey googleDma = + PropertyKey.general("google_dma_result", tag: PropertyTags.analytics); + static const PropertyKey currentIgcBalance = PropertyKey.general("current_balance", tag: PropertyTags.igc); static const PropertyKey currentIgcBalanceValidation = @@ -106,6 +122,10 @@ class PropertyKeys { return PropertyKey.general("abtest_$key", tag: PropertyTags.guruAB); } + static PropertyKey buildExperimentProperty(String key) { + return PropertyKey.general("exp_$key", tag: PropertyTags.guruExperiment); + } + static PropertyKey requestNotificationPermissionTimes = const PropertyKey.general("request_notification_permission_times"); @@ -114,4 +134,9 @@ class PropertyKeys { static PropertyKey deniedNotificationPermissionTimes = const PropertyKey.general("denied_notification_permission_times"); + + static PropertyKey buildAuthCredentialKey(AuthType authType) { + return PropertyKey.general("${getAuthName(authType)}_auth_credential", + tag: PropertyTags.account); + } } diff --git a/guru_app/lib/property/property_tags.dart b/guru_app/lib/property/property_tags.dart index ca125c4..500a0ef 100644 --- a/guru_app/lib/property/property_tags.dart +++ b/guru_app/lib/property/property_tags.dart @@ -11,6 +11,7 @@ class PropertyTags { static const String failedOrders = "failed_orders"; static const String strategyAds = "StrategyAds"; static const String guruAB = "GuruAB"; + static const String guruExperiment = "guru_experiment"; static const String iap = UtilsPropertyTags.iap; static const String ads = UtilsPropertyTags.ads; diff --git a/guru_app/lib/property/settings/global_settings.dart b/guru_app/lib/property/settings/global_settings.dart index e1a5be2..9e45cc3 100644 --- a/guru_app/lib/property/settings/global_settings.dart +++ b/guru_app/lib/property/settings/global_settings.dart @@ -6,4 +6,4 @@ mixin GlobalSettings { final SettingBoolData isNoAds = SettingBoolData(PropertyKeys.isNoAds, defaultValue: false); final SettingIntData bestLevel = SettingIntData(PropertyKeys.bestLevel, defaultValue: 1); -} +} \ No newline at end of file diff --git a/guru_app/lib/test/test_guru_app_creator.dart b/guru_app/lib/test/test_guru_app_creator.dart index f7f1cb5..3c1ae7a 100644 --- a/guru_app/lib/test/test_guru_app_creator.dart +++ b/guru_app/lib/test/test_guru_app_creator.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:guru_app/analytics/abtest/abtest_model.dart'; import 'package:guru_app/financial/data/db/order_database.dart'; import 'package:guru_app/financial/manifest/manifest.dart'; import 'package:guru_app/guru_app.dart'; @@ -11,6 +12,4 @@ part 'test_guru_app_creator.g.dart'; @guruSpecCreator AppSpec createSampleAppSpec(String flavor) { return _GuruSpecFactory.create(flavor); -} - - +} \ No newline at end of file diff --git a/guru_app/lib/test/test_guru_app_creator.g.dart b/guru_app/lib/test/test_guru_app_creator.g.dart index b114e79..87ada73 100644 --- a/guru_app/lib/test/test_guru_app_creator.g.dart +++ b/guru_app/lib/test/test_guru_app_creator.g.dart @@ -24,6 +24,8 @@ class _Guru_testRemoteConfigConstants { }; static String getDefaultConfigString(String key) => _defaultConfigs[key]; + + static String getKey(String key) => key; } class _Guru_testAppSpec extends AppSpec { @@ -34,6 +36,9 @@ class _Guru_testAppSpec extends AppSpec { @override final appName = 'GuruApp'; + @override + final appCategory = AppCategory.app; + @override final flavor = 'guru_test'; @@ -95,6 +100,9 @@ class _Guru_testAppSpec extends AppSpec { trackingNotificationPermissionPassLimitTimes: 10, allowInterstitialAsAlternativeReward: false, showInternalAdsWhenBannerUnavailable: true, + subscriptionRestoreGraceCount: 3, + fullscreenAdsMinInterval: 60, + enabledSyncAccountProfile: false, ); @override @@ -158,6 +166,13 @@ class _Guru_testAppSpec extends AppSpec { @override final defaultRemoteConfig = _Guru_testRemoteConfigConstants._defaultConfigs; + + @override + final localABTestExperiments = _GuruTestABTestExperiments.experiments; + + @override + String getRemoteConfigKey(String key) => + _Guru_testRemoteConfigConstants.getKey(key); } class _Guru_testProducts { @@ -165,8 +180,6 @@ class _Guru_testProducts { static final propRegExp = RegExp(r"^theme_(.*)_(.*)$"); - static final themeMulRegExp = RegExp(r"^theme_(.*)_(.*)$"); - static const noAds = ProductId( android: 'so.a.iap.noads.699', ios: 'so.i.iap.noads.699', @@ -314,8 +327,7 @@ class _Guru_testProducts { buildCoin200Manifest, buildStagePackManifest, buildPremiumWeekManifest, - buildPremiumYearManifest, - buildThemeMulManifest + buildPremiumYearManifest ]; static Future buildNoAdsManifest(TransactionIntent intent) async { @@ -323,6 +335,7 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, @@ -344,17 +357,21 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, }; final details =
[]; details.add(Details.define( - 'igc', intent.sales ? max(500, (intent.rate * 500).toInt()) : 500)); + 'igc', intent.sales ? max(500, (intent.rate * 500).toInt()) : 500, + sku: 'igc')); details.add(Details.define( - 'cup', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)); + 'cup', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1, + sku: 'cup')); details.add(Details.define( - 'frag', intent.sales ? max(20, (intent.rate * 20).toInt()) : 20)); + 'frag', intent.sales ? max(20, (intent.rate * 20).toInt()) : 20, + sku: 'frag')); return Manifest('no_ads', extras: extras, details: details); } @@ -379,6 +396,7 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, @@ -413,17 +431,20 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, }; final details =
[]; details.add(Details.define( - 'prop', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1) - ..setString('prop_id', matches.first.group(1)!)); + 'prop', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1, + sku: '${matches.first.group(1)!}_${matches.first.group(2)!}') + ..setString('theme_id', matches.first.group(1)!)); details.add(Details.define( - 'pc', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1) - ..setString('prop_id', matches.first.group(2)!)); + 'pc', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1, + sku: 'pc') + ..setString('theme_id', matches.first.group(2)!)); return Manifest('prop', extras: extras, details: details); } @@ -444,12 +465,13 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, }; final details =
[]; - details.add(Details.define('no_ads', 1)); + details.add(Details.define('no_ads', 1, sku: 'no_ads')); return Manifest('no_ads', extras: extras, details: details); } @@ -467,13 +489,15 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, }; final details =
[]; details.add(Details.define( - 'coin', intent.sales ? max(200, (intent.rate * 200).toInt()) : 200)); + 'coin', intent.sales ? max(200, (intent.rate * 200).toInt()) : 200, + sku: 'coin')); return Manifest('coin', extras: extras, details: details); } @@ -483,13 +507,15 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, }; final details =
[]; details.add(Details.define( - 'stage', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1) + 'stage', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1, + sku: 'stage') ..setInt('stage', 1)); return Manifest('stage_1', extras: extras, details: details); } @@ -500,6 +526,7 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, @@ -510,7 +537,8 @@ class _Guru_testProducts { } final details =
[]; details.add(Details.define( - 'igc', intent.sales ? max(8000, (intent.rate * 8000).toInt()) : 8000)); + 'igc', intent.sales ? max(8000, (intent.rate * 8000).toInt()) : 8000, + sku: 'igc')); return Manifest('sub', extras: extras, details: details); } @@ -520,6 +548,7 @@ class _Guru_testProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, @@ -529,51 +558,54 @@ class _Guru_testProducts { extras[ExtraReservedField.offerId] = intent.productId.offerId; } final details =
[]; - details.add(Details.define('igc', - intent.sales ? max(16000, (intent.rate * 16000).toInt()) : 16000)); + details.add(Details.define( + 'igc', intent.sales ? max(16000, (intent.rate * 16000).toInt()) : 16000, + sku: 'igc')); return Manifest('sub', extras: extras, details: details); } - static ProductId themeMul( - String category, - String themeId, - ) => - GuruApp.instance.defineProductId('theme_${category}_${themeId}', - TransactionAttributes.possessive, TransactionMethod.igc); - - static Future buildThemeMulManifest( - TransactionIntent intent) async { - final matches = themeMulRegExp.allMatches(intent.productId.sku); - if (matches.isEmpty) { - return null; - } - final extras = { - ExtraReservedField.scene: intent.scene, - ExtraReservedField.rate: intent.rate, - ExtraReservedField.sales: intent.sales, - 'theme_id': matches.first.group(2)!, - 'cate': matches.first.group(1)!, - }; - return Manifest('${matches.first.group(1)!}', extras: extras); - } - - static bool isOwnThemeMul( - OrderEntity entity, - String category, - String themeId, - ) { - if (entity.state == TransactionState.success && - entity.category == 'theme_mul') { - final match = themeMulRegExp.firstMatch(entity.sku); - return match?.group(1) == category && match?.group(2) == themeId; - } - return false; - } - static Set get iapIds => {...oneOffChargeIapIds, ...subscriptionsIapIds}; } +class _GuruTestABTestExperiments { + static final test = ABTestExperiment( + name: 'test', + startTs: 1706457600000, // 2024-01-29 00:00:00.000 + endTs: 1706457600000, // 2024-01-29 00:00:00.000 + audience: ABTestAudience(filters: [ + VersionFilter.lessThan('2.3.0'), + CountryFilter.excluded({'us', 'cn', 'en'}), + PlatformFilter( + androidCondition: AndroidCondition( + opt: ConditionOpt.greaterThanOrEquals, sdkInt: 33), + iosCondition: + IosCondition(opt: ConditionOpt.greaterThanOrEquals, version: 14), + ), + ], variant: 2)); + + static final test2 = ABTestExperiment( + name: 'test2', + startTs: 1706457600000, // 2024-01-29 00:00:00.000 + endTs: 1706457600000, // 2024-01-29 00:00:00.000 + audience: ABTestAudience(filters: [ + VersionFilter.lessThan('2.3.0'), + CountryFilter.included({'cn'}), + PlatformFilter( + androidCondition: + AndroidCondition(opt: ConditionOpt.lessThan, sdkInt: 24), + iosCondition: + IosCondition(opt: ConditionOpt.greaterThanOrEquals, version: 14), + ), + NewUserFilter(), + ], variant: 5)); + + static final experiments = { + 'test': test, + 'test2': test2, + }; +} + class _SpiderRemoteConfigConstants { static const iadsConfig = 'iads_config'; @@ -589,6 +621,8 @@ class _SpiderRemoteConfigConstants { }; static String getDefaultConfigString(String key) => _defaultConfigs[key]; + + static String getKey(String key) => key; } class _SpiderAppSpec extends AppSpec { @@ -599,6 +633,9 @@ class _SpiderAppSpec extends AppSpec { @override final appName = 'Spider'; + @override + final appCategory = AppCategory.game; + @override final flavor = 'Spider'; @@ -680,6 +717,13 @@ class _SpiderAppSpec extends AppSpec { @override final defaultRemoteConfig = _SpiderRemoteConfigConstants._defaultConfigs; + + @override + final localABTestExperiments = _SpiderABTestExperiments.experiments; + + @override + String getRemoteConfigKey(String key) => + _SpiderRemoteConfigConstants.getKey(key); } class _SpiderProducts { @@ -726,6 +770,7 @@ class _SpiderProducts { return null; } final extras = { + ExtraReservedField.contentId: intent.productId.sku, ExtraReservedField.scene: intent.scene, ExtraReservedField.rate: intent.rate, ExtraReservedField.sales: intent.sales, @@ -733,7 +778,8 @@ class _SpiderProducts { }; final details =
[]; details.add(Details.define( - 'theme', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1)); + 'theme', intent.sales ? max(1, (intent.rate * 1).toInt()) : 1, + sku: 'theme')); return Manifest('${matches.first.group(1)!}', extras: extras, details: details); } @@ -754,6 +800,10 @@ class _SpiderProducts { {...oneOffChargeIapIds, ...subscriptionsIapIds}; } +class _SpiderABTestExperiments { + static final experiments = {}; +} + class RemoteConfigConstants { static const iadsConfig = 'iads_config'; @@ -764,6 +814,18 @@ class RemoteConfigConstants { static const analyticsConfig = 'analytics_config'; } +class ABTestExperimentConstants { + static Map get experiments { + if (GuruApp.instance.flavor == 'guru_test') { + return _GuruTestABTestExperiments.experiments; + } + if (GuruApp.instance.flavor == 'Spider') { + return _SpiderABTestExperiments.experiments; + } + return {}; + } +} + class ProductIds { static ProductId get noAds { if (GuruApp.instance.flavor == 'guru_test') { @@ -872,16 +934,6 @@ class ProductIds { return ProductId.invalid; } - static ProductId themeMul( - String category, - String themeId, - ) { - if (GuruApp.instance.flavor == 'guru_test') { - return _Guru_testProducts.themeMul(category, themeId); - } - return ProductId.invalid; - } - static Set get noAdsCapIds { if (GuruApp.instance.flavor == 'guru_test') { return _Guru_testProducts.noAdsCapIds; @@ -921,10 +973,6 @@ class ProductCategory { static theme(String themeId) { "theme_${themeId}"; } - - static themeMul(String category) { - "${category}"; - } } class _GuruSpecFactory { diff --git a/guru_app/packages/firebase/guru_fiam/.gitignore b/guru_app/packages/firebase/guru_fiam/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/guru_app/packages/firebase/guru_fiam/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/guru_app/packages/firebase/guru_fiam/CHANGELOG.md b/guru_app/packages/firebase/guru_fiam/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/packages/firebase/guru_fiam/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/packages/firebase/guru_fiam/LICENSE b/guru_app/packages/firebase/guru_fiam/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/packages/firebase/guru_fiam/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/packages/firebase/guru_fiam/README.md b/guru_app/packages/firebase/guru_fiam/README.md new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/guru_app/packages/firebase/guru_fiam/README.md @@ -0,0 +1,39 @@ + + +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. diff --git a/guru_app/packages/firebase/guru_fiam/analysis_options.yaml b/guru_app/packages/firebase/guru_fiam/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/packages/firebase/guru_fiam/analysis_options.yaml @@ -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 diff --git a/guru_app/packages/firebase/guru_fiam/lib/guru_fiam.dart b/guru_app/packages/firebase/guru_fiam/lib/guru_fiam.dart new file mode 100644 index 0000000..c0847fd --- /dev/null +++ b/guru_app/packages/firebase/guru_fiam/lib/guru_fiam.dart @@ -0,0 +1,7 @@ +library guru_fiam; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/guru_app/packages/firebase/guru_fiam/pubspec.yaml b/guru_app/packages/firebase/guru_fiam/pubspec.yaml new file mode 100644 index 0000000..b26ad1f --- /dev/null +++ b/guru_app/packages/firebase/guru_fiam/pubspec.yaml @@ -0,0 +1,54 @@ +name: guru_fiam +description: "A new Flutter project." +version: 3.0.0 +homepage: + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + firebase_in_app_messaging: 0.7.4+8 +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/packages/firebase/guru_fiam/test/guru_fiam_test.dart b/guru_app/packages/firebase/guru_fiam/test/guru_fiam_test.dart new file mode 100644 index 0000000..27a4acb --- /dev/null +++ b/guru_app/packages/firebase/guru_fiam/test/guru_fiam_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:guru_fiam/guru_fiam.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/guru_app/packages/guru_assistant/example/lib/data/initializer.dart b/guru_app/packages/guru_assistant/example/lib/data/initializer.dart index ce28b20..68db64c 100644 --- a/guru_app/packages/guru_assistant/example/lib/data/initializer.dart +++ b/guru_app/packages/guru_assistant/example/lib/data/initializer.dart @@ -27,7 +27,7 @@ class _RootPackage extends RootPackage { Log.isDebug = kDebugMode; await DebugSettings.instance.refresh(); - + Initializer.initialPath = AppPages.initialPath; RouteCenter.initialize(routeMatchers: [ @@ -42,23 +42,34 @@ class _RootPackage extends RootPackage { ]); } - @override // TODO: implement localizationsDelegates Iterable get localizationsDelegates => const [ - // Built-in localization of basic text for Material widgets - GlobalMaterialLocalizations.delegate, - // Built-in localization for text direction LTR/RTL - GlobalWidgetsLocalizations.delegate, - // Built-in localization of basic text for Cupertino widgets - GlobalCupertinoLocalizations.delegate, - ]; + // Built-in localization of basic text for Material widgets + GlobalMaterialLocalizations.delegate, + // Built-in localization for text direction LTR/RTL + GlobalWidgetsLocalizations.delegate, + // Built-in localization of basic text for Cupertino widgets + GlobalCupertinoLocalizations.delegate, + ]; @override // TODO: implement supportedLocales Iterable get supportedLocales => throw UnimplementedError(); } +class ComplianceProtocol implements IGuruSdkComplianceProtocol { + @override + int getCurrentLevel() { + return 1; + } + + @override + String getLevelName() { + return ""; + } +} + @singleton class Initializer { // final TransactionService transactionService; @@ -79,7 +90,10 @@ class Initializer { static AppEnv _buildAppEnv({String flavor = ""}) { final rootPackage = _RootPackage(); - return AppEnv(spec: createAppSpec(flavor), package: rootPackage); + return AppEnv( + spec: createAppSpec(flavor), + package: rootPackage, + complianceProtocol: ComplianceProtocol()); } static Future ensureInitialized() async { diff --git a/guru_app/packages/guru_assistant/example/lib/data/initializer.g.dart b/guru_app/packages/guru_assistant/example/lib/data/initializer.g.dart index b74255d..2ed59d4 100644 --- a/guru_app/packages/guru_assistant/example/lib/data/initializer.g.dart +++ b/guru_app/packages/guru_assistant/example/lib/data/initializer.g.dart @@ -30,4 +30,6 @@ class _GuruSpecFactory { } } -class Flavors {} +class Flavors { + static const String classic = "classic"; +} diff --git a/guru_app/packages/guru_assistant/example/pubspec.lock b/guru_app/packages/guru_assistant/example/pubspec.lock index e9fca40..2e5d485 100644 --- a/guru_app/packages/guru_assistant/example/pubspec.lock +++ b/guru_app/packages/guru_assistant/example/pubspec.lock @@ -14,14 +14,14 @@ packages: name: _flutterfire_internals url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.12" + version: "1.3.16" adjust_sdk: dependency: transitive description: name: adjust_sdk url: "https://pub.flutter-io.cn" source: hosted - version: "4.33.0" + version: "4.36.0" analyzer: dependency: transitive description: @@ -175,21 +175,21 @@ packages: name: cloud_firestore url: "https://pub.flutter-io.cn" source: hosted - version: "4.3.1" + version: "4.13.6" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "5.10.1" + version: "6.0.10" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.1" + version: "3.8.10" code_builder: dependency: transitive description: @@ -265,7 +265,7 @@ packages: description: path: "packages/design" ref: "v2.3.0" - resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b" url: "git@github.com:castbox/guru_ui.git" source: git version: "2.0.2" @@ -274,7 +274,7 @@ packages: description: path: "packages/design_generator" ref: "v2.3.0" - resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b" url: "git@github.com:castbox/guru_ui.git" source: git version: "2.0.2" @@ -283,7 +283,7 @@ packages: description: path: "packages/design_spec" ref: "v2.3.0" - resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b" url: "git@github.com:castbox/guru_ui.git" source: git version: "2.0.2" @@ -300,7 +300,7 @@ packages: name: device_info_plus url: "https://pub.flutter-io.cn" source: hosted - version: "8.2.2" + version: "9.1.1" device_info_plus_platform_interface: dependency: transitive description: @@ -356,147 +356,147 @@ packages: name: firebase_analytics url: "https://pub.flutter-io.cn" source: hosted - version: "10.1.0" + version: "10.7.4" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.17" + version: "3.9.0" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web url: "https://pub.flutter-io.cn" source: hosted - version: "0.5.1+8" + version: "0.5.5+12" firebase_auth: dependency: transitive description: name: firebase_auth url: "https://pub.flutter-io.cn" source: hosted - version: "4.2.4" + version: "4.15.3" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "6.11.7" + version: "7.0.9" firebase_auth_web: dependency: transitive description: name: firebase_auth_web url: "https://pub.flutter-io.cn" source: hosted - version: "5.2.4" + version: "5.8.13" firebase_core: dependency: transitive description: name: firebase_core url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.24.2" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "4.8.0" + version: "5.0.0" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.flutter-io.cn" source: hosted - version: "2.6.0" + version: "2.10.0" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.9" + version: "3.4.8" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.11" + version: "3.6.16" firebase_dynamic_links: dependency: transitive description: name: firebase_dynamic_links url: "https://pub.flutter-io.cn" source: hosted - version: "5.0.11" + version: "5.4.8" firebase_dynamic_links_platform_interface: dependency: transitive description: name: firebase_dynamic_links_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.3+26" + version: "0.2.6+16" firebase_in_app_messaging: dependency: transitive description: name: firebase_in_app_messaging url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.0+10" + version: "0.7.4+8" firebase_in_app_messaging_platform_interface: dependency: transitive description: name: firebase_in_app_messaging_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.1+29" + version: "0.2.4+16" firebase_messaging: dependency: transitive description: name: firebase_messaging url: "https://pub.flutter-io.cn" source: hosted - version: "14.2.1" + version: "14.7.9" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "4.2.10" + version: "4.5.18" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.11" + version: "3.5.18" firebase_remote_config: dependency: transitive description: name: firebase_remote_config url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.9" + version: "4.3.8" firebase_remote_config_platform_interface: dependency: transitive description: name: firebase_remote_config_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.29" + version: "1.4.16" firebase_remote_config_web: dependency: transitive description: name: firebase_remote_config_web url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.18" + version: "1.4.16" fixnum: dependency: transitive description: @@ -504,11 +504,25 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" + flame: + dependency: transitive + description: + name: flame + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: transitive + description: + name: flutter_animate + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.4.0" flutter_blurhash: dependency: transitive description: @@ -605,19 +619,17 @@ packages: dependency: transitive description: path: "." - ref: "v2.3.1" - resolved-ref: e4438b7ece793a85da477b685e60c79981be281a + ref: "v2.3.4" + resolved-ref: "804fd22ddc1fc31acecdf72e936dabc0193379c5" url: "git@github.com:castbox/guru_analytics_flutter.git" source: git version: "2.0.0" guru_app: - dependency: "direct dev" + dependency: "direct overridden" description: - path: "." - ref: "v2.3.0" - resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" - url: "git@github.com:castbox/guru_app.git" - source: git + path: "../../.." + relative: true + source: path version: "2.1.0" guru_applifecycle_flutter: dependency: transitive @@ -633,7 +645,7 @@ packages: description: path: "." ref: "v2.3.8" - resolved-ref: "6fb5191d4cffc6a5728df7ee8af793fcc27fd081" + resolved-ref: "4cb520a2f9bea14300b0d2b452e183bcc42779f9" url: "git@github.com:castbox/guru_applovin_flutter.git" source: git version: "2.3.0" @@ -649,7 +661,7 @@ packages: description: path: "plugins/guru_navigator" ref: "v2.3.0" - resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7 url: "git@github.com:castbox/guru_app.git" source: git version: "0.0.1" @@ -658,7 +670,7 @@ packages: description: path: "plugins/guru_platform_data" ref: "v2.3.0" - resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7 url: "git@github.com:castbox/guru_app.git" source: git version: "0.0.1" @@ -667,34 +679,30 @@ packages: description: path: "packages/guru_popup" ref: "v2.3.0" - resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b" url: "git@github.com:castbox/guru_ui.git" source: git version: "2.3.0" guru_spec: - dependency: "direct dev" + dependency: "direct overridden" description: - path: "packages/guru_spec" - ref: "v2.3.0" - resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" - url: "git@github.com:castbox/guru_app.git" - source: git + path: "../../guru_spec" + relative: true + source: path version: "1.1.0" guru_utils: - dependency: "direct dev" + dependency: "direct overridden" description: - path: "packages/guru_utils" - ref: "v2.3.0" - resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" - url: "git@github.com:castbox/guru_app.git" - source: git + path: "../../guru_utils" + relative: true + source: path version: "2.1.0" guru_widgets: dependency: "direct dev" description: path: "packages/guru_widgets" ref: "v2.3.0" - resolved-ref: "57ef494b3db78c71932234986eab58a4a9063c6b" + resolved-ref: "8bef6934ac28e2d7c2b7ed56efa7941653ea681b" url: "git@github.com:castbox/guru_ui.git" source: git version: "2.2.0" @@ -866,6 +874,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" + ordered_set: + dependency: transitive + description: + name: ordered_set + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.2" package_config: dependency: transitive description: @@ -997,7 +1012,7 @@ packages: description: path: "plugins/persistent" ref: "v2.3.0" - resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7 url: "git@github.com:castbox/guru_app.git" source: git version: "0.0.1" @@ -1109,7 +1124,7 @@ packages: description: path: "plugins/soundpool" ref: "v2.3.0" - resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7 url: "git@github.com:castbox/guru_app.git" source: git version: "2.3.0" @@ -1321,7 +1336,7 @@ packages: description: path: "plugins/vibration" ref: "v2.3.0" - resolved-ref: "86c3ad1d5b550e01e4e5e9c56ac845c34809eeb2" + resolved-ref: a63c7acc1ed71a6e858116025bd741b2d7c554c7 url: "git@github.com:castbox/guru_app.git" source: git version: "1.7.5" @@ -1388,6 +1403,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.1.4" + win32_registry: + dependency: transitive + description: + name: win32_registry + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" xdg_directories: dependency: transitive description: diff --git a/guru_app/packages/guru_assistant/example/pubspec.yaml b/guru_app/packages/guru_assistant/example/pubspec.yaml index d5332f6..0ffaf01 100644 --- a/guru_app/packages/guru_assistant/example/pubspec.yaml +++ b/guru_app/packages/guru_assistant/example/pubspec.yaml @@ -100,6 +100,16 @@ dev_dependencies: # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. + +dependency_overrides: + guru_app: + path: ../../../ + + guru_utils: + path: ../../guru_utils + + guru_spec: + path: ../../guru_spec flutter: # The following line ensures that the Material Icons font is diff --git a/guru_app/packages/guru_assistant/pubspec.lock b/guru_app/packages/guru_assistant/pubspec.lock index dabe946..552c1e5 100644 --- a/guru_app/packages/guru_assistant/pubspec.lock +++ b/guru_app/packages/guru_assistant/pubspec.lock @@ -208,7 +208,7 @@ packages: sha256: "73ff438fe46028f0e19f55da18b6ddc6906ab750562cd7d9ffab77ff8c0c4307" url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.0" + version: "6.0.10" cloud_firestore_web: dependency: transitive description: @@ -216,7 +216,7 @@ packages: sha256: "232e45e95970d3a6baab8f50f9c3a6e2838d145d9d91ec9a7392837c44296397" url: "https://pub.flutter-io.cn" source: hosted - version: "3.9.0" + version: "3.8.10" code_builder: dependency: transitive description: @@ -578,6 +578,13 @@ packages: source: sdk version: "0.0.0" flutter_animate: + dependency: transitive + description: + name: flutter_animate + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.4.0" + flutter_blurhash: dependency: transitive description: name: flutter_animate diff --git a/guru_app/packages/guru_fb_game/.gitignore b/guru_app/packages/guru_fb_game/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/guru_app/packages/guru_fb_game/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/guru_app/packages/guru_fb_game/CHANGELOG.md b/guru_app/packages/guru_fb_game/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/packages/guru_fb_game/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/packages/guru_fb_game/LICENSE b/guru_app/packages/guru_fb_game/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/packages/guru_fb_game/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/packages/guru_fb_game/README.md b/guru_app/packages/guru_fb_game/README.md new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/guru_app/packages/guru_fb_game/README.md @@ -0,0 +1,39 @@ + + +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. diff --git a/guru_app/packages/guru_fb_game/analysis_options.yaml b/guru_app/packages/guru_fb_game/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/packages/guru_fb_game/analysis_options.yaml @@ -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 diff --git a/guru_app/packages/guru_fb_game/example/.gitignore b/guru_app/packages/guru_fb_game/example/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/guru_app/packages/guru_fb_game/example/README.md b/guru_app/packages/guru_fb_game/example/README.md new file mode 100644 index 0000000..2b3fce4 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/guru_app/packages/guru_fb_game/example/analysis_options.yaml b/guru_app/packages/guru_fb_game/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/guru_app/packages/guru_fb_game/example/lib/main.dart b/guru_app/packages/guru_fb_game/example/lib/main.dart new file mode 100644 index 0000000..8e94089 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/lib/main.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // TRY THIS: Try running your application with "flutter run". You'll see + // the application has a purple toolbar. Then, without quitting the app, + // try changing the seedColor in the colorScheme below to Colors.green + // and then invoke "hot reload" (save your changes or press the "hot + // reload" button in a Flutter-supported IDE, or press "r" if you used + // the command line to start the app). + // + // Notice that the counter didn't reset back to zero; the application + // state is not lost during the reload. To reset the state, use hot + // restart instead. + // + // This works for code too, not just values: Most code changes can be + // tested with just a hot reload. + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + // This call to setState tells the Flutter framework that something has + // changed in this State, which causes it to rerun the build method below + // so that the display can reflect the updated values. If we changed + // _counter without calling setState(), then the build method would not be + // called again, and so nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // TRY THIS: Try changing the color here to a specific color (to + // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar + // change color while the other colors stay the same. + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: Column( + // Column is also a layout widget. It takes a list of children and + // arranges them vertically. By default, it sizes itself to fit its + // children horizontally, and tries to be as tall as its parent. + // + // Column has various properties to control how it sizes itself and + // how it positions its children. Here we use mainAxisAlignment to + // center the children vertically; the main axis here is the vertical + // axis because Columns are vertical (the cross axis would be + // horizontal). + // + // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" + // action in the IDE, or press "p" in the console), to see the + // wireframe for each widget. + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/guru_app/packages/guru_fb_game/example/pubspec.lock b/guru_app/packages/guru_fb_game/example/pubspec.lock new file mode 100644 index 0000000..4de467f --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/pubspec.lock @@ -0,0 +1,188 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" +sdks: + dart: ">=3.2.3 <4.0.0" diff --git a/guru_app/packages/guru_fb_game/example/pubspec.yaml b/guru_app/packages/guru_fb_game/example/pubspec.yaml new file mode 100644 index 0000000..2342adc --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/pubspec.yaml @@ -0,0 +1,90 @@ +name: example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.2.3 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/packages/guru_fb_game/example/test/widget_test.dart b/guru_app/packages/guru_fb_game/example/test/widget_test.dart new file mode 100644 index 0000000..092d222 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/guru_app/packages/guru_fb_game/example/web/event-logger.js b/guru_app/packages/guru_fb_game/example/web/event-logger.js new file mode 100644 index 0000000..9dbd972 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/web/event-logger.js @@ -0,0 +1,102 @@ +let globalLogger; + +class EventLogger { + appId = ''; + deviceInfo = {}; + version = 10 + deviceStr = ''; + events = []; + info = {}; //用户信息 + + + constructor({ appId, deviceInfo, info }) { + this.appId = appId; + let u = navigator.userAgent; + let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; + let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); + let tzOffset = new Date().valueOf(); + this.deviceInfo = { + platform: isAndroid ? 'ANDROID' : 'IOS', + country: navigator.languages[1], + tzOffset, + deviceType: 'other', + brand: '', + model: '', + screenH: document.body.clientHeight, + screenW: document.body.clientWidth, + osVersion: '', + language: navigator.language, + ...deviceInfo + }; + for (let key in this.deviceInfo) this.deviceStr += `${key}=${this.deviceInfo[key]};`; + this.info = info; + } + + + getHeaders() { + let headers = new Headers(); + headers.append('X-APP-ID', this.appId); + headers.append('X-DEVICE-INFO', this.deviceStr) + headers.append('content-type', 'application/json'); + headers.append('Content-Encoding', 'gzip'); + return headers; + } + + zip(str) { + const binaryString = pako.gzip(str) + return binaryString; + } + + log(event) { + event = { + ...event, + timestamp: Date.now(), + info: this.info + }; + this.events.push(event); + if (this.events.length > 0) this.logEvent(); + } + + + async logEvent() { + const requestBody = { + version: this.version, + deviceInfo: this.deviceInfo, + events: [...this.events] + }; + const bodyStr = JSON.stringify(requestBody); + const gzippedStr = this.zip(bodyStr); + const config = { + headers: this.getHeaders(), + body: gzippedStr, + method: 'POST', + }; + await fetch('https://collect.saas.castbox.fm/event', config); + this.events = []; + } +} + +/** + * + * appInfo { + * appId + * deviceInfo + * version, + * info + * } + */ +function initEventLogger(appInfo) { + globalLogger = new EventLogger(JSON.parse(appInfo)); +} + +function castboxLogEvent(eventName, param, properties) { + if (globalLogger) { + globalLogger.log({ + event: eventName, + param: JSON.parse(param), + properties: JSON.parse(properties) + }); + } +} + + diff --git a/guru_app/packages/guru_fb_game/example/web/favicon.png b/guru_app/packages/guru_fb_game/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/guru_app/packages/guru_fb_game/example/web/fb-function.js b/guru_app/packages/guru_fb_game/example/web/fb-function.js new file mode 100644 index 0000000..6af04aa --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/web/fb-function.js @@ -0,0 +1,331 @@ +/** + * 获取facebook 用户基本信息 + * @returns + */ +function getFBProfile() { + const photo = FBInstant.player.getPhoto() + const name = FBInstant.player.getName() + const id = FBInstant.player.getID() + const contextType = FBInstant.context.getType() + return JSON.stringify({ photo, name, id, contextType }) +} + +function getFBProfileWithUid(success, error) { + const photo = FBInstant.player.getPhoto() + const name = FBInstant.player.getName() + const id = FBInstant.player.getID() + const contextType = FBInstant.context.getType() + FBInstant.player.getASIDAsync().then((userId) => { + success(JSON.stringify({ photo, name, id, contextType, userId })) + }).catch(err => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +function getEntryPointData() { + const data = FBInstant.getEntryPointData(); + return JSON.stringify(data); +} + +function getLocale() { + return FBInstant.getLocale(); +} + +//IOS, Android, WEB +function getPlatform() { + return FBInstant.getPlatform() +} + +// 分享游戏 +function shareGame(data, success, error) { + FBInstant.shareAsync(JSON.parse(data)) + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +function logEvent(eventName, valueToSum, parameters) { + FBInstant.logEvent(eventName, valueToSum, JSON.parse(parameters)) +} + +function inviteUser(data, success, error) { + FBInstant.inviteAsync(JSON.parse(data)) + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +// 检测当前支持广告类型 ["getInterstitialAdAsync", "getRewardedVideoAsync"] +function checkSupportedAds() { + let supportedAPIs = FBInstant.getSupportedAPIs() + return JSON.stringify(supportedAPIs); +} + +// 加载,展示插屏广告 +let preloadedInterstitial = null +function loadInterstitial(id, success, error) { + //插屏预载 + FBInstant.getInterstitialAdAsync(id) + .then((res) => { + preloadedInterstitial = res + return preloadedInterstitial.loadAsync + }).then(res => { + success() + }).catch((err) => { + error(err.message); + console.error(err.message) + }) +} +function showInterstitial(success, error) { + if (preloadedInterstitial == null) { + return error("showInterstitial preloadedInterstitial is null") + } + //插屏播放 + preloadedInterstitial + .showAsync() + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} +// 加载,展示激励视频 +let preloadedRewardedVideo = null +function loadRewarded(id, success, error) { + //激励视频预载 + FBInstant.getRewardedVideoAsync(id) + .then((res) => { + preloadedRewardedVideo = res + return preloadedRewardedVideo.loadAsync + }).then(res => { + success() + }).catch((err) => { + error(err.message); + console.error(err.message) + }) +} +function showRewarded(success, error) { + if (preloadedRewardedVideo == null) { + return error("showInterstitial preloadedRewardedVideo is null") + } + //插屏播放 + preloadedRewardedVideo + .showAsync() + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} +// 展示,隐藏banner广告 +function showBannerAds(id, success, error) { + FBInstant.loadBannerAdAsync(id) + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} +function hideBannerAds(id, success, error) { + FBInstant.hideBannerAdAsync(id) + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +// tournament 相关 + +function createTournament(initialScore, config, data, success, error) { + /** + * initialScore: number + * config: { + * title?: string + * image?: base64, + * sortOrder?: "HIGHER_IS_BETTER"| "LOWER_IS_BETTER" + * scoreFormat?: "NUMERIC" | "TIME" + * endTime?: unix timestamp (seconds) + * } + * data: {} customized + */ + FBInstant.tournament + .createAsync({ + initialScore, + config: JSON.parse(config), + data: JSON.parse(data), + }) + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +function postTournamentScore(id, score, success, error) { + FBInstant.tournament + .joinAsync(id) + .then(function () { + return FBInstant.tournament.postScoreAsync(score); + }) + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +function shareTournament(score, data, success, error) { + //data: customized {} + FBInstant.tournament + .shareAsync({ + score, + data: JSON.parse(data), + }) + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +function getCurrentTournament(success, error) { + FBInstant.getTournamentAsync() + .then(function (tournament) { + success( + JSON.stringify({ + id: tournament.getContextID(), + endTime: tournament.getEndTime(), + title: tournament.getTitle(), + payLoad: tournament.getPayload(), + }) + ) + }) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +// player 相关 + +function fbSetData(data) { + FBInstant.player.setDataAsync(JSON.parse(data)) +} + +function fbGetData(keys, success, error) { + FBInstant.player + .getDataAsync(JSON.parse(keys)) + .then((res) => { + success(JSON.stringify(res)) + }) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +function subscribeBot(success, error) { + FBInstant.player + .canSubscribeBotAsync() + .then((can_subscribe) => { + if (can_subscribe) { + return FBInstant.player.subscribeBotAsync() + } + }) + .then(success) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +function getConnectedPlayersAsync(success, error) { + //Fetches an array of ConnectedPlayer objects containing information about active players (people who played the game in the last 90 days) that are connected to the current player. + FBInstant.player.getConnectedPlayersAsync() + .then(function (res) { + var players = res.map(function (player) { + return { + player_id: player.getID(), + name: player.getName(), + picUrl: player.getPhoto() + } + }) + success(JSON.stringify(players)) + }) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +// context 相关 + +function getContextInfo() { + return JSON.stringify({ + id: FBInstant.context.getID(), + type: FBInstant.context.getType(), + }) +} + +function chooseContext(success, error) { + FBInstant.context.chooseAsync({ + filters: ['INCLUDE_EXISTING_CHALLENGES'], + minSize: 2, + maxSize: 2, + }).then(function () { + var contextInfo = { + id: FBInstant.context.getID(), + type: FBInstant.context.getType(), + } + console.log("chooseContext after", contextInfo) + success(JSON.stringify(contextInfo)) + }) + .catch((err) => { + error(JSON.stringify(err)) + console.error(err.message) + }) +} + +function createAsync(playerId, success, error) { + FBInstant.context + .createAsync(playerId) + .then(success) + .catch((err) => { + console.log("createAsync error", JSON.stringify(err)) + error(JSON.stringify(err)) + }) +} + +function sendPlayWith(data, success, error) { + FBInstant.context + .chooseAsync(JSON.parse(data)) + .then(success) + .catch((err) => { + console.log(err.message) + error(JSON.stringify(err)) + }) +} + +function updateAsync(img) { + FBInstant.updateAsync({ + action: 'CUSTOM', + cta: 'Join The Fight', + image: img, + text: { + default: 'X just invaded Y\'s village!', + }, + template: 'VILLAGE_INVASION', + data: { myReplayData: 'test', date: new Date().getMilliseconds() }, + strategy: 'IMMEDIATE', + notification: 'NO_PUSH', + }).then(function () { + // closes the game after the update is posted. + + }); +} \ No newline at end of file diff --git a/guru_app/packages/guru_fb_game/example/web/icons/Icon-192.png b/guru_app/packages/guru_fb_game/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/guru_app/packages/guru_fb_game/example/web/icons/Icon-512.png b/guru_app/packages/guru_fb_game/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/guru_app/packages/guru_fb_game/example/web/icons/Icon-maskable-192.png b/guru_app/packages/guru_fb_game/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/guru_app/packages/guru_fb_game/example/web/icons/Icon-maskable-512.png b/guru_app/packages/guru_fb_game/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/guru_app/packages/guru_fb_game/example/web/index.html b/guru_app/packages/guru_fb_game/example/web/index.html new file mode 100644 index 0000000..789ba7c --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/web/index.html @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + example + + + + + + + + + + + + + + +
+
+
+ + diff --git a/guru_app/packages/guru_fb_game/example/web/index.js b/guru_app/packages/guru_fb_game/example/web/index.js new file mode 100644 index 0000000..b4d2e91 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/web/index.js @@ -0,0 +1,68 @@ + +/** + * showLaunchView 展示首页 + * @param {Object} options + * @param {string} options.cover 顶部图片url,有则显示,没有不显示 + * @param {string} options.logo logo url,有则显示,没有不显示 + * @param {boolean} options.loading loading,有则显示,没有不显示 + * @param {string} options.guruLimited guru limited 文案,默认"@ Guru network limited" + */ +function showLaunchView(options = { + guruLimited: '@ Guru network limited' +}) { + const view = document.createElement('div') + view.id = 'launch_view' + + if (options.cover) { + const img = document.createElement('img') + img.src = options.cover + img.className = 'cover' + view.appendChild(img) + } + + if (options.logo) { + const img = document.createElement('img') + img.src = options.logo + img.className = 'logo' + view.appendChild(img) + } + + if (options.loading) { + const loading = document.createElement('div') + loading.className = 'loading' + view.appendChild(loading) + } + + const text = document.createElement('p') + text.className = 'guru_text' + view.appendChild(text) + + document.body.append(view) +} + +function hideLaunchView() { + const view = document.querySelector('#launch_view') + if (view) { + view.style.display = 'none' + } + showFlutterView() +} + +function showFlutterView() { + document.querySelector("flutter-view").style.display = "block" +} + +/** + * 打开debug dialog + * @param {string} message 展示内容 + */ +function showDebugDialog(message) { + const view = document.querySelector("#debug_dialog") + if (view) view.style.display = "flex" + const content = document.querySelector(".content") + content.innerHTML = message +} + +function closeDebugeDialog(e) { + e.target.style.display = "none" +} \ No newline at end of file diff --git a/guru_app/packages/guru_fb_game/example/web/manifest.json b/guru_app/packages/guru_fb_game/example/web/manifest.json new file mode 100644 index 0000000..096edf8 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/guru_app/packages/guru_fb_game/example/web/pako.min.js b/guru_app/packages/guru_fb_game/example/web/pako.min.js new file mode 100644 index 0000000..ba39731 --- /dev/null +++ b/guru_app/packages/guru_fb_game/example/web/pako.min.js @@ -0,0 +1 @@ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).pako=t()}}(function(){return function r(s,o,l){function h(e,t){if(!o[e]){if(!s[e]){var a="function"==typeof require&&require;if(!t&&a)return a(e,!0);if(d)return d(e,!0);var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}var n=o[e]={exports:{}};s[e][0].call(n.exports,function(t){return h(s[e][1][t]||t)},n,n.exports,r,s,o,l)}return o[e].exports}for(var d="function"==typeof require&&require,t=0;t>>6:(a<65536?e[r++]=224|a>>>12:(e[r++]=240|a>>>18,e[r++]=128|a>>>12&63),e[r++]=128|a>>>6&63),e[r++]=128|63&a);return e},a.buf2binstring=function(t){return d(t,t.length)},a.binstring2buf=function(t){for(var e=new l.Buf8(t.length),a=0,i=e.length;a>10&1023,o[i++]=56320|1023&n)}return d(o,i)},a.utf8border=function(t,e){var a;for((e=e||t.length)>t.length&&(e=t.length),a=e-1;0<=a&&128==(192&t[a]);)a--;return a<0?e:0===a?e:a+h[t[a]]>e?a:e}},{"./common":3}],5:[function(t,e,a){"use strict";e.exports=function(t,e,a,i){for(var n=65535&t|0,r=t>>>16&65535|0,s=0;0!==a;){for(a-=s=2e3>>1:t>>>1;e[a]=t}return e}();e.exports=function(t,e,a,i){var n=o,r=i+a;t^=-1;for(var s=i;s>>8^n[255&(t^e[s])];return-1^t}},{}],8:[function(t,e,a){"use strict";var l,_=t("../utils/common"),h=t("./trees"),u=t("./adler32"),c=t("./crc32"),i=t("./messages"),d=0,f=4,b=0,g=-2,m=-1,w=4,n=2,p=8,v=9,r=286,s=30,o=19,k=2*r+1,y=15,x=3,z=258,B=z+x+1,S=42,E=113,A=1,Z=2,R=3,C=4;function N(t,e){return t.msg=i[e],e}function O(t){return(t<<1)-(4t.avail_out&&(a=t.avail_out),0!==a&&(_.arraySet(t.output,e.pending_buf,e.pending_out,a,t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))}function U(t,e){h._tr_flush_block(t,0<=t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,I(t.strm)}function T(t,e){t.pending_buf[t.pending++]=e}function F(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function L(t,e){var a,i,n=t.max_chain_length,r=t.strstart,s=t.prev_length,o=t.nice_match,l=t.strstart>t.w_size-B?t.strstart-(t.w_size-B):0,h=t.window,d=t.w_mask,f=t.prev,_=t.strstart+z,u=h[r+s-1],c=h[r+s];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(h[(a=e)+s]===c&&h[a+s-1]===u&&h[a]===h[r]&&h[++a]===h[r+1]){r+=2,a++;do{}while(h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&r<_);if(i=z-(_-r),r=_-z,sl&&0!=--n);return s<=t.lookahead?s:t.lookahead}function H(t){var e,a,i,n,r,s,o,l,h,d,f=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=f+(f-B)){for(_.arraySet(t.window,t.window,f,f,0),t.match_start-=f,t.strstart-=f,t.block_start-=f,e=a=t.hash_size;i=t.head[--e],t.head[e]=f<=i?i-f:0,--a;);for(e=a=f;i=t.prev[--e],t.prev[e]=f<=i?i-f:0,--a;);n+=f}if(0===t.strm.avail_in)break;if(s=t.strm,o=t.window,l=t.strstart+t.lookahead,h=n,d=void 0,d=s.avail_in,h=x)for(r=t.strstart-t.insert,t.ins_h=t.window[r],t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x)if(i=h._tr_tally(t,t.strstart-t.match_start,t.match_length-x),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=x){for(t.match_length--;t.strstart++,t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x&&t.match_length<=t.prev_length){for(n=t.strstart+t.lookahead-x,i=h._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-x),t.lookahead-=t.prev_length-1,t.prev_length-=2;++t.strstart<=n&&(t.ins_h=(t.ins_h<t.pending_buf_size-5&&(a=t.pending_buf_size-5);;){if(t.lookahead<=1){if(H(t),0===t.lookahead&&e===d)return A;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+a;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,U(t,!1),0===t.strm.avail_out))return A;if(t.strstart-t.block_start>=t.w_size-B&&(U(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(U(t,!0),0===t.strm.avail_out?R:C):(t.strstart>t.block_start&&(U(t,!1),t.strm.avail_out),A)}),new M(4,4,8,4,j),new M(4,5,16,8,j),new M(4,6,32,32,j),new M(4,4,16,16,K),new M(8,16,32,32,K),new M(8,16,128,128,K),new M(8,32,128,256,K),new M(32,128,258,1024,K),new M(32,258,258,4096,K)],a.deflateInit=function(t,e){return G(t,e,p,15,8,0)},a.deflateInit2=G,a.deflateReset=q,a.deflateResetKeep=Y,a.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?g:(t.state.gzhead=e,b):g},a.deflate=function(t,e){var a,i,n,r;if(!t||!t.state||5>8&255),T(i,i.gzhead.time>>16&255),T(i,i.gzhead.time>>24&255),T(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),T(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(T(i,255&i.gzhead.extra.length),T(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=c(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(T(i,0),T(i,0),T(i,0),T(i,0),T(i,0),T(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),T(i,3),i.status=E);else{var s=p+(i.w_bits-8<<4)<<8;s|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(s|=32),s+=31-s%31,i.status=E,F(i,s),0!==i.strstart&&(F(i,t.adler>>>16),F(i,65535&t.adler)),t.adler=1}if(69===i.status)if(i.gzhead.extra){for(n=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),I(t),n=i.pending,i.pending!==i.pending_buf_size));)T(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),I(t),n=i.pending,i.pending===i.pending_buf_size)){r=1;break}T(i,r=i.gzindexn&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),0===r&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),I(t),n=i.pending,i.pending===i.pending_buf_size)){r=1;break}T(i,r=i.gzindexn&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),0===r&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&I(t),i.pending+2<=i.pending_buf_size&&(T(i,255&t.adler),T(i,t.adler>>8&255),t.adler=0,i.status=E)):i.status=E),0!==i.pending){if(I(t),0===t.avail_out)return i.last_flush=-1,b}else if(0===t.avail_in&&O(e)<=O(a)&&e!==f)return N(t,-5);if(666===i.status&&0!==t.avail_in)return N(t,-5);if(0!==t.avail_in||0!==i.lookahead||e!==d&&666!==i.status){var o=2===i.strategy?function(t,e){for(var a;;){if(0===t.lookahead&&(H(t),0===t.lookahead)){if(e===d)return A;break}if(t.match_length=0,a=h._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(U(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(U(t,!0),0===t.strm.avail_out?R:C):t.last_lit&&(U(t,!1),0===t.strm.avail_out)?A:Z}(i,e):3===i.strategy?function(t,e){for(var a,i,n,r,s=t.window;;){if(t.lookahead<=z){if(H(t),t.lookahead<=z&&e===d)return A;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=x&&0t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=x?(a=h._tr_tally(t,1,t.match_length-x),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=h._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(U(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(U(t,!0),0===t.strm.avail_out?R:C):t.last_lit&&(U(t,!1),0===t.strm.avail_out)?A:Z}(i,e):l[i.level].func(i,e);if(o!==R&&o!==C||(i.status=666),o===A||o===R)return 0===t.avail_out&&(i.last_flush=-1),b;if(o===Z&&(1===e?h._tr_align(i):5!==e&&(h._tr_stored_block(i,0,0,!1),3===e&&(D(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),I(t),0===t.avail_out))return i.last_flush=-1,b}return e!==f?b:i.wrap<=0?1:(2===i.wrap?(T(i,255&t.adler),T(i,t.adler>>8&255),T(i,t.adler>>16&255),T(i,t.adler>>24&255),T(i,255&t.total_in),T(i,t.total_in>>8&255),T(i,t.total_in>>16&255),T(i,t.total_in>>24&255)):(F(i,t.adler>>>16),F(i,65535&t.adler)),I(t),0=a.w_size&&(0===r&&(D(a.head),a.strstart=0,a.block_start=0,a.insert=0),h=new _.Buf8(a.w_size),_.arraySet(h,e,d-a.w_size,a.w_size,0),e=h,d=a.w_size),s=t.avail_in,o=t.next_in,l=t.input,t.avail_in=d,t.next_in=0,t.input=e,H(a);a.lookahead>=x;){for(i=a.strstart,n=a.lookahead-(x-1);a.ins_h=(a.ins_h<>>=v=p>>>24,c-=v,0===(v=p>>>16&255))S[r++]=65535&p;else{if(!(16&v)){if(0==(64&v)){p=b[(65535&p)+(u&(1<>>=v,c-=v),c<15&&(u+=B[i++]<>>=v=p>>>24,c-=v,!(16&(v=p>>>16&255))){if(0==(64&v)){p=g[(65535&p)+(u&(1<>>=v,c-=v,(v=r-s)>3,u&=(1<<(c-=k<<3))-1,t.next_in=i,t.next_out=r,t.avail_in=i>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function r(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Z.Buf16(320),this.work=new Z.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function s(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=F,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Z.Buf32(i),e.distcode=e.distdyn=new Z.Buf32(n),e.sane=1,e.back=-1,U):T}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,s(t)):T}function l(t,e){var a,i;return t&&t.state?(i=t.state,e<0?(a=0,e=-e):(a=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=r.wsize?(Z.arraySet(r.window,e,a-r.wsize,r.wsize,0),r.wnext=0,r.whave=r.wsize):(i<(n=r.wsize-r.wnext)&&(n=i),Z.arraySet(r.window,e,a-i,n,r.wnext),(i-=n)?(Z.arraySet(r.window,e,a-i,i,0),r.wnext=i,r.whave=r.wsize):(r.wnext+=n,r.wnext===r.wsize&&(r.wnext=0),r.whave>>8&255,a.check=C(a.check,E,2,0),d=h=0,a.mode=2;break}if(a.flags=0,a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=30;break}if(8!=(15&h)){t.msg="unknown compression method",a.mode=30;break}if(d-=4,y=8+(15&(h>>>=4)),0===a.wbits)a.wbits=y;else if(y>a.wbits){t.msg="invalid window size",a.mode=30;break}a.dmax=1<>8&1),512&a.flags&&(E[0]=255&h,E[1]=h>>>8&255,a.check=C(a.check,E,2,0)),d=h=0,a.mode=3;case 3:for(;d<32;){if(0===o)break t;o--,h+=i[r++]<>>8&255,E[2]=h>>>16&255,E[3]=h>>>24&255,a.check=C(a.check,E,4,0)),d=h=0,a.mode=4;case 4:for(;d<16;){if(0===o)break t;o--,h+=i[r++]<>8),512&a.flags&&(E[0]=255&h,E[1]=h>>>8&255,a.check=C(a.check,E,2,0)),d=h=0,a.mode=5;case 5:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[r++]<>>8&255,a.check=C(a.check,E,2,0)),d=h=0}else a.head&&(a.head.extra=null);a.mode=6;case 6:if(1024&a.flags&&(o<(u=a.length)&&(u=o),u&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Array(a.head.extra_len)),Z.arraySet(a.head.extra,i,r,u,y)),512&a.flags&&(a.check=C(a.check,i,u,r)),o-=u,r+=u,a.length-=u),a.length))break t;a.length=0,a.mode=7;case 7:if(2048&a.flags){if(0===o)break t;for(u=0;y=i[r+u++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y)),y&&u>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=12;break;case 10:for(;d<32;){if(0===o)break t;o--,h+=i[r++]<>>=7&d,d-=7&d,a.mode=27;break}for(;d<3;){if(0===o)break t;o--,h+=i[r++]<>>=1)){case 0:a.mode=14;break;case 1:if(H(a),a.mode=20,6!==e)break;h>>>=2,d-=2;break t;case 2:a.mode=17;break;case 3:t.msg="invalid block type",a.mode=30}h>>>=2,d-=2;break;case 14:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[r++]<>>16^65535)){t.msg="invalid stored block lengths",a.mode=30;break}if(a.length=65535&h,d=h=0,a.mode=15,6===e)break t;case 15:a.mode=16;case 16:if(u=a.length){if(o>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,286>>=3,d-=3}for(;a.have<19;)a.lens[A[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,z={bits:a.lenbits},x=O(0,a.lens,0,19,a.lencode,0,a.work,z),a.lenbits=z.bits,x){t.msg="invalid code lengths set",a.mode=30;break}a.have=0,a.mode=19;case 19:for(;a.have>>16&255,w=65535&S,!((g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<>>=g,d-=g,a.lens[a.have++]=w;else{if(16===w){for(B=g+2;d>>=g,d-=g,0===a.have){t.msg="invalid bit length repeat",a.mode=30;break}y=a.lens[a.have-1],u=3+(3&h),h>>>=2,d-=2}else if(17===w){for(B=g+3;d>>=g)),h>>>=3,d-=3}else{for(B=g+7;d>>=g)),h>>>=7,d-=7}if(a.have+u>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=30;break}for(;u--;)a.lens[a.have++]=y}}if(30===a.mode)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=30;break}if(a.lenbits=9,z={bits:a.lenbits},x=O(D,a.lens,0,a.nlen,a.lencode,0,a.work,z),a.lenbits=z.bits,x){t.msg="invalid literal/lengths set",a.mode=30;break}if(a.distbits=6,a.distcode=a.distdyn,z={bits:a.distbits},x=O(I,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,z),a.distbits=z.bits,x){t.msg="invalid distances set",a.mode=30;break}if(a.mode=20,6===e)break t;case 20:a.mode=21;case 21:if(6<=o&&258<=l){t.next_out=s,t.avail_out=l,t.next_in=r,t.avail_in=o,a.hold=h,a.bits=d,N(t,_),s=t.next_out,n=t.output,l=t.avail_out,r=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,12===a.mode&&(a.back=-1);break}for(a.back=0;m=(S=a.lencode[h&(1<>>16&255,w=65535&S,!((g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<>p)])>>>16&255,w=65535&S,!(p+(g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<>>=p,d-=p,a.back+=p}if(h>>>=g,d-=g,a.back+=g,a.length=w,0===m){a.mode=26;break}if(32&m){a.back=-1,a.mode=12;break}if(64&m){t.msg="invalid literal/length code",a.mode=30;break}a.extra=15&m,a.mode=22;case 22:if(a.extra){for(B=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=23;case 23:for(;m=(S=a.distcode[h&(1<>>16&255,w=65535&S,!((g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<>p)])>>>16&255,w=65535&S,!(p+(g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<>>=p,d-=p,a.back+=p}if(h>>>=g,d-=g,a.back+=g,64&m){t.msg="invalid distance code",a.mode=30;break}a.offset=w,a.extra=15&m,a.mode=24;case 24:if(a.extra){for(B=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=30;break}a.mode=25;case 25:if(0===l)break t;if(u=_-l,a.offset>u){if((u=a.offset-u)>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=30;break}u>a.wnext?(u-=a.wnext,c=a.wsize-u):c=a.wnext-u,u>a.length&&(u=a.length),b=a.window}else b=n,c=s-a.offset,u=a.length;for(lu?(b=N[O+s[p]],g=A[Z+s[p]]):(b=96,g=0),l=1<>z)+(h-=l)]=c<<24|b<<16|g|0,0!==h;);for(l=1<>=1;if(0!==l?(E&=l-1,E+=l):E=0,p++,0==--R[w]){if(w===k)break;w=e[a+s[p]]}if(y>>7)]}function T(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function F(t,e,a){t.bi_valid>n-a?(t.bi_buf|=e<>n-t.bi_valid,t.bi_valid+=a-n):(t.bi_buf|=e<>>=1,a<<=1,0<--e;);return a>>>1}function j(t,e,a){var i,n,r=new Array(m+1),s=0;for(i=1;i<=m;i++)r[i]=s=s+a[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=H(r[o]++,o))}}function K(t){var e;for(e=0;e<_;e++)t.dyn_ltree[2*e]=0;for(e=0;e>1;1<=a;a--)Y(t,r,a);for(n=l;a=t.heap[1],t.heap[1]=t.heap[t.heap_len--],Y(t,r,1),i=t.heap[1],t.heap[--t.heap_max]=a,t.heap[--t.heap_max]=i,r[2*n]=r[2*a]+r[2*i],t.depth[n]=(t.depth[a]>=t.depth[i]?t.depth[a]:t.depth[i])+1,r[2*a+1]=r[2*i+1]=n,t.heap[1]=n++,Y(t,r,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var a,i,n,r,s,o,l=e.dyn_tree,h=e.max_code,d=e.stat_desc.static_tree,f=e.stat_desc.has_stree,_=e.stat_desc.extra_bits,u=e.stat_desc.extra_base,c=e.stat_desc.max_length,b=0;for(r=0;r<=m;r++)t.bl_count[r]=0;for(l[2*t.heap[t.heap_max]+1]=0,a=t.heap_max+1;a>=7;i>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return o;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e>>3,(r=t.static_len+3+7>>>3)<=n&&(n=r)):n=r=a+5,a+4<=n&&-1!==e?Q(t,e,a,i):4===t.strategy||r===n?(F(t,2+(i?1:0),3),q(t,S,E)):(F(t,4+(i?1:0),3),function(t,e,a,i){var n;for(F(t,e-257,5),F(t,a-1,5),F(t,i-4,4),n=0;n>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&a,t.last_lit++,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(Z[a]+f+1)]++,t.dyn_dtree[2*U(e)]++),t.last_lit===t.lit_bufsize-1},a._tr_align=function(t){var e;F(t,2,3),L(t,w,S),16===(e=t).bi_valid?(T(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}},{"../utils/common":3}],15:[function(t,e,a){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],"/":[function(t,e,a){"use strict";var i={};(0,t("./lib/utils/common").assign)(i,t("./lib/deflate"),t("./lib/inflate"),t("./lib/zlib/constants")),e.exports=i},{"./lib/deflate":1,"./lib/inflate":2,"./lib/utils/common":3,"./lib/zlib/constants":6}]},{},[])("/")}); diff --git a/guru_app/packages/guru_fb_game/lib/guru_fb_game.dart b/guru_app/packages/guru_fb_game/lib/guru_fb_game.dart new file mode 100644 index 0000000..aa735e3 --- /dev/null +++ b/guru_app/packages/guru_fb_game/lib/guru_fb_game.dart @@ -0,0 +1,59 @@ +library guru_fb_game; +import 'package:guru_fb_game/model/model.dart'; +import 'package:guru_fb_game/utils.dart'; + +class AppConfig { + final String appVersion; + final String appBuildNumber; + final String guruLogAppId; + final String bundleId; + + final String? adUnitIdBanner; + final String? adUnitIdInter; + final String? adUnitIdRewardVideo; + + const AppConfig({ + required this.appVersion, + this.appBuildNumber = "1", + required this.guruLogAppId, + required this.bundleId, + this.adUnitIdBanner, + this.adUnitIdInter, + this.adUnitIdRewardVideo + }); +} + +class GuruFbGame { + static final GuruFbGame _instance = GuruFbGame._(); + + static GuruFbGame get instance => _instance; + + GuruFbGame._(); + + FbProfile? fbProfile; + + Map? fbtournament; + + Future _initialize(AppConfig config) async { + // setUrlStrategy(null); + fbProfile = await FbGameGlobalUtils.getFBProfile(); + final fbPlatform = FbGameGlobalUtils.getPlatform(); + final deviceInfo = GuruLogDeviceData(appId: config.bundleId, version: config.appVersion, fbPlatform: fbPlatform); + final logInfoData = GuruLogInfoData(deveiceId: '', uid: fbProfile?.userId ?? ''); + final initData = InitGuruLogEventData(appId: config.guruLogAppId, deviceInfo: deviceInfo, info: logInfoData); + FbGameLogUtils.initGuruLogEvent(initData); + fbtournament = await FbGameTournamentUtils.getCurrentTournament(); + await initAds(config); + } + + Future initAds(AppConfig config) async{ + List list = FbGameAdUtils.checkSupportedAds(); + if (list.contains('getInterstitialAdAsync') && config.adUnitIdInter != null) { + FbGameAdUtils.loadInterstitial(config.adUnitIdInter!); + } + + if (list.contains('getRewardedVideoAsync') && config.adUnitIdRewardVideo != null) { + FbGameAdUtils.loadRewarded(config.adUnitIdRewardVideo!); + } + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_fb_game/lib/model/model.dart b/guru_app/packages/guru_fb_game/lib/model/model.dart new file mode 100644 index 0000000..396a334 --- /dev/null +++ b/guru_app/packages/guru_fb_game/lib/model/model.dart @@ -0,0 +1,232 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'model.g.dart'; + +@JsonSerializable() +class FbProfile { + final String photo; + final String name; + final String id; + final String contextType; + final String? userId; + + const FbProfile({ + required this.photo, + required this.name, + required this.id, + required this.contextType, + this.userId + }); + + factory FbProfile.fromJson(Map json) => _$FbProfileFromJson(json); + + Map toJson() => _$FbProfileToJson(this); +} + +@JsonSerializable() +class FbShareData { + final String image; + final String text; + final Map data; + final List shareDestination; + final bool switchContext; + + const FbShareData({ + this.image = 'data:image/png;base64,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', + required this.text, + this.data = const {}, + this.shareDestination = const [ + 'NEWSFEED', + 'GROUP', + 'COPY_LINK', + 'MESSENGER' + ], + this.switchContext = false, + }); + + factory FbShareData.fromJson(Map json) => _$FbShareDataFromJson(json); + + Map toJson() => _$FbShareDataToJson(this); +} + +@JsonSerializable() +class FbInviteTextData { + final String text; + // localizations: { + // ar_AR: 'X \u0641\u0642\u0637 \u063A\u0632\u062A ' + + // '\u0642\u0631\u064A\u0629 Y!', + // en_US: 'X just invaded Y\'s village!', + // es_LA: '\u00A1X acaba de invadir el pueblo de Y!' + // } + final Map localizations; + + const FbInviteTextData({required this.text, required this.localizations}); + + Map toMap() { + return { + 'default': text, + 'localizations': localizations, + }; + } + + + factory FbInviteTextData.fromJson(Map json) => _$FbInviteTextDataFromJson(json); + + Map toJson() => _$FbInviteTextDataToJson(this); +} + +@JsonSerializable() +class FbInviteData { + final String image; + final FbInviteTextData text; + final FbInviteTextData? cta; + final FbInviteTextData? dialogTitle; + final List? filters; // ['NEW_CONTEXT_ONLY', 'EXISTING_PLAYERS_ONLY'] + // sections: [ + // {sectionType: 'GROUPS', maxResults: 2}, + // {sectionType: 'USERS'} + // ], + final List>? sections; + + const FbInviteData({ + required this.image, + required this.text, + this.cta, + this.dialogTitle, + this.filters, + this.sections, + }); + + factory FbInviteData.fromJson(Map json) => _$FbInviteDataFromJson(json); + + Map toJson() => _$FbInviteDataToJson(this); +} + +@JsonSerializable() +class GuruLogDeviceData{ + final String appId; + final String version; + final String fbPlatform; + + GuruLogDeviceData({ + required this.appId, + this.version = '1.0.0', + required this.fbPlatform + }); + + factory GuruLogDeviceData.fromJson(Map json) => _$GuruLogDeviceDataFromJson(json); + + Map toJson() => _$GuruLogDeviceDataToJson(this); +} + +@JsonSerializable() +class GuruLogInfoData { + final String? deveiceId; + final String? uid; + final String? adjustId; + final String? adId; + final String? firebaseId; + + const GuruLogInfoData({ + this.deveiceId, + this.uid, + this.adjustId, + this.adId, + this.firebaseId + }); + + factory GuruLogInfoData.fromJson(Map json) => _$GuruLogInfoDataFromJson(json); + + Map toJson() => _$GuruLogInfoDataToJson(this); +} + +@JsonSerializable() +class InitGuruLogEventData { + final String appId; + final GuruLogDeviceData deviceInfo; + final GuruLogInfoData info; + + const InitGuruLogEventData({ + required this.appId, + required this.deviceInfo, + required this.info + }); + + factory InitGuruLogEventData.fromJson(Map json) => _$InitGuruLogEventDataFromJson(json); + + Map toJson() => _$InitGuruLogEventDataToJson(this); +} + +enum FBTournamentSortOrder{ + HIGHER_IS_BETTER, LOWER_IS_BETTER; +} + +enum FBTournamentSortFormat{ + NUMERIC, TIME; +} + +@JsonSerializable() +class FbTournamentConfig{ + final String? title; + final String? image; + // "HIGHER_IS_BETTER"| "LOWER_IS_BETTER" + final FBTournamentSortOrder? sortOrder; + // "NUMERIC" | "TIME" + final FBTournamentSortFormat? scoreFormat; + // timestamp + final num? endTime; + + const FbTournamentConfig({ + this.title, + this.image, + this.sortOrder, + this.scoreFormat, + this.endTime + }); + + factory FbTournamentConfig.fromJson(Map json) => _$FbTournamentConfigFromJson(json); + + Map toJson() => _$FbTournamentConfigToJson(this); +} + +@JsonSerializable() +class FbUserInfo { + @JsonKey(name: 'player_id', defaultValue: "") + final String id; + + @JsonKey(name: 'user_id', defaultValue: "") + final String userId; + + @JsonKey(name: 'name', defaultValue: "") + final String name; + + @JsonKey(name: 'picUrl', defaultValue: "") + final String picUrl; + + FbUserInfo(this.id, this.userId, this.name, this.picUrl); + + factory FbUserInfo.fromJson(Map json) => _$FbUserInfoFromJson(json); + + Map toJson() => _$FbUserInfoToJson(this); +} + +enum FBContextType { + @JsonValue("POST") POST, + @JsonValue("THREAD") THREAD, + @JsonValue("GROUP") GROUP, + @JsonValue("SOLO") SOLO +} + +@JsonSerializable() +class FBContextInfo { + final String id; + final FBContextType type; + + const FBContextInfo({ + required this.id, + required this.type, + }); + + factory FBContextInfo.fromJson(Map json) => _$FBContextInfoFromJson(json); + + Map toJson() => _$FBContextInfoToJson(this); +} \ No newline at end of file diff --git a/guru_app/packages/guru_fb_game/lib/model/model.g.dart b/guru_app/packages/guru_fb_game/lib/model/model.g.dart new file mode 100644 index 0000000..9aa282a --- /dev/null +++ b/guru_app/packages/guru_fb_game/lib/model/model.g.dart @@ -0,0 +1,199 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FbProfile _$FbProfileFromJson(Map json) => FbProfile( + photo: json['photo'] as String, + name: json['name'] as String, + id: json['id'] as String, + contextType: json['contextType'] as String, + userId: json['userId'] as String?, + ); + +Map _$FbProfileToJson(FbProfile instance) => { + 'photo': instance.photo, + 'name': instance.name, + 'id': instance.id, + 'contextType': instance.contextType, + 'userId': instance.userId, + }; + +FbShareData _$FbShareDataFromJson(Map json) => FbShareData( + image: json['image'] as String? ?? + 'data:image/png;base64,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', + text: json['text'] as String, + data: (json['data'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ) ?? + const {}, + shareDestination: (json['shareDestination'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ['NEWSFEED', 'GROUP', 'COPY_LINK', 'MESSENGER'], + switchContext: json['switchContext'] as bool? ?? false, + ); + +Map _$FbShareDataToJson(FbShareData instance) => + { + 'image': instance.image, + 'text': instance.text, + 'data': instance.data, + 'shareDestination': instance.shareDestination, + 'switchContext': instance.switchContext, + }; + +FbInviteTextData _$FbInviteTextDataFromJson(Map json) => + FbInviteTextData( + text: json['text'] as String, + localizations: Map.from(json['localizations'] as Map), + ); + +Map _$FbInviteTextDataToJson(FbInviteTextData instance) => + { + 'text': instance.text, + 'localizations': instance.localizations, + }; + +FbInviteData _$FbInviteDataFromJson(Map json) => FbInviteData( + image: json['image'] as String, + text: FbInviteTextData.fromJson(json['text'] as Map), + cta: json['cta'] == null + ? null + : FbInviteTextData.fromJson(json['cta'] as Map), + dialogTitle: json['dialogTitle'] == null + ? null + : FbInviteTextData.fromJson( + json['dialogTitle'] as Map), + filters: + (json['filters'] as List?)?.map((e) => e as String).toList(), + sections: (json['sections'] as List?) + ?.map((e) => e as Map) + .toList(), + ); + +Map _$FbInviteDataToJson(FbInviteData instance) => + { + 'image': instance.image, + 'text': instance.text, + 'cta': instance.cta, + 'dialogTitle': instance.dialogTitle, + 'filters': instance.filters, + 'sections': instance.sections, + }; + +GuruLogDeviceData _$GuruLogDeviceDataFromJson(Map json) => + GuruLogDeviceData( + appId: json['appId'] as String, + version: json['version'] as String? ?? '1.0.0', + fbPlatform: json['fbPlatform'] as String, + ); + +Map _$GuruLogDeviceDataToJson(GuruLogDeviceData instance) => + { + 'appId': instance.appId, + 'version': instance.version, + 'fbPlatform': instance.fbPlatform, + }; + +GuruLogInfoData _$GuruLogInfoDataFromJson(Map json) => + GuruLogInfoData( + deveiceId: json['deveiceId'] as String?, + uid: json['uid'] as String?, + adjustId: json['adjustId'] as String?, + adId: json['adId'] as String?, + firebaseId: json['firebaseId'] as String?, + ); + +Map _$GuruLogInfoDataToJson(GuruLogInfoData instance) => + { + 'deveiceId': instance.deveiceId, + 'uid': instance.uid, + 'adjustId': instance.adjustId, + 'adId': instance.adId, + 'firebaseId': instance.firebaseId, + }; + +InitGuruLogEventData _$InitGuruLogEventDataFromJson( + Map json) => + InitGuruLogEventData( + appId: json['appId'] as String, + deviceInfo: GuruLogDeviceData.fromJson( + json['deviceInfo'] as Map), + info: GuruLogInfoData.fromJson(json['info'] as Map), + ); + +Map _$InitGuruLogEventDataToJson( + InitGuruLogEventData instance) => + { + 'appId': instance.appId, + 'deviceInfo': instance.deviceInfo, + 'info': instance.info, + }; + +FbTournamentConfig _$FbTournamentConfigFromJson(Map json) => + FbTournamentConfig( + title: json['title'] as String?, + image: json['image'] as String?, + sortOrder: $enumDecodeNullable( + _$FBTournamentSortOrderEnumMap, json['sortOrder']), + scoreFormat: $enumDecodeNullable( + _$FBTournamentSortFormatEnumMap, json['scoreFormat']), + endTime: json['endTime'] as num?, + ); + +Map _$FbTournamentConfigToJson(FbTournamentConfig instance) => + { + 'title': instance.title, + 'image': instance.image, + 'sortOrder': _$FBTournamentSortOrderEnumMap[instance.sortOrder], + 'scoreFormat': _$FBTournamentSortFormatEnumMap[instance.scoreFormat], + 'endTime': instance.endTime, + }; + +const _$FBTournamentSortOrderEnumMap = { + FBTournamentSortOrder.HIGHER_IS_BETTER: 'HIGHER_IS_BETTER', + FBTournamentSortOrder.LOWER_IS_BETTER: 'LOWER_IS_BETTER', +}; + +const _$FBTournamentSortFormatEnumMap = { + FBTournamentSortFormat.NUMERIC: 'NUMERIC', + FBTournamentSortFormat.TIME: 'TIME', +}; + +FbUserInfo _$FbUserInfoFromJson(Map json) => FbUserInfo( + json['player_id'] as String? ?? '', + json['user_id'] as String? ?? '', + json['name'] as String? ?? '', + json['picUrl'] as String? ?? '', + ); + +Map _$FbUserInfoToJson(FbUserInfo instance) => + { + 'player_id': instance.id, + 'user_id': instance.userId, + 'name': instance.name, + 'picUrl': instance.picUrl, + }; + +FBContextInfo _$FBContextInfoFromJson(Map json) => + FBContextInfo( + id: json['id'] as String, + type: $enumDecode(_$FBContextTypeEnumMap, json['type']), + ); + +Map _$FBContextInfoToJson(FBContextInfo instance) => + { + 'id': instance.id, + 'type': _$FBContextTypeEnumMap[instance.type]!, + }; + +const _$FBContextTypeEnumMap = { + FBContextType.POST: 'POST', + FBContextType.THREAD: 'THREAD', + FBContextType.GROUP: 'GROUP', + FBContextType.SOLO: 'SOLO', +}; diff --git a/guru_app/packages/guru_fb_game/lib/utils.dart b/guru_app/packages/guru_fb_game/lib/utils.dart new file mode 100644 index 0000000..3111215 --- /dev/null +++ b/guru_app/packages/guru_fb_game/lib/utils.dart @@ -0,0 +1,312 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:js' as js; + +import 'package:guru_fb_game/model/model.dart'; + +class FbGameGlobalUtils { + static Future getFBProfile() { + final completer = Completer(); + + js.context.callMethod('getFBProfileWithUid', [ + (success) => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future shareGame(FbShareData data) { + final completer = Completer(); + + js.context.callMethod('shareGame', [ + json.encode(data.toJson()), + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future inviteUser(FbInviteData data) { + final completer = Completer(); + + js.context.callMethod('inviteUser', [ + json.encode(data.toJson()), + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Map getEntryPointData() { + final result = js.context.callMethod('getEntryPointData'); + return json.decode(result); + } + + static String getLocale() { + final result = js.context.callMethod('getLocale'); + return result ?? ""; + } + + static String getPlatform() { + String result = "web"; + result = js.context.callMethod('getPlatform'); + return result; + } +} + +class FbGamePlayerUtils { + static void saveData(Map data) { + js.context.callMethod('fbSetData', [json.encode(data)]); + } + + static Future getData(List keys) { + final completer = Completer(); + + js.context.callMethod('fbGetData', [ + json.encode(keys), + (success) => completer.complete(json.decode(success)), + (error) => completer.completeError(json.decode(error)), + ]); + + return completer.future; + } + + //获取当前环境下的用户信息,包括用户id,用户名称,用户头像 + Future getPlayersAsync() async { + final completer = Completer(); + js.context.callMethod('getPlayersAsync', [ + (value) { + List> players = json.decode(value); + final users = players.map((e) => FbUserInfo.fromJson(e)).toList(); + completer.complete(users); + }, + (error) => completer.completeError(json.decode(error)), + ]); + return completer.future; + } + + /** + * 获取玩家同玩好友的信息 + * 返回的值是数组 + */ + Future getConnectedPlayersAsync() { + final completer = Completer(); + js.context.callMethod('getConnectedPlayersAsync', [ + (value) { + List> players = json.decode(value); + final users = players.map((e) => FbUserInfo.fromJson(e)).toList(); + completer.complete(users); + }, + (error) => completer.completeError(json.decode(error)), + ]); + return completer.future; + } + + static Future subscribeBot() { + final completer = Completer(); + + js.context.callMethod('subscribeBot', [ + () => completer.complete(), + (error) => completer.completeError(json.decode(error)), + ]); + + return completer.future; + } +} + +class FbGameTournamentUtils { + static Future createTournament(num initialScore, FbTournamentConfig config, Map data) { + final completer = Completer(); + + js.context.callMethod('createTournament', [ + initialScore, + json.encode(config.toJson()), + json.encode(data), + (value) => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future postTournamentScore(String id, num score) { + final completer = Completer(); + + js.context.callMethod('postTournamentScore', [ + id, + score, + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future shareTournament(num score, Map data) { + final completer = Completer(); + + js.context.callMethod('shareTournament', [ + score, + json.encode(data), + (value) => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future getCurrentTournament() { + final completer = Completer(); + + js.context.callMethod('getCurrentTournament', [ + (success) => completer.complete(json.decode(success)), + (error) => completer.completeError(error), + ]); + + // success 返回 map + // { + // id: tournament.getContextID(), + // endTime: tournament.getEndTime(), + // title: tournament.getTitle(), + // payLoad: tournament.getPayload() + // } + + return completer.future; + } +} + +class FbGameContextUtils { + static Future createAsync(String playerId) { + final completer = Completer(); + js.context.callMethod('createAsync', [ + playerId, + (value) => completer.complete(), + (error) => completer.completeError(json.decode(error)), + ]); + return completer.future; + } + + static void updateAsync(String img) { + js.context.callMethod('updateAsync', [img]); + } + + static FBContextInfo getContext() { + final result = js.context.callMethod('getContextInfo'); + return FBContextInfo.fromJson(json.decode(result)); + } + + static Future contextSwitchAsync(String contextId) { + final completer = Completer(); + js.context.callMethod('contextSwitchAsync', [ + contextId, + () => completer.complete(), + (error) => completer.completeError(json.decode(error)), + ]); + return completer.future; + } + + //切换当前环境,如果选择朋友,则可以获取到朋友的信息 + static Future chooseContext() async { + final completer = Completer(); + js.context.callMethod('chooseContext', [ + (value) => completer.complete(FBContextInfo.fromJson(json.decode(value))), + (error) => completer.completeError(json.decode(error)), + ]); + return completer.future; + } +} + +class FbGameLogUtils { + static void fbLogEvent(String eventName, int valueToSum, Map params) { + js.context.callMethod('logEvent', [eventName, valueToSum, json.encode(params)]); + } + + static void initGuruLogEvent(InitGuruLogEventData data) { + js.context.callMethod('initEventLogger', [json.encode(data.toJson())]); + } + + static void guruLogEvent(String eventName, Map params, Map properties) { + js.context.callMethod('castboxLogEvent', [eventName, json.encode(params), json.encode(properties)]); + } +} + +class FbGameAdUtils { + static List checkSupportedAds() { + final String listStr = js.context.callMethod("checkSupportedAds"); + List list = json.decode(listStr); + return list; + } + + static Future showBanner(String id) { + final completer = Completer(); + + js.context.callMethod('showBannerAds', [ + id, + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future hideBanner(String id) { + final completer = Completer(); + + js.context.callMethod('hideBannerAds', [ + id, + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future loadInterstitial(String id) { + final completer = Completer(); + + js.context.callMethod('loadInterstitial', [ + id, + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future showInterstitial() { + final completer = Completer(); + + js.context.callMethod('showInterstitial', [ + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future loadRewarded(String id) { + final completer = Completer(); + + js.context.callMethod('loadRewarded', [ + id, + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } + + static Future showRewarded() { + final completer = Completer(); + + js.context.callMethod('showRewarded', [ + () => completer.complete(), + (error) => completer.completeError(error), + ]); + + return completer.future; + } +} \ No newline at end of file diff --git a/guru_app/packages/guru_fb_game/pubspec.yaml b/guru_app/packages/guru_fb_game/pubspec.yaml new file mode 100644 index 0000000..2e5894f --- /dev/null +++ b/guru_app/packages/guru_fb_game/pubspec.yaml @@ -0,0 +1,56 @@ +name: guru_fb_game +description: "A new Flutter package project." +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + build_runner: 2.4.7 + json_serializable: 6.7.1 + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/packages/guru_fb_game/test/guru_fb_game_test.dart b/guru_app/packages/guru_fb_game/test/guru_fb_game_test.dart new file mode 100644 index 0000000..53a62f6 --- /dev/null +++ b/guru_app/packages/guru_fb_game/test/guru_fb_game_test.dart @@ -0,0 +1,9 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:guru_fb_game/guru_fb_game.dart'; + +void main() { + test('adds one to input values', () { + + }); +} diff --git a/guru_app/packages/guru_login/.gitignore b/guru_app/packages/guru_login/.gitignore new file mode 100644 index 0000000..96486fd --- /dev/null +++ b/guru_app/packages/guru_login/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/guru_app/packages/guru_login/CHANGELOG.md b/guru_app/packages/guru_login/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/guru_app/packages/guru_login/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/guru_app/packages/guru_login/LICENSE b/guru_app/packages/guru_login/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/guru_app/packages/guru_login/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/guru_app/packages/guru_login/README.md b/guru_app/packages/guru_login/README.md new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/guru_app/packages/guru_login/README.md @@ -0,0 +1,39 @@ + + +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. diff --git a/guru_app/packages/guru_login/analysis_options.yaml b/guru_app/packages/guru_login/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/guru_app/packages/guru_login/analysis_options.yaml @@ -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 diff --git a/guru_app/packages/guru_login/lib/data/account_credentials.dart b/guru_app/packages/guru_login/lib/data/account_credentials.dart new file mode 100644 index 0000000..2a297ee --- /dev/null +++ b/guru_app/packages/guru_login/lib/data/account_credentials.dart @@ -0,0 +1,383 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_login_facebook/flutter_login_facebook.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; + +part 'account_credentials.g.dart'; + +class AccountCredentials {} + +@JsonSerializable(explicitToJson: true) +class AppleCredential extends Credential { + /// An identifier associated with the authenticated user. + /// + /// This will always be provided on iOS and macOS systems. On Android, however, this will not be present. + /// This will stay the same between sign ins, until the user deauthorizes your App. + /// + /// Can be `null` + @JsonKey(name: 'user_identifier') + final String? userIdentifier; + + /// The users given name, in case it was requested. + /// You will need to provide the [AppleIDAuthorizationScopes.fullName] scope to the [AppleIDAuthorizationRequest] for requesting this information. + /// + /// This information will only be provided on the first authorizations. + /// Upon further authorizations, you will only get the [userIdentifier], + /// meaning you will need to store this data securely on your servers. + /// For more information see: https://forums.developer.apple.com/thread/121496 + /// + /// Can be `null` + @JsonKey(name: 'given_name') + final String? givenName; + + /// The users family name, in case it was requested. + /// You will need to provide the [AppleIDAuthorizationScopes.fullName] scope to the [AppleIDAuthorizationRequest] for requesting this information. + /// + /// This information will only be provided on the first authorizations. + /// Upon further authorizations, you will only get the [userIdentifier], + /// meaning you will need to store this data securely on your servers. + /// For more information see: https://forums.developer.apple.com/thread/121496 + /// + /// Can be `null` + @JsonKey(name: 'family_name') + final String? familyName; + + /// The users email in case it was requested. + /// You will need to provide the [AppleIDAuthorizationScopes.email] scope to the [AppleIDAuthorizationRequest] for requesting this information. + /// + /// This information will only be provided on the first authorizations. + /// Upon further authorizations, you will only get the [userIdentifier], + /// meaning you will need to store this data securely on your servers. + /// For more information see: https://forums.developer.apple.com/thread/121496 + /// + /// Can be `null` + @JsonKey(name: 'email') + final String? email; + + /// The verification code for the current authorization. + /// + /// This code should be used by your server component to validate the authorization with Apple within 5 minutes upon receiving it. + @JsonKey(name: 'authorization_code') + final String authorizationCode; + + /// A JSON Web Token (JWT) that securely communicates information about the user to your app. + /// + /// Can be `null`. + @JsonKey(name: 'identity_token') + final String? identityToken; + + /// The `state` parameter that was passed to the request. + /// + /// This data is not modified by Apple. + /// + /// Can be `null` + @JsonKey(name: 'state') + final String? state; + + AppleCredential( + {required this.authorizationCode, + this.userIdentifier, + this.givenName, + this.familyName, + this.email, + this.identityToken, + this.state}); + + factory AppleCredential.fromJson(Map json) => _$AppleCredentialFromJson(json); + + Map toJson() => _$AppleCredentialToJson(this); + + static FutureOr build(AuthorizationCredentialAppleID credential) { + return AppleCredential( + authorizationCode: credential.authorizationCode, + userIdentifier: credential.userIdentifier, + givenName: credential.givenName, + familyName: credential.familyName, + email: credential.email, + identityToken: credential.identityToken, + state: credential.state); + } + + @override + String toString() { + return 'AppleCredential{userIdentifier: $userIdentifier, givenName: $givenName, familyName: $familyName, email: $email, authorizationCode: $authorizationCode, identityToken: $identityToken, state: $state}'; + } + + @override + String get token => authorizationCode; + + @override + AuthType get authType => AuthType.apple; +} + +@JsonSerializable() +class FacebookPictureData { + @JsonKey(name: 'width') + final int width; + + @JsonKey(name: 'height') + final int height; + + @JsonKey(name: 'is_silhouette', defaultValue: false) + final bool isSilhouette; + + @JsonKey(name: 'url', defaultValue: '') + final String url; + + FacebookPictureData({this.width = 0, this.height = 0, this.isSilhouette = false, this.url = ""}); + + @override + String toString() { + return '${width}x$height $url}'; + } + + factory FacebookPictureData.fromJson(Map json) => + _$FacebookPictureDataFromJson(json); + + Map toJson() => _$FacebookPictureDataToJson(this); +} + +@JsonSerializable() +class FacebookPicture { + @JsonKey(name: 'data') + final FacebookPictureData? data; + + FacebookPicture({this.data}); + + @override + String toString() { + return '[FP] $data'; + } + + factory FacebookPicture.fromJson(Map json) => _$FacebookPictureFromJson(json); + + Map toJson() => _$FacebookPictureToJson(this); +} + +@JsonSerializable() +class FacebookProfile { + @JsonKey(name: 'id') + final String? uid; + + @JsonKey(name: 'name') + final String? displayName; + + @JsonKey(name: 'first_name') + final String? firstName; + + @JsonKey(name: 'last_name') + final String? lastName; + + @JsonKey(name: 'email') + final String? email; + + @JsonKey(name: 'phoneNumber') + final String? phoneNumber; + + @JsonKey(name: 'gender') + final String? gender; + + @JsonKey(name: 'picture') + final FacebookPicture? picture; + + FacebookProfile( + {this.uid, + this.displayName, + this.firstName, + this.lastName, + this.gender, + this.email, + this.phoneNumber, + this.picture}); + + factory FacebookProfile.fromJson(Map json) => _$FacebookProfileFromJson(json); + + String get pictureUrl => picture?.data?.url ?? ""; + + Map toJson() => _$FacebookProfileToJson(this); + + @override + String toString() { + return 'FacebookProfile{uid: $uid, displayName: $displayName, firstName: $firstName, lastName: $lastName, email: $email, phoneNumber: $phoneNumber, gender: $gender, picture: $picture}'; + } +} + +class FacebookAccessTokenConvert + implements JsonConverter> { + const FacebookAccessTokenConvert(); + + @override + FacebookAccessToken fromJson(Map json) { + try { + return FacebookAccessToken.fromMap(json); + } catch (error) { + return FacebookAccessToken.fromMap({}); + } + } + + @override + Map toJson(FacebookAccessToken accessToken) { + return accessToken.toMap() ?? {}; + } +} + +const facebookAccessTokenConvert = FacebookAccessTokenConvert(); + +@JsonSerializable(explicitToJson: true) +class FacebookCredential extends Credential { + @JsonKey(name: 'facebook_access_token') + @facebookAccessTokenConvert + final FacebookAccessToken accessToken; + + @override + String get token => accessToken.token; + + @override + AuthType get authType => AuthType.facebook; + + FacebookCredential({required this.accessToken}); + + factory FacebookCredential.fromJson(Map json) => + _$FacebookCredentialFromJson(json); + + Map toJson() => _$FacebookCredentialToJson(this); + + static FutureOr build(FacebookAccessToken accessToken) { + return FacebookCredential(accessToken: accessToken); + } + + @override + String toString() { + return 'FacebookMetadata{accessToken: $accessToken}'; + } +} + +@JsonSerializable() +class GoogleProfile { + @JsonKey(name: 'id') + final String? uid; + + @JsonKey(name: 'name') + final String? displayName; + + @JsonKey(name: 'first_name') + final String? firstName; + + @JsonKey(name: 'last_name') + final String? lastName; + + @JsonKey(name: 'email') + final String? email; + + @JsonKey(name: 'phone_number') + final String? phoneNumber; + + @JsonKey(name: 'gender') + final String? gender; + + @JsonKey(name: 'photoUrl') + final String? photoUrl; + + GoogleProfile( + {this.uid, + this.displayName, + this.firstName, + this.lastName, + this.gender, + this.email, + this.phoneNumber, + this.photoUrl}); + + factory GoogleProfile.fromJson(Map json) => _$GoogleProfileFromJson(json); + + Map toJson() => _$GoogleProfileToJson(this); + + @override + String toString() { + return 'GoogleProfile{uid: $uid, displayName: $displayName, firstName: $firstName, lastName: $lastName, email: $email, phoneNumber: $phoneNumber, gender: $gender, photoUrl: $photoUrl}'; + } +} + +@JsonSerializable() +class GoogleAuth { + @JsonKey(name: 'id_token') + String idToken; + + @JsonKey(name: 'access_token') + String? accessToken; + + @JsonKey(name: 'server_auth_code') + String? serverAuthCode; + + GoogleAuth({required this.idToken, this.accessToken, this.serverAuthCode}); + + factory GoogleAuth.fromJson(Map json) => _$GoogleAuthFromJson(json); + + Map toJson() => _$GoogleAuthToJson(this); + + @override + String toString() { + return 'GoogleAuth{idToken: $idToken, accessToken: $accessToken, serverAuthCode: $serverAuthCode}'; + } +} + +@JsonSerializable(explicitToJson: true) +class GoogleCredential extends Credential { + @JsonKey(name: 'google_auth') + final GoogleAuth auth; + + @JsonKey(name: 'google_profile') + final GoogleProfile? profile; + + String get idToken => auth.idToken; + + String? get accessToken => auth.accessToken; + + String? get serverAuthCode => auth.serverAuthCode; + + String? get uid => profile?.uid; + + String? get email => profile?.email; + + String? get name => profile?.displayName; + + GoogleCredential({required this.auth, this.profile}); + + @override + String get token => idToken; + + @override + AuthType get authType => AuthType.google; + + factory GoogleCredential.fromJson(Map json) => _$GoogleCredentialFromJson(json); + + Map toJson() => _$GoogleCredentialToJson(this); + + static FutureOr build(GoogleSignInAccount account) async { + final googleAuth = await account.authentication; + + GoogleAuth auth = GoogleAuth( + idToken: googleAuth.idToken!, + accessToken: googleAuth.accessToken, + serverAuthCode: account.serverAuthCode); + + GoogleProfile profile = GoogleProfile( + uid: account.id, + displayName: account.displayName, + email: account.email, + firstName: account.displayName, + lastName: account.displayName, + photoUrl: account.photoUrl, + ); + return GoogleCredential(auth: auth, profile: profile); + } + + @override + String toString() { + return 'GoogleMetadata{auth: $auth, profile: $profile}'; + } +} diff --git a/guru_app/packages/guru_login/lib/data/account_credentials.g.dart b/guru_app/packages/guru_login/lib/data/account_credentials.g.dart new file mode 100644 index 0000000..e592e0b --- /dev/null +++ b/guru_app/packages/guru_login/lib/data/account_credentials.g.dart @@ -0,0 +1,130 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'account_credentials.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppleCredential _$AppleCredentialFromJson(Map json) => AppleCredential( + authorizationCode: json['authorization_code'] as String, + userIdentifier: json['user_identifier'] as String?, + givenName: json['given_name'] as String?, + familyName: json['family_name'] as String?, + email: json['email'] as String?, + identityToken: json['identity_token'] as String?, + state: json['state'] as String?, + ); + +Map _$AppleCredentialToJson(AppleCredential instance) => { + 'user_identifier': instance.userIdentifier, + 'given_name': instance.givenName, + 'family_name': instance.familyName, + 'email': instance.email, + 'authorization_code': instance.authorizationCode, + 'identity_token': instance.identityToken, + 'state': instance.state, + }; + +FacebookPictureData _$FacebookPictureDataFromJson(Map json) => FacebookPictureData( + width: json['width'] as int? ?? 0, + height: json['height'] as int? ?? 0, + isSilhouette: json['is_silhouette'] as bool? ?? false, + url: json['url'] as String? ?? '', + ); + +Map _$FacebookPictureDataToJson(FacebookPictureData instance) => { + 'width': instance.width, + 'height': instance.height, + 'is_silhouette': instance.isSilhouette, + 'url': instance.url, + }; + +FacebookPicture _$FacebookPictureFromJson(Map json) => FacebookPicture( + data: json['data'] == null + ? null + : FacebookPictureData.fromJson(json['data'] as Map), + ); + +Map _$FacebookPictureToJson(FacebookPicture instance) => { + 'data': instance.data, + }; + +FacebookProfile _$FacebookProfileFromJson(Map json) => FacebookProfile( + uid: json['id'] as String?, + displayName: json['name'] as String?, + firstName: json['first_name'] as String?, + lastName: json['last_name'] as String?, + gender: json['gender'] as String?, + email: json['email'] as String?, + phoneNumber: json['phoneNumber'] as String?, + picture: json['picture'] == null + ? null + : FacebookPicture.fromJson(json['picture'] as Map), + ); + +Map _$FacebookProfileToJson(FacebookProfile instance) => { + 'id': instance.uid, + 'name': instance.displayName, + 'first_name': instance.firstName, + 'last_name': instance.lastName, + 'email': instance.email, + 'phoneNumber': instance.phoneNumber, + 'gender': instance.gender, + 'picture': instance.picture, + }; + +FacebookCredential _$FacebookCredentialFromJson(Map json) => FacebookCredential( + accessToken: facebookAccessTokenConvert + .fromJson(json['facebook_access_token'] as Map), + ); + +Map _$FacebookCredentialToJson(FacebookCredential instance) => { + 'facebook_access_token': facebookAccessTokenConvert.toJson(instance.accessToken), + }; + +GoogleProfile _$GoogleProfileFromJson(Map json) => GoogleProfile( + uid: json['id'] as String?, + displayName: json['name'] as String?, + firstName: json['first_name'] as String?, + lastName: json['last_name'] as String?, + gender: json['gender'] as String?, + email: json['email'] as String?, + phoneNumber: json['phone_number'] as String?, + photoUrl: json['photoUrl'] as String?, + ); + +Map _$GoogleProfileToJson(GoogleProfile instance) => { + 'id': instance.uid, + 'name': instance.displayName, + 'first_name': instance.firstName, + 'last_name': instance.lastName, + 'email': instance.email, + 'phone_number': instance.phoneNumber, + 'gender': instance.gender, + 'photoUrl': instance.photoUrl, + }; + +GoogleAuth _$GoogleAuthFromJson(Map json) => GoogleAuth( + idToken: json['id_token'] as String, + accessToken: json['access_token'] as String?, + serverAuthCode: json['server_auth_code'] as String?, + ); + +Map _$GoogleAuthToJson(GoogleAuth instance) => { + 'id_token': instance.idToken, + 'access_token': instance.accessToken, + 'server_auth_code': instance.serverAuthCode, + }; + +GoogleCredential _$GoogleCredentialFromJson(Map json) => GoogleCredential( + auth: GoogleAuth.fromJson(json['google_auth'] as Map), + profile: json['google_profile'] == null + ? null + : GoogleProfile.fromJson(json['google_profile'] as Map), + ); + +Map _$GoogleCredentialToJson(GoogleCredential instance) => { + 'google_auth': instance.auth.toJson(), + 'google_profile': instance.profile?.toJson(), + }; diff --git a/guru_app/packages/guru_login/lib/data/account_model.dart b/guru_app/packages/guru_login/lib/data/account_model.dart new file mode 100644 index 0000000..4b9e9be --- /dev/null +++ b/guru_app/packages/guru_login/lib/data/account_model.dart @@ -0,0 +1,42 @@ +// class AuthType { +// static const String apple = "apple"; +// static const String google = "google"; +// static const String facebook = "facebook"; +// static const String anonymous = "anonymous"; +// } +// +// enum AuthCode { success, conflict, error } +// +// String authTypeName(String authType) { +// switch (authType) { +// case AuthType.facebook: +// return "Facebook"; +// case AuthType.google: +// return "Google"; +// case AuthType.apple: +// return "Apple"; +// default: +// return "Anonymous"; +// } +// } +// +// class AuthResult { +// final AuthCode code; +// final dynamic errorCause; +// final dynamic credential; +// +// bool get isSuccess => code == AuthCode.success; +// +// AuthResult.success(this.credential) +// : code = AuthCode.success, +// errorCause = null; +// +// AuthResult.error(this.errorCause) +// : code = AuthCode.error, +// credential = null; +// +// @override +// String toString() { +// return 'AuthResult{code: $code, credential: $credential, errorCause: $errorCause}'; +// } +// } diff --git a/guru_app/packages/guru_login/lib/guru_login.dart b/guru_app/packages/guru_login/lib/guru_login.dart new file mode 100644 index 0000000..88371b7 --- /dev/null +++ b/guru_app/packages/guru_login/lib/guru_login.dart @@ -0,0 +1,154 @@ +library guru_login; + +import 'dart:convert'; + +import 'package:flutter_login_facebook/flutter_login_facebook.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:guru_login/data/account_credentials.dart'; +import 'package:guru_login/data/account_model.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/auth/auth_credential_manager.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; + +class _FacebookAuthCredential extends AuthCredentialDelegate { + FacebookLogin? _facebook; + + FacebookLogin get facebook => (_facebook ??= FacebookLogin()); + + @override + AuthType get authType => throw UnimplementedError(); + + @override + Future login() async { + if (await facebook.isLoggedIn) { + await facebook.logOut(); + } + final result = await facebook.logIn( + permissions: [FacebookPermission.publicProfile, FacebookPermission.email], + customPermissions: ['gaming_profile', 'gaming_user_picture']); + Log.d("signInWithFacebook: ${result.status} ${result.accessToken} ${result.error.toString()}"); + switch (result.status) { + case FacebookLoginStatus.success: + final FacebookAccessToken accessToken = result.accessToken!; + Log.d(''' + [Facebook] Logged in! + + Token: ${accessToken.token} + User id: ${accessToken.userId} + Expires: ${accessToken.expires} + Permissions: ${accessToken.permissions} + Declined permissions: ${accessToken.declinedPermissions} + ''', tag: "facebook_login"); + return AuthResult.success(FacebookCredential(accessToken: accessToken)); + case FacebookLoginStatus.cancel: + Log.d('Login cancelled by the user.', tag: "facebook_login"); + break; + case FacebookLoginStatus.error: + Log.d( + 'Something went wrong with the login process.\n' + 'Here\'s the error Facebook gave us: ${result.error.toString()}', + tag: "facebook_login"); + break; + default: + } + return AuthResult.error("[${result.status}]"); + } + + @override + Future logout() async { + facebook.logOut(); + } + + @override + Credential deserializeCredential(String data) { + final map = json.decode(data); + return FacebookCredential.fromJson(map); + } +} + +class _GoogleAuthCredential extends AuthCredentialDelegate { + GoogleSignIn? _google; + + GoogleSignIn get google => (_google ??= GoogleSignIn()); + + @override + AuthType get authType => throw UnimplementedError(); + + @override + Future login() async { + final googleAccount = await google.signIn(); + + if (googleAccount != null) { + final googleCredential = await GoogleCredential.build(googleAccount); + Log.d(''' + [Google] Logged in! + IdToken: ${googleCredential.idToken} + AccessToken: ${googleCredential.accessToken} + ServerAuthCode: ${googleCredential.serverAuthCode} + User id: ${googleCredential.uid} + Name: ${googleCredential.name} + Email: ${googleCredential.email} + ''', tag: "google_login"); + return AuthResult.success(googleCredential); + } + return AuthResult.error("Google login failed"); + } + + @override + Future logout() async { + google.signOut(); + } + + @override + Credential deserializeCredential(String data) { + final map = json.decode(data); + return GoogleCredential.fromJson(map); + } +} + +class _AppleAuthCredential extends AuthCredentialDelegate { + @override + AuthType get authType => throw UnimplementedError(); + + @override + Future login() async { + final credential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + ); + Log.d("credential: ${credential.authorizationCode}"); + Log.d(credential.toString()); + + final appleCredential = await AppleCredential.build(credential); + Log.d(''' + [Apple] Logged in! + UserIdentifier: ${appleCredential.userIdentifier} + GivenName: ${appleCredential.givenName} + FamilyName: ${appleCredential.familyName} + AuthorizationCode: ${appleCredential.authorizationCode} + IdentityToken: ${appleCredential.identityToken} + Email: ${appleCredential.email} + State: ${appleCredential.state} + ''', tag: "apple_login"); + return AuthResult.success(appleCredential); + } + + @override + Future logout() async {} + + @override + Credential deserializeCredential(String data) { + final map = json.decode(data); + return AppleCredential.fromJson(map); + } +} + +class GuruLogin { + static List supportedAuthCredentialDelegates = [ + _FacebookAuthCredential(), + _GoogleAuthCredential(), + _AppleAuthCredential(), + ]; +} diff --git a/guru_app/packages/guru_login/pubspec.yaml b/guru_app/packages/guru_login/pubspec.yaml new file mode 100644 index 0000000..9909ab4 --- /dev/null +++ b/guru_app/packages/guru_login/pubspec.yaml @@ -0,0 +1,69 @@ +name: guru_login +description: A new Flutter project. +version: 2.3.0 +homepage: + +environment: + sdk: '>=2.18.6 <3.0.0' + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + + flutter_login_facebook: 1.9.0 + google_sign_in: 6.1.4 + sign_in_with_apple: 4.3.0 + + guru_utils: + git: + url: git@git.chengdu.pundit.company:castbox/guru_sdk.git + path: guru_app/packages/guru_utils + ref: v2.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + json_serializable: 6.5.4 + +dependency_overrides: + guru_utils: + path: ../guru_utils + build_runner: 2.3.3 +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/guru_app/packages/guru_login/test/guru_login_test.dart b/guru_app/packages/guru_login/test/guru_login_test.dart new file mode 100644 index 0000000..c8a00ba --- /dev/null +++ b/guru_app/packages/guru_login/test/guru_login_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:guru_login/guru_login.dart'; + +void main() { + +} diff --git a/guru_app/packages/guru_spec/lib/src/guru_spec_generator.dart b/guru_app/packages/guru_spec/lib/src/guru_spec_generator.dart index 23b1093..f06649e 100644 --- a/guru_app/packages/guru_spec/lib/src/guru_spec_generator.dart +++ b/guru_app/packages/guru_spec/lib/src/guru_spec_generator.dart @@ -97,6 +97,7 @@ class GuruSpecGenerator extends GeneratorForAnnotation { } codeString.write(_buildGlobalRemoteConfigConstants()); + codeString.write(_buildGlobalExperiments()); codeString.write(_buildGlobalProductIds()); codeString.write(_buildGlobalProductCategoryConstants()); codeString.write(_buildAppSpecFactory()); @@ -277,7 +278,8 @@ class GuruSpecGenerator extends GeneratorForAnnotation { String _buildGlobalRemoteConfigConstants() { final List fieldList = []; - final codeBlocks = [const Code("{")]; + final List methodList = []; + for (var fieldEntry in remoteConfigKeys.entries) { final fieldValue = fieldEntry.key; final fieldName = fieldEntry.value; @@ -298,6 +300,36 @@ class GuruSpecGenerator extends GeneratorForAnnotation { return DartFormatter().format([classBuilder.accept(emitter)].join('\n\n')); } + String _buildGlobalExperiments() { + final List methodList = []; + final codeBlocks = []; + + for (var flavor in flavors) { + codeBlocks.add(Code("if (GuruApp.instance.flavor == '$flavor') {")); + codeBlocks.add( + Code("return _${capitalize(flavor, useCamelCase: true)}ABTestExperiments.experiments;")); + codeBlocks.add(const Code("}")); + } + codeBlocks.add(const Code("return {};")); + + methodList.add(Method((method) { + method + ..name = "experiments" + ..static = true + ..type = MethodType.getter + ..returns = refer("Map") + ..body = Block.of(codeBlocks); + })); + + final classBuilder = Class((clazz) { + clazz + ..name = 'ABTestExperimentConstants' + ..methods.addAll(methodList); + }); + final emitter = DartEmitter(); + return DartFormatter().format([classBuilder.accept(emitter)].join('\n\n')); + } + String _buildAppSpecFactory() { final creator = []; for (var flavor in flavors) { @@ -366,6 +398,9 @@ class GuruSpecGenerator extends GeneratorForAnnotation { try { final productsClassBuilders = buildProducts(yamlMap['products'] ?? {}, flavor: flavor); + final experimentClassBuilders = + buildExperiments(yamlMap['experiments'] ?? {}, flavor: flavor); + // final productManifestClassBuilder = // _buildProductManifest(flavor, yamlMap['product_manifest'] ?? {}); @@ -383,14 +418,17 @@ class GuruSpecGenerator extends GeneratorForAnnotation { ..assignment = Code("_$className._()") ..modifier = FieldModifier.final$), _buildAppName(yamlMap['app_name']), + _buildAppCategory(yamlMap['app_category'] ?? "game"), _buildFlavor(yamlMap['flavor'] ?? "Main"), _buildAppDetails(yamlMap['details']), _buildDeployment(yamlMap['deployment']), _buildAdsProfile(yamlMap['ads_profile'], yamlMap['details'] ?? {}), _buildProductProfile(flavor: flavor), _buildAdjustProfile(yamlMap['adjust_profile'] ?? {}), - _buildDefaultRemoteConfig(flavor: flavor) + _buildDefaultRemoteConfig(flavor: flavor), + _buildABTestExperiments(flavor: flavor) ]) + ..methods.addAll([_buildGetRemoteConfigKey(flavor: flavor)]) ..extend = refer("AppSpec"); }); final emitter = DartEmitter(); @@ -406,6 +444,8 @@ class GuruSpecGenerator extends GeneratorForAnnotation { codeString .write(DartFormatter().format([productsClassBuilder.accept(emitter)].join('\n\n'))); } + codeString + .write(DartFormatter().format([experimentClassBuilders.accept(emitter)].join('\n\n'))); return codeString.toString(); } catch (error, stacktrace) { log.info("error:$stacktrace"); @@ -424,6 +464,15 @@ class GuruSpecGenerator extends GeneratorForAnnotation { ..modifier = FieldModifier.final$); } + Field _buildAppCategory(String name) { + return Field((field) => field + ..name = "appCategory" + ..static = false + ..assignment = Code('AppCategory.$name') + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + Field _buildFlavor(String flavor) { return Field((field) => field ..name = "flavor" @@ -489,9 +538,14 @@ class GuruSpecGenerator extends GeneratorForAnnotation { "ads_compliant_initialization": "adsCompliantInitialization", "notification_permission_prompt_trigger": "notificationPermissionPromptTrigger", "tracking_notification_permission_pass": "trackingNotificationPermissionPass", - "tracking_notification_permission_pass_limit_times": "trackingNotificationPermissionPassLimitTimes", + "tracking_notification_permission_pass_limit_times": + "trackingNotificationPermissionPassLimitTimes", "allow_interstitial_as_alternative_reward": "allowInterstitialAsAlternativeReward", - "show_internal_ads_when_banner_unavailable": "showInternalAdsWhenBannerUnavailable" + "show_internal_ads_when_banner_unavailable": "showInternalAdsWhenBannerUnavailable", + "subscription_restore_grace_count": "subscriptionRestoreGraceCount", + "fullscreen_ads_min_interval": "fullscreenAdsMinInterval", + "enabled_sync_account_profile": "enabledSyncAccountProfile", + "purchase_event_trigger": "purchaseEventTrigger" }; final RegExp numericOrBoolRegex = RegExp(r'^(true|false|-?\d+)$'); @@ -647,6 +701,32 @@ class GuruSpecGenerator extends GeneratorForAnnotation { ..modifier = FieldModifier.final$); } + Method _buildGetRemoteConfigKey({String flavor = ""}) { + final body = Code("_${capitalize(flavor)}RemoteConfigConstants.getKey(key)"); + return Method((method) { + method + ..name = "getRemoteConfigKey" + ..annotations.add(const CodeExpression(Code('override'))) + ..requiredParameters.addAll([ + Parameter((p) => p + ..type = refer("String") + ..name = 'key') + ]) + ..returns = refer("String") + ..lambda = true + ..body = body; + }); + } + + Field _buildABTestExperiments({String flavor = ""}) { + return Field((field) => field + ..name = "localABTestExperiments" + ..static = false + ..assignment = Code("_${capitalize(flavor, useCamelCase: true)}ABTestExperiments.experiments") + ..annotations.add(const CodeExpression(Code('override'))) + ..modifier = FieldModifier.final$); + } + Field _buildAdjustProfile(Map map) { final codeBlocks = [const Code("AdjustProfile(")]; final appTokenMap = map['app_token']; @@ -819,6 +899,15 @@ class GuruSpecGenerator extends GeneratorForAnnotation { static final Map categoryMethodMap = {}; static final Map categoryFieldNameMap = {}; + static const conditionOpts = { + "eq": "equals", + "gt": "greaterThan", + "gte": "greaterThanOrEquals", + "lt": "lessThan", + "lte": "lessThanOrEquals", + "ne": "notEquals" + }; + List buildProducts(Map map, {String flavor = ""}) { final List regExpField = []; final List fieldList = []; @@ -853,8 +942,10 @@ class GuruSpecGenerator extends GeneratorForAnnotation { final offers = fieldData['offers'] ?? []; final basePlan = fieldData['base_plan'] ?? ''; final group = fieldData['group'] ?? ''; - final points = ((fieldData['points'] ?? false) as bool) && (attr == 'consumable') ? true : false; - log.info("fieldName:$fieldName fieldData:${fieldData['attr']} points:$points(${fieldData['points']})"); + final points = + ((fieldData['points'] ?? false) as bool) && (attr == 'consumable') ? true : false; + log.info( + "fieldName:$fieldName fieldData:${fieldData['attr']} points:$points(${fieldData['points']})"); if (attr == 'subscriptions') { if (group.isEmpty == true) { final todo = @@ -972,7 +1063,8 @@ class GuruSpecGenerator extends GeneratorForAnnotation { ..name = fieldName ..static = true ..assignment = sku.isNotEmpty - ? Code("ProductId.fromSku(sku: '$sku', attr: TransactionAttributes.$attr, points: $points)") + ? Code( + "ProductId.fromSku(sku: '$sku', attr: TransactionAttributes.$attr, points: $points)") : Code( "ProductId(android: '$android', ios: '$ios',${basePlan.toString().isNotEmpty ? "basePlan: '$basePlan', " : ''}attr: TransactionAttributes.$attr, points: $points)") ..modifier = FieldModifier.constant)); @@ -1159,6 +1251,123 @@ class GuruSpecGenerator extends GeneratorForAnnotation { ]; } + String toSetString(String str) { + final set = str.split(","); + return set.map((e) => "'$e'").toSet().toString(); + } + + Class buildExperiments(Map map, {String flavor = ""}) { + final List fieldList = []; + final List experimentsCode = [const Code("{")]; + for (var fieldEntry in map.entries) { + final experimentName = fieldEntry.key; + final experimentData = fieldEntry.value; + final start = experimentData['start']; + final end = experimentData['end']; + final audience = experimentData['audience']; + + final startDt = DateTime.parse(start); + final endDt = DateTime.parse(end); + + final codeBlocks = [const Code("ABTestExperiment(")]; + codeBlocks.add(Code("name: '$experimentName',")); + codeBlocks.add(Code("startTs: ${startDt.millisecondsSinceEpoch}, // $startDt")); + codeBlocks.add(Code("endTs: ${endDt.millisecondsSinceEpoch}, // $endDt")); + + log.info( + "===> start:$start(${DateTime.parse(start).millisecondsSinceEpoch}) end:$end(${DateTime.parse(end)})"); + final audienceCodeBlocks = [const Code("audience: ABTestAudience(")]; + audienceCodeBlocks.add(const Code("filters: [")); + final filters = audience['filters'] as List; + for (var filter in filters) { + log.info("filter:$filter"); + final version = filter['version']; + if (version != null && version is Map) { + final opt = version['opt']; + final mmp = version['mmp']; + if (conditionOpts.containsKey(opt) && mmp != null) { + audienceCodeBlocks.add(Code("VersionFilter.${conditionOpts[opt]}('$mmp'),")); + } + } + + final newUser = filter['new_user']; + if (newUser.toString() == "true") { + audienceCodeBlocks.add(const Code("NewUserFilter(),")); + } + + final country = filter['country']; + if (country != null && country is Map) { + final included = country['included'] as String?; + final excluded = country['excluded'] as String?; + if (included != null && included.isNotEmpty) { + audienceCodeBlocks.add(Code("CountryFilter.included(${toSetString(included)}),")); + } else if (excluded != null && excluded.isNotEmpty) { + audienceCodeBlocks.add(Code("CountryFilter.excluded(${toSetString(excluded)}),")); + } + } + + final platform = filter['platform']; + if (platform != null && platform is Map) { + audienceCodeBlocks.add(const Code("PlatformFilter(")); + + final androidCondition = platform["android"]; + if (androidCondition != null && androidCondition is Map) { + final opt = androidCondition['opt']; + final sdkInt = androidCondition['ver']; + if (conditionOpts.containsKey(opt) && sdkInt != null) { + audienceCodeBlocks.add(Code( + "androidCondition: AndroidCondition(opt: ConditionOpt.${conditionOpts[opt]}, sdkInt:$sdkInt),")); + } + } + + final iosCondition = platform["ios"]; + if (iosCondition != null && iosCondition is Map) { + final opt = iosCondition['opt']; + final version = iosCondition['ver']; + if (conditionOpts.containsKey(opt) && version != null) { + audienceCodeBlocks.add(Code( + "iosCondition: IosCondition(opt: ConditionOpt.${conditionOpts[opt]}, version:$version),")); + } + } + audienceCodeBlocks.add(const Code("),")); + } + } + audienceCodeBlocks.add(const Code("],")); + audienceCodeBlocks.add(Code("variant: ${audience['variant']})")); + + codeBlocks.addAll(audienceCodeBlocks); + codeBlocks.add(const Code(")")); + + final fieldName = camelCase(experimentName); + if (fieldName != '') { + fieldList.add(Field( + (field) => field + ..name = fieldName + ..static = true + ..assignment = Block.of(codeBlocks) + ..modifier = FieldModifier.final$, + )); + // codeBlocks.add(Code("$fieldName: '$fieldData',")); + experimentsCode.add(Code("'$experimentName': $fieldName,")); + } + } + experimentsCode.add(const Code("}")); + + fieldList.add(Field( + (field) => field + ..name = "experiments" + ..static = true + ..assignment = Block.of(experimentsCode) + ..modifier = FieldModifier.final$, + )); + + return Class((clazz) { + clazz + ..name = '_${capitalize(flavor, useCamelCase: true)}ABTestExperiments' + ..fields.addAll(fieldList); + }); + } + static final Map> validateSameCategoryManifestMap = {}; Method _buildProductManifestMethod(String flavor, String productName, String category, String sku, @@ -1185,6 +1394,7 @@ class GuruSpecGenerator extends GeneratorForAnnotation { } detailsBlocks.add(const Code("final details =
[];")); extrasBlocks.add(const Code("final extras = {")); + extrasBlocks.add(const Code("ExtraReservedField.contentId: intent.productId.sku,")); extrasBlocks.add(const Code("ExtraReservedField.scene: intent.scene,")); extrasBlocks.add(const Code("ExtraReservedField.rate: intent.rate,")); extrasBlocks.add(const Code("ExtraReservedField.sales: intent.sales,")); @@ -1193,6 +1403,7 @@ class GuruSpecGenerator extends GeneratorForAnnotation { if (detailsExp.hasMatch(data.key)) { final details = data.value as Map; final type = details.remove("type"); + String sku = details.remove("sku") ?? type; final amount = details.remove("amount"); if (type == null || amount == null) { const todo = 'Please define `type` or `amount` field to guru_spec.yaml.'; @@ -1211,11 +1422,20 @@ class GuruSpecGenerator extends GeneratorForAnnotation { ); } + final filledMatches = fillParamsExp.allMatches(sku).toList(); + if (filledMatches.isNotEmpty) { + for (var idx = 0; idx < filledMatches.length; ++idx) { + sku = sku.replaceAllMapped(fillParamsExp, (match) { + return "\${matches.first.group(${match.group(1)})!}"; + }); + } + } + if (ignoreSales == true) { - detailsBlocks.add(Code("details.add(Details.define('$type', $amount)")); + detailsBlocks.add(Code("details.add(Details.define('$type', $amount, sku: '$sku')")); } else { detailsBlocks.add(Code( - "details.add(Details.define('$type', intent.sales ? max($amount, (intent.rate * $amount).toInt()) : $amount)")); + "details.add(Details.define('$type', intent.sales ? max($amount, (intent.rate * $amount).toInt()) : $amount, sku: '$sku')")); } for (var detail in details.entries) { final fieldData = detail.value; @@ -1293,7 +1513,7 @@ class GuruSpecGenerator extends GeneratorForAnnotation { if (filledMatches.isNotEmpty) { for (var idx = 0; idx < filledMatches.length; ++idx) { final match = filledMatches[idx]; - return "setString('${skuParamsList[idx]}', matches.first.group(${match.group(1)})!)"; + return "setString('$fieldName', matches.first.group(${match.group(1)})!)"; } } else { final matches = typeParamsExp.allMatches(fieldData); @@ -1635,6 +1855,9 @@ class GuruSpecGenerator extends GeneratorForAnnotation { Class buildRemoteConfigConstantsClass(Map map, {String flavor = ""}) { final List fieldList = []; final codeBlocks = [const Code("{")]; + final mapping = map.remove("_mapping"); + Code getKeyBody = const Code("key"); + for (var fieldEntry in map.entries) { final fieldName = camelCase(fieldEntry.key); final fieldData = fieldEntry.value; @@ -1648,6 +1871,7 @@ class GuruSpecGenerator extends GeneratorForAnnotation { remoteConfigKeys[fieldEntry.key] = fieldName; } } + codeBlocks.add(const Code("}")); fieldList.add(Field((field) => field @@ -1657,6 +1881,26 @@ class GuruSpecGenerator extends GeneratorForAnnotation { ..assignment = Block.of(codeBlocks) ..modifier = FieldModifier.constant)); + if (mapping != null && mapping is Map) { + final mappingBlocks = [const Code("{")]; + for (var fieldEntry in mapping.entries) { + final fieldName = fieldEntry.key; + final fieldData = fieldEntry.value; + if (fieldName != '' && fieldData != null) { + mappingBlocks.add(Code("'$fieldName': '$fieldData',")); + } + } + mappingBlocks.add(const Code("}")); + + fieldList.add(Field((field) => field + ..name = "_mapping" + ..static = true + ..type = refer("Map") + ..modifier = FieldModifier.constant + ..assignment = Block.of(mappingBlocks))); + getKeyBody = const Code("_mapping[key] ?? key"); + } + final List methodList = []; methodList.add(Method((method) => method ..name = 'getDefaultConfigString' @@ -1670,6 +1914,18 @@ class GuruSpecGenerator extends GeneratorForAnnotation { ..body = const Code("_defaultConfigs[key]") ..static = true)); + methodList.add(Method((method) => method + ..name = 'getKey' + ..returns = refer('String') + ..lambda = true + ..requiredParameters.addAll([ + Parameter((p) => p + ..type = refer("String") + ..name = "key") + ]) + ..body = getKeyBody + ..static = true)); + return Class((clazz) { clazz ..name = '_${capitalize(flavor)}RemoteConfigConstants' diff --git a/guru_app/packages/guru_utils/lib/auth/auth_credential_manager.dart b/guru_app/packages/guru_utils/lib/auth/auth_credential_manager.dart new file mode 100644 index 0000000..3aa6085 --- /dev/null +++ b/guru_app/packages/guru_utils/lib/auth/auth_credential_manager.dart @@ -0,0 +1,108 @@ +/// +/// AuthCredentialManager is a singleton class that manages the authentication process. +/// It is responsible for delegating the login process to the appropriate [AuthCredentialDelegate]. +/// +enum AuthType { apple, google, facebook, anonymous } + +enum AuthCode { success, conflict, error } + +String getAuthName(AuthType authType) { + switch (authType) { + case AuthType.apple: + return "apple"; + case AuthType.google: + return "google"; + case AuthType.facebook: + return "facebook"; + default: + return "anonymous"; + } +} + +class AuthResult { + final AuthCode code; + final dynamic errorCause; + final Credential? credential; + + bool get isSuccess => code == AuthCode.success; + + AuthResult.success(this.credential) + : code = AuthCode.success, + errorCause = null; + + AuthResult.error(this.errorCause) + : code = AuthCode.error, + credential = null; + + @override + String toString() { + return 'AuthResult{code: $code, credential: $credential, errorCause: $errorCause}'; + } +} + +abstract class Credential { + AuthType get authType; + + String get token; + + bool get isAnonymous => authType == AuthType.anonymous; + + bool get isFacebook => authType == AuthType.facebook; + + bool get isGoogle => authType == AuthType.google; + + bool get isApple => authType == AuthType.apple; +} + +abstract class AuthCredentialDelegate { + AuthType get authType; + + Future login(); + + Future logout(); + + Credential deserializeCredential(String data); + + const AuthCredentialDelegate(); +} + +class AuthCredentialManager { + AuthCredentialManager._(this._delegates); + + static late final AuthCredentialManager _instance; + + static AuthCredentialManager get instance => _instance; + + static void initialize(List delegates) { + _instance = + AuthCredentialManager._({for (var delegate in delegates) delegate.authType: delegate}); + } + + final Map _delegates; + + List get supportedAuthType => _delegates.keys.toList(); + + Future loginWith(AuthType authType) async { + final delegate = _delegates[authType]; + if (delegate == null) { + return AuthResult.error("Delegate not found"); + } + return delegate.login(); + } + + Future logout(AuthType authType) async { + final delegate = _delegates[authType]; + if (delegate == null) { + return AuthResult.error("Delegate not found"); + } + return delegate.logout(); + } + + Credential? deserializeCredential(AuthType authType, String credentialData) { + final delegate = _delegates[authType]; + if (delegate == null) { + return null; + } + return delegate.deserializeCredential(credentialData); + } +} diff --git a/guru_app/packages/guru_utils/lib/controller/ads_controller.dart b/guru_app/packages/guru_utils/lib/controller/ads_controller.dart index 6f69935..0a1e55b 100644 --- a/guru_app/packages/guru_utils/lib/controller/ads_controller.dart +++ b/guru_app/packages/guru_utils/lib/controller/ads_controller.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:guru_utils/ads/ads.dart'; import 'package:guru_utils/ads/data/ads_model.dart'; import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_utils/extensions/extensions.dart'; import 'package:guru_utils/log/log.dart'; import 'package:guru_utils/timer/timer_scheduler.dart'; @@ -31,8 +32,8 @@ class AdsCompleter { } void close() { - Log.d("AdsCompleter success!", tag: "Ads"); - completer.complete(AdsResult.build(adType, AdCause.ignore)); + Log.d("AdsCompleter close with ignore!", tag: "Ads"); + completer.safeComplete(AdsResult.build(adType, AdCause.ignore)); } bool get isCompleted => completer.isCompleted; diff --git a/guru_app/packages/guru_utils/lib/controller/aware/ads/banner_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/ads/banner_aware.dart index 73d6429..ecd06f8 100644 --- a/guru_app/packages/guru_utils/lib/controller/aware/ads/banner_aware.dart +++ b/guru_app/packages/guru_utils/lib/controller/aware/ads/banner_aware.dart @@ -179,9 +179,10 @@ mixin SharedBannerAware on AdsController { bannerAds ??= await AdsManagerDelegate.instance.createBannerAds( scene: currentBannerScene, observer: AdsLifecycleObserverDelegate( - onAdLoadedCallback: onBannerAdLoaded, - onAdLoadFailedCallback: onBannerAdLoadFailed, - onAdClickedCallback: onBannerAdClicked)); + onAdLoadedCallback: onBannerAdLoaded, + onAdLoadFailedCallback: onBannerAdLoadFailed, + onAdClickedCallback: onBannerAdClicked + )); return bannerAds!; } diff --git a/guru_app/packages/guru_utils/lib/controller/aware/ads/rewarded_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/ads/rewarded_aware.dart index 561dcd3..25c0e59 100644 --- a/guru_app/packages/guru_utils/lib/controller/aware/ads/rewarded_aware.dart +++ b/guru_app/packages/guru_utils/lib/controller/aware/ads/rewarded_aware.dart @@ -161,7 +161,7 @@ mixin RewardedAware on AdsController { } continue; } else if (state == AdState.loaded) { - completer.complete(AdCause.success); + completer.safeComplete(AdCause.success); return; } } diff --git a/guru_app/packages/guru_utils/lib/controller/aware/keep_screen_on_aware.dart b/guru_app/packages/guru_utils/lib/controller/aware/keep_screen_on_aware.dart index 9ff2efa..36895e0 100644 --- a/guru_app/packages/guru_utils/lib/controller/aware/keep_screen_on_aware.dart +++ b/guru_app/packages/guru_utils/lib/controller/aware/keep_screen_on_aware.dart @@ -21,19 +21,19 @@ mixin KeepScreenOnAware on LifecycleController { static bool _scheduleTimer(int duration) { final interval = SystemClock.elapsedRealtime().inMilliseconds - keepingAt; final remaining = duration - interval; - Log.d("schedule timer interval: $interval, remaining: $remaining"); + Log.i("schedule timer interval: $interval, remaining: $remaining"); if (remaining < 1000) { checkKeepScreenTimer?.cancel(); checkKeepScreenTimer = null; // 如果这里的 Duration 是大于等于 0 的,表示已经超时或是配置了关闭 if (duration < 0) { - Log.d("Always keepScreenOn, no need to set a timer"); + Log.i("Always keepScreenOn, no need to set a timer"); return true; } _restoreKeepScreenStatus(); return false; } else { - Log.d("schedule new timer remaining: $remaining"); + Log.i("schedule new timer remaining: $remaining"); checkKeepScreenTimer = Timer(Duration(milliseconds: remaining), () { _scheduleTimer(duration); }); @@ -83,7 +83,7 @@ mixin KeepScreenOnAware on LifecycleController { void keep({String? info}) { final elapsed = SystemClock.elapsedRealtime().inMilliseconds; - Log.d("[$screenName] KEEP${info != null ? "($info)" : ""} interval: ${elapsed - keepingAt}"); + Log.i("[$screenName] KEEP${info != null ? "($info)" : ""} interval: ${elapsed - keepingAt}"); keepingAt = elapsed; } diff --git a/guru_app/packages/guru_utils/lib/database/batch/batch_aware.dart b/guru_app/packages/guru_utils/lib/database/batch/batch_aware.dart index c43903a..b57a375 100644 --- a/guru_app/packages/guru_utils/lib/database/batch/batch_aware.dart +++ b/guru_app/packages/guru_utils/lib/database/batch/batch_aware.dart @@ -13,6 +13,8 @@ mixin BatchAware { T? getData(String key) => _subject.value[key]; + bool exists(String key) => _subject.value.containsKey(key); + List get dataList => _subject.value.values.toList() ?? []; bool get isDataEmpty => _subject.value.isEmpty; diff --git a/guru_app/packages/guru_utils/lib/database/database.dart b/guru_app/packages/guru_utils/lib/database/database.dart index ad680e5..c8eda4c 100644 --- a/guru_app/packages/guru_utils/lib/database/database.dart +++ b/guru_app/packages/guru_utils/lib/database/database.dart @@ -1,5 +1,8 @@ import 'dart:io'; +import 'package:dartx/dartx_io.dart'; +import 'package:guru_utils/file/file_utils.dart'; +import 'package:guru_utils/id/id_utils.dart'; import 'package:guru_utils/log/log.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; @@ -11,7 +14,6 @@ export 'package:sqflite/sqflite.dart'; /// part 'migration.dart'; - typedef TableCreator = Future Function(Transaction delegate); abstract class AppDatabase { @@ -55,7 +57,6 @@ abstract class AppDatabase { } }); } - Future initDatabase() async { final isTesting = Platform.environment.containsKey('FLUTTER_TEST'); final dbDir = isTesting ? await getTestingPath() : await getDatabasesPath(); @@ -85,6 +86,53 @@ abstract class AppDatabase { Future runInTransaction(Future Function(Transaction txn) action) { return getDb().transaction(action); } + + Future backup(String token) async { + final uuid = IdUtils.keyUuid(token); + final isTesting = Platform.environment.containsKey('FLUTTER_TEST'); + final dbDir = isTesting ? await getTestingPath() : await getDatabasesPath(); + final dbFile = File(join(dbDir, "$dbName.db")); + final backupDbFile = File(join(dbDir, "$dbName.$uuid.db")); + Log.d("backup dbPath:${dbFile.name} to ${backupDbFile.name}"); + if (await FileUtils.instance.checkFileExists(backupDbFile)) { + await backupDbFile.delete(); + } + await dbFile.copy(backupDbFile.path); + } + + Future restore(String token) async { + final uuid = IdUtils.keyUuid(token); + final isTesting = Platform.environment.containsKey('FLUTTER_TEST'); + final dbDir = isTesting ? await getTestingPath() : await getDatabasesPath(); + final dbFile = File(join(dbDir, "$dbName.db")); + final restoreDbFile = File(join(dbDir, "$dbName.$uuid.db")); + Log.d("restore dbPath:${restoreDbFile.name} to ${dbFile.name}"); + if (await FileUtils.instance.checkFileExists(dbFile)) { + _database.close(); + await dbFile.delete(); + } + + if (await FileUtils.instance.checkFileExists(restoreDbFile) == false) { + Log.w("backupDbFile not exists"); + return false; + } + await restoreDbFile.copy(dbFile.path); + return true; + } + + Future switchSession(String oldToken, String newToken) async { + if (oldToken == newToken) { + Log.w("same uid, no need to switch"); + return false; + } + await getDb().close(); + if (oldToken.isNotEmpty) { + await backup(oldToken); + } + await restore(newToken); + await initDatabase(); + return true; + } } extension DatabaseExt on AppDatabase { diff --git a/guru_app/packages/guru_utils/lib/device/device_utils.dart b/guru_app/packages/guru_utils/lib/device/device_utils.dart index db2f064..8e66dc4 100644 --- a/guru_app/packages/guru_utils/lib/device/device_utils.dart +++ b/guru_app/packages/guru_utils/lib/device/device_utils.dart @@ -28,6 +28,33 @@ class DeviceUtils { static DeviceInfo? mockDeviceInfo; + static AndroidDeviceInfo? cachedAndroidDeviceInfo; + static IosDeviceInfo? cachedIOSDeviceInfo; + + static Future initialize() async { + try { + if (Platform.isAndroid) { + cachedAndroidDeviceInfo ??= (await deviceInfoPlugin.androidInfo); + } else if (Platform.isIOS) { + cachedIOSDeviceInfo ??= (await deviceInfoPlugin.iosInfo); + } + } catch (error, stacktrace) { + Log.w("DeviceInfo initialize error!"); + } + } + + static Future reload() async { + try { + if (Platform.isAndroid) { + cachedAndroidDeviceInfo = (await deviceInfoPlugin.androidInfo); + } else if (Platform.isIOS) { + cachedIOSDeviceInfo = (await deviceInfoPlugin.iosInfo); + } + } catch (error, stacktrace) { + Log.w("DeviceInfo initialize error!"); + } + } + // static Future getDeviceId() async { // String deviceId = ""; // try { @@ -100,6 +127,39 @@ class DeviceUtils { } } + /// ios 返回大版本号 + /// android 返回 SdkInt + /// https://developer.android.com/reference/android/os/Build.VERSION_CODES + static Future getOSVersion() async { + try { + if (Platform.isIOS) { + final info = await DeviceInfoPlugin().iosInfo; + final versions = info.systemVersion.split(".") ?? []; + return int.parse(versions[0]); + } else if (Platform.isAndroid) { + final info = await DeviceInfoPlugin().androidInfo; + return info.version.sdkInt; + } + } catch (error, stacktrace) { + Log.w("getOSVersion error!($error) stacktrace: $stacktrace)"); + } + return -1; + } + + static int peekOSVersion() { + try { + if (Platform.isIOS) { + final versions = cachedIOSDeviceInfo?.systemVersion.split(".") ?? []; + return int.parse(versions[0]); + } else if (Platform.isAndroid) { + return cachedAndroidDeviceInfo?.version.sdkInt ?? -1; + } + } catch (error, stacktrace) { + Log.w("getOSVersion error!($error) stacktrace: $stacktrace)"); + } + return -1; + } + static Future buildDeviceInfo( {required String deviceId, String firebasePushToken = '', String uid = ''}) async { Log.d('device init'); @@ -118,13 +178,13 @@ class DeviceUtils { PackageInfo packageInfo = await PackageInfo.fromPlatform(); if (Platform.isAndroid) { const androidId = AndroidId(); - var build = await deviceInfoPlugin.androidInfo; + var build = cachedAndroidDeviceInfo ?? await deviceInfoPlugin.androidInfo; _androidId = (await androidId.getId()) ?? deviceId; _deviceName = build.model; _brand = build.brand; _deviceType = 'android'; } else if (Platform.isIOS) { - var data = await deviceInfoPlugin.iosInfo; + var data = cachedIOSDeviceInfo ?? await deviceInfoPlugin.iosInfo; _deviceName = data.name ?? ""; _brand = data.model ?? ""; if (DeviceUtils.isTablet()) { @@ -199,7 +259,8 @@ class DeviceUtils { } } - static Future buildContactDeviceInfo({required int firstInstallTime, List? extraMessages}) async { + static Future buildContactDeviceInfo( + {required int firstInstallTime, List? extraMessages}) async { final StringBuffer sb = StringBuffer(); sb.writeln("-----Please type your reply above this line-----"); sb.writeln( diff --git a/guru_app/packages/guru_utils/lib/extensions/async_extension.dart b/guru_app/packages/guru_utils/lib/extensions/async_extension.dart index eceae6b..6eddeeb 100644 --- a/guru_app/packages/guru_utils/lib/extensions/async_extension.dart +++ b/guru_app/packages/guru_utils/lib/extensions/async_extension.dart @@ -27,3 +27,12 @@ extension StreamControllerExtension on StreamController { } } } + +extension FutureExtension on Completer { + void safeComplete([FutureOr? value]) { + if (isCompleted) { + return; + } + complete(value); + } +} diff --git a/guru_app/packages/guru_utils/lib/extensions/list_extension.dart b/guru_app/packages/guru_utils/lib/extensions/list_extension.dart index 9d9437d..953588c 100644 --- a/guru_app/packages/guru_utils/lib/extensions/list_extension.dart +++ b/guru_app/packages/guru_utils/lib/extensions/list_extension.dart @@ -19,7 +19,7 @@ extension ListExtension on Iterable { return filtered; } - T? firstWhereOrNull(bool test(T element), {T? orElse()?}) { + T? firstWhereOrNull(bool Function(T element) test, {T? Function()? orElse}) { for (T element in this) { if (test(element)) return element; } @@ -30,5 +30,42 @@ extension ListExtension on Iterable { T? firstOrNull() { return isNotEmpty ? first : null; } - + + /// The first element whose value and index satisfies [test]. + /// + /// Returns `null` if there are no element and index satisfying [test]. + T? firstWhereIndexedOrNull(bool Function(int index, T element) test) { + var index = 0; + for (var element in this) { + if (test(index++, element)) return element; + } + return null; + } + + /// The last element satisfying [test], or `null` if there are none. + T? lastWhereOrNull(bool Function(T element) test) { + T? result; + for (var element in this) { + if (test(element)) result = element; + } + return result; + } + + /// The last element whose index and value satisfies [test]. + /// + /// Returns `null` if no element and index satisfies [test]. + T? lastWhereIndexedOrNull(bool Function(int index, T element) test) { + T? result; + var index = 0; + for (var element in this) { + if (test(index++, element)) result = element; + } + return result; + } + + /// The last element, or `null` if the iterable is empty. + T? get lastOrNull { + if (isEmpty) return null; + return last; + } } diff --git a/guru_app/packages/guru_utils/lib/guru_utils.dart b/guru_app/packages/guru_utils/lib/guru_utils.dart index 1b855c0..c0d0b82 100644 --- a/guru_app/packages/guru_utils/lib/guru_utils.dart +++ b/guru_app/packages/guru_utils/lib/guru_utils.dart @@ -1,5 +1,7 @@ library guru_utils; +import 'package:guru_utils/auth/auth_credential_manager.dart'; + typedef ToastDelegate = void Function(String message, {Duration duration}); class GuruUtils { diff --git a/guru_app/packages/guru_utils/lib/http/http_ex.dart b/guru_app/packages/guru_utils/lib/http/http_ex.dart index 8f0dfd7..fa2843e 100644 --- a/guru_app/packages/guru_utils/lib/http/http_ex.dart +++ b/guru_app/packages/guru_utils/lib/http/http_ex.dart @@ -58,6 +58,7 @@ class HttpEx { static void init(CdnConfig cdnConfig, String firebaseStoragePrefix) { _cdnConfig = cdnConfig; _firebaseStoragePrefix = firebaseStoragePrefix; + Log.d("HttpEx.init: $cdnConfig $firebaseStoragePrefix"); } static bool isFirebaseStoragePrefix(String url) { diff --git a/guru_app/packages/guru_utils/lib/http/http_model.dart b/guru_app/packages/guru_utils/lib/http/http_model.dart index 455df17..ff7afc6 100644 --- a/guru_app/packages/guru_utils/lib/http/http_model.dart +++ b/guru_app/packages/guru_utils/lib/http/http_model.dart @@ -13,6 +13,12 @@ class CdnConfig { CdnConfig(this.prefix, this.cdnPrefix, this.fatalThreshold, this.fallbackPrefix, this.monitor); + + @override + String toString() { + return 'CdnConfig{prefix: $prefix, cdnPrefix: $cdnPrefix, fallbackPrefix: $fallbackPrefix, fatalThreshold: $fatalThreshold, monitor: $monitor}'; + } + factory CdnConfig.fromJson(Map json, {String defaultStoragePrefix = '', String defaultCdnPrefix = ''}) => CdnConfig( diff --git a/guru_app/packages/guru_utils/lib/image/image_utils.dart b/guru_app/packages/guru_utils/lib/image/image_utils.dart index 282bd7c..a4982b7 100644 --- a/guru_app/packages/guru_utils/lib/image/image_utils.dart +++ b/guru_app/packages/guru_utils/lib/image/image_utils.dart @@ -12,7 +12,8 @@ import 'package:flutter/services.dart'; import 'package:image/image.dart'; /// Created by Haoyi on 2022/6/8 - +/// +/// class ImageUtils { static Future loadImageFromFile( String path, { diff --git a/guru_app/packages/guru_utils/lib/log/log.dart b/guru_app/packages/guru_utils/lib/log/log.dart index 0537b57..c47f4b5 100644 --- a/guru_app/packages/guru_utils/lib/log/log.dart +++ b/guru_app/packages/guru_utils/lib/log/log.dart @@ -23,6 +23,7 @@ class LogLevel { static const warning = 3; static const error = 4; static const wtf = 5; + static const nothing = -1; static const List levels = [ Level.trace, @@ -377,10 +378,12 @@ class Log { Log._appName = appName; tagColors[_appName] = CmdColor("${AnsiColor.ansiEsc}33;1m"); _persistentLevel = persistentLevel; - await PersistentLog.createLogger( - logName: persistentLogName, - fileSizeLimit: persistentLogFileSize, - fileCount: persistentLogCount); + if (persistentLevel != LogLevel.nothing) { + await PersistentLog.createLogger( + logName: persistentLogName, + fileSizeLimit: persistentLogFileSize, + fileCount: persistentLogCount); + } } static void setListen(bool listen) { diff --git a/guru_app/packages/guru_utils/lib/manifest/manifest.dart b/guru_app/packages/guru_utils/lib/manifest/manifest.dart index 39e73d3..05eb426 100644 --- a/guru_app/packages/guru_utils/lib/manifest/manifest.dart +++ b/guru_app/packages/guru_utils/lib/manifest/manifest.dart @@ -9,6 +9,12 @@ part "manifest.g.dart"; class DetailsReservedType { static const String igc = "igc"; + static const String igb = "igb"; +} + +class DetailsAttr { + static const permanent = 1; + static const consumable = 2; } class ExtraReservedField { @@ -17,13 +23,18 @@ class ExtraReservedField { static const String basePlanId = "__base_plan_id"; static const String sales = "__sales"; static const String rate = "__rate"; + static const String contentId = "__contentId"; + static const String barterId = "__barterId"; + static const String transactionTs = "__transaction_ts"; } class DetailsReservedField { static const String type = "__type"; + static const String sku = "__sku"; static const String amount = "__amount"; static const String name = "__name"; static const String icon = "__icon"; + static const String attr = "__attr"; } class ManifestStringConvert implements JsonConverter { @@ -53,8 +64,10 @@ class Details { const Details._({this.bundle = const {}}); - Details.define(String type, int amount, {Map params = const {}}) + Details.define(String type, int amount, + {String? sku, Map params = const {}}) : bundle = { + DetailsReservedField.sku: sku ?? type, DetailsReservedField.type: type, DetailsReservedField.amount: amount }; @@ -65,6 +78,12 @@ class Details { String get type => bundle[DetailsReservedField.type] ?? "unknown"; + String get sku => bundle[DetailsReservedField.sku] ?? type; + + String? get name => bundle[DetailsReservedField.name] ?? "unknown"; + + int get attr => bundle[DetailsReservedField.attr] ?? DetailsAttr.consumable; + Map toJson() => _$DetailsToJson(this); void merge(Details details) { @@ -143,6 +162,32 @@ class Manifest { String? get offerId => extras[ExtraReservedField.offerId]; + /// 在 TransactionMethod为IAP时,specific 对应的是 IAP 的 SKU, + /// 如一些特定含有 ID的物品,如用 IGC购买了某个道具,这里面的 contentId 就是道具的 ID + String get contentId => extras[ExtraReservedField.contentId] ?? ""; + + // 如果你Barter进行购买,这个字段会有值 + String get barterId => extras[ExtraReservedField.barterId] ?? ""; + + int? get transactionTs => extras[ExtraReservedField.transactionTs]; + + Manifest.barter(this.category, String contentId, String barterId, + {Map extras = const {}, this.details = const
[]}) + : assert(contentId.isNotEmpty), + assert(barterId.isNotEmpty), + extras = { + ...extras, + ...{ExtraReservedField.contentId: contentId, ExtraReservedField.barterId: barterId} + }; + + Manifest.action(this.category, String scene, + {Map extras = const {}, this.details = const
[]}) + : assert(scene.isNotEmpty), + extras = { + ...extras, + ...{ExtraReservedField.scene: scene} + }; + const Manifest(this.category, {this.extras = const {}, this.details = const
[]}); diff --git a/guru_app/packages/guru_utils/lib/packages/guru_package.dart b/guru_app/packages/guru_utils/lib/packages/guru_package.dart index 8f33b12..6ca096a 100644 --- a/guru_app/packages/guru_utils/lib/packages/guru_package.dart +++ b/guru_app/packages/guru_utils/lib/packages/guru_package.dart @@ -21,6 +21,8 @@ abstract class GuruPackage { Future initializeAsync() async {} + Future switchSession(String oldToken, String newToken) async {} + Iterable get supportedLocales; Iterable> get localizationsDelegates; diff --git a/guru_app/packages/guru_utils/lib/property/app_property.dart b/guru_app/packages/guru_utils/lib/property/app_property.dart index 3e56a52..af810ac 100644 --- a/guru_app/packages/guru_utils/lib/property/app_property.dart +++ b/guru_app/packages/guru_utils/lib/property/app_property.dart @@ -49,6 +49,11 @@ class AppProperty implements PropertyDelegate { _instance = AppProperty._(storage, cacheSize: cacheSize); } + static void reload({int cacheSize = 256}) { + _instance._cache.clear((_) {}); + _instance = AppProperty._(_instance._storage, cacheSize: cacheSize); + } + @override Future setInt(PropertyKey key, int value) { return _setValue(key, value.toString()); diff --git a/guru_app/packages/guru_utils/lib/router/route_path.dart b/guru_app/packages/guru_utils/lib/router/route_path.dart index 51750ab..215b89a 100644 --- a/guru_app/packages/guru_utils/lib/router/route_path.dart +++ b/guru_app/packages/guru_utils/lib/router/route_path.dart @@ -10,9 +10,9 @@ class RoutePath { const RoutePath(this.name, {this.segmentSpecifier = false, this.parentPath}); - String get _path => "${parentPath?.path() ?? ""}$mainPath"; + String get _path => segmentSpecifier ? mainPath : "${parentPath?.path() ?? ""}$mainPath"; - String get mainPath => "/${segmentSpecifier ? ":" : ""}$name"; + String get mainPath => segmentSpecifier ? "${parentPath?.mainPath ?? ""}/:$name" : "/$name"; String path({String? segmentPath, Map? queryParams}) { final requestQueryParams = queryParams?.entries.map((e) => "${e.key}=${e.value}").toList() ?? []; @@ -20,7 +20,7 @@ class RoutePath { if (segmentSpecifier) { if (segmentPath != null) { - path = path.replaceAll(":$segmentSpecifier", segmentPath); + path = path.replaceAll(":$name", segmentPath); } else { assert(false, "The route($path) need to provide segment path parameters!!"); } diff --git a/guru_app/packages/guru_utils/lib/settings/settings.dart b/guru_app/packages/guru_utils/lib/settings/settings.dart index 7924211..6895a5c 100644 --- a/guru_app/packages/guru_utils/lib/settings/settings.dart +++ b/guru_app/packages/guru_utils/lib/settings/settings.dart @@ -32,7 +32,7 @@ abstract class Settings with UtilsSettings { setting.init(bundle); } if (!_initialized) { - _syncVersion(); + await _syncVersion(); _initialized = true; } return bundle; diff --git a/guru_app/packages/guru_utils/lib/settings/utils_settings.dart b/guru_app/packages/guru_utils/lib/settings/utils_settings.dart index 7b75043..6ee1f7a 100644 --- a/guru_app/packages/guru_utils/lib/settings/utils_settings.dart +++ b/guru_app/packages/guru_utils/lib/settings/utils_settings.dart @@ -6,7 +6,7 @@ class UtilsSettingsKeys { static const PropertyKey version = PropertyKey.setting("version"); static const PropertyKey buildNumber = PropertyKey.setting("build_number"); static const PropertyKey firstInstallTime = PropertyKey.general("first_install_time"); - static const PropertyKey firstInstallVersion = PropertyKey.general("first_install_version"); + static const PropertyKey firstInstallVersion = PropertyKey.setting("first_install_version"); static const PropertyKey prevInstallVersion = PropertyKey.general("prev_install_version"); static const PropertyKey latestInstallVersion = PropertyKey.general("latest_install_version"); static const PropertyKey previousInstalledVersion = @@ -38,9 +38,38 @@ mixin UtilsSettings { final SettingStringData version = SettingStringData(UtilsSettingsKeys.version, defaultValue: ""); final SettingStringData buildNumber = SettingStringData(UtilsSettingsKeys.buildNumber, defaultValue: ""); + final SettingStringData firstInstallVersion = + SettingStringData(UtilsSettingsKeys.firstInstallVersion, defaultValue: ""); + + Future _auditVersion(String versionBuildNumber) async { + final firstInstallTime = await AppProperty.getInstance() + .getOrCreateInt(UtilsSettingsKeys.firstInstallTime, DateTimeUtils.currentTimeInMillis()); + RuntimeProperty.instance.setInt(UtilsSettingsKeys.firstInstallTime, firstInstallTime); + + final fiv = firstInstallVersion.get(); + + final latestInstallVersion = await AppProperty.getInstance() + .getOrCreateString(UtilsSettingsKeys.latestInstallVersion, fiv); + + if (latestInstallVersion != versionBuildNumber) { + AppProperty.getInstance() + .setString(UtilsSettingsKeys.latestInstallVersion, versionBuildNumber); + AppProperty.getInstance() + .setString(UtilsSettingsKeys.prevInstallVersion, latestInstallVersion); + final previousInstalledVersion = await AppProperty.getInstance() + .getOrCreateString(UtilsSettingsKeys.previousInstalledVersion, fiv); + List versionList = previousInstalledVersion.split("|"); + versionList.add(versionBuildNumber); + if (versionList.length > 15) { + versionList = versionList.sublist(versionList.length - 15); + } + AppProperty.getInstance() + .setString(UtilsSettingsKeys.previousInstalledVersion, versionList.join("|")); + } + } Future _syncVersion() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); final currentVersion = packageInfo.version.replaceAll(RegExp(r'-.*'), ''); final currentBuildName = packageInfo.buildNumber; Log.d("VERSION: ${version.get()}-${buildNumber.get()} => $currentVersion-$currentBuildName"); @@ -51,30 +80,13 @@ mixin UtilsSettings { if (currentBuildName != buildNumber.get()) { buildNumber.set(currentBuildName); } + final versionBuildNumber = "${version.get()}-${buildNumber.get()}"; - - final firstInstallTime = await AppProperty.getInstance() - .getOrCreateInt(UtilsSettingsKeys.firstInstallTime, DateTimeUtils.currentTimeInMillis()); - RuntimeProperty.instance.setInt(UtilsSettingsKeys.firstInstallTime, firstInstallTime); - final firstInstallVersion = await AppProperty.getInstance() - .getOrCreateString(UtilsSettingsKeys.firstInstallVersion, versionBuildNumber); - final latestInstallVersion = await AppProperty.getInstance() - .getOrCreateString(UtilsSettingsKeys.latestInstallVersion, firstInstallVersion); - - if (latestInstallVersion != versionBuildNumber) { - AppProperty.getInstance() - .setString(UtilsSettingsKeys.latestInstallVersion, versionBuildNumber); - AppProperty.getInstance() - .setString(UtilsSettingsKeys.prevInstallVersion, latestInstallVersion); - final previousInstalledVersion = await AppProperty.getInstance() - .getOrCreateString(UtilsSettingsKeys.previousInstalledVersion, firstInstallVersion); - List versionList = previousInstalledVersion.split("|"); - versionList.add(versionBuildNumber); - if (versionList.length > 15) { - versionList = versionList.sublist(versionList.length - 15); - } - AppProperty.getInstance() - .setString(UtilsSettingsKeys.previousInstalledVersion, versionList.join("|")); + if (firstInstallVersion.get() == "") { + final fiv = await AppProperty.getInstance() + .getOrCreateString(UtilsSettingsKeys.firstInstallVersion, versionBuildNumber); + firstInstallVersion.set(fiv); } + _auditVersion(versionBuildNumber); } } diff --git a/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsConstants.kt b/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsConstants.kt index 74a7fa1..989f479 100644 --- a/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsConstants.kt +++ b/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsConstants.kt @@ -18,6 +18,7 @@ object GuruAnalyticsConstants { const val SetScreen = "setScreen" const val ZipLogs = "zipLogs" const val GetStatistic = "getStatistic" + const val SetConsents = "setConsents" } object ErrorCode { @@ -51,4 +52,11 @@ object GuruAnalyticsConstants { const val X_DEVICE_INFO = "xDeviceInfo" } +} + +object ConsentFieldName { + const val AD_STORAGE = "ad_storage" + const val ANALYTICS_STORAGE = "analytics_storage" + const val AD_PERSONALIZATION = "ad_personalization" + const val AD_USER_DATA = "ad_user_data" } \ No newline at end of file diff --git a/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsFlutterPlugin.kt b/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsFlutterPlugin.kt index 2839db3..8cf8a4e 100644 --- a/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsFlutterPlugin.kt +++ b/guru_app/plugins/guru_analytics_flutter/android/src/main/kotlin/guru/core/flutter/analytics/guru_analytics_flutter/GuruAnalyticsFlutterPlugin.kt @@ -6,6 +6,8 @@ import android.os.Looper import android.util.Log import androidx.annotation.NonNull import com.google.firebase.analytics.FirebaseAnalytics +import com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus +import guru.core.analytics.Constants import guru.core.analytics.GuruAnalytics import guru.core.analytics.data.db.model.EventPriority import guru.core.analytics.data.model.AnalyticsOptions @@ -116,6 +118,34 @@ class GuruAnalyticsFlutterPlugin : FlutterPlugin, MethodCallHandler { } } + private fun getConsentStatus(@NonNull call: MethodCall, consentField: String): ConsentStatus { + val granted = call.argument(consentField) ?: false + return if (granted) ConsentStatus.GRANTED else ConsentStatus.DENIED + } + + private fun callSetConsents(@NonNull call: MethodCall, @NonNull result: Result) { + val adStorageStatus = getConsentStatus(call, ConsentFieldName.AD_STORAGE) + val analyticsStorageStatus = getConsentStatus(call, ConsentFieldName.ANALYTICS_STORAGE) + val adPersonalizationStatus = getConsentStatus(call, ConsentFieldName.AD_PERSONALIZATION) + val adUserDataStatus = getConsentStatus(call, ConsentFieldName.AD_USER_DATA) + + val analytics = FirebaseAnalytics.getInstance(appContext) + analytics.setConsent( + mapOf( + FirebaseAnalytics.ConsentType.AD_STORAGE to adStorageStatus, + FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE to analyticsStorageStatus, + FirebaseAnalytics.ConsentType.AD_PERSONALIZATION to adPersonalizationStatus, + FirebaseAnalytics.ConsentType.AD_USER_DATA to adUserDataStatus + ) + ) + Log.d( + "GuruAnalytics", + "callSetConsents $adStorageStatus $analyticsStorageStatus $adPersonalizationStatus $adUserDataStatus" + ) + result.success(true) + + } + private fun callLogEvent(@NonNull call: MethodCall, @NonNull result: Result) { val eventName = call.argument(GuruAnalyticsConstants.FieldName.EVENT_NAME) if (eventName == null) { @@ -230,6 +260,7 @@ class GuruAnalyticsFlutterPlugin : FlutterPlugin, MethodCallHandler { GuruAnalyticsConstants.Method.SetScreen -> callSetScreen(call, result) GuruAnalyticsConstants.Method.ZipLogs -> callZipLogs(call, result) GuruAnalyticsConstants.Method.GetStatistic -> callGetStatistic(call, result) + GuruAnalyticsConstants.Method.SetConsents -> callSetConsents(call, result) else -> { result.notImplemented() } diff --git a/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsConstants.swift b/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsConstants.swift index ffbc8ff..119901b 100644 --- a/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsConstants.swift +++ b/guru_app/plugins/guru_analytics_flutter/ios/Classes/GuruAnalyticsConstants.swift @@ -12,6 +12,7 @@ class AnalyticsErrorCode { static let GET_APP_INSTANCE_ID_FAILED = "GET_INSTANCE_ID_FAILED" static let PROPERTY_ERROR = "PROPERTY_ERROR" static let EVENT_ERROR = "EVENT_ERROR" + static let SET_CONSENTS_ERROR = "SET_CONSENTS_ERROR" } class AnalyticsMethod { @@ -27,6 +28,7 @@ class AnalyticsMethod { static let SetScreen = "setScreen" static let ZipLogs = "zipLogs" static let GetStatistic = "getStatistic" + static let SetConsents = "setConsents" } class AnalyticsFieldName { @@ -50,3 +52,10 @@ class AnalyticsFieldName { static let X_APP_ID = "xAppId"; static let X_DEVICE_INFO = "xDeviceInfo"; } + +class ConsentsFieldName { + static let AD_STORAGE = "ad_storage" + static let ANALYTICS_STORAGE = "analytics_storage" + static let AD_PERSONALIZATION = "ad_personalization" + static let AD_USER_DATA = "ad_user_data" +} diff --git a/guru_app/plugins/guru_analytics_flutter/ios/Classes/SwiftGuruAnalyticsFlutterPlugin.swift b/guru_app/plugins/guru_analytics_flutter/ios/Classes/SwiftGuruAnalyticsFlutterPlugin.swift index 90c9873..6656dd1 100644 --- a/guru_app/plugins/guru_analytics_flutter/ios/Classes/SwiftGuruAnalyticsFlutterPlugin.swift +++ b/guru_app/plugins/guru_analytics_flutter/ios/Classes/SwiftGuruAnalyticsFlutterPlugin.swift @@ -42,13 +42,44 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { // result(FlutterError.init(code: error.errorCode.description, message: error.errorMessage, details: "\(sessionId) \(error.errorMessage) [\(msg)]) } + private func getConsentStatus(_ arguments: [String : Any], consentField: String)-> ConsentStatus { + return ((arguments[consentField] as? Bool) ?? false) ? ConsentStatus.granted : ConsentStatus.denied + } + + private func callSetConsents(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String : Any] + else { + let error = FlutterError.init( + code: AnalyticsErrorCode.SET_CONSENTS_ERROR, + message: "arguments is Empty", + details: "arguments is Empty") + result(error) + return + } + + let adStorageStatus = getConsentStatus(arguments, consentField: ConsentsFieldName.AD_STORAGE) + let analyticsStorageStatus = getConsentStatus(arguments, consentField: ConsentsFieldName.ANALYTICS_STORAGE) + let adPersonalizationStatus = getConsentStatus(arguments, consentField: ConsentsFieldName.AD_PERSONALIZATION) + let adUserDataStatus = getConsentStatus(arguments, consentField: ConsentsFieldName.AD_USER_DATA) + + Analytics.setConsent([ + .adStorage : adStorageStatus, + .analyticsStorage: analyticsStorageStatus, + .adPersonalization: adPersonalizationStatus, + .adUserData: adUserDataStatus + ]) + NSLog("[GuruAnalytics] ==> setConsents(\(adStorageStatus) \(analyticsStorageStatus) \(adPersonalizationStatus) \(adUserDataStatus)") + result(true) + + } + private func processAnalyticsCallback(_ errorCode: Int, _ info: String) { registeredChannel?.invokeMethod("onAnalyticsCallback", arguments: [ "code": errorCode, "errorInfo": info ]) } - + private func callLogEvent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String : Any], let eventName = arguments[AnalyticsFieldName.EVENT_NAME] as? String @@ -60,17 +91,17 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { result(error) return } - + let parametersMap = arguments[AnalyticsFieldName.PARAMETERS] as? [String : Any] ?? [String : Any]() - + GuruAnalytics.logEvent(eventName, parameters: parametersMap) if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { NSLog("[GuruAnalytics] ==> logEvent(\(eventName)") } - + result(true) } - + private func callSetUserProperty(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String : Any], let name = arguments[AnalyticsFieldName.NAME] as? String, @@ -88,9 +119,9 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { NSLog("[GuruAnalytics] ==> setUserProperty \(name) : \(value)") } result(true) - + } - + private func callSetDeviceId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String : Any], let deviceId = arguments[AnalyticsFieldName.DEVICE_ID] as? String @@ -107,9 +138,9 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { NSLog("[GuruAnalytics] ==> setDeviceId \(deviceId)") } result(true) - + } - + private func callSetUid(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String : Any], let userId = arguments[AnalyticsFieldName.USER_ID] as? String @@ -127,7 +158,7 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { } result(true) } - + private func callSetAdjustId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String : Any], let adjustId = arguments[AnalyticsFieldName.ADJUST_ID] as? String @@ -144,9 +175,9 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { NSLog("[GuruAnalytics] ==> setAdjustId \(adjustId)") } result(true) - + } - + private func callSetAdId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String : Any], let adId = arguments[AnalyticsFieldName.AD_ID] as? String @@ -164,7 +195,7 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { } result(true) } - + private func callSetFirebaseId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String : Any], let firebaseId = arguments[AnalyticsFieldName.FIREBASE_ID] as? String @@ -181,9 +212,9 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { NSLog("[GuruAnalytics] ==> setFirebaseId \(firebaseId)") } result(true) - + } - + private func callSetScreen(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String : Any], let screen = arguments[AnalyticsFieldName.SCREEN] as? String @@ -201,7 +232,7 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { } result(true) } - + private func callZipLogs(_ call: FlutterMethodCall, result: @escaping FlutterResult) { if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { NSLog("[GuruAnalytics] ==> callZipLogs") @@ -210,7 +241,7 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { result(url?.absoluteString ?? "") } } - + private func callGetStatistic(_ call: FlutterMethodCall, result: @escaping FlutterResult) { if (SwiftGuruAnalyticsFlutterPlugin.debugMode) { NSLog("[GuruAnalytics] ==> callGetStatistic") @@ -219,7 +250,7 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { result(["logged" : loggedEventCount, "uploaded" : uploadedEventCount]) } } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // result("iOS " + UIDevice.current.systemVersion) NSLog("[GuruAnalytics] methodHandler \(call.method)") @@ -260,6 +291,9 @@ public class SwiftGuruAnalyticsFlutterPlugin: NSObject, FlutterPlugin { case AnalyticsMethod.GetStatistic: callGetStatistic(call, result: result) break; + case AnalyticsMethod.SetConsents: + callSetConsents(call, result: result) + break; default: result(FlutterMethodNotImplemented) } diff --git a/guru_app/plugins/guru_analytics_flutter/lib/event_logger.dart b/guru_app/plugins/guru_analytics_flutter/lib/event_logger.dart index 4683267..ccf2db4 100644 --- a/guru_app/plugins/guru_analytics_flutter/lib/event_logger.dart +++ b/guru_app/plugins/guru_analytics_flutter/lib/event_logger.dart @@ -355,12 +355,14 @@ class EventLogger { /// * [adSource] Ad Network Name /// * [adUnitName] Ad Unit Name or Id /// - static logAdRevenue(double adRevenue, String adPlatform, String currency) { - final parameters = { + static logAdRevenue(double adRevenue, String adPlatform, String currency, + {Map extras = const {}}) { + final parameters = _filterOutNulls({ FirebaseEventsParams.AD_PLATFORM: adPlatform, FirebaseEventsParams.CURRENCY: currency, FirebaseEventsParams.VALUE: adRevenue, - }; + ...extras + }); firebaseLogEvent(name: "tch_ad_rev_roas_001", parameters: parameters); if (_capabilities.hasCapability(AppEventCapabilities.guru)) { guruLogEvent( @@ -369,6 +371,22 @@ class EventLogger { transmit("tch_ad_rev_roas_001", parameters); } + static logAdRevenue020(double adRevenue, String adPlatform, String currency, + {Map extras = const {}}) { + final parameters = _filterOutNulls({ + FirebaseEventsParams.AD_PLATFORM: adPlatform, + FirebaseEventsParams.CURRENCY: currency, + FirebaseEventsParams.VALUE: adRevenue, + ...extras + }); + firebaseLogEvent(name: "tch_ad_rev_roas_02", parameters: parameters); + if (_capabilities.hasCapability(AppEventCapabilities.guru)) { + guruLogEvent( + name: "tch_ad_rev_roas_02", parameters: parameters, priority: EventPriority.EMERGENCE); + } + transmit("tch_ad_rev_roas_02", parameters); + } + /// logAdLtv. /// /// * [phase] ltv所属的阶段 现有阶段有: tch_ad_rev_top40, tch_ad_rev_top30, @@ -389,7 +407,7 @@ class EventLogger { required String itemId, required String method, }) { - final parameters = filterOutNulls({ + final parameters = _filterOutNulls({ FirebaseEventsParams.CONTENT_TYPE: contentType, FirebaseEventsParams.ITEM_ID: itemId, FirebaseEventsParams.METHOD: method, diff --git a/guru_app/plugins/guru_analytics_flutter/lib/events_constants.dart b/guru_app/plugins/guru_analytics_flutter/lib/events_constants.dart index d30691d..62f2854 100644 --- a/guru_app/plugins/guru_analytics_flutter/lib/events_constants.dart +++ b/guru_app/plugins/guru_analytics_flutter/lib/events_constants.dart @@ -14,6 +14,7 @@ class Method { static const zipLogs = "zipLogs"; static const getStatistic = "getStatistic"; static const onAnalyticsCallback = "onAnalyticsCallback"; + static const setConsents = "setConsents"; } class FieldName { diff --git a/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_event_logger.dart b/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_event_logger.dart index 9b32711..856086b 100644 --- a/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_event_logger.dart +++ b/guru_app/plugins/guru_analytics_flutter/lib/guru/guru_event_logger.dart @@ -34,14 +34,12 @@ class AnalyticsCode { static const EVENT_FG = 1002; // fg 事件 } -typedef PriorityGetter = int Function(String name, Map parameters); +typedef PriorityGetter = int Function(String name, Map parameters); typedef AnalyticsCallback = void Function(int eventCode, String? errorInfo); class GuruEventLogger { - static const MethodChannel _channel = MethodChannel( - 'flutter.guru.guru_analytics_flutter'); + static const MethodChannel _channel = MethodChannel('flutter.guru.guru_analytics_flutter'); static final RegExp _nonAlphaNumeric = RegExp('[^a-zA-Z0-9_]'); static final RegExp _alpha = RegExp('[a-zA-Z]'); @@ -60,39 +58,45 @@ class GuruEventLogger { return await _channel.invokeMethod(Method.getAppInstanceId); } - static int _defaultPriorityGetter(String name, - Map parameters) { + Future setConsents(Map consents) async { + return await _channel.invokeMethod(Method.setConsents, consents); + } + + static int _defaultPriorityGetter(String name, Map parameters) { return EventPriority.DEFAULT; } Future initialize( - {required String appId, required String deviceInfo, int batchLimit = 32, - int uploadPeriodInSeconds = 60, int delayedInSeconds = 10, - int eventExpiredInDays = 7, bool debug = false, - AnalyticsCallback? callback}) async { + {required String appId, + required String deviceInfo, + int batchLimit = 32, + int uploadPeriodInSeconds = 60, + int delayedInSeconds = 10, + int eventExpiredInDays = 7, + bool debug = false, + AnalyticsCallback? callback}) async { if (!_initialized) { if (callback != null) { _analyticsCallback = callback; } _channel.setMethodCallHandler((call) => processMethodCall(call)); - _initialized = await _channel.invokeMethod( - Method.initialize, { - FieldName.batchLimit: batchLimit, - FieldName.uploadPeriodInSeconds: uploadPeriodInSeconds, - FieldName.delayedInSeconds: delayedInSeconds, - FieldName.eventExpiredInDays: eventExpiredInDays, - FieldName.DEBUG: debug, - FieldName.X_APP_ID: appId, - FieldName.X_DEVICE_INFO: deviceInfo - }) ?? false; + _initialized = await _channel.invokeMethod(Method.initialize, { + FieldName.batchLimit: batchLimit, + FieldName.uploadPeriodInSeconds: uploadPeriodInSeconds, + FieldName.delayedInSeconds: delayedInSeconds, + FieldName.eventExpiredInDays: eventExpiredInDays, + FieldName.DEBUG: debug, + FieldName.X_APP_ID: appId, + FieldName.X_DEVICE_INFO: deviceInfo + }) ?? + false; } } Future processMethodCall(MethodCall methodCall) async { switch (methodCall.method) { case Method.onAnalyticsCallback: - _analyticsCallback.call( - methodCall.arguments[FieldName.EVENT_CODE] as int, + _analyticsCallback.call(methodCall.arguments[FieldName.EVENT_CODE] as int, methodCall.arguments[FieldName.EVENT_ERROR_INFO]); break; default: @@ -105,9 +109,11 @@ class GuruEventLogger { } Future logEvent(String eventName, - {String? itemCategory, String? itemName, num? value, int? priority, Map< - String, - dynamic>? parameters}) { + {String? itemCategory, + String? itemName, + num? value, + int? priority, + Map? parameters}) { if (eventName.isEmpty || eventName.length > 128 || eventName.indexOf(_alpha) != 0 || @@ -128,8 +134,7 @@ class GuruEventLogger { mergedParams.addAll(parameters); } - return _channel.invokeMethod( - Method.logEvent, { + return _channel.invokeMethod(Method.logEvent, { FieldName.EVENT_NAME: eventName, FieldName.PARAMETERS: _filterOutNulls(mergedParams), FieldName.PRIORITY: priority ?? _priorityGetter(eventName, mergedParams) @@ -148,47 +153,38 @@ class GuruEventLogger { ); } return _channel.invokeMethod( - Method.setUserProperty, - {FieldName.NAME: name, FieldName.VALUE: value}); + Method.setUserProperty, {FieldName.NAME: name, FieldName.VALUE: value}); } Future setDeviceId(String deviceId) { return _channel - .invokeMethod( - Method.setDeviceId, {FieldName.DEVICE_ID: deviceId}); + .invokeMethod(Method.setDeviceId, {FieldName.DEVICE_ID: deviceId}); } Future setUserId(String uid) { - return _channel.invokeMethod( - Method.setUid, {FieldName.USER_ID: uid}); + return _channel.invokeMethod(Method.setUid, {FieldName.USER_ID: uid}); } Future setAdjustId(String adjustId) { return _channel - .invokeMethod( - Method.setAdjustId, {FieldName.ADJUST_ID: adjustId}); + .invokeMethod(Method.setAdjustId, {FieldName.ADJUST_ID: adjustId}); } Future setAdId(String adId) { - return _channel.invokeMethod( - Method.setAdId, {FieldName.AD_ID: adId}); + return _channel.invokeMethod(Method.setAdId, {FieldName.AD_ID: adId}); } Future setFirebaseId(String firebaseId) { return _channel - .invokeMethod(Method.setFirebaseId, - {FieldName.FIREBASE_ID: firebaseId}); + .invokeMethod(Method.setFirebaseId, {FieldName.FIREBASE_ID: firebaseId}); } Future setScreen(String screenName) { - return _channel - .invokeMethod( - Method.setScreen, {FieldName.SCREEN: screenName}); + return _channel.invokeMethod(Method.setScreen, {FieldName.SCREEN: screenName}); } Future zipLogs() async { - final result = await _channel.invokeMethod( - Method.zipLogs, {}); + final result = await _channel.invokeMethod(Method.zipLogs, {}); if (result is String) { if (Platform.isAndroid) { return result; @@ -201,8 +197,7 @@ class GuruEventLogger { } Future getStatistic() async { - final json = await _channel.invokeMethod( - Method.getStatistic, {}); + final json = await _channel.invokeMethod(Method.getStatistic, {}); return GuruStatistic.fromJson(json); } diff --git a/guru_app/plugins/guru_applovin_flutter/android/build.gradle b/guru_app/plugins/guru_applovin_flutter/android/build.gradle index bb73062..2beeea7 100644 --- a/guru_app/plugins/guru_applovin_flutter/android/build.gradle +++ b/guru_app/plugins/guru_applovin_flutter/android/build.gradle @@ -104,7 +104,7 @@ dependencies { // implementation 'guru.ads.max:max-adapter:0.2.0' - api 'com.applovin:applovin-sdk:11.11.3' + api 'com.applovin:applovin-sdk:12.1.0' api 'com.applovin.mediation:facebook-adapter:6.16.0.2' // DT Exchange api 'com.applovin.mediation:fyber-adapter:8.2.3.3' @@ -117,7 +117,7 @@ dependencies { api 'com.applovin.mediation:verve-adapter:2.19.0.0' api 'com.applovin.mediation:vungle-adapter:6.11.0.1' api 'com.applovin.mediation:chartboost-adapter:9.4.1.0' - api 'com.applovin.mediation:inmobi-adapter:10.1.4.3' + api 'com.applovin.mediation:inmobi-adapter:10.6.3.0' api 'com.amazon.android:aps-sdk:9.8.4' api 'com.applovin.mediation:amazon-tam-adapter:9.8.4.0' api 'com.applovin.mediation:mintegral-adapter:16.5.11.0' @@ -130,4 +130,6 @@ dependencies { api 'com.moloco.sdk.adapters:applovin:1.6.0.0' api 'com.applovin.mediation:ogury-presage-adapter:5.6.2.0' + api 'com.applovin.mediation:mobilefuse-adapter:1.7.0.0' + } diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdStatus.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdStatus.kt index 8959793..ecc426e 100644 --- a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdStatus.kt +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/AdStatus.kt @@ -90,4 +90,12 @@ annotation class ConsentErrorCode { } } } +} + +@IntDef(ScreenOrientation.PORTRAIT, ScreenOrientation.LANDSCAPE) +annotation class ScreenOrientation { + companion object { + const val PORTRAIT = 0 + const val LANDSCAPE = 1 + } } \ No newline at end of file diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/BannerAd.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/BannerAd.kt index 7d21803..c882839 100644 --- a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/BannerAd.kt +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/BannerAd.kt @@ -36,6 +36,9 @@ class BannerAd(id: Int, channel: MethodChannel) : MaxAdViewAdListener, MaxAdReve private var bannerAd: MaxAdView? = null + @ScreenOrientation + private var orientation: Int = ScreenOrientation.PORTRAIT + companion object { private val allAds: SparseArray = SparseArray() @@ -65,6 +68,13 @@ class BannerAd(id: Int, channel: MethodChannel) : MaxAdViewAdListener, MaxAdReve iterator.next().dispose(activity) } } + + fun orientationChanged(activity: Activity, @ScreenOrientation orientation: Int) { + var iterator = allAds.valueIterator() + if (iterator.hasNext()) { + iterator.next().updateOrientation(activity, orientation) + } + } } init { @@ -188,7 +198,20 @@ class BannerAd(id: Int, channel: MethodChannel) : MaxAdViewAdListener, MaxAdReve contentParent.removeView(contentView) } - override fun onAdLoadFailed(adUnitId: String?, err: MaxError) { + internal fun updateOrientation(activity: Activity, @ScreenOrientation orientation: Int) { + val content = activity.findViewById(id) + if (this.orientation != orientation && content != null) { + val isVisible = bannerAd!!.visibility != View.GONE + bannerAd!!.stopAutoRefresh() + bannerAd!!.visibility = View.GONE + content.removeAllViews() + (content.parent as ViewGroup).removeView(content) + if (isVisible) show(activity) + } + this.orientation = orientation + } + + override fun onAdLoadFailed(adUnitId: String, err: MaxError) { if (status == AdStatus.LOADED) { return } @@ -222,33 +245,33 @@ class BannerAd(id: Int, channel: MethodChannel) : MaxAdViewAdListener, MaxAdReve ) } - override fun onAdClicked(ad: MaxAd?) { + override fun onAdClicked(ad: MaxAd) { channel.invokeMethod("onBannerAdClicked", AdHelp.argumentsMap(id)) } - override fun onAdDisplayed(ad: MaxAd?) { + override fun onAdDisplayed(ad: MaxAd) { channel.invokeMethod( "onBannerAdDisplayed", AdHelp.argumentsMapEx( id, parameters = mapOf( - "ad_revenue" to (ad?.revenue ?: -1.0), - "ad_format" to (ad?.format?.label ?: ""), - "ad_source" to (ad?.networkName ?: ""), - "ad_unit_name" to (ad?.adUnitId ?: "") + "ad_revenue" to (ad.revenue ?: -1.0), + "ad_format" to (ad.format?.label ?: ""), + "ad_source" to (ad.networkName ?: ""), + "ad_unit_name" to (ad.adUnitId ?: "") ) ) ) } - override fun onAdExpanded(ad: MaxAd?) { + override fun onAdExpanded(ad: MaxAd) { } - override fun onAdCollapsed(ad: MaxAd?) { + override fun onAdCollapsed(ad: MaxAd) { } - override fun onAdLoaded(ad: MaxAd?) { + override fun onAdLoaded(ad: MaxAd) { // Banner ad is ready to be shown. interstitialAd!!.isReady() will now return 'true' val waterfallName = try { - ad?.waterfall?.let { waterfall -> + ad.waterfall?.let { waterfall -> logWarn("Waterfall Name: " + waterfall.name + " and Test Name: " + waterfall.testName) logWarn("Waterfall latency was: " + waterfall.latencyMillis + " milliseconds") @@ -267,24 +290,24 @@ class BannerAd(id: Int, channel: MethodChannel) : MaxAdViewAdListener, MaxAdReve "unknown" } val parameters = mapOf( - "ad_revenue" to (ad?.revenue ?: -1.0), - "ad_format" to (ad?.format?.label ?: ""), - "ad_source" to (ad?.networkName ?: ""), - "ad_unit_name" to (ad?.adUnitId ?: ""), + "ad_revenue" to (ad.revenue ?: -1.0), + "ad_format" to (ad.format?.label ?: ""), + "ad_source" to (ad.networkName ?: ""), + "ad_unit_name" to (ad.adUnitId ?: ""), "ad_waterfall_name" to waterfallName, ) status = AdStatus.LOADED channel.invokeMethod("onBannerAdLoaded", AdHelp.argumentsMapEx(id, parameters = parameters)) } - override fun onAdHidden(ad: MaxAd?) { + override fun onAdHidden(ad: MaxAd) { // Interstitial ad is hidden. Pre-load the next ad status = AdStatus.HIDDEN channel.invokeMethod("onBannerAdHidden", AdHelp.argumentsMap(id)) Logger.w("Ads", "onBannerAdHidden") } - override fun onAdDisplayFailed(ad: MaxAd?, err: MaxError) { + override fun onAdDisplayFailed(ad: MaxAd, err: MaxError) { // Interstitial ad failed to display. We recommend loading the next ad status = AdStatus.FAILED channel.invokeMethod( @@ -293,7 +316,7 @@ class BannerAd(id: Int, channel: MethodChannel) : MaxAdViewAdListener, MaxAdReve ) } - override fun onAdRevenuePaid(ad: MaxAd?) { + override fun onAdRevenuePaid(ad: MaxAd) { channel.invokeMethod( "onAdImpression", mapOf( "payload" to AdHelp.toAdPayload(ad) diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovinFlutterPlugin.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovinFlutterPlugin.kt index e8a8425..dd014e6 100644 --- a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovinFlutterPlugin.kt +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/GuruApplovinFlutterPlugin.kt @@ -97,6 +97,7 @@ class GuruApplovinFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar "setKeywords" -> callSetKeywords(call, result) "clearTargetingData" -> callClearTargetingData(call, result) "getBannerAdSize" -> callGetBannerAdSize(call, result) + "updateOrientation" -> callUpdateOrientation(call, result) else -> result.notImplemented() } } @@ -475,7 +476,11 @@ class GuruApplovinFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar var bannerAd = BannerAd.getAdForId(id!!) if (bannerAd == null || (bannerAd.status != AdStatus.LOADED && bannerAd.status != AdStatus.HIDDEN)) { Logger.w("Ads", "applovin native callShowBannerAd ad is not loaded ${bannerAd?.status}") - result.error("ad_not_loaded", "show failed for banner ad, no ad was loaded ${bannerAd?.status}", null) + result.error( + "ad_not_loaded", + "show failed for banner ad, no ad was loaded ${bannerAd?.status}", + null + ) return } val anchorOffset: String? = methodCall.argument("anchorOffset") @@ -502,6 +507,16 @@ class GuruApplovinFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar Logger.d("Ads", "applovin native callShowBannerAd show") } + + private fun callUpdateOrientation(methodCall: MethodCall, result: MethodChannel.Result) { + @ScreenOrientation + val orientation: Int = methodCall.argument("orientation") ?: ScreenOrientation.PORTRAIT + activity?.apply { + BannerAd.orientationChanged(this, orientation) + } + result.success(true) + } + private fun callHideBannerAd(id: Int?, result: MethodChannel.Result) { if (id == null) { Logger.w("Ads", "applovin native callHideBannerAd no_hash_id") @@ -716,6 +731,7 @@ class GuruApplovinFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAwar result.success(true) Logger.w("Ads", "native callClearTargetingData") } + private fun callGetBannerAdSize(methodCall: MethodCall, result: MethodChannel.Result) { if (activity == null) { Logger.w("Ads", "applovin native callGetBannerAdSize activity_is_null") diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/InterstitialAd.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/InterstitialAd.kt index 45665f1..1fed105 100644 --- a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/InterstitialAd.kt +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/InterstitialAd.kt @@ -146,7 +146,7 @@ class InterstitialAd(id: Int, channel: MethodChannel) : MaxAdListener, MaxAdReve } } - override fun onAdLoadFailed(adUnitId: String?, err: MaxError) { + override fun onAdLoadFailed(adUnitId: String, err: MaxError) { // Interstitial ad failed to load. We recommend re-trying in 3 seconds. val waterfallName = try { @@ -182,19 +182,19 @@ class InterstitialAd(id: Int, channel: MethodChannel) : MaxAdListener, MaxAdReve ) } - override fun onAdClicked(ad: MaxAd?) { + override fun onAdClicked(ad: MaxAd) { channel.invokeMethod("onInterstitialAdClicked", AdHelp.argumentsMap(id)) logDebug("onAdClicked") } - override fun onAdDisplayed(ad: MaxAd?) { + override fun onAdDisplayed(ad: MaxAd) { val parameters = mapOf( - "ad_revenue" to (ad?.revenue ?: -1.0), - "ad_format" to (ad?.format?.label ?: ""), - "ad_source" to (ad?.networkName ?: ""), - "ad_unit_name" to (ad?.adUnitId ?: ""), - "ad_creative_id" to (ad?.creativeId ?: ""), - "ad_network_name" to (ad?.networkName ?: "") + "ad_revenue" to (ad.revenue ?: -1.0), + "ad_format" to (ad.format?.label ?: ""), + "ad_source" to (ad.networkName ?: ""), + "ad_unit_name" to (ad.adUnitId ?: ""), + "ad_creative_id" to (ad.creativeId ?: ""), + "ad_network_name" to (ad.networkName ?: "") ) channel.invokeMethod( "onInterstitialAdDisplayed", AdHelp.argumentsMapEx( @@ -204,10 +204,10 @@ class InterstitialAd(id: Int, channel: MethodChannel) : MaxAdListener, MaxAdReve logDebug("onAdDisplayed $parameters") } - override fun onAdLoaded(ad: MaxAd?) { + override fun onAdLoaded(ad: MaxAd) { // Interstitial ad is ready to be shown. interstitialAd!!.isReady() will now return 'true' val waterfallName = try { - ad?.waterfall?.let { waterfall -> + ad.waterfall?.let { waterfall -> logWarn("Waterfall Name: " + waterfall.name + " and Test Name: " + waterfall.testName) logWarn("Waterfall latency was: " + waterfall.latencyMillis + " milliseconds") @@ -236,25 +236,25 @@ class InterstitialAd(id: Int, channel: MethodChannel) : MaxAdListener, MaxAdReve logDebug("onAdLoaded") } - override fun onAdHidden(ad: MaxAd?) { + override fun onAdHidden(ad: MaxAd) { // Interstitial ad is hidden. Pre-load the next ad status = AdStatus.CREATED channel.invokeMethod("onInterstitialAdHidden", AdHelp.argumentsMap(id)) logDebug("onAdHidden") } - override fun onAdDisplayFailed(ad: MaxAd?, err: MaxError) { + override fun onAdDisplayFailed(ad: MaxAd, err: MaxError) { // Interstitial ad failed to display. We recommend loading the next ad status = AdStatus.FAILED channel.invokeMethod( "onInterstitialAdDisplayFailed", AdHelp.argumentsMapEx( id, parameters = mapOf( "errorCode" to err.code, - "ad_format" to (ad?.format?.label ?: ""), - "ad_source" to (ad?.networkName ?: ""), - "ad_unit_name" to (ad?.adUnitId ?: ""), - "ad_creative_id" to (ad?.creativeId ?: ""), - "ad_network_name" to (ad?.networkName ?: "") + "ad_format" to (ad.format?.label ?: ""), + "ad_source" to (ad.networkName ?: ""), + "ad_unit_name" to (ad.adUnitId ?: ""), + "ad_creative_id" to (ad.creativeId ?: ""), + "ad_network_name" to (ad.networkName ?: "") ) ) ) @@ -262,7 +262,7 @@ class InterstitialAd(id: Int, channel: MethodChannel) : MaxAdListener, MaxAdReve } - override fun onAdRevenuePaid(ad: MaxAd?) { + override fun onAdRevenuePaid(ad: MaxAd) { channel.invokeMethod( "onAdImpression", mapOf( "payload" to AdHelp.toAdPayload(ad) diff --git a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/RewardedVideoAd.kt b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/RewardedVideoAd.kt index 84d086f..9a1f909 100644 --- a/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/RewardedVideoAd.kt +++ b/guru_app/plugins/guru_applovin_flutter/android/src/main/kotlin/flutter/guru/guru_applovin_flutter/RewardedVideoAd.kt @@ -140,7 +140,7 @@ class RewardedVideoAd(id: Int, channel: MethodChannel) : MaxRewardedAdListener, } } - override fun onAdLoadFailed(adUnitId: String?, err: MaxError) { + override fun onAdLoadFailed(adUnitId: String, err: MaxError) { // Rewarded ad failed to display. We recommend loading the next ad val waterfallName = try { err.waterfall?.let { waterfall -> @@ -174,19 +174,19 @@ class RewardedVideoAd(id: Int, channel: MethodChannel) : MaxRewardedAdListener, logDebug("onAdLoadFailed $err") } - override fun onAdClicked(ad: MaxAd?) { + override fun onAdClicked(ad: MaxAd) { channel.invokeMethod("onRewardedVideoAdClicked", argumentsMap(id)) logDebug("onAdClicked") } - override fun onAdDisplayed(ad: MaxAd?) { + override fun onAdDisplayed(ad: MaxAd) { val parameters = mapOf( - "ad_revenue" to (ad?.revenue ?: -1.0), - "ad_format" to (ad?.format?.label ?: ""), - "ad_source" to (ad?.networkName ?: ""), - "ad_unit_name" to (ad?.adUnitId ?: ""), - "ad_creative_id" to (ad?.creativeId ?: ""), - "ad_network_name" to (ad?.networkName ?: ""), + "ad_revenue" to (ad.revenue ?: -1.0), + "ad_format" to (ad.format?.label ?: ""), + "ad_source" to (ad.networkName ?: ""), + "ad_unit_name" to (ad.adUnitId ?: ""), + "ad_creative_id" to (ad.creativeId ?: ""), + "ad_network_name" to (ad.networkName ?: ""), "payload" to AdHelp.toAdPayload(ad) ) channel.invokeMethod( @@ -197,28 +197,28 @@ class RewardedVideoAd(id: Int, channel: MethodChannel) : MaxRewardedAdListener, logDebug("onAdDisplayed $parameters") } - override fun onRewardedVideoCompleted(ad: MaxAd?) { + override fun onRewardedVideoCompleted(ad: MaxAd) { channel.invokeMethod("onRewardedVideoCompleted", argumentsMap(id)) logDebug("onRewardedVideoCompleted") } - override fun onUserRewarded(ad: MaxAd?, reward: MaxReward?) { + override fun onUserRewarded(ad: MaxAd, reward: MaxReward) { // Rewarded ad was displayed and user should receive the reward channel.invokeMethod( "onRewardedVideoUserRewarded", argumentsMap( - id, "rewardLabel", reward?.label - ?: "", "rewardAmount", reward?.amount ?: 0 + id, "rewardLabel", reward.label + ?: "", "rewardAmount", reward.amount ?: 0 ) ) logDebug("onUserRewarded") } - override fun onRewardedVideoStarted(ad: MaxAd?) { + override fun onRewardedVideoStarted(ad: MaxAd) { channel.invokeMethod("onRewardedVideoStarted", argumentsMap(id)) logDebug("onUserRewarded") } - override fun onAdLoaded(ad: MaxAd?) { + override fun onAdLoaded(ad: MaxAd) { // Rewarded ad is ready to be shown. rewardedAd!!.isReady() will now return 'true' val waterfallName = try { ad?.waterfall?.let { waterfall -> @@ -246,14 +246,14 @@ class RewardedVideoAd(id: Int, channel: MethodChannel) : MaxRewardedAdListener, channel.invokeMethod("onRewardedVideoAdLoaded", argumentsMapEx(id, parameters = parameters)) } - override fun onAdHidden(ad: MaxAd?) { + override fun onAdHidden(ad: MaxAd) { // rewarded ad is hidden. Pre-load the next ad status = AdStatus.CREATED channel.invokeMethod("onRewardedVideoAdHidden", argumentsMap(id)) logDebug("onAdHidden") } - override fun onAdDisplayFailed(ad: MaxAd?, err: MaxError) { + override fun onAdDisplayFailed(ad: MaxAd, err: MaxError) { // Rewarded ad failed to display. We recommend loading the next ad status = AdStatus.FAILED channel.invokeMethod( @@ -266,7 +266,7 @@ class RewardedVideoAd(id: Int, channel: MethodChannel) : MaxRewardedAdListener, logDebug("onAdDisplayFailed $err") } - override fun onAdRevenuePaid(ad: MaxAd?) { + override fun onAdRevenuePaid(ad: MaxAd) { channel.invokeMethod( "onAdImpression", mapOf( "payload" to AdHelp.toAdPayload(ad) diff --git a/guru_app/plugins/guru_applovin_flutter/example/pubspec.lock b/guru_app/plugins/guru_applovin_flutter/example/pubspec.lock index 9511594..604688f 100644 --- a/guru_app/plugins/guru_applovin_flutter/example/pubspec.lock +++ b/guru_app/plugins/guru_applovin_flutter/example/pubspec.lock @@ -5,27 +5,31 @@ packages: dependency: transitive description: name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.flutter-io.cn" source: hosted - version: "2.9.0" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" @@ -33,13 +37,15 @@ packages: dependency: transitive description: name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.flutter-io.cn" source: hosted - version: "1.16.0" + version: "1.18.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons + sha256: "1989d917fbe8e6b39806207df5a3fdd3d816cbd090fac2ce26fb45e9a71476e5" url: "https://pub.flutter-io.cn" source: hosted version: "1.0.4" @@ -47,6 +53,7 @@ packages: dependency: transitive description: name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" @@ -59,6 +66,7 @@ packages: dependency: "direct dev" description: name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 url: "https://pub.flutter-io.cn" source: hosted version: "1.0.4" @@ -78,6 +86,7 @@ packages: dependency: transitive description: name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" @@ -85,30 +94,34 @@ packages: dependency: transitive description: name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.12" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.5" + version: "0.5.0" meta: dependency: transitive description: name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.0" + version: "1.10.0" path: dependency: transitive description: name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.2" + version: "1.8.3" sky_engine: dependency: transitive description: flutter @@ -118,34 +131,39 @@ packages: dependency: transitive description: name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.flutter-io.cn" source: hosted - version: "1.9.0" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.flutter-io.cn" source: hosted - version: "1.10.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.1" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" @@ -153,15 +171,25 @@ packages: dependency: transitive description: name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.12" + version: "0.6.1" vector_math: dependency: transitive description: name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.2" + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/BannerAd.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/BannerAd.swift index a6ba9e7..58b9217 100644 --- a/guru_app/plugins/guru_applovin_flutter/ios/Classes/BannerAd.swift +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/BannerAd.swift @@ -34,6 +34,12 @@ public class BannerAd: NSObject, MAAdViewAdDelegate, MAAdRevenueDelegate { return allAds[id] } + static func orientationChanged(orientation: Int) { + for ad in allAds.values { + ad.updateOrientation(orientation: orientation) + } + } + init(id: Int, channel: FlutterMethodChannel) { self.id = id self.channel = channel @@ -50,34 +56,9 @@ public class BannerAd: NSObject, MAAdViewAdDelegate, MAAdRevenueDelegate { bannerAd?.placement = placement let delegate = UIApplication.shared.delegate as? FlutterAppDelegate let rootViewController = delegate?.window.rootViewController - if let ad = bannerAd, let screen = rootViewController?.view { + if let ad = bannerAd { ad.delegate = self - - screen.addSubview(ad) - - ad.translatesAutoresizingMaskIntoConstraints = false - - let bottomAnchor: NSLayoutYAxisAnchor - if #available(iOS 11.0, *) { - bottomAnchor = screen.safeAreaLayoutGuide.bottomAnchor - } else { - bottomAnchor = screen.bottomAnchor - } - var offset = anchorOffset - if (anchorType == 0) { - offset = -anchorOffset - } - let height = (UIDevice.current.userInterfaceIdiom == .pad) ? 90 : 50 - let width = UIScreen.main.bounds.width - NSLog("Ads !!!!!!!!! anchorType +\(anchorType) 2222222 \(anchorType == 0)") - NSLayoutConstraint.activate([ - ad.centerXAnchor.constraint(equalTo: screen.centerXAnchor, constant: CGFloat(horizontalCenterOffset)), - //anchorType == 0 ? guide.bottomAnchor : guide.topAnchor - ad.bottomAnchor.constraint(equalTo: bottomAnchor, constant: CGFloat(offset)), - ad.heightAnchor.constraint(equalToConstant: CGFloat(height)), - ad.widthAnchor.constraint(equalToConstant: CGFloat(width)) - ]) - + ad.revenueDelegate = self ad.tag = id ad.isHidden = true ad.stopAutoRefresh() @@ -96,11 +77,50 @@ public class BannerAd: NSObject, MAAdViewAdDelegate, MAAdRevenueDelegate { } func show() { - if let bannerAd = bannerAd, bannerAd.isHidden{ + if let bannerAd = bannerAd, bannerAd.isHidden { + let delegate = UIApplication.shared.delegate as? FlutterAppDelegate + let rootViewController = delegate?.window.rootViewController + if let screen = rootViewController?.view, !screen.subviews.contains(bannerAd) { + screen.addSubview(bannerAd) + bannerAd.translatesAutoresizingMaskIntoConstraints = false + + let bottomAnchor: NSLayoutYAxisAnchor + if #available(iOS 11.0, *) { + bottomAnchor = screen.safeAreaLayoutGuide.bottomAnchor + } else { + bottomAnchor = screen.bottomAnchor + } + var offset = anchorOffset + if (anchorType == 0) { + offset = -anchorOffset + } + let height = (UIDevice.current.userInterfaceIdiom == .pad) ? 90 : 50 + let width = UIScreen.main.bounds.width + NSLog("Ads !!!!!!!!! anchorType +\(anchorType) 2222222 \(anchorType == 0)") + NSLayoutConstraint.activate([ + bannerAd.centerXAnchor.constraint(equalTo: screen.centerXAnchor, constant: CGFloat(horizontalCenterOffset)), + //anchorType == 0 ? guide.bottomAnchor : guide.topAnchor + bannerAd.bottomAnchor.constraint(equalTo: bottomAnchor, constant: CGFloat(offset)), + bannerAd.heightAnchor.constraint(equalToConstant: CGFloat(height)), + bannerAd.widthAnchor.constraint(equalToConstant: CGFloat(width)) + ]) + } bannerAd.isHidden = false bannerAd.startAutoRefresh() } } + + func updateOrientation(orientation: Int) { + if let ad = bannerAd, let view = ad.viewWithTag(id) { + let hidden = ad.isHidden + ad.isHidden = true + ad.stopAutoRefresh() + view.removeFromSuperview() + if (!hidden) { + show() + } + } + } func hide() { if let bannerAd = bannerAd, !bannerAd.isHidden{ @@ -116,6 +136,7 @@ public class BannerAd: NSObject, MAAdViewAdDelegate, MAAdRevenueDelegate { } ba.stopAutoRefresh() ba.delegate = nil + ba.revenueDelegate = nil ba.isHidden = true if let view = bannerAd!.viewWithTag(id) { view.removeFromSuperview() diff --git a/guru_app/plugins/guru_applovin_flutter/ios/Classes/SwiftGuruApplovinFlutterPlugin.swift b/guru_app/plugins/guru_applovin_flutter/ios/Classes/SwiftGuruApplovinFlutterPlugin.swift index bc0b62a..c15e605 100644 --- a/guru_app/plugins/guru_applovin_flutter/ios/Classes/SwiftGuruApplovinFlutterPlugin.swift +++ b/guru_app/plugins/guru_applovin_flutter/ios/Classes/SwiftGuruApplovinFlutterPlugin.swift @@ -95,6 +95,9 @@ public class SwiftGuruApplovinFlutterPlugin: NSObject, FlutterPlugin { case "getBannerAdSize": callGetBannerAdSize(call, result: result) break; + case "updateeOrientation": + callClearTargetingData(call, result: result) + break; default: result(FlutterMethodNotImplemented) } @@ -550,11 +553,22 @@ public class SwiftGuruApplovinFlutterPlugin: NSObject, FlutterPlugin { ALSdk.shared()?.targetingData.clearAll() result(true) } + + private func callUpdateOrientation(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + guard let orientation = arguments["orientation"] as? Int else { + result(FlutterError(code: "invalid_orientation", message: "a null or empty Orientation was provided", details: nil)) + return + } + BannerAd.orientationChanged(orientation: orientation) + result(true) + } private func callGetBannerAdSize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let height = (UIDevice.current.userInterfaceIdiom == .pad) ? 90.0 : 50.0 let width = UIScreen.main.bounds.width result(["width": width, "height": height] as [String : Any]) } + } diff --git a/guru_app/plugins/guru_applovin_flutter/ios/guru_applovin_flutter.podspec b/guru_app/plugins/guru_applovin_flutter/ios/guru_applovin_flutter.podspec index 3353cb0..e2001d0 100644 --- a/guru_app/plugins/guru_applovin_flutter/ios/guru_applovin_flutter.podspec +++ b/guru_app/plugins/guru_applovin_flutter/ios/guru_applovin_flutter.podspec @@ -33,11 +33,12 @@ A new flutter plugin project. s.dependency 'AppLovinMediationAmazonAdMarketplaceAdapter', '4.7.5.0' s.dependency 'AppLovinMediationBidMachineAdapter', '2.3.0.2.0' s.dependency 'AppLovinMediationMobileFuseAdapter', '1.5.2.0' - s.dependency 'MolocoCustomAdapterAppLovin', '1.3.0.0' + s.dependency 'MolocoCustomAdapterAppLovin', '1.4.0.0' s.dependency 'AppLovinMediationOguryPresageAdapter', '4.2.2.0' s.dependency 'OpenWrapSDK', '3.1.0' s.dependency 'AppLovinPubMaticAdapter', '1.1.0' s.dependency 'GuruConsent', '1.4.1' + s.dependency 'TradPlusAdSDK', '10.6.0' s.platform = :ios, '11.0' diff --git a/guru_app/plugins/guru_applovin_flutter/lib/guru_applovin_flutter.dart b/guru_app/plugins/guru_applovin_flutter/lib/guru_applovin_flutter.dart index d0c39da..324d1ba 100644 --- a/guru_app/plugins/guru_applovin_flutter/lib/guru_applovin_flutter.dart +++ b/guru_app/plugins/guru_applovin_flutter/lib/guru_applovin_flutter.dart @@ -37,6 +37,11 @@ class AdStatus { } } +class ScreenOrientation { + static const int portrait = 0; + static const int landscape = 1; +} + class GuruApplovinFlutter { @visibleForTesting GuruApplovinFlutter.private(MethodChannel channel) : _channel = channel { @@ -142,6 +147,10 @@ class GuruApplovinFlutter { return Size(result["width"] ?? -1, result["height"] ?? -1); } + Future updateOrientation(int orientation) async { + return await invokeBooleanMethod("updateOrientation", orientation); + } + Future _handleMethod(MethodCall call) { assert(call.arguments is Map); final Map argumentsMap = call.arguments; diff --git a/guru_app/plugins/guru_platform_data/android/build.gradle b/guru_app/plugins/guru_platform_data/android/build.gradle index 3fef5c5..ec2bb2e 100644 --- a/guru_app/plugins/guru_platform_data/android/build.gradle +++ b/guru_app/plugins/guru_platform_data/android/build.gradle @@ -50,5 +50,6 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'guru.core.checker:GuruChecker:1.0.0' + implementation 'androidx.preference:preference-ktx:1.2.1' } diff --git a/guru_app/plugins/guru_platform_data/android/src/main/kotlin/app/guru/guru_platform_data/GuruPlatformData.kt b/guru_app/plugins/guru_platform_data/android/src/main/kotlin/app/guru/guru_platform_data/GuruPlatformData.kt index 62ca270..f27337c 100644 --- a/guru_app/plugins/guru_platform_data/android/src/main/kotlin/app/guru/guru_platform_data/GuruPlatformData.kt +++ b/guru_app/plugins/guru_platform_data/android/src/main/kotlin/app/guru/guru_platform_data/GuruPlatformData.kt @@ -20,6 +20,8 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result +import android.content.SharedPreferences +import androidx.preference.PreferenceManager /** GuruPlatformDataPlugin */ class GuruPlatformData : FlutterPlugin, MethodCallHandler, ActivityAware { @@ -39,6 +41,7 @@ class GuruPlatformData : FlutterPlugin, MethodCallHandler, ActivityAware { const val KeepScreenOn = "keep_screen_on" const val GetLocalTimezone = "getLocalTimezone" const val GetAvailableTimezones = "getAvailableTimezones" + const val GetPurposeConsents = "get_purpose_consents" } } @@ -143,6 +146,24 @@ class GuruPlatformData : FlutterPlugin, MethodCallHandler, ActivityAware { } } + MethodNames.GetPurposeConsents -> { + handler.post { + val purposeConsents = activity?.let { + val sharedPref: SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(it) + // Example value: "1111111111" + return@let sharedPref.getString("IABTCF_PurposeConsents", "") + } ?: "" + + result.success(purposeConsents) + // Purposes are zero-indexed. Index 0 contains information about Purpose 1. + // if (!purposeConsents.isEmpty()) { + // val purposeOneString: String = purposeConsents.charAt(0) + // val hasConsentForPurposeOne = purposeOneString.equals("1") + // } + } + } + else -> { handler.post { result.error("not impl!!!", "${call.method} not impl!", "") diff --git a/guru_app/plugins/guru_platform_data/ios/Classes/SwiftGuruPlatformDataPlugin.swift b/guru_app/plugins/guru_platform_data/ios/Classes/SwiftGuruPlatformDataPlugin.swift index 1bae2a4..4e0d917 100644 --- a/guru_app/plugins/guru_platform_data/ios/Classes/SwiftGuruPlatformDataPlugin.swift +++ b/guru_app/plugins/guru_platform_data/ios/Classes/SwiftGuruPlatformDataPlugin.swift @@ -68,6 +68,11 @@ public class SwiftGuruPlatformDataPlugin: NSObject, FlutterPlugin { let knownTimeZoneNames = TimeZone.knownTimeZoneIdentifiers result(knownTimeZoneNames) break; + case "get_purpose_consents": + let purposeConsents = UserDefaults.standard.string(forKey: "IABTCF_PurposeConsents") + // Purposes are zero-indexed. Index 0 contains information about Purpose 1. + result(purposeConsents) + break; default: result(FlutterMethodNotImplemented) } diff --git a/guru_app/plugins/guru_platform_data/lib/guru_platform_data.dart b/guru_app/plugins/guru_platform_data/lib/guru_platform_data.dart index 2ec4bd0..dcbf001 100644 --- a/guru_app/plugins/guru_platform_data/lib/guru_platform_data.dart +++ b/guru_app/plugins/guru_platform_data/lib/guru_platform_data.dart @@ -43,6 +43,7 @@ class GuruPlatformData { static const _resetBrightness = "reset_brightness"; static const _isKeptScreenOn = "is_kept_screen_on"; static const _keepScreenOn = "keep_screen_on"; + static const _getPurposeConsents = "get_purpose_consents"; static Future getAdsLimitTracking() { return perform.invokeMethod(_getAdsLimitTracking); @@ -63,8 +64,7 @@ class GuruPlatformData { static Future getTrackingAuthorizationStatus() async { if (Platform.isIOS) { - return (await perform.invokeMethod( - _getTrackingAuthorizationStatus)) ?? + return (await perform.invokeMethod(_getTrackingAuthorizationStatus)) ?? TrackingAuthorizationStatus.unknown; } else { return TrackingAuthorizationStatus.authorized; @@ -85,8 +85,7 @@ class GuruPlatformData { static Future isAppInstalled(String packageName) async { if (Platform.isAndroid) { - return (await perform.invokeMethod( - _isAppInstalled, {"package_name": packageName})) ?? + return (await perform.invokeMethod(_isAppInstalled, {"package_name": packageName})) ?? false; } else { return false; @@ -95,8 +94,7 @@ class GuruPlatformData { static Future jumpToAppPrivacySettings() async { if (Platform.isIOS) { - return await perform.invokeMethod(_jumpToAppPrivacySettings) ?? - false; + return await perform.invokeMethod(_jumpToAppPrivacySettings) ?? false; } else { return true; } @@ -107,13 +105,13 @@ class GuruPlatformData { } static Future setBrightness(double brightness) async { - return await perform.invokeMethod( - _setBrightness, {"brightness": brightness.clamp(0.0, 1.0)}) ?? false; + return await perform + .invokeMethod(_setBrightness, {"brightness": brightness.clamp(0.0, 1.0)}) ?? + false; } static Future keepScreenOn(bool on) async { - return await perform.invokeMethod( - _keepScreenOn, {"on": on}) ?? false; + return await perform.invokeMethod(_keepScreenOn, {"on": on}) ?? false; } static Future isKeptScreenOn() async { @@ -124,8 +122,7 @@ class GuruPlatformData { /// Returns local timezone from the native layer. /// static Future getLocalTimezone() async { - final String? localTimezone = - await perform.invokeMethod("getLocalTimezone"); + final String? localTimezone = await perform.invokeMethod("getLocalTimezone"); if (localTimezone == null) { throw ArgumentError("Invalid return from platform getLocalTimezone()"); } @@ -137,12 +134,14 @@ class GuruPlatformData { /// static Future> getAvailableTimezones() async { final List? availableTimezones = - await perform.invokeListMethod("getAvailableTimezones"); + await perform.invokeListMethod("getAvailableTimezones"); if (availableTimezones == null) { - throw ArgumentError( - "Invalid return from platform getAvailableTimezones()"); + throw ArgumentError("Invalid return from platform getAvailableTimezones()"); } return availableTimezones; } + static Future getPurposeConsents() async { + return await perform.invokeMethod(_getPurposeConsents) ?? ""; + } } diff --git a/guru_app/pubspec.lock b/guru_app/pubspec.lock index b46b001..c45170b 100644 --- a/guru_app/pubspec.lock +++ b/guru_app/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.flutter-io.cn" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.flutter-io.cn" source: hosted - version: "8.8.1" + version: "8.9.1" cached_network_image: dependency: transitive description: @@ -205,26 +205,26 @@ packages: dependency: transitive description: name: cloud_firestore_platform_interface - sha256: "73ff438fe46028f0e19f55da18b6ddc6906ab750562cd7d9ffab77ff8c0c4307" + sha256: fa177fa85f7665c76e1ebec252a5b280b4b47612b4d70fe286944814fff1d4f2 url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.0" + version: "6.0.10" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - sha256: "232e45e95970d3a6baab8f50f9c3a6e2838d145d9d91ec9a7392837c44296397" + sha256: d0ebbf0927e627c0d7d2f3177d3b6f0050e5d811c08c2b646b0c746a2b502cb7 url: "https://pub.flutter-io.cn" source: hosted - version: "3.9.0" + version: "3.8.10" code_builder: dependency: transitive description: name: code_builder - sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.flutter-io.cn" source: hosted - version: "4.9.0" + version: "4.10.0" collection: dependency: transitive description: @@ -292,21 +292,21 @@ packages: design: dependency: transitive description: - path: "guru_ui/packages/design" + path: "packages/design" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: dbdf35db67b183ee523e08acc5b55fe2fd7bfc98 + url: "git@github.com:castbox/guru_ui.git" source: git - version: "2.0.2" + version: "3.0.0" design_spec: dependency: transitive description: - path: "guru_ui/packages/design_spec" + path: "packages/design_spec" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: dbdf35db67b183ee523e08acc5b55fe2fd7bfc98 + url: "git@github.com:castbox/guru_ui.git" source: git - version: "2.0.2" + version: "3.0.0" device_apps: dependency: transitive description: @@ -439,10 +439,10 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 + sha256: "57e61d6010e253b36d38191cefd6199d7849152cdcd234b61ca290cdb278a0ba" url: "https://pub.flutter-io.cn" source: hosted - version: "2.10.0" + version: "2.11.4" firebase_crashlytics: dependency: "direct main" description: @@ -475,22 +475,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.2.6+16" - firebase_in_app_messaging: - dependency: "direct main" - description: - name: firebase_in_app_messaging - sha256: "1d82ecbb72e2de2161c07c1aab056797aefc80b97cc87772525028882412ec3d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.7.4+8" - firebase_in_app_messaging_platform_interface: - dependency: transitive - description: - name: firebase_in_app_messaging_platform_interface - sha256: "7e7aa5338b8aa82ccee5b5c9468bae27acf5ec710208317ffbe72f51f29e97d6" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.4+16" firebase_messaging: dependency: "direct main" description: @@ -547,6 +531,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" + flame: + dependency: transitive + description: + name: flame + sha256: "24ce7b69bddc4f23aebdd9317fc24f08c37d4b42239baa00a34b768b6de9af85" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" flutter: dependency: "direct main" description: flutter @@ -649,10 +641,10 @@ packages: guru_applifecycle_flutter: dependency: transitive description: - path: "guru_app/plugins/guru_applifecycle_flutter" + path: "plugins/guru_applifecycle_flutter" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: f42e3a2f80abf04c730ae8f18ece5e47cf4e2a85 + url: "git@github.com:castbox/guru_app.git" source: git version: "0.0.1" guru_applovin_flutter: @@ -665,10 +657,10 @@ packages: guru_navigator: dependency: "direct main" description: - path: "guru_app/plugins/guru_navigator" + path: "plugins/guru_navigator" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: f42e3a2f80abf04c730ae8f18ece5e47cf4e2a85 + url: "git@github.com:castbox/guru_app.git" source: git version: "0.0.1" guru_platform_data: @@ -681,12 +673,12 @@ packages: guru_popup: dependency: "direct main" description: - path: "guru_ui/packages/guru_popup" + path: "packages/guru_popup" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: dbdf35db67b183ee523e08acc5b55fe2fd7bfc98 + url: "git@github.com:castbox/guru_ui.git" source: git - version: "2.3.0" + version: "3.0.0" guru_spec: dependency: "direct dev" description: @@ -704,12 +696,12 @@ packages: guru_widgets: dependency: transitive description: - path: "guru_ui/packages/guru_widgets" + path: "packages/guru_widgets" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: dbdf35db67b183ee523e08acc5b55fe2fd7bfc98 + url: "git@github.com:castbox/guru_ui.git" source: git - version: "2.2.0" + version: "3.0.0" http: dependency: transitive description: @@ -762,18 +754,18 @@ packages: dependency: transitive description: name: in_app_purchase_platform_interface - sha256: "5168afbc54f406f741252b66d41872c1193a0066a6edcb587176290b92e2d537" + sha256: "412efce2b9238c5ace4f057acad43f793ed06880e366d26ae322e796cadb051a" url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.6" + version: "1.3.7" in_app_purchase_storekit: dependency: transitive description: name: in_app_purchase_storekit - sha256: "4c29a82fcc68484fc3ac9ae5c2eab8bb3954e3a3d6059d1a31c8383630697fa6" + sha256: c4b17a7f2ca8ddc7fd7996a8c32a3af6beddf91d651997c8675a5f23c103c9bc url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.8" + version: "0.3.8+1" intl: dependency: transitive description: @@ -874,10 +866,18 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.4" + version: "1.0.5" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.4.4" nm: dependency: transitive description: @@ -894,6 +894,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" + ordered_set: + dependency: transitive + description: + name: ordered_set + sha256: "3858c7d84619edfab87c3e367584648020903187edb70b52697646f4b2a93022" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.2" package_config: dependency: transitive description: @@ -954,10 +962,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -970,10 +978,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -994,49 +1002,49 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6" + sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" url: "https://pub.flutter-io.cn" source: hosted - version: "12.0.1" + version: "12.0.5" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306" + sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b url: "https://pub.flutter-io.cn" source: hosted - version: "9.2.0" + version: "9.4.0" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df" + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.0+2" + version: "0.1.1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1 + sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.2" + version: "4.2.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004" + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.0" + version: "0.2.1" persistent: dependency: transitive description: - path: "guru_app/plugins/persistent" + path: "plugins/persistent" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: f42e3a2f80abf04c730ae8f18ece5e47cf4e2a85 + url: "git@github.com:castbox/guru_app.git" source: git version: "0.0.1" petitparser: @@ -1067,10 +1075,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.flutter-io.cn" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -1151,10 +1159,10 @@ packages: soundpool: dependency: transitive description: - path: "guru_app/plugins/soundpool" + path: "plugins/soundpool" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: f42e3a2f80abf04c730ae8f18ece5e47cf4e2a85 + url: "git@github.com:castbox/guru_app.git" source: git version: "2.3.0" soundpool_macos: @@ -1225,10 +1233,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.0+2" + version: "2.5.3" stack_trace: dependency: transitive description: @@ -1297,10 +1305,10 @@ packages: dependency: transitive description: name: time - sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.3" + version: "2.1.4" timing: dependency: transitive description: @@ -1337,18 +1345,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: c0766a55ab42cefaa728cabc951e82919ab41a3a4fee0aaa96176ca82da8cc51 + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.flutter-io.cn" source: hosted - version: "6.2.1" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "46b81e3109cbb2d6b81702ad3077540789a3e74e22795eb9f0b7d494dbaa72ea" + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" url: "https://pub.flutter-io.cn" source: hosted - version: "6.2.2" + version: "6.2.4" url_launcher_linux: dependency: transitive description: @@ -1369,10 +1377,10 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: @@ -1408,10 +1416,10 @@ packages: vibration: dependency: transitive description: - path: "guru_app/plugins/vibration" + path: "plugins/vibration" ref: "v3.0.0" - resolved-ref: "4839b05d54b6eaf32d5f40a5d3c5e1b9caadb3b7" - url: "git@git.chengdu.pundit.company:castbox/guru_sdk.git" + resolved-ref: f42e3a2f80abf04c730ae8f18ece5e47cf4e2a85 + url: "git@github.com:castbox/guru_app.git" source: git version: "1.7.5" vibration_web: @@ -1442,10 +1450,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: cc1f6c632a248278a091fd7d9a68f624906830f7c1c5aa66503fae0804633e1c + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.2" + version: "2.4.0" webview_flutter: dependency: transitive description: @@ -1458,18 +1466,18 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: "161af93c2abaf94ef2192bffb53a3658b2d721a3bf99b69aa1e47814ee18cc96" + sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0" url: "https://pub.flutter-io.cn" source: hosted - version: "3.13.2" + version: "3.15.0" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "80b40ae4fb959957eef9fa8970b6c9accda9f49fc45c2b75154696a8e8996cfe" + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d url: "https://pub.flutter-io.cn" source: hosted - version: "2.9.1" + version: "2.10.0" webview_flutter_wkwebview: dependency: transitive description: diff --git a/guru_app/pubspec.yaml b/guru_app/pubspec.yaml index c98f48e..ce5c383 100644 --- a/guru_app/pubspec.yaml +++ b/guru_app/pubspec.yaml @@ -17,7 +17,6 @@ dependencies: firebase_remote_config: 4.3.8 firebase_messaging: 14.7.9 firebase_dynamic_links: 5.4.8 - firebase_in_app_messaging: 0.7.4+8 firebase_auth: 4.15.3 cloud_firestore: 4.13.6 @@ -69,7 +68,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.0 - + mockito: 5.4.4 guru_spec: path: packages/guru_spec diff --git a/guru_app/test/analytics/dma_test.dart b/guru_app/test/analytics/dma_test.dart new file mode 100644 index 0000000..d1cadaa --- /dev/null +++ b/guru_app/test/analytics/dma_test.dart @@ -0,0 +1,122 @@ +/// Created by Haoyi on 2023/5/5 + +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:guru_app/analytics/data/analytics_model.dart'; +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_app/property/property_keys.dart'; +import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/property/app_property.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:logger/logger.dart'; + +import 'dma_test.mocks.dart'; + +class MockPurposeConsents { + static String purposeConsents = "0"; +} + +class ConsentTest { + final String purpose; + final String expect; + + ConsentTest(this.purpose, this.expect); +} + +@GenerateMocks([GuruApp, AppSpec, Deployment, AdjustProfile, AppProperty]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + Log.init("DMA-TEST", persistentLevel: LogLevel.nothing); + + MethodChannel platformDataChannel = const MethodChannel('app.guru.guru_platform_data'); + TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + .setMockMethodCallHandler(platformDataChannel, (call) async { + switch (call.method) { + case "get_purpose_consents": + return MockPurposeConsents.purposeConsents; + } + }); + + MethodChannel adjustChannel = const MethodChannel('com.adjust.sdk/api'); + TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + .setMockMethodCallHandler(adjustChannel, (call) async { + switch (call.method) { + case "trackThirdPartySharing": + print(call.arguments); + break; + } + }); + + const analyticChannel = MethodChannel("flutter.guru.guru_analytics_flutter"); + TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + .setMockMethodCallHandler(analyticChannel, (call) async { + print("${call.method} ${call.arguments}"); + return true; + }); + + final guruApp = MockGuruApp(); + GuruApp.setMockInstance(guruApp); + final MockAdjustProfile adjustProfile = MockAdjustProfile(); + when(adjustProfile.isEnabled).thenReturn(true); + + final MockAppSpec appSpec = MockAppSpec(); + final MockDeployment deployment = MockDeployment(); + + when(deployment.enableAnalyticsStatistic).thenReturn(false); + when(appSpec.deployment).thenReturn(deployment); + when(guruApp.appSpec).thenReturn(appSpec); + + final MockAppProperty mockAppProperty = MockAppProperty(); + when(mockAppProperty.getString(PropertyKeys.googleDma, defValue: anyNamed('defValue'))) + .thenAnswer((_) async => ""); + when(mockAppProperty.setString(PropertyKeys.googleDma, any)).thenAnswer((_) async => true); + AppProperty.setMock(mockAppProperty); + GuruAnalytics.setMock(); + test("Test Dma", () async { + when(guruApp.adjustProfile).thenReturn(adjustProfile); + final consents = [ + ConsentTest("0", "0100"), + ConsentTest("0000001", "0100"), + ConsentTest("00000010", "0100"), + ConsentTest("00000010000", "0100"), + ConsentTest("0010001", "0100"), + ConsentTest("0001001", "0100"), + ConsentTest("00100010000", "0100"), + ConsentTest("00010010000", "0100"), + ConsentTest("0011001", "0110"), + ConsentTest("00110010", "0110"), + ConsentTest("00110010000", "0110"), + ConsentTest("1", "1100"), + ConsentTest("10", "1100"), + ConsentTest("1000001", "1101"), + ConsentTest("10000010", "1101"), + ConsentTest("10000010000", "1101"), + ConsentTest("1010001", "1101"), + ConsentTest("1001001", "1101"), + ConsentTest("10100010000", "1101"), + ConsentTest("10010010000", "1101"), + ConsentTest("1011001", "1111"), + ConsentTest("10110010", "1111"), + ConsentTest("10110010000", "1111"), + ConsentTest("11111111111", "1111"), + ConsentTest("1011001000000000011100", "1111"), + ConsentTest("1010001000000000001110", "1101"), + ConsentTest("adfadf2132", ""), + ]; + + final analyticsConfig = json.decode( + '{"cap":"firebase|facebook|guru", "init_delay_s": 10, "google_dma": [1, 0, 12, 65], "dma_country": []}'); + GuruAnalytics.mockCountryCode = "cn"; + for (int i = 0; i < consents.length; ++i) { + MockPurposeConsents.purposeConsents = consents[i].purpose; + String result = await GuruAnalytics.instance + .refreshConsents(analyticsConfig: AnalyticsConfig.fromJson(analyticsConfig)); + expect(result, consents[i].expect); + } + }); +} diff --git a/guru_app/test/analytics/dma_test.mocks.dart b/guru_app/test/analytics/dma_test.mocks.dart new file mode 100644 index 0000000..cc98a36 --- /dev/null +++ b/guru_app/test/analytics/dma_test.mocks.dart @@ -0,0 +1,1057 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in guru_app/test/analytics/dma_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i12; +import 'dart:ui' as _i10; + +import 'package:flutter/material.dart' as _i11; +import 'package:guru_app/account/model/user.dart' as _i13; +import 'package:guru_app/analytics/abtest/abtest_model.dart' as _i15; +import 'package:guru_app/analytics/guru_analytics.dart' as _i6; +import 'package:guru_app/app/app_models.dart' as _i4; +import 'package:guru_app/financial/product/product_model.dart' as _i7; +import 'package:guru_app/firebase/firebase.dart' as _i16; +import 'package:guru_app/guru_app.dart' as _i3; +import 'package:guru_app/property/app_property.dart' as _i8; +import 'package:guru_utils/ads/ads.dart' as _i5; +import 'package:guru_utils/auth/auth_credential_manager.dart' as _i14; +import 'package:guru_utils/packages/guru_package.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRootPackage_0 extends _i1.SmartFake implements _i2.RootPackage { + _FakeRootPackage_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeAppSpec_1 extends _i1.SmartFake implements _i3.AppSpec { + _FakeAppSpec_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeIGuruSdkProtocol_2 extends _i1.SmartFake + implements _i3.IGuruSdkProtocol { + _FakeIGuruSdkProtocol_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRemoteDeployment_3 extends _i1.SmartFake + implements _i4.RemoteDeployment { + _FakeRemoteDeployment_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeAppDetails_4 extends _i1.SmartFake implements _i4.AppDetails { + _FakeAppDetails_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeAdsProfile_5 extends _i1.SmartFake implements _i5.AdsProfile { + _FakeAdsProfile_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeAdjustProfile_6 extends _i1.SmartFake implements _i6.AdjustProfile { + _FakeAdjustProfile_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeProductProfile_7 extends _i1.SmartFake + implements _i7.ProductProfile { + _FakeProductProfile_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDeployment_8 extends _i1.SmartFake implements _i4.Deployment { + _FakeDeployment_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePropertyBundle_9 extends _i1.SmartFake + implements _i8.PropertyBundle { + _FakePropertyBundle_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [GuruApp]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGuruApp extends _i1.Mock implements _i3.GuruApp { + MockGuruApp() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.RootPackage get rootPackage => (super.noSuchMethod( + Invocation.getter(#rootPackage), + returnValue: _FakeRootPackage_0( + this, + Invocation.getter(#rootPackage), + ), + ) as _i2.RootPackage); + + @override + _i3.AppSpec get appSpec => (super.noSuchMethod( + Invocation.getter(#appSpec), + returnValue: _FakeAppSpec_1( + this, + Invocation.getter(#appSpec), + ), + ) as _i3.AppSpec); + + @override + _i3.IGuruSdkProtocol get protocol => (super.noSuchMethod( + Invocation.getter(#protocol), + returnValue: _FakeIGuruSdkProtocol_2( + this, + Invocation.getter(#protocol), + ), + ) as _i3.IGuruSdkProtocol); + + @override + _i4.RemoteDeployment get remoteDeployment => (super.noSuchMethod( + Invocation.getter(#remoteDeployment), + returnValue: _FakeRemoteDeployment_3( + this, + Invocation.getter(#remoteDeployment), + ), + ) as _i4.RemoteDeployment); + + @override + String get appName => (super.noSuchMethod( + Invocation.getter(#appName), + returnValue: _i9.dummyValue( + this, + Invocation.getter(#appName), + ), + ) as String); + + @override + String get flavor => (super.noSuchMethod( + Invocation.getter(#flavor), + returnValue: _i9.dummyValue( + this, + Invocation.getter(#flavor), + ), + ) as String); + + @override + _i4.AppDetails get details => (super.noSuchMethod( + Invocation.getter(#details), + returnValue: _FakeAppDetails_4( + this, + Invocation.getter(#details), + ), + ) as _i4.AppDetails); + + @override + _i5.AdsProfile get adsProfile => (super.noSuchMethod( + Invocation.getter(#adsProfile), + returnValue: _FakeAdsProfile_5( + this, + Invocation.getter(#adsProfile), + ), + ) as _i5.AdsProfile); + + @override + _i6.AdjustProfile get adjustProfile => (super.noSuchMethod( + Invocation.getter(#adjustProfile), + returnValue: _FakeAdjustProfile_6( + this, + Invocation.getter(#adjustProfile), + ), + ) as _i6.AdjustProfile); + + @override + _i7.ProductProfile get productProfile => (super.noSuchMethod( + Invocation.getter(#productProfile), + returnValue: _FakeProductProfile_7( + this, + Invocation.getter(#productProfile), + ), + ) as _i7.ProductProfile); + + @override + Map get defaultRemoteConfig => (super.noSuchMethod( + Invocation.getter(#defaultRemoteConfig), + returnValue: {}, + ) as Map); + + @override + Set get conversionEvents => (super.noSuchMethod( + Invocation.getter(#conversionEvents), + returnValue: {}, + ) as Set); + + @override + Iterable<_i10.Locale> get supportedLocales => (super.noSuchMethod( + Invocation.getter(#supportedLocales), + returnValue: <_i10.Locale>[], + ) as Iterable<_i10.Locale>); + + @override + Iterable<_i11.LocalizationsDelegate> get localizationsDelegates => + (super.noSuchMethod( + Invocation.getter(#localizationsDelegates), + returnValue: <_i11.LocalizationsDelegate>[], + ) as Iterable<_i11.LocalizationsDelegate>); + + @override + String getRemoteConfigKey(String? key) => (super.noSuchMethod( + Invocation.method( + #getRemoteConfigKey, + [key], + ), + returnValue: _i9.dummyValue( + this, + Invocation.method( + #getRemoteConfigKey, + [key], + ), + ), + ) as String); + + @override + _i4.RemoteDeployment refreshRemoteDeployment() => (super.noSuchMethod( + Invocation.method( + #refreshRemoteDeployment, + [], + ), + returnValue: _FakeRemoteDeployment_3( + this, + Invocation.method( + #refreshRemoteDeployment, + [], + ), + ), + ) as _i4.RemoteDeployment); + + @override + _i12.Future switchAccount( + _i13.GuruUser? user, + _i14.Credential? credential, { + _i13.GuruUser? oldUser, + }) => + (super.noSuchMethod( + Invocation.method( + #switchAccount, + [ + user, + credential, + ], + {#oldUser: oldUser}, + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + void showToast( + String? message, { + Duration? duration = const Duration(seconds: 3), + }) => + super.noSuchMethod( + Invocation.method( + #showToast, + [message], + {#duration: duration}, + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AppSpec]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAppSpec extends _i1.Mock implements _i3.AppSpec { + MockAppSpec() { + _i1.throwOnMissingStub(this); + } + + @override + String get appName => (super.noSuchMethod( + Invocation.getter(#appName), + returnValue: _i9.dummyValue( + this, + Invocation.getter(#appName), + ), + ) as String); + + @override + _i3.AppCategory get appCategory => (super.noSuchMethod( + Invocation.getter(#appCategory), + returnValue: _i3.AppCategory.game, + ) as _i3.AppCategory); + + @override + String get flavor => (super.noSuchMethod( + Invocation.getter(#flavor), + returnValue: _i9.dummyValue( + this, + Invocation.getter(#flavor), + ), + ) as String); + + @override + _i4.AppDetails get details => (super.noSuchMethod( + Invocation.getter(#details), + returnValue: _FakeAppDetails_4( + this, + Invocation.getter(#details), + ), + ) as _i4.AppDetails); + + @override + _i5.AdsProfile get adsProfile => (super.noSuchMethod( + Invocation.getter(#adsProfile), + returnValue: _FakeAdsProfile_5( + this, + Invocation.getter(#adsProfile), + ), + ) as _i5.AdsProfile); + + @override + _i7.ProductProfile get productProfile => (super.noSuchMethod( + Invocation.getter(#productProfile), + returnValue: _FakeProductProfile_7( + this, + Invocation.getter(#productProfile), + ), + ) as _i7.ProductProfile); + + @override + _i6.AdjustProfile get adjustProfile => (super.noSuchMethod( + Invocation.getter(#adjustProfile), + returnValue: _FakeAdjustProfile_6( + this, + Invocation.getter(#adjustProfile), + ), + ) as _i6.AdjustProfile); + + @override + _i4.Deployment get deployment => (super.noSuchMethod( + Invocation.getter(#deployment), + returnValue: _FakeDeployment_8( + this, + Invocation.getter(#deployment), + ), + ) as _i4.Deployment); + + @override + Map get defaultRemoteConfig => (super.noSuchMethod( + Invocation.getter(#defaultRemoteConfig), + returnValue: {}, + ) as Map); + + @override + Map get localABTestExperiments => + (super.noSuchMethod( + Invocation.getter(#localABTestExperiments), + returnValue: {}, + ) as Map); + + @override + String getRemoteConfigKey(String? key) => (super.noSuchMethod( + Invocation.method( + #getRemoteConfigKey, + [key], + ), + returnValue: _i9.dummyValue( + this, + Invocation.method( + #getRemoteConfigKey, + [key], + ), + ), + ) as String); +} + +/// A class which mocks [Deployment]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDeployment extends _i1.Mock implements _i4.Deployment { + MockDeployment() { + _i1.throwOnMissingStub(this); + } + + @override + int get propertyCacheSize => (super.noSuchMethod( + Invocation.getter(#propertyCacheSize), + returnValue: 0, + ) as int); + + @override + bool get enableDithering => (super.noSuchMethod( + Invocation.getter(#enableDithering), + returnValue: false, + ) as bool); + + @override + bool get disableRewardsAds => (super.noSuchMethod( + Invocation.getter(#disableRewardsAds), + returnValue: false, + ) as bool); + + @override + bool get enableAnalyticsStatistic => (super.noSuchMethod( + Invocation.getter(#enableAnalyticsStatistic), + returnValue: false, + ) as bool); + + @override + bool get autoRestoreIap => (super.noSuchMethod( + Invocation.getter(#autoRestoreIap), + returnValue: false, + ) as bool); + + @override + int get initIgc => (super.noSuchMethod( + Invocation.getter(#initIgc), + returnValue: 0, + ) as int); + + @override + int get igcBalanceSecret => (super.noSuchMethod( + Invocation.getter(#igcBalanceSecret), + returnValue: 0, + ) as int); + + @override + bool get syncAccountProfile => (super.noSuchMethod( + Invocation.getter(#syncAccountProfile), + returnValue: false, + ) as bool); + + @override + bool get autoRequestNotificationPermission => (super.noSuchMethod( + Invocation.getter(#autoRequestNotificationPermission), + returnValue: false, + ) as bool); + + @override + int get logFileSizeLimit => (super.noSuchMethod( + Invocation.getter(#logFileSizeLimit), + returnValue: 0, + ) as int); + + @override + int get logFileCount => (super.noSuchMethod( + Invocation.getter(#logFileCount), + returnValue: 0, + ) as int); + + @override + int get persistentLogLevel => (super.noSuchMethod( + Invocation.getter(#persistentLogLevel), + returnValue: 0, + ) as int); + + @override + Set get conversionEvents => (super.noSuchMethod( + Invocation.getter(#conversionEvents), + returnValue: {}, + ) as Set); + + @override + int get apiConnectTimeout => (super.noSuchMethod( + Invocation.getter(#apiConnectTimeout), + returnValue: 0, + ) as int); + + @override + int get apiReceiveTimeout => (super.noSuchMethod( + Invocation.getter(#apiReceiveTimeout), + returnValue: 0, + ) as int); + + @override + int get iosSandboxSubsRenewalSpeed => (super.noSuchMethod( + Invocation.getter(#iosSandboxSubsRenewalSpeed), + returnValue: 0, + ) as int); + + @override + bool get adsCompliantInitialization => (super.noSuchMethod( + Invocation.getter(#adsCompliantInitialization), + returnValue: false, + ) as bool); + + @override + _i16.PromptTrigger get notificationPermissionPromptTrigger => + (super.noSuchMethod( + Invocation.getter(#notificationPermissionPromptTrigger), + returnValue: _i16.PromptTrigger.rationale, + ) as _i16.PromptTrigger); + + @override + bool get trackingNotificationPermissionPass => (super.noSuchMethod( + Invocation.getter(#trackingNotificationPermissionPass), + returnValue: false, + ) as bool); + + @override + int get trackingNotificationPermissionPassLimitTimes => (super.noSuchMethod( + Invocation.getter(#trackingNotificationPermissionPassLimitTimes), + returnValue: 0, + ) as int); + + @override + bool get enabledGuruAnalyticsStrategy => (super.noSuchMethod( + Invocation.getter(#enabledGuruAnalyticsStrategy), + returnValue: false, + ) as bool); + + @override + bool get allowInterstitialAsAlternativeReward => (super.noSuchMethod( + Invocation.getter(#allowInterstitialAsAlternativeReward), + returnValue: false, + ) as bool); + + @override + bool get showInternalAdsWhenBannerUnavailable => (super.noSuchMethod( + Invocation.getter(#showInternalAdsWhenBannerUnavailable), + returnValue: false, + ) as bool); + + @override + int get subscriptionRestoreGraceCount => (super.noSuchMethod( + Invocation.getter(#subscriptionRestoreGraceCount), + returnValue: 0, + ) as int); + + @override + int get fullscreenAdsMinInterval => (super.noSuchMethod( + Invocation.getter(#fullscreenAdsMinInterval), + returnValue: 0, + ) as int); + + @override + int get subscriptionGraceDays => (super.noSuchMethod( + Invocation.getter(#subscriptionGraceDays), + returnValue: 0, + ) as int); + + @override + bool get enabledSyncAccountProfile => (super.noSuchMethod( + Invocation.getter(#enabledSyncAccountProfile), + returnValue: false, + ) as bool); + + @override + Map toJson() => (super.noSuchMethod( + Invocation.method( + #toJson, + [], + ), + returnValue: {}, + ) as Map); +} + +/// A class which mocks [AdjustProfile]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAdjustProfile extends _i1.Mock implements _i6.AdjustProfile { + MockAdjustProfile() { + _i1.throwOnMissingStub(this); + } + + @override + String get appToken => (super.noSuchMethod( + Invocation.getter(#appToken), + returnValue: _i9.dummyValue( + this, + Invocation.getter(#appToken), + ), + ) as String); + + @override + Map get eventNameMapping => + (super.noSuchMethod( + Invocation.getter(#eventNameMapping), + returnValue: {}, + ) as Map); + + @override + bool get isEnabled => (super.noSuchMethod( + Invocation.getter(#isEnabled), + returnValue: false, + ) as bool); +} + +/// A class which mocks [AppProperty]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAppProperty extends _i1.Mock implements _i8.AppProperty { + MockAppProperty() { + _i1.throwOnMissingStub(this); + } + + @override + _i12.Future setInt( + _i8.PropertyKey? key, + int? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setInt, + [ + key, + value, + ], + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future getAndIncrease( + _i8.PropertyKey? key, { + int? defValue = 0, + String? tag, + }) => + (super.noSuchMethod( + Invocation.method( + #getAndIncrease, + [key], + { + #defValue: defValue, + #tag: tag, + }, + ), + returnValue: _i12.Future.value(0), + ) as _i12.Future); + + @override + _i12.Future increaseAndGet( + _i8.PropertyKey? key, { + int? defValue = 0, + }) => + (super.noSuchMethod( + Invocation.method( + #increaseAndGet, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(0), + ) as _i12.Future); + + @override + _i12.Future decreaseAndGet( + _i8.PropertyKey? key, { + int? defValue = 0, + }) => + (super.noSuchMethod( + Invocation.method( + #decreaseAndGet, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(0), + ) as _i12.Future); + + @override + _i12.Future getAndDecrease( + _i8.PropertyKey? key, { + int? defValue = 0, + String? tag, + }) => + (super.noSuchMethod( + Invocation.method( + #getAndDecrease, + [key], + { + #defValue: defValue, + #tag: tag, + }, + ), + returnValue: _i12.Future.value(0), + ) as _i12.Future); + + @override + _i12.Future setDouble( + _i8.PropertyKey? key, + double? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setDouble, + [ + key, + value, + ], + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future setString( + _i8.PropertyKey? key, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setString, + [ + key, + value, + ], + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future setBool( + _i8.PropertyKey? key, + bool? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setBool, + [ + key, + value, + ], + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future setProperties(_i8.PropertyBundle? bundle) => + (super.noSuchMethod( + Invocation.method( + #setProperties, + [bundle], + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future> loadAllValues() => + (super.noSuchMethod( + Invocation.method( + #loadAllValues, + [], + ), + returnValue: _i12.Future>.value( + <_i8.PropertyKey, String>{}), + ) as _i12.Future>); + + @override + _i12.Future> getValues( + List<_i8.PropertyKey>? keys) => + (super.noSuchMethod( + Invocation.method( + #getValues, + [keys], + ), + returnValue: _i12.Future>.value( + <_i8.PropertyKey, String>{}), + ) as _i12.Future>); + + @override + _i12.Future getInt( + _i8.PropertyKey? key, { + required int? defValue, + }) => + (super.noSuchMethod( + Invocation.method( + #getInt, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(0), + ) as _i12.Future); + + @override + _i12.Future getDouble( + _i8.PropertyKey? key, { + required double? defValue, + }) => + (super.noSuchMethod( + Invocation.method( + #getDouble, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(0.0), + ) as _i12.Future); + + @override + _i12.Future getString( + _i8.PropertyKey? key, { + required String? defValue, + }) => + (super.noSuchMethod( + Invocation.method( + #getString, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(_i9.dummyValue( + this, + Invocation.method( + #getString, + [key], + {#defValue: defValue}, + ), + )), + ) as _i12.Future); + + @override + _i12.Future getBool( + _i8.PropertyKey? key, { + required bool? defValue, + }) => + (super.noSuchMethod( + Invocation.method( + #getBool, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future getOrCreateInt( + _i8.PropertyKey? key, + int? ifAbsent, + ) => + (super.noSuchMethod( + Invocation.method( + #getOrCreateInt, + [ + key, + ifAbsent, + ], + ), + returnValue: _i12.Future.value(0), + ) as _i12.Future); + + @override + _i12.Future getOrCreateDouble( + _i8.PropertyKey? key, + double? ifAbsent, + ) => + (super.noSuchMethod( + Invocation.method( + #getOrCreateDouble, + [ + key, + ifAbsent, + ], + ), + returnValue: _i12.Future.value(0.0), + ) as _i12.Future); + + @override + _i12.Future getOrCreateString( + _i8.PropertyKey? key, + String? ifAbsent, + ) => + (super.noSuchMethod( + Invocation.method( + #getOrCreateString, + [ + key, + ifAbsent, + ], + ), + returnValue: _i12.Future.value(_i9.dummyValue( + this, + Invocation.method( + #getOrCreateString, + [ + key, + ifAbsent, + ], + ), + )), + ) as _i12.Future); + + @override + _i12.Future getOrCreateBool( + _i8.PropertyKey? key, + bool? ifAbsent, + ) => + (super.noSuchMethod( + Invocation.method( + #getOrCreateBool, + [ + key, + ifAbsent, + ], + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future<_i8.PropertyBundle> loadValuesByTag(String? tag) => + (super.noSuchMethod( + Invocation.method( + #loadValuesByTag, + [tag], + ), + returnValue: + _i12.Future<_i8.PropertyBundle>.value(_FakePropertyBundle_9( + this, + Invocation.method( + #loadValuesByTag, + [tag], + ), + )), + ) as _i12.Future<_i8.PropertyBundle>); + + @override + _i12.Future<_i8.PropertyBundle> loadValuesByUsage(int? usage) => + (super.noSuchMethod( + Invocation.method( + #loadValuesByUsage, + [usage], + ), + returnValue: + _i12.Future<_i8.PropertyBundle>.value(_FakePropertyBundle_9( + this, + Invocation.method( + #loadValuesByUsage, + [usage], + ), + )), + ) as _i12.Future<_i8.PropertyBundle>); + + @override + _i12.Future remove(_i8.PropertyKey? key) => (super.noSuchMethod( + Invocation.method( + #remove, + [key], + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future removeAllWithTag(String? tag) => (super.noSuchMethod( + Invocation.method( + #removeAllWithTag, + [tag], + ), + returnValue: _i12.Future.value(false), + ) as _i12.Future); + + @override + _i12.Future getBoolOrNull( + _i8.PropertyKey? key, { + bool? defValue, + }) => + (super.noSuchMethod( + Invocation.method( + #getBoolOrNull, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(), + ) as _i12.Future); + + @override + _i12.Future getDoubleOrNull( + _i8.PropertyKey? key, { + double? defValue, + }) => + (super.noSuchMethod( + Invocation.method( + #getDoubleOrNull, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(), + ) as _i12.Future); + + @override + _i12.Future getIntOrNull( + _i8.PropertyKey? key, { + int? defValue, + }) => + (super.noSuchMethod( + Invocation.method( + #getIntOrNull, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(), + ) as _i12.Future); + + @override + _i12.Future getStringOrNull( + _i8.PropertyKey? key, { + String? defValue, + }) => + (super.noSuchMethod( + Invocation.method( + #getStringOrNull, + [key], + {#defValue: defValue}, + ), + returnValue: _i12.Future.value(), + ) as _i12.Future); +} diff --git a/guru_app/tools/bin/pulish_gitea.py b/guru_app/tools/bin/pulish_gitea.py index fe3b087..f63bca5 100755 --- a/guru_app/tools/bin/pulish_gitea.py +++ b/guru_app/tools/bin/pulish_gitea.py @@ -14,7 +14,7 @@ print(f"upgrade castbox library to {newrepo}") # Get the current directory current_dir = os.getcwd() - +# git@git.chengdu.pundit.company:castbox/guru_sdk.git # Iterate through all files in the directory for dirpath, dirnames, filenames in os.walk(current_dir): # Check if the file is a pubspec.yaml file diff --git a/guru_ui/assets/images/ic_purchase_failed.png b/guru_ui/assets/images/ic_purchase_failed.png new file mode 100644 index 0000000000000000000000000000000000000000..e1235c0f21a96736ff9f1ac809d367f42251da2f GIT binary patch literal 2405 zcmdUx`#;p%8pppg#+@;Jb2;=)6Qf*4F)ClAzGtGDNqedxw@@yz?U>Lev17`0WRICK zxs>YFX;5-k>@BiS)C?)tiYR7=6uGp8a@qFxoWJ6nAJ%%m*6X#_^Q`rJUe9_S?olR9 zTf;~L003{f$FkJrW5g#!9&fix76Vd{& z2Y|!&ujvU!TLa-AX z0kygm+zt{zC{a}hQXLR%qyUuzX z(3`iZ;xGtgGHx4&p1=a(&wusYrVg};yS3`9F`WM;x1LCPMusG*A<^!hb*mN~F_Nmu zo2QIt(`6Igu{n~a<;ujw*_D%u7dT)P3g(cFpi<-njycnB(M2(pRs?sWbb9_Km1k2D0)Jh*GD$U64IzKF(W*L^|MR#>*9-YbM#jWn!EXxsJZ%qH5G&H0 znb@b;uEJ48aQobuAPT~^CezuH+Xc6Ad~+yE`DSAZU-n7dp~T%$ZVFQNwq%P`YCQsP z@cFWF=Xx@~SaXV#s#UcGal`uDukN5r=Nx%oHjH!1{k>gDA+R>hfp|jCQ(fJVDLJMg zeVuk3p{CDAi7>+A>Z=02>=($46JiyCZXHNBb>^3+RivhB4MmGpgw54@sEfe5J_@ zOj~`n|I*CTN@;9<{d`xRN+vbBVK3WaJU`q3Bc-~%y5__Mc{%5julbX-`S(VswcWo2 zh=+Fb@5%sa;XUu~A6A8xW&(3L9D1Q)q}02~-OpvpYk^$*d1v=0kS>ab;sm6sP)rDh zP}-R7@-JR+XU^uaAu`K3=%R5b4pfBzhnhCa_hQ;CX$MScHW11)cQZ-(@`lHS^nF%0 z(psEktC)%`d2!7d)=tjm!t)+9HtYlZLbC?IR+=mCkJgSpzb6(loy;`OIjBn?aH*RPWi>H z$50(+8t*nO3SW?A;-JzA8z(8(Z#e0eV(zDak(O07(H`bC4$+kOXAK?Y@Ie6Nc^l;G zhCxJGrZSJa9Ulh=7%)sog)lY$%0+J6FB5m^GEb%Rr{F| zCpEZ45k(7XNZ|zU!-tVPkw1EPt$JK-Lewy?bpuzK5Y;A&I$)xMgTBB;e7X`w5(E43 zrC5yQPZ;9UQ6|9s0x1R~S%yUdI?6cMLm&k(l0{fofCist4E;n$#h~qj4$%*x41g@@ z9as!43DTe+Lb(}wB@iiLc?qzksf98QijDSgWvQsalZv~UVjLmrtX^=A7VSTTN}FO+iCwd&l92sm#>&NwK740yT>#JygGI@ z$>homXN=u)#Ma*3FLn98mjL$To$v{Gft5aSl7eW^P>rt_WGxZhbd!F0P}lm|_yO*=%?%keFY@^BT}wmv{Cx3>FwQ<@f+`yZ^|nlv6;F$-%6 z@5;W@!4x{T_9K+4%Y&LL7i!uYyFH{f&irh;rS6^;wx3xmh)}koznlB^&txY?vDR~- zog-!icD=jI{#ERoUd)N$ihTojHXp3h6J{CaDb*?qv(lOuO+LJ(mxX$R(rffG{Y4`J zrAT^2YaHkN`k<+LX+G1T;l zJ|O`5oT5QJk7`)y6vTvYt`|3h2h&`v(V`G6II(`yrJ}k5kSG(nK#^_h(&P1o;D$tA z!sCcFy3%)O@mqz(tp8H{o7l5iR_}~9p6Q(Xws(i&Tfiq6`vpo+2VAO3vdwKU99%lX e>;G$cX&p#vx~hj-MXaxKK literal 0 HcmV?d00001 diff --git a/guru_ui/example/lib/pages/button/button_controller.dart b/guru_ui/example/lib/pages/button/button_controller.dart index fb0f9c8..1d6c04d 100644 --- a/guru_ui/example/lib/pages/button/button_controller.dart +++ b/guru_ui/example/lib/pages/button/button_controller.dart @@ -1,9 +1,11 @@ +import 'package:example/pages/button/button_design_model.dart'; import 'package:get/get.dart'; import 'button_model.dart'; class ButtonController extends GetxController { final ButtonModel model = ButtonModel(); + final ButtonDesignSpec designSpec = ButtonDesignSpec.get(); @override void onReady() { diff --git a/guru_ui/example/lib/pages/button/button_design_model.dart b/guru_ui/example/lib/pages/button/button_design_model.dart new file mode 100644 index 0000000..004a5cb --- /dev/null +++ b/guru_ui/example/lib/pages/button/button_design_model.dart @@ -0,0 +1,24 @@ +import 'package:design/design.dart'; + + +part 'button_design_model.g.dart'; + +@DesignSpec(width: 750, height: 1624) +abstract class ButtonDesignSpec implements BasicDesignSpec { + @SpecSize(SpecWidth(630), SpecHeight(104, consistent: true)) + Size get buttonS1; + + @SpecSize(SpecWidth(542), SpecHeight(96, consistent: true)) + Size get buttonS2; + + @SpecSize(SpecWidth(480), SpecHeight(80, consistent: true)) + Size get buttonS3; + + @SpecSize(SpecWidth(400), SpecHeight(72, consistent: true)) + Size get buttonS4; + + @SpecSize(SpecWidth(172), SpecHeight(60, consistent: true)) + Size get buttonS5; + + static ButtonDesignSpec get() => _ButtonDesignSpec.get(); +} \ No newline at end of file diff --git a/guru_ui/example/lib/pages/button/button_design_model.g.dart b/guru_ui/example/lib/pages/button/button_design_model.g.dart new file mode 100644 index 0000000..6d0d83a --- /dev/null +++ b/guru_ui/example/lib/pages/button/button_design_model.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'button_design_model.dart'; + +// ************************************************************************** +// DesignSpecGenerator +// ************************************************************************** + +class _ButtonDesignSpec extends ButtonDesignSpec { + _ButtonDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.buttonS1, + this.buttonS2, + this.buttonS3, + this.buttonS4, + this.buttonS5, + ); + + static final designMetrics = DesignMetrics.create(const Size(750.0, 1624.0)); + + static final Map _cache = {}; + + @override + final Size buttonS1; + + @override + final Size buttonS2; + + @override + final Size buttonS3; + + @override + final Size buttonS4; + + @override + final Size buttonS5; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + static _ButtonDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _ButtonDesignSpec._( + _measuredMetrics, offset, + Size(_measuredMetrics.measureWidth(630.0), _measuredMetrics.measureHeight(104.0, consistent: true)), + Size(_measuredMetrics.measureWidth(542.0), _measuredMetrics.measureHeight(96.0, consistent: true)), + Size(_measuredMetrics.measureWidth(480.0), _measuredMetrics.measureHeight(80.0, consistent: true)), + Size(_measuredMetrics.measureWidth(400.0), _measuredMetrics.measureHeight(72.0, consistent: true)), + Size(_measuredMetrics.measureWidth(200.0), _measuredMetrics.measureHeight(60.0, consistent: true)) + ); + } + + static ButtonDesignSpec get({Offset offset = Offset.zero}) { + final Size measuredSize = Get.size; + final key = BasicDesignSpec.buildSpecKey(measuredSize, offset); + _ButtonDesignSpec? designSpec = _cache[key]; + if (kDebugMode || designSpec == null) { + designSpec = _create(measuredSize, offset: offset); + _cache[key] = designSpec; + } + return designSpec; + } +} diff --git a/guru_ui/example/lib/pages/button/button_view.dart b/guru_ui/example/lib/pages/button/button_view.dart index ea78574..8d20052 100644 --- a/guru_ui/example/lib/pages/button/button_view.dart +++ b/guru_ui/example/lib/pages/button/button_view.dart @@ -4,7 +4,6 @@ import 'package:example/pages/widgets/guru_demo_page.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:guru_widgets/guru_widgets.dart'; -import 'package:guru_popup/guru_popup.dart'; import '../../theme/button/custom_standard_button_theme.dart'; import 'button_controller.dart'; import 'package:guru_widgets/theme/guru_theme.dart'; @@ -20,9 +19,11 @@ class ButtonPage extends GetWidget { bool info = false, bool summary = false, bool outline = false, - GuruButtonStyle? style}) { + GuruButtonStyle? style, + Size? size, + GuruButtonSizeSpec sizeSpec = GuruButtonSizeSpec.s2}) { return GuruButton( - size: const Size(271, 48), + size: size ?? controller.designSpec.buttonS2, action: action ? "Action" : null, leading: leading ? Assets.imagesIcAds : null, trailing: trailing ? Assets.imagesIcCoin : null, @@ -32,124 +33,232 @@ class ButtonPage extends GetWidget { summary: summary ? "Button Summary" : null, style: style, fillType: outline ? GuruButtonFillType.outline : GuruButtonFillType.solid, + sizeSpec: sizeSpec, ); } Widget buildBody() { - const spacer = SizedSpacer(height: 8, width: 8); + const spacer = SizedSpacer(height: 16, width: 8); return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ spacer, - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - GuruButton( - size: Size(80, 48), - leading: Assets.imagesIcAds, - fillType: GuruButtonFillType.solid), - spacer, - GuruButton( - size: Size(80, 48), - leading: Assets.imagesIcAds, - fillType: GuruButtonFillType.outline, - tintLeading: true), - ], - ), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + buildStandardButtonDemoWidget( + leading: true, + sizeSpec: GuruButtonSizeSpec.s1, + size: Size(80, controller.designSpec.buttonS1.height)), + buildStandardButtonDemoWidget( + leading: true, + outline: true, + sizeSpec: GuruButtonSizeSpec.s1, + size: Size(80, controller.designSpec.buttonS1.height)), + ]), + spacer, + buildStandardButtonDemoWidget( + action: true, + sizeSpec: GuruButtonSizeSpec.s1, + size: controller.designSpec.buttonS1), + spacer, + buildStandardButtonDemoWidget( + action: true, + sizeSpec: GuruButtonSizeSpec.s1, + outline: true, + size: controller.designSpec.buttonS1), spacer, buildStandardButtonDemoWidget( action: true, leading: true, - trailing: true, - info: true, - summary: true, - style: GuruButtonStyle.positive), + sizeSpec: GuruButtonSizeSpec.s1, + size: controller.designSpec.buttonS1), spacer, buildStandardButtonDemoWidget( action: true, leading: true, - trailing: true, - info: true, - summary: true, - style: GuruButtonStyle.neutral), + sizeSpec: GuruButtonSizeSpec.s1, + outline: true, + size: controller.designSpec.buttonS1), + spacer, + buildStandardButtonDemoWidget( + action: true, + summary: true, + sizeSpec: GuruButtonSizeSpec.s1, + size: controller.designSpec.buttonS1), + spacer, + buildStandardButtonDemoWidget( + action: true, + summary: true, + sizeSpec: GuruButtonSizeSpec.s1, + outline: true, + size: controller.designSpec.buttonS1), spacer, buildStandardButtonDemoWidget( action: true, - leading: true, trailing: true, info: true, - summary: true, - style: GuruButtonStyle.negative), + sizeSpec: GuruButtonSizeSpec.s1, + size: controller.designSpec.buttonS1), spacer, - const GuruButton(size: Size(271, 48), child: Text('Custom Widget')), + buildStandardButtonDemoWidget( + action: true, + trailing: true, + info: true, + sizeSpec: GuruButtonSizeSpec.s1, + outline: true, + size: controller.designSpec.buttonS1), spacer, - const GuruButton( - size: Size(271, 48), - fillType: GuruButtonFillType.outline, - child: Text('Custom Widget'), - ), - + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + buildStandardButtonDemoWidget( + leading: true, + size: Size(80, controller.designSpec.buttonS2.height)), + buildStandardButtonDemoWidget( + leading: true, + outline: true, + size: Size(80, controller.designSpec.buttonS2.height)), + ]), spacer, - buildStandardButtonDemoWidget(action: true, style: CustomStandardButtonTheme.custom), + buildStandardButtonDemoWidget(action: true), spacer, buildStandardButtonDemoWidget(action: true, outline: true), spacer, buildStandardButtonDemoWidget(action: true, leading: true), spacer, - buildStandardButtonDemoWidget(action: true, leading: true, outline: true), - spacer, - buildStandardButtonDemoWidget(action: true, trailing: true), - spacer, - buildStandardButtonDemoWidget(action: true, trailing: true, outline: true), - spacer, - buildStandardButtonDemoWidget(action: true, leading: true, trailing: true), - spacer, - buildStandardButtonDemoWidget(action: true, leading: true, trailing: true, outline: true), - spacer, - buildStandardButtonDemoWidget(action: true, trailing: true, info: true), - spacer, - buildStandardButtonDemoWidget(action: true, trailing: true, info: true, outline: true), - spacer, - buildStandardButtonDemoWidget(action: true, leading: true, trailing: true, info: true), - spacer, buildStandardButtonDemoWidget( - action: true, leading: true, trailing: true, info: true, outline: true), + action: true, leading: true, outline: true), spacer, buildStandardButtonDemoWidget(action: true, summary: true), spacer, - buildStandardButtonDemoWidget(action: true, summary: true, outline: true), - spacer, - buildStandardButtonDemoWidget(action: true, leading: true, summary: true), - spacer, - buildStandardButtonDemoWidget(action: true, leading: true, summary: true, outline: true), - spacer, - buildStandardButtonDemoWidget(action: true, trailing: true, summary: true), - spacer, - buildStandardButtonDemoWidget(action: true, trailing: true, summary: true, outline: true), - spacer, - buildStandardButtonDemoWidget(action: true, leading: true, trailing: true, summary: true), + buildStandardButtonDemoWidget( + action: true, summary: true, outline: true), spacer, buildStandardButtonDemoWidget( - action: true, leading: true, trailing: true, summary: true, outline: true), - spacer, - buildStandardButtonDemoWidget(action: true, trailing: true, info: true, summary: true), + action: true, trailing: true, info: true), spacer, buildStandardButtonDemoWidget( - action: true, trailing: true, info: true, summary: true, outline: true), + action: true, trailing: true, info: true, outline: true), + spacer, + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + buildStandardButtonDemoWidget( + leading: true, + size: Size(80, controller.designSpec.buttonS3.height)), + buildStandardButtonDemoWidget( + leading: true, + outline: true, + size: Size(80, controller.designSpec.buttonS3.height)), + ]), spacer, buildStandardButtonDemoWidget( - action: true, leading: true, trailing: true, info: true, summary: true), + sizeSpec: GuruButtonSizeSpec.s3, + action: true, + size: controller.designSpec.buttonS3), spacer, buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s3, + action: true, + outline: true, + size: controller.designSpec.buttonS3), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s3, action: true, leading: true, + size: controller.designSpec.buttonS3), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s3, + action: true, + leading: true, + outline: true, + size: controller.designSpec.buttonS3), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s3, + action: true, + summary: true, + size: controller.designSpec.buttonS3), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s3, + action: true, + outline: true, + summary: true, + size: controller.designSpec.buttonS3), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s3, + action: true, trailing: true, info: true, - summary: true, - outline: true), + size: controller.designSpec.buttonS3), spacer, - + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s3, + action: true, + trailing: true, + info: true, + outline: true, + size: controller.designSpec.buttonS3), + spacer, + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + buildStandardButtonDemoWidget( + leading: true, + sizeSpec: GuruButtonSizeSpec.s4, + size: Size(80, controller.designSpec.buttonS4.height)), + buildStandardButtonDemoWidget( + leading: true, + outline: true, + sizeSpec: GuruButtonSizeSpec.s4, + size: Size(80, controller.designSpec.buttonS4.height)), + ]), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s4, + action: true, + size: Size(100, controller.designSpec.buttonS4.height)), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s4, + action: true, + outline: true, + size: Size(100, controller.designSpec.buttonS4.height)), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s4, + action: true, + leading: true, + size: controller.designSpec.buttonS4), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s4, + action: true, + leading: true, + outline: true, + size: controller.designSpec.buttonS4), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s4, + action: true, + trailing: true, + info: true, + size: controller.designSpec.buttonS4), + spacer, + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s4, + action: true, + trailing: true, + info: true, + outline: true, + size: controller.designSpec.buttonS4), + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s5, + action: true, + size: controller.designSpec.buttonS5), + buildStandardButtonDemoWidget( + sizeSpec: GuruButtonSizeSpec.s5, + action: true, + outline: true, + size: controller.designSpec.buttonS5), ], ), ); diff --git a/guru_ui/example/lib/pages/root/root_controller.dart b/guru_ui/example/lib/pages/root/root_controller.dart index 4e3bc5e..5913d68 100644 --- a/guru_ui/example/lib/pages/root/root_controller.dart +++ b/guru_ui/example/lib/pages/root/root_controller.dart @@ -15,6 +15,7 @@ import 'package:guru_popup/guru_popup.dart'; import 'root_model.dart'; class RootController extends GuruAssetOverlayController { + final GlobalKey key = GlobalKey(); RootDesignSpec get designSpec => RootDesignSpec.get(); final BehaviorSubject focusPageSubject = BehaviorSubject.seeded(0); final BehaviorSubject showLabels = BehaviorSubject.seeded(false); @@ -34,10 +35,7 @@ class RootController extends GuruAssetOverlayController { } void showAssetsOverlay() { - final completer = GuruPopup.instance.showAssetsOverlay(director); - startClaim(50, 'asset_overlay', onCompleted: () { - completer.complete(); - }); + } void showWaterMarkOverlay(){ diff --git a/guru_ui/example/lib/pages/root/root_view.dart b/guru_ui/example/lib/pages/root/root_view.dart index b953cff..762d34e 100644 --- a/guru_ui/example/lib/pages/root/root_view.dart +++ b/guru_ui/example/lib/pages/root/root_view.dart @@ -188,7 +188,9 @@ class RootPage extends GetWidget { ], ), body: FlexibleContainer(child: Column(children: [ - GuruTabBar(items: [ + GuruTabBar( + key: controller.key, + items: [ GuruTabBarItem(title: 'title1'), GuruTabBarItem(title: 'title1') ]), diff --git a/guru_ui/example/lib/pages/settings/settings_view.dart b/guru_ui/example/lib/pages/settings/settings_view.dart index da11df8..a13f1d9 100644 --- a/guru_ui/example/lib/pages/settings/settings_view.dart +++ b/guru_ui/example/lib/pages/settings/settings_view.dart @@ -3,6 +3,7 @@ import 'package:example/data/settings/ui_settings.dart'; import 'package:example/theme/example_theme.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:guru_utils/app_ownership/app_ownership_utils.dart'; import 'package:guru_utils/audio/audio_effector.dart'; import 'package:guru_widgets/pages/settings/guru_settings_page.dart'; import 'package:guru_widgets/button/single_tap_widget.dart'; @@ -106,6 +107,7 @@ class SettingsPage extends GetWidget { ], policyUrl: Uri.parse("https://www.baidu.com"), termsOfServiceUrl: Uri.parse("https://www.baidu.com"), + appOwnerinfo: const AppOwnerInfo('assets/images/ic_share.png'), // onPolicyTap: () { // RouteCenter.instance.openPath(Routes.webview.path()); // }, diff --git a/guru_ui/example/lib/pages/store/store_controller.dart b/guru_ui/example/lib/pages/store/store_controller.dart index a7901f0..0df32bf 100644 --- a/guru_ui/example/lib/pages/store/store_controller.dart +++ b/guru_ui/example/lib/pages/store/store_controller.dart @@ -1,8 +1,32 @@ import 'package:design/design.dart'; import 'package:example/pages/store/store_design_spec.dart'; -import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_widgets/overlay/guru_asset_controller.dart'; +import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_widgets/assetbar/guru_asset_bar.dart'; -class StoreController extends LifecycleController { +class StoreController extends GuruAssetOverlayController { + final key1 = GlobalKey(); + final key2 = GlobalKey(); StoreDesignSpec get designSpec => StoreDesignSpec.get(); -} \ No newline at end of file + void showAssetsOverlay1() { + final completer = GuruPopup.instance.showAssetsOverlay(director); + startClaim(50, + assetIcon: 'assets/images/ic_coin.png', + scene: 'asset_overlay', + key: key1, + barSize: GuruAssetBarSizeSpec.s1, onCompleted: () { + completer.complete(); + }); + } + + void showAssetsOverlay2() { + final completer = GuruPopup.instance.showAssetsOverlay(director); + startClaim(50, + assetIcon: 'assets/images/ic_coin.png', + scene: 'asset_overlay', + key: key2, onCompleted: () { + completer.complete(); + }); + } +} diff --git a/guru_ui/example/lib/pages/store/store_page.dart b/guru_ui/example/lib/pages/store/store_page.dart index 515d0b1..2d429ad 100644 --- a/guru_ui/example/lib/pages/store/store_page.dart +++ b/guru_ui/example/lib/pages/store/store_page.dart @@ -1,23 +1,55 @@ import 'package:design/design.dart'; -import 'package:example/generated/app_strings.dart'; import 'package:example/pages/store/store_controller.dart'; import 'package:example/pages/store/store_design_spec.dart'; import 'package:example/theme/example_theme.dart'; import 'package:guru_widgets/guru_widgets.dart'; import 'package:guru_widgets/assetbar/guru_asset_bar.dart'; -import 'package:guru_utils/random/random_utils.dart'; class StorePage extends GetWidget { StoreDesignSpec get designModel => controller.designSpec; const StorePage({super.key}); Widget buildBody(BuildContext context) { - // return guru.StorePage(items: [ - // guru.CardGroup(style: guru.PurchaseCardStyle.igc, items: [ - // guru.CardItem() - // ]) - // ]); - return EmptySpacer(); + return Container( + child: Stack(children: [ + Positioned( + top: 50, + right: 20, + child: GuruAssetBar( + key: controller.key1, + style: GuruAssetBarStyle.igc, + balanceStream: Stream.periodic( + const Duration(seconds: 3000), (count) => count * 1000))), + Positioned( + top: 50, + left: 50, + child: GuruAssetBar( + key: controller.key2, + style: GuruAssetBarThemeMixin.joker, + balanceStream: Stream.periodic( + const Duration(seconds: 3000), (count) => count * 1000))), + Positioned( + top: 200, + left: 100, + child: GuruButton( + size: Size(200, 20), + action: 'play', + onPressed: () { + controller.showAssetsOverlay1(); + }), + ), + Positioned( + top: 280, + left: 100, + child: GuruButton( + size: Size(200, 20), + action: 'play', + onPressed: () { + controller.showAssetsOverlay2(); + }), + ), + ]), + ); } @override diff --git a/guru_ui/example/lib/theme/example_theme.dart b/guru_ui/example/lib/theme/example_theme.dart index 4ce02ec..d45223d 100644 --- a/guru_ui/example/lib/theme/example_theme.dart +++ b/guru_ui/example/lib/theme/example_theme.dart @@ -51,7 +51,7 @@ class ExampleThemes { subtitleColor: Color(0xFF6C6D71), summaryColor: Colors.black, backgroundColor: Colors.white), - appBarTheme: const GuruAppBarTheme(backgroundColor: Colors.blue, hasStatusBar: true), + appBarTheme: const GuruAppBarTheme(backgroundColor: Colors.blue), tabBarTheme: const GuruTabBarTheme( backgroundColor: Color(0xFFF1F1F1), selectedColor: Colors.white, diff --git a/guru_ui/lib/pages/store/bundle_card.dart b/guru_ui/lib/pages/store/bundle_card.dart index 1128da7..0da9aab 100644 --- a/guru_ui/lib/pages/store/bundle_card.dart +++ b/guru_ui/lib/pages/store/bundle_card.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:design/design.dart'; import 'package:guru_app/financial/iap/iap_model.dart'; import 'package:guru_ui/pages/store/store_page.dart'; @@ -7,7 +8,7 @@ import 'package:guru_widgets/theme/guru_theme.dart'; part 'bundle_card.g.dart'; -@DesignSpec(width: 670, height: 388, specMode: SpecMode.nested) +@DesignSpec(width: 750, height: 388, specMode: SpecMode.nested) abstract class BundleCardDesignSpec implements BasicDesignSpec { @SpecEdgeInsets.only( top: SpecHeight(12), @@ -131,13 +132,16 @@ class BundleCard extends StatelessWidget { children: [ bundleItem.builder(context, designSpec), Padding(padding: designSpec.bundleInfoPadding, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Text( + AutoSizeText( bundleItem.title, + minFontSize: 10, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( color: styleTheme.titleColor ?? Colors.white, fontWeight: GuruTheme.fwExtraBold, @@ -148,18 +152,24 @@ class BundleCard extends StatelessWidget { Padding( padding: EdgeInsets.only( top: designSpec.bundleTitleBottomSpcing), - child: Text( + child: AutoSizeText( bundleItem.summary!, + minFontSize: 8, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( color: styleTheme.summaryColor ?? Colors.white, fontSize: designSpec.bundleSummaryFontsize, fontWeight: GuruTheme.fwMedium), )) - ]), + ]) + ), + SizedBox(width: 4), GuruButton( size: designSpec.bundleButtonSize, - action: product != null ? product!.details.price : 'BUY', - summary: bundleItem.summary, + sizeSpec: GuruButtonSizeSpec.s3, + action: product != null ? product!.details.price : '1000000000', + // summary: bundleItem.summary, style: styleTheme.buttonStyle, onPressed: () { model.onTap.call(); diff --git a/guru_ui/lib/pages/store/bundle_card.g.dart b/guru_ui/lib/pages/store/bundle_card.g.dart index 7f828db..5c60044 100644 --- a/guru_ui/lib/pages/store/bundle_card.g.dart +++ b/guru_ui/lib/pages/store/bundle_card.g.dart @@ -21,7 +21,7 @@ class _BundleCardDesignSpec extends BundleCardDesignSpec { this.bundleButtonSize, ); - static final designMetrics = DesignMetrics.create(const Size(670.0, 388.0)); + static final designMetrics = DesignMetrics.create(const Size(750.0, 388.0)); static final Map _cache = {}; @@ -60,6 +60,7 @@ class _BundleCardDesignSpec extends BundleCardDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _BundleCardDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -83,8 +84,10 @@ class _BundleCardDesignSpec extends BundleCardDesignSpec { consistent: false), // bundleLabelRightSpcing _measuredMetrics.measureHeight(136.0, consistent: false), // bundleLabelSize - _measuredMetrics.measureAbsoluteFontSize(32.0), // bundleTitleFontsize - _measuredMetrics.measureAbsoluteFontSize(20.0), // bundleSummaryFontsize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // bundleTitleFontsize + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: false), // bundleSummaryFontsize _measuredMetrics.measureHeight(8.0, consistent: false), // bundleTitleBottomSpcing Size(_measuredMetrics.measureWidth(200.0), diff --git a/guru_ui/lib/pages/store/purchase_banner.dart b/guru_ui/lib/pages/store/purchase_banner.dart index 44dcede..c61d372 100644 --- a/guru_ui/lib/pages/store/purchase_banner.dart +++ b/guru_ui/lib/pages/store/purchase_banner.dart @@ -46,7 +46,7 @@ abstract class PurchaseBannerDesignSpec implements BasicDesignSpec { @SpecWidth(390) double get productDetailsWidth; - @SpecFontSize(26) + @SpecAbsoluteFontSize(26, consistent: true) double get productDetailsFontSize; @SpecFontSize(26) @@ -61,7 +61,7 @@ abstract class PurchaseBannerDesignSpec implements BasicDesignSpec { @SpecHorizontal(40) double get productDetailsStartSpacing; - @SpecHorizontal(280) + @SpecHorizontal(300) double get productDetailsEndSpacing; @SpecVertical(20) @@ -79,7 +79,7 @@ abstract class PurchaseBannerDesignSpec implements BasicDesignSpec { @SpecHeight(40) double get tipsSize; - @SpecHeight(207) + @SpecHeight(136) double get tipsMinHeight; @SpecWidth(486) @@ -191,6 +191,7 @@ class PurchaseBanner extends StatelessWidget { width: double.infinity, child: TapWidget( onTap: () { + // model.onTap(); if (guruTheme.feedbackCapabilities.canPerform()) { FeedbackManager.instance.perform(FeedbackOccasion.clickItem); } @@ -210,38 +211,37 @@ class PurchaseBanner extends StatelessWidget { end: 0, top: 0, bottom: 0, - child: styleTheme.mainBackGround != null ? Image.asset(styleTheme.mainBackGround!, fit: BoxFit.fill, height: designSpec.bannerHeight) : Container()), + child: styleTheme.mainBackGround != null ? Transform.flip( + flipX: textDirection == TextDirection.ltr, + child: Image.asset(styleTheme.mainBackGround!, fit: BoxFit.fitHeight, height: designSpec.bannerHeight), + ) : Container()), Positioned.directional( + width: designSpec.removeAdsBackgroungWidth, textDirection: textDirection, - end: designSpec.removeAdsImageEndSpacing, - top: designSpec.removeAdsImageVerticalSpacing, - child: styleTheme.mainImage != null ? Image.asset( - styleTheme.mainImage!, - fit: BoxFit.fill, - width: designSpec.removeAdsImageWidth, - height: designSpec.removeAdsImageWidth, - ): Container()), + end: 0, + top: 0, + bottom: 0, + child: styleTheme.mainBackGround != null ? Image.asset(styleTheme.mainImage!, fit: BoxFit.fitHeight, height: designSpec.bannerHeight) : Container()), Positioned.directional( textDirection: textDirection, width: designSpec.measuredSize.width - designSpec.bannerHorizontalSpcing * 2 - designSpec.productDetailsEndSpacing, - start: 0, - top: 0, + start: designSpec.productDetailsStartSpacing, + top: 16, bottom: 0, - child: Padding( - padding: EdgeInsets.only(left: designSpec.productDetailsStartSpacing, top: 16), - child: Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, height: designSpec.summaryHeight, child: AutoSizeText( bannerItem.summary ?? '', maxLines: 2, + overflow: TextOverflow.visible, style: TextStyle( - height: 1.34, + height: 1.4, fontSize: designSpec.productDetailsFontSize, fontWeight: GuruTheme.fwBold, color: const Color(0xFF7E1E00)), @@ -256,7 +256,7 @@ class PurchaseBanner extends StatelessWidget { model.onTap(); }) ], - ))), + )), if (bannerItem.tips != null && bannerItem.tips!.isNotEmpty) Positioned.directional( textDirection: textDirection, @@ -265,7 +265,7 @@ class PurchaseBanner extends StatelessWidget { child: GestureDetector( onTap: () { GuruPopup.instance.showTipsOverlay( - WidgetUtils.getWidgetBoundary(tipsKey, offset: const Offset(0.0, 0.0)), + WidgetUtils.getWidgetBoundary(tipsKey, offset: Offset(0.0, Get.statusBarHeight / Get.pixelRatio)), width: designSpec.tipsWidth, height: designSpec.tipsMinHeight, radius: Radius.circular(designSpec.tipsRadius), diff --git a/guru_ui/lib/pages/store/purchase_banner.g.dart b/guru_ui/lib/pages/store/purchase_banner.g.dart index 483b2d4..e2b1176 100644 --- a/guru_ui/lib/pages/store/purchase_banner.g.dart +++ b/guru_ui/lib/pages/store/purchase_banner.g.dart @@ -35,7 +35,7 @@ class _PurchaseBannerDesignSpec extends PurchaseBannerDesignSpec { this.tipsGap, this.tipsRadius, this.tipsContentPadding, - this.summaryHeight + this.summaryHeight, ); static final designMetrics = DesignMetrics.create(const Size(750.0, 200.0)); @@ -112,7 +112,7 @@ class _PurchaseBannerDesignSpec extends PurchaseBannerDesignSpec { final double tipsGap; @override - final double tipsRadius; + final double tipsRadius; @override final EdgeInsets tipsContentPadding; @@ -128,6 +128,7 @@ class _PurchaseBannerDesignSpec extends PurchaseBannerDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _PurchaseBannerDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -144,7 +145,8 @@ class _PurchaseBannerDesignSpec extends PurchaseBannerDesignSpec { _measuredMetrics.measureVertical(80.0), // removeAdsImageEndSpacing _measuredMetrics.measureVertical(28.0), // removeAdsImageVerticalSpacing _measuredMetrics.measureWidth(390.0), // productDetailsWidth - _measuredMetrics.measureFontSize(26.0), // productDetailsFontSize + _measuredMetrics.measureAbsoluteFontSize(26.0, + consistent: true), // productDetailsFontSize _measuredMetrics.measureFontSize(26.0), // productPriceFontSize Offset( 0.0, @@ -152,13 +154,13 @@ class _PurchaseBannerDesignSpec extends PurchaseBannerDesignSpec { .measureVertical(1.0)), // productPriceTextShadowOffset _measuredMetrics.measureVertical(32.0), // productDetailsTopSpacing _measuredMetrics.measureHorizontal(40.0), // productDetailsStartSpacing - _measuredMetrics.measureHorizontal(280.0), // productDetailsEndSpacing + _measuredMetrics.measureHorizontal(300.0), // productDetailsEndSpacing _measuredMetrics.measureVertical(20.0), // purchaseButtonTopSpacing _measuredMetrics.measureVertical(56.0), // purchaseButtonHeight _measuredMetrics.measureWidth(168.0), // purchaseButtonWidth _measuredMetrics.measureVertical(16.0), // tipsSpacing _measuredMetrics.measureHeight(40.0, consistent: false), // tipsSize - _measuredMetrics.measureHeight(207.0, consistent: false), // tipsMinHeight + _measuredMetrics.measureHeight(136.0, consistent: false), // tipsMinHeight _measuredMetrics.measureWidth(486.0), // tipsWidth _measuredMetrics.measureFontSize(26.0), // tipsFontSize _measuredMetrics.measureVertical(12.0), // tipsGap diff --git a/guru_ui/lib/pages/store/purchase_card.dart b/guru_ui/lib/pages/store/purchase_card.dart index e11028c..b545f8c 100644 --- a/guru_ui/lib/pages/store/purchase_card.dart +++ b/guru_ui/lib/pages/store/purchase_card.dart @@ -61,7 +61,7 @@ abstract class PurchaseCardDesignSpec implements BasicDesignSpec { @SpecHeight(92, consistent: true) double get labelSize; - @SpecHeight(88) + @SpecHeight(88, consistent: true) double get labelTopSpacing; @SpecWidth(6) @@ -214,7 +214,7 @@ class PurchaseCard extends StatelessWidget { size: designSpec.purchaseButtonSize, sizeSpec: GuruButtonSizeSpec.s5, style: GuruButtonStyle.purchase, - action: product != null ? product!.details.price : 'BUY', + action: product != null ? product!.details.price : '1000000000', onPressed: () { model.onTap.call(); }, diff --git a/guru_ui/lib/pages/store/purchase_card.g.dart b/guru_ui/lib/pages/store/purchase_card.g.dart index 94f17e3..ba90a1c 100644 --- a/guru_ui/lib/pages/store/purchase_card.g.dart +++ b/guru_ui/lib/pages/store/purchase_card.g.dart @@ -25,7 +25,7 @@ class _PurchaseCardDesignSpec extends PurchaseCardDesignSpec { this.purchaseButtonSize, this.labelSize, this.labelTopSpacing, - this.labalEndSpacing + this.labalEndSpacing, ); static final designMetrics = DesignMetrics.create(const Size(210.0, 320.0)); @@ -88,6 +88,7 @@ class _PurchaseCardDesignSpec extends PurchaseCardDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _PurchaseCardDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -108,7 +109,8 @@ class _PurchaseCardDesignSpec extends PurchaseCardDesignSpec { top: _measuredMetrics.measureVertical(22.0), bottom: _measuredMetrics.measureVertical(2.0)), _measuredMetrics.measureFontSize(18.0), // bonusFontSize - _measuredMetrics.measureAbsoluteFontSize(36.0), // quantityFontSize + _measuredMetrics.measureAbsoluteFontSize(36.0, + consistent: false), // quantityFontSize EdgeInsets.only( left: _measuredMetrics.measureHorizontal(12.0), right: _measuredMetrics.measureHorizontal(12.0), @@ -126,12 +128,13 @@ class _PurchaseCardDesignSpec extends PurchaseCardDesignSpec { top: _measuredMetrics.measureVertical(18.0), bottom: _measuredMetrics.measureVertical(12.0)), _measuredMetrics.measureHeight(12.0, - consistent: false), // purchaseButtonTopSpacing - Size(_measuredMetrics.measureHeight(60.0) * 2.83333333, - _measuredMetrics.measureHeight(60.0)), // purchaseButtonSize - _measuredMetrics.measureHeight(92.0, consistent: true), - _measuredMetrics.measureHeight(88.0), - _measuredMetrics.measureWidth(6.0) + consistent: true), // purchaseButtonTopSpacing + Size(_measuredMetrics.measureHeight(60.0, consistent: true) * 2.83333333, + _measuredMetrics.measureHeight(60.0, consistent: true)), // purchaseButtonSize + _measuredMetrics.measureHeight(92.0, consistent: true), // labelSize + _measuredMetrics.measureHeight(88.0, + consistent: true), // labelTopSpacing + _measuredMetrics.measureWidth(6.0), // labalEndSpacing ); } diff --git a/guru_ui/lib/pages/store/store_controller.dart b/guru_ui/lib/pages/store/store_controller.dart index cccb5e6..0916b68 100644 --- a/guru_ui/lib/pages/store/store_controller.dart +++ b/guru_ui/lib/pages/store/store_controller.dart @@ -4,82 +4,84 @@ import 'package:guru_app/analytics/guru_analytics.dart'; import 'package:guru_app/financial/iap/iap_manager.dart'; import 'package:guru_app/financial/product/product_model.dart'; import 'package:guru_app/guru_app.dart'; -import 'package:guru_app/test/test_guru_app_creator.dart'; import 'package:guru_ui/pages/store/store_page.dart'; -import 'package:guru_utils/controller/lifecycle_controller.dart'; import 'package:guru_app/controller/assets_aware.dart'; import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_widgets/assetbar/guru_asset_bar.dart'; import 'package:guru_widgets/dialog/guru_dialog.dart'; import 'package:guru_widgets/overlay/guru_asset_controller.dart'; import 'package:guru_ui/localizations/ui_strings.dart'; -import 'package:rxdart/src/subjects/behavior_subject.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; class StoreController extends GuruAssetOverlayController with AssetsAware { final Map keys = {}; bool productsInitFinished = false; + final assetBarStyle = GuruTheme.of(Get.context!).assetBarTheme.styles[GuruAssetBarStyle.igc]; + GlobalKey getKey(String key) { return keys[key] ??= GlobalKey(); } void handlePurchase(ProductId productId) async { Log.w("handlePurchase"); - final products = await IapManager.instance - .buildProducts({productId.createIntent(scene: "store")}); - final product = products.getProduct(productId); + final products = + await IapManager.instance.buildProducts({productId.createIntent(scene: "store")}); + final product = products.getProduct(productId); final appStrings = UIStrings.get(); // final store = currentProductStore; // final product = store.getProduct(productId); - if (product != null) { - GuruAnalytics.instance.logEventEx("iap_gems_clk", itemCategory: "store", itemName: productId.sku); + GuruAnalytics.instance + .logEventEx("iap_gems_clk", itemCategory: "store", itemName: productId.sku); - final successStream = Stream.fromFuture(requestProduct(product).catchError((error, stacktrace) { - Log.w("requestProduct error! $error", stackTrace: stacktrace); - return false; - })).asBroadcastStream(); + final successStream = + Stream.fromFuture(requestProduct(product).catchError((error, stacktrace) { + Log.w("requestProduct error! $error", stackTrace: stacktrace); + return false; + })).asBroadcastStream(); - Log.d("request product:${product.sku} ${jsonEncode(product.manifest)} ${product.manifest.details.length}"); + Log.d( + "request product:${product.sku} ${jsonEncode(product.manifest)} ${product.manifest.details.length}"); - final completer = GuruPopup.instance.showFeedbackLoading( - successStream: successStream, - feedbackText: 'Purchase Succeeded' - ); + final completer = GuruPopup.instance + .showFeedbackLoading(successStream: successStream, feedbackText: 'Purchase Succeeded'); - successStream.listen((event) async { - Log.v("buy result:$event"); - if (event) { - GuruAnalytics.instance.logEventEx("iap_gem_scs", itemCategory: "home", itemName: product.sku); - await Future.delayed(const Duration(milliseconds: 1000)); - completer.complete(); - GuruPopup.instance.showStandardDialog( - illustration: Image.asset('assets/images/ic_purchase.png',package: 'guru_ui'), + successStream.listen((event) async { + Log.v("buy result:$event"); + if (event) { + GuruAnalytics.instance + .logEventEx("iap_gem_scs", itemCategory: "home", itemName: product.sku); + await Future.delayed(const Duration(milliseconds: 1000)); + completer.complete(); + GuruPopup.instance.showStandardDialog( + illustration: Image.asset('assets/images/ic_purchase.png', package: 'guru_ui'), closable: false, title: 'Completed', summary: 'Your purchase is complete. Have fun playing th game!', - primaryButtonAction: 'Coutinue', + primaryButtonAction: 'Continue', onPrimaryButtonPressed: () { final assetsCompleter = GuruPopup.instance.showAssetsOverlay(director); - startClaim(50, 'asset_overlay', onCompleted: () { + startClaim(50, + scene: 'asset_overlay', + assetIcon: assetBarStyle!.assetIcon, + key: getKey("assetBar"), + onCompleted: () { assetsCompleter.complete(); }); return DialogResult(true, null); - } - ); - + }); + } else { + completer.complete(); + if (isIapCanceled) { + GuruPopup.instance.showStandardOverlay("Purchase Failed", icon: "assets/images/ic_purchase_failed.png", package: 'guru_ui'); } else { - completer.complete(); - if (isIapCanceled) { - GuruPopup.instance.showStardardOverlay(appStrings.store); - // ToastUtils.showCommonToast(appStrings.iapSuspended); - } else { - GuruPopup.instance.showStardardOverlay(appStrings.store); - // ToastUtils.showCommonToast(appStrings.checkNetworkAndGp); - } + GuruPopup.instance.showStandardOverlay("Purchase Failed", icon: "assets/images/ic_purchase_failed.png", package: 'guru_ui'); } - }); - } + } + }); + } } Set initIntents(List list) { @@ -94,16 +96,16 @@ class StoreController extends GuruAssetOverlayController with AssetsAware { } } } - final Set idsSet = Set.of(ids.toSet()); + final Set idsSet = Set.of(ids.toSet()); final intents = idsSet.map((id) => id.createIntent(scene: "store")).toSet(); return intents; } - void initProducts(List list){ + void initProducts(List list) { if (!productsInitFinished) { final intents = initIntents(list); observeIapProducts(intents); productsInitFinished = true; } } -} \ No newline at end of file +} diff --git a/guru_ui/lib/pages/store/store_design_spec.dart b/guru_ui/lib/pages/store/store_design_spec.dart index 1795086..a86aad5 100644 --- a/guru_ui/lib/pages/store/store_design_spec.dart +++ b/guru_ui/lib/pages/store/store_design_spec.dart @@ -10,6 +10,9 @@ abstract class StoreDesignSpec implements BasicDesignSpec { SpecStatusBarHeight(50, wrappedInSafeArea: true), SpecVertical(0)) double get appBarTopSpacing; + @SpecHeight(72, consistent: true) + double get appBarIconButtonSize; + @SpecHeight(48, consistent: true) double get appBarIconSize; @@ -19,8 +22,11 @@ abstract class StoreDesignSpec implements BasicDesignSpec { @SpecAbsoluteFontSize(56, consistent: true) double get appBarTitleFontSize; - @SpecEdgeInsets.symmetric(horizontal: SpecHorizontal(40), vertical: SpecVertical(8)) + @SpecEdgeInsets.symmetric(horizontal: SpecHorizontal(28), vertical: SpecVertical(8)) EdgeInsets get appbarPadding; + + @SpecEdgeInsets.symmetric(horizontal: SpecHorizontal(12)) + EdgeInsets get assetsBarPadding; @SpecEdgeInsets.only(start: SpecHorizontal(40), end: SpecHorizontal(40)) EdgeInsets get contentPadding; @@ -46,13 +52,13 @@ abstract class StoreDesignSpec implements BasicDesignSpec { @SpecEdgeInsets.symmetric(horizontal: SpecHorizontal(8)) EdgeInsets get dividerItemPadding; - @SpecHeight(26, consistent: true) + @SpecHeight(32, consistent: true) double get dividerTipSize; @SpecAbsoluteFontSize(32, consistent: true) double get dividerTipFontSize; - @SpecHeight(4) + @SpecHeight(4, consistent: true) double get dividerLineHeight; @SpecVertical(40, consistent: true) @@ -61,7 +67,7 @@ abstract class StoreDesignSpec implements BasicDesignSpec { @SpecVertical(20) double get cardGridCrossAxisSpacing; - @SpecHeight(136) + @SpecHeight(136, consistent: true) double get tipsMinHeight; @SpecWidth(486) @@ -83,10 +89,10 @@ abstract class StoreDesignSpec implements BasicDesignSpec { @SpecHeight(32) double get tipsRadius; - @NestedSpec(670, 200, consistentHeight: true) + @NestedSpec(750, 200, consistentHeight: true) PurchaseBannerDesignSpec get purchaseBannerDesignSpec; - @NestedSpec(670, 388, consistentHeight: true) + @NestedSpec(750, 388, consistentHeight: true) BundleCardDesignSpec get bundleCardDesignSpec; @SpecHeight(36, consistent: true) diff --git a/guru_ui/lib/pages/store/store_design_spec.g.dart b/guru_ui/lib/pages/store/store_design_spec.g.dart index 5661372..dfda9d5 100644 --- a/guru_ui/lib/pages/store/store_design_spec.g.dart +++ b/guru_ui/lib/pages/store/store_design_spec.g.dart @@ -11,10 +11,12 @@ class _StoreDesignSpec extends StoreDesignSpec { this.measuredMetrics, this.specOffset, this.appBarTopSpacing, + this.appBarIconButtonSize, this.appBarIconSize, this.appBarSize, this.appBarTitleFontSize, this.appbarPadding, + this.assetsBarPadding, this.contentPadding, this.srcollViewPadding, this.groupItemSpacing, @@ -46,6 +48,9 @@ class _StoreDesignSpec extends StoreDesignSpec { @override final double appBarTopSpacing; + @override + final double appBarIconButtonSize; + @override final double appBarIconSize; @@ -58,6 +63,9 @@ class _StoreDesignSpec extends StoreDesignSpec { @override final EdgeInsets appbarPadding; + @override + final EdgeInsets assetsBarPadding; + @override final EdgeInsets contentPadding; @@ -132,6 +140,7 @@ class _StoreDesignSpec extends StoreDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _StoreDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -141,15 +150,23 @@ class _StoreDesignSpec extends StoreDesignSpec { _measuredMetrics, offset, (_measuredMetrics.wrappedInSafeAreaStatusBarHeight(50.0) + _measuredMetrics.measureVertical(0.0)), // appBarTopSpacing + _measuredMetrics.measureHeight(72.0, + consistent: true), // appBarIconButtonSize _measuredMetrics.measureHeight(48.0, consistent: true), // appBarIconSize Size(_measuredMetrics.measureWidth(750.0), - _measuredMetrics.measureHeight(114.0, consistent: true)), // appBarSize - _measuredMetrics.measureAbsoluteFontSize(56.0, consistent: true), // appBarTitleFontSize + _measuredMetrics.measureHeight(114.0)), // appBarSize + _measuredMetrics.measureAbsoluteFontSize(56.0, + consistent: true), // appBarTitleFontSize EdgeInsets.only( - left: _measuredMetrics.measureHorizontal(40.0), - right: _measuredMetrics.measureHorizontal(40.0), - top: _measuredMetrics.measureVertical(8.0), + left: _measuredMetrics.measureHorizontal(28.0), + right: _measuredMetrics.measureHorizontal(28.0), + top: _measuredMetrics.measureVertical(8.0), bottom: _measuredMetrics.measureVertical(8.0)), + EdgeInsets.only( + left: _measuredMetrics.measureHorizontal(12.0), + right: _measuredMetrics.measureHorizontal(12.0), + top: 0.0, + bottom: 0.0), EdgeInsets.only( left: _measuredMetrics.measureHorizontal(40.0), right: _measuredMetrics.measureHorizontal(40.0), @@ -158,42 +175,45 @@ class _StoreDesignSpec extends StoreDesignSpec { EdgeInsets.only( left: 0.0, right: 0.0, - top: _measuredMetrics.measureVertical(48.0, consistent: true), - bottom: _measuredMetrics.measureVertical(40.0, consistent: true)), - _measuredMetrics.measureHeight(56.0, consistent: true), - _measuredMetrics.measureVertical(8.0, consistent: true), // tipsSpacing + top: _measuredMetrics.measureVertical(48.0), + bottom: _measuredMetrics.measureVertical(40.0)), + _measuredMetrics.measureHeight(56.0, + consistent: true), // groupItemSpacing + _measuredMetrics.measureVertical(8.0), // tipsSpacing _measuredMetrics.measureHeight(40.0, consistent: true), // tipsSize _measuredMetrics.measureHeight(40.0, consistent: true), // dividerHeight EdgeInsets.only( left: 0.0, right: 0.0, - top: _measuredMetrics.measureVertical(8.0, consistent: true), - bottom: _measuredMetrics.measureVertical(40.0, consistent: true)), + top: _measuredMetrics.measureVertical(8.0), + bottom: _measuredMetrics.measureVertical(40.0)), EdgeInsets.only( left: _measuredMetrics.measureHorizontal(8.0), right: _measuredMetrics.measureHorizontal(8.0), top: 0.0, bottom: 0.0), - _measuredMetrics.measureHeight(26.0, consistent: true), // dividerTipSize - _measuredMetrics.measureAbsoluteFontSize(32.0, consistent: true), // dividerTipFontSize + _measuredMetrics.measureHeight(32.0, consistent: true), // dividerTipSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: true), // dividerTipFontSize _measuredMetrics.measureHeight(4.0, consistent: true), // dividerLineHeight - _measuredMetrics.measureVertical(40.0, consistent: true), // cardGridMainAxisSpacing - _measuredMetrics.measureVertical(20.0, consistent: true), // cardGridCrossAxisSpacing - _measuredMetrics.measureHeight(138.0, consistent: true), // tipsMinHeight + _measuredMetrics.measureVertical(40.0), // cardGridMainAxisSpacing + _measuredMetrics.measureVertical(20.0), // cardGridCrossAxisSpacing + _measuredMetrics.measureHeight(136.0, consistent: true), // tipsMinHeight _measuredMetrics.measureWidth(486.0), // tipsWidth - _measuredMetrics.measureAbsoluteFontSize(26.0), // tipsFontSize + _measuredMetrics.measureAbsoluteFontSize(26.0, + consistent: false), // tipsFontSize _measuredMetrics.measureVertical(12.0), // tipsGap EdgeInsets.only( left: _measuredMetrics.measureWidth(32.0), right: _measuredMetrics.measureWidth(32.0), - top: _measuredMetrics.measureHeight(22.0, consistent: true), - bottom: _measuredMetrics.measureHeight(26.0, consistent: true)), + top: _measuredMetrics.measureHeight(22.0), + bottom: _measuredMetrics.measureHeight(26.0)), _measuredMetrics.measureHeight(32.0, consistent: false), // tipsRadius PurchaseBannerDesignSpec.create( Size( _measuredMetrics - .measureWidth(670.0, consistent: false) + .measureWidth(750.0, consistent: false) .clamp(0.0, 4294967295.0), _measuredMetrics .measureHeight(200.0, consistent: true) @@ -202,7 +222,7 @@ class _StoreDesignSpec extends StoreDesignSpec { BundleCardDesignSpec.create( Size( _measuredMetrics - .measureWidth(670.0, consistent: false) + .measureWidth(750.0, consistent: false) .clamp(0.0, 4294967295.0), _measuredMetrics .measureHeight(388.0, consistent: true) diff --git a/guru_ui/lib/pages/store/store_page.dart b/guru_ui/lib/pages/store/store_page.dart index 492d9d0..cc3f2e1 100644 --- a/guru_ui/lib/pages/store/store_page.dart +++ b/guru_ui/lib/pages/store/store_page.dart @@ -1,8 +1,5 @@ -import 'dart:convert'; - import 'package:auto_size_text/auto_size_text.dart'; import 'package:design/design.dart'; -import 'package:guru_app/analytics/guru_analytics.dart'; import 'package:guru_app/financial/iap/iap_model.dart'; import 'package:guru_app/financial/product/product_store.dart'; import 'package:guru_app/guru_app.dart'; @@ -14,8 +11,6 @@ import 'package:guru_ui/pages/store/purchase_card.dart'; import 'package:guru_ui/pages/store/store_controller.dart'; import 'package:guru_app/financial/product/product_model.dart'; import 'package:guru_ui/pages/store/store_design_spec.dart'; -import 'package:guru_ui/widget/dialog/dialog_utils.dart'; -import 'package:guru_widgets/button/purchase_button.dart'; import 'package:guru_widgets/button/single_tap_widget.dart'; import 'package:guru_widgets/common/flexible_container.dart'; import 'package:guru_widgets/common/spacer.dart'; @@ -223,6 +218,7 @@ class StorePage extends GetWidget with _UIData { final store = snapshot.data; return Center( child: GridView.builder( + padding: const EdgeInsets.all(0), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( @@ -379,7 +375,7 @@ class StorePage extends GetWidget with _UIData { width: designSpec.tipsWidth, height: designSpec.tipsMinHeight, radius: Radius.circular(designSpec.tipsRadius), - spacing: 8, + spacing: Get.statusBarHeight / Get.pixelRatio + 4, nipHeight: 8, nipWidth: 18, backgroundColor: Colors.black.withOpacity(0.9), @@ -440,16 +436,22 @@ class StorePage extends GetWidget with _UIData { children: [ subPage ? TapWidget( - inkWell: true, - shape: const CircleBorder(), - onTap: () { - RouteCenter.instance.back(); - }, - child: closeWidget) + inkWell: true, + shape: const CircleBorder(), + onTap: () { + RouteCenter.instance.back(); + }, + child: SizedBox.square( + dimension: designSpec.appBarIconButtonSize, + child: Center( + child: closeWidget), + )) : title, - GuruAssetBar( + Padding(padding: designSpec.assetsBarPadding, child: GuruAssetBar( + key: controller.getKey("assetBar"), style: GuruAssetBarStyle.igc, - balanceStream: controller.observableIgcBalance) + balanceStream: controller.observableIgcBalance)) + ], )); } @@ -467,6 +469,7 @@ class StorePage extends GetWidget with _UIData { } return SafeArea( + bottom: false, child: Column(children: [ SizedSpacer(height: designSpec.appBarTopSpacing), buildAppBar(), @@ -491,7 +494,7 @@ class StorePage extends GetWidget with _UIData { fontSize: designSpec.tipsFontSize, fontWeight: GuruTheme.fwMedium, color: Colors.white, - height: 1.7, + height: 1.56, ), ), ), diff --git a/guru_ui/lib/pages/subscription/subscription_card.dart b/guru_ui/lib/pages/subscription/subscription_card.dart new file mode 100644 index 0000000..b565298 --- /dev/null +++ b/guru_ui/lib/pages/subscription/subscription_card.dart @@ -0,0 +1,632 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:design/design.dart'; +import 'package:flutter/material.dart'; +import 'package:guru_app/financial/iap/iap_model.dart'; +import 'package:guru_ui/pages/subscription/subscription_page.dart'; +import 'package:guru_ui/widget/image/adaptive_image.dart'; +import 'package:guru_widgets/button/single_tap_widget.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; + +part 'subscription_card.g.dart'; + +@DesignSpec(width: 242, height: 320, nestedSpec: true) +abstract class SubscriptionMainCardDesignSpec implements BasicDesignSpec { + @SpecSize(SpecWidth(242), SpecHeight(58)) + Size get decorationSize; + + @SpecRadius.only(topStart: SpecHeight(24), topEnd: SpecHeight(24)) + BorderRadiusDirectional get premiumCenterCardDecorationRadius; + + @SpecAbsoluteFontSize(22) + double get decorationFontSize; + + @SpecHeight(4) + double get cardBorderWidth; + + @SpecRadius.circular(SpecHeight(24)) + BorderRadiusDirectional get cardRadius; + + @SpecEdgeInsets.only( + top: SpecHeight(40), start: SpecWidth(16), end: SpecWidth(16)) + EdgeInsetsDirectional get titleMargin; + + @SpecAbsoluteFontSize(38) + double get titleFontSize; + + @SpecEdgeInsets.only( + top: SpecHeight(10), start: SpecWidth(16), end: SpecWidth(16)) + EdgeInsetsDirectional get subTitleMargin; + + @SpecAbsoluteFontSize(20) + double get subTitleFontSize; + + @SpecEdgeInsets.only( + bottom: SpecHeight(6), start: SpecWidth(16), end: SpecWidth(16)) + EdgeInsetsDirectional get thenMargin; + + @SpecAbsoluteFontSize(20) + double get thenFontSize; + + @SpecEdgeInsets.only( + bottom: SpecHeight(32), start: SpecWidth(16), end: SpecWidth(16)) + EdgeInsetsDirectional get priceMargin; + + @SpecAbsoluteFontSize(24) + double get priceFontSize; + + @SpecHeight(-30) + double get cardLabelTopSpcing; + + @SpecHeight(-20) + double get cardLabelRightSpcing; + + @SpecHeight(112) + double get cardLabelSize; + + static SubscriptionMainCardDesignSpec create(Size size, + {Offset offset = Offset.zero}) => + _SubscriptionMainCardDesignSpec.from(size, offset: offset); +} + +@DesignSpec(width: 206, height: 268, nestedSpec: true) +abstract class SubscriptionEdgeCardDesignSpec implements BasicDesignSpec { + @SpecHeight(4) + double get cardBorderWidth; + + @SpecRadius.circular(SpecHeight(24)) + BorderRadiusDirectional get cardRadius; + + @SpecEdgeInsets.only( + top: SpecHeight(40), start: SpecWidth(16), end: SpecWidth(16)) + EdgeInsetsDirectional get titleMargin; + + @SpecAbsoluteFontSize(32) + double get titleFontSize; + + @SpecEdgeInsets.only( + top: SpecHeight(10), start: SpecWidth(16), end: SpecWidth(16)) + EdgeInsetsDirectional get subTitleMargin; + + @SpecAbsoluteFontSize(20) + double get subTitleFontSize; + + @SpecEdgeInsets.only( + bottom: SpecHeight(4), start: SpecWidth(16), end: SpecWidth(16)) + EdgeInsetsDirectional get thenMargin; + + @SpecAbsoluteFontSize(17) + double get thenFontSize; + + @SpecEdgeInsets.only( + bottom: SpecHeight(32), start: SpecWidth(16), end: SpecWidth(16)) + EdgeInsetsDirectional get priceMargin; + + @SpecAbsoluteFontSize(24) + double get priceFontSize; + + @SpecHeight(-30) + double get cardLabelTopSpcing; + + @SpecHeight(-20) + double get cardLabelRightSpcing; + + @SpecHeight(112) + double get cardLabelSize; + + static SubscriptionEdgeCardDesignSpec create(Size size, + {Offset offset = Offset.zero}) => + _SubscriptionEdgeCardDesignSpec.from(size, offset: offset); +} + +@DesignSpec(width: 650, height: 124, nestedSpec: true) +abstract class SubscriptionListCardDesignSpec implements BasicDesignSpec { + @SpecRadius.circular(SpecHeight(24)) + BorderRadiusDirectional get cardRadius; + + @SpecEdgeInsets.only(start: SpecWidth(40), end: SpecWidth(40)) + EdgeInsetsDirectional get cardPadding; + + @SpecAbsoluteFontSize(32) + double get titleFontSize; + + @SpecEdgeInsets.only(top: SpecWidth(10)) + EdgeInsetsDirectional get subTitleMargin; + + @SpecAbsoluteFontSize(22) + double get subTitleFontSize; + + @SpecAbsoluteFontSize(26) + double get priceFontSize; + + @SpecHeight(-12) + double get cardLabelTopSpcing; + + @SpecHeight(20) + double get cardLabelRightSpcing; + + @SpecSize(SpecWidth(126), SpecHeight(36)) + Size get cardLabelSize; + + static SubscriptionListCardDesignSpec create(Size size, + {Offset offset = Offset.zero}) => + _SubscriptionListCardDesignSpec.from(size, offset: offset); +} + +class SubscriptionMainCardModel { + final IapProduct? product; + final SubscriptionCardItem cardItem; + final SubscriptionTheme subscriptionTheme; + final GuruColorScheme? colorScheme; + final SubscriptionMainCardDesignSpec designSpec; + final VoidCallback onTap; + final bool selected; + + SubscriptionMainCardModel( + {required this.product, + required this.cardItem, + required this.subscriptionTheme, + this.colorScheme, + required this.designSpec, + required this.onTap, + this.selected = false}); +} + +class SubscriptionMainCard extends StatelessWidget { + final SubscriptionMainCardModel model; + + IapProduct? get product => model.product; + + SubscriptionCardItem get cardItem => model.cardItem; + + SubscriptionTheme get subscriptionTheme => model.subscriptionTheme; + + GuruColorScheme? get colorScheme => model.colorScheme; + + SubscriptionMainCardDesignSpec get designSpec => model.designSpec; + + bool get selected => model.selected; + + const SubscriptionMainCard({Key? key, required this.model}) : super(key: key); + + @override + Widget build(BuildContext context) { + final activeTheme = subscriptionTheme.activeCardStyle; + final defaultTheme = subscriptionTheme.cardStyle; + final activeDecoration = activeTheme?.decoration ?? + BoxDecoration( + color: colorScheme?.primaryContentColor, + borderRadius: designSpec.cardRadius, + border: Border.all( + color: colorScheme?.primaryColor ?? const Color(0xFF07B25E), + width: designSpec.cardBorderWidth, + strokeAlign: BorderSide.strokeAlignCenter, //StrokeAlign.inside, + ), + ); + final defaultDecoration = defaultTheme?.decoration ?? + BoxDecoration( + color: colorScheme?.primaryContentColor, + borderRadius: designSpec.cardRadius); + + final decoration = selected ? activeDecoration : defaultDecoration; + + return TapWidget( + onTap: () { + model.onTap(); + }, + child: Container( + decoration: decoration, + width: designSpec.measuredSize.width, + height: designSpec.measuredSize.height, + child: Stack(clipBehavior: Clip.none, children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (cardItem.labelText != null) + Container( + width: designSpec.decorationSize.width, + height: designSpec.decorationSize.height, + decoration: BoxDecoration( + borderRadius: + designSpec.premiumCenterCardDecorationRadius, + color: selected + ? const Color(0xFF07B25E) + : const Color(0x12FFFFFF), + ), + alignment: Alignment.center, + padding: EdgeInsetsDirectional.only( + top: designSpec.cardBorderWidth), + child: AutoSizeText( + cardItem.labelText!, + stepGranularity: 0.1, + minFontSize: 5, + maxLines: 1, + style: TextStyle( + color: selected + ? Colors.white + : const Color(0x4DFFFFFF), + fontSize: designSpec.decorationFontSize, + fontWeight: GuruTheme.fwSemiBold, + ), + ), + ), + Padding( + padding: designSpec.titleMargin, + child: AutoSizeText( + cardItem.title, + maxLines: 2, + minFontSize: 5, + textAlign: TextAlign.center, + style: TextStyle( + height: 1.236, + color: selected + ? (activeTheme?.cardTitleColor ?? + Colors.white) + : (defaultTheme?.cardTitleColor ?? + const Color(0xE6FFFFFF)), + fontSize: designSpec.titleFontSize, + fontWeight: GuruTheme.fwBold), + ), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (cardItem.thenTexg != null) + Padding( + padding: designSpec.thenMargin, + child: Text( + cardItem.thenTexg!, + style: TextStyle( + height: 1.25, + color: selected + ? (activeTheme?.cardSubTitleColor ?? + const Color(0xE6FFFFFF)) + : (defaultTheme?.cardSubTitleColor ?? + const Color(0xB3FFFFFF)), + fontSize: designSpec.thenFontSize, + fontWeight: GuruTheme.fwMedium, + ), + ), + ), + Padding( + padding: designSpec.priceMargin, + child: AutoSizeText( + product?.details.price ?? "?", + minFontSize: 5, + style: TextStyle( + height: 1.25, + color: selected + ? (activeTheme?.cardPriceColor ?? + const Color(0xE6FFFFFF)) + : (defaultTheme?.cardPriceColor ?? + const Color(0xB3FFFFFF)), + fontSize: designSpec.priceFontSize, + fontWeight: GuruTheme.fwSemiBold, + ), + ), + ), + ]) + ], + ), + if (cardItem.labelImage != null) + Positioned.directional( + textDirection: Directionality.of(context), + top: designSpec.cardLabelTopSpcing, + end: designSpec.cardLabelRightSpcing, + child: AdaptiveImage(cardItem.labelImage!, + height: designSpec.cardLabelSize)) + ]))); + } +} + +class SubscriptionEdgeCardModel { + final IapProduct? product; + final SubscriptionCardItem cardItem; + final SubscriptionTheme subscriptionTheme; + final GuruColorScheme? colorScheme; + final SubscriptionEdgeCardDesignSpec designSpec; + final VoidCallback onTap; + final bool selected; + + SubscriptionEdgeCardModel( + {required this.product, + required this.cardItem, + required this.subscriptionTheme, + this.colorScheme, + required this.designSpec, + required this.onTap, + this.selected = false}); +} + +class SubscriptionEdgeCard extends StatelessWidget { + final SubscriptionEdgeCardModel model; + + IapProduct? get product => model.product; + + SubscriptionCardItem get cardItem => model.cardItem; + + SubscriptionTheme get subscriptionTheme => model.subscriptionTheme; + + GuruColorScheme? get colorScheme => model.colorScheme; + + SubscriptionEdgeCardDesignSpec get designSpec => model.designSpec; + + bool get selected => model.selected; + + const SubscriptionEdgeCard({Key? key, required this.model}) : super(key: key); + + @override + Widget build(BuildContext context) { + final activeTheme = subscriptionTheme.activeCardStyle; + final defaultTheme = subscriptionTheme.cardStyle; + final activeDecoration = activeTheme?.decoration ?? + BoxDecoration( + color: colorScheme?.primaryContentColor, + borderRadius: designSpec.cardRadius, + border: Border.all( + color: colorScheme?.primaryColor ?? const Color(0xFF07B25E), + width: designSpec.cardBorderWidth, + strokeAlign: BorderSide.strokeAlignInside, //StrokeAlign.inside, + ), + ); + final defaultDecoration = defaultTheme?.decoration ?? + BoxDecoration( + color: colorScheme?.primaryContentColor ?? const Color(0xff252525), + borderRadius: designSpec.cardRadius); + + final decoration = selected ? activeDecoration : defaultDecoration; + + return TapWidget( + onTap: () { + model.onTap(); + }, + child: Container( + decoration: decoration, + width: designSpec.measuredSize.width, + height: designSpec.measuredSize.height, + child: + Stack(fit: StackFit.expand, clipBehavior: Clip.none, children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: designSpec.titleMargin, + child: AutoSizeText( + cardItem.title, + maxLines: 2, + minFontSize: 5, + textAlign: TextAlign.center, + style: TextStyle( + height: 1.25, + color: selected + ? (activeTheme?.cardTitleColor ?? + Colors.white) + : (defaultTheme?.cardTitleColor ?? + const Color(0xE6FFFFFF)), + fontSize: designSpec.titleFontSize, + fontWeight: GuruTheme.fwBold), + ), + ), + if (cardItem.subTitle != null) + Padding( + padding: designSpec.subTitleMargin, + child: Text( + cardItem.subTitle!, + maxLines: 2, + textAlign: TextAlign.center, + style: TextStyle( + height: 1.25, + color: selected + ? (activeTheme?.cardSubTitleColor ?? + const Color(0xE6FFFFFF)) + : (defaultTheme?.cardSubTitleColor ?? + const Color(0xB3FFFFFF)), + fontSize: designSpec.subTitleFontSize, + fontWeight: GuruTheme.fwMedium), + ), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (cardItem.thenTexg != null) + Padding( + padding: designSpec.thenMargin, + child: Text( + cardItem.thenTexg!, + style: TextStyle( + height: 1.29, + color: selected + ? const Color(0xE6FFFFFF) + : const Color(0xB3FFFFFF), + fontSize: designSpec.thenFontSize, + fontWeight: GuruTheme.fwMedium, + ), + ), + ), + Padding( + padding: designSpec.priceMargin, + child: AutoSizeText( + product?.details.price ?? "?", + minFontSize: 5, + maxLines: 1, + style: TextStyle( + height: 1.25, + color: selected + ? (activeTheme?.cardPriceColor ?? + const Color(0xE6FFFFFF)) + : (defaultTheme?.cardPriceColor ?? + const Color(0xB3FFFFFF)), + fontSize: designSpec.priceFontSize, + fontWeight: GuruTheme.fwSemiBold, + ), + ), + ), + ]) + ], + ), + if (cardItem.labelImage != null) + Positioned.directional( + textDirection: Directionality.of(context), + top: designSpec.cardLabelTopSpcing, + end: designSpec.cardLabelRightSpcing, + child: AdaptiveImage(cardItem.labelImage!, + height: designSpec.cardLabelSize)) + ]))); + } +} + +class SubscriptionListCardModel { + final IapProduct? product; + final SubscriptionCardItem cardItem; + final SubscriptionTheme subscriptionTheme; + final GuruColorScheme? colorScheme; + final SubscriptionListCardDesignSpec designSpec; + final VoidCallback onTap; + final bool selected; + + SubscriptionListCardModel( + {required this.product, + required this.cardItem, + required this.subscriptionTheme, + this.colorScheme, + required this.designSpec, + required this.onTap, + this.selected = false}); +} + +class SubscriptionListCard extends StatelessWidget { + final SubscriptionListCardModel model; + + IapProduct? get product => model.product; + + SubscriptionCardItem get cardItem => model.cardItem; + + SubscriptionTheme get subscriptionTheme => model.subscriptionTheme; + + GuruColorScheme? get colorScheme => model.colorScheme; + + SubscriptionListCardDesignSpec get designSpec => model.designSpec; + + bool get selected => model.selected; + + const SubscriptionListCard({Key? key, required this.model}) : super(key: key); + + @override + Widget build(BuildContext context) { + final activeTheme = subscriptionTheme.activeCardStyle; + final defaultTheme = subscriptionTheme.cardStyle; + final activeDecoration = activeTheme?.decoration ?? + BoxDecoration( + color: colorScheme?.primaryContentColor, + borderRadius: designSpec.cardRadius, + border: Border.all( + color: colorScheme?.primaryColor ?? const Color(0xFF07B25E), + width: 2, + strokeAlign: BorderSide.strokeAlignInside, //StrokeAlign.inside, + ), + ); + final defaultDecoration = defaultTheme?.decoration ?? + BoxDecoration( + color: colorScheme?.primaryContentColor ?? const Color(0xff252525), + borderRadius: designSpec.cardRadius); + + final decoration = selected ? activeDecoration : defaultDecoration; + + return TapWidget( + onTap: () { + model.onTap(); + }, + child: Container( + decoration: decoration, + width: designSpec.measuredSize.width, + height: designSpec.measuredSize.height, + child: + Stack(fit: StackFit.expand, clipBehavior: Clip.none, children: [ + Padding( + padding: designSpec.cardPadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + cardItem.title, + maxLines: 1, + minFontSize: 5, + textAlign: TextAlign.center, + style: TextStyle( + height: 1.25, + color: selected + ? (activeTheme?.cardTitleColor ?? + Colors.white) + : (defaultTheme?.cardTitleColor ?? + const Color(0xE6FFFFFF)), + fontSize: designSpec.titleFontSize, + fontWeight: GuruTheme.fwBold), + ), + if (cardItem.subTitle != null) + Padding( + padding: designSpec.subTitleMargin, + child: Text( + cardItem.subTitle!, + maxLines: 2, + textAlign: TextAlign.center, + style: TextStyle( + height: 1.25, + color: selected + ? (activeTheme?.cardSubTitleColor ?? + const Color(0xE6FFFFFF)) + : (defaultTheme?.cardSubTitleColor ?? + const Color(0xB3FFFFFF)), + fontSize: designSpec.subTitleFontSize, + fontWeight: GuruTheme.fwMedium), + ), + ), + ], + ), + Text( + product?.details.price ?? "?", + style: TextStyle( + height: 1.25, + color: selected + ? (activeTheme?.cardPriceColor ?? + const Color(0xE6FFFFFF)) + : (defaultTheme?.cardPriceColor ?? + const Color(0xB3FFFFFF)), + fontSize: designSpec.priceFontSize, + fontWeight: GuruTheme.fwSemiBold, + ), + ), + ], + ), + ), + if (cardItem.labelImage != null) + Positioned.directional( + textDirection: Directionality.of(context), + top: designSpec.cardLabelTopSpcing, + end: designSpec.cardLabelRightSpcing, + child: AdaptiveImage( + cardItem.labelImage!, + height: designSpec.cardLabelSize.height, + width: designSpec.cardLabelSize.width, + )) + ]))); + } +} diff --git a/guru_ui/lib/pages/subscription/subscription_card.g.dart b/guru_ui/lib/pages/subscription/subscription_card.g.dart new file mode 100644 index 0000000..3d9f537 --- /dev/null +++ b/guru_ui/lib/pages/subscription/subscription_card.g.dart @@ -0,0 +1,397 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'subscription_card.dart'; + +// ************************************************************************** +// DesignSpecGenerator +// ************************************************************************** + +class _SubscriptionMainCardDesignSpec extends SubscriptionMainCardDesignSpec { + _SubscriptionMainCardDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.decorationSize, + this.premiumCenterCardDecorationRadius, + this.decorationFontSize, + this.cardBorderWidth, + this.cardRadius, + this.titleMargin, + this.titleFontSize, + this.subTitleMargin, + this.subTitleFontSize, + this.thenMargin, + this.thenFontSize, + this.priceMargin, + this.priceFontSize, + this.cardLabelTopSpcing, + this.cardLabelRightSpcing, + this.cardLabelSize, + ); + + static final designMetrics = DesignMetrics.create(const Size(242.0, 320.0)); + + static final Map _cache = {}; + + @override + final Size decorationSize; + + @override + final BorderRadiusDirectional premiumCenterCardDecorationRadius; + + @override + final double decorationFontSize; + + @override + final double cardBorderWidth; + + @override + final BorderRadiusDirectional cardRadius; + + @override + final EdgeInsetsDirectional titleMargin; + + @override + final double titleFontSize; + + @override + final EdgeInsetsDirectional subTitleMargin; + + @override + final double subTitleFontSize; + + @override + final EdgeInsetsDirectional thenMargin; + + @override + final double thenFontSize; + + @override + final EdgeInsetsDirectional priceMargin; + + @override + final double priceFontSize; + + @override + final double cardLabelTopSpcing; + + @override + final double cardLabelRightSpcing; + + @override + final double cardLabelSize; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + + static _SubscriptionMainCardDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _SubscriptionMainCardDesignSpec._( + _measuredMetrics, offset, + Size(_measuredMetrics.measureWidth(242.0), + _measuredMetrics.measureHeight(58.0)), // decorationSize + BorderRadiusDirectional.only( + topStart: Radius.circular(_measuredMetrics.measureHeight(20.0)), + topEnd: Radius.circular(_measuredMetrics.measureHeight(20.0)), + bottomStart: Radius.circular(0.0), + bottomEnd: Radius.circular(0.0)), // premiumCenterCardDecorationRadius + _measuredMetrics.measureAbsoluteFontSize(22.0, + consistent: false), // decorationFontSize + _measuredMetrics.measureHeight(4.0, consistent: false), // cardBorderWidth + BorderRadiusDirectional.circular( + _measuredMetrics.measureHeight(24.0)), // cardRadius + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(16.0), + end: _measuredMetrics.measureWidth(16.0), + top: _measuredMetrics.measureHeight(40.0), + bottom: 0.0), // titleMargin + _measuredMetrics.measureAbsoluteFontSize(38.0, + consistent: false), // titleFontSize + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(16.0), + end: _measuredMetrics.measureWidth(16.0), + top: _measuredMetrics.measureHeight(10.0), + bottom: 0.0), // titleMargin + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: false), + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(16.0), + end: _measuredMetrics.measureWidth(16.0), + top: 0.0, + bottom: _measuredMetrics.measureHeight(6.0)), + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: false), + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(16.0), + end: _measuredMetrics.measureWidth(16.0), + top: 0.0, + bottom: _measuredMetrics.measureHeight(32.0)), // priceMargin + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // priceFontSize + _measuredMetrics.measureHeight(-56.0, + consistent: false), // cardLabelTopSpcing + _measuredMetrics.measureHeight(-40.0, + consistent: false), // cardLabelRightSpcing + _measuredMetrics.measureHeight(136.0, consistent: false), // cardLabelSize + ); + } + + static SubscriptionMainCardDesignSpec from( + Size size, { + Offset offset = Offset.zero, + }) { + final Size measuredSize = size; + final key = BasicDesignSpec.buildSpecKey(measuredSize, offset); + _SubscriptionMainCardDesignSpec? designSpec = _cache[key]; + if (kDebugMode || designSpec == null) { + designSpec = _create(measuredSize, offset: offset); + _cache[key] = designSpec; + } + return designSpec; + } +} + + +class _SubscriptionEdgeCardDesignSpec extends SubscriptionEdgeCardDesignSpec { + _SubscriptionEdgeCardDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.cardBorderWidth, + this.cardRadius, + this.titleMargin, + this.titleFontSize, + this.subTitleMargin, + this.subTitleFontSize, + this.thenMargin, + this.thenFontSize, + this.priceMargin, + this.priceFontSize, + this.cardLabelTopSpcing, + this.cardLabelRightSpcing, + this.cardLabelSize, + ); + + static final designMetrics = DesignMetrics.create(const Size(206.0, 268.0)); + + static final Map _cache = {}; + + @override + final double cardBorderWidth; + + @override + final BorderRadiusDirectional cardRadius; + + @override + final EdgeInsetsDirectional titleMargin; + + @override + final double titleFontSize; + + @override + final EdgeInsetsDirectional subTitleMargin; + + @override + final double subTitleFontSize; + + @override + final EdgeInsetsDirectional thenMargin; + + @override + final double thenFontSize; + + @override + final EdgeInsetsDirectional priceMargin; + + @override + final double priceFontSize; + + @override + final double cardLabelTopSpcing; + + @override + final double cardLabelRightSpcing; + + @override + final double cardLabelSize; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + + static _SubscriptionEdgeCardDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _SubscriptionEdgeCardDesignSpec._( + _measuredMetrics, offset, + _measuredMetrics.measureHeight(4.0, consistent: false), // cardBorderWidth + BorderRadiusDirectional.circular( + _measuredMetrics.measureHeight(24.0)), // cardRadius + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(16.0), + end: _measuredMetrics.measureWidth(16.0), + top: _measuredMetrics.measureHeight(40.0), + bottom: 0.0), // titleMargin + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // titleFontSize + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(16.0), + end: _measuredMetrics.measureWidth(16.0), + top: _measuredMetrics.measureHeight(10.0), + bottom: 0.0), // titleMargin + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: false), + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(16.0), + end: _measuredMetrics.measureWidth(16.0), + top: 0.0, + bottom: _measuredMetrics.measureHeight(4.0)), + _measuredMetrics.measureAbsoluteFontSize(17.0, + consistent: false), + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(16.0), + end: _measuredMetrics.measureWidth(16.0), + top: 0.0, + bottom: _measuredMetrics.measureHeight(32.0)), // priceMargin + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // priceFontSize + _measuredMetrics.measureHeight(-56.0, + consistent: false), // cardLabelTopSpcing + _measuredMetrics.measureHeight(-40.0, + consistent: false), // cardLabelRightSpcing + _measuredMetrics.measureHeight(136.0, consistent: false), // cardLabelSize + ); + } + + static SubscriptionEdgeCardDesignSpec from( + Size size, { + Offset offset = Offset.zero, + }) { + final Size measuredSize = size; + final key = BasicDesignSpec.buildSpecKey(measuredSize, offset); + _SubscriptionEdgeCardDesignSpec? designSpec = _cache[key]; + if (kDebugMode || designSpec == null) { + designSpec = _create(measuredSize, offset: offset); + _cache[key] = designSpec; + } + return designSpec; + } +} + + +class _SubscriptionListCardDesignSpec extends SubscriptionListCardDesignSpec { + _SubscriptionListCardDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.cardRadius, + this.cardPadding, + this.titleFontSize, + this.subTitleMargin, + this.subTitleFontSize, + this.priceFontSize, + this.cardLabelTopSpcing, + this.cardLabelRightSpcing, + this.cardLabelSize, + ); + + static final designMetrics = DesignMetrics.create(const Size(650.0, 124.0)); + + static final Map _cache = {}; + + @override + final BorderRadiusDirectional cardRadius; + + @override + final EdgeInsetsDirectional cardPadding; + + @override + final double titleFontSize; + + @override + final EdgeInsetsDirectional subTitleMargin; + + @override + final double subTitleFontSize; + + @override + final double priceFontSize; + + @override + final double cardLabelTopSpcing; + + @override + final double cardLabelRightSpcing; + + @override + final Size cardLabelSize; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + + static _SubscriptionListCardDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _SubscriptionListCardDesignSpec._( + _measuredMetrics, offset, + BorderRadiusDirectional.circular( + _measuredMetrics.measureHeight(24.0)), // cardRadius + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(40.0), + end: _measuredMetrics.measureWidth(40.0), + top: 0.0, + bottom: 0.0), + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // titleFontSize + EdgeInsetsDirectional.only( + start: 0.0, + end: 0.0, + top: _measuredMetrics.measureHeight(10.0), + bottom: 0.0), // subTitleMargin + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: false), + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // priceFontSize + _measuredMetrics.measureHeight(-12.0, + consistent: false), // cardLabelTopSpcing + _measuredMetrics.measureHeight(20.0, + consistent: false), // cardLabelRightSpcing + Size(_measuredMetrics.measureWidth(126.0, consistent: false), _measuredMetrics.measureHeight(36.0, consistent: false)), // cardLabelSize + ); + } + + static SubscriptionListCardDesignSpec from( + Size size, { + Offset offset = Offset.zero, + }) { + final Size measuredSize = size; + final key = BasicDesignSpec.buildSpecKey(measuredSize, offset); + _SubscriptionListCardDesignSpec? designSpec = _cache[key]; + if (kDebugMode || designSpec == null) { + designSpec = _create(measuredSize, offset: offset); + _cache[key] = designSpec; + } + return designSpec; + } +} diff --git a/guru_ui/lib/pages/subscription/subscription_controller.dart b/guru_ui/lib/pages/subscription/subscription_controller.dart new file mode 100644 index 0000000..cc106de --- /dev/null +++ b/guru_ui/lib/pages/subscription/subscription_controller.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; +import 'package:design/design.dart'; +import 'package:guru_app/analytics/guru_analytics.dart'; +import 'package:guru_app/financial/iap/iap_manager.dart'; +import 'package:guru_app/financial/product/product_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_ui/pages/store/store_page.dart'; +import 'package:guru_app/controller/assets_aware.dart'; +import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_widgets/assetbar/guru_asset_bar.dart'; +import 'package:guru_widgets/dialog/guru_dialog.dart'; +import 'package:guru_utils/controller/lifecycle_controller.dart'; +import 'package:guru_ui/localizations/ui_strings.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; +import 'package:rxdart/rxdart.dart'; + +class SubscriptionController extends LifecycleController with AssetsAware { + final ScrollController scrollController = ScrollController(); + + final BehaviorSubject _selectProductIndexSubject = BehaviorSubject.seeded(1); + + Stream get observableSelectProductIndex => _selectProductIndexSubject.stream; + + void updateSelectedProductIndex(int index) { + _selectProductIndexSubject.add(index); + } + + Future restoreSubscription() async { + final oldStore = currentIapAssetStore.clone(); + await restorePurchases(); + final newStore = currentIapAssetStore; + final added = []; + newStore.data.forEach((key, value) { + if (oldStore.getAsset(key) == null) { + added.add(key.sku); + } + }); + // GuruAnalytics.instance.logEventEx("premium_restore_clk", + // itemCategory: from, + // itemName: added.isEmpty ? "non" : added.join(","), + // parameters: {"level": RuntimeProperty.instance.gameAudit.currentLevel}); + } + + // Future purchase(IapProduct? product, String? offerId) async { + // GuruAnalytics.instance.logEventEx("premium_click", + // itemCategory: from, + // itemName: "${product?.sku}${offerId != null ? "_$offerId" : ""}", + // parameters: {"level": RuntimeProperty.instance.gameAudit.currentLevel}); + // if (product == null) { + // return; + // } + // final appStrings = AppStrings.get(); + // final loadingCompleter = + // DialogUtils.showLoadingDialog(appStrings.processing, onCompleted: () {}); + // final success = await requestProduct(product).catchError((error, stacktrace) { + // Log.w("requestProduct error! $error", stackTrace: stacktrace); + // return false; + // }); + // loadingCompleter.complete(); + // if (success) { + // if (showBonusDialog) { + // ToastUtils.showCommonToast(appStrings.purchaseSuccess); + // if (await AppDB.instance.insertTodayPremiumDailyBonusEntity()) { + // await DialogUtils.showPremiumDailyBonusDialog("premium"); + // } + // RouteCenter.instance.backPageAndClearTop(mainPath: Routes.premium.mainPath, result: true); + // } else { + // RouteCenter.instance.backPageAndClearTop(mainPath: Routes.premium.mainPath, result: true); + // ToastUtils.showCommonToast(appStrings.purchaseSuccess); + // } + // GuruAnalytics.instance.logEventEx("premium_scs", + // itemCategory: from, + // itemName: "${product.sku}${offerId != null ? "_$offerId" : ""}", + // parameters: {"level": RuntimeProperty.instance.gameAudit.currentLevel}); + // } else { + // if (isIapCanceled) { + // ToastUtils.showCommonToast(appStrings.iapSuspended); + // } else { + // ToastUtils.showCommonToast(appStrings.checkNetworkAndGp); + // } + // RouteCenter.instance.back(); + // } + // } +} diff --git a/guru_ui/lib/pages/subscription/subscription_page.dart b/guru_ui/lib/pages/subscription/subscription_page.dart new file mode 100644 index 0000000..612121e --- /dev/null +++ b/guru_ui/lib/pages/subscription/subscription_page.dart @@ -0,0 +1,413 @@ +import 'dart:ui'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:design/design.dart'; +import 'package:guru_app/financial/iap/iap_model.dart'; +import 'package:guru_app/guru_app.dart'; +import 'package:guru_ui/guru_widget.dart'; +import 'package:guru_ui/localizations/l10n/generated/app_localizations.dart'; +import 'package:guru_ui/localizations/ui_strings.dart'; +import 'package:guru_ui/pages/subscription/subscription_card.dart'; +import 'package:guru_ui/pages/subscription/subscription_controller.dart'; +import 'package:guru_widgets/button/single_tap_widget.dart'; +import 'package:guru_widgets/pages/webview/guru_webview_page.dart'; +import 'package:guru_widgets/theme/guru_theme.dart'; +import 'package:guru_popup/guru_popup.dart'; +import 'package:guru_utils/router/router.dart'; + +part 'subscription_page.g.dart'; + +@DesignSpec(width: 750, height: 1624) +abstract class SubscriptionDesignSpec implements BasicDesignSpec { + @SpecEdgeInsets.only( + start: SpecWidth(40), end: SpecWidth(40), top: SpecStatusBarHeight(50)) + EdgeInsetsDirectional get appBarMargin; + + @CombinedSpec(SpecHeight(88), SpecStatusBarHeight(50)) + double get appBarHeight; + + @SpecHeight(48) + double get closeIconSize; + + @SpecAbsoluteFontSize(24, consistent: true) + double get restoreFontSize; + + @SpecEdgeInsets.only( + top: SpecHeight(24, consistent: true), + bottom: SpecNavigationBarHeight(30), + start: SpecWidth(32), + end: SpecWidth(32)) + EdgeInsetsDirectional get bottomCardsPadding; + + @SpecHeight(48, consistent: true) + double get premiumCardsBottomSpacing; + + @SpecSize(SpecWidth(630), SpecHeight(104)) + Size get buttonSize; + + @SpecEdgeInsets.only(top: SpecHeight(28), bottom: SpecHeight(32)) + EdgeInsetsDirectional get policyPadding; + + @SpecAbsoluteFontSize(20) + double get policyFontSize; + + @NestedSpec(242, 320) + SubscriptionMainCardDesignSpec get mainCardSpec; + + @NestedSpec(206, 268) + SubscriptionEdgeCardDesignSpec get edgeCardSpec; + + @NestedSpec(650, 124) + SubscriptionListCardDesignSpec get listCardSpec; + + @SpecHeight(22) + double get listCardBottomSpacing; + + static SubscriptionDesignSpec get() => _SubscriptionDesignSpec.get(); +} + +enum SubscriptionCardType { edge, main, list } + +class SubscriptionCardStyle { + final Decoration? decoration; + final Color? cardTitleColor; + final Color? cardSubTitleColor; + final Color? cardThenColor; + final Color? cardPriceColor; + + SubscriptionCardStyle( + {this.decoration, + this.cardTitleColor, + this.cardSubTitleColor, + this.cardThenColor, + this.cardPriceColor}); +} + +class SubscriptionTheme { + final Color? backgroundColor; + final Decoration? bottomDecoration; + final SubscriptionCardStyle? cardStyle; + final SubscriptionCardStyle? activeCardStyle; + final GuruButtonStyle buttonStyle; + + static SubscriptionTheme defaultTheme = + SubscriptionTheme(buttonStyle: GuruButtonStyle.neutral); + + SubscriptionTheme( + {this.backgroundColor, + this.bottomDecoration, + this.cardStyle, + this.activeCardStyle, + required this.buttonStyle}); +} + +class SubscriptionCardItem { + // final IapProduct productId; + final String title; + final String? subTitle; + final String? thenTexg; + final String price; + final String? labelImage; + final String? labelText; + final String? buttonText; + final SubscriptionCardType type; + + SubscriptionCardItem({ + // required this.productId, + required this.title, + this.subTitle, + this.thenTexg, + required this.price, + this.labelText, + this.labelImage, + this.buttonText, + this.type = SubscriptionCardType.edge, + }); +} + +class _UIHolder { + ThemeData? _theme; + GuruThemeData? _guruTheme; + SubscriptionTheme? _subscriptionTheme; + + late SubscriptionDesignSpec _designSpec; + late AppLocalizations appStrings; + + _UIHolder(); + + void attach(BuildContext context) { + _theme ??= Theme.of(context); + _guruTheme ??= GuruTheme.of(context); + _subscriptionTheme ??= _guruTheme?.getCustomTheme( + SubscriptionTheme.defaultTheme.runtimeType) ?? + SubscriptionTheme.defaultTheme; + _designSpec = SubscriptionDesignSpec.get(); + appStrings = UIStrings.get(); + } +} + +mixin _UIData { + final _UIHolder _uiHolder = _UIHolder(); + + ThemeData get theme => _uiHolder._theme ?? ThemeData(); + + GuruThemeData get guruTheme => _uiHolder._guruTheme ?? GuruThemeData(); + + GuruColorScheme get colorScheme => guruTheme.colorScheme; + + SubscriptionTheme get subscriptionTheme => + _uiHolder._subscriptionTheme ?? SubscriptionTheme.defaultTheme; + + SubscriptionDesignSpec get designSpec => _uiHolder._designSpec; + + AppLocalizations get appStrings => _uiHolder.appStrings; +} + +class SubscriptionPage extends GetWidget with _UIData { + final List items; + final Widget Function(BuildContext) contentBuilder; + + SubscriptionPage( + {super.key, required this.items, required this.contentBuilder}); + + Widget _buildPremiumCards(BuildContext context, int index) { + bool hasListItem = false; + for (var item in items) { + if (item.type == SubscriptionCardType.list) { + hasListItem = true; + } + } + final List widgets = []; + for (var i = 0; i < items.length; i++) { + final selected = i == index; + if (hasListItem) { + widgets.add(SubscriptionListCard( + model: SubscriptionListCardModel( + cardItem: items[i], + designSpec: designSpec.listCardSpec, + subscriptionTheme: subscriptionTheme, + onTap: () { + controller.updateSelectedProductIndex(i); + }, + selected: selected, + product: null), + )); + if (i != items.length - 1) { + widgets.add(SizedBox(height: designSpec.listCardBottomSpacing)); + } + } else { + if (items[i].type == SubscriptionCardType.edge) { + widgets.add(SubscriptionEdgeCard( + model: SubscriptionEdgeCardModel( + cardItem: items[i], + designSpec: designSpec.edgeCardSpec, + subscriptionTheme: subscriptionTheme, + onTap: () { + controller.updateSelectedProductIndex(i); + }, + selected: selected, + product: null), + )); + } else { + widgets.add(SubscriptionMainCard( + model: SubscriptionMainCardModel( + cardItem: items[i], + designSpec: designSpec.mainCardSpec, + subscriptionTheme: subscriptionTheme, + onTap: () { + controller.updateSelectedProductIndex(i); + }, + selected: selected, + product: null), + )); + } + } + } + return Padding( + padding: EdgeInsets.only(bottom: designSpec.premiumCardsBottomSpacing), + child: hasListItem + ? Column( + children: widgets, + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: widgets, + )); + } + + Widget _buildBottom(BuildContext context) { + return Container( + decoration: subscriptionTheme.bottomDecoration ?? + const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0x00121212), Color(0xFF121212), Color(0xFF121212)], + stops: [0.0, 55 / 584, 1.0], + ), + ), + padding: designSpec.bottomCardsPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + StreamBuilder( + stream: controller.observableSelectProductIndex, + builder: (context, snapshot) { + final int index = snapshot.data ?? 1; + return _buildPremiumCards(context, index); + }, + ), + StreamBuilder( + stream: controller.observableSelectProductIndex, + builder: (context, snapshot) { + final int index = snapshot.data ?? 1; + return GuruButton( + size: designSpec.buttonSize, + sizeSpec: GuruButtonSizeSpec.s1, + action: items[index].buttonText ?? 'Buy', + onPressed: () { + // final selectedProduct = items[index].productId; + // controller.purchase(selectedProduct, selectedProduct.offerId); + }, + ); + }, + ), + Padding( + padding: designSpec.policyPadding, + child: _buildPolicy(), + ), + ], + ), + ); + } + + Widget _buildContent(BuildContext context) { + return Stack(fit: StackFit.expand, children: [ + SingleChildScrollView( + child: contentBuilder(context), + ), + Align( + alignment: Alignment.bottomCenter, + child: _buildBottom(context), + ), + Align( + alignment: Alignment.topCenter, + child: Container( + width: designSpec.measuredSize.width, + height: designSpec.appBarHeight, + padding: designSpec.appBarMargin, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TapWidget( + onTap: () { + RouteCenter.instance.back(); + }, + child: guruTheme.iconScheme.closeIcon != null + ? Image.asset( + guruTheme.iconScheme.closeIcon!, + width: designSpec.closeIconSize, + height: designSpec.closeIconSize, + fit: BoxFit.fill, + ) + : Icon( + Icons.close, + size: designSpec.closeIconSize, + color: + colorScheme.primaryContentColor ?? Colors.white, + ), + ), + TapWidget( + onTap: () {}, + child: Text( + 'restore', + style: TextStyle( + color: colorScheme.primaryContentColor ?? Colors.white, + fontSize: designSpec.restoreFontSize, + fontWeight: GuruTheme.fwMedium, + ), + ), + ), + ], + ), + )) + ]); + } + + @override + Widget build(BuildContext context) { + _uiHolder.attach(context); + + return MediaQuery.removePadding( + context: context, + removeTop: true, + child: Scaffold( + backgroundColor: subscriptionTheme.backgroundColor ?? + colorScheme.backgroundColor ?? + const Color(0xFF121212), + body: _buildContent(context), + ), + ); + } + + Widget _buildPolicy() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _textLink( + text: 'privacyPolicy', + // text: appStrings.privacyPolicy, + url: GuruApp.instance.details.policyUrl, + onAfterTap: () { + GuruAnalytics.instance + .logEventEx("settings", itemName: "privacy_clk"); + }, + ), + Text( + " & ", + style: TextStyle( + fontSize: designSpec.policyFontSize, + color: const Color(0x66FFFFFF), + fontWeight: GuruTheme.fwRegular, + ), + ), + _textLink( + text: 'termsOfService', + // text: appStrings.termsOfService, + url: GuruApp.instance.details.termsUrl, + onAfterTap: () { + GuruAnalytics.instance.logEventEx("settings", itemName: "tos_clk"); + }, + ) + ], + ); + } + + Widget _textLink( + {required String text, required String url, VoidCallback? onAfterTap}) { + return TapWidget( + child: AutoSizeText( + text, + stepGranularity: 0.1, + minFontSize: 5, + maxLines: 1, + style: TextStyle( + fontSize: designSpec.policyFontSize, + color: const Color(0x66FFFFFF), + decoration: TextDecoration.underline, + fontWeight: GuruTheme.fwRegular, + ), + ), + onTap: () { + GuruPopup.instance.showDialog( + widget: GuruWebviewPage(url: Uri.dataFromString(url), title: text), + useSafeArea: false); + onAfterTap?.call(); + }, + ); + } +} diff --git a/guru_ui/lib/pages/subscription/subscription_page.g.dart b/guru_ui/lib/pages/subscription/subscription_page.g.dart new file mode 100644 index 0000000..e58bc94 --- /dev/null +++ b/guru_ui/lib/pages/subscription/subscription_page.g.dart @@ -0,0 +1,152 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'subscription_page.dart'; + +// ************************************************************************** +// DesignSpecGenerator +// ************************************************************************** + +class _SubscriptionDesignSpec extends SubscriptionDesignSpec { + _SubscriptionDesignSpec._( + this.measuredMetrics, + this.specOffset, + this.appBarMargin, + this.appBarHeight, + this.closeIconSize, + this.restoreFontSize, + this.bottomCardsPadding, + this.premiumCardsBottomSpacing, + this.buttonSize, + this.policyPadding, + this.policyFontSize, + this.mainCardSpec, + this.edgeCardSpec, + this.listCardSpec, + this.listCardBottomSpacing, + ); + + static final designMetrics = DesignMetrics.create(const Size(750.0, 1624.0)); + + static final Map _cache = {}; + + @override + final EdgeInsetsDirectional appBarMargin; + + @override + final double appBarHeight; + + @override + final double closeIconSize; + + @override + final double restoreFontSize; + + @override + final EdgeInsetsDirectional bottomCardsPadding; + + @override + final double premiumCardsBottomSpacing; + + @override + final Size buttonSize; + + @override + final EdgeInsetsDirectional policyPadding; + + @override + final double policyFontSize; + + @override + final SubscriptionMainCardDesignSpec mainCardSpec; + + @override + final SubscriptionEdgeCardDesignSpec edgeCardSpec; + + @override + final SubscriptionListCardDesignSpec listCardSpec; + + @override + final double listCardBottomSpacing; + + @override + final MeasuredMetrics measuredMetrics; + + @override + final Offset specOffset; + + @override + Size get measuredSize => measuredMetrics.size; + + static _SubscriptionDesignSpec _create( + Size measuredSize, { + Offset offset = Offset.zero, + }) { + final _measuredMetrics = designMetrics.measure(measuredSize); + return _SubscriptionDesignSpec._( + _measuredMetrics, offset, + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(40.0), + end: _measuredMetrics.measureWidth(40.0), + top: _measuredMetrics.statusBarHeight(50.0), + bottom: 0.0), // appBarMargin + (_measuredMetrics.measureHeight(88.0) + + _measuredMetrics.statusBarHeight(50.0)), // appBarHeight + _measuredMetrics.measureHeight(48.0, consistent: false), // closeIconSize + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: true), // restoreFontSize + EdgeInsetsDirectional.only( + start: _measuredMetrics.measureWidth(32.0), + end: _measuredMetrics.measureWidth(32.0), + top: _measuredMetrics.measureHeight(24.0, consistent: true), + bottom: _measuredMetrics.navigationBarHeight(30)), // bottomCardsTopSpacing + _measuredMetrics.measureHeight(48, consistent: true), + Size(_measuredMetrics.measureWidth(630), _measuredMetrics.measureHeight(104, consistent: true)), + EdgeInsetsDirectional.only( + start: 0.0, + end: 0.0, + top: _measuredMetrics.measureHeight(28.0), + bottom: _measuredMetrics.measureHeight(32.0)), // policyPadding + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: false), // policyFontSize + SubscriptionMainCardDesignSpec.create( + Size( + _measuredMetrics + .measureWidth(242.0, consistent: false) + .clamp(0.0, 4294967295.0), + _measuredMetrics + .measureHeight(320.0, consistent: false) + .clamp(0.0, 4294967295.0)), + offset: offset), + SubscriptionEdgeCardDesignSpec.create( + Size( + _measuredMetrics + .measureWidth(206.0, consistent: false) + .clamp(0.0, 4294967295.0), + _measuredMetrics + .measureHeight(268.0, consistent: false) + .clamp(0.0, 4294967295.0)), + offset: offset), + SubscriptionListCardDesignSpec.create( + Size( + _measuredMetrics + .measureWidth(650.0, consistent: false) + .clamp(0.0, 4294967295.0), + _measuredMetrics + .measureHeight(124.0, consistent: false) + .clamp(0.0, 4294967295.0)), + offset: offset), + _measuredMetrics.measureHeight(22.0, consistent: false) + ); + } + + static SubscriptionDesignSpec get({Offset offset = Offset.zero}) { + final Size measuredSize = Get.size; + final key = BasicDesignSpec.buildSpecKey(measuredSize, offset); + _SubscriptionDesignSpec? designSpec = _cache[key]; + if (kDebugMode || designSpec == null) { + designSpec = _create(measuredSize, offset: offset); + _cache[key] = designSpec; + } + return designSpec; + } +} diff --git a/guru_ui/packages/daily_challenge/lib/daily_challenge_package.dart b/guru_ui/packages/daily_challenge/lib/daily_challenge_package.dart index 9cddc28..eb8e417 100644 --- a/guru_ui/packages/daily_challenge/lib/daily_challenge_package.dart +++ b/guru_ui/packages/daily_challenge/lib/daily_challenge_package.dart @@ -29,6 +29,8 @@ class DailyChallengeCallbacks { void onCongratulationsPageImpression(DateTime dateTime) {} void onCongratulationsPageCollect(DateTime dateTime) {} + + void onCalendarCollected(DateTime dateTime) {} } class DailyChallengeConfig { diff --git a/guru_ui/packages/daily_challenge/lib/ui/pages/calendar/challenge_calendar_controller.dart b/guru_ui/packages/daily_challenge/lib/ui/pages/calendar/challenge_calendar_controller.dart index d1d329c..4b859e1 100644 --- a/guru_ui/packages/daily_challenge/lib/ui/pages/calendar/challenge_calendar_controller.dart +++ b/guru_ui/packages/daily_challenge/lib/ui/pages/calendar/challenge_calendar_controller.dart @@ -29,71 +29,56 @@ class CalendarModel { List get currentDailyChallenges => entities[DateTimeUtils.generateYearMonthDayNum(focusDate)] ?? []; - const CalendarModel( - this.focusDate, this.entities, this.awards, this.latestAchieved); + const CalendarModel(this.focusDate, this.entities, this.awards, this.latestAchieved); - static final CalendarModel defaultModel = - CalendarModel(DateTime.now(), {}, {}, null); + static final CalendarModel defaultModel = CalendarModel(DateTime.now(), {}, {}, null); - int get completedDaysInMonth => - DailyChallengeManager.instance.getAchievedDays(entities); + int get completedDaysInMonth => DailyChallengeManager.instance.getAchievedDays(entities); bool get isCompleted => - DailyChallengeManager.instance - .getDailyProgress(focusDate, currentDailyChallenges) >= - 100; + DailyChallengeManager.instance.getDailyProgress(focusDate, currentDailyChallenges) >= 100; - int get totalDaysInMonth => - DateTimeUtils.daysInMonth(focusDate.year, focusDate.month); + int get totalDaysInMonth => DateTimeUtils.daysInMonth(focusDate.year, focusDate.month); int get month => focusDate.month; int get yearMonth => DateTimeUtils.generateYearMonthNum(focusDate); - bool get isAchievedAward => - DailyChallengeManager.instance.getAchieved(yearMonth) >= 0; + bool get isAchievedAward => DailyChallengeManager.instance.getAchieved(yearMonth) >= 0; DailyChallengeAccessMode get accessMode => DailyChallengeManager.instance.getAccessMode(focusDate); } class ChallengeCalendarController extends AdsController with RewardedAware { - final BehaviorSubject focusDateTimeSubject = - BehaviorSubject.seeded(DateTime(0)); + final BehaviorSubject focusDateTimeSubject = BehaviorSubject.seeded(DateTime(0)); final BehaviorSubject calendarModelSubject = BehaviorSubject.seeded(CalendarModel.defaultModel); - final BehaviorSubject latestAchievedSubject = - BehaviorSubject.seeded(null); + final BehaviorSubject latestAchievedSubject = BehaviorSubject.seeded(null); DateTime get currentFocusDateTime => focusDateTimeSubject.value; Stream get observableFocusDateTime => focusDateTimeSubject.stream; Stream get observableFocusYearMonthChangedDateTime => - observableFocusDateTime - .distinct((prev, next) => DateTimeUtils.isSameMonth(prev, next)); + observableFocusDateTime.distinct((prev, next) => DateTimeUtils.isSameMonth(prev, next)); - Stream>> - get observableYearMonthDailyChallengeEntities => - observableFocusYearMonthChangedDateTime.switchMap((focusDate) => - DailyChallengeManager.instance - .observableDailyChallengeMonthData(focusDate)); + Stream>> get observableYearMonthDailyChallengeEntities => + observableFocusYearMonthChangedDateTime.switchMap((focusDate) => + DailyChallengeManager.instance.observableDailyChallengeMonthData(focusDate)); Stream> get observableAwards => DailyChallengeManager.instance.observableAwards; - Stream get observableCalendarModel => - calendarModelSubject.stream; + Stream get observableCalendarModel => calendarModelSubject.stream; - Stream get observableLatestAchievedDateTime => - latestAchievedSubject.stream; + Stream get observableLatestAchievedDateTime => latestAchievedSubject.stream; CalendarModel get currentCalendarModel => calendarModelSubject.value; - int get monthPageIndex => - DailyChallengeManager.instance.getMonthIndex(currentFocusDateTime); + int get monthPageIndex => DailyChallengeManager.instance.getMonthIndex(currentFocusDateTime); final DateFormat monthFormat = DateFormat.MMMM(); final DateFormat MMMMyFormat = DateFormat('MMMM y'); @@ -144,13 +129,12 @@ class ChallengeCalendarController extends AdsController with RewardedAware { } if (focusEntity != null) { - focusDate = - DateTimeUtils.createDateTimeFromDateNum(focusEntity.yearMonthDay); + focusDate = DateTimeUtils.createDateTimeFromDateNum(focusEntity.yearMonthDay); } else { final now = DateTime.now(); DateTime start = DateTime(dateTime.year, dateTime.month); - DateTime end = DateTime(dateTime.year, dateTime.month, - DateTimeUtils.daysInMonth(dateTime.year, dateTime.month)); + DateTime end = DateTime( + dateTime.year, dateTime.month, DateTimeUtils.daysInMonth(dateTime.year, dateTime.month)); start = start.isBefore(DailyChallengeManager.instance.config.start) ? DailyChallengeManager.instance.config.start : start; @@ -174,16 +158,13 @@ class ChallengeCalendarController extends AdsController with RewardedAware { final currentFocusDate = currentFocusDateTime; final dstIdx = DailyChallengeManager.instance.getMonthIndex(dateTime); - final srcIdx = - DailyChallengeManager.instance.getMonthIndex(currentFocusDate); + final srcIdx = DailyChallengeManager.instance.getMonthIndex(currentFocusDate); if (dstIdx == srcIdx) { return; } if ((srcIdx - dstIdx).abs() == 1) { - pageController.animateToPage( - DailyChallengeManager.instance.getMonthIndex(dateTime), - duration: const Duration(milliseconds: 450), - curve: Curves.easeInOut); + pageController.animateToPage(DailyChallengeManager.instance.getMonthIndex(dateTime), + duration: const Duration(milliseconds: 450), curve: Curves.easeInOut); } else { pageController.jumpToPage(dstIdx); } @@ -193,8 +174,7 @@ class ChallengeCalendarController extends AdsController with RewardedAware { if (date == null) { return []; } - final entities = - DailyChallengeManager.instance.getDailyChallengeByDate(date); + final entities = DailyChallengeManager.instance.getDailyChallengeByDate(date); return [DailyChallengeEvent(entities)]; } @@ -224,9 +204,8 @@ class ChallengeCalendarController extends AdsController with RewardedAware { } Future showLoading(Completer loadingCompleter) async { - final cancellableStream = Future.delayed(const Duration(seconds: 5)) - .then((value) => true) - .asStream(); + final cancellableStream = + Future.delayed(const Duration(seconds: 5)).then((value) => true).asStream(); final completer = GuruPopup.instance.showCancelableLoading( cancellableStream: cancellableStream, onCompleted: () { @@ -244,8 +223,7 @@ class ChallengeCalendarController extends AdsController with RewardedAware { Future playOrReplay(DailyChallengeAccessMode accessMode) async { if (accessMode == DailyChallengeAccessMode.reward) { - final adResult = await showRewardedAd( - scene: "daily_ads_play", loadingDialog: showLoading); + final adResult = await showRewardedAd(scene: "daily_ads_play", loadingDialog: showLoading); if (adResult.cause != AdCause.success) { return; } @@ -255,8 +233,7 @@ class ChallengeCalendarController extends AdsController with RewardedAware { Log.d("before playOrReplay:${calendarModel.isCompleted}"); DailyChallengeManager.instance.playOrReplay( - calendarModel.focusDate, calendarModel.currentDailyChallenges, - onCompleted: () { + calendarModel.focusDate, calendarModel.currentDailyChallenges, onCompleted: () { Log.d("onCompleted after playOrReplay:${calendarModel.isCompleted} "); if (!calendarModel.isCompleted) { latestAchievedSubject.addEx(calendarModel.focusDate); @@ -266,12 +243,12 @@ class ChallengeCalendarController extends AdsController with RewardedAware { Future _checkAndCollect(DateTime focusDate) async { await DailyChallengeManager.instance.checkAndCollect(focusDate); + DailyChallengeManager.instance.config.callbacks?.onCalendarCollected(focusDate); } @override void onResumed() { - final dateTime = - DailyChallengeManager.instance.consumeFocusDateAfterPageResume(); + final dateTime = DailyChallengeManager.instance.consumeFocusDateAfterPageResume(); if (dateTime != null) { _switchMonth(dateTime); } @@ -281,8 +258,7 @@ class ChallengeCalendarController extends AdsController with RewardedAware { void onInit() { super.onInit(); final now = DateTime.now(); - pageController = PageController( - initialPage: DailyChallengeManager.instance.getMonthIndex(now)); + pageController = PageController(initialPage: DailyChallengeManager.instance.getMonthIndex(now)); addSubscription(Rx.combineLatest4( observableFocusDateTime, observableYearMonthDailyChallengeEntities, diff --git a/guru_ui/packages/daily_challenge/lib/ui/pages/calendar/challenge_calendar_page.dart b/guru_ui/packages/daily_challenge/lib/ui/pages/calendar/challenge_calendar_page.dart index d1c435e..00ac8c7 100644 --- a/guru_ui/packages/daily_challenge/lib/ui/pages/calendar/challenge_calendar_page.dart +++ b/guru_ui/packages/daily_challenge/lib/ui/pages/calendar/challenge_calendar_page.dart @@ -414,8 +414,8 @@ class ChallengeCalendarPage extends GetWidget }); } - // 这里没有直接使用AnimatedTransformBuilder的原因是由于该设计边框过窄,在做一些缩放动画时会补上层clip掉 - // 因为这里使用Overlay的办法完成相应的动画 + // 这里没有直接使用AnimatedTransformBuilder的原因是由于该设计边框过窄,在做一些缩放动画时会被上层clip掉 + // 因此这里使用Overlay的办法完成相应的动画 Widget achievedMarker(Widget marker) { final GlobalKey key = GlobalKey(); return AnimatedTransformBuilder.single( diff --git a/guru_ui/packages/design/lib/design.dart b/guru_ui/packages/design/lib/design.dart index b7cae16..b11d6c3 100644 --- a/guru_ui/packages/design/lib/design.dart +++ b/guru_ui/packages/design/lib/design.dart @@ -25,4 +25,5 @@ part 'design_offset.dart'; part 'design_size.dart'; -part 'design_spec_model.dart'; \ No newline at end of file +part 'design_spec_model.dart'; + diff --git a/guru_ui/packages/design/lib/design_field.dart b/guru_ui/packages/design/lib/design_field.dart index f4f1bff..58e37ce 100644 --- a/guru_ui/packages/design/lib/design_field.dart +++ b/guru_ui/packages/design/lib/design_field.dart @@ -61,31 +61,46 @@ class DesignField extends DesignObject { final combinedValue = value as CombinedValue; return combinedValue.combine(metrics); case _DesignMethod.statusBarHeight: - final statusBarHeight = Get.statusBarHeight / Get.pixelRatio; + final statusBarHeight = Get.statusBarHeight / + (metrics.adaptation?.pixelRatio ?? + SystemAdaptation.globalPixelRation ?? + Get.pixelRatio); if (statusBarHeight != 0) { return statusBarHeight; } return value * 1.0 * metrics.hScale; case _DesignMethod.wrappedInSafeAreaStatusBarHeight: - final statusBarHeight = Get.statusBarHeight / Get.pixelRatio; + final statusBarHeight = Get.statusBarHeight / + (metrics.adaptation?.pixelRatio ?? + SystemAdaptation.globalPixelRation ?? + Get.pixelRatio); if (statusBarHeight != 0) { return 0; } return value * 1.0 * metrics.hScale; case _DesignMethod.navigationBarHeight: - final navigationBarHeight = Get.bottomBarHeight / Get.pixelRatio; + final navigationBarHeight = Get.bottomBarHeight / + (metrics.adaptation?.pixelRatio ?? + SystemAdaptation.globalPixelRation ?? + Get.pixelRatio); if (navigationBarHeight != 0) { return navigationBarHeight; } return value * 1.0 * metrics.hScale; case _DesignMethod.wrappedInSafeAreaNavigationBarHeight: - final navigationBarHeight = Get.bottomBarHeight / Get.pixelRatio; + final navigationBarHeight = Get.bottomBarHeight / + (metrics.adaptation?.pixelRatio ?? + SystemAdaptation.globalPixelRation ?? + Get.pixelRatio); if (navigationBarHeight != 0) { return 0; } return value * 1.0 * metrics.hScale; case _DesignMethod.absoluteFontSize: - return ((((value * metrics.hScale) + (value * metrics.wScale)) / 2) / Get.textScaleFactor); + return ((((value * metrics.hScale) + (value * metrics.wScale)) / 2) / + (metrics.adaptation?.textScaleFactor ?? + SystemAdaptation.globalTextScaleFactor ?? + Get.textScaleFactor)); // return ((((value * metrics.hScale) + (value * metrics.wScale)) / 2) * Get.textScaleFactor); case _DesignMethod.average: default: @@ -95,7 +110,11 @@ class DesignField extends DesignObject { @override double measure(_DeviceMetrics metrics) { - _size ??= _calculate(designMethod, metrics, designValue); + _size ??= _calculate( + designMethod, + metrics, + designValue, + ); return _size!; } diff --git a/guru_ui/packages/design/lib/design_metrics.dart b/guru_ui/packages/design/lib/design_metrics.dart index 20b99fa..ca669c6 100644 --- a/guru_ui/packages/design/lib/design_metrics.dart +++ b/guru_ui/packages/design/lib/design_metrics.dart @@ -9,9 +9,10 @@ class _DeviceMetrics { final Size size; final TextDirection? direction; final Offset offset; + final SystemAdaptation? adaptation; const _DeviceMetrics(this.wScale, this.hScale, this.size, this.direction, - {this.offset = Offset.zero}); + {this.offset = Offset.zero, this.adaptation}); } enum DesignFit { fitWidth, fitHeight, fill } @@ -32,9 +33,24 @@ enum _DesignMethod { wrappedInSafeAreaNavigationBarHeight } +class SystemAdaptation { + final double? pixelRatio; + + final double? textScaleFactor; + + const SystemAdaptation({this.pixelRatio, this.textScaleFactor}); + + static SystemAdaptation? defaultSystemAdaptation; + + static double? get globalPixelRation => defaultSystemAdaptation?.pixelRatio; + + static double? get globalTextScaleFactor => defaultSystemAdaptation?.textScaleFactor; +} + class DesignMetrics { Size designResolution = Size.zero; final List objects = []; + final SystemAdaptation? adaptation; late Size measuredSize; static Map originFields = { @@ -50,9 +66,9 @@ class DesignMetrics { 9: DesignField._origin(9), }; - DesignMetrics.create(this.designResolution); + DesignMetrics.create(this.designResolution, {this.adaptation}); - DesignMetrics.dynamic() : designResolution = Size.zero; + DesignMetrics.dynamic({this.adaptation}) : designResolution = Size.zero; DesignField origin(num value) { final result = originFields[value]; @@ -228,7 +244,8 @@ class DesignMetrics { MeasuredMetrics measure(Size size) { measuredSize = size; final deviceMetrics = _DeviceMetrics( - size.width / designResolution.width, size.height / designResolution.height, size, null); + size.width / designResolution.width, size.height / designResolution.height, size, null, + adaptation: adaptation); for (var field in objects) { field.measure(deviceMetrics); } @@ -317,7 +334,9 @@ class MeasuredMetrics { double measureAbsoluteFontSize(double fontSize, {bool consistent = false}) { if (consistent) { - return fontSize * metrics.hScale / Get.textScaleFactor; + return fontSize * + metrics.hScale / + (metrics.adaptation?.textScaleFactor ?? Get.textScaleFactor); } return DesignField._calculate(_DesignMethod.absoluteFontSize, metrics, fontSize); } diff --git a/guru_ui/packages/design_generator/lib/src/generator.dart b/guru_ui/packages/design_generator/lib/src/generator.dart index 583c883..fe20ddb 100644 --- a/guru_ui/packages/design_generator/lib/src/generator.dart +++ b/guru_ui/packages/design_generator/lib/src/generator.dart @@ -43,6 +43,7 @@ class DesignSpecGenerator extends GeneratorForAnnotation<_design.DesignSpec> { static const specModeField = "specMode"; @deprecated static const nestedSpecField = "nestedSpec"; + static const textScaleFactorField = "textScaleFactor"; final _fieldAnnotationProcessors = [ FieldProcessor(_design.SpecOrigin, ["double"], _generateAssignSpecOrigin), @@ -116,11 +117,13 @@ class DesignSpecGenerator extends GeneratorForAnnotation<_design.DesignSpec> { final specMode = nestedSpec ? SpecMode.nested : (annotation.peek(specModeField)?.intValue ?? SpecMode.useGet); + final textScaleFactor = annotation.peek(textScaleFactorField)?.doubleValue; designSpec = _design.DesignSpec( width: annotation.peek(widthField)?.doubleValue ?? 0.0, height: annotation.peek(heightField)?.doubleValue ?? 0.0, specMode: specMode, - nestedSpec: nestedSpec); + nestedSpec: nestedSpec, + textScaleFactor: textScaleFactor); if (designSpec.specMode == SpecMode.nested && element.getMethod("create")?.isStatic != true) { final name = element.displayName; const todo = 'Nested specs need to provide a static `create` function'; @@ -277,8 +280,10 @@ class DesignSpecGenerator extends GeneratorForAnnotation<_design.DesignSpec> { Field _buildDesignMetrics() => Field((field) => field ..name = "designMetrics" ..static = true - ..assignment = - Code("DesignMetrics.create(const Size(${designSpec.width}, ${designSpec.height}))") + ..assignment = designSpec.textScaleFactor == null + ? Code("DesignMetrics.create(const Size(${designSpec.width}, ${designSpec.height}))") + : Code( + "DesignMetrics.create(const Size(${designSpec.width}, ${designSpec.height}), adaptation: const SystemAdaptation(textScaleFactor: ${designSpec.textScaleFactor}))") ..modifier = FieldModifier.final$); Field _buildCurrentDesignSpec(ClassElement element) => Field((field) => field diff --git a/guru_ui/packages/design_spec/lib/design_annotations.dart b/guru_ui/packages/design_spec/lib/design_annotations.dart index 2ca955e..c9da5c1 100644 --- a/guru_ui/packages/design_spec/lib/design_annotations.dart +++ b/guru_ui/packages/design_spec/lib/design_annotations.dart @@ -15,12 +15,14 @@ class DesignSpec { final int specMode; @Deprecated("在2.0.5之后,该参数将会移除,使用SpecMode中的useNested代替") final bool nestedSpec; + final double? textScaleFactor; const DesignSpec( {required this.width, required this.height, this.specMode = SpecMode.useGet, - this.nestedSpec = false}); + this.nestedSpec = false, + this.textScaleFactor}); } abstract class SpecField {} diff --git a/guru_ui/packages/guru_popup/lib/dialog/dialog_aware.dart b/guru_ui/packages/guru_popup/lib/dialog/dialog_aware.dart index aa4ccab..2f676fa 100644 --- a/guru_ui/packages/guru_popup/lib/dialog/dialog_aware.dart +++ b/guru_ui/packages/guru_popup/lib/dialog/dialog_aware.dart @@ -514,4 +514,19 @@ extension DialogAware on GuruPopup { primaryButtonInfo: "assets/images/illustration.png", primaryButtonSummary: "assets/images/illustration.png"); } + + void showBitmapDialog({required AppOwnerInfo info}) async { + final img = await AppOwnershipUtils.generateAppBitmap(info); + GuruPopup.instance.showStandardDialog( + title: 'Guru Game', + subtitle: info.appName, + illustration: Image.memory(img), + summary: + 'This statement affirms that the "${info.appName}" app, including its content and technology, is the sole property of GURU GAME team, created and launched in ${info.lanuchYear}.', + primaryButtonAction: 'Got it', + onPrimaryButtonPressed: () { + RouteCenter.instance.back(); + return DialogResult(true, null); + }); + } } diff --git a/guru_ui/packages/guru_popup/lib/guru_popup.dart b/guru_ui/packages/guru_popup/lib/guru_popup.dart index 4c24e4b..6f11fa3 100644 --- a/guru_ui/packages/guru_popup/lib/guru_popup.dart +++ b/guru_ui/packages/guru_popup/lib/guru_popup.dart @@ -13,7 +13,9 @@ import 'package:guru_popup/overlay/tips/tips.dart'; import 'package:guru_utils/audio/audio_effector.dart'; import 'package:guru_utils/core/ext.dart'; import 'package:guru_utils/log/log.dart'; +import 'package:guru_utils/router/router.dart'; import 'package:guru_utils/visual_feast/visual_feast_widget.dart'; +import 'package:guru_utils/app_ownership/app_ownership_utils.dart'; import 'package:guru_widgets/guru_widgets.dart'; import 'package:guru_widgets/pages/webview/guru_webview_page.dart'; import 'package:guru_widgets/overlay/guru_loading.dart'; diff --git a/guru_ui/packages/guru_popup/lib/overlay/overlay_aware.dart b/guru_ui/packages/guru_popup/lib/overlay/overlay_aware.dart index 0bcd674..265cf83 100644 --- a/guru_ui/packages/guru_popup/lib/overlay/overlay_aware.dart +++ b/guru_ui/packages/guru_popup/lib/overlay/overlay_aware.dart @@ -262,8 +262,8 @@ extension OverlayAware on GuruPopup { return completer; } - void showStardardOverlay(String text, - {String? icon, Duration duration = const Duration(milliseconds: 2000)}) { + void showStandardOverlay(String text, + {String? icon, Duration duration = const Duration(milliseconds: 2000), String? package}) { final completer = Completer(); final navigatorState = Navigator.of(Get.overlayContext!, rootNavigator: false); final overlayState = navigatorState.overlay!; @@ -272,7 +272,7 @@ extension OverlayAware on GuruPopup { builder: (BuildContext context) => Material( child: Center( - child: GuruStandardOverlayWidget(text: text, icon: icon))) + child: GuruStandardOverlayWidget(text: text, icon: icon, package: package))) ); completer.future.then((params) { overlayEntry.remove(); diff --git a/guru_ui/packages/guru_widgets/lib/appbar/guru_app_bar.dart b/guru_ui/packages/guru_widgets/lib/appbar/guru_app_bar.dart index b9b1d28..a526628 100644 --- a/guru_ui/packages/guru_widgets/lib/appbar/guru_app_bar.dart +++ b/guru_ui/packages/guru_widgets/lib/appbar/guru_app_bar.dart @@ -22,7 +22,10 @@ abstract class GuruAppBarDesignSpec implements BasicDesignSpec { @SpecHeight(94) double get appBarLargeHeight; - @SpecHorizontal(40) + @SpecHorizontal(24) + double get backIconLeftSpacing; + + @SpecHorizontal(16) double get backIconMarginStart; @SpecHeight(48) @@ -141,7 +144,7 @@ class GuruAppBar extends StatelessWidget implements PreferredSizeWidget { return Container( color: fillColor, - padding: EdgeInsets.only(top: topSpcing), + padding: EdgeInsets.only(top: topSpcing, left: designSpec.backIconLeftSpacing), child: AppBar( elevation: 0, centerTitle: size != GuruAppBarSize.large, diff --git a/guru_ui/packages/guru_widgets/lib/appbar/guru_app_bar.g.dart b/guru_ui/packages/guru_widgets/lib/appbar/guru_app_bar.g.dart index 2c3f80a..2481049 100644 --- a/guru_ui/packages/guru_widgets/lib/appbar/guru_app_bar.g.dart +++ b/guru_ui/packages/guru_widgets/lib/appbar/guru_app_bar.g.dart @@ -14,6 +14,7 @@ class _GuruAppBarDesignSpec extends GuruAppBarDesignSpec { this.appBarLargTopSpcing, this.appBarHeight, this.appBarLargeHeight, + this.backIconLeftSpacing, this.backIconMarginStart, this.leadingIconSize, this.titleTextSmallSize, @@ -38,6 +39,9 @@ class _GuruAppBarDesignSpec extends GuruAppBarDesignSpec { @override final double appBarLargeHeight; + @override + final double backIconLeftSpacing; + @override final double backIconMarginStart; @@ -64,6 +68,7 @@ class _GuruAppBarDesignSpec extends GuruAppBarDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruAppBarDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -72,16 +77,21 @@ class _GuruAppBarDesignSpec extends GuruAppBarDesignSpec { return _GuruAppBarDesignSpec._( _measuredMetrics, offset, _measuredMetrics.statusBarHeight(50.0), // appBarMarginTop - _measuredMetrics.measureHeight(10.0, consistent: false), // appBarLargTopSpcing + _measuredMetrics.measureHeight(10.0, + consistent: false), // appBarLargTopSpcing _measuredMetrics.measureHeight(88.0, consistent: false), // appBarHeight _measuredMetrics.measureHeight(94.0, consistent: false), // appBarLargeHeight - _measuredMetrics.measureHorizontal(40.0), // backIconMarginStart + _measuredMetrics.measureHorizontal(24.0), // backIconLeftSpacing + _measuredMetrics.measureHorizontal(16.0), // backIconMarginStart _measuredMetrics.measureHeight(48.0, consistent: false), // leadingIconSize - _measuredMetrics.measureAbsoluteFontSize(34.0), // titleTextSmallSize - _measuredMetrics.measureAbsoluteFontSize(40.0), // titleTextMiddleSize - _measuredMetrics.measureAbsoluteFontSize(50.0), // titleTextLargeSize + _measuredMetrics.measureAbsoluteFontSize(34.0, + consistent: false), // titleTextSmallSize + _measuredMetrics.measureAbsoluteFontSize(40.0, + consistent: false), // titleTextMiddleSize + _measuredMetrics.measureAbsoluteFontSize(50.0, + consistent: false), // titleTextLargeSize _measuredMetrics.measureHeight(48.0, consistent: false), // actionIconSize ); } diff --git a/guru_ui/packages/guru_widgets/lib/assetbar/guru_asset_bar.dart b/guru_ui/packages/guru_widgets/lib/assetbar/guru_asset_bar.dart index 4c7b4db..ec4b87f 100644 --- a/guru_ui/packages/guru_widgets/lib/assetbar/guru_asset_bar.dart +++ b/guru_ui/packages/guru_widgets/lib/assetbar/guru_asset_bar.dart @@ -216,11 +216,10 @@ class GuruAssetBarState extends State final GlobalKey _assetIconKey = GlobalKey(); StreamSubscription? _subscription; late AnimationController controller; - final BehaviorSubject balanceTransition = + final BehaviorSubject?> balanceTransition = BehaviorSubject.seeded(null); int currentBalance = 0; - bool showIncreaseAnimation = false; @override void initState() { @@ -230,14 +229,14 @@ class GuruAssetBarState extends State _listenBalance(); } - void updateBalanceTransition(BalanceTransition transition) { - final latestBalanceTransition = balanceTransition.value; - if (latestBalanceTransition?.listener != null) { - latestBalanceTransition!.animation - .removeStatusListener(latestBalanceTransition.listener!); - } - balanceTransition.addEx(transition); - } + // void updateBalanceTransition(BalanceTransition transition) { + // final latestBalanceTransition = balanceTransition.value; + // if (latestBalanceTransition?.listener != null) { + // latestBalanceTransition!.animation + // .removeStatusListener(latestBalanceTransition.listener!); + // } + // balanceTransition.addEx(transition); + // } void _listenBalance() { _subscription ??= widget.balanceStream.listen((balance) { @@ -250,24 +249,17 @@ class GuruAssetBarState extends State currentBalance = balance; return; } - statusListener(status) { - if (status == AnimationStatus.completed) { - currentBalance = balance; - } - } + final lastBalance = currentBalance; + currentBalance = balance; - updateBalanceTransition(BalanceTransition( - IntTween(begin: currentBalance, end: balance).animate(controller) - ..addStatusListener(statusListener), - statusListener)); + balanceTransition.addEx(IntTween(begin: lastBalance, end: currentBalance).animate(controller)); controller.forward(from: 0.0); } else { if (balanceTransition.isClosed) { return; } currentBalance = balance; - updateBalanceTransition( - BalanceTransition(AlwaysStoppedAnimation(balance), null)); + balanceTransition.addEx(AlwaysStoppedAnimation(balance)); } } }); @@ -278,10 +270,10 @@ class GuruAssetBarState extends State } Widget _buildBalanceText() { - return StreamBuilder( + return StreamBuilder?>( stream: balanceTransition.stream, builder: (context, snapshot) { - final animation = snapshot.data?.animation ?? + final animation = snapshot.data ?? AlwaysStoppedAnimation(currentBalance); if (animation is AlwaysStoppedAnimation) { return AutoSizeText( diff --git a/guru_ui/packages/guru_widgets/lib/assetbar/guru_asset_bar.g.dart b/guru_ui/packages/guru_widgets/lib/assetbar/guru_asset_bar.g.dart index 04a07ba..ec402ae 100644 --- a/guru_ui/packages/guru_widgets/lib/assetbar/guru_asset_bar.g.dart +++ b/guru_ui/packages/guru_widgets/lib/assetbar/guru_asset_bar.g.dart @@ -52,6 +52,7 @@ class _GuruAssetBarS1DesignSpec extends GuruAssetBarS1DesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruAssetBarS1DesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -63,7 +64,8 @@ class _GuruAssetBarS1DesignSpec extends GuruAssetBarS1DesignSpec { _measuredMetrics.measureHeight(2.0, consistent: false), // assetIconTopSpcing _measuredMetrics.measureHeight(56.0, consistent: false), // barHeight - _measuredMetrics.measureAbsoluteFontSize(24.0), // balanceFontSize + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // balanceFontSize EdgeInsets.only( left: _measuredMetrics.measureWidth(4.0), right: _measuredMetrics.measureWidth(4.0), @@ -151,6 +153,7 @@ class _GuruAssetBarS2DesignSpec extends GuruAssetBarS2DesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruAssetBarS2DesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -167,7 +170,8 @@ class _GuruAssetBarS2DesignSpec extends GuruAssetBarS2DesignSpec { _measuredMetrics.measureHeight(44.0, consistent: false), // barHeight _measuredMetrics.measureWidth(71.0), // balanceWidth _measuredMetrics.measureHorizontal(49.0), // balanceStartSpacing - _measuredMetrics.measureAbsoluteFontSize(24.0), // balanceFontSize + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // balanceFontSize _measuredMetrics.measureHeight(48.0, consistent: false), // trailingSize ); } diff --git a/guru_ui/packages/guru_widgets/lib/banner/purchase_banner.g.dart b/guru_ui/packages/guru_widgets/lib/banner/purchase_banner.g.dart index 34d560c..eaec66f 100644 --- a/guru_ui/packages/guru_widgets/lib/banner/purchase_banner.g.dart +++ b/guru_ui/packages/guru_widgets/lib/banner/purchase_banner.g.dart @@ -128,6 +128,7 @@ class _PurchaseBannerDesignSpec extends PurchaseBannerDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _PurchaseBannerDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, diff --git a/guru_ui/packages/guru_widgets/lib/button/guru_button.dart b/guru_ui/packages/guru_widgets/lib/button/guru_button.dart index ea4e69c..f60ad11 100644 --- a/guru_ui/packages/guru_widgets/lib/button/guru_button.dart +++ b/guru_ui/packages/guru_widgets/lib/button/guru_button.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:design/design.dart'; import 'package:flutter/material.dart'; import 'package:guru_widgets/common/spacer.dart'; +import 'package:guru_widgets/guru_widgets.dart'; import 'package:guru_widgets/image/adaptive_image.dart'; import 'package:guru_widgets/theme/guru_theme.dart'; import 'package:guru_utils/widget/widget_utils.dart'; @@ -80,7 +81,7 @@ abstract class GuruButtonS1DesignSpec implements GuruButtonDesignSpec { double get iconSize; @override - @SpecAbsoluteFontSize(36) + @SpecAbsoluteFontSize(36, consistent: true) double get mainFontSize; @override @@ -106,18 +107,18 @@ abstract class GuruButtonS2DesignSpec implements GuruButtonDesignSpec { double get iconSize; @override - @SpecAbsoluteFontSize(32) + @SpecAbsoluteFontSize(32, consistent: true) double get mainFontSize; @override - @SpecAbsoluteFontSize(20) + @SpecAbsoluteFontSize(20, consistent: true) double get summaryFontSize; static GuruButtonS2DesignSpec from(Size size, {Offset offset = Offset.zero}) => _GuruButtonS2DesignSpec.from(size, offset: offset); } -@DesignSpec(width: 542, height: 96, specMode: SpecMode.useSize) +@DesignSpec(width: 480, height: 80, specMode: SpecMode.useSize) abstract class GuruButtonS3DesignSpec implements GuruButtonDesignSpec { @override @SpecHorizontal(12) @@ -128,22 +129,22 @@ abstract class GuruButtonS3DesignSpec implements GuruButtonDesignSpec { double get infoMinWidth; @override - @SpecHeight(56) + @SpecHeight(48) double get iconSize; @override - @SpecAbsoluteFontSize(32) + @SpecAbsoluteFontSize(28, consistent: true) double get mainFontSize; @override - @SpecAbsoluteFontSize(20) + @SpecAbsoluteFontSize(18, consistent: true) double get summaryFontSize; static GuruButtonS3DesignSpec from(Size size, {Offset offset = Offset.zero}) => _GuruButtonS3DesignSpec.from(size, offset: offset); } -@DesignSpec(width: 542, height: 96, specMode: SpecMode.useSize) +@DesignSpec(width: 400, height: 72, specMode: SpecMode.useSize) abstract class GuruButtonS4DesignSpec implements GuruButtonDesignSpec { @override @SpecHorizontal(12) @@ -154,15 +155,15 @@ abstract class GuruButtonS4DesignSpec implements GuruButtonDesignSpec { double get infoMinWidth; @override - @SpecHeight(56) + @SpecHeight(48) double get iconSize; @override - @SpecAbsoluteFontSize(32) + @SpecAbsoluteFontSize(26, consistent: true) double get mainFontSize; @override - @SpecAbsoluteFontSize(20) + @SpecAbsoluteFontSize(18, consistent: true) double get summaryFontSize; static GuruButtonS4DesignSpec from(Size size, {Offset offset = Offset.zero}) => @@ -184,11 +185,11 @@ abstract class GuruButtonS5DesignSpec implements GuruButtonDesignSpec { double get iconSize; @override - @SpecAbsoluteFontSize(26) + @SpecAbsoluteFontSize(26, consistent: true) double get mainFontSize; @override - @SpecAbsoluteFontSize(18) + @SpecAbsoluteFontSize(18, consistent: true) double get summaryFontSize; static GuruButtonS5DesignSpec from(Size size, {Offset offset = Offset.zero}) => @@ -232,6 +233,9 @@ class GuruButton extends StatefulWidget { // Button 说明(第二行) final String? summary; + // outline模式下,border宽度 + final double? borderWidth; + const GuruButton({super.key, required this.size, this.child, @@ -246,6 +250,7 @@ class GuruButton extends StatefulWidget { this.throttleTime = 800, this.style, this.fillType, + this.borderWidth, this.sizeSpec = GuruButtonSizeSpec.s2}); @override @@ -274,13 +279,12 @@ class GuruButtonState extends State { } } - Widget _buildContent(ThemeData themeData, + Widget _buildContent(ThemeData themeData, GuruButtonDesignSpec designSpec, {bool isOutline = false, Color tintColor = Colors.white}) { final child = widget.child; if (child != null) { return child; } - final designSpec = GuruButtonDesignSpec.from(widget.sizeSpec, widget.size); final items = []; final leading = widget.leading; @@ -292,10 +296,12 @@ class GuruButtonState extends State { } final action = widget.action; if (action != null) { - items.add(Text(action, + items.add(AutoSizeText(action, + maxLines: 1, + minFontSize: 10, style: TextStyle( - fontSize: designSpec.mainFontSize, color: tintColor, + fontSize: designSpec.mainFontSize, fontWeight: GuruTheme.fwSemiBold))); } final trailing = widget.trailing; @@ -386,18 +392,21 @@ class GuruButtonState extends State { child: SizedBox(width: widget.size.width, height: widget.size.height, child: wrapper)); } - Widget buildElevatedButton(ThemeData themeData, GuruThemeData guruTheme, + Widget buildElevatedButton(ThemeData themeData, GuruButtonDesignSpec designSpec, GuruThemeData guruTheme, GuruButtonElevatedScheme? elevatedScheme, {bool isOutline = false, Widget? child}) { final primaryColor = elevatedScheme?.backgroundColor ?? themeData.primaryColor; - return ElevatedButton( + return SizedBox( + width: widget.size.width, + height: widget.size.height, + child: ElevatedButton( onPressed: () { _dispatchOnPress(capabilities: guruTheme.feedbackCapabilities); }, style: ButtonStyle( shape: MaterialStateProperty.all( RoundedRectangleBorder( - side: isOutline ? BorderSide(color: primaryColor, width: 2) : BorderSide.none, + side: isOutline ? BorderSide(color: primaryColor, width: widget.borderWidth ?? 2) : BorderSide.none, borderRadius: elevatedScheme?.radius ?? BorderRadius.circular(widget.size.height * 0.5), // 设置圆角半径 ), @@ -414,15 +423,19 @@ class GuruButtonState extends State { shadowColor: elevatedScheme?.shadowColor != null ? MaterialStateProperty.all(elevatedScheme!.shadowColor!) : null, + minimumSize: MaterialStateProperty.all(Size(widget.size.width, designSpec.measuredSize.height)), + maximumSize: MaterialStateProperty.all(Size(widget.size.width, designSpec.measuredSize.height)) ), child: child, - ); + )); } @override Widget build(BuildContext context) { final themeData = Theme.of(context); final guruThemeData = GuruTheme.of(context); + final designSpec = GuruButtonDesignSpec.from(widget.sizeSpec, widget.size); + GuruButtonDecorator? decorator = guruThemeData.buttonTheme.defaultDecorator; final style = widget.style; if (style != null) { @@ -436,7 +449,7 @@ class GuruButtonState extends State { if (decorator != null) { final contentTintColor = decorator.contentTintColor ?? themeData.primaryColor; - final child = _buildContent(themeData, tintColor: contentTintColor); + final child = _buildContent(themeData, designSpec, tintColor: contentTintColor); return buildDecorator(themeData, guruThemeData, decorator, child: child); } else { final elevatedScheme = style != null @@ -444,11 +457,11 @@ class GuruButtonState extends State { : guruThemeData.buttonTheme.primaryElevatedScheme; final fillType = widget.fillType ?? elevatedScheme?.fillType ?? GuruButtonFillType.solid; final isOutline = fillType == GuruButtonFillType.outline; - final child = _buildContent(themeData, + final child = _buildContent(themeData, designSpec, isOutline: isOutline, tintColor: (isOutline ? themeData.primaryColor : elevatedScheme?.contentTintColor) ?? Colors.white); - return buildElevatedButton(themeData, guruThemeData, elevatedScheme, + return buildElevatedButton(themeData, designSpec, guruThemeData, elevatedScheme, isOutline: isOutline, child: child); } } diff --git a/guru_ui/packages/guru_widgets/lib/button/guru_button.g.dart b/guru_ui/packages/guru_widgets/lib/button/guru_button.g.dart index bd9c366..ce8a72f 100644 --- a/guru_ui/packages/guru_widgets/lib/button/guru_button.g.dart +++ b/guru_ui/packages/guru_widgets/lib/button/guru_button.g.dart @@ -44,6 +44,7 @@ class _GuruButtonStyleDesignSpec extends GuruButtonStyleDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruButtonStyleDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -109,6 +110,7 @@ class _GuruButtonS1DesignSpec extends GuruButtonS1DesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruButtonS1DesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -119,8 +121,10 @@ class _GuruButtonS1DesignSpec extends GuruButtonS1DesignSpec { _measuredMetrics.measureHorizontal(12.0), // itemHorizontalSpacing _measuredMetrics.measureHorizontal(72.0), // infoMinWidth _measuredMetrics.measureHeight(56.0, consistent: false), // iconSize - _measuredMetrics.measureAbsoluteFontSize(36.0), // mainFontSize - _measuredMetrics.measureAbsoluteFontSize(20.0), // summaryFontSize + _measuredMetrics.measureAbsoluteFontSize(36.0, + consistent: true), // mainFontSize + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: false), // summaryFontSize ); } @@ -177,6 +181,7 @@ class _GuruButtonS2DesignSpec extends GuruButtonS2DesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruButtonS2DesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -187,8 +192,10 @@ class _GuruButtonS2DesignSpec extends GuruButtonS2DesignSpec { _measuredMetrics.measureHorizontal(12.0), // itemHorizontalSpacing _measuredMetrics.measureHorizontal(72.0), // infoMinWidth _measuredMetrics.measureHeight(56.0, consistent: false), // iconSize - _measuredMetrics.measureAbsoluteFontSize(32.0, consistent: false), // mainFontSize - _measuredMetrics.measureAbsoluteFontSize(20.0), // summaryFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: true), // mainFontSize + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: true), // summaryFontSize ); } @@ -218,7 +225,7 @@ class _GuruButtonS3DesignSpec extends GuruButtonS3DesignSpec { this.summaryFontSize, ); - static final designMetrics = DesignMetrics.create(const Size(542.0, 96.0)); + static final designMetrics = DesignMetrics.create(const Size(480.0, 80.0)); static final Map _cache = {}; @@ -245,6 +252,7 @@ class _GuruButtonS3DesignSpec extends GuruButtonS3DesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruButtonS3DesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -254,9 +262,11 @@ class _GuruButtonS3DesignSpec extends GuruButtonS3DesignSpec { _measuredMetrics, offset, _measuredMetrics.measureHorizontal(12.0), // itemHorizontalSpacing _measuredMetrics.measureHorizontal(72.0), // infoMinWidth - _measuredMetrics.measureHeight(56.0, consistent: false), // iconSize - _measuredMetrics.measureAbsoluteFontSize(32.0), // mainFontSize - _measuredMetrics.measureAbsoluteFontSize(20.0), // summaryFontSize + _measuredMetrics.measureHeight(48.0, consistent: false), // iconSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: true), // mainFontSize + _measuredMetrics.measureAbsoluteFontSize(18.0, + consistent: true), // summaryFontSize ); } @@ -286,7 +296,7 @@ class _GuruButtonS4DesignSpec extends GuruButtonS4DesignSpec { this.summaryFontSize, ); - static final designMetrics = DesignMetrics.create(const Size(542.0, 96.0)); + static final designMetrics = DesignMetrics.create(const Size(400.0, 72.0)); static final Map _cache = {}; @@ -313,6 +323,7 @@ class _GuruButtonS4DesignSpec extends GuruButtonS4DesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruButtonS4DesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -322,9 +333,11 @@ class _GuruButtonS4DesignSpec extends GuruButtonS4DesignSpec { _measuredMetrics, offset, _measuredMetrics.measureHorizontal(12.0), // itemHorizontalSpacing _measuredMetrics.measureHorizontal(72.0), // infoMinWidth - _measuredMetrics.measureHeight(56.0, consistent: false), // iconSize - _measuredMetrics.measureAbsoluteFontSize(32.0), // mainFontSize - _measuredMetrics.measureAbsoluteFontSize(20.0), // summaryFontSize + _measuredMetrics.measureHeight(48.0, consistent: false), // iconSize + _measuredMetrics.measureAbsoluteFontSize(26.0, + consistent: true), // mainFontSize + _measuredMetrics.measureAbsoluteFontSize(18.0, + consistent: true), // summaryFontSize ); } @@ -381,6 +394,7 @@ class _GuruButtonS5DesignSpec extends GuruButtonS5DesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruButtonS5DesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -391,8 +405,10 @@ class _GuruButtonS5DesignSpec extends GuruButtonS5DesignSpec { _measuredMetrics.measureHorizontal(12.0), // itemHorizontalSpacing _measuredMetrics.measureHorizontal(72.0), // infoMinWidth _measuredMetrics.measureHeight(44.0, consistent: false), // iconSize - _measuredMetrics.measureAbsoluteFontSize(26.0), // mainFontSize - _measuredMetrics.measureAbsoluteFontSize(18.0), // summaryFontSize + _measuredMetrics.measureAbsoluteFontSize(26.0, + consistent: true), // mainFontSize + _measuredMetrics.measureAbsoluteFontSize(18.0, + consistent: true), // summaryFontSize ); } diff --git a/guru_ui/packages/guru_widgets/lib/dialog/guru_dialog.g.dart b/guru_ui/packages/guru_widgets/lib/dialog/guru_dialog.g.dart index 334d8c3..f2fd3aa 100644 --- a/guru_ui/packages/guru_widgets/lib/dialog/guru_dialog.g.dart +++ b/guru_ui/packages/guru_widgets/lib/dialog/guru_dialog.g.dart @@ -52,6 +52,7 @@ class _GuruDialogContainerDesignSpec extends GuruDialogContainerDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruDialogContainerDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -182,6 +183,7 @@ class _GuruDialogDesignSpec extends GuruDialogDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruDialogDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -207,13 +209,15 @@ class _GuruDialogDesignSpec extends GuruDialogDesignSpec { right: _measuredMetrics.measureHorizontal(96.0), top: _measuredMetrics.measureVertical(16.0), bottom: _measuredMetrics.measureVertical(8.0)), - _measuredMetrics.measureAbsoluteFontSize(36.0), // titleFontSize + _measuredMetrics.measureAbsoluteFontSize(36.0, + consistent: false), // titleFontSize EdgeInsets.only( left: _measuredMetrics.measureHorizontal(96.0), right: _measuredMetrics.measureHorizontal(96.0), top: _measuredMetrics.measureVertical(16.0), bottom: 0.0), - _measuredMetrics.measureAbsoluteFontSize(28.0), // subtitleFontSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: false), // subtitleFontSize EdgeInsets.only( left: _measuredMetrics.measureHorizontal(63.0), right: _measuredMetrics.measureHorizontal(63.0), @@ -226,7 +230,8 @@ class _GuruDialogDesignSpec extends GuruDialogDesignSpec { right: _measuredMetrics.measureHorizontal(56.0), top: 0.0, bottom: _measuredMetrics.measureVertical(24.0)), - _measuredMetrics.measureAbsoluteFontSize(28.0), // summaryFontSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: false), // summaryFontSize EdgeInsets.only( left: 0.0, right: 0.0, @@ -239,7 +244,8 @@ class _GuruDialogDesignSpec extends GuruDialogDesignSpec { right: _measuredMetrics.measureHorizontal(56.0), top: _measuredMetrics.measureVertical(28.0), bottom: 0.0), - _measuredMetrics.measureAbsoluteFontSize(32.0), // secondaryButtonFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // secondaryButtonFontSize ); } @@ -340,6 +346,7 @@ class _GuruSpotlightDialogDesignSpec extends GuruSpotlightDialogDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruSpotlightDialogDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -365,13 +372,15 @@ class _GuruSpotlightDialogDesignSpec extends GuruSpotlightDialogDesignSpec { right: _measuredMetrics.measureHorizontal(56.0), top: _measuredMetrics.measureVertical(0.0), bottom: _measuredMetrics.measureVertical(24.0)), - _measuredMetrics.measureAbsoluteFontSize(36.0), // titleFontSize + _measuredMetrics.measureAbsoluteFontSize(36.0, + consistent: false), // titleFontSize EdgeInsets.only( left: _measuredMetrics.measureHorizontal(56.0), right: _measuredMetrics.measureHorizontal(56.0), top: 0.0, bottom: _measuredMetrics.measureVertical(24.0)), - _measuredMetrics.measureAbsoluteFontSize(28.0), // subtitleFontSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: false), // subtitleFontSize Size(_measuredMetrics.measureWidth(654.0), _measuredMetrics.measureWidth(654.0) * 0.667), // illustrationSize EdgeInsets.only( @@ -379,7 +388,8 @@ class _GuruSpotlightDialogDesignSpec extends GuruSpotlightDialogDesignSpec { right: _measuredMetrics.measureHorizontal(56.0), top: 0.0, bottom: _measuredMetrics.measureVertical(48.0)), - _measuredMetrics.measureAbsoluteFontSize(28.0), // summaryFontSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: false), // summaryFontSize _measuredMetrics.measureHeight(120.0, consistent: false), // scrollAbleContentMaxHeight EdgeInsets.only( @@ -394,7 +404,8 @@ class _GuruSpotlightDialogDesignSpec extends GuruSpotlightDialogDesignSpec { right: _measuredMetrics.measureHorizontal(56.0), top: _measuredMetrics.measureVertical(28.0), bottom: 0.0), - _measuredMetrics.measureAbsoluteFontSize(32.0), // secondaryButtonFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // secondaryButtonFontSize ); } @@ -491,6 +502,7 @@ class _GuruExtendDialogDesignSpec extends GuruExtendDialogDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruExtendDialogDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -518,19 +530,22 @@ class _GuruExtendDialogDesignSpec extends GuruExtendDialogDesignSpec { right: _measuredMetrics.measureHorizontal(24.0), top: 0.0, bottom: _measuredMetrics.measureVertical(14.0)), - _measuredMetrics.measureAbsoluteFontSize(36.0), // titleFontSize + _measuredMetrics.measureAbsoluteFontSize(36.0, + consistent: false), // titleFontSize EdgeInsets.only( left: 0.0, right: 0.0, top: 0.0, bottom: _measuredMetrics.measureVertical(24.0)), - _measuredMetrics.measureAbsoluteFontSize(28.0), // subtitleFontSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: false), // subtitleFontSize EdgeInsets.only( left: 0.0, right: 0.0, top: 0.0, bottom: _measuredMetrics.measureVertical(64.0)), - _measuredMetrics.measureAbsoluteFontSize(28.0), // summaryFontSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: false), // summaryFontSize EdgeInsets.only( left: 0.0, right: 0.0, @@ -543,7 +558,8 @@ class _GuruExtendDialogDesignSpec extends GuruExtendDialogDesignSpec { right: _measuredMetrics.measureHorizontal(56.0), top: _measuredMetrics.measureVertical(28.0), bottom: 0.0), - _measuredMetrics.measureAbsoluteFontSize(32.0), // secondaryButtonFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // secondaryButtonFontSize ); } @@ -640,6 +656,7 @@ class _GuruConfirmDialogDesignSpec extends GuruConfirmDialogDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruConfirmDialogDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -662,19 +679,22 @@ class _GuruConfirmDialogDesignSpec extends GuruConfirmDialogDesignSpec { right: _measuredMetrics.measureHorizontal(56.0), top: 0.0, bottom: _measuredMetrics.measureVertical(8.0)), - _measuredMetrics.measureAbsoluteFontSize(36.0), // titleFontSize + _measuredMetrics.measureAbsoluteFontSize(36.0, + consistent: false), // titleFontSize EdgeInsets.only( left: _measuredMetrics.measureHorizontal(16.0), right: _measuredMetrics.measureHorizontal(16.0), top: _measuredMetrics.measureVertical(16.0), bottom: _measuredMetrics.measureVertical(56.0)), - EdgeInsets.only( + EdgeInsets.only( left: _measuredMetrics.measureHorizontal(16.0), right: _measuredMetrics.measureHorizontal(16.0), top: _measuredMetrics.measureVertical(16.0), - bottom: _measuredMetrics.measureVertical(42.0)), - _measuredMetrics.measureAbsoluteFontSize(32.0), // noTitleSummaryFontSize - _measuredMetrics.measureAbsoluteFontSize(28.0), // summaryFontSize + bottom: _measuredMetrics.measureVertical(40.0)), + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // noTitleSummaryFontSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: false), // summaryFontSize _measuredMetrics.measureHeight(88.0, consistent: false), // summaryMinHeight _measuredMetrics.measureHeight(96.0, @@ -686,7 +706,8 @@ class _GuruConfirmDialogDesignSpec extends GuruConfirmDialogDesignSpec { right: _measuredMetrics.measureHorizontal(56.0), top: _measuredMetrics.measureVertical(44.0), bottom: 0.0), - _measuredMetrics.measureAbsoluteFontSize(32.0), // secondaryButtonFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // secondaryButtonFontSize Size(_measuredMetrics.measureHeight(96.0) * 2.83333333, _measuredMetrics.measureHeight(96.0)), // buttonSize _measuredMetrics.measureHorizontal(29.0), // buttonSpacing @@ -770,6 +791,7 @@ class _GuruAlertDialogDesignSpec extends GuruAlertDialogDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruAlertDialogDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -787,21 +809,25 @@ class _GuruAlertDialogDesignSpec extends GuruAlertDialogDesignSpec { right: 0.0, top: 0.0, bottom: _measuredMetrics.measureVertical(16.0)), - _measuredMetrics.measureAbsoluteFontSize(36.0), // titleFontSize + _measuredMetrics.measureAbsoluteFontSize(36.0, + consistent: false), // titleFontSize EdgeInsets.only( left: _measuredMetrics.measureHorizontal(56.0), right: _measuredMetrics.measureHorizontal(56.0), top: _measuredMetrics.measureVertical(64.0), bottom: _measuredMetrics.measureVertical(64.0)), - _measuredMetrics.measureAbsoluteFontSize(32.0), // noTitleSummaryFontSize - _measuredMetrics.measureAbsoluteFontSize(28.0), // summaryFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // noTitleSummaryFontSize + _measuredMetrics.measureAbsoluteFontSize(28.0, + consistent: false), // summaryFontSize _measuredMetrics.measureHeight(88.0, consistent: false), // summaryMinHeight _measuredMetrics.measureHeight(96.0, consistent: false), // noTitleSummaryMinHeight _measuredMetrics.measureHeight(104.0, consistent: false), // buttonHeight _measuredMetrics.measureHeight(1.0, consistent: false), // dividerSize - _measuredMetrics.measureAbsoluteFontSize(32.0), // buttonFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // buttonFontSize ); } diff --git a/guru_ui/packages/guru_widgets/lib/navigationbar/guru_navigation_bar.g.dart b/guru_ui/packages/guru_widgets/lib/navigationbar/guru_navigation_bar.g.dart index 913ba27..230f212 100644 --- a/guru_ui/packages/guru_widgets/lib/navigationbar/guru_navigation_bar.g.dart +++ b/guru_ui/packages/guru_widgets/lib/navigationbar/guru_navigation_bar.g.dart @@ -60,6 +60,7 @@ class _GuruNavigationBarDesignSpec extends GuruNavigationBarDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruNavigationBarDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, diff --git a/guru_ui/packages/guru_widgets/lib/overlay/asset/asset_background.dart b/guru_ui/packages/guru_widgets/lib/overlay/asset/asset_background.dart index ba900cc..817092d 100644 --- a/guru_ui/packages/guru_widgets/lib/overlay/asset/asset_background.dart +++ b/guru_ui/packages/guru_widgets/lib/overlay/asset/asset_background.dart @@ -1,4 +1,4 @@ -import 'dart:ui'; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:guru_utils/visual_feast/visual_feast_animation.dart'; @@ -16,6 +16,7 @@ class AssetBackground extends VisualFeastRender { // int get orderInLayer => 1; final GlobalKey barKey; final GuruAssetBarSizeSpec barSize; + final ui.Image assetIcon; late Rect boundary; @@ -25,7 +26,7 @@ class AssetBackground extends VisualFeastRender { String get renderId => "background"; - AssetBackground(this.barSize, this.barKey); + AssetBackground(this.barSize, this.barKey, this.assetIcon); @override void onAttach() { @@ -57,12 +58,34 @@ class AssetBackground extends VisualFeastRender { super.onRender(canvas); canvas.save(); RenderObject? widgetRenderBox = barKey.currentContext?.findRenderObject(); + final GuruAssetBarS1DesignSpec designSpecS1 = GuruThemeDesignSpec.get().assetBarS1DesignSpec; + final GuruAssetBarS2DesignSpec designSpecS2 = GuruThemeDesignSpec.get().assetBarS2DesignSpec; if (widgetRenderBox != null) { Offset widgetOffset = (widgetRenderBox as RenderBox).localToGlobal(Offset.zero); Size size = widgetRenderBox.size; - canvas.clipRect(Rect.fromLTWH(widgetOffset.dx, widgetOffset.dy, size.width, size.height), clipOp: ClipOp.difference); + Path path = Path(); + path.fillType = PathFillType.evenOdd; + path.addRect(Rect.largest); + if (barSize == GuruAssetBarSizeSpec.s1) { + RRect rrect = RRect.fromRectAndRadius(Rect.fromLTWH(widgetOffset.dx, widgetOffset.dy + designSpecS1.assetIconTopSpcing, size.width, designSpecS1.barHeight), Radius.circular(designSpecS1.barHeight / 2)); + path.addRRect(rrect); + } else { + final double offsetTop = (designSpecS2.measuredSize.height - designSpecS2.barHeight) / 2; + RRect rrect = RRect.fromRectAndRadius(Rect.fromLTWH(10, offsetTop, (size.width - 11), designSpecS2.barHeight), Radius.circular(designSpecS2.barHeight / 2)); + final path1 = Path(); + path1.addRRect(rrect); + path.addPath(path1, widgetOffset); + } + canvas.clipPath(path); } canvas.drawColor(Colors.black.withOpacity(opacity), BlendMode.srcOver); canvas.restore(); + if (widgetRenderBox != null && barSize == GuruAssetBarSizeSpec.s2) { + Offset widgetOffset = (widgetRenderBox as RenderBox).localToGlobal(Offset.zero); + final Paint paint = Paint()..filterQuality = FilterQuality.high; + final src = Rect.fromLTWH(0, 0, assetIcon.width.toDouble(), assetIcon.height.toDouble()); + final dst = Rect.fromLTWH(widgetOffset.dx, widgetOffset.dy, designSpecS2.assetIconSize, designSpecS2.assetIconSize); + canvas.drawImageRect(assetIcon, src, dst, paint); + } } } diff --git a/guru_ui/packages/guru_widgets/lib/overlay/asset/asset_reward.dart b/guru_ui/packages/guru_widgets/lib/overlay/asset/asset_reward.dart index a98751d..94afd9d 100644 --- a/guru_ui/packages/guru_widgets/lib/overlay/asset/asset_reward.dart +++ b/guru_ui/packages/guru_widgets/lib/overlay/asset/asset_reward.dart @@ -8,6 +8,7 @@ import 'package:guru_utils/svg/svg_parser.dart'; import 'package:guru_utils/log/log.dart'; import 'package:guru_utils/visual_feast/engine/visual_feast_engine.dart'; import 'package:guru_utils/visual_feast/visual_feast_animation.dart'; +import 'package:guru_widgets/assetbar/guru_asset_bar.dart'; import 'package:guru_widgets/overlay/guru_asset.dart'; import 'package:guru_widgets/theme/guru_theme.dart'; @@ -136,11 +137,12 @@ class AssetsReward extends VisualFeastRender { final List assets = []; Rect boundary = Rect.zero; + final GuruAssetBarSizeSpec barSize; VoidCallback? onFirstAssetComplete; VoidCallback? onAllAssetsComplete; - AssetsReward(this.key, { + AssetsReward(this.key, this.barSize, { this.onFirstAssetComplete, this.onAllAssetsComplete, }) : theme = GuruTheme.of(Get.context!).getCustomTheme(AssetOverlayTheme.defaultTheme.runtimeType) ?? AssetOverlayTheme.defaultTheme; @@ -151,8 +153,17 @@ class AssetsReward extends VisualFeastRender { Size widgetsize = widgetRenderBox.size; Rect dst = Rect.fromLTWH(widgetOffset.dx, widgetOffset.dy, widgetsize.height, widgetsize.height); final size = Get.size; - final src = Rect.fromCenter(center: Offset(size.width / 2, size.height / 2 + 40), width: widgetsize.height, height: widgetsize.height); - const renderRect = Rect.fromLTWH(0, 0, 20, 20); + Rect src; + Rect renderRect; + if (barSize == GuruAssetBarSizeSpec.s1) { + final GuruAssetBarS1DesignSpec designSpec = GuruThemeDesignSpec.get().assetBarS1DesignSpec; + renderRect = Rect.fromLTWH(0, 0, designSpec.assetIconSize, designSpec.assetIconSize); + src = Rect.fromCenter(center: Offset(size.width / 2, size.height / 2 + designSpec.assetIconSize * 2), width: widgetsize.height, height: widgetsize.height); + } else { + final GuruAssetBarS2DesignSpec designSpec = GuruThemeDesignSpec.get().assetBarS2DesignSpec; + renderRect = Rect.fromLTWH(0, 0, designSpec.assetIconSize, designSpec.assetIconSize); + src = Rect.fromCenter(center: Offset(size.width / 2, size.height / 2 + designSpec.assetIconSize * 2), width: widgetsize.height, height: widgetsize.height); + } final gemSprite = getSprite("asset"); // final radian = (2 * pi) / gemModels.length; diff --git a/guru_ui/packages/guru_widgets/lib/overlay/guru_asset_controller.dart b/guru_ui/packages/guru_widgets/lib/overlay/guru_asset_controller.dart index 7ece781..56c3aac 100644 --- a/guru_ui/packages/guru_widgets/lib/overlay/guru_asset_controller.dart +++ b/guru_ui/packages/guru_widgets/lib/overlay/guru_asset_controller.dart @@ -1,5 +1,5 @@ -import 'dart:ui' as ui show Image; import 'package:design/design.dart'; +import 'package:flame/cache.dart'; import 'package:flame/flame.dart'; import 'package:guru_utils/visual_feast/visual_feast_aware.dart'; import 'package:guru_utils/visual_feast/engine/visual_feast_engine.dart'; @@ -10,37 +10,43 @@ import 'package:guru_widgets/overlay/asset/asset_text.dart'; import 'package:guru_widgets/overlay/asset/asset_reward.dart'; import 'package:guru_utils/collection/collectionutils.dart'; -class GuruAssetOverlayController extends LifecycleController with VisualFeastAware { +class GuruAssetOverlayController extends LifecycleController + with VisualFeastAware { VisualFeastEngine? engine; - Future loadGemsResource(VisualFeastEngine engine) async { - final imageFutures = [ - Flame.images.load("ic_gem.png"), - ]; - final loadedResources = await Future.wait([ - Future.wait(imageFutures) - ]); - final images = loadedResources[0]; - addSprite("asset", VisualFeastSprite.fromImage(images[0])); - } + // Future loadGemsResource(VisualFeastEngine engine) async { + // final imageFutures = [ + // Flame.images.load("ic_gem.png"), + // ]; + // final loadedResources = await Future.wait([Future.wait(imageFutures)]); + // final images = loadedResources[0]; + // addSprite("asset", VisualFeastSprite.fromImage(images[0])); + // } - void startClaim( - int gems, - String scene, - GlobalKey key, { - bool useBg = true, - VoidCallback? onCompleted, - GuruAssetBarSizeSpec barSize = GuruAssetBarSizeSpec.s1 - }) async { + void startClaim(int gems, + {String scene = '', + required GlobalKey key, + bool useBg = true, + VoidCallback? onCompleted, + GuruAssetBarSizeSpec barSize = GuruAssetBarSizeSpec.s2, + required String assetIcon}) async { await Future.delayed(const Duration(milliseconds: 200)); engine ??= createEngine(); - await loadGemsResource(engine!); + + final flameImage = Images(prefix: ''); + final imageFutures = [ + flameImage.load(assetIcon), + ]; + final loadedResources = await Future.wait([Future.wait(imageFutures)]); + final images = loadedResources[0]; + addSprite("asset", VisualFeastSprite.fromImage(images[0])); - final background = AssetBackground(barSize, key); + final background = AssetBackground(barSize, key, images[0]); final assetText = AssetText(gems); final gemsReward = AssetsReward( key, + barSize, onFirstAssetComplete: () { claimGems(gems, scene); }, @@ -48,10 +54,9 @@ class GuruAssetOverlayController extends LifecycleController with VisualFeastAwa background.hideBackground(); await Future.delayed(const Duration(milliseconds: 650)); onCompleted?.call(); - // engine!.dispose(); }, ); - + engine!.attachRenders( ListUtils.filterOutNulls([background, gemsReward, assetText])); @@ -78,4 +83,4 @@ class GuruAssetOverlayController extends LifecycleController with VisualFeastAwa void onInit() { super.onInit(); } -} \ No newline at end of file +} diff --git a/guru_ui/packages/guru_widgets/lib/overlay/guru_loading.dart b/guru_ui/packages/guru_widgets/lib/overlay/guru_loading.dart index eb8c5fe..2917957 100644 --- a/guru_ui/packages/guru_widgets/lib/overlay/guru_loading.dart +++ b/guru_ui/packages/guru_widgets/lib/overlay/guru_loading.dart @@ -496,8 +496,9 @@ class GuruStandardOverlayWidget extends StatefulWidget { final String? icon; final String text; final String scene; + final String? package; - const GuruStandardOverlayWidget({Key? key, this.icon, this.text = "", this.scene = ""}) + const GuruStandardOverlayWidget({Key? key, this.icon, this.text = "", this.scene = "", this.package}) : super(key: key); @override @@ -538,7 +539,8 @@ class GuruStandardOverlayState extends State { height: designSpec.feedbackIconSize, child: widget.icon != null ? Image.asset(widget.icon!, width: designSpec.feedbackIconSize, - height: designSpec.feedbackIconSize) : Icon(Icons.fork_right, size: designSpec.feedbackIconSize, color: textColor)), + height: designSpec.feedbackIconSize, + package: widget.package,) : Icon(Icons.fork_right, size: designSpec.feedbackIconSize, color: textColor)), SizedSpacer(height: designSpec.dotBottomSpacing), AutoSizeText(widget.text, maxLines: 3, diff --git a/guru_ui/packages/guru_widgets/lib/overlay/guru_loading.g.dart b/guru_ui/packages/guru_widgets/lib/overlay/guru_loading.g.dart index 6d0ef69..e77e923 100644 --- a/guru_ui/packages/guru_widgets/lib/overlay/guru_loading.g.dart +++ b/guru_ui/packages/guru_widgets/lib/overlay/guru_loading.g.dart @@ -48,6 +48,7 @@ class _GuruLoadingDesignSpec extends GuruLoadingDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruLoadingDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -131,6 +132,7 @@ class _GuruSimpleLoadingDesignSpec extends GuruSimpleLoadingDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruSimpleLoadingDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -143,7 +145,8 @@ class _GuruSimpleLoadingDesignSpec extends GuruSimpleLoadingDesignSpec { right: _measuredMetrics.measureHorizontal(40.0), top: _measuredMetrics.measureVertical(46.0), bottom: _measuredMetrics.measureVertical(46.0)), - _measuredMetrics.measureAbsoluteFontSize(24.0), // loadingFontSize + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // loadingFontSize _measuredMetrics.measureHeight(72.0, consistent: false), // loadingIconSize _measuredMetrics.measureHeight(36.0, consistent: false), // dotSize @@ -218,6 +221,7 @@ class _GuruCancellableLoadingDesignSpec @override Size get measuredSize => measuredMetrics.size; + static _GuruCancellableLoadingDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -225,7 +229,8 @@ class _GuruCancellableLoadingDesignSpec final _measuredMetrics = designMetrics.measure(measuredSize); return _GuruCancellableLoadingDesignSpec._( _measuredMetrics, offset, - _measuredMetrics.measureAbsoluteFontSize(24.0), // loadingFontSize + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // loadingFontSize _measuredMetrics.measureHeight(72.0, consistent: false), // loadingIconSize _measuredMetrics.measureHeight(8.0, @@ -238,7 +243,8 @@ class _GuruCancellableLoadingDesignSpec _measuredMetrics.measureHeight(182.0, consistent: false), // contentMinHeight _measuredMetrics.measureHeight(1.0, consistent: false), // dividerSize - _measuredMetrics.measureAbsoluteFontSize(32.0), // cancelFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // cancelFontSize _measuredMetrics.measureHeight(96.0, consistent: false), // cancelButtonHeight ); @@ -297,6 +303,7 @@ class _GuruFeedbackLoadingDesignSpec extends GuruFeedbackLoadingDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruFeedbackLoadingDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -309,7 +316,8 @@ class _GuruFeedbackLoadingDesignSpec extends GuruFeedbackLoadingDesignSpec { right: _measuredMetrics.measureHorizontal(40.0), top: _measuredMetrics.measureVertical(46.0), bottom: _measuredMetrics.measureVertical(46.0)), - _measuredMetrics.measureAbsoluteFontSize(24.0), // loadingFontSize + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // loadingFontSize _measuredMetrics.measureHeight(36.0, consistent: false), // dotSize _measuredMetrics.measureHeight(72.0, consistent: false), // feedbackIconSize diff --git a/guru_ui/packages/guru_widgets/lib/pages/awards/guru_awards_page.dart b/guru_ui/packages/guru_widgets/lib/pages/awards/guru_awards_page.dart index c972fb5..4f3592a 100644 --- a/guru_ui/packages/guru_widgets/lib/pages/awards/guru_awards_page.dart +++ b/guru_ui/packages/guru_widgets/lib/pages/awards/guru_awards_page.dart @@ -8,10 +8,10 @@ part 'guru_awards_page.g.dart'; @DesignSpec(width: 750, height: 1624) abstract class GuruAwardsPageDesignSpec implements BasicDesignSpec { - @SpecOrigin(12) + @SpecHeight(24, consistent: true) double get tabbarTopSpacing; - @SpecOrigin(6) + @SpecHeight(12, consistent: true) double get tabbarBottomSpacing; static GuruAwardsPageDesignSpec get() => _GuruAwardsPageDesignSpec.get(); @@ -89,6 +89,7 @@ class _GuruAwardsPageState extends State { body: FlexibleContainer( child: widget.showTabbar ? GuruTabBar( + initialIndex: widget.initialIndex, tabbarTopSpacing: designSpec.tabbarTopSpacing, tabbarBottomSpacing: designSpec.tabbarBottomSpacing, items: widget.items ?? [], diff --git a/guru_ui/packages/guru_widgets/lib/pages/awards/guru_awards_page.g.dart b/guru_ui/packages/guru_widgets/lib/pages/awards/guru_awards_page.g.dart index 95dbde5..addb5e1 100644 --- a/guru_ui/packages/guru_widgets/lib/pages/awards/guru_awards_page.g.dart +++ b/guru_ui/packages/guru_widgets/lib/pages/awards/guru_awards_page.g.dart @@ -32,6 +32,7 @@ class _GuruAwardsPageDesignSpec extends GuruAwardsPageDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruAwardsPageDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -39,8 +40,8 @@ class _GuruAwardsPageDesignSpec extends GuruAwardsPageDesignSpec { final _measuredMetrics = designMetrics.measure(measuredSize); return _GuruAwardsPageDesignSpec._( _measuredMetrics, offset, - 12.0, // tabbarTopSpacing - 6.0, // tabbarBottomSpacing + _measuredMetrics.measureHeight(24.0, consistent: true), // tabbarTopSpacing + _measuredMetrics.measureHeight(12.0, consistent: true) // tabbarBottomSpacing ); } diff --git a/guru_ui/packages/guru_widgets/lib/pages/navigation/guru_navigation_page.g.dart b/guru_ui/packages/guru_widgets/lib/pages/navigation/guru_navigation_page.g.dart index f405fa9..8692fad 100644 --- a/guru_ui/packages/guru_widgets/lib/pages/navigation/guru_navigation_page.g.dart +++ b/guru_ui/packages/guru_widgets/lib/pages/navigation/guru_navigation_page.g.dart @@ -24,6 +24,7 @@ class _GuruNavigationPageDesignSpec extends GuruNavigationPageDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruNavigationPageDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, diff --git a/guru_ui/packages/guru_widgets/lib/pages/settings/guru_settings_page.dart b/guru_ui/packages/guru_widgets/lib/pages/settings/guru_settings_page.dart index 02a7b2a..c1f5283 100644 --- a/guru_ui/packages/guru_widgets/lib/pages/settings/guru_settings_page.dart +++ b/guru_ui/packages/guru_widgets/lib/pages/settings/guru_settings_page.dart @@ -229,7 +229,7 @@ class GuruVersionWidgetState extends State { } else if (passwordController.text == 'trademark') { } else if (passwordController.text == 'legal') { - AppOwnershipUtils.showDialog(info: widget.info); + GuruPopup.instance.showBitmapDialog(info: widget.info); } else if (passwordController.text == "853211") { Settings.get().debugMode.set(false); } else { diff --git a/guru_ui/packages/guru_widgets/lib/pages/settings/guru_settings_page.g.dart b/guru_ui/packages/guru_widgets/lib/pages/settings/guru_settings_page.g.dart index 69bff94..157f75d 100644 --- a/guru_ui/packages/guru_widgets/lib/pages/settings/guru_settings_page.g.dart +++ b/guru_ui/packages/guru_widgets/lib/pages/settings/guru_settings_page.g.dart @@ -44,6 +44,7 @@ class _GuruSettingsDesignSpec extends GuruSettingsDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruSettingsDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, diff --git a/guru_ui/packages/guru_widgets/lib/sliderbar/guru_slider_bar.g.dart b/guru_ui/packages/guru_widgets/lib/sliderbar/guru_slider_bar.g.dart index 7a50863..445347a 100644 --- a/guru_ui/packages/guru_widgets/lib/sliderbar/guru_slider_bar.g.dart +++ b/guru_ui/packages/guru_widgets/lib/sliderbar/guru_slider_bar.g.dart @@ -56,6 +56,7 @@ class _GuruSliderBarDesignSpec extends GuruSliderBarDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruSliderBarDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, diff --git a/guru_ui/packages/guru_widgets/lib/tabbar/guru_tab_bar.g.dart b/guru_ui/packages/guru_widgets/lib/tabbar/guru_tab_bar.g.dart index 64c02de..8c3bb31 100644 --- a/guru_ui/packages/guru_widgets/lib/tabbar/guru_tab_bar.g.dart +++ b/guru_ui/packages/guru_widgets/lib/tabbar/guru_tab_bar.g.dart @@ -60,6 +60,7 @@ class _GuruTabBarDesignSpec extends GuruTabBarDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruTabBarDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, diff --git a/guru_ui/packages/guru_widgets/lib/theme/guru_theme.dart b/guru_ui/packages/guru_widgets/lib/theme/guru_theme.dart index 6147e24..c983872 100644 --- a/guru_ui/packages/guru_widgets/lib/theme/guru_theme.dart +++ b/guru_ui/packages/guru_widgets/lib/theme/guru_theme.dart @@ -49,10 +49,7 @@ class GuruButtonDecorator { final Color? contentTintColor; GuruButtonDecorator( - {required this.active, - this.inactive, - this.pressed, - this.contentTintColor = Colors.white}); + {required this.active, this.inactive, this.pressed, this.contentTintColor = Colors.white}); } class GuruButtonElevatedScheme { @@ -92,9 +89,7 @@ class GuruButtonStyle { @override bool operator ==(Object other) => identical(this, other) || - other is GuruButtonStyle && - runtimeType == other.runtimeType && - name == other.name; + other is GuruButtonStyle && runtimeType == other.runtimeType && name == other.name; @override int get hashCode => name.hashCode; @@ -137,10 +132,7 @@ class GuruOverlayColorScheme { final Color? dividerColor; const GuruOverlayColorScheme( - {this.backgroundColor, - this.expressiveColor, - this.textColor, - this.dividerColor}); + {this.backgroundColor, this.expressiveColor, this.textColor, this.dividerColor}); } class GuruTheme extends StatelessWidget { @@ -156,8 +148,14 @@ class GuruTheme extends StatelessWidget { final Widget child; final GuruThemeData guruTheme; + final SystemAdaptation? adaptation; - const GuruTheme({super.key, required this.guruTheme, required this.child}); + GuruTheme({super.key, required this.guruTheme, required this.child, this.adaptation}) { + /// FIXME: 如果这里嵌套使用将会出现问题 + if (adaptation != null) { + SystemAdaptation.defaultSystemAdaptation = adaptation; + } + } static GuruThemeData of(BuildContext context) { final _InheritedGuruTheme? inheritedTheme = @@ -285,14 +283,13 @@ class GuruNavigationBarTheme { final BottomNavigationBarType? type; const GuruNavigationBarTheme( - {this.backgroundColor, - this.selectedColor, - this.selectedBackgroundColor, - this.unSelctedColor, - this.navigationBarTopDividerColor, - this.navigationBarTopDividerHeight, - this.type - }); + {this.backgroundColor, + this.selectedColor, + this.selectedBackgroundColor, + this.unSelctedColor, + this.navigationBarTopDividerColor, + this.navigationBarTopDividerHeight, + this.type}); } class GuruSliderBarTheme { @@ -304,15 +301,14 @@ class GuruSliderBarTheme { final Duration? duration; final Curve? curve; - const GuruSliderBarTheme({ - this.anchorIcon, - this.anchorOffset, - this.horizontalOffset, - this.background, - this.items, - this.duration, - this.curve - }); + const GuruSliderBarTheme( + {this.anchorIcon, + this.anchorOffset, + this.horizontalOffset, + this.background, + this.items, + this.duration, + this.curve}); } @DesignSpec(width: 750, height: 1624) @@ -366,7 +362,7 @@ class GuruThemeData { final GuruSliderBarTheme sliderBarTheme; final GuruGemsOverlayTheme gemsOverlayTheme; final GuruCustomTheme customTheme; - + final FeedbackCapabilities feedbackCapabilities; GuruThemeDesignSpec get designSpec => GuruThemeDesignSpec.get(); @@ -398,8 +394,7 @@ class GuruThemeData { class _InheritedGuruTheme extends InheritedWidget { final GuruThemeData? themeData; - const _InheritedGuruTheme(this.themeData, {required Widget child}) - : super(child: child); + const _InheritedGuruTheme(this.themeData, {required Widget child}) : super(child: child); @override bool updateShouldNotify(covariant _InheritedGuruTheme oldGuruTheme) { diff --git a/guru_ui/packages/guru_widgets/lib/theme/guru_theme.g.dart b/guru_ui/packages/guru_widgets/lib/theme/guru_theme.g.dart index 1967753..683b674 100644 --- a/guru_ui/packages/guru_widgets/lib/theme/guru_theme.g.dart +++ b/guru_ui/packages/guru_widgets/lib/theme/guru_theme.g.dart @@ -56,6 +56,7 @@ class _GuruThemeDesignSpec extends GuruThemeDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruThemeDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, diff --git a/guru_ui/packages/guru_widgets/lib/tile/guru_list_tile.g.dart b/guru_ui/packages/guru_widgets/lib/tile/guru_list_tile.g.dart index 5d25a5e..fed2cf5 100644 --- a/guru_ui/packages/guru_widgets/lib/tile/guru_list_tile.g.dart +++ b/guru_ui/packages/guru_widgets/lib/tile/guru_list_tile.g.dart @@ -88,6 +88,7 @@ class _GuruListTileDesignSpec extends GuruListTileDesignSpec { @override Size get measuredSize => measuredMetrics.size; + static _GuruListTileDesignSpec _create( Size measuredSize, { Offset offset = Offset.zero, @@ -115,22 +116,27 @@ class _GuruListTileDesignSpec extends GuruListTileDesignSpec { end: 0.0, top: _measuredMetrics.measureVertical(56.0), bottom: _measuredMetrics.measureVertical(28.0)), // groupHeaderPadding - _measuredMetrics.measureAbsoluteFontSize(26.0), // groupTitleFontSize + _measuredMetrics.measureAbsoluteFontSize(26.0, + consistent: false), // groupTitleFontSize EdgeInsetsDirectional.only( start: _measuredMetrics.measureHorizontal(48.0), end: 0.0, top: _measuredMetrics.measureVertical(16.0), bottom: _measuredMetrics.measureVertical(48.0)), // descriptionPadding - _measuredMetrics.measureAbsoluteFontSize(24.0), // descriptionFontSize + _measuredMetrics.measureAbsoluteFontSize(24.0, + consistent: false), // descriptionFontSize _measuredMetrics.measureHeight(48.0, consistent: false), // leadingSize EdgeInsetsDirectional.only( start: _measuredMetrics.measureHorizontal(20.0), end: _measuredMetrics.measureHorizontal(20.0), top: 0.0, bottom: 0.0), // titlePadding - _measuredMetrics.measureAbsoluteFontSize(32.0), // titleFontSize - _measuredMetrics.measureAbsoluteFontSize(4.0), // summaryTopSpacing - _measuredMetrics.measureAbsoluteFontSize(20.0), // summaryFontSize + _measuredMetrics.measureAbsoluteFontSize(32.0, + consistent: false), // titleFontSize + _measuredMetrics.measureAbsoluteFontSize(4.0, + consistent: false), // summaryTopSpacing + _measuredMetrics.measureAbsoluteFontSize(20.0, + consistent: false), // summaryFontSize _measuredMetrics.measureHeight(48.0, consistent: false), // trailingSize Size(_measuredMetrics.measureHeight(72.0) * 1.44444444, _measuredMetrics.measureHeight(72.0)), // switchBarSize diff --git a/guru_ui/pubspec.lock b/guru_ui/pubspec.lock index 1f3edde..d68e797 100644 --- a/guru_ui/pubspec.lock +++ b/guru_ui/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "68796c31f510c8455a06fed75fc97d8e5ad04d324a830322ab3efc9feb6201c1" + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.flutter-io.cn" source: hosted - version: "5.2.0" + version: "6.2.0" android_id: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "5be16bf1707658e4c03078d4a9b90208ded217fb02c163e207d334082412f2fb" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.5" + version: "2.3.4" dartx: dependency: transitive description: