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();