push notifications

This commit is contained in:
2026-11-03 20:26:24 +05:30
parent a1b2b41912
commit 3ee348c09f
221 changed files with 9440 additions and 4899 deletions
+405
View File
@@ -0,0 +1,405 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_ANDROID && !UNITY_EDITOR
using Firebase.Extensions;
using Firebase.Messaging;
#endif
/// <summary>
/// Android FCM registration for the player client. The Editor and dedicated server builds do not talk to Firebase.
/// </summary>
public class PushNotificationService : MonoBehaviour
{
public const string BroadcastTopic = "kk_broadcast";
public const string ChannelId = "support";
public static PushNotificationService instance { get; private set; }
static string _pendingTicketId;
readonly List<Action> _mainThread = new List<Action>();
readonly object _mainLock = new object();
#if UNITY_ANDROID && !UNITY_EDITOR
const string RegisteredFidKey = "WalkInvest.SupportPushFid";
const string RegisteredUserKey = "WalkInvest.SupportPushUserId";
const string PostNotificationsPermission = "android.permission.POST_NOTIFICATIONS";
const float UnregisterTimeoutSeconds = 5f;
string _fid;
string _lastRegisteredFid;
int _lastRegisteredUserId;
bool _sessionReady;
bool _registerQueued;
bool _registerInFlight;
bool _topicSubscribed;
bool _subscribeInFlight;
bool _firebaseReady;
Firebase.FirebaseApp _firebaseApp;
#endif
public static void EnsureCreated()
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (instance != null)
return;
var go = new GameObject(nameof(PushNotificationService));
go.AddComponent<PushNotificationService>();
#endif
}
public static void OnGameSessionReady()
{
#if UNITY_ANDROID && !UNITY_EDITOR
EnsureCreated();
if (instance == null)
return;
instance._sessionReady = true;
instance.RequestRegister();
#endif
}
/// <summary>
/// Deletes this install on the admin API, then runs <paramref name="next"/>.
/// A failed or timed-out delete still continues.
/// </summary>
public static void UnregisterThen(Action next)
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (instance != null && instance.CanUnregister)
{
instance.StartCoroutine(instance.UnregisterAndContinue(next));
return;
}
if (instance != null)
instance.ForgetLocalRegistration();
#endif
next?.Invoke();
}
public static string ConsumePendingTicket()
{
string ticketId = _pendingTicketId;
_pendingTicketId = null;
return ticketId;
}
void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
#if UNITY_ANDROID && !UNITY_EDITOR
_lastRegisteredFid = PlayerPrefs.GetString(RegisteredFidKey, "");
_lastRegisteredUserId = PlayerPrefs.GetInt(RegisteredUserKey, 0);
InitializeFirebase();
#endif
}
void OnDestroy()
{
if (instance == this)
instance = null;
#if UNITY_ANDROID && !UNITY_EDITOR
if (_firebaseReady)
{
FirebaseMessaging.RegistrationReceived -= OnRegistrationReceived;
FirebaseMessaging.MessageReceived -= OnMessageReceived;
}
#endif
}
void Update()
{
Action[] batch = null;
lock (_mainLock)
{
if (_mainThread.Count == 0)
return;
batch = _mainThread.ToArray();
_mainThread.Clear();
}
for (int i = 0; i < batch.Length; i++)
batch[i]?.Invoke();
}
#if UNITY_ANDROID && !UNITY_EDITOR
bool CanUnregister
{
get { return _sessionReady && !string.IsNullOrEmpty(_fid) && SupportClient.IsSignedIn; }
}
void InitializeFirebase()
{
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task =>
{
if (instance != this)
return;
if (task.IsFaulted || task.IsCanceled)
{
Logger.Log("Firebase dependency check failed.");
return;
}
var status = task.Result;
if (status != Firebase.DependencyStatus.Available)
{
Logger.Log("Firebase dependencies unavailable: " + status);
return;
}
_firebaseApp = Firebase.FirebaseApp.DefaultInstance;
if (_firebaseApp == null)
{
Logger.Log("Firebase app missing.");
return;
}
_firebaseReady = true;
EnsureNotificationChannel();
RequestNotificationPermission();
FirebaseMessaging.RegistrationReceived += OnRegistrationReceived;
FirebaseMessaging.MessageReceived += OnMessageReceived;
FirebaseMessaging.RegisterAsync().ContinueWithOnMainThread(register =>
{
if (instance != this)
return;
if (register.IsFaulted)
Logger.Log("FCM register failed: " + register.Exception.GetBaseException().Message);
});
});
}
void OnRegistrationReceived(object sender, RegistrationReceivedEventArgs args)
{
string fid = args != null ? args.InstallationId : null;
RunOnMain(() => HandleRegistration(fid));
}
void OnMessageReceived(object sender, MessageReceivedEventArgs args)
{
string type = null;
string ticketId = null;
if (args != null && args.Message != null && args.Message.Data != null)
{
args.Message.Data.TryGetValue("type", out type);
args.Message.Data.TryGetValue("ticketId", out ticketId);
}
string typeCopy = type;
string ticketCopy = ticketId;
RunOnMain(() => HandleMessage(typeCopy, ticketCopy));
}
void HandleRegistration(string fid)
{
if (string.IsNullOrEmpty(fid))
return;
if (fid != _fid)
{
_topicSubscribed = false;
_subscribeInFlight = false;
}
_fid = fid;
if (!_topicSubscribed && !_subscribeInFlight)
{
_subscribeInFlight = true;
StartCoroutine(SubscribeBroadcast());
}
RequestRegister();
}
void HandleMessage(string type, string ticketId)
{
if (!string.Equals(type, "support_reply", StringComparison.Ordinal))
return;
if (string.IsNullOrEmpty(ticketId))
return;
CustomerSupportCanvas canvas = CustomerSupportCanvas.instance;
if (canvas != null && canvas.isActiveAndEnabled)
canvas.OpenFromNotification(ticketId);
else
_pendingTicketId = ticketId;
}
IEnumerator SubscribeBroadcast()
{
var task = FirebaseMessaging.SubscribeAsync(BroadcastTopic);
while (!task.IsCompleted)
yield return null;
_subscribeInFlight = false;
if (task.IsFaulted || task.IsCanceled)
{
Logger.Log("FCM topic subscribe failed.");
yield break;
}
_topicSubscribed = true;
}
void RequestRegister()
{
if (!_sessionReady || string.IsNullOrEmpty(_fid))
return;
_registerQueued = true;
if (!_registerInFlight)
StartCoroutine(RegisterLoop());
}
IEnumerator RegisterLoop()
{
_registerInFlight = true;
while (_registerQueued && _sessionReady)
{
_registerQueued = false;
yield return RegisterDeviceBody();
}
_registerInFlight = false;
}
IEnumerator RegisterDeviceBody()
{
if (!_sessionReady || string.IsNullOrEmpty(_fid))
yield break;
if (!SupportClient.IsSignedIn)
{
string username = LoginManager.SessionUsername;
string password = LoginManager.SessionPassword;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
Logger.Log("Push registration skipped: support session is not available.");
yield break;
}
SupportCallResult<SupportLoginResponse> login = null;
yield return SupportClient.Login(username, password, r => login = r);
if (!_sessionReady)
yield break;
if (login == null || !login.ok)
{
Logger.Log("Push registration skipped: support login failed.");
yield break;
}
}
int userId = SupportClient.UserId;
if (_fid == _lastRegisteredFid && userId != 0 && userId == _lastRegisteredUserId)
yield break;
string fid = _fid;
SupportCallResult<SupportDeviceResponse> result = null;
yield return SupportClient.RegisterDevice(fid, "android", r => result = r);
if (!_sessionReady || fid != _fid)
yield break;
if (result == null || !result.ok)
{
Logger.Log("Push registration failed: " + (result != null ? result.error : "no result"));
yield break;
}
_lastRegisteredFid = fid;
_lastRegisteredUserId = SupportClient.UserId;
PlayerPrefs.SetString(RegisteredFidKey, _lastRegisteredFid ?? "");
PlayerPrefs.SetInt(RegisteredUserKey, _lastRegisteredUserId);
PlayerPrefs.Save();
}
IEnumerator UnregisterAndContinue(Action next)
{
_sessionReady = false;
bool done = false;
StartCoroutine(FinishUnregister(_fid, () => done = true));
float start = Time.unscaledTime;
while (!done && Time.unscaledTime - start < UnregisterTimeoutSeconds)
yield return null;
if (!done)
Logger.Log("Push unregister timed out.");
ForgetLocalRegistration();
next?.Invoke();
}
IEnumerator FinishUnregister(string fid, Action done)
{
SupportCallResult<SupportDeviceResponse> result = null;
yield return SupportClient.UnregisterDevice(fid, r => result = r);
if (result == null || !result.ok)
Logger.Log("Push unregister failed: " + (result != null ? result.error : "no result"));
done?.Invoke();
}
void ForgetLocalRegistration()
{
_lastRegisteredFid = null;
_lastRegisteredUserId = 0;
PlayerPrefs.DeleteKey(RegisteredFidKey);
PlayerPrefs.DeleteKey(RegisteredUserKey);
PlayerPrefs.Save();
}
void RunOnMain(Action action)
{
if (action == null)
return;
lock (_mainLock)
_mainThread.Add(action);
}
static void RequestNotificationPermission()
{
try
{
using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
{
if (version.GetStatic<int>("SDK_INT") < 33)
return;
}
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(PostNotificationsPermission))
UnityEngine.Android.Permission.RequestUserPermission(PostNotificationsPermission);
}
catch (Exception e)
{
Logger.Log("Notification permission request failed: " + e.Message);
}
}
static void EnsureNotificationChannel()
{
try
{
using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
{
if (version.GetStatic<int>("SDK_INT") < 26)
return;
}
using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
using (var channel = new AndroidJavaObject("android.app.NotificationChannel", ChannelId, "Support", 4))
using (var manager = activity.Call<AndroidJavaObject>("getSystemService", "notification"))
{
channel.Call("setDescription", "Support replies and announcements");
manager.Call("createNotificationChannel", channel);
}
}
catch (Exception e)
{
Logger.Log("Notification channel setup failed: " + e.Message);
}
}
#endif
}