Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
cd94337
Add basic interface for customer center
facumenzella Oct 8, 2025
e9739bc
Merge branch 'main' into feat/customercenter-staticinterface
facumenzella Oct 8, 2025
6f3b3d8
Merge remote-tracking branch 'origin/main' into feat/customercenter-s…
facumenzella Oct 10, 2025
4cb892d
Merge branch 'feat/customercenter-staticinterface' of github.com:Reve…
facumenzella Oct 10, 2025
48a35fe
update deps
facumenzella Oct 10, 2025
3b54e0b
Merge branch 'main' into feat/customercenter-staticinterface
facumenzella Oct 13, 2025
6c90d5b
Add empty CustomerCenterCallbacks
facumenzella Oct 15, 2025
d846da5
Merge branch 'feat/customercenter-staticinterface' of github.com:Reve…
facumenzella Oct 15, 2025
8ad87e1
Remove result
facumenzella Oct 16, 2025
388aa64
remove extras
facumenzella Oct 16, 2025
9c01e8d
push missing piece
facumenzella Oct 16, 2025
15bc076
callbacks
vegaro Oct 16, 2025
893a33f
add callbacks to PurchasesListener
vegaro Oct 17, 2025
6574a3a
fix missing callback
vegaro Oct 17, 2025
f9e3a4b
remove catching to prevent catching devs exceptions
vegaro Oct 17, 2025
71a8b60
log clean up
vegaro Oct 17, 2025
a18dc9b
add internal back
vegaro Oct 17, 2025
01a351e
assign instead of copy
facumenzella Oct 17, 2025
e88bb72
fix indentation
vegaro Oct 17, 2025
5c97e94
NormalizeNullString
vegaro Oct 17, 2025
6795466
Introduce CustomerCenterBehaviour.cs
facumenzella Oct 17, 2025
65f5025
private constructor
vegaro Oct 21, 2025
b14e0e5
fix );
vegaro Oct 21, 2025
9be5133
made events internal
vegaro Oct 21, 2025
6309e18
log cleanup
vegaro Oct 22, 2025
8a79fbd
fix whitespace
vegaro Oct 22, 2025
c530fc3
Revert "fix whitespace"
vegaro Oct 22, 2025
7695ad2
fix whitespace
vegaro Oct 22, 2025
49261da
replace with constants instead of enum
vegaro Oct 22, 2025
9911fdf
make CallbacksProxy internal to fix compilation
vegaro Oct 22, 2025
89cc290
Merge branch 'feat/customercenter-staticinterface' into feat/customer…
facumenzella Oct 22, 2025
ff37b06
fix properly conflict
facumenzella Oct 29, 2025
603e8da
Merge branch 'main' into feat/customercenter-monobehaviour
facumenzella Oct 29, 2025
7686ae3
Merge branch 'main' into feat/customercenter-monobehaviour
facumenzella Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 290 additions & 0 deletions RevenueCatUI/Scripts/CustomerCenterBehaviour.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
using System;
using System.Threading;
using UnityEngine;
using UnityEngine.Events;
using RevenueCat;

namespace RevenueCatUI
{
/// <summary>
/// MonoBehaviour component for presenting the RevenueCat Customer Center from the Unity Editor.
/// Mirrors <see cref="CustomerCenterPresenter"/> but exposes UnityEvents so flows can be wired
/// directly through the Inspector.
/// </summary>
[AddComponentMenu("RevenueCat/Customer Center Behaviour")]
public class CustomerCenterBehaviour : MonoBehaviour
{
[Serializable] public class StringEvent : UnityEvent<string> { }
[Serializable] public class RefundRequestCompletedEvent : UnityEvent<string, string> { }
[Serializable] public class ManagementOptionSelectedEvent : UnityEvent<string, string> { }
[Serializable] public class CustomActionSelectedEvent : UnityEvent<string, string> { }

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

/// <summary>
/// Presents the Customer Center. Can be wired to Unity UI (e.g. buttons) or invoked programmatically.
/// </summary>
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;
}
}
}
2 changes: 2 additions & 0 deletions RevenueCatUI/Scripts/CustomerCenterBehaviour.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions Subtester/Assets/Scripts/PurchasesListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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>();
purchases.SetLogLevel(Purchases.LogLevel.Verbose);
Expand Down Expand Up @@ -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();
Expand Down