Compare commits

...

35 Commits

Author SHA1 Message Date
胡宇飞 5c1f73fc18 update: 完善 AdjustId 的缓存机制和二次上报机制
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-17 09:57:44 +08:00
胡宇飞 8a81ed78b4 update: 修正 AdjustID 获取的回调和时机
--story=1020639 --user=yufei.hu 【Unity】-【BI】Firebase 数据新增上报用户属性 adjust_id https://www.tapd.cn/58098289/s/1157505

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-16 21:04:46 +08:00
胡宇飞 76fc4f5c26 update: 上报Firebase用户属性 adjust_id
--story=1020972 --user=yufei.hu 【中台】【SDK】新增 Firebase 用户属性打点: adjust_id https://www.tapd.cn/33527076/s/1157507

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-16 19:19:04 +08:00
胡宇飞 72bf076537 fix: 广告 SDK 优化广告重试时间 从最高间隔 8 秒 -> 64 秒, 减少无网时高频请求
--story=1020971 --user=yufei.hu 【中台】【ADS】优化 IV 和 RV 广告加载失败后的重试等待时间 https://www.tapd.cn/33527076/s/1157502

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-16 18:46:16 +08:00
胡宇飞 19a46fff1e fix: 为 iap_purchase 添加 sandbox 参数
--story=1020884 --user=yufei.hu 【中台】【BUG】补齐 iap_purchase 打点的 sandbox 参数 https://www.tapd.cn/33527076/s/1156167

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-10 17:12:57 +08:00
胡宇飞 4989926a47 fix: 修复广告 TCH 打点携带 sandbox 属性的BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-03 17:09:53 +08:00
胡宇飞 fe691235d6 update:新增 INTER 和 RV 广告关闭的回调参数
--story=1020788 --user=yufei.hu 【中台】【SDK】新增INTER 和 RV 广告关闭的回调参数 https://www.tapd.cn/33527076/s/1154205

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-02 14:08:09 +08:00
胡宇飞 dc47cec8bd fix: 修复一个 Json 解析的错误问题
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-01 20:41:24 +08:00
胡宇飞 2075f676b9 fix: 删除 RemoteConfigBase 内OnChange 回调的JSON 属性
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-01 18:27:47 +08:00
胡宇飞 660303e45d update: 添加 Amazon 测试接口
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-01 18:21:41 +08:00
胡宇飞 2a895b370e Merge branch 'hotfix/1.0.13' into dev
Signed-off-by: huyufei <yufei.hu@castbox.fm>

# Conflicts:
#	Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs
2024-07-01 14:25:18 +08:00
胡宇飞 1256880b22 update: 更新 IOS 内的打包管线逻辑. UnityFramework 设置ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES ->NO
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-28 12:25:52 +08:00
胡宇飞 8cc083410d update: 新增打印 DeviceData 的日志
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-27 18:53:24 +08:00
胡宇飞 81f37625c1 fix: 修复 AdStatus 的显示 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-27 18:47:36 +08:00
胡宇飞 1a9481b094 update: Tch 001 和 Tch 02 打点, 添加 sandbox 参数
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 16:17:23 +08:00
胡宇飞 2174bcf1a3 fix: 修复 noti_perm 获取Allow 返回值
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 13:09:08 +08:00
胡宇飞 602662881c fix: 对齐 SDK 接口参数
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 13:09:04 +08:00
胡宇飞 e36f7483a3 fix: 修复打点时打日志报空的 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 13:09:00 +08:00
胡宇飞 7cbc5ac148 fix: 修复打开 Channel 跳转的 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 13:08:56 +08:00
胡宇飞 e8b3112cc5 【中台】【广告】修复 Amazon 升级 Adapter 至 9.9.5.0 无法加载广告的问题
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-24 10:19:26 +08:00
胡宇飞 e0e78da9a3 update: 【中台】【广告】添加 MAX 平台的黑屏修复配置
--story=1020664 --user=yufei.hu 【中台】【广告】添加 MAX 平台的黑屏修复配置 https://www.tapd.cn/33527076/s/1152218

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 17:05:03 +08:00
胡宇飞 9e7e94ef36 update: 添加 iOS 标志位缓存, 请求时直接返回状态
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 16:46:42 +08:00
胡宇飞 80e38bf85d update: 优化 iOS Noti 打点逻辑
--story=1020629 --user=yufei.hu 【中台】【SDK】加入消息弹框管理,中台 noti_perm 打点逻辑优化 https://www.tapd.cn/33527076/s/1152197

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 16:18:18 +08:00
胡宇飞 cfe81b5583 fix: 修复提示 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 14:13:58 +08:00
胡宇飞 34ae9e3f0b update: 更新Status默认值获取
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 14:01:33 +08:00
胡宇飞 eba48e4a75 update: 完善 Android 端的Noti 请求缓存逻辑
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 13:15:50 +08:00
胡宇飞 a53c153338 update: 更新 Android 请求状态
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 11:28:57 +08:00
胡宇飞 ffcb846a64 update: 添加构建管线, 修复授权回调BUG 逻辑
--story=1020629 --user=yufei.hu 【中台】【BUG】修复 noti_perm 属性设置不正确的问题 https://www.tapd.cn/33527076/s/1152099
2024-06-21 11:25:50 +08:00
胡宇飞 dbb56e5a32 update: 更新 Notification 的Android 实现
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 09:45:26 +08:00
胡宇飞 b6e038a027 update: 更新库引用
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-20 18:52:53 +08:00
胡宇飞 4e24169b25 update: 新增 Notification 服务和对一个的 noti 查询功能
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-20 18:20:38 +08:00
胡宇飞 9024b8171c Merge tag '1.0.13' into dev
Signed-off-by: huyufei <yufei.hu@castbox.fm>

# Conflicts:
#	Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs
2024-06-19 19:31:28 +08:00
胡宇飞 f96e506a19 update: 完善数据打印逻辑
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-17 19:10:53 +08:00
胡宇飞 c59f76aead update: 添加续订打点修改逻辑
--story=1020368 --user=yufei.hu 【中台】【IAP】续订打点修改逻辑 https://www.tapd.cn/33527076/s/1150863

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-17 15:01:09 +08:00
胡宇飞 ba90a32195 update: remove old firebase version hotfix
--story=1020565 --user=yufei.hu 【Unity】-【BI】IOS设备 升级Firebase/Analytics SDK 版本大于10.23.0 https://www.tapd.cn/33527076/s/1150717

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-15 08:27:58 +08:00
40 changed files with 1009 additions and 139 deletions

View File

@ -1,8 +1,8 @@
{
"name": "Guru.Editor",
"rootNamespace": "",
"references": [
"Guru.Runtime"
"Guru.Runtime",
"Guru.Notification"
],
"includePlatforms": [
"Editor"

View File

@ -41,8 +41,8 @@ namespace Guru.Editor
[PostProcessBuild(47)] // MAX POD Process Order
// Firebase 10.20.0 fixed to 10.22.0. BUT higher version do not open this ATTRIBUTE !!
// [PostProcessBuild(47)] // MAX POD Process Order
public static void PostBuildFixPodDeps(BuildTarget target, string projPath)
{
if (target != BuildTarget.iOS) return;

View File

@ -11,7 +11,7 @@ namespace Guru
/// </summary>
public class IOSPostBuildSwift
{
[PostProcessBuild(40)]
[PostProcessBuild(2000)]
public static void OnPostProcessBuild(BuildTarget target, string buildPath)
{
if (target != BuildTarget.iOS) return;
@ -43,7 +43,7 @@ namespace Guru
// 设置主项目的SWIFT构建支持
project.SetBuildProperty(mainTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
project.SetBuildProperty(frameworkTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "NO");
project.WriteToFile(projectPath);
}

View File

@ -6,6 +6,7 @@
"MaxSdk",
"MaxSdk.Scripts",
"Amazon",
"Amazon.Scripts",
"OpenWrapSDK",
"UniWebView-CSharp",
"UnityEngine.Purchasing",
@ -17,7 +18,9 @@
"Google.Play.Review",
"Google.Play.Common",
"Guru.LitJson",
"Unity.Advertisement.IosSupport"
"Unity.Advertisement.IosSupport",
"Unity.Notifications.Android",
"Unity.Notifications.iOS"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@ -1,11 +1,14 @@
namespace Guru
{
using UnityEngine;
using com.adjust.sdk;
using System;
using System.Collections;
public static class AdjustService
public class AdjustService
{
public const string Version = "1.6.1";
public const string AdjustVersion = "4.38.0"; // Adjust SDK Version
@ -15,6 +18,8 @@ namespace Guru
public const string K_IAP_PURCHASE = "iap_purchase"; // 固定点位事件
public const string K_SUB_PURCHASE = "sub_purchase"; // 固定点位事件
private static Action<string> _onSessionSuccessCallback;
private static string _adId = "";
public static string AdId
@ -45,7 +50,7 @@ namespace Guru
/// </summary>
/// <param name="appToken"></param>
/// <param name="fbAppId">MIR 追踪 AppID</param>
public static void StartService(string appToken, string fbAppId = "")
public static void StartService(string appToken, string fbAppId = "", Action<string> onSessionSuccess = null)
{
if (string.IsNullOrEmpty(appToken))
{
@ -53,6 +58,8 @@ namespace Guru
return;
}
_onSessionSuccessCallback = onSessionSuccess;
InstallEvent(IPMConfig.FIREBASE_ID, IPMConfig.IPM_DEVICE_ID); // 注入启动参数
AdjustEnvironment environment = GetAdjustEnvironment();
@ -61,6 +68,7 @@ namespace Guru
config.setDelayStart(DelayTime);
config.setPreinstallTrackingEnabled(true); // Adjust Preinstall
config.setSessionSuccessDelegate(OnSessionSuccessCallback); // SessionSuccess
#if UNITY_ANDROID
if (!string.IsNullOrEmpty(fbAppId)) config.setFbAppId(fbAppId); // 注入 MIR ID
@ -71,7 +79,7 @@ namespace Guru
config.setLogDelegate(log => LogI(LOG_TAG, log));
config.setEventSuccessDelegate(OnEventSuccessCallback);
config.setEventFailureDelegate(OnEventFailureCallback);
config.setSessionSuccessDelegate(OnSessionSuccessCallback);
config.setSessionFailureDelegate(OnSessionFailureCallback);
config.setAttributionChangedDelegate(OnAttributionChangedCallback);
#endif
@ -272,27 +280,10 @@ namespace Guru
private static void OnSessionSuccessCallback(AdjustSessionSuccess sessionSuccessData)
{
LogI(LOG_TAG,"Session tracked successfully!");
LogI(LOG_TAG,$"{LOG_TAG} --- Session tracked successfully!");
if (sessionSuccessData.Message != null)
{
LogI(LOG_TAG,"Message: " + sessionSuccessData.Message);
}
if (sessionSuccessData.Timestamp != null)
{
LogI(LOG_TAG,"Timestamp: " + sessionSuccessData.Timestamp);
}
if (sessionSuccessData.Adid != null)
{
LogI(LOG_TAG, "Adid: " + sessionSuccessData.Adid);
}
if (sessionSuccessData.JsonResponse != null)
{
LogI(LOG_TAG, "JsonResponse: " + sessionSuccessData.GetJsonResponse());
}
var adid = sessionSuccessData.Adid;
_onSessionSuccessCallback?.Invoke(adid);
}
private static void OnSessionFailureCallback(AdjustSessionFailure sessionFailureData)

View File

@ -79,7 +79,7 @@ namespace Guru
/// <summary>
/// 初始化平台
/// </summary>
public void Initialize()
public void Initialize(bool isDebug = false)
{
#if UNITY_EDITOR
Debug.Log($"<color=orange>=== Amazon will not init on Editor ===</color>");
@ -93,11 +93,9 @@ namespace Guru
// 初始化Amazon
Amazon.Initialize (AmazonAppID);
Amazon.SetAdNetworkInfo(new AdNetworkInfo(DTBAdNetwork.MAX));
#if UNITY_EDITOR || DEBUG
Amazon.EnableTesting (true); // Make sure to take this off when going live.
#else
Amazon.EnableLogging (false);
#endif
Debug.Log($"[Ads] --- Amazon init start isDebug:{isDebug}, AmazonID:{AmazonAppID}");
Amazon.EnableTesting (isDebug); // Make sure to take this off when going live.
Amazon.EnableLogging (isDebug);
#if UNITY_IOS
Amazon.SetAPSPublisherExtendedIdFeatureEnabled(true);

View File

@ -50,7 +50,7 @@ namespace Guru
* before it can request an ad using OpenWrap SDK.
* The storeURL is the URL where users can download your app from the App Store/Google Play Store.
*/
public void Initialize()
public void Initialize(bool isDebug = false)
{
#if UNITY_EDITOR
Debug.Log($"<color=orange>=== PubMatic will not init on Editor ===</color>");

View File

@ -23,7 +23,7 @@ namespace Guru
protected override void InitService()
{
base.InitService();
InitChannels(); // 启动各广告渠道代理
InitChannels(_isDebug); // 启动各广告渠道代理
}
#endregion
@ -37,14 +37,14 @@ namespace Guru
/// <summary>
/// 各渠道初始化
/// </summary>
private void InitChannels()
private void InitChannels(bool isDebug)
{
_adChannels = new HashSet<IAdChannel>();
IAdChannel channel = null;
_asyncLoader = null;
_chanelMax = new AdChanelMax(); // 默认持有MAXChannel
_chanelMax.Initialize();
_chanelMax.Initialize(isDebug);
if(_initSpec != null) _chanelMax.SetBannerBackColor(_initSpec.bannerColorHex);
//------------ 以下为扩展的广告渠道 ------------------
@ -52,7 +52,7 @@ namespace Guru
// 开启渠道需要添加对应的宏
channel = new AdChanelAmazon();
channel.Initialize();
channel.Initialize(isDebug);
_adChannels.Add(channel); // Amazon
_asyncLoader = channel as IAsyncRequestChannel;
if (_asyncLoader != null)

View File

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using Guru;
using UnityEngine;
namespace Guru
{
using System;
using UnityEngine;
using System.Collections.Generic;
public abstract class ADServiceBase<T> : IADService where T : new()
{
// 单利定义
@ -23,6 +22,8 @@ namespace Guru
public bool IsInitialized => MaxSdk.IsInitialized() || _isServiceStarted;
protected bool IsNetworkEnabled => Application.internetReachability != NetworkReachability.NotReachable;
private const int MAX_ADS_RELOAD_INTERVAL = 6; // 广告加载最高时间为 2 的 6 次方 = 64秒
private bool _isServiceStarted;
protected Action _onSdkInitReady;
@ -33,10 +34,12 @@ namespace Guru
public static Action<string> OnInterstitialStartLoad;
public static Action OnInterstitialLoaded;
public static Action OnInterstitialFailed;
public static Action OnInterstitialClosed;
public static Action<string> OnRewardedStartLoad;
public static Action OnRewardLoaded;
public static Action OnRewardFailed;
public static Action OnRewardClosed;
protected AdsModel _model;
protected AdsInitSpec _initSpec = null;
@ -50,6 +53,7 @@ namespace Guru
}
}
internal bool _isDebug = false;
/// <summary>
/// 启动广告服务
@ -62,11 +66,23 @@ namespace Guru
if (IsInitialized) return; // 已经初始化后, 无需再次初始化
_initSpec = initSpec;
if (_initSpec == null) _initSpec = AdsInitSpec.BuildDefault();
_isDebug = _initSpec.isDebug;
_isServiceStarted = true;
_onSdkInitReady = callback;
if(_model == null) _model = AdsModel.Create();
this.Log("AD SDK Start Init");
InitMaxCallbacks(); // 初始化 MAX 广告
InitService(); // 内部继承接口
}
/// <summary>
/// 初始化 MAX 广告组件
/// </summary>
private void InitMaxCallbacks()
{
//-------------- 初始化回调 ------------------
MaxSdkCallbacks.OnSdkInitializedEvent += OnMaxSdkInitializedCallBack;
MaxSdkCallbacks.Interstitial.OnAdRevenuePaidEvent += OnAdRevenuePaidEvent;
@ -94,10 +110,7 @@ namespace Guru
MaxSdkCallbacks.Rewarded.OnAdReceivedRewardEvent += OnRewardedAdReceivedRewardEvent;
//-------------- SDK 初始化 -------------------
if (_initSpec == null) _initSpec = AdsInitSpec.BuildDefault();
MaxSdk.SetVerboseLogging(_initSpec.isDebug);
InitService(); // 内部继承接口
MaxSdk.SetExtraParameter("enable_black_screen_fixes", "true"); // 修复黑屏
}
protected virtual void InitService()
@ -138,6 +151,13 @@ namespace Guru
set => Model.BuyNoAds = value;
}
private float GetRetryDelaySeconds(int retryCount)
{
// 最低 2^retryCount 秒
// 最高 2^6 = 64 秒
return (float)Math.Pow(2, Math.Min(MAX_ADS_RELOAD_INTERVAL, retryCount));
}
#region Lifecycele
public void OnAppPaused(bool paused)
@ -417,7 +437,7 @@ namespace Guru
private string _iadsCategory = "main";
private int _interstitialRetryAttempt;
protected float _iadsLoadStartTime;
private Action _interstitialDismissAction;
private Action _interCloseAction;
protected bool _isIadsLoading = false;
public bool IsIadsLoading => _isIadsLoading;
@ -473,7 +493,7 @@ namespace Guru
}
_iadsCategory = category;
_interstitialDismissAction = dismissAction;
_interCloseAction = dismissAction;
MaxSdk.ShowInterstitial(GetInterstitialID());
// RequestInterstitialAD(); // 直接加载下一个广告
@ -500,8 +520,8 @@ namespace Guru
this.LogError(
$"OnInterstitialFailedEvent AdLoadFailureInfo:{errorInfo.AdLoadFailureInfo}, Message: {errorInfo.Message}");
_interstitialRetryAttempt++;
double retryDelay = Math.Pow(2, Math.Min(3, _interstitialRetryAttempt));
DelayCall((float)retryDelay, RequestInterstitialAD);
float retryDelay = GetRetryDelaySeconds(_interstitialRetryAttempt);
DelayCall(retryDelay, RequestInterstitialAD);
// Analytics.ADIadsFailed(adUnitId, (int)errorInfo.Code, GetAdsLoadDuration(ref _iadsLoadStartTime), _iadsCategory);
Analytics.ADIadsFailed(AdParams.Build(adUnitId,
duration: GetAdsLoadDuration(ref _iadsLoadStartTime), category: _iadsCategory,
@ -540,7 +560,8 @@ namespace Guru
protected virtual void OnInterstitialDismissedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
// Interstitial ad is hidden. Pre-load the next ad
_interstitialDismissAction?.Invoke();
_interCloseAction?.Invoke();
OnInterstitialClosed?.Invoke();
// Analytics.ADIadsClose(adUnitId, _iadsCategory);
Analytics.ADIadsClose(AdParams.Build(adUnitId, category: _iadsCategory));
//延时加载下一个广告
@ -554,9 +575,9 @@ namespace Guru
private string _rewardCategory = "main";
private int _rewardRetryAttempt;
protected float _radsLoadStartTime;
private Action _rewardAction;
private Action<string> _failAction;
private Action _dismissAction;
private Action _rvRewardAction;
private Action<string> _rvFailAction;
private Action _rvDismissAction;
protected bool _isRadsLoading = false;
public bool IsRadsLoading => _isRadsLoading;
@ -620,9 +641,9 @@ namespace Guru
}
_rewardCategory = category;
_rewardAction = rewardAction;
_failAction = failAction;
_dismissAction = dismissAction;
_rvRewardAction = rewardAction;
_rvFailAction = failAction;
_rvDismissAction = dismissAction;
MaxSdk.ShowRewardedAd(GetRewardedID());
// RequestRewardedAD();
@ -657,8 +678,8 @@ namespace Guru
errorCode: (int)errorInfo.Code,
waterfallName: errorInfo?.WaterfallInfo?.Name ?? ""));
_rewardRetryAttempt++;
double retryDelay = Math.Pow(2, Math.Min(3, _rewardRetryAttempt));
DelayCall((float)retryDelay, RequestRewardedAD);
float retryDelay = GetRetryDelaySeconds(_rewardRetryAttempt);
DelayCall(retryDelay, RequestRewardedAD);
OnRewardFailed?.Invoke();
}
@ -674,7 +695,7 @@ namespace Guru
duration: GetAdsLoadDuration(ref _radsLoadStartTime), category: _rewardCategory,
errorCode: (int)errorInfo.Code,
waterfallName: errorInfo?.WaterfallInfo?.Name ?? ""));
_failAction?.Invoke("OnRewardedAdFailedToDisplayEvent");
_rvFailAction?.Invoke("OnRewardedAdFailedToDisplayEvent");
DelayCall(2.0f, RequestRewardedAD);
OnRewardFailed?.Invoke();
@ -697,11 +718,17 @@ namespace Guru
protected virtual void OnRewardedAdDismissedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
this.Log("OnRewardedAdDismissedEvent");
_rvDismissAction?.Invoke();
OnRewardClosed?.Invoke();
// Analytics.ADRadsClose(adUnitId, _rewardCategory);
Analytics.ADRadsClose(AdParams.Build(adUnitId, category: _rewardCategory));
_dismissAction?.Invoke();
//延时加载下一个广告
DelayCall(2.0f, RequestRewardedAD);
}
protected virtual void OnRewardedAdReceivedRewardEvent(string adUnitId, MaxSdk.Reward reward,
@ -711,7 +738,7 @@ namespace Guru
// Analytics.ADRadsRewarded(adUnitId, _rewardCategory);
Analytics.ADRadsRewarded(AdParams.Build(adUnitId, category: _rewardCategory));
// Rewarded ad was displayed and user should receive the reward
_rewardAction?.Invoke();
_rvRewardAction?.Invoke();
}
#endregion

View File

@ -32,10 +32,11 @@ namespace Guru
/// <summary>
/// MAX 渠道初始化, 启动服务
/// </summary>
public void Initialize()
public void Initialize(bool isDebug = false)
{
MaxSdk.SetSdkKey(GuruSettings.Instance.ADSetting.SDK_KEY);
MaxSdk.SetUserId(IPMConfig.IPM_UID); // 上报用户ID
MaxSdk.SetVerboseLogging(isDebug); // 设置调试数据
MaxSdk.InitializeSdk();
}

View File

@ -8,7 +8,7 @@ namespace Guru
{
// Action<string> OnRequestOver { get; set; }
void Initialize();
void Initialize(bool isDebug = false);
string Name { get;}

View File

@ -295,9 +295,11 @@ namespace Guru
/// <param name="orderId"></param>
/// <param name="orderType"></param>
/// <param name="timestamp"></param>
public static void Tch001IAPRev(double value, string productId, string orderId, string orderType, string timestamp)
/// <param name="isTest"></param>
public static void Tch001IAPRev(double value, string productId, string orderId, string orderType, string timestamp, bool isTest = false)
{
TchRevEvent(EventTchAdRev001Impression, IAPPlatform, value, orderType, productId, orderId, timestamp);
string sandbox = isTest ? "true" : "false";
TchRevEvent(EventTchAdRev001Impression, IAPPlatform, value, orderType, productId, orderId, timestamp, sandbox);
}
/// <summary>
@ -309,11 +311,13 @@ namespace Guru
/// <param name="orderId"></param>
/// <param name="orderType"></param>
/// <param name="timestamp"></param>
/// <param name="isTest"></param>
// public static void Tch02IAPRev(double value, string productId, string orderId, string orderType, string timestamp)
public static void Tch02IAPRev(double value, string productId, string orderId, string orderType, string timestamp)
public static void Tch02IAPRev(double value, string productId, string orderId, string orderType, string timestamp, bool isTest = false)
{
if (!EnableTch02Event) return;
TchRevEvent(EventTchAdRev02Impression, IAPPlatform, value, orderType, productId, orderId, timestamp);
string sandbox = isTest ? "true" : "false";
TchRevEvent(EventTchAdRev02Impression, IAPPlatform, value, orderType, productId, orderId, timestamp, sandbox);
}
/// <summary>
@ -362,8 +366,9 @@ namespace Guru
/// <param name="productId"></param>
/// <param name="orderId"></param>
/// <param name="timestamp"></param>
/// <param name="sandbox"></param>
private static void TchRevEvent(string evtName, string platform, double value,
string orderType = "", string productId = "", string orderId = "", string timestamp = "")
string orderType = "", string productId = "", string orderId = "", string timestamp = "", string sandbox = "")
{
var data = new Dictionary<string, dynamic>()
{
@ -373,10 +378,13 @@ namespace Guru
};
//--------- Extra data for IAP receipt ---------------
if(!string.IsNullOrEmpty(orderType)) data["order_type"] = orderType;
if(!string.IsNullOrEmpty(productId)) data["product_id"] = productId;
if(!string.IsNullOrEmpty(orderId)) data["order_id"] = orderId;
if(!string.IsNullOrEmpty(timestamp)) data["trans_ts"] = timestamp;
if(!string.IsNullOrEmpty(sandbox)) data["sandbox"] = sandbox;
//--------- Extra data for IAP receipt ---------------
LogEvent(evtName, data);
@ -603,15 +611,19 @@ namespace Guru
/// </summary>
/// <param name="productId"></param>
/// <param name="usdPrice"></param>
/// <param name="userCurrency"></param>
/// <param name="payPrice"></param>
/// <param name="orderId"></param>
/// <param name="orderType"></param>
/// <param name="orderDate"></param>
/// <param name="scene"></param>
/// <param name="isFree"></param>
public static void ReportIAPSuccessEvent(double usdPrice, string productId, BaseOrderData orderData)
/// <param name="orderData"></param>
/// <param name="isTest"></param>
public static void ReportIAPSuccessEvent(BaseOrderData orderData, double usdPrice, bool isTest = false)
{
if (orderData == null) return;
if (!isTest && usdPrice == 0)
{
Debug.Log($"[SDK] --- Pruchase value is 0, skip report orders");
return;
}
string productId = orderData.productId;
string userCurrency = orderData.userCurrency;
double payPrice = orderData.payPrice;
string orderType = orderData.OrderType();
@ -636,21 +648,23 @@ namespace Guru
}
// TCH 001
Tch001IAPRev(usdPrice, productId, orderId, orderType, orderDate);
Tch001IAPRev(usdPrice, productId, orderId, orderType, orderDate, isTest);
// TCH 020
Tch02IAPRev(usdPrice, productId, orderId, orderType, orderDate);
Tch02IAPRev(usdPrice, productId, orderId, orderType, orderDate, isTest);
// Facebook Track IAP Purchase
FBPurchase(usdPrice, USD, "iap", IAPPlatform);
if (orderData.orderType == 1)
{
// sub_pruchase : Firebase + Guru + Adjust
SubPurchase(usdPrice, productId, orderId, orderDate, productToken, receipt);
SubPurchase(usdPrice, productId, orderId, orderDate, productToken, receipt, isTest);
}
else
{
// iap_purchase : Firebase + Guru + Adjust
IAPPurchase(usdPrice, productId, orderId, orderDate, productToken, receipt);
IAPPurchase(usdPrice, productId, orderId, orderDate, productToken, receipt, isTest);
}
// IAP Ret true : Firebase + Guru + Adjust
@ -669,21 +683,33 @@ namespace Guru
/// <param name="productId"></param>
/// <param name="orderId"></param>
/// <param name="orderDate"></param>
/// <param name="purchaseToken"></param>
/// <param name="receipt"></param>
/// <param name="isSandbox"></param>
public static void IAPPurchase(double value, string productId, string orderId, string orderDate,
string purchaseToken = "", string receipt = "")
string purchaseToken = "", string receipt = "", bool isSandbox = false)
{
IAPPurchaseReport(EventIAPPurchase, value, productId, orderId, "IAP", orderDate, purchaseToken, receipt);
IAPPurchaseReport(EventIAPPurchase, value, productId, orderId, "IAP", orderDate, purchaseToken, receipt, isSandbox);
}
/// <summary>
/// SUB 订阅上报
/// </summary>
/// <param name="value"></param>
/// <param name="productId"></param>
/// <param name="orderId"></param>
/// <param name="orderDate"></param>
/// <param name="purchaseToken"></param>
/// <param name="receipt"></param>
/// <param name="isSandbox"></param>
public static void SubPurchase(double value, string productId, string orderId, string orderDate,
string purchaseToken = "", string receipt = "")
string purchaseToken = "", string receipt = "", bool isSandbox = false)
{
IAPPurchaseReport(EventSubPurchase, value, productId, orderId, "SUB", orderDate, purchaseToken, receipt);
IAPPurchaseReport(EventSubPurchase, value, productId, orderId, "SUB", orderDate, purchaseToken, receipt, isSandbox);
}
private static void IAPPurchaseReport(string eventName, double value, string productId, string orderId, string orderType, string orderDate,
string purchaseToken = "", string receipt = "")
private static void IAPPurchaseReport(string eventName, double value, string productId,
string orderId, string orderType, string orderDate, string purchaseToken = "", string receipt = "", bool isSandbox = false)
{
var dict = new Dictionary<string, dynamic>()
@ -694,20 +720,17 @@ namespace Guru
[ParameterProductId] = productId,
["order_id"] = orderId,
["order_type"] = orderType,
["trans_ts"] = orderDate
["trans_ts"] = orderDate,
["sandbox"] = isSandbox? "true": "false"
};
// 上报Firebase + 自打点
LogEvent(eventName, dict, new EventSetting() { EnableFirebaseAnalytics = true, });
// 上报Firebase + 自打点
LogEvent(eventName, dict, new EventSetting() { EnableFirebaseAnalytics = true });
// 上报 Adjust 支付事件
// if (value > 0)
// {
// 根据事件名称来获取对应的事件Tokeniap_purchase/sub_purchase
LogAdjustRevenueEvent(eventName, value, productId, orderId, purchaseToken, receipt, dict);
// }
}
#endregion

View File

@ -1,5 +1,4 @@
using UnityEngine;
namespace Guru
{
@ -8,6 +7,7 @@ namespace Guru
using System.Collections.Generic;
using com.adjust.sdk;
using Facebook.Unity;
using UnityEngine;
using Firebase.Analytics;
using Firebase.Crashlytics;
@ -180,9 +180,9 @@ namespace Guru
/// <param name="eventName"></param>
/// <param name="extras"></param>
/// <param name="eventSetting"></param>
/// <param name="priority"></param>
internal static void LogEvent(string eventName, Dictionary<string, dynamic> extras, EventSetting eventSetting = null, int priority = -1)
{
Log.I(TAG, $"eventName:{eventName}, params:{string.Join(",", extras)}");
CustomLogEvent(eventName, extras, priority); // 自定义打点上报
CheckLogCache(eventName, extras, eventSetting); // log缓存和消费
@ -194,7 +194,10 @@ namespace Guru
return;
}
eventSetting ??= _defaultEventSetting;
string paramStr = string.Join(",", extras);
Log.I(TAG, $"eventName:{eventName}, params:{paramStr}");
if (eventSetting == null) eventSetting = _defaultEventSetting;
if (eventSetting.EnableFirebaseAnalytics)
{
List<Parameter> parameters = new List<Parameter>();
@ -216,7 +219,6 @@ namespace Guru
parameters.Add(new Parameter(kv.Key, decimal.ToDouble(decimalValue)));
else
parameters.Add(new Parameter(kv.Key, kv.Value.ToString()));
}
FirebaseAnalytics.LogEvent(eventName, parameters.ToArray());

View File

@ -52,7 +52,10 @@ namespace Guru
try
{
// return JsonMapper.ToObject<T>(jsonStr);
return JsonConvert.DeserializeObject<T>(jsonStr);
return JsonConvert.DeserializeObject<T>(jsonStr, new JsonSerializerSettings()
{
ObjectCreationHandling = ObjectCreationHandling.Replace,
});
}
catch (Exception e)
{

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using Firebase;
using Firebase.Analytics;
using Firebase.Extensions;
using UnityEngine;
namespace Guru
{
@ -117,10 +117,34 @@ namespace Guru
// 启动 AdjustService
string appToken = GuruSettings.Instance.AdjustSetting?.GetAppToken() ?? "";
string fbAppId = GuruSettings.Instance.IPMSetting.FacebookAppId;
AdjustService.StartService(appToken, fbAppId);
if (!string.IsNullOrEmpty(IPMConfig.ADJUST_ID))
{
ReportAdjustId(IPMConfig.ADJUST_ID); // 二次启动后,若有值则立即上报属性
}
AdjustService.StartService(appToken, fbAppId, adjustId =>
{
// 获取 ADID
if (string.IsNullOrEmpty(adjustId))
{
adjustId = "not_set";
}
else
{
IPMConfig.ADJUST_ID = adjustId;
}
ReportAdjustId(adjustId);
});
});
}
private static void ReportAdjustId(string adjustId)
{
FirebaseAnalytics.SetUserProperty("adjust_id", adjustId); // 仅上报 Firebase 用户属性
Debug.Log($"[SDK] --- Firebase + Adjust ID: {adjustId}");
}

View File

@ -50,6 +50,7 @@ namespace Guru
{
GenDeviceId();
}
return SavedDeviceId; // 优先使用缓存的 DeviceID
}
}

View File

@ -34,19 +34,14 @@ namespace Guru
{
try
{
Debug.Log($"[IAP] --- Apple Order Response: {response}");
ResponseData<OrderResponse> responseData = JsonUtility.FromJson<ResponseData<OrderResponse>>(response);
if (responseData != null && responseData.data != null)
{
double usdPrice = responseData.data.usdPrice;
string productId = orderData.productId;
bool isTest = responseData.data.test;
// Analytics.Tch001IAPRev(usdPrice, productId, orderData.orderId, orderData.OrderType(), orderData.payedDate);
// Analytics.Tch02IAPRev(usdPrice);
//
// AdjustService.TrackSubPurchase(usdPrice, productId);
// Analytics.SubPurchase(usdPrice, productId);
Analytics.ReportIAPSuccessEvent(usdPrice, productId, orderData);
Analytics.ReportIAPSuccessEvent(orderData, usdPrice, isTest);
}
}
catch (Exception ex)

View File

@ -31,21 +31,14 @@ namespace Guru
{
try
{
Debug.Log($"[IAP] --- Google Order Response: {response}");
ResponseData<OrderResponse> responseData = JsonUtility.FromJson<ResponseData<OrderResponse>>(response);
if (responseData != null && responseData.data != null)
{
double usdPrice = responseData.data.usdPrice;
string productId = orderData.RealProductId;
bool isTest = responseData.data.test;
// Analytics.Tch001IAPRev(usdPrice, productId, orderId, orderType, orderDate); // TCH 001
// // Analytics.Tch02IAPRev(usdPrice, productId, orderId, orderTypeString, timestamp);
// Analytics.Tch02IAPRev(usdPrice); // TCH 020
//
// // Adjust Track IAP Purchase
// AdjustService.TrackIAPPurchase(usdPrice, productId); // 上报 IAP 支付事件
// Analytics.IAPPurchase(usdPrice, productId);
Analytics.ReportIAPSuccessEvent(usdPrice, productId, orderData);
Analytics.ReportIAPSuccessEvent(orderData, usdPrice, isTest);
}
}
catch (Exception ex)

View File

@ -6,10 +6,12 @@ namespace Guru
public class OrderResponse
{
public double usdPrice;
public bool test;
public string state;
public override string ToString()
{
return $"{nameof(usdPrice)}: {usdPrice}";
return $"{nameof(usdPrice)}: {usdPrice} {nameof(test)}: {test} {nameof(state)}: {state}";
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1fef034d48ac449fb99531b40139954e
timeCreated: 1718844748

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8c0dd2d4b63445c2828e05c10274d672
timeCreated: 1718845536

View File

@ -0,0 +1,178 @@
#if UNITY_ANDROID
namespace Guru.Notification
{
using System;
using System.IO;
using UnityEditor.Android;
using System.Xml;
public class PostCustomActivity: IPostGenerateGradleAndroidProject
{
private const int POST_ORDER = 10;
private const string K_PREMISSION_POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS";
private const string K_CUSTOM_NOTIFICATION_ACTIVITY = "custom_notification_android_activity";
private const string V_DEFAULT_GURU_ACTIVITY = "com.google.firebase.messaging.MessageForwardingService";
const string K_ANDROID_NAMESPACE_URI = "http://schemas.android.com/apk/res/android";
public int callbackOrder => POST_ORDER;
public void OnPostGenerateGradleAndroidProject(string path)
{
SetupAndroidManifest(path);
}
/// <summary>
/// 设置 Android Manifest
/// </summary>
/// <param name="projectPath"></param>
/// <exception cref="FileNotFoundException"></exception>
private void SetupAndroidManifest(string projectPath)
{
var manifestPath = $"{projectPath}/src/main/AndroidManifest.xml";
if (!File.Exists(manifestPath))
throw new FileNotFoundException($"'{manifestPath}' doesn't exist.");
XmlDocument manifestDoc = new XmlDocument();
manifestDoc.Load(manifestPath);
InjectAndroidManifest(manifestPath, manifestDoc);
manifestDoc.Save(manifestPath);
}
internal static void InjectAndroidManifest(string manifestPath, XmlDocument manifestDoc)
{
string mainActivity = GetLauncherActivity(manifestDoc);
AppendAndroidMetadataField(manifestPath, manifestDoc, K_CUSTOM_NOTIFICATION_ACTIVITY, mainActivity);
AppendAndroidPermissionField(manifestPath, manifestDoc, K_PREMISSION_POST_NOTIFICATIONS);
UnityEngine.Debug.Log($"<color=#88ff00>Add custom notification activity: {mainActivity} success!!</color>");
}
internal static void AppendAndroidPermissionField(string manifestPath, XmlDocument xmlDoc, string name, string maxSdk = null)
{
AppendAndroidPermissionField(manifestPath, xmlDoc, "uses-permission", name, maxSdk);
}
internal static void AppendAndroidPermissionField(string manifestPath, XmlDocument xmlDoc, string tagName, string name, string maxSdk)
{
var manifestNode = xmlDoc.SelectSingleNode("manifest");
if (manifestNode == null)
throw new ArgumentException(string.Format("Missing 'manifest' node in '{0}'.", manifestPath));
XmlElement metaDataNode = null;
foreach (XmlNode node in manifestNode.ChildNodes)
{
if (!(node is XmlElement) || node.Name != tagName)
continue;
var element = (XmlElement)node;
var elementName = element.GetAttribute("name", K_ANDROID_NAMESPACE_URI);
if (elementName == name)
{
if (maxSdk == null)
return;
var maxSdkAttr = element.GetAttribute("maxSdkVersion", K_ANDROID_NAMESPACE_URI);
if (!string.IsNullOrEmpty(maxSdkAttr))
return;
metaDataNode = element;
}
}
if (metaDataNode == null)
{
metaDataNode = xmlDoc.CreateElement(tagName);
metaDataNode.SetAttribute("name", K_ANDROID_NAMESPACE_URI, name);
}
if (maxSdk != null)
metaDataNode.SetAttribute("maxSdkVersion", K_ANDROID_NAMESPACE_URI, maxSdk);
manifestNode.AppendChild(metaDataNode);
}
internal static void AppendAndroidMetadataField(string manifestPath, XmlDocument xmlDoc, string name, string value)
{
var applicationNode = xmlDoc.SelectSingleNode("manifest/application");
if (applicationNode == null)
throw new ArgumentException(string.Format("Missing 'application' node in '{0}'.", manifestPath));
var nodes = xmlDoc.SelectNodes("manifest/application/meta-data");
if (nodes != null)
{
// Check if there is a 'meta-data' with the same name.
foreach (XmlNode node in nodes)
{
var element = node as XmlElement;
if (element == null)
continue;
var elementName = element.GetAttribute("name", K_ANDROID_NAMESPACE_URI);
if (elementName == name)
{
element.SetAttribute("value", K_ANDROID_NAMESPACE_URI, value);
return;
}
}
}
XmlElement metaDataNode = xmlDoc.CreateElement("meta-data");
metaDataNode.SetAttribute("name", K_ANDROID_NAMESPACE_URI, name);
metaDataNode.SetAttribute("value", K_ANDROID_NAMESPACE_URI, value);
applicationNode.AppendChild(metaDataNode);
}
internal static string GetLauncherActivity(XmlDocument xmlDoc)
{
var applicationNode = xmlDoc.SelectSingleNode("manifest/application");
if (applicationNode == null)
throw new ArgumentException($"Missing 'application' node in doc.");
var nodes = xmlDoc.SelectNodes("manifest/application/activity");
if (nodes != null)
{
foreach (XmlNode node in nodes)
{
var activityNode = node as XmlElement;
if (activityNode == null)
continue;
if (activityNode.HasChildNodes)
{
var intentFilterNode = activityNode.SelectSingleNode("intent-filter");
if(intentFilterNode == null || !intentFilterNode.HasChildNodes)
continue;
foreach (XmlElement childNode in intentFilterNode)
{
if(childNode == null) continue;
// 判断 action/category 二者取其一
if (childNode.Name == "action" && childNode.InnerXml.Contains("android.intent.action.MAIN"))
{
var activityName = activityNode.GetAttribute("name", K_ANDROID_NAMESPACE_URI);
return activityName;
}
if (childNode.Name == "category" && childNode.InnerXml.Contains("android.intent.category.LAUNCHER"))
{
var activityName = activityNode.GetAttribute("name", K_ANDROID_NAMESPACE_URI);
return activityName;
}
}
}
}
}
return V_DEFAULT_GURU_ACTIVITY;
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2e6de94961cc47abbabbc7e872085a98
timeCreated: 1718934648

View File

@ -0,0 +1,6 @@
{
"name": "GuruNotification.Editor",
"includePlatforms": [
"Editor"
]
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e75025463d0c436482bf4c3dab674315
timeCreated: 1718845553

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d6dd827f370441718ca2e49f3f603e4e
timeCreated: 1718845525

View File

@ -0,0 +1,15 @@
namespace Guru.Notification
{
using System;
public interface INotificationAgent
{
void Init();
string GetStatus();
bool IsAllowed();
void RequestPermission(Action<string> callback = null);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 43e051a65e9b469b8b9e8a9f6b4be944
timeCreated: 1718844775

View File

@ -0,0 +1,252 @@
namespace Guru.Notification
{
using System;
using UnityEngine;
#if UNITY_ANDROID
using UnityEngine.Android;
using Unity.Notifications.Android;
#endif
public class NotificationAgentAndroid : INotificationAgent
{
public const string FCM_DEFAULT_CHANNEL_ID = "fcm_default_channel";
private const string STATUS_GRANTED = "granted";
private const string STATUS_DENIDED = "denied";
// private const string STATUS_NOT_DETERMINED = "not_determined";
private const int REQUEST_PERMISSION_SDK_VERSION = 33;
private const string PERMISSION_POST_NOTIFICATION = "android.permission.POST_NOTIFICATIONS";
private bool _initOnce = false;
private string _notiStatus;
private string SavedNotiPermStatus
{
get => PlayerPrefs.GetString(nameof(SavedNotiPermStatus), "");
set => PlayerPrefs.SetString(nameof(SavedNotiPermStatus), value);
}
/// <summary>
/// 初始化
/// </summary>
public void Init()
{
if (!_initOnce) return;
_initOnce = true;
_notiStatus = STATUS_DENIDED;
if (!string.IsNullOrEmpty(SavedNotiPermStatus))
{
_notiStatus = SavedNotiPermStatus;
}
#if UNITY_ANDROID
InitPlugins();
#endif
}
/// <summary>
/// 获取状态
/// </summary>
/// <returns></returns>
public string GetStatus()
{
if (!_initOnce) Init();
#if UNITY_ANDROID
UpdateNotiStatus();
#endif
return _notiStatus;
}
/// <summary>
/// 设置授权状态
/// </summary>
/// <param name="status">授权状态</param>
private void SetGrantStatus(string status)
{
_notiStatus = status;
SavedNotiPermStatus = status;
}
public bool IsAllowed()
{
return _notiStatus == STATUS_GRANTED;
}
public void RequestPermission(Action<string> callback = null)
{
#if UNITY_ANDROID
RequestAndroidPermission(callback);
#endif
}
// -------------------- Android 获取状态逻辑 --------------------
#if UNITY_ANDROID
private PermissionStatus _permissionStatus;
private void TryExecute(Action handler)
{
try
{
handler?.Invoke();
}
catch (Exception ex)
{
Debug.LogError(ex);
}
}
/// <summary>
/// 初始化插件
/// </summary>
private void InitPlugins()
{
AndroidNotificationCenter.Initialize();
Debug.Log($"[Noti][AND] --- Notification Service InitPlugins");
UpdateNotiStatus();
}
/// <summary>
/// 更新 Notification 状态码
/// </summary>
private void UpdateNotiStatus()
{
TryExecute(() =>
{
_permissionStatus = AndroidNotificationCenter.UserPermissionToPost;
var status = "";
switch (_permissionStatus)
{
// case PermissionStatus.NotRequested:
// _notiStatus = STATUS_NOT_DETERMINED;
// break;
case PermissionStatus.Allowed:
status = STATUS_GRANTED;;
break;
default:
status = STATUS_DENIDED;
break;
}
SetGrantStatus(status);
Debug.LogWarning($"[SDK][AND] --- UpdateNotiStatus:{_notiStatus} | UserPermissionToPost:{_permissionStatus}");
});
}
private Action<string> _onPermissionCallback;
private PermissionCallbacks _permissionCallbacks;
private void RequestAndroidPermission(Action<string> callback = null)
{
UpdateNotiStatus();
if (_notiStatus == STATUS_GRANTED)
{
callback?.Invoke(_notiStatus);
return;
}
_onPermissionCallback = callback;
TryExecute(() =>
{
var sdkInt = GetAndroidSDKVersion();
if (sdkInt < REQUEST_PERMISSION_SDK_VERSION)
{
// 低版本处理方式
Debug.Log($"[SDK][Noti] --- #2 SDK {sdkInt} not requested -> open channel");
AndroidNotificationCenter.RegisterNotificationChannel(new AndroidNotificationChannel(FCM_DEFAULT_CHANNEL_ID,
FCM_DEFAULT_CHANNEL_ID, "", Importance.Default)); // 打开ChannelID
SetGrantStatus(STATUS_GRANTED);
}
else
{
// SDK 33 以上,请求弹窗
bool hasPermission = Permission.HasUserAuthorizedPermission(PERMISSION_POST_NOTIFICATION);
if (hasPermission)
{
SetGrantStatus(STATUS_GRANTED);
callback?.Invoke(STATUS_GRANTED);
return;
}
Debug.Log($"[SDK][Noti] --- #3 SDK {sdkInt} :: Ask Post Permission");
Permission.RequestUserPermission(PERMISSION_POST_NOTIFICATION, SetupPermissionCallbacks());
}
});
}
private PermissionCallbacks SetupPermissionCallbacks()
{
if(_permissionCallbacks != null) DisposePermissionCallbacks();
_permissionCallbacks = new PermissionCallbacks();
_permissionCallbacks.PermissionGranted += OnPermissionGranted;
_permissionCallbacks.PermissionDenied += OnPermissionDenied;
_permissionCallbacks.PermissionDeniedAndDontAskAgain += OnPermissionDenied;
return _permissionCallbacks;
}
private void DisposePermissionCallbacks()
{
if (_permissionCallbacks != null)
{
_permissionCallbacks.PermissionGranted -= OnPermissionGranted;
_permissionCallbacks.PermissionDenied -= OnPermissionDenied;
_permissionCallbacks.PermissionDeniedAndDontAskAgain -= OnPermissionDenied;
_permissionCallbacks = null;
}
}
/// <summary>
/// 请求通过
/// </summary>
/// <param name="permissionName"></param>
private void OnPermissionGranted(string permissionName)
{
if (permissionName == PERMISSION_POST_NOTIFICATION)
{
_notiStatus = STATUS_GRANTED;
_onPermissionCallback?.Invoke(_notiStatus);
}
SetGrantStatus(STATUS_GRANTED);
DisposePermissionCallbacks();
}
/// <summary>
/// 请求拒绝
/// </summary>
/// <param name="permissionName"></param>
private void OnPermissionDenied(string permissionName)
{
if (permissionName == PERMISSION_POST_NOTIFICATION)
{
_notiStatus = STATUS_DENIDED;
_onPermissionCallback?.Invoke(_notiStatus);
}
SetGrantStatus(STATUS_DENIDED);
DisposePermissionCallbacks();
}
private int GetAndroidSDKVersion()
{
int sdkInt = 999;
TryExecute(() =>
{
using (AndroidJavaClass jc = new AndroidJavaClass("android.os.Build$VERSION"))
{
sdkInt = jc.GetStatic<int>("SDK_INT");
Debug.LogWarning($"[SDK] --- Android SDK Version:{sdkInt}");
}
});
return sdkInt;
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 554fcea56ce74a80a2424e5c037ce6c0
timeCreated: 1718844764

View File

@ -0,0 +1,151 @@
namespace Guru.Notification
{
using System;
using UnityEngine;
#if UNITY_IOS
using System.Threading.Tasks;
using Unity.Notifications.iOS;
#endif
public class NotificationAgentIOS : INotificationAgent
{
private const string STATUS_GRANTED = "granted";
private const string STATUS_DENIDED = "denied";
private const string STATUS_PROVISIONAL = "provisional";
private const string STATUS_NOT_DETERMINED = "not_determined";
private static bool _initOnce;
private static int _waitSeconds = 30;
private string SavedNotiPermStatus
{
get => PlayerPrefs.GetString(nameof(SavedNotiPermStatus), "");
set => PlayerPrefs.SetString(nameof(SavedNotiPermStatus), value);
}
private string _notiStatus;
public void Init()
{
if (_initOnce) return;
_initOnce = true;
_notiStatus = SavedNotiPermStatus;
if (string.IsNullOrEmpty(_notiStatus))
_notiStatus = STATUS_NOT_DETERMINED;
#if UNITY_IOS
InitPlugins();
#endif
}
public string GetStatus()
{
if (!_initOnce) Init();
#if UNITY_IOS
UpdateStatus();
#endif
return _notiStatus;
}
public bool IsAllowed()
{
return _notiStatus == STATUS_GRANTED;
}
public void RequestPermission(Action<string> callback = null)
{
if (!_initOnce) Init();
if (_notiStatus == STATUS_GRANTED || _notiStatus == STATUS_DENIDED)
{
Debug.Log($"[SDK][Noti][iOS] --- Already has Status: {_notiStatus}");
callback?.Invoke(_notiStatus); // 已获得授权, 直接返回结果
return;
}
#if UNITY_IOS
RequestIOSPermission(callback);
#endif
}
#if UNITY_IOS
private void InitPlugins()
{
UpdateStatus();
}
/// <summary>
/// 更新状态
/// </summary>
private void UpdateStatus()
{
string status = STATUS_NOT_DETERMINED;
var authorizationStatus = iOSNotificationCenter.GetNotificationSettings().AuthorizationStatus;
switch (authorizationStatus)
{
case AuthorizationStatus.Authorized:
status = STATUS_GRANTED;
break;
case AuthorizationStatus.Denied:
status = STATUS_DENIDED;
break;
case AuthorizationStatus.NotDetermined:
status = STATUS_NOT_DETERMINED;
break;
case AuthorizationStatus.Provisional:
status = STATUS_PROVISIONAL;
break;
default:
Debug.Log($"[SDK][Noti][iOS] --- Unmarked AuthorizationStatus: {status}");
break;
}
SetGrantStatus(status);
}
private void SetGrantStatus(string status)
{
_notiStatus = status;
SavedNotiPermStatus = status;
}
/// <summary>
/// 请求 IOS 的推送
/// </summary>
/// <param name="callback"></param>
private async void RequestIOSPermission(Action<string> callback = null)
{
Debug.Log($"[SDK][Noti][iOS] --- RequestIOSPermission start");
int timePassed = 0;
using (var req = new AuthorizationRequest(AuthorizationOption.Alert | AuthorizationOption.Badge, true))
{
while (!req.IsFinished && timePassed < _waitSeconds)
{
timePassed++;
await Task.Delay(1000);
};
if (timePassed >= _waitSeconds)
{
Debug.LogWarning($"[SDK][Noti][iOS] --- RequestIOSPermission timeout");
}
UpdateStatus();
callback?.Invoke(_notiStatus);
Debug.Log($"[SDK][Noti][iOS] --- User Selected: {_notiStatus}");
}
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5068c6e238e74251955320761c96182b
timeCreated: 1718871877

View File

@ -0,0 +1,58 @@
namespace Guru.Notification
{
using System;
using System.Threading.Tasks;
using UnityEngine;
/// <summary>
/// For Editor to use Notifications
/// </summary>
public class NotificationAgentStub: INotificationAgent
{
private const string STATUS_GRANTED = "granted";
private const string STATUS_DENIDED = "denied";
private const string STATUS_NOT_DETERMINED = "not_determined";
private Action<string> _onPermissionCallback;
private float _delaySeconds = 1.0f;
private string EditorGrantedStatus
{
get => PlayerPrefs.GetString(nameof(EditorGrantedStatus), STATUS_NOT_DETERMINED);
set => PlayerPrefs.SetString(nameof(EditorGrantedStatus), value);
}
public void Init()
{
Debug.Log($"[SDK][Noti][EDT] --- NotificationAgentStub Init: {EditorGrantedStatus}");
}
public string GetStatus() => EditorGrantedStatus;
public bool IsAllowed()
{
return EditorGrantedStatus == STATUS_GRANTED;
}
public void RequestPermission(Action<string> callback = null)
{
Debug.Log($"[SDK][Noti][EDT] --- RequestPermission ---");
_onPermissionCallback = callback;
DelayCallPermissionHandle();
}
/// <summary>
/// 延迟模拟回调
/// </summary>
private async void DelayCallPermissionHandle()
{
await Task.Delay((int)(1000 * _delaySeconds));
EditorGrantedStatus = STATUS_GRANTED;
_onPermissionCallback?.Invoke(EditorGrantedStatus);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6550e85d25634c46b7a57c490dcea173
timeCreated: 1718850440

View File

@ -0,0 +1,112 @@
namespace Guru.Notification
{
using UnityEngine;
using System;
/// <summary>
/// 消息管理器
/// </summary>
public class NotificationService
{
// 服务版本号
public const string Version = "0.0.1";
// 初始化标志位
private static bool _initOnce;
private static string DefaultPermissionStatus
{
get
{
#if UNITY_IOS
return "not_determined";
#endif
return "denied";
}
}
#region 初始化
private static INotificationAgent _agent;
internal static INotificationAgent Agent
{
get
{
if (_agent == null)
{
_agent = GetAgent();
}
return _agent;
}
}
private static INotificationAgent GetAgent()
{
#if UNITY_EDITOR
return new NotificationAgentStub();
#elif UNITY_ANDROID
return new NotificationAgentAndroid();
#elif UNITY_IOS
return new NotificationAgentIOS();
#endif
return null;
}
/// <summary>
/// 初始化
/// </summary>
public static void Initialize()
{
if (_initOnce) return;
_initOnce = true;
Agent?.Init(); // 初始化代理
}
#endregion
#region 接口
/// <summary>
/// 拉起 Noti 请求
/// </summary>
/// <param name="callback"></param>
public static void RequestPermission(Action<string> callback = null)
{
if (Agent != null)
{
Agent.RequestPermission(callback);
return;
}
Debug.LogError($"[SDK][Noti] --- Agent is missing, return default status: {DefaultPermissionStatus}");
callback?.Invoke(DefaultPermissionStatus);
}
public static bool IsPermissionGranted()
{
return Agent?.IsAllowed() ?? false;
}
public static string GetStatus()
{
if(!_initOnce) Initialize();
if(Agent != null) return Agent.GetStatus();
Debug.LogError($"[SDK][Noti] --- Agent is missing, return default status: {DefaultPermissionStatus}");
return DefaultPermissionStatus;
}
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e0f31750e8bc48a6a5e4f56ee2d5e1cc
timeCreated: 1718846076

View File

@ -1,6 +1,8 @@
namespace Guru
{
using System;
using UnityEngine;
using Newtonsoft.Json;
public abstract class RemoteConfigBase<T>: IRemoteConfig<T> where T : IRemoteConfig<T>
{
@ -8,6 +10,8 @@ namespace Guru
/// 配置是否可用
/// </summary>
public bool enable { get; set; } = true;
[JsonIgnore]
public Action<T> OnValueChanged { get; set; }
/// <summary>
/// 转为Json

View File

@ -12,6 +12,7 @@
"dependencies": {
"com.unity.ads.ios-support": "1.2.0",
"com.unity.editorcoroutines": "1.0.0",
"com.unity.mobile.notifications": "2.2.2",
"com.unity.mobile.android-logcat": "1.3.2",
"com.unity.purchasing": "4.10.0"
}