656 lines
20 KiB
C#
656 lines
20 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using DG.Tweening;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
public class CustomerSupportCanvas : MonoBehaviour
|
|
{
|
|
const float ScreenFadeSeconds = 0.5f;
|
|
const float PollIntervalSeconds = 5f;
|
|
|
|
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;
|
|
/// <summary>
|
|
/// If 0 open tickets, "No open tickets", otherwise "X Open Tickets"
|
|
/// </summary>
|
|
public TMP_Text txtTicketTitle;
|
|
public RectTransform ticketListParent;
|
|
/// <summary>
|
|
/// Item for the ticket list, single TMP_Text inside. Which should be like "Ticket #001", Once clicked, Show the necessary chat.
|
|
/// </summary>
|
|
public GameObject ticketListItemPrefab;
|
|
public Sprite spriteTicketOpen, spriteTicketClosed;
|
|
/// <summary>
|
|
/// If no chat is selected, show this text, Also dont show the input field if this is shown.
|
|
/// </summary>
|
|
public GameObject noChatSelectedTxt;
|
|
public RectTransform ticketChatParent;
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public GameObject ticketChatItemPrefab;
|
|
|
|
public TMP_InputField inputField;
|
|
public Button btnSend;
|
|
|
|
public Button newTicketBtn;
|
|
public GameObject newTicketPanel;
|
|
public RectTransform ticketReasonListParent;
|
|
/// <summary>
|
|
/// 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;
|
|
/// </summary>
|
|
public GameObject ticketReasonItemPrefab;
|
|
public Button btnCreateTicket;
|
|
public Button btnCancelTicket;
|
|
|
|
readonly List<SupportTicket> _tickets = new List<SupportTicket>();
|
|
readonly List<Toggle> _reasonToggles = new List<Toggle>();
|
|
readonly List<string> _reasonSlugs = new List<string>();
|
|
|
|
string _selectedTicketId;
|
|
string _selectedCategorySlug;
|
|
string _selectedCategoryLabel;
|
|
Coroutine _pollRoutine;
|
|
Coroutine _busyRoutine;
|
|
bool _requestInFlight;
|
|
bool _reasonsBuilt;
|
|
ToggleGroup _reasonToggleGroup;
|
|
|
|
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, true, false);
|
|
SetCanvasGroupVisible(ticketScreen, false, false);
|
|
ClearChatSelection();
|
|
HideNewTicketPanel();
|
|
EnsureReasonToggles();
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
StopPoll();
|
|
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);
|
|
gameObject.SetActive(false);
|
|
}
|
|
|
|
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<SupportTicketListResponse> 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<SupportThread> 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<SupportThread> result = null;
|
|
yield return SupportClient.CreateTicket(category, body, r => result = r);
|
|
if (result == null || !result.ok)
|
|
{
|
|
ShowError(result != null ? result.error : "Could not create ticket.");
|
|
yield break;
|
|
}
|
|
|
|
HideNewTicketPanel();
|
|
|
|
SupportCallResult<SupportTicketListResponse> 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<bool> 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;
|
|
Debug.Log("[CustomerSupport] support login as user='" + (username ?? "") +
|
|
"' passwordLength=" + (password != null ? password.Length : 0));
|
|
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
|
{
|
|
ShowError("Please sign in again to use customer support.");
|
|
done?.Invoke(false);
|
|
yield break;
|
|
}
|
|
|
|
SupportCallResult<SupportLoginResponse> 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);
|
|
if (btnSend != null)
|
|
btnSend.gameObject.SetActive(visible);
|
|
}
|
|
|
|
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<TMP_Text>(true);
|
|
if (label != null)
|
|
label.text = "Ticket #" + (i + 1).ToString("000");
|
|
|
|
Image image = item.GetComponent<Image>();
|
|
if (image == null)
|
|
image = item.GetComponentInChildren<Image>(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<Button>();
|
|
if (button == null)
|
|
button = item.GetComponentInChildren<Button>(true);
|
|
if (button == null)
|
|
{
|
|
button = item.AddComponent<Button>();
|
|
if (image != null)
|
|
button.targetGraphic = image;
|
|
}
|
|
|
|
string ticketId = ticket.id;
|
|
button.onClick.RemoveAllListeners();
|
|
button.onClick.AddListener(() => SelectTicket(ticketId));
|
|
}
|
|
}
|
|
|
|
void ApplyThread(SupportThread thread)
|
|
{
|
|
if (thread == null)
|
|
return;
|
|
|
|
ClearChatMessages();
|
|
if (ticketChatItemPrefab == null || ticketChatParent == null || thread.messages == null)
|
|
return;
|
|
|
|
for (int i = 0; i < thread.messages.Length; i++)
|
|
{
|
|
SupportMessage message = thread.messages[i];
|
|
if (message == null)
|
|
continue;
|
|
|
|
GameObject item = Instantiate(ticketChatItemPrefab, ticketChatParent);
|
|
item.SetActive(true);
|
|
|
|
bool isStaff = string.Equals(message.authorKind, SupportClient.AuthorKind.Staff, StringComparison.OrdinalIgnoreCase);
|
|
Transform left = item.transform.childCount > 0 ? item.transform.GetChild(0) : null;
|
|
Transform right = item.transform.childCount > 1 ? item.transform.GetChild(1) : null;
|
|
|
|
if (left != null)
|
|
left.gameObject.SetActive(isStaff);
|
|
if (right != null)
|
|
right.gameObject.SetActive(!isStaff);
|
|
|
|
Transform activeSide = isStaff ? left : right;
|
|
if (activeSide != null)
|
|
{
|
|
TMP_Text text = activeSide.GetComponentInChildren<TMP_Text>(true);
|
|
if (text != null)
|
|
text.text = message.body ?? "";
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClearChatMessages()
|
|
{
|
|
ClearChildren(ticketChatParent);
|
|
}
|
|
|
|
void RefreshTicketPreviewFromThread(SupportThread thread)
|
|
{
|
|
if (thread == null || thread.ticket == null || string.IsNullOrEmpty(thread.ticket.id))
|
|
return;
|
|
|
|
bool found = false;
|
|
for (int i = 0; i < _tickets.Count; i++)
|
|
{
|
|
if (_tickets[i] != null && _tickets[i].id == thread.ticket.id)
|
|
{
|
|
_tickets[i] = thread.ticket;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
_tickets.Insert(0, thread.ticket);
|
|
|
|
UpdateOpenTicketTitle();
|
|
}
|
|
|
|
void UpdateOpenTicketTitle()
|
|
{
|
|
if (txtTicketTitle == null)
|
|
return;
|
|
|
|
int openCount = 0;
|
|
for (int i = 0; i < _tickets.Count; i++)
|
|
{
|
|
if (_tickets[i] != null && _tickets[i].status == SupportClient.Status.Open)
|
|
openCount++;
|
|
}
|
|
|
|
txtTicketTitle.text = openCount == 0
|
|
? "No open tickets"
|
|
: openCount + (openCount == 1 ? " Open Ticket" : " Open Tickets");
|
|
}
|
|
|
|
void EnsureReasonToggles()
|
|
{
|
|
if (_reasonsBuilt || ticketReasonListParent == null || ticketReasonItemPrefab == null)
|
|
return;
|
|
|
|
_reasonsBuilt = true;
|
|
ClearChildren(ticketReasonListParent);
|
|
_reasonToggles.Clear();
|
|
_reasonSlugs.Clear();
|
|
|
|
_reasonToggleGroup = ticketReasonListParent.GetComponent<ToggleGroup>();
|
|
if (_reasonToggleGroup == null)
|
|
_reasonToggleGroup = ticketReasonListParent.gameObject.AddComponent<ToggleGroup>();
|
|
_reasonToggleGroup.allowSwitchOff = true;
|
|
|
|
for (int i = 0; i < Categories.Length; i++)
|
|
{
|
|
string slug = Categories[i].slug;
|
|
string label = Categories[i].label;
|
|
|
|
GameObject item = Instantiate(ticketReasonItemPrefab, ticketReasonListParent);
|
|
item.SetActive(true);
|
|
|
|
Toggle toggle = item.GetComponent<Toggle>();
|
|
if (toggle == null)
|
|
toggle = item.GetComponentInChildren<Toggle>(true);
|
|
if (toggle == null)
|
|
continue;
|
|
|
|
toggle.group = _reasonToggleGroup;
|
|
toggle.isOn = false;
|
|
toggle.onValueChanged.RemoveAllListeners();
|
|
|
|
TMP_Text text = item.GetComponentInChildren<TMP_Text>(true);
|
|
if (text != null)
|
|
text.text = label;
|
|
|
|
string capturedSlug = slug;
|
|
string capturedLabel = label;
|
|
toggle.onValueChanged.AddListener(isOn =>
|
|
{
|
|
if (isOn)
|
|
{
|
|
_selectedCategorySlug = capturedSlug;
|
|
_selectedCategoryLabel = capturedLabel;
|
|
}
|
|
else if (_selectedCategorySlug == capturedSlug)
|
|
{
|
|
_selectedCategorySlug = null;
|
|
_selectedCategoryLabel = null;
|
|
}
|
|
UpdateCreateButtonInteractable();
|
|
});
|
|
|
|
_reasonToggles.Add(toggle);
|
|
_reasonSlugs.Add(slug);
|
|
}
|
|
|
|
UpdateCreateButtonInteractable();
|
|
}
|
|
|
|
void ResetReasonToggles()
|
|
{
|
|
_selectedCategorySlug = null;
|
|
_selectedCategoryLabel = null;
|
|
for (int i = 0; i < _reasonToggles.Count; i++)
|
|
{
|
|
if (_reasonToggles[i] != null)
|
|
_reasonToggles[i].SetIsOnWithoutNotify(false);
|
|
}
|
|
UpdateCreateButtonInteractable();
|
|
}
|
|
|
|
void UpdateCreateButtonInteractable()
|
|
{
|
|
if (btnCreateTicket != null)
|
|
btnCreateTicket.interactable = !_requestInFlight && !string.IsNullOrEmpty(_selectedCategorySlug);
|
|
}
|
|
|
|
void RunExclusive(IEnumerator routine)
|
|
{
|
|
if (_requestInFlight)
|
|
return;
|
|
if (_busyRoutine != null)
|
|
StopCoroutine(_busyRoutine);
|
|
_busyRoutine = StartCoroutine(RunExclusiveCoroutine(routine));
|
|
}
|
|
|
|
IEnumerator RunExclusiveCoroutine(IEnumerator routine)
|
|
{
|
|
_requestInFlight = true;
|
|
SetBusyUi(true);
|
|
try
|
|
{
|
|
yield return routine;
|
|
}
|
|
finally
|
|
{
|
|
_requestInFlight = false;
|
|
_busyRoutine = null;
|
|
SetBusyUi(false);
|
|
}
|
|
}
|
|
|
|
void SetBusyUi(bool busy)
|
|
{
|
|
if (btnSend != null)
|
|
btnSend.interactable = !busy && !string.IsNullOrEmpty(_selectedTicketId);
|
|
if (newTicketBtn != null)
|
|
newTicketBtn.interactable = !busy;
|
|
if (btnTickets != null)
|
|
btnTickets.interactable = !busy;
|
|
UpdateCreateButtonInteractable();
|
|
if (busy && btnCreateTicket != null)
|
|
btnCreateTicket.interactable = false;
|
|
}
|
|
|
|
void StopPoll()
|
|
{
|
|
if (_pollRoutine != null)
|
|
{
|
|
StopCoroutine(_pollRoutine);
|
|
_pollRoutine = null;
|
|
}
|
|
}
|
|
|
|
static void SetCanvasGroupVisible(CanvasGroup group, bool visible, bool animate)
|
|
{
|
|
if (group == null)
|
|
return;
|
|
|
|
group.DOKill();
|
|
group.blocksRaycasts = visible;
|
|
group.interactable = visible;
|
|
if (animate)
|
|
group.DOFade(visible ? 1f : 0f, ScreenFadeSeconds);
|
|
else
|
|
group.alpha = visible ? 1f : 0f;
|
|
}
|
|
|
|
static void ClearChildren(Transform parent)
|
|
{
|
|
if (parent == null)
|
|
return;
|
|
for (int i = parent.childCount - 1; i >= 0; i--)
|
|
Destroy(parent.GetChild(i).gameObject);
|
|
}
|
|
|
|
static void ShowError(string message)
|
|
{
|
|
if (string.IsNullOrEmpty(message))
|
|
message = "Something went wrong. Please try again.";
|
|
if (MessageBoxDialog.IsAvailable)
|
|
MessageBoxDialog.ShowBasic(message);
|
|
else
|
|
Debug.LogWarning(message);
|
|
}
|
|
}
|