using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class CustomerSupportCanvas : MonoBehaviour
{
const float ScreenFadeSeconds = 0.5f;
const float PollIntervalSeconds = 5f;
const float ChatLayoutRebuildDelay = 0.1f;
const int ChatWrapEveryChars = 25;
static readonly (string slug, string label)[] Categories =
{
(SupportClient.Category.Bug, "Report Bug"),
(SupportClient.Category.Cheating, "Report cheating"),
(SupportClient.Category.Withdrawal, "Request withdrawal"),
(SupportClient.Category.Crypto, "Pay with crypto"),
(SupportClient.Category.Other, "Other"),
};
public CanvasGroup helpScreen;
public Button btnCloseHelp;
public Button btnFaq, btnTickets;
public string faqUrl = "https://kickkings.app/faq";
public CanvasGroup ticketScreen;
public Button btnCloseTicket;
///
/// If 0 open tickets, "No open tickets", otherwise "X Open Tickets"
///
public TMP_Text txtTicketTitle;
public RectTransform ticketListParent;
///
/// Item for the ticket list, single TMP_Text inside. Which should be like "Ticket #001", Once clicked, Show the necessary chat.
///
public GameObject ticketListItemPrefab;
public Sprite spriteTicketOpen, spriteTicketClosed;
///
/// If no chat is selected, show this text, Also dont show the input field if this is shown.
///
public GameObject noChatSelectedTxt;
public RectTransform ticketChatParent;
///
/// ScrollRect that wraps . If unset, resolved from the chat parent.
///
public ScrollRect ticketChatScroll;
///
/// Two children, first is left, second is right, only 1 TMP_Text inside each, which should be the message. enable left for customer support agent, enable right for player.
///
public GameObject ticketChatItemPrefab;
public TMP_InputField inputField;
public Button btnSend;
public Button newTicketBtn;
public GameObject newTicketPanel;
public RectTransform ticketReasonListParent;
///
/// Item for the ticket reason list, a Toggle with a TMP_Text inside, which should be like "Reason 1", "Reason 2", "Reason 3", etc. Only 1 can be on. By default it will be all off, If all off, create Button should be interactable=false;
///
public GameObject ticketReasonItemPrefab;
public Button btnCreateTicket;
public Button btnCancelTicket;
readonly List _tickets = new List();
readonly List _reasonToggles = new List();
readonly List _reasonSlugs = new List();
public static CustomerSupportCanvas instance;
string _selectedTicketId;
string _selectedCategorySlug;
string _selectedCategoryLabel;
string _pushTicketId;
Coroutine _pollRoutine;
Coroutine _busyRoutine;
Coroutine _chatLayoutRoutine;
string _chatFingerprint;
bool _requestInFlight;
bool _reasonsBuilt;
ToggleGroup _reasonToggleGroup;
void Awake()
{
instance = this;
}
void OnDestroy()
{
if (instance == this)
instance = null;
}
void Start()
{
if (btnFaq != null)
btnFaq.onClick.AddListener(LaunchFAQ);
if (btnTickets != null)
btnTickets.onClick.AddListener(ShowTicketScreen);
if (btnCloseHelp != null)
btnCloseHelp.onClick.AddListener(CloseHelp);
if (btnCloseTicket != null)
btnCloseTicket.onClick.AddListener(ShowHelpScreen);
if (btnSend != null)
btnSend.onClick.AddListener(OnSendClicked);
if (newTicketBtn != null)
newTicketBtn.onClick.AddListener(OpenNewTicketPanel);
if (btnCreateTicket != null)
btnCreateTicket.onClick.AddListener(OnCreateTicketClicked);
if (btnCancelTicket != null)
btnCancelTicket.onClick.AddListener(CloseNewTicketPanel);
SetCanvasGroupVisible(helpScreen, false, false);
SetCanvasGroupVisible(ticketScreen, false, false);
ClearChatSelection();
HideNewTicketPanel();
EnsureReasonToggles();
string pendingTicket = PushNotificationService.ConsumePendingTicket();
if (!string.IsNullOrEmpty(pendingTicket))
OpenFromNotification(pendingTicket);
}
///
/// Opens the ticket named by a support push. A reply for the thread already on screen refreshes it immediately.
///
public void OpenFromNotification(string ticketId)
{
if (string.IsNullOrEmpty(ticketId))
return;
bool ticketVisible = ticketScreen != null && ticketScreen.blocksRaycasts;
if (ticketVisible && _selectedTicketId == ticketId)
{
StartCoroutine(RefreshOpenThread(ticketId));
return;
}
if (_requestInFlight)
{
_pushTicketId = ticketId;
return;
}
RunExclusive(ShowTicketFromNotification(ticketId));
}
IEnumerator ShowTicketFromNotification(string ticketId)
{
HideNewTicketPanel();
SetCanvasGroupVisible(helpScreen, false, true);
SetCanvasGroupVisible(ticketScreen, true, true);
bool ok = false;
yield return EnsureSupportAuth(r => ok = r);
if (!ok)
yield break;
SupportCallResult list = null;
yield return SupportClient.ListTickets(r => list = r);
if (list != null && list.ok)
RebuildTicketList(list.data != null ? list.data.tickets : null);
if (_selectedTicketId == ticketId)
yield return RefreshOpenThread(ticketId);
else
SelectTicket(ticketId);
}
IEnumerator RefreshOpenThread(string ticketId)
{
SupportCallResult result = null;
yield return SupportClient.GetThread(ticketId, r => result = r);
if (result == null || !result.ok)
yield break;
if (_selectedTicketId != ticketId)
yield break;
ApplyThread(result.data);
RefreshTicketPreviewFromThread(result.data);
}
void OnDisable()
{
StopPoll();
StopChatLayoutRoutine();
if (_busyRoutine != null)
{
StopCoroutine(_busyRoutine);
_busyRoutine = null;
}
_requestInFlight = false;
}
public void ShowHelpScreen()
{
StopPoll();
ClearChatSelection();
HideNewTicketPanel();
SetCanvasGroupVisible(helpScreen, true, true);
SetCanvasGroupVisible(ticketScreen, false, true);
}
public void ShowTicketScreen()
{
HideNewTicketPanel();
ClearChatSelection();
SetCanvasGroupVisible(helpScreen, false, true);
SetCanvasGroupVisible(ticketScreen, true, true);
RunExclusive(RefreshTicketsFlow());
}
void CloseHelp()
{
StopPoll();
ClearChatSelection();
HideNewTicketPanel();
SetCanvasGroupVisible(helpScreen, false, true);
SetCanvasGroupVisible(ticketScreen, false, true);
}
void LaunchFAQ()
{
Application.OpenURL(faqUrl);
}
void OpenNewTicketPanel()
{
EnsureReasonToggles();
ResetReasonToggles();
if (newTicketPanel != null)
newTicketPanel.SetActive(true);
UpdateCreateButtonInteractable();
}
void CloseNewTicketPanel()
{
HideNewTicketPanel();
}
void HideNewTicketPanel()
{
if (newTicketPanel != null)
newTicketPanel.SetActive(false);
ResetReasonToggles();
}
void OnSendClicked()
{
if (_requestInFlight || string.IsNullOrEmpty(_selectedTicketId))
return;
string body = inputField != null ? inputField.text.Trim() : "";
if (string.IsNullOrEmpty(body))
return;
RunExclusive(SendMessageFlow(body));
}
void OnCreateTicketClicked()
{
if (_requestInFlight || string.IsNullOrEmpty(_selectedCategorySlug))
return;
string body = _selectedCategorySlug == SupportClient.Category.Other
? "Hi I need assistance"
: "Hi, I need assistance with " + _selectedCategoryLabel;
RunExclusive(CreateTicketFlow(_selectedCategorySlug, body));
}
IEnumerator RefreshTicketsFlow()
{
bool ok = false;
yield return EnsureSupportAuth(r => ok = r);
if (!ok)
yield break;
SupportCallResult result = null;
yield return SupportClient.ListTickets(r => result = r);
if (result == null || !result.ok)
{
ShowError(result != null ? result.error : "Could not load tickets.");
yield break;
}
RebuildTicketList(result.data != null ? result.data.tickets : null);
}
IEnumerator SendMessageFlow(string body)
{
bool ok = false;
yield return EnsureSupportAuth(r => ok = r);
if (!ok)
yield break;
string ticketId = _selectedTicketId;
SupportCallResult result = null;
yield return SupportClient.SendMessage(ticketId, body, r => result = r);
if (result == null || !result.ok)
{
ShowError(result != null ? result.error : "Could not send message.");
yield break;
}
if (inputField != null)
inputField.text = "";
ApplyThread(result.data);
RefreshTicketPreviewFromThread(result.data);
RebuildTicketListFromCache();
}
IEnumerator CreateTicketFlow(string category, string body)
{
bool ok = false;
yield return EnsureSupportAuth(r => ok = r);
if (!ok)
yield break;
SupportCallResult result = null;
yield return SupportClient.CreateTicket(category, body, SupportClient.ReadUnityPlayerLog(), r => result = r);
if (result == null || !result.ok)
{
ShowError(result != null ? result.error : "Could not create ticket.");
yield break;
}
HideNewTicketPanel();
SupportCallResult list = null;
yield return SupportClient.ListTickets(r => list = r);
if (list != null && list.ok)
RebuildTicketList(list.data != null ? list.data.tickets : null);
else if (result.data != null && result.data.ticket != null)
{
_tickets.Clear();
_tickets.Add(result.data.ticket);
RebuildTicketListFromCache();
}
if (result.data != null && result.data.ticket != null)
SelectTicket(result.data.ticket.id, result.data);
}
IEnumerator EnsureSupportAuth(Action done)
{
if (SupportClient.IsSignedIn)
{
done?.Invoke(true);
yield break;
}
string username = !string.IsNullOrEmpty(LoginManager.SessionUsername)
? LoginManager.SessionUsername
: (LoginManager.CurrentUser != null ? LoginManager.CurrentUser.username : null);
string password = LoginManager.SessionPassword;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
ShowError("Please sign in again to use customer support.");
done?.Invoke(false);
yield break;
}
SupportCallResult result = null;
yield return SupportClient.Login(username, password, r => result = r);
if (result == null || !result.ok)
{
ShowError(result != null ? result.error : "Support login failed.");
done?.Invoke(false);
yield break;
}
done?.Invoke(true);
}
IEnumerator PollSelectedTicket()
{
string ticketId = _selectedTicketId;
if (string.IsNullOrEmpty(ticketId))
yield break;
yield return SupportClient.PollThread(ticketId, PollIntervalSeconds, result =>
{
if (result == null || !result.ok)
return;
if (_selectedTicketId != ticketId)
return;
ApplyThread(result.data);
RefreshTicketPreviewFromThread(result.data);
});
}
void SelectTicket(string ticketId, SupportThread initialThread = null)
{
if (string.IsNullOrEmpty(ticketId))
return;
_selectedTicketId = ticketId;
if (noChatSelectedTxt != null)
noChatSelectedTxt.SetActive(false);
SetChatInputVisible(true);
if (initialThread != null)
ApplyThread(initialThread);
else
ClearChatMessages();
StopPoll();
_pollRoutine = StartCoroutine(PollSelectedTicket());
}
void ClearChatSelection()
{
StopPoll();
_selectedTicketId = null;
ClearChatMessages();
if (noChatSelectedTxt != null)
noChatSelectedTxt.SetActive(true);
SetChatInputVisible(false);
if (inputField != null)
inputField.text = "";
}
void SetChatInputVisible(bool visible)
{
if (inputField != null)
{
inputField.gameObject.SetActive(visible);
inputField.interactable = visible && !_requestInFlight;
}
if (btnSend != null)
{
btnSend.gameObject.SetActive(visible);
btnSend.interactable = visible && !_requestInFlight && !string.IsNullOrEmpty(_selectedTicketId);
}
}
void RebuildTicketList(SupportTicket[] tickets)
{
_tickets.Clear();
if (tickets != null)
{
for (int i = 0; i < tickets.Length; i++)
{
if (tickets[i] != null)
_tickets.Add(tickets[i]);
}
}
RebuildTicketListFromCache();
}
void RebuildTicketListFromCache()
{
ClearChildren(ticketListParent);
UpdateOpenTicketTitle();
if (ticketListItemPrefab == null || ticketListParent == null)
return;
for (int i = 0; i < _tickets.Count; i++)
{
SupportTicket ticket = _tickets[i];
if (ticket == null)
continue;
GameObject item = Instantiate(ticketListItemPrefab, ticketListParent);
item.SetActive(true);
TMP_Text label = item.GetComponentInChildren(true);
if (label != null)
{
string title = "Ticket #" + (i + 1).ToString("000");
if (!string.IsNullOrEmpty(ticket.categoryLabel))
title += "\n" + ticket.categoryLabel;
label.text = title;
}
Image image = item.GetComponent();
if (image == null)
image = item.GetComponentInChildren(true);
if (image != null)
{
bool isOpen = ticket.status == SupportClient.Status.Open;
Sprite sprite = isOpen ? spriteTicketOpen : spriteTicketClosed;
if (sprite != null)
image.sprite = sprite;
}
Button button = item.GetComponent