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