diff --git a/RevenueCatUI/Scripts/CustomerCenterBehaviour.cs b/RevenueCatUI/Scripts/CustomerCenterBehaviour.cs new file mode 100644 index 00000000..182c9252 --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterBehaviour.cs @@ -0,0 +1,290 @@ +using System; +using System.Threading; +using UnityEngine; +using UnityEngine.Events; +using RevenueCat; + +namespace RevenueCatUI +{ + /// + /// MonoBehaviour component for presenting the RevenueCat Customer Center from the Unity Editor. + /// Mirrors but exposes UnityEvents so flows can be wired + /// directly through the Inspector. + /// + [AddComponentMenu("RevenueCat/Customer Center Behaviour")] + public class CustomerCenterBehaviour : MonoBehaviour + { + [Serializable] public class StringEvent : UnityEvent { } + [Serializable] public class RefundRequestCompletedEvent : UnityEvent { } + [Serializable] public class ManagementOptionSelectedEvent : UnityEvent { } + [Serializable] public class CustomActionSelectedEvent : UnityEvent { } + + [Header("Events")] + [Tooltip("Invoked after the Customer Center is successfully dismissed.")] + public UnityEvent OnDismissed = new UnityEvent(); + + [Tooltip("Invoked if an error prevents the Customer Center from showing.")] + public UnityEvent OnError = new UnityEvent(); + + [Tooltip("Invoked when the manage subscriptions screen is about to be shown.")] + public UnityEvent OnShowingManageSubscriptions = new UnityEvent(); + + [Tooltip("Invoked when a restore operation starts from the Customer Center.")] + public UnityEvent OnRestoreStarted = new UnityEvent(); + + [Tooltip("Invoked when a restore operation finishes successfully.")] + public UnityEvent OnRestoreCompleted = new UnityEvent(); + + [Tooltip("Invoked when a restore operation fails.")] + public UnityEvent OnRestoreFailed = new UnityEvent(); + + [Tooltip("Invoked when a feedback survey is completed. Provides the option identifier.")] + public StringEvent OnFeedbackSurveyCompleted = new StringEvent(); + + [Tooltip("Invoked when a refund request is initiated. Provides the product identifier.")] + public StringEvent OnRefundRequestStarted = new StringEvent(); + + [Tooltip("Invoked when a refund request finishes. Provides the product identifier and resulting status.")] + public RefundRequestCompletedEvent OnRefundRequestCompleted = new RefundRequestCompletedEvent(); + + [Tooltip("Invoked when a management option is selected. Provides the option and optional URL.")] + public ManagementOptionSelectedEvent OnManagementOptionSelected = new ManagementOptionSelectedEvent(); + + [Tooltip("Invoked when a custom action is selected. Provides the action identifier and optional purchase identifier.")] + public CustomActionSelectedEvent OnCustomActionSelected = new CustomActionSelectedEvent(); + + private SynchronizationContext unitySynchronizationContext; + private bool isPresenting; + private Exception lastPresentationException; + private Purchases.CustomerInfo lastRestoreCustomerInfo; + private Purchases.Error lastRestoreError; + private string lastFeedbackSurveyOptionId; + private string lastRefundRequestStartedProductId; + private string lastRefundRequestCompletedProductId; + private string lastRefundRequestStatus = RefundRequestStatus.Error; + private string lastManagementOption = CustomerCenterManagementOption.Unknown; + private string lastManagementOptionUrl; + private string lastCustomActionId; + private string lastCustomActionPurchaseId; + + public bool IsPresenting => isPresenting; + public Exception LastPresentationException => lastPresentationException; + public Purchases.CustomerInfo LastRestoreCustomerInfo => lastRestoreCustomerInfo; + public Purchases.Error LastRestoreError => lastRestoreError; + public string LastFeedbackSurveyOptionId => lastFeedbackSurveyOptionId; + public string LastRefundRequestStartedProductId => lastRefundRequestStartedProductId; + public string LastRefundRequestCompletedProductId => lastRefundRequestCompletedProductId; + public string LastRefundRequestStatus => lastRefundRequestStatus; + public string LastManagementOption => lastManagementOption; + public string LastManagementOptionUrl => lastManagementOptionUrl; + public string LastCustomActionId => lastCustomActionId; + public string LastCustomActionPurchaseId => lastCustomActionPurchaseId; + + private void Awake() + { + unitySynchronizationContext = SynchronizationContext.Current ?? new SynchronizationContext(); + } + + /// + /// Presents the Customer Center. Can be wired to Unity UI (e.g. buttons) or invoked programmatically. + /// + public async void PresentCustomerCenter() + { + if (isPresenting) + { + Debug.LogWarning("[RevenueCatUI] Customer Center is already being presented."); + return; + } + + isPresenting = true; + lastPresentationException = null; + ResetLastEventData(); + + try + { + var callbacks = CreateCallbacks(); + await CustomerCenterPresenter.Present(callbacks); + InvokeUnityEvent(OnDismissed, nameof(OnDismissed)); + } + catch (Exception e) + { + lastPresentationException = e; + Debug.LogError($"[RevenueCatUI] Exception in CustomerCenterBehaviour: {e.Message}"); + InvokeUnityEvent(OnError, nameof(OnError)); + } + finally + { + isPresenting = false; + } + } + + private CustomerCenterCallbacks CreateCallbacks() + { + return new CustomerCenterCallbacks + { + OnFeedbackSurveyCompleted = args => + { + lastFeedbackSurveyOptionId = args?.FeedbackSurveyOptionId; + DispatchToUnityThread(() => + InvokeUnityEvent(OnFeedbackSurveyCompleted, lastFeedbackSurveyOptionId, nameof(OnFeedbackSurveyCompleted))); + }, + OnShowingManageSubscriptions = () => + { + DispatchToUnityThread(() => + InvokeUnityEvent(OnShowingManageSubscriptions, nameof(OnShowingManageSubscriptions))); + }, + OnRestoreCompleted = args => + { + lastRestoreCustomerInfo = args?.CustomerInfo; + DispatchToUnityThread(() => + InvokeUnityEvent(OnRestoreCompleted, nameof(OnRestoreCompleted))); + }, + OnRestoreFailed = args => + { + lastRestoreError = args?.Error; + DispatchToUnityThread(() => + InvokeUnityEvent(OnRestoreFailed, nameof(OnRestoreFailed))); + }, + OnRestoreStarted = () => + { + DispatchToUnityThread(() => + InvokeUnityEvent(OnRestoreStarted, nameof(OnRestoreStarted))); + }, + OnRefundRequestStarted = args => + { + lastRefundRequestStartedProductId = args?.ProductIdentifier; + DispatchToUnityThread(() => + InvokeUnityEvent(OnRefundRequestStarted, lastRefundRequestStartedProductId, nameof(OnRefundRequestStarted))); + }, + OnRefundRequestCompleted = args => + { + lastRefundRequestCompletedProductId = args?.ProductIdentifier; + lastRefundRequestStatus = args?.RefundRequestStatus ?? RefundRequestStatus.Error; + DispatchToUnityThread(() => + InvokeUnityEvent(OnRefundRequestCompleted, lastRefundRequestCompletedProductId, lastRefundRequestStatus, nameof(OnRefundRequestCompleted))); + }, + OnManagementOptionSelected = args => + { + lastManagementOption = args?.Option ?? CustomerCenterManagementOption.Unknown; + lastManagementOptionUrl = args?.Url; + DispatchToUnityThread(() => + InvokeUnityEvent(OnManagementOptionSelected, lastManagementOption, lastManagementOptionUrl, nameof(OnManagementOptionSelected))); + }, + OnCustomActionSelected = args => + { + lastCustomActionId = args?.ActionId; + lastCustomActionPurchaseId = args?.PurchaseIdentifier; + DispatchToUnityThread(() => + InvokeUnityEvent(OnCustomActionSelected, lastCustomActionId, lastCustomActionPurchaseId, nameof(OnCustomActionSelected))); + } + }; + } + + private void DispatchToUnityThread(Action action) + { + if (unitySynchronizationContext == null) + { + action?.Invoke(); + return; + } + + if (SynchronizationContext.Current == unitySynchronizationContext) + { + action?.Invoke(); + } + else + { + unitySynchronizationContext.Post(_ => action?.Invoke(), null); + } + } + + private void InvokeUnityEvent(UnityEvent unityEvent, string eventName) + { + if (unityEvent == null) + { + return; + } + + if (unityEvent.GetPersistentEventCount() == 0) + { + Debug.Log($"[RevenueCatUI] Customer Center {eventName} event has no listeners."); + } + + unityEvent.Invoke(); + } + + private void InvokeUnityEvent(StringEvent unityEvent, string argument, string eventName) + { + if (unityEvent == null) + { + return; + } + + if (unityEvent.GetPersistentEventCount() == 0) + { + Debug.Log($"[RevenueCatUI] Customer Center {eventName} event has no listeners."); + } + + unityEvent.Invoke(argument); + } + + private void InvokeUnityEvent(RefundRequestCompletedEvent unityEvent, string productId, string status, string eventName) + { + if (unityEvent == null) + { + return; + } + + if (unityEvent.GetPersistentEventCount() == 0) + { + Debug.Log($"[RevenueCatUI] Customer Center {eventName} event has no listeners."); + } + + unityEvent.Invoke(productId, status); + } + + private void InvokeUnityEvent(ManagementOptionSelectedEvent unityEvent, string option, string url, string eventName) + { + if (unityEvent == null) + { + return; + } + + if (unityEvent.GetPersistentEventCount() == 0) + { + Debug.Log($"[RevenueCatUI] Customer Center {eventName} event has no listeners."); + } + + unityEvent.Invoke(option, url); + } + + private void InvokeUnityEvent(CustomActionSelectedEvent unityEvent, string actionId, string purchaseIdentifier, string eventName) + { + if (unityEvent == null) + { + return; + } + + if (unityEvent.GetPersistentEventCount() == 0) + { + Debug.Log($"[RevenueCatUI] Customer Center {eventName} event has no listeners."); + } + + unityEvent.Invoke(actionId, purchaseIdentifier); + } + + private void ResetLastEventData() + { + lastRestoreCustomerInfo = null; + lastRestoreError = null; + lastFeedbackSurveyOptionId = null; + lastRefundRequestStartedProductId = null; + lastRefundRequestCompletedProductId = null; + lastRefundRequestStatus = RefundRequestStatus.Error; + lastManagementOption = CustomerCenterManagementOption.Unknown; + lastManagementOptionUrl = null; + lastCustomActionId = null; + lastCustomActionPurchaseId = null; + } + } +} diff --git a/RevenueCatUI/Scripts/CustomerCenterBehaviour.cs.meta b/RevenueCatUI/Scripts/CustomerCenterBehaviour.cs.meta new file mode 100644 index 00000000..f723cbfb --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d9d4f0560a2542c29864fadff17802d3 diff --git a/Subtester/Assets/Scripts/PurchasesListener.cs b/Subtester/Assets/Scripts/PurchasesListener.cs index 7d761448..f9a1ae31 100644 --- a/Subtester/Assets/Scripts/PurchasesListener.cs +++ b/Subtester/Assets/Scripts/PurchasesListener.cs @@ -11,6 +11,7 @@ public class PurchasesListener : Purchases.UpdatedCustomerInfoListener public RectTransform parentPanel; public GameObject buttonPrefab; public Text infoLabel; + public RevenueCatUI.CustomerCenterBehaviour customerCenterBehaviour; private bool simulatesAskToBuyInSandbox; @@ -71,6 +72,7 @@ private void Start() CreateButton("Present Paywall for Offering", PresentPaywallForOffering); CreateButton("Present Paywall If Needed", PresentPaywallIfNeeded); CreateButton("Present Customer Center", PresentCustomerCenter); + CreateButton("Present Customer Center (Behaviour)", PresentCustomerCenterViaBehaviour); var purchases = GetComponent(); purchases.SetLogLevel(Purchases.LogLevel.Verbose); @@ -236,6 +238,23 @@ void PresentCustomerCenter() StartCoroutine(PresentCustomerCenterCoroutine()); } + void PresentCustomerCenterViaBehaviour() + { + if (customerCenterBehaviour == null) + { + Debug.LogWarning("Subtester: CustomerCenterBehaviour reference not assigned."); + if (infoLabel != null) + { + infoLabel.text = "Customer Center Behaviour not assigned."; + } + return; + } + + Debug.Log("Subtester: launching customer center via behaviour"); + if (infoLabel != null) infoLabel.text = "Launching Customer Center via Behaviour..."; + customerCenterBehaviour.PresentCustomerCenter(); + } + private System.Collections.IEnumerator PresentPaywallCoroutine() { var task = RevenueCatUI.PaywallsPresenter.Present();