From cd94337661393de452d3bc291cf91075afee4a93 Mon Sep 17 00:00:00 2001 From: Facundo Menzella Date: Wed, 8 Oct 2025 10:17:29 +0200 Subject: [PATCH 01/24] Add basic interface for customer center --- .../AndroidManifest.xml | 6 +- .../ui/CustomerCenterTrampolineActivity.java | 69 +++++++++++ .../purchasesunity/ui/RevenueCatUI.java | 24 +++- RevenueCatUI/Plugins/iOS/RevenueCatUI.m | 40 ++++++ .../CustomerCenterPlatformPresenter.cs | 47 ++++++++ .../CustomerCenterPlatformPresenter.cs.meta | 11 ++ .../Scripts/CustomerCenterPresenter.cs | 34 ++++++ .../Scripts/CustomerCenterPresenter.cs.meta | 11 ++ RevenueCatUI/Scripts/CustomerCenterResult.cs | 83 +++++++++++++ .../Scripts/CustomerCenterResult.cs.meta | 11 ++ .../Scripts/ICustomerCenterPresenter.cs | 17 +++ .../Scripts/ICustomerCenterPresenter.cs.meta | 11 ++ .../Android/AndroidCustomerCenterPresenter.cs | 114 ++++++++++++++++++ .../AndroidCustomerCenterPresenter.cs.meta | 11 ++ .../iOS/IOSCustomerCenterPresenter.cs | 62 ++++++++++ .../iOS/IOSCustomerCenterPresenter.cs.meta | 11 ++ Subtester/Assets/Scripts/PurchasesListener.cs | 69 ++++++++--- 17 files changed, 613 insertions(+), 18 deletions(-) create mode 100644 RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java create mode 100644 RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs create mode 100644 RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs.meta create mode 100644 RevenueCatUI/Scripts/CustomerCenterPresenter.cs create mode 100644 RevenueCatUI/Scripts/CustomerCenterPresenter.cs.meta create mode 100644 RevenueCatUI/Scripts/CustomerCenterResult.cs create mode 100644 RevenueCatUI/Scripts/CustomerCenterResult.cs.meta create mode 100644 RevenueCatUI/Scripts/ICustomerCenterPresenter.cs create mode 100644 RevenueCatUI/Scripts/ICustomerCenterPresenter.cs.meta create mode 100644 RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs create mode 100644 RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs.meta create mode 100644 RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs create mode 100644 RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs.meta diff --git a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/AndroidManifest.xml b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/AndroidManifest.xml index cec1e46d..762555bc 100644 --- a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/AndroidManifest.xml +++ b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/AndroidManifest.xml @@ -8,10 +8,14 @@ android:exported="false" android:theme="@android:style/Theme.Translucent.NoTitleBar" tools:node="merge" /> + - diff --git a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java new file mode 100644 index 00000000..3abb67b7 --- /dev/null +++ b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java @@ -0,0 +1,69 @@ +package com.revenuecat.purchasesunity.ui; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.activity.ComponentActivity; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; + +import com.revenuecat.purchases.Purchases; +import com.revenuecat.purchases.ui.revenuecatui.customercenter.CustomerCenterActivity; + +public class CustomerCenterTrampolineActivity extends ComponentActivity { + private static final String TAG = "PurchasesUnity"; + + private static final String RESULT_DISMISSED = "DISMISSED"; + private static final String RESULT_ERROR = "ERROR"; + + private ActivityResultLauncher launcher; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + launcher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + RevenueCatUI.sendCustomerCenterResult(RESULT_DISMISSED); + finish(); + } + ); + + if (!Purchases.isConfigured()) { + Log.e(TAG, "Purchases is not configured. Cannot launch Customer Center."); + RevenueCatUI.sendCustomerCenterResult(RESULT_ERROR); + finish(); + return; + } + + try { + Intent intent = CustomerCenterActivity.Companion.createIntent$revenuecatui_defaultsRelease(this); + launcher.launch(intent); + } catch (Throwable t) { + Log.e(TAG, "Error launching CustomerCenterActivity", t); + RevenueCatUI.sendCustomerCenterResult(RESULT_ERROR); + finish(); + } + } + + public static void presentCustomerCenter(Activity activity) { + if (activity == null) { + Log.e(TAG, "Activity is null; cannot launch Customer Center"); + RevenueCatUI.sendCustomerCenterResult(RESULT_ERROR); + return; + } + + Intent intent = new Intent(activity, CustomerCenterTrampolineActivity.class); + + try { + activity.startActivity(intent); + } catch (Throwable t) { + Log.e(TAG, "Error launching CustomerCenterTrampolineActivity", t); + RevenueCatUI.sendCustomerCenterResult(RESULT_ERROR); + } + } +} diff --git a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/RevenueCatUI.java b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/RevenueCatUI.java index 3a7ede43..72beaa31 100644 --- a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/RevenueCatUI.java +++ b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/RevenueCatUI.java @@ -5,13 +5,18 @@ public class RevenueCatUI { public interface PaywallCallbacks { void onPaywallResult(String result); } + public interface CustomerCenterCallbacks { void onCustomerCenterResult(String result); } private static final String TAG = "RevenueCatUI"; private static volatile PaywallCallbacks paywallCallbacks; + private static volatile CustomerCenterCallbacks customerCenterCallbacks; public static void registerPaywallCallbacks(PaywallCallbacks cb) { paywallCallbacks = cb; } public static void unregisterPaywallCallbacks() { paywallCallbacks = null; } + public static void registerCustomerCenterCallbacks(CustomerCenterCallbacks cb) { customerCenterCallbacks = cb; } + public static void unregisterCustomerCenterCallbacks() { customerCenterCallbacks = null; } + public static void presentPaywall(Activity activity, String offeringIdentifier, boolean displayCloseButton) { PaywallTrampolineActivity.presentPaywall(activity, offeringIdentifier, displayCloseButton); } @@ -20,6 +25,10 @@ public static void presentPaywallIfNeeded(Activity activity, String requiredEnti PaywallTrampolineActivity.presentPaywallIfNeeded(activity, requiredEntitlementIdentifier, offeringIdentifier, displayCloseButton); } + public static void presentCustomerCenter(Activity activity) { + CustomerCenterTrampolineActivity.presentCustomerCenter(activity); + } + public static void sendPaywallResult(String result) { try { PaywallCallbacks cb = paywallCallbacks; @@ -32,4 +41,17 @@ public static void sendPaywallResult(String result) { Log.e(TAG, "Error sending paywall result: " + e.getMessage()); } } -} \ No newline at end of file + + public static void sendCustomerCenterResult(String result) { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onCustomerCenterResult(result); + } else { + Log.w(TAG, "No callback registered to receive customer center result: " + result); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending customer center result: " + e.getMessage()); + } + } +} diff --git a/RevenueCatUI/Plugins/iOS/RevenueCatUI.m b/RevenueCatUI/Plugins/iOS/RevenueCatUI.m index a4970f47..48d31717 100644 --- a/RevenueCatUI/Plugins/iOS/RevenueCatUI.m +++ b/RevenueCatUI/Plugins/iOS/RevenueCatUI.m @@ -5,6 +5,7 @@ #import typedef void (*RCUIPaywallResultCallback)(const char *result); +typedef void (*RCUICustomerCenterCallback)(const char *result); static NSString *const kRCUIOptionRequiredEntitlementIdentifier = @"requiredEntitlementIdentifier"; static NSString *const kRCUIOptionOfferingIdentifier = @"offeringIdentifier"; @@ -55,6 +56,16 @@ static void RCUIInvokeCallback(RCUIPaywallResultCallback callback, NSString *tok }); } +static void RCUICustomerCenterInvokeCallback(RCUICustomerCenterCallback callback, NSString *token) { + if (callback == NULL) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + callback((token ?: @"ERROR").UTF8String); + }); +} + static BOOL RCUIEnsureReady(RCUIPaywallResultCallback callback) { if (!RCPurchases.isConfigured) { RCUIInvokeCallback(callback, @"ERROR", @"PurchasesNotConfigured"); @@ -75,6 +86,15 @@ static BOOL RCUIEnsureReady(RCUIPaywallResultCallback callback) { return options; } +static BOOL RCUICustomerCenterEnsureReady(RCUICustomerCenterCallback callback) { + if (!RCPurchases.isConfigured) { + RCUICustomerCenterInvokeCallback(callback, @"ERROR"); + return NO; + } + + return YES; +} + static void RCUIPresentPaywallInternal(NSString *offeringIdentifier, BOOL displayCloseButton, RCUIPaywallResultCallback callback) { @@ -146,3 +166,23 @@ void rcui_presentPaywallIfNeeded(const char *requiredEntitlementIdentifier, RCUIPresentPaywallIfNeededInternal(entitlement, offering, displayCloseButton ? YES : NO, callback); } + +void rcui_presentCustomerCenter(RCUICustomerCenterCallback callback) { + if (!RCUICustomerCenterEnsureReady(callback)) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (@available(iOS 15.0, *)) { + __block CustomerCenterProxy *proxy = [[CustomerCenterProxy alloc] init]; + proxy.shouldShowCloseButton = YES; + + [proxy presentWithResultHandler:^{ + RCUICustomerCenterInvokeCallback(callback, @"DISMISSED"); + proxy = nil; + }]; + } else { + RCUICustomerCenterInvokeCallback(callback, @"NOT_PRESENTED"); + } + }); +} diff --git a/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs b/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs new file mode 100644 index 00000000..6b21a304 --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using UnityEngine; +using RevenueCatUI; + +namespace RevenueCatUI.Internal +{ + /// + /// Factory responsible for providing the platform-specific implementation that presents the Customer Center UI. + /// + internal static class CustomerCenterPlatformPresenter + { + private static ICustomerCenterPresenter _instance; + + internal static ICustomerCenterPresenter Instance + { + get + { + if (_instance == null) + { + _instance = CreatePlatformPresenter(); + } + + return _instance; + } + } + + private static ICustomerCenterPresenter CreatePlatformPresenter() + { +#if UNITY_IOS && !UNITY_EDITOR + return new Platforms.IOSCustomerCenterPresenter(); +#elif UNITY_ANDROID && !UNITY_EDITOR + return new Platforms.AndroidCustomerCenterPresenter(); +#else + return new UnsupportedCustomerCenterPresenter(); +#endif + } + } + + internal class UnsupportedCustomerCenterPresenter : ICustomerCenterPresenter + { + public Task PresentAsync() + { + Debug.LogWarning("[RevenueCatUI] Customer Center presentation is not supported on this platform."); + return Task.FromResult(CustomerCenterResult.NotPresented); + } + } +} diff --git a/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs.meta b/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs.meta new file mode 100644 index 00000000..37836dcd --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 356fa6181e654762a95a0a555372427f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RevenueCatUI/Scripts/CustomerCenterPresenter.cs b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs new file mode 100644 index 00000000..e04d2339 --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using UnityEngine; +using RevenueCatUI.Internal; + +namespace RevenueCatUI +{ + /// + /// Main interface for presenting the RevenueCat Customer Center experience. + /// + public static class CustomerCenterPresenter + { + /// + /// Presents the Customer Center UI modally. + /// + /// A task describing the outcome of the presentation. + public static async Task Present() + { + try + { + Debug.Log("[RevenueCatUI] Presenting Customer Center..."); + var presenter = CustomerCenterPlatformPresenter.Instance; + var result = await presenter.PresentAsync(); + Debug.Log($"[RevenueCatUI] Customer Center finished with result: {result.Result}"); + return result; + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI] Error presenting Customer Center: {e.Message}"); + return CustomerCenterResult.Error; + } + } + } +} diff --git a/RevenueCatUI/Scripts/CustomerCenterPresenter.cs.meta b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs.meta new file mode 100644 index 00000000..04aecb2a --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aad9315345344f3791b232bd0d92df6e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RevenueCatUI/Scripts/CustomerCenterResult.cs b/RevenueCatUI/Scripts/CustomerCenterResult.cs new file mode 100644 index 00000000..22e34a35 --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterResult.cs @@ -0,0 +1,83 @@ +using System; + +namespace RevenueCatUI +{ + /// + /// Represents the outcome of a Customer Center presentation. + /// + [Serializable] + public class CustomerCenterResult + { + /// + /// The result type returned by the native SDKs. + /// + public CustomerCenterResultType Result { get; } + + /// + /// Creates a new CustomerCenterResult. + /// + /// The result type. + public CustomerCenterResult(CustomerCenterResultType result) + { + Result = result; + } + + internal static CustomerCenterResult Dismissed => new CustomerCenterResult(CustomerCenterResultType.Dismissed); + internal static CustomerCenterResult NotPresented => new CustomerCenterResult(CustomerCenterResultType.NotPresented); + internal static CustomerCenterResult Error => new CustomerCenterResult(CustomerCenterResultType.Error); + + public override string ToString() + { + return $"CustomerCenterResult({Result})"; + } + } + + /// + /// Enum describing the possible outcomes of a Customer Center presentation. + /// + public enum CustomerCenterResultType + { + /// + /// The Customer Center was presented and then dismissed. + /// + Dismissed, + + /// + /// The Customer Center could not be presented (e.g., unavailable on platform). + /// + NotPresented, + + /// + /// An error occurred while attempting to present the Customer Center. + /// + Error + } + + /// + /// Helpers to convert CustomerCenterResultType values to the native string representations. + /// + public static class CustomerCenterResultTypeExtensions + { + public static string ToNativeString(this CustomerCenterResultType resultType) + { + return resultType switch + { + CustomerCenterResultType.Dismissed => "DISMISSED", + CustomerCenterResultType.NotPresented => "NOT_PRESENTED", + CustomerCenterResultType.Error => "ERROR", + _ => "ERROR" + }; + } + + public static CustomerCenterResultType FromNativeString(string nativeResult) + { + return nativeResult switch + { + "DISMISSED" => CustomerCenterResultType.Dismissed, + "NOT_PRESENTED" => CustomerCenterResultType.NotPresented, + "ERROR" => CustomerCenterResultType.Error, + _ => CustomerCenterResultType.Error + }; + } + } +} diff --git a/RevenueCatUI/Scripts/CustomerCenterResult.cs.meta b/RevenueCatUI/Scripts/CustomerCenterResult.cs.meta new file mode 100644 index 00000000..2de3c1f1 --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d61083d86deb49c1b3c6ea847d18c832 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs b/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs new file mode 100644 index 00000000..063d6062 --- /dev/null +++ b/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using RevenueCatUI; + +namespace RevenueCatUI.Internal +{ + /// + /// Platform abstraction for presenting the RevenueCat Customer Center experience. + /// + internal interface ICustomerCenterPresenter + { + /// + /// Presents the Customer Center UI modally. + /// + /// A task describing the outcome of the presentation. + Task PresentAsync(); + } +} diff --git a/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs.meta b/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs.meta new file mode 100644 index 00000000..bc065602 --- /dev/null +++ b/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2738e3a6602f4d7fbb1674825a0c6ed5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs new file mode 100644 index 00000000..f8dc924a --- /dev/null +++ b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs @@ -0,0 +1,114 @@ +#if UNITY_ANDROID && !UNITY_EDITOR +using System; +using System.Threading.Tasks; +using RevenueCatUI.Internal; +using UnityEngine; +using UnityEngine.Android; + +namespace RevenueCatUI.Platforms +{ + internal class AndroidCustomerCenterPresenter : ICustomerCenterPresenter + { + private readonly AndroidJavaClass _plugin; + private readonly CallbacksProxy _callbacks; + private TaskCompletionSource _current; + + public AndroidCustomerCenterPresenter() + { + try + { + _plugin = new AndroidJavaClass("com.revenuecat.purchasesunity.ui.RevenueCatUI"); + _callbacks = new CallbacksProxy(this); + _plugin.CallStatic("registerCustomerCenterCallbacks", _callbacks); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Failed to initialize RevenueCatUI plugin for Customer Center: {e.Message}"); + } + } + + ~AndroidCustomerCenterPresenter() + { + try { _plugin?.CallStatic("unregisterCustomerCenterCallbacks"); } catch { } + } + + public Task PresentAsync() + { + if (_plugin == null) + { + Debug.LogError("[RevenueCatUI][Android] Plugin not initialized. Cannot present Customer Center."); + return Task.FromResult(CustomerCenterResult.Error); + } + + if (_current != null && !_current.Task.IsCompleted) + { + Debug.LogWarning("[RevenueCatUI][Android] Customer Center presentation already in progress; rejecting new request."); + return _current.Task; + } + + var currentActivity = AndroidApplication.currentActivity; + if (currentActivity == null) + { + Debug.LogError("[RevenueCatUI][Android] Current activity is null. Cannot present Customer Center."); + return Task.FromResult(CustomerCenterResult.Error); + } + + _current = new TaskCompletionSource(); + try + { + _plugin.CallStatic("presentCustomerCenter", currentActivity); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Exception in presentCustomerCenter: {e.Message}"); + _current.TrySetResult(CustomerCenterResult.Error); + var task = _current.Task; + _current = null; + return task; + } + + return _current.Task; + } + + private void OnCustomerCenterResult(string resultData) + { + if (_current == null) + { + Debug.LogWarning("[RevenueCatUI][Android] Received Customer Center result with no pending request."); + return; + } + + try + { + var token = resultData?.Split('|')[0] ?? "ERROR"; + var type = CustomerCenterResultTypeExtensions.FromNativeString(token); + _current.TrySetResult(new CustomerCenterResult(type)); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Failed to handle Customer Center result '{resultData}': {e.Message}. Setting Error."); + _current.TrySetResult(CustomerCenterResult.Error); + } + finally + { + _current = null; + } + } + + private class CallbacksProxy : AndroidJavaProxy + { + private readonly AndroidCustomerCenterPresenter _owner; + + public CallbacksProxy(AndroidCustomerCenterPresenter owner) : base("com.revenuecat.purchasesunity.ui.RevenueCatUI$CustomerCenterCallbacks") + { + _owner = owner; + } + + public void onCustomerCenterResult(string result) + { + _owner.OnCustomerCenterResult(result); + } + } + } +} +#endif diff --git a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs.meta b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs.meta new file mode 100644 index 00000000..1c56667e --- /dev/null +++ b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dcf7429739f94defb8f03b399749c5f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs new file mode 100644 index 00000000..3b0cf8aa --- /dev/null +++ b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs @@ -0,0 +1,62 @@ +#if UNITY_IOS && !UNITY_EDITOR +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using RevenueCatUI.Internal; + +namespace RevenueCatUI.Platforms +{ + internal class IOSCustomerCenterPresenter : ICustomerCenterPresenter + { + private delegate void CustomerCenterResultCallback(string result); + + [DllImport("__Internal")] private static extern void rcui_presentCustomerCenter(CustomerCenterResultCallback cb); + + private static TaskCompletionSource s_current; + + public Task PresentAsync() + { + if (s_current != null && !s_current.Task.IsCompleted) + { + UnityEngine.Debug.LogWarning("[RevenueCatUI][iOS] Customer Center presentation already in progress; rejecting new request."); + return s_current.Task; + } + + var tcs = new TaskCompletionSource(); + s_current = tcs; + try + { + rcui_presentCustomerCenter(OnCompleted); + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"[RevenueCatUI][iOS] Exception in presentCustomerCenter: {e.Message}"); + tcs.TrySetResult(CustomerCenterResult.Error); + s_current = null; + } + return tcs.Task; + } + + [AOT.MonoPInvokeCallback(typeof(CustomerCenterResultCallback))] + private static void OnCompleted(string result) + { + try + { + var token = (result ?? "ERROR"); + var native = token.Split('|')[0]; + var type = CustomerCenterResultTypeExtensions.FromNativeString(native); + s_current?.TrySetResult(new CustomerCenterResult(type)); + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"[RevenueCatUI][iOS] Failed to handle Customer Center completion: {e.Message}"); + s_current?.TrySetResult(CustomerCenterResult.Error); + } + finally + { + s_current = null; + } + } + } +} +#endif diff --git a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs.meta b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs.meta new file mode 100644 index 00000000..cf2f795c --- /dev/null +++ b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dc5dd9c2a3fd49278b7c8471d4618513 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Subtester/Assets/Scripts/PurchasesListener.cs b/Subtester/Assets/Scripts/PurchasesListener.cs index dd7b9b95..73238389 100644 --- a/Subtester/Assets/Scripts/PurchasesListener.cs +++ b/Subtester/Assets/Scripts/PurchasesListener.cs @@ -69,7 +69,8 @@ private void Start() CreateButton("Present Paywall", PresentPaywallResult); CreateButton("Present Paywall with Options", PresentPaywallWithOptions); CreateButton("Present Paywall for Offering", PresentPaywallForOffering); - CreateButton("Present Paywall If Needed", PresentPaywallIfNeeded); + CreateButton("Present Paywall If Needed", PresentPaywallIfNeeded); + CreateButton("Present Customer Center", PresentCustomerCenter); var purchases = GetComponent(); purchases.SetLogLevel(Purchases.LogLevel.Verbose); @@ -221,13 +222,20 @@ void PresentPaywallForOffering() StartCoroutine(PresentPaywallForOfferingCoroutine()); } - void PresentPaywallIfNeeded() - { - Debug.Log("Subtester: launching paywall if needed for test entitlement"); - if (infoLabel != null) infoLabel.text = "Checking entitlement and launching paywall if needed..."; - StartCoroutine(PresentPaywallIfNeededCoroutine()); - } - + void PresentPaywallIfNeeded() + { + Debug.Log("Subtester: launching paywall if needed for test entitlement"); + if (infoLabel != null) infoLabel.text = "Checking entitlement and launching paywall if needed..."; + StartCoroutine(PresentPaywallIfNeededCoroutine()); + } + + void PresentCustomerCenter() + { + Debug.Log("Subtester: launching customer center"); + if (infoLabel != null) infoLabel.text = "Launching Customer Center..."; + StartCoroutine(PresentCustomerCenterCoroutine()); + } + private System.Collections.IEnumerator PresentPaywallCoroutine() { var task = RevenueCatUI.PaywallsPresenter.Present(); @@ -258,9 +266,23 @@ private System.Collections.IEnumerator PresentPaywallCoroutine() infoLabel.text = $"Paywall result: {status}"; Debug.Log($"Subtester: {status}"); - } - } - + } + } + + private System.Collections.IEnumerator PresentCustomerCenterCoroutine() + { + var task = RevenueCatUI.CustomerCenterPresenter.Present(); + while (!task.IsCompleted) { yield return null; } + + var result = task.Result; + Debug.Log("Subtester: customer center result = " + result); + + if (infoLabel != null) + { + infoLabel.text = $"Customer Center result: {GetCustomerCenterResultStatus(result)}"; + } + } + private System.Collections.IEnumerator PresentPaywallWithOptionsCoroutine() { var options = new RevenueCatUI.PaywallOptions @@ -419,11 +441,26 @@ private System.Collections.IEnumerator PresentPaywallIfNeededCoroutine() } infoLabel.text = message; } - } - - private string GetPaywallResultStatus(RevenueCatUI.PaywallResult result) - { - switch (result.Result) + } + + private string GetCustomerCenterResultStatus(RevenueCatUI.CustomerCenterResult result) + { + switch (result.Result) + { + case RevenueCatUI.CustomerCenterResultType.Dismissed: + return "DISMISSED - Customer Center closed"; + case RevenueCatUI.CustomerCenterResultType.NotPresented: + return "NOT PRESENTED - Customer Center unavailable"; + case RevenueCatUI.CustomerCenterResultType.Error: + return "ERROR - Customer Center failed to present"; + default: + return $"UNKNOWN - Received: {result}"; + } + } + + private string GetPaywallResultStatus(RevenueCatUI.PaywallResult result) + { + switch (result.Result) { case RevenueCatUI.PaywallResultType.Purchased: return "PURCHASED - User completed a purchase"; From 48a35fe0eaeb1721734952d650b53a62d3a311a3 Mon Sep 17 00:00:00 2001 From: Facundo Menzella Date: Fri, 10 Oct 2025 14:54:32 +0200 Subject: [PATCH 02/24] update deps --- RevenueCatUI/Plugins/Editor/RevenueCatUIDependencies.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/Plugins/Editor/RevenueCatUIDependencies.xml b/RevenueCatUI/Plugins/Editor/RevenueCatUIDependencies.xml index c2b6020d..3883b085 100644 --- a/RevenueCatUI/Plugins/Editor/RevenueCatUIDependencies.xml +++ b/RevenueCatUI/Plugins/Editor/RevenueCatUIDependencies.xml @@ -2,11 +2,11 @@ // TODO: Automatically update this to the latest version - + - + \ No newline at end of file From 6c90d5bbdd0f4a438d976adf39c2601de889b286 Mon Sep 17 00:00:00 2001 From: Facundo Menzella Date: Wed, 15 Oct 2025 14:55:36 +0200 Subject: [PATCH 03/24] Add empty CustomerCenterCallbacks --- RevenueCatUI/Scripts/CustomerCenterCallbacks.cs | 16 ++++++++++++++++ .../Scripts/CustomerCenterPlatformPresenter.cs | 2 +- RevenueCatUI/Scripts/CustomerCenterPresenter.cs | 5 +++-- RevenueCatUI/Scripts/ICustomerCenterPresenter.cs | 3 ++- .../Android/AndroidCustomerCenterPresenter.cs | 2 +- .../Platforms/iOS/IOSCustomerCenterPresenter.cs | 2 +- 6 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 RevenueCatUI/Scripts/CustomerCenterCallbacks.cs diff --git a/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs b/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs new file mode 100644 index 00000000..327d907d --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs @@ -0,0 +1,16 @@ +namespace RevenueCatUI +{ + /// + /// Placeholder container for upcoming Customer Center callback support. + /// + public sealed class CustomerCenterCallbacks + { + internal static readonly CustomerCenterCallbacks None = new CustomerCenterCallbacks(); + + // Callbacks will be added in a future PR; keeping this empty confirms the + // new API surface compiles across all platforms without behavior changes. + public CustomerCenterCallbacks() + { + } + } +} diff --git a/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs b/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs index 6b21a304..512cc0cd 100644 --- a/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs +++ b/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs @@ -38,7 +38,7 @@ private static ICustomerCenterPresenter CreatePlatformPresenter() internal class UnsupportedCustomerCenterPresenter : ICustomerCenterPresenter { - public Task PresentAsync() + public Task PresentAsync(CustomerCenterCallbacks callbacks) { Debug.LogWarning("[RevenueCatUI] Customer Center presentation is not supported on this platform."); return Task.FromResult(CustomerCenterResult.NotPresented); diff --git a/RevenueCatUI/Scripts/CustomerCenterPresenter.cs b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs index e04d2339..73da74e8 100644 --- a/RevenueCatUI/Scripts/CustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs @@ -13,14 +13,15 @@ public static class CustomerCenterPresenter /// /// Presents the Customer Center UI modally. /// + /// Placeholder for future callback support. /// A task describing the outcome of the presentation. - public static async Task Present() + public static async Task Present(CustomerCenterCallbacks callbacks = null) { try { Debug.Log("[RevenueCatUI] Presenting Customer Center..."); var presenter = CustomerCenterPlatformPresenter.Instance; - var result = await presenter.PresentAsync(); + var result = await presenter.PresentAsync(callbacks ?? CustomerCenterCallbacks.None); Debug.Log($"[RevenueCatUI] Customer Center finished with result: {result.Result}"); return result; } diff --git a/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs b/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs index 063d6062..f6d1bc62 100644 --- a/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs @@ -11,7 +11,8 @@ internal interface ICustomerCenterPresenter /// /// Presents the Customer Center UI modally. /// + /// Callback container reserved for future expansion. /// A task describing the outcome of the presentation. - Task PresentAsync(); + Task PresentAsync(CustomerCenterCallbacks callbacks); } } diff --git a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs index f8dc924a..8902e229 100644 --- a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs @@ -32,7 +32,7 @@ public AndroidCustomerCenterPresenter() try { _plugin?.CallStatic("unregisterCustomerCenterCallbacks"); } catch { } } - public Task PresentAsync() + public Task PresentAsync(CustomerCenterCallbacks callbacks) { if (_plugin == null) { diff --git a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs index 3b0cf8aa..7684c573 100644 --- a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs @@ -14,7 +14,7 @@ internal class IOSCustomerCenterPresenter : ICustomerCenterPresenter private static TaskCompletionSource s_current; - public Task PresentAsync() + public Task PresentAsync(CustomerCenterCallbacks callbacks) { if (s_current != null && !s_current.Task.IsCompleted) { From 8ad87e1671ff8286fdd0f7377999885d4dc158a2 Mon Sep 17 00:00:00 2001 From: Facundo Menzella Date: Thu, 16 Oct 2025 10:05:14 +0200 Subject: [PATCH 04/24] Remove result --- .../CustomerCenterPlatformPresenter.cs | 4 +- .../Scripts/CustomerCenterPresenter.cs | 10 +-- RevenueCatUI/Scripts/CustomerCenterResult.cs | 83 ------------------- .../Scripts/ICustomerCenterPresenter.cs | 4 +- .../Android/AndroidCustomerCenterPresenter.cs | 20 ++--- .../iOS/IOSCustomerCenterPresenter.cs | 15 ++-- 6 files changed, 25 insertions(+), 111 deletions(-) delete mode 100644 RevenueCatUI/Scripts/CustomerCenterResult.cs diff --git a/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs b/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs index 512cc0cd..4aad608f 100644 --- a/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs +++ b/RevenueCatUI/Scripts/CustomerCenterPlatformPresenter.cs @@ -38,10 +38,10 @@ private static ICustomerCenterPresenter CreatePlatformPresenter() internal class UnsupportedCustomerCenterPresenter : ICustomerCenterPresenter { - public Task PresentAsync(CustomerCenterCallbacks callbacks) + public Task PresentAsync(CustomerCenterCallbacks callbacks) { Debug.LogWarning("[RevenueCatUI] Customer Center presentation is not supported on this platform."); - return Task.FromResult(CustomerCenterResult.NotPresented); + return Task.CompletedTask; } } } diff --git a/RevenueCatUI/Scripts/CustomerCenterPresenter.cs b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs index 73da74e8..16c990b5 100644 --- a/RevenueCatUI/Scripts/CustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs @@ -14,21 +14,19 @@ public static class CustomerCenterPresenter /// Presents the Customer Center UI modally. /// /// Placeholder for future callback support. - /// A task describing the outcome of the presentation. - public static async Task Present(CustomerCenterCallbacks callbacks = null) + /// A task that completes when the presentation ends. + public static async Task Present(CustomerCenterCallbacks callbacks = null) { try { Debug.Log("[RevenueCatUI] Presenting Customer Center..."); var presenter = CustomerCenterPlatformPresenter.Instance; - var result = await presenter.PresentAsync(callbacks ?? CustomerCenterCallbacks.None); - Debug.Log($"[RevenueCatUI] Customer Center finished with result: {result.Result}"); - return result; + await presenter.PresentAsync(callbacks ?? CustomerCenterCallbacks.None); + Debug.Log("[RevenueCatUI] Customer Center finished."); } catch (Exception e) { Debug.LogError($"[RevenueCatUI] Error presenting Customer Center: {e.Message}"); - return CustomerCenterResult.Error; } } } diff --git a/RevenueCatUI/Scripts/CustomerCenterResult.cs b/RevenueCatUI/Scripts/CustomerCenterResult.cs deleted file mode 100644 index 22e34a35..00000000 --- a/RevenueCatUI/Scripts/CustomerCenterResult.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; - -namespace RevenueCatUI -{ - /// - /// Represents the outcome of a Customer Center presentation. - /// - [Serializable] - public class CustomerCenterResult - { - /// - /// The result type returned by the native SDKs. - /// - public CustomerCenterResultType Result { get; } - - /// - /// Creates a new CustomerCenterResult. - /// - /// The result type. - public CustomerCenterResult(CustomerCenterResultType result) - { - Result = result; - } - - internal static CustomerCenterResult Dismissed => new CustomerCenterResult(CustomerCenterResultType.Dismissed); - internal static CustomerCenterResult NotPresented => new CustomerCenterResult(CustomerCenterResultType.NotPresented); - internal static CustomerCenterResult Error => new CustomerCenterResult(CustomerCenterResultType.Error); - - public override string ToString() - { - return $"CustomerCenterResult({Result})"; - } - } - - /// - /// Enum describing the possible outcomes of a Customer Center presentation. - /// - public enum CustomerCenterResultType - { - /// - /// The Customer Center was presented and then dismissed. - /// - Dismissed, - - /// - /// The Customer Center could not be presented (e.g., unavailable on platform). - /// - NotPresented, - - /// - /// An error occurred while attempting to present the Customer Center. - /// - Error - } - - /// - /// Helpers to convert CustomerCenterResultType values to the native string representations. - /// - public static class CustomerCenterResultTypeExtensions - { - public static string ToNativeString(this CustomerCenterResultType resultType) - { - return resultType switch - { - CustomerCenterResultType.Dismissed => "DISMISSED", - CustomerCenterResultType.NotPresented => "NOT_PRESENTED", - CustomerCenterResultType.Error => "ERROR", - _ => "ERROR" - }; - } - - public static CustomerCenterResultType FromNativeString(string nativeResult) - { - return nativeResult switch - { - "DISMISSED" => CustomerCenterResultType.Dismissed, - "NOT_PRESENTED" => CustomerCenterResultType.NotPresented, - "ERROR" => CustomerCenterResultType.Error, - _ => CustomerCenterResultType.Error - }; - } - } -} diff --git a/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs b/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs index f6d1bc62..2938ce97 100644 --- a/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/ICustomerCenterPresenter.cs @@ -12,7 +12,7 @@ internal interface ICustomerCenterPresenter /// Presents the Customer Center UI modally. /// /// Callback container reserved for future expansion. - /// A task describing the outcome of the presentation. - Task PresentAsync(CustomerCenterCallbacks callbacks); + /// A task that completes when the presentation finishes. + Task PresentAsync(CustomerCenterCallbacks callbacks); } } diff --git a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs index 8902e229..01c3137f 100644 --- a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs @@ -11,7 +11,7 @@ internal class AndroidCustomerCenterPresenter : ICustomerCenterPresenter { private readonly AndroidJavaClass _plugin; private readonly CallbacksProxy _callbacks; - private TaskCompletionSource _current; + private TaskCompletionSource _current; public AndroidCustomerCenterPresenter() { @@ -32,12 +32,12 @@ public AndroidCustomerCenterPresenter() try { _plugin?.CallStatic("unregisterCustomerCenterCallbacks"); } catch { } } - public Task PresentAsync(CustomerCenterCallbacks callbacks) + public Task PresentAsync(CustomerCenterCallbacks callbacks) { if (_plugin == null) { Debug.LogError("[RevenueCatUI][Android] Plugin not initialized. Cannot present Customer Center."); - return Task.FromResult(CustomerCenterResult.Error); + return Task.CompletedTask; } if (_current != null && !_current.Task.IsCompleted) @@ -50,10 +50,10 @@ public Task PresentAsync(CustomerCenterCallbacks callbacks if (currentActivity == null) { Debug.LogError("[RevenueCatUI][Android] Current activity is null. Cannot present Customer Center."); - return Task.FromResult(CustomerCenterResult.Error); + return Task.CompletedTask; } - _current = new TaskCompletionSource(); + _current = new TaskCompletionSource(); try { _plugin.CallStatic("presentCustomerCenter", currentActivity); @@ -61,7 +61,7 @@ public Task PresentAsync(CustomerCenterCallbacks callbacks catch (Exception e) { Debug.LogError($"[RevenueCatUI][Android] Exception in presentCustomerCenter: {e.Message}"); - _current.TrySetResult(CustomerCenterResult.Error); + _current.TrySetResult(false); var task = _current.Task; _current = null; return task; @@ -80,14 +80,14 @@ private void OnCustomerCenterResult(string resultData) try { - var token = resultData?.Split('|')[0] ?? "ERROR"; - var type = CustomerCenterResultTypeExtensions.FromNativeString(token); - _current.TrySetResult(new CustomerCenterResult(type)); + var token = resultData ?? "ERROR"; + Debug.Log($"[RevenueCatUI][Android] Customer Center completed with token '{token}'."); + _current.TrySetResult(true); } catch (Exception e) { Debug.LogError($"[RevenueCatUI][Android] Failed to handle Customer Center result '{resultData}': {e.Message}. Setting Error."); - _current.TrySetResult(CustomerCenterResult.Error); + _current.TrySetResult(false); } finally { diff --git a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs index 7684c573..4c430d5a 100644 --- a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs @@ -12,9 +12,9 @@ internal class IOSCustomerCenterPresenter : ICustomerCenterPresenter [DllImport("__Internal")] private static extern void rcui_presentCustomerCenter(CustomerCenterResultCallback cb); - private static TaskCompletionSource s_current; + private static TaskCompletionSource s_current; - public Task PresentAsync(CustomerCenterCallbacks callbacks) + public Task PresentAsync(CustomerCenterCallbacks callbacks) { if (s_current != null && !s_current.Task.IsCompleted) { @@ -22,7 +22,7 @@ public Task PresentAsync(CustomerCenterCallbacks callbacks return s_current.Task; } - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); s_current = tcs; try { @@ -31,7 +31,7 @@ public Task PresentAsync(CustomerCenterCallbacks callbacks catch (Exception e) { UnityEngine.Debug.LogError($"[RevenueCatUI][iOS] Exception in presentCustomerCenter: {e.Message}"); - tcs.TrySetResult(CustomerCenterResult.Error); + tcs.TrySetResult(false); s_current = null; } return tcs.Task; @@ -43,14 +43,13 @@ private static void OnCompleted(string result) try { var token = (result ?? "ERROR"); - var native = token.Split('|')[0]; - var type = CustomerCenterResultTypeExtensions.FromNativeString(native); - s_current?.TrySetResult(new CustomerCenterResult(type)); + UnityEngine.Debug.Log($"[RevenueCatUI][iOS] Customer Center completed with token '{token}'."); + s_current?.TrySetResult(true); } catch (Exception e) { UnityEngine.Debug.LogError($"[RevenueCatUI][iOS] Failed to handle Customer Center completion: {e.Message}"); - s_current?.TrySetResult(CustomerCenterResult.Error); + s_current?.TrySetResult(false); } finally { From 388aa648a32cc998ecf2d76b3c4bc16a3a53092e Mon Sep 17 00:00:00 2001 From: Facundo Menzella Date: Thu, 16 Oct 2025 10:57:43 +0200 Subject: [PATCH 05/24] remove extras --- RevenueCatUI/Scripts/CustomerCenterCallbacks.cs.meta | 2 ++ RevenueCatUI/Scripts/CustomerCenterResult.cs.meta | 11 ----------- 2 files changed, 2 insertions(+), 11 deletions(-) create mode 100644 RevenueCatUI/Scripts/CustomerCenterCallbacks.cs.meta delete mode 100644 RevenueCatUI/Scripts/CustomerCenterResult.cs.meta diff --git a/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs.meta b/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs.meta new file mode 100644 index 00000000..b2028c0d --- /dev/null +++ b/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 47d53514a5e1b4837a14f7e67e935fa9 \ No newline at end of file diff --git a/RevenueCatUI/Scripts/CustomerCenterResult.cs.meta b/RevenueCatUI/Scripts/CustomerCenterResult.cs.meta deleted file mode 100644 index 2de3c1f1..00000000 --- a/RevenueCatUI/Scripts/CustomerCenterResult.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d61083d86deb49c1b3c6ea847d18c832 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: From 9c01e8d564009d5e231750477a5b8b08ce520897 Mon Sep 17 00:00:00 2001 From: Facundo Menzella Date: Thu, 16 Oct 2025 11:01:42 +0200 Subject: [PATCH 06/24] push missing piece --- Subtester/Assets/Scripts/PurchasesListener.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/Subtester/Assets/Scripts/PurchasesListener.cs b/Subtester/Assets/Scripts/PurchasesListener.cs index 4a0531bb..0038cdc4 100644 --- a/Subtester/Assets/Scripts/PurchasesListener.cs +++ b/Subtester/Assets/Scripts/PurchasesListener.cs @@ -274,12 +274,11 @@ private System.Collections.IEnumerator PresentCustomerCenterCoroutine() var task = RevenueCatUI.CustomerCenterPresenter.Present(); while (!task.IsCompleted) { yield return null; } - var result = task.Result; - Debug.Log("Subtester: customer center result = " + result); + Debug.Log("Subtester: customer center finished."); if (infoLabel != null) { - infoLabel.text = $"Customer Center result: {GetCustomerCenterResultStatus(result)}"; + infoLabel.text = "Customer Center finished (no callbacks yet)"; } } @@ -431,21 +430,6 @@ private System.Collections.IEnumerator PresentPaywallIfNeededCoroutine() } } - private string GetCustomerCenterResultStatus(RevenueCatUI.CustomerCenterResult result) - { - switch (result.Result) - { - case RevenueCatUI.CustomerCenterResultType.Dismissed: - return "DISMISSED - Customer Center closed"; - case RevenueCatUI.CustomerCenterResultType.NotPresented: - return "NOT PRESENTED - Customer Center unavailable"; - case RevenueCatUI.CustomerCenterResultType.Error: - return "ERROR - Customer Center failed to present"; - default: - return $"UNKNOWN - Received: {result}"; - } - } - private string GetPaywallResultStatus(RevenueCatUI.PaywallResult result) { switch (result.Result) From 15bc076b20c6a3eedd6781e038685021d21b3e6b Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:20:16 +0200 Subject: [PATCH 07/24] callbacks --- .../ui/CustomerCenterTrampolineActivity.java | 81 ++++- .../purchasesunity/ui/RevenueCatUI.java | 137 ++++++++- RevenueCatUI/Plugins/iOS/RevenueCatUI.m | 169 ++++++++++- .../Scripts/CustomerCenterCallbacks.cs | 150 +++++++++- .../Scripts/CustomerCenterPresenter.cs | 17 +- .../Android/AndroidCustomerCenterPresenter.cs | 280 ++++++++++++++++-- .../iOS/IOSCustomerCenterPresenter.cs | 211 ++++++++++++- 7 files changed, 985 insertions(+), 60 deletions(-) diff --git a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java index 37e5b3b9..0cf24495 100644 --- a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java +++ b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java @@ -10,17 +10,20 @@ import androidx.annotation.Nullable; import com.revenuecat.purchases.Purchases; +import com.revenuecat.purchases.customercenter.CustomerCenterListener; +import com.revenuecat.purchases.hybridcommon.mappers.MappersHelpersKt; +import com.revenuecat.purchases.hybridcommon.ui.CustomerCenterListenerWrapper; import com.revenuecat.purchases.ui.revenuecatui.customercenter.ShowCustomerCenter; +import java.util.Map; + import kotlin.Unit; public class CustomerCenterTrampolineActivity extends ComponentActivity { private static final String TAG = "PurchasesUnity"; - private static final String RESULT_DISMISSED = "DISMISSED"; - private static final String RESULT_ERROR = "ERROR"; - private ActivityResultLauncher launcher; + private CustomerCenterListener customerCenterListener; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -29,31 +32,93 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { launcher = registerForActivityResult( new ShowCustomerCenter(), ignored -> { - RevenueCatUI.sendCustomerCenterResult(RESULT_DISMISSED); + RevenueCatUI.sendCustomerCenterDismissed(); finish(); } ); if (!Purchases.isConfigured()) { Log.e(TAG, "Purchases is not configured. Cannot launch Customer Center."); - RevenueCatUI.sendCustomerCenterResult(RESULT_ERROR); + RevenueCatUI.sendCustomerCenterError(); finish(); return; } + customerCenterListener = createCustomerCenterListener(); + Purchases.getSharedInstance().setCustomerCenterListener(customerCenterListener); + try { launcher.launch(Unit.INSTANCE); } catch (Throwable t) { Log.e(TAG, "Error launching CustomerCenterActivity", t); - RevenueCatUI.sendCustomerCenterResult(RESULT_ERROR); + RevenueCatUI.sendCustomerCenterError(); finish(); } } + @Override + protected void onDestroy() { + super.onDestroy(); + if (Purchases.isConfigured()) { + Purchases.getSharedInstance().setCustomerCenterListener(null); + } + } + + private CustomerCenterListener createCustomerCenterListener() { + return new CustomerCenterListenerWrapper() { + @Override + public void onFeedbackSurveyCompletedWrapper(@Nullable String feedbackSurveyOptionId) { + if (feedbackSurveyOptionId != null) { + RevenueCatUI.sendFeedbackSurveyCompleted(feedbackSurveyOptionId); + } + } + + @Override + public void onManagementOptionSelectedWrapper(@Nullable String action, @Nullable String url) { + if (action != null) { + RevenueCatUI.sendManagementOptionSelected(action, url); + } + } + + @Override + public void onCustomActionSelectedWrapper(@Nullable String actionId, @Nullable String purchaseIdentifier) { + if (actionId != null) { + RevenueCatUI.sendCustomActionSelected(actionId, purchaseIdentifier); + } + } + + @Override + public void onShowingManageSubscriptionsWrapper() { + RevenueCatUI.sendShowingManageSubscriptions(); + } + + @Override + public void onRestoreCompletedWrapper(@Nullable Map customerInfo) { + if (customerInfo != null) { + String customerInfoJson = MappersHelpersKt.convertToJson(customerInfo).toString(); + RevenueCatUI.sendRestoreCompleted(customerInfoJson); + } + } + + @Override + public void onRestoreFailedWrapper(@Nullable Map error) { + if (error != null) { + String errorJson = MappersHelpersKt.convertToJson(error).toString(); + RevenueCatUI.sendRestoreFailed(errorJson); + } + } + + @Override + public void onRestoreStartedWrapper() { + RevenueCatUI.sendRestoreStarted(); + } + }; + } + public static void presentCustomerCenter(Activity activity) { if (activity == null) { Log.e(TAG, "Activity is null; cannot launch Customer Center"); - RevenueCatUI.sendCustomerCenterResult(RESULT_ERROR); + RevenueCatUI.sendCustomerCenterError(); return; } @@ -63,7 +128,7 @@ public static void presentCustomerCenter(Activity activity) { activity.startActivity(intent); } catch (Throwable t) { Log.e(TAG, "Error launching CustomerCenterTrampolineActivity", t); - RevenueCatUI.sendCustomerCenterResult(RESULT_ERROR); + RevenueCatUI.sendCustomerCenterError(); } } } diff --git a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/RevenueCatUI.java b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/RevenueCatUI.java index bae6d51d..ac44ced9 100644 --- a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/RevenueCatUI.java +++ b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/RevenueCatUI.java @@ -3,9 +3,24 @@ import android.app.Activity; import android.util.Log; +import androidx.annotation.Nullable; + public class RevenueCatUI { public interface PaywallCallbacks { void onPaywallResult(String result); } - public interface CustomerCenterCallbacks { void onCustomerCenterResult(String result); } + + public interface CustomerCenterCallbacks { + void onCustomerCenterDismissed(); + void onCustomerCenterError(); + void onFeedbackSurveyCompleted(String feedbackSurveyOptionId); + void onShowingManageSubscriptions(); + void onRestoreCompleted(String customerInfoJson); + void onRestoreFailed(String errorJson); + void onRestoreStarted(); + void onRefundRequestStarted(String productIdentifier); + void onRefundRequestCompleted(String productIdentifier, String refundRequestStatus); + void onManagementOptionSelected(String option, @Nullable String url); + void onCustomActionSelected(String actionId, @Nullable String purchaseIdentifier); + } private static final String TAG = "RevenueCatUI"; private static volatile PaywallCallbacks paywallCallbacks; @@ -42,16 +57,128 @@ public static void sendPaywallResult(String result) { } } - public static void sendCustomerCenterResult(String result) { + public static void sendCustomerCenterDismissed() { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onCustomerCenterDismissed(); + } else { + Log.w(TAG, "No callback registered to receive customer center dismissed"); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending customer center dismissed: " + e.getMessage()); + } + } + + public static void sendCustomerCenterError() { try { CustomerCenterCallbacks cb = customerCenterCallbacks; if (cb != null) { - cb.onCustomerCenterResult(result); + cb.onCustomerCenterError(); } else { - Log.w(TAG, "No callback registered to receive customer center result: " + result); + Log.w(TAG, "No callback registered to receive customer center error"); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending customer center error: " + e.getMessage()); + } + } + + public static void sendFeedbackSurveyCompleted(String feedbackSurveyOptionId) { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onFeedbackSurveyCompleted(feedbackSurveyOptionId); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending feedback survey completed: " + e.getMessage()); + } + } + + public static void sendShowingManageSubscriptions() { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onShowingManageSubscriptions(); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending showing manage subscriptions: " + e.getMessage()); + } + } + + public static void sendRestoreCompleted(String customerInfoJson) { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onRestoreCompleted(customerInfoJson); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending restore completed: " + e.getMessage()); + } + } + + public static void sendRestoreFailed(String errorJson) { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onRestoreFailed(errorJson); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending restore failed: " + e.getMessage()); + } + } + + public static void sendRestoreStarted() { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onRestoreStarted(); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending restore started: " + e.getMessage()); + } + } + + public static void sendRefundRequestStarted(String productIdentifier) { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onRefundRequestStarted(productIdentifier); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending refund request started: " + e.getMessage()); + } + } + + public static void sendRefundRequestCompleted(String productIdentifier, String refundRequestStatus) { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onRefundRequestCompleted(productIdentifier, refundRequestStatus); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending refund request completed: " + e.getMessage()); + } + } + + public static void sendManagementOptionSelected(String option, @Nullable String url) { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onManagementOptionSelected(option, url); + } + } catch (Throwable e) { + Log.e(TAG, "Error sending management option selected: " + e.getMessage()); + } + } + + public static void sendCustomActionSelected(String actionId, @Nullable String purchaseIdentifier) { + try { + CustomerCenterCallbacks cb = customerCenterCallbacks; + if (cb != null) { + cb.onCustomActionSelected(actionId, purchaseIdentifier); } } catch (Throwable e) { - Log.e(TAG, "Error sending customer center result: " + e.getMessage()); + Log.e(TAG, "Error sending custom action selected: " + e.getMessage()); } } } diff --git a/RevenueCatUI/Plugins/iOS/RevenueCatUI.m b/RevenueCatUI/Plugins/iOS/RevenueCatUI.m index c917e047..58dc047f 100644 --- a/RevenueCatUI/Plugins/iOS/RevenueCatUI.m +++ b/RevenueCatUI/Plugins/iOS/RevenueCatUI.m @@ -5,7 +5,9 @@ #import typedef void (*RCUIPaywallResultCallback)(const char *result); -typedef void (*RCUICustomerCenterCallback)(const char *result); +typedef void (*RCUICustomerCenterDismissedCallback)(void); +typedef void (*RCUICustomerCenterErrorCallback)(void); +typedef void (*RCUICustomerCenterEventCallback)(const char *eventName, const char *payload); static NSString *const kRCUIOptionRequiredEntitlementIdentifier = @"requiredEntitlementIdentifier"; static NSString *const kRCUIOptionOfferingIdentifier = @"offeringIdentifier"; @@ -57,13 +59,23 @@ static void RCUIInvokeCallback(RCUIPaywallResultCallback callback, NSString *tok }); } -static void RCUICustomerCenterInvokeCallback(RCUICustomerCenterCallback callback, NSString *token) { +static void RCUICustomerCenterInvokeDismissedCallback(RCUICustomerCenterDismissedCallback callback) { if (callback == NULL) { return; } dispatch_async(dispatch_get_main_queue(), ^{ - callback((token ?: @"ERROR").UTF8String); + callback(); + }); +} + +static void RCUICustomerCenterInvokeErrorCallback(RCUICustomerCenterErrorCallback callback) { + if (callback == NULL) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + callback(); }); } @@ -106,9 +118,9 @@ static id RCUIJSONObjectFromJSONString(NSString *jsonString) { return options; } -static BOOL RCUICustomerCenterEnsureReady(RCUICustomerCenterCallback callback) { +static BOOL RCUICustomerCenterEnsureReady(RCUICustomerCenterErrorCallback errorCallback) { if (!RCPurchases.isConfigured) { - RCUICustomerCenterInvokeCallback(callback, @"ERROR"); + RCUICustomerCenterInvokeErrorCallback(errorCallback); return NO; } @@ -192,22 +204,161 @@ void rcui_presentPaywallIfNeeded(const char *requiredEntitlementIdentifier, RCUIPresentPaywallIfNeededInternal(entitlement, offering, contextJson, displayCloseButton ? YES : NO, callback); } -void rcui_presentCustomerCenter(RCUICustomerCenterCallback callback) { - if (!RCUICustomerCenterEnsureReady(callback)) { +@interface RCUICustomerCenterDelegate : NSObject +@property (nonatomic, copy) RCUICustomerCenterEventCallback eventCallback; +@end + +@implementation RCUICustomerCenterDelegate + +- (void)customerCenterViewControllerWasDismissed:(CustomerCenterUIViewController *)controller API_AVAILABLE(ios(15.0)) { + +} + +- (void)customerCenterViewControllerDidStartRestore:(CustomerCenterUIViewController *)controller API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onRestoreStarted", ""); + }); + } +} + +- (void)customerCenterViewController:(CustomerCenterUIViewController *)controller + didFinishRestoringWithCustomerInfoDictionary:(NSDictionary *)customerInfoDictionary API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:customerInfoDictionary options:0 error:&error]; + if (jsonData && !error) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onRestoreCompleted", jsonString.UTF8String); + }); + } + } +} + +- (void)customerCenterViewController:(CustomerCenterUIViewController *)controller + didFailRestoringWithErrorDictionary:(NSDictionary *)errorDictionary API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:errorDictionary options:0 error:&error]; + if (jsonData && !error) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onRestoreFailed", jsonString.UTF8String); + }); + } + } +} + +- (void)customerCenterViewControllerDidShowManageSubscriptions:(CustomerCenterUIViewController *)controller API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onShowingManageSubscriptions", ""); + }); + } +} + +- (void)customerCenterViewController:(CustomerCenterUIViewController *)controller +didStartRefundRequestForProductWithID:(NSString *)productID API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onRefundRequestStarted", productID.UTF8String ?: ""); + }); + } +} + +- (void)customerCenterViewController:(CustomerCenterUIViewController *)controller +didCompleteRefundRequestForProductWithID:(NSString *)productID + withStatus:(NSString *)status API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + NSDictionary *payload = @{ + @"productIdentifier": productID ?: @"", + @"refundRequestStatus": status ?: @"" + }; + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error]; + if (jsonData && !error) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onRefundRequestCompleted", jsonString.UTF8String); + }); + } + } +} + +- (void)customerCenterViewController:(CustomerCenterUIViewController *)controller +didCompleteFeedbackSurveyWithOptionID:(NSString *)optionID API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onFeedbackSurveyCompleted", optionID.UTF8String ?: ""); + }); + } +} + +- (void)customerCenterViewController:(CustomerCenterUIViewController *)controller +didSelectCustomerCenterManagementOption:(NSString *)optionID +withURL:(NSString *)url API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + NSDictionary *payload = @{ + @"option": optionID ?: @"", + @"url": url ?: [NSNull null] + }; + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error]; + if (jsonData && !error) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onManagementOptionSelected", jsonString.UTF8String); + }); + } + } +} + +- (void)customerCenterViewController:(CustomerCenterUIViewController *)controller + didSelectCustomAction:(NSString *)actionID + withPurchaseIdentifier:(NSString *)purchaseIdentifier API_AVAILABLE(ios(15.0)) { + if (self.eventCallback) { + NSDictionary *payload = @{ + @"actionId": actionID ?: @"", + @"purchaseIdentifier": purchaseIdentifier ?: [NSNull null] + }; + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error]; + if (jsonData && !error) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.eventCallback("onCustomActionSelected", jsonString.UTF8String); + }); + } + } +} + +@end + +void rcui_presentCustomerCenter(RCUICustomerCenterDismissedCallback dismissedCallback, + RCUICustomerCenterErrorCallback errorCallback, + RCUICustomerCenterEventCallback eventCallback) { + if (!RCUICustomerCenterEnsureReady(errorCallback)) { return; } dispatch_async(dispatch_get_main_queue(), ^{ if (@available(iOS 15.0, *)) { __block CustomerCenterProxy *proxy = [[CustomerCenterProxy alloc] init]; + __block RCUICustomerCenterDelegate *delegate = [[RCUICustomerCenterDelegate alloc] init]; + delegate.eventCallback = eventCallback; + proxy.shouldShowCloseButton = YES; + [proxy setDelegate:delegate]; [proxy presentWithResultHandler:^{ - RCUICustomerCenterInvokeCallback(callback, @"DISMISSED"); + RCUICustomerCenterInvokeDismissedCallback(dismissedCallback); + [proxy setDelegate:nil]; proxy = nil; + delegate = nil; }]; } else { - RCUICustomerCenterInvokeCallback(callback, @"NOT_PRESENTED"); + RCUICustomerCenterInvokeErrorCallback(errorCallback); } }); } diff --git a/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs b/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs index 327d907d..0079c895 100644 --- a/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs +++ b/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs @@ -1,16 +1,154 @@ +using System; + namespace RevenueCatUI { + public enum RefundRequestStatus + { + Success, + UserCancelled, + Error + } + + public enum CustomerCenterManagementOption + { + Cancel, + CustomUrl, + MissingPurchase, + RefundRequest, + ChangePlans, + Unknown + } + + public sealed class FeedbackSurveyCompletedEventArgs + { + public string FeedbackSurveyOptionId { get; } + + public FeedbackSurveyCompletedEventArgs(string feedbackSurveyOptionId) + { + FeedbackSurveyOptionId = feedbackSurveyOptionId; + } + } + + public sealed class RestoreCompletedEventArgs + { + public Purchases.CustomerInfo CustomerInfo { get; } + + public RestoreCompletedEventArgs(Purchases.CustomerInfo customerInfo) + { + CustomerInfo = customerInfo; + } + } + + public sealed class RestoreFailedEventArgs + { + public Purchases.Error Error { get; } + + public RestoreFailedEventArgs(Purchases.Error error) + { + Error = error; + } + } + + public sealed class RefundRequestStartedEventArgs + { + public string ProductIdentifier { get; } + + public RefundRequestStartedEventArgs(string productIdentifier) + { + ProductIdentifier = productIdentifier; + } + } + + public sealed class RefundRequestCompletedEventArgs + { + public string ProductIdentifier { get; } + public RefundRequestStatus RefundRequestStatus { get; } + + public RefundRequestCompletedEventArgs(string productIdentifier, RefundRequestStatus refundRequestStatus) + { + ProductIdentifier = productIdentifier; + RefundRequestStatus = refundRequestStatus; + } + } + + public sealed class ManagementOptionSelectedEventArgs + { + public CustomerCenterManagementOption Option { get; } + public string Url { get; } + + public ManagementOptionSelectedEventArgs(CustomerCenterManagementOption option, string url = null) + { + Option = option; + Url = url; + } + } + + public sealed class CustomActionSelectedEventArgs + { + public string ActionId { get; } + public string PurchaseIdentifier { get; } + + public CustomActionSelectedEventArgs(string actionId, string purchaseIdentifier = null) + { + ActionId = actionId; + PurchaseIdentifier = purchaseIdentifier; + } + } + /// - /// Placeholder container for upcoming Customer Center callback support. + /// Callbacks for Customer Center events. /// public sealed class CustomerCenterCallbacks { internal static readonly CustomerCenterCallbacks None = new CustomerCenterCallbacks(); - // Callbacks will be added in a future PR; keeping this empty confirms the - // new API surface compiles across all platforms without behavior changes. - public CustomerCenterCallbacks() - { - } + /// + /// Called when a feedback survey is completed with the selected option ID. + /// + public Action OnFeedbackSurveyCompleted { get; set; } + + /// + /// Called when the manage subscriptions section is being shown. + /// + public Action OnShowingManageSubscriptions { get; set; } + + /// + /// Called when a restore operation is completed successfully. + /// + public Action OnRestoreCompleted { get; set; } + + /// + /// Called when a restore operation fails. + /// + public Action OnRestoreFailed { get; set; } + + /// + /// Called when a restore operation starts. + /// + public Action OnRestoreStarted { get; set; } + + /// + /// Called when a refund request starts with the product identifier. + /// iOS only - This callback will never be called on Android as refund requests are not supported on that platform. + /// + public Action OnRefundRequestStarted { get; set; } + + /// + /// Called when a refund request completes with status information. + /// iOS only - This callback will never be called on Android as refund requests are not supported on that platform. + /// + public Action OnRefundRequestCompleted { get; set; } + + /// + /// Called when a customer center management option is selected. + /// For 'CustomUrl' options, the Url parameter will contain the URL. + /// For all other options, the Url parameter will be null. + /// + public Action OnManagementOptionSelected { get; set; } + + /// + /// Called when a custom action is selected in the customer center. + /// + public Action OnCustomActionSelected { get; set; } } } diff --git a/RevenueCatUI/Scripts/CustomerCenterPresenter.cs b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs index 16c990b5..190ef121 100644 --- a/RevenueCatUI/Scripts/CustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/CustomerCenterPresenter.cs @@ -12,9 +12,24 @@ public static class CustomerCenterPresenter { /// /// Presents the Customer Center UI modally. + /// The Customer Center allows users to manage their subscriptions, view purchase history, + /// request refunds (iOS only), and access support options - all configured through the + /// RevenueCat dashboard. /// - /// Placeholder for future callback support. + /// Optional callbacks for Customer Center events such as restore operations, + /// refund requests, feedback surveys, and management options. /// A task that completes when the presentation ends. + /// + /// + /// var callbacks = new CustomerCenterCallbacks + /// { + /// OnRestoreCompleted = (args) => Debug.Log($"Restore completed: {args.CustomerInfo}"), + /// OnRestoreFailed = (args) => Debug.LogError($"Restore failed: {args.Error.Message}"), + /// OnFeedbackSurveyCompleted = (args) => Debug.Log($"Survey completed: {args.FeedbackSurveyOptionId}") + /// }; + /// await CustomerCenterPresenter.Present(callbacks); + /// + /// public static async Task Present(CustomerCenterCallbacks callbacks = null) { try diff --git a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs index 01c3137f..67a08eba 100644 --- a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs @@ -1,6 +1,8 @@ #if UNITY_ANDROID && !UNITY_EDITOR using System; using System.Threading.Tasks; +using RevenueCat; +using RevenueCat.SimpleJSON; using RevenueCatUI.Internal; using UnityEngine; using UnityEngine.Android; @@ -12,6 +14,7 @@ internal class AndroidCustomerCenterPresenter : ICustomerCenterPresenter private readonly AndroidJavaClass _plugin; private readonly CallbacksProxy _callbacks; private TaskCompletionSource _current; + private CustomerCenterCallbacks _storedCallbacks; public AndroidCustomerCenterPresenter() { @@ -29,72 +32,263 @@ public AndroidCustomerCenterPresenter() ~AndroidCustomerCenterPresenter() { - try { _plugin?.CallStatic("unregisterCustomerCenterCallbacks"); } catch { } + try + { + _plugin?.CallStatic("unregisterCustomerCenterCallbacks"); + _storedCallbacks = null; + _current = null; + } + catch { } } public Task PresentAsync(CustomerCenterCallbacks callbacks) { if (_plugin == null) { - Debug.LogError("[RevenueCatUI][Android] Plugin not initialized. Cannot present Customer Center."); - return Task.CompletedTask; + throw new InvalidOperationException("[RevenueCatUI][Android] Plugin not initialized. Cannot present Customer Center."); } if (_current != null && !_current.Task.IsCompleted) { - Debug.LogWarning("[RevenueCatUI][Android] Customer Center presentation already in progress; rejecting new request."); + Debug.LogWarning("[RevenueCatUI][Android] Customer Center presentation already in progress; returning existing task."); return _current.Task; } var currentActivity = AndroidApplication.currentActivity; if (currentActivity == null) { - Debug.LogError("[RevenueCatUI][Android] Current activity is null. Cannot present Customer Center."); - return Task.CompletedTask; + throw new InvalidOperationException("[RevenueCatUI][Android] Current activity is null. Cannot present Customer Center."); } + _storedCallbacks = callbacks; _current = new TaskCompletionSource(); + try { _plugin.CallStatic("presentCustomerCenter", currentActivity); } catch (Exception e) { - Debug.LogError($"[RevenueCatUI][Android] Exception in presentCustomerCenter: {e.Message}"); - _current.TrySetResult(false); - var task = _current.Task; + _storedCallbacks = null; _current = null; - return task; + throw new InvalidOperationException($"[RevenueCatUI][Android] Exception in presentCustomerCenter: {e.Message}", e); } return _current.Task; } - private void OnCustomerCenterResult(string resultData) + private void OnCustomerCenterDismissed() { if (_current == null) { - Debug.LogWarning("[RevenueCatUI][Android] Received Customer Center result with no pending request."); + Debug.LogWarning("[RevenueCatUI][Android] Received Customer Center dismissed with no pending request."); return; } try { - var token = resultData ?? "ERROR"; - Debug.Log($"[RevenueCatUI][Android] Customer Center completed with token '{token}'."); + Debug.Log("[RevenueCatUI][Android] Customer Center dismissed."); _current.TrySetResult(true); } catch (Exception e) { - Debug.LogError($"[RevenueCatUI][Android] Failed to handle Customer Center result '{resultData}': {e.Message}. Setting Error."); - _current.TrySetResult(false); + Debug.LogError($"[RevenueCatUI][Android] Failed to handle Customer Center dismissed: {e.Message}"); + _current.TrySetException(e); } finally { + _storedCallbacks = null; _current = null; } } + private void OnCustomerCenterError() + { + if (_current == null) + { + Debug.LogWarning("[RevenueCatUI][Android] Received Customer Center error with no pending request."); + return; + } + + try + { + Debug.LogError("[RevenueCatUI][Android] Customer Center presentation failed."); + _current.TrySetException(new Exception("Customer Center presentation failed")); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Failed to handle Customer Center error: {e.Message}"); + _current.TrySetException(e); + } + finally + { + _storedCallbacks = null; + _current = null; + } + } + + private void OnFeedbackSurveyCompleted(string feedbackSurveyOptionId) + { + try + { + _storedCallbacks?.OnFeedbackSurveyCompleted?.Invoke( + new FeedbackSurveyCompletedEventArgs(feedbackSurveyOptionId) + ); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnFeedbackSurveyCompleted callback: {e.Message}"); + } + } + + private void OnShowingManageSubscriptions() + { + try + { + _storedCallbacks?.OnShowingManageSubscriptions?.Invoke(); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnShowingManageSubscriptions callback: {e.Message}"); + } + } + + private void OnRestoreCompleted(string customerInfoJson) + { + try + { + var customerInfo = new Purchases.CustomerInfo(JSON.Parse(customerInfoJson)); + _storedCallbacks?.OnRestoreCompleted?.Invoke( + new RestoreCompletedEventArgs(customerInfo) + ); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnRestoreCompleted callback: {e.Message}"); + } + } + + private void OnRestoreFailed(string errorJson) + { + try + { + var error = new Purchases.Error(JSON.Parse(errorJson)); + _storedCallbacks?.OnRestoreFailed?.Invoke( + new RestoreFailedEventArgs(error) + ); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnRestoreFailed callback: {e.Message}"); + } + } + + private void OnRestoreStarted() + { + try + { + _storedCallbacks?.OnRestoreStarted?.Invoke(); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnRestoreStarted callback: {e.Message}"); + } + } + + private void OnRefundRequestStarted(string productIdentifier) + { + try + { + _storedCallbacks?.OnRefundRequestStarted?.Invoke( + new RefundRequestStartedEventArgs(productIdentifier) + ); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnRefundRequestStarted callback: {e.Message}"); + } + } + + private void OnRefundRequestCompleted(string productIdentifier, string refundRequestStatus) + { + try + { + RefundRequestStatus status; + switch (refundRequestStatus?.ToUpperInvariant()) + { + case "SUCCESS": + status = RefundRequestStatus.Success; + break; + case "USERCANCELLED": + case "USER_CANCELLED": + status = RefundRequestStatus.UserCancelled; + break; + default: + status = RefundRequestStatus.Error; + break; + } + + _storedCallbacks?.OnRefundRequestCompleted?.Invoke( + new RefundRequestCompletedEventArgs(productIdentifier, status) + ); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnRefundRequestCompleted callback: {e.Message}"); + } + } + + private void OnManagementOptionSelected(string option, string url) + { + try + { + CustomerCenterManagementOption optionEnum; + switch (option?.ToLowerInvariant()) + { + case "cancel": + optionEnum = CustomerCenterManagementOption.Cancel; + break; + case "custom_url": + optionEnum = CustomerCenterManagementOption.CustomUrl; + break; + case "missing_purchase": + optionEnum = CustomerCenterManagementOption.MissingPurchase; + break; + case "refund_request": + optionEnum = CustomerCenterManagementOption.RefundRequest; + break; + case "change_plans": + optionEnum = CustomerCenterManagementOption.ChangePlans; + break; + default: + optionEnum = CustomerCenterManagementOption.Unknown; + break; + } + + _storedCallbacks?.OnManagementOptionSelected?.Invoke( + new ManagementOptionSelectedEventArgs(optionEnum, url) + ); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnManagementOptionSelected callback: {e.Message}"); + } + } + + private void OnCustomActionSelected(string actionId, string purchaseIdentifier) + { + try + { + _storedCallbacks?.OnCustomActionSelected?.Invoke( + new CustomActionSelectedEventArgs(actionId, purchaseIdentifier) + ); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][Android] Error in OnCustomActionSelected callback: {e.Message}"); + } + } + private class CallbacksProxy : AndroidJavaProxy { private readonly AndroidCustomerCenterPresenter _owner; @@ -104,9 +298,59 @@ public CallbacksProxy(AndroidCustomerCenterPresenter owner) : base("com.revenuec _owner = owner; } - public void onCustomerCenterResult(string result) + public void onCustomerCenterDismissed() + { + _owner.OnCustomerCenterDismissed(); + } + + public void onCustomerCenterError() + { + _owner.OnCustomerCenterError(); + } + + public void onFeedbackSurveyCompleted(string feedbackSurveyOptionId) + { + _owner.OnFeedbackSurveyCompleted(feedbackSurveyOptionId); + } + + public void onShowingManageSubscriptions() + { + _owner.OnShowingManageSubscriptions(); + } + + public void onRestoreCompleted(string customerInfoJson) + { + _owner.OnRestoreCompleted(customerInfoJson); + } + + public void onRestoreFailed(string errorJson) + { + _owner.OnRestoreFailed(errorJson); + } + + public void onRestoreStarted() + { + _owner.OnRestoreStarted(); + } + + public void onRefundRequestStarted(string productIdentifier) + { + _owner.OnRefundRequestStarted(productIdentifier); + } + + public void onRefundRequestCompleted(string productIdentifier, string refundRequestStatus) + { + _owner.OnRefundRequestCompleted(productIdentifier, refundRequestStatus); + } + + public void onManagementOptionSelected(string option, string url) + { + _owner.OnManagementOptionSelected(option, url); + } + + public void onCustomActionSelected(string actionId, string purchaseIdentifier) { - _owner.OnCustomerCenterResult(result); + _owner.OnCustomActionSelected(actionId, purchaseIdentifier); } } } diff --git a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs index 4c430d5a..612421ea 100644 --- a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs @@ -2,60 +2,245 @@ using System; using System.Runtime.InteropServices; using System.Threading.Tasks; +using RevenueCat; +using RevenueCat.SimpleJSON; using RevenueCatUI.Internal; +using UnityEngine; namespace RevenueCatUI.Platforms { internal class IOSCustomerCenterPresenter : ICustomerCenterPresenter { - private delegate void CustomerCenterResultCallback(string result); + private delegate void CustomerCenterDismissedCallback(); + private delegate void CustomerCenterErrorCallback(); + private delegate void CustomerCenterEventCallback(string eventName, string payload); - [DllImport("__Internal")] private static extern void rcui_presentCustomerCenter(CustomerCenterResultCallback cb); + [DllImport("__Internal")] + private static extern void rcui_presentCustomerCenter( + CustomerCenterDismissedCallback dismissedCallback, + CustomerCenterErrorCallback errorCallback, + CustomerCenterEventCallback eventCallback); private static TaskCompletionSource s_current; + private static CustomerCenterCallbacks s_storedCallbacks; + + ~IOSCustomerCenterPresenter() + { + s_storedCallbacks = null; + s_current = null; + } public Task PresentAsync(CustomerCenterCallbacks callbacks) { if (s_current != null && !s_current.Task.IsCompleted) { - UnityEngine.Debug.LogWarning("[RevenueCatUI][iOS] Customer Center presentation already in progress; rejecting new request."); + Debug.LogWarning("[RevenueCatUI][iOS] Customer Center presentation already in progress; returning existing task."); return s_current.Task; } + s_storedCallbacks = callbacks; var tcs = new TaskCompletionSource(); s_current = tcs; + try { - rcui_presentCustomerCenter(OnCompleted); + rcui_presentCustomerCenter(OnDismissed, OnError, OnEvent); } catch (Exception e) { - UnityEngine.Debug.LogError($"[RevenueCatUI][iOS] Exception in presentCustomerCenter: {e.Message}"); - tcs.TrySetResult(false); + s_storedCallbacks = null; s_current = null; + throw new InvalidOperationException($"[RevenueCatUI][iOS] Exception in presentCustomerCenter: {e.Message}", e); } + return tcs.Task; } - [AOT.MonoPInvokeCallback(typeof(CustomerCenterResultCallback))] - private static void OnCompleted(string result) + [AOT.MonoPInvokeCallback(typeof(CustomerCenterDismissedCallback))] + private static void OnDismissed() { + if (s_current == null) + { + Debug.LogWarning("[RevenueCatUI][iOS] Received Customer Center dismissed with no pending request."); + return; + } + try { - var token = (result ?? "ERROR"); - UnityEngine.Debug.Log($"[RevenueCatUI][iOS] Customer Center completed with token '{token}'."); - s_current?.TrySetResult(true); + Debug.Log("[RevenueCatUI][iOS] Customer Center dismissed."); + s_current.TrySetResult(true); } catch (Exception e) { - UnityEngine.Debug.LogError($"[RevenueCatUI][iOS] Failed to handle Customer Center completion: {e.Message}"); - s_current?.TrySetResult(false); + Debug.LogError($"[RevenueCatUI][iOS] Failed to handle Customer Center dismissed: {e.Message}"); + s_current.TrySetException(e); } finally { + s_storedCallbacks = null; s_current = null; } } + + [AOT.MonoPInvokeCallback(typeof(CustomerCenterErrorCallback))] + private static void OnError() + { + if (s_current == null) + { + Debug.LogWarning("[RevenueCatUI][iOS] Received Customer Center error with no pending request."); + return; + } + + try + { + Debug.LogError("[RevenueCatUI][iOS] Customer Center presentation failed."); + s_current.TrySetException(new Exception("Customer Center presentation failed")); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][iOS] Failed to handle Customer Center error: {e.Message}"); + s_current.TrySetException(e); + } + finally + { + s_storedCallbacks = null; + s_current = null; + } + } + + [AOT.MonoPInvokeCallback(typeof(CustomerCenterEventCallback))] + private static void OnEvent(string eventName, string payload) + { + try + { + switch (eventName) + { + case "onRestoreStarted": + s_storedCallbacks?.OnRestoreStarted?.Invoke(); + break; + + case "onRestoreCompleted": + if (!string.IsNullOrEmpty(payload)) + { + var customerInfo = new Purchases.CustomerInfo(JSON.Parse(payload)); + s_storedCallbacks?.OnRestoreCompleted?.Invoke( + new RestoreCompletedEventArgs(customerInfo)); + } + break; + + case "onRestoreFailed": + if (!string.IsNullOrEmpty(payload)) + { + var error = new Purchases.Error(JSON.Parse(payload)); + s_storedCallbacks?.OnRestoreFailed?.Invoke( + new RestoreFailedEventArgs(error)); + } + break; + + case "onShowingManageSubscriptions": + s_storedCallbacks?.OnShowingManageSubscriptions?.Invoke(); + break; + + case "onRefundRequestStarted": + if (!string.IsNullOrEmpty(payload)) + { + s_storedCallbacks?.OnRefundRequestStarted?.Invoke( + new RefundRequestStartedEventArgs(payload)); + } + break; + + case "onRefundRequestCompleted": + if (!string.IsNullOrEmpty(payload)) + { + var data = JSON.Parse(payload); + var productIdentifier = data["productIdentifier"]; + var statusString = data["refundRequestStatus"]; + + RefundRequestStatus status; + switch (statusString?.Value?.ToUpperInvariant()) + { + case "SUCCESS": + status = RefundRequestStatus.Success; + break; + case "USERCANCELLED": + case "USER_CANCELLED": + status = RefundRequestStatus.UserCancelled; + break; + default: + status = RefundRequestStatus.Error; + break; + } + + s_storedCallbacks?.OnRefundRequestCompleted?.Invoke( + new RefundRequestCompletedEventArgs(productIdentifier, status)); + } + break; + + case "onFeedbackSurveyCompleted": + if (!string.IsNullOrEmpty(payload)) + { + s_storedCallbacks?.OnFeedbackSurveyCompleted?.Invoke( + new FeedbackSurveyCompletedEventArgs(payload)); + } + break; + + case "onManagementOptionSelected": + if (!string.IsNullOrEmpty(payload)) + { + var data = JSON.Parse(payload); + var option = data["option"]?.Value; + var url = data["url"]?.Value; + + CustomerCenterManagementOption optionEnum; + switch (option?.ToLowerInvariant()) + { + case "cancel": + optionEnum = CustomerCenterManagementOption.Cancel; + break; + case "custom_url": + optionEnum = CustomerCenterManagementOption.CustomUrl; + break; + case "missing_purchase": + optionEnum = CustomerCenterManagementOption.MissingPurchase; + break; + case "refund_request": + optionEnum = CustomerCenterManagementOption.RefundRequest; + break; + case "change_plans": + optionEnum = CustomerCenterManagementOption.ChangePlans; + break; + default: + optionEnum = CustomerCenterManagementOption.Unknown; + break; + } + + s_storedCallbacks?.OnManagementOptionSelected?.Invoke( + new ManagementOptionSelectedEventArgs(optionEnum, url)); + } + break; + + case "onCustomActionSelected": + if (!string.IsNullOrEmpty(payload)) + { + var data = JSON.Parse(payload); + var actionId = data["actionId"]?.Value; + var purchaseIdentifier = data["purchaseIdentifier"]?.Value; + + s_storedCallbacks?.OnCustomActionSelected?.Invoke( + new CustomActionSelectedEventArgs(actionId, purchaseIdentifier)); + } + break; + + default: + Debug.LogWarning($"[RevenueCatUI][iOS] Unknown customer center event: {eventName}"); + break; + } + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI][iOS] Error handling customer center event '{eventName}': {e.Message}"); + } + } } } #endif From 893a33fdb26e78fe0ebc10ea6adf2aa13f50b73d Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:53:06 +0200 Subject: [PATCH 08/24] add callbacks to PurchasesListener --- Subtester/Assets/Scripts/PurchasesListener.cs | 152 +++++++++++++----- 1 file changed, 115 insertions(+), 37 deletions(-) diff --git a/Subtester/Assets/Scripts/PurchasesListener.cs b/Subtester/Assets/Scripts/PurchasesListener.cs index 0038cdc4..507446eb 100644 --- a/Subtester/Assets/Scripts/PurchasesListener.cs +++ b/Subtester/Assets/Scripts/PurchasesListener.cs @@ -69,8 +69,8 @@ private void Start() CreateButton("Present Paywall", PresentPaywallResult); CreateButton("Present Paywall with Options", PresentPaywallWithOptions); CreateButton("Present Paywall for Offering", PresentPaywallForOffering); - CreateButton("Present Paywall If Needed", PresentPaywallIfNeeded); - CreateButton("Present Customer Center", PresentCustomerCenter); + CreateButton("Present Paywall If Needed", PresentPaywallIfNeeded); + CreateButton("Present Customer Center", PresentCustomerCenter); var purchases = GetComponent(); purchases.SetLogLevel(Purchases.LogLevel.Verbose); @@ -222,20 +222,20 @@ void PresentPaywallForOffering() StartCoroutine(PresentPaywallForOfferingCoroutine()); } - void PresentPaywallIfNeeded() - { - Debug.Log("Subtester: launching paywall if needed for test entitlement"); - if (infoLabel != null) infoLabel.text = "Checking entitlement and launching paywall if needed..."; - StartCoroutine(PresentPaywallIfNeededCoroutine()); - } - - void PresentCustomerCenter() - { - Debug.Log("Subtester: launching customer center"); - if (infoLabel != null) infoLabel.text = "Launching Customer Center..."; - StartCoroutine(PresentCustomerCenterCoroutine()); - } - + void PresentPaywallIfNeeded() + { + Debug.Log("Subtester: launching paywall if needed for test entitlement"); + if (infoLabel != null) infoLabel.text = "Checking entitlement and launching paywall if needed..."; + StartCoroutine(PresentPaywallIfNeededCoroutine()); + } + + void PresentCustomerCenter() + { + Debug.Log("Subtester: launching customer center"); + if (infoLabel != null) infoLabel.text = "Launching Customer Center..."; + StartCoroutine(PresentCustomerCenterCoroutine()); + } + private System.Collections.IEnumerator PresentPaywallCoroutine() { var task = RevenueCatUI.PaywallsPresenter.Present(); @@ -266,22 +266,100 @@ private System.Collections.IEnumerator PresentPaywallCoroutine() infoLabel.text = $"Paywall result: {status}"; Debug.Log($"Subtester: {status}"); - } - } - - private System.Collections.IEnumerator PresentCustomerCenterCoroutine() - { - var task = RevenueCatUI.CustomerCenterPresenter.Present(); - while (!task.IsCompleted) { yield return null; } - - Debug.Log("Subtester: customer center finished."); - - if (infoLabel != null) - { - infoLabel.text = "Customer Center finished (no callbacks yet)"; - } - } - + } + } + + private System.Collections.IEnumerator PresentCustomerCenterCoroutine() + { + var callbacks = new RevenueCatUI.CustomerCenterCallbacks + { + OnFeedbackSurveyCompleted = (args) => + { + Debug.Log($"Subtester: OnFeedbackSurveyCompleted - Option ID: {args.FeedbackSurveyOptionId}"); + if (infoLabel != null) + { + infoLabel.text = $"Feedback survey completed: {args.FeedbackSurveyOptionId}"; + } + }, + OnShowingManageSubscriptions = () => + { + Debug.Log("Subtester: OnShowingManageSubscriptions"); + if (infoLabel != null) + { + infoLabel.text = "Showing manage subscriptions"; + } + }, + OnRestoreCompleted = (args) => + { + Debug.Log($"Subtester: OnRestoreCompleted - CustomerInfo: {args.CustomerInfo}"); + if (infoLabel != null) + { + DisplayCustomerInfo(args.CustomerInfo); + } + }, + OnRestoreFailed = (args) => + { + Debug.Log($"Subtester: OnRestoreFailed - Error: {args.Error}"); + if (infoLabel != null) + { + LogError(args.Error); + } + }, + OnRestoreStarted = () => + { + Debug.Log("Subtester: OnRestoreStarted"); + if (infoLabel != null) + { + infoLabel.text = "Restore started..."; + } + }, + OnRefundRequestStarted = (args) => + { + Debug.Log($"Subtester: OnRefundRequestStarted - Product: {args.ProductIdentifier}"); + if (infoLabel != null) + { + infoLabel.text = $"Refund request started for: {args.ProductIdentifier}"; + } + }, + OnRefundRequestCompleted = (args) => + { + Debug.Log($"Subtester: OnRefundRequestCompleted - Product: {args.ProductIdentifier}, Status: {args.RefundRequestStatus}"); + if (infoLabel != null) + { + infoLabel.text = $"Refund request completed for {args.ProductIdentifier}: {args.RefundRequestStatus}"; + } + }, + OnManagementOptionSelected = (args) => + { + string urlInfo = args.Url != null ? $", URL: {args.Url}" : ""; + Debug.Log($"Subtester: OnManagementOptionSelected - Option: {args.Option}{urlInfo}"); + if (infoLabel != null) + { + infoLabel.text = $"Management option selected: {args.Option}{urlInfo}"; + } + }, + OnCustomActionSelected = (args) => + { + string purchaseInfo = args.PurchaseIdentifier != null ? $", Purchase: {args.PurchaseIdentifier}" : ""; + Debug.Log($"Subtester: OnCustomActionSelected - Action: {args.ActionId}{purchaseInfo}"); + if (infoLabel != null) + { + infoLabel.text = $"Custom action selected: {args.ActionId}{purchaseInfo}"; + } + } + }; + + var task = RevenueCatUI.CustomerCenterPresenter.Present(callbacks); + while (!task.IsCompleted) { yield return null; } + + Debug.Log("Subtester: customer center finished."); + + if (infoLabel != null) + { + infoLabel.text = "Customer Center finished"; + } + } + private System.Collections.IEnumerator PresentPaywallWithOptionsCoroutine() { var options = new RevenueCatUI.PaywallOptions(displayCloseButton: false); @@ -428,11 +506,11 @@ private System.Collections.IEnumerator PresentPaywallIfNeededCoroutine() } infoLabel.text = message; } - } - - private string GetPaywallResultStatus(RevenueCatUI.PaywallResult result) - { - switch (result.Result) + } + + private string GetPaywallResultStatus(RevenueCatUI.PaywallResult result) + { + switch (result.Result) { case RevenueCatUI.PaywallResultType.Purchased: return "PURCHASED - User completed a purchase"; From 6574a3af3a83cd54bd4a94659ca35a2c5557d0b1 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:30:40 +0200 Subject: [PATCH 09/24] fix missing callback --- .../ui/CustomerCenterTrampolineActivity.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java index 0cf24495..39b4cd5d 100644 --- a/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java +++ b/RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/src/main/java/com/revenuecat/purchasesunity/ui/CustomerCenterTrampolineActivity.java @@ -7,6 +7,7 @@ import androidx.activity.ComponentActivity; import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.revenuecat.purchases.Purchases; @@ -66,6 +67,13 @@ protected void onDestroy() { private CustomerCenterListener createCustomerCenterListener() { return new CustomerCenterListenerWrapper() { + @Override + public void onManagementOptionSelectedWrapper(@NonNull String s, + @Nullable String s1, + @Nullable String s2) { + // Ignored since it's deprecated + } + @Override public void onFeedbackSurveyCompletedWrapper(@Nullable String feedbackSurveyOptionId) { if (feedbackSurveyOptionId != null) { @@ -74,14 +82,16 @@ public void onFeedbackSurveyCompletedWrapper(@Nullable String feedbackSurveyOpti } @Override - public void onManagementOptionSelectedWrapper(@Nullable String action, @Nullable String url) { + public void onManagementOptionSelectedWrapper(@Nullable String action, + @Nullable String url) { if (action != null) { RevenueCatUI.sendManagementOptionSelected(action, url); } } @Override - public void onCustomActionSelectedWrapper(@Nullable String actionId, @Nullable String purchaseIdentifier) { + public void onCustomActionSelectedWrapper(@Nullable String actionId, + @Nullable String purchaseIdentifier) { if (actionId != null) { RevenueCatUI.sendCustomActionSelected(actionId, purchaseIdentifier); } From f9e3a4bc41ed7db92c431f0c5b24c8a4ec6cc170 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:31:12 +0200 Subject: [PATCH 10/24] remove catching to prevent catching devs exceptions --- .../Android/AndroidCustomerCenterPresenter.cs | 185 +++++--------- .../iOS/IOSCustomerCenterPresenter.cs | 229 +++++++++--------- 2 files changed, 172 insertions(+), 242 deletions(-) diff --git a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs index 67a08eba..da943acd 100644 --- a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs @@ -129,164 +129,101 @@ private void OnCustomerCenterError() private void OnFeedbackSurveyCompleted(string feedbackSurveyOptionId) { - try - { - _storedCallbacks?.OnFeedbackSurveyCompleted?.Invoke( - new FeedbackSurveyCompletedEventArgs(feedbackSurveyOptionId) - ); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnFeedbackSurveyCompleted callback: {e.Message}"); - } + _storedCallbacks?.OnFeedbackSurveyCompleted?.Invoke( + new FeedbackSurveyCompletedEventArgs(feedbackSurveyOptionId) + ); } private void OnShowingManageSubscriptions() { - try - { - _storedCallbacks?.OnShowingManageSubscriptions?.Invoke(); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnShowingManageSubscriptions callback: {e.Message}"); - } + _storedCallbacks?.OnShowingManageSubscriptions?.Invoke(); } private void OnRestoreCompleted(string customerInfoJson) { - try - { - var customerInfo = new Purchases.CustomerInfo(JSON.Parse(customerInfoJson)); - _storedCallbacks?.OnRestoreCompleted?.Invoke( - new RestoreCompletedEventArgs(customerInfo) - ); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnRestoreCompleted callback: {e.Message}"); - } + var customerInfo = new Purchases.CustomerInfo(JSON.Parse(customerInfoJson)); + _storedCallbacks?.OnRestoreCompleted?.Invoke( + new RestoreCompletedEventArgs(customerInfo) + ); } private void OnRestoreFailed(string errorJson) { - try - { - var error = new Purchases.Error(JSON.Parse(errorJson)); - _storedCallbacks?.OnRestoreFailed?.Invoke( - new RestoreFailedEventArgs(error) - ); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnRestoreFailed callback: {e.Message}"); - } + var error = new Purchases.Error(JSON.Parse(errorJson)); + _storedCallbacks?.OnRestoreFailed?.Invoke( + new RestoreFailedEventArgs(error) + ); } private void OnRestoreStarted() { - try - { - _storedCallbacks?.OnRestoreStarted?.Invoke(); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnRestoreStarted callback: {e.Message}"); - } + _storedCallbacks?.OnRestoreStarted?.Invoke(); } private void OnRefundRequestStarted(string productIdentifier) { - try - { - _storedCallbacks?.OnRefundRequestStarted?.Invoke( - new RefundRequestStartedEventArgs(productIdentifier) - ); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnRefundRequestStarted callback: {e.Message}"); - } + _storedCallbacks?.OnRefundRequestStarted?.Invoke( + new RefundRequestStartedEventArgs(productIdentifier) + ); } private void OnRefundRequestCompleted(string productIdentifier, string refundRequestStatus) { - try - { - RefundRequestStatus status; - switch (refundRequestStatus?.ToUpperInvariant()) - { - case "SUCCESS": - status = RefundRequestStatus.Success; - break; - case "USERCANCELLED": - case "USER_CANCELLED": - status = RefundRequestStatus.UserCancelled; - break; - default: - status = RefundRequestStatus.Error; - break; - } - - _storedCallbacks?.OnRefundRequestCompleted?.Invoke( - new RefundRequestCompletedEventArgs(productIdentifier, status) - ); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnRefundRequestCompleted callback: {e.Message}"); + RefundRequestStatus status; + switch (refundRequestStatus?.ToUpperInvariant()) + { + case "SUCCESS": + status = RefundRequestStatus.Success; + break; + case "USERCANCELLED": + case "USER_CANCELLED": + status = RefundRequestStatus.UserCancelled; + break; + default: + status = RefundRequestStatus.Error; + break; } + + _storedCallbacks?.OnRefundRequestCompleted?.Invoke( + new RefundRequestCompletedEventArgs(productIdentifier, status) + ); } private void OnManagementOptionSelected(string option, string url) { - try - { - CustomerCenterManagementOption optionEnum; - switch (option?.ToLowerInvariant()) - { - case "cancel": - optionEnum = CustomerCenterManagementOption.Cancel; - break; - case "custom_url": - optionEnum = CustomerCenterManagementOption.CustomUrl; - break; - case "missing_purchase": - optionEnum = CustomerCenterManagementOption.MissingPurchase; - break; - case "refund_request": - optionEnum = CustomerCenterManagementOption.RefundRequest; - break; - case "change_plans": - optionEnum = CustomerCenterManagementOption.ChangePlans; - break; - default: - optionEnum = CustomerCenterManagementOption.Unknown; - break; - } - - _storedCallbacks?.OnManagementOptionSelected?.Invoke( - new ManagementOptionSelectedEventArgs(optionEnum, url) - ); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnManagementOptionSelected callback: {e.Message}"); + CustomerCenterManagementOption optionEnum; + switch (option?.ToLowerInvariant()) + { + case "cancel": + optionEnum = CustomerCenterManagementOption.Cancel; + break; + case "custom_url": + optionEnum = CustomerCenterManagementOption.CustomUrl; + break; + case "missing_purchase": + optionEnum = CustomerCenterManagementOption.MissingPurchase; + break; + case "refund_request": + optionEnum = CustomerCenterManagementOption.RefundRequest; + break; + case "change_plans": + optionEnum = CustomerCenterManagementOption.ChangePlans; + break; + default: + optionEnum = CustomerCenterManagementOption.Unknown; + break; } + + _storedCallbacks?.OnManagementOptionSelected?.Invoke( + new ManagementOptionSelectedEventArgs(optionEnum, url) + ); } private void OnCustomActionSelected(string actionId, string purchaseIdentifier) { - try - { - _storedCallbacks?.OnCustomActionSelected?.Invoke( - new CustomActionSelectedEventArgs(actionId, purchaseIdentifier) - ); - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][Android] Error in OnCustomActionSelected callback: {e.Message}"); - } + _storedCallbacks?.OnCustomActionSelected?.Invoke( + new CustomActionSelectedEventArgs(actionId, purchaseIdentifier) + ); } private class CallbacksProxy : AndroidJavaProxy diff --git a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs index 612421ea..5c3ef992 100644 --- a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs @@ -111,134 +111,127 @@ private static void OnError() [AOT.MonoPInvokeCallback(typeof(CustomerCenterEventCallback))] private static void OnEvent(string eventName, string payload) { - try + switch (eventName) { - switch (eventName) - { - case "onRestoreStarted": - s_storedCallbacks?.OnRestoreStarted?.Invoke(); - break; - - case "onRestoreCompleted": - if (!string.IsNullOrEmpty(payload)) - { - var customerInfo = new Purchases.CustomerInfo(JSON.Parse(payload)); - s_storedCallbacks?.OnRestoreCompleted?.Invoke( - new RestoreCompletedEventArgs(customerInfo)); - } - break; - - case "onRestoreFailed": - if (!string.IsNullOrEmpty(payload)) - { - var error = new Purchases.Error(JSON.Parse(payload)); - s_storedCallbacks?.OnRestoreFailed?.Invoke( - new RestoreFailedEventArgs(error)); - } - break; - - case "onShowingManageSubscriptions": - s_storedCallbacks?.OnShowingManageSubscriptions?.Invoke(); - break; - - case "onRefundRequestStarted": - if (!string.IsNullOrEmpty(payload)) - { - s_storedCallbacks?.OnRefundRequestStarted?.Invoke( - new RefundRequestStartedEventArgs(payload)); - } - break; + case "onRestoreStarted": + s_storedCallbacks?.OnRestoreStarted?.Invoke(); + break; + + case "onRestoreCompleted": + if (!string.IsNullOrEmpty(payload)) + { + var customerInfo = new Purchases.CustomerInfo(JSON.Parse(payload)); + s_storedCallbacks?.OnRestoreCompleted?.Invoke( + new RestoreCompletedEventArgs(customerInfo)); + } + break; + + case "onRestoreFailed": + if (!string.IsNullOrEmpty(payload)) + { + var error = new Purchases.Error(JSON.Parse(payload)); + s_storedCallbacks?.OnRestoreFailed?.Invoke( + new RestoreFailedEventArgs(error)); + } + break; + + case "onShowingManageSubscriptions": + s_storedCallbacks?.OnShowingManageSubscriptions?.Invoke(); + break; + + case "onRefundRequestStarted": + if (!string.IsNullOrEmpty(payload)) + { + s_storedCallbacks?.OnRefundRequestStarted?.Invoke( + new RefundRequestStartedEventArgs(payload)); + } + break; + + case "onRefundRequestCompleted": + if (!string.IsNullOrEmpty(payload)) + { + var data = JSON.Parse(payload); + var productIdentifier = data["productIdentifier"]; + var statusString = data["refundRequestStatus"]; - case "onRefundRequestCompleted": - if (!string.IsNullOrEmpty(payload)) + RefundRequestStatus status; + switch (statusString?.Value?.ToUpperInvariant()) { - var data = JSON.Parse(payload); - var productIdentifier = data["productIdentifier"]; - var statusString = data["refundRequestStatus"]; - - RefundRequestStatus status; - switch (statusString?.Value?.ToUpperInvariant()) - { - case "SUCCESS": - status = RefundRequestStatus.Success; - break; - case "USERCANCELLED": - case "USER_CANCELLED": - status = RefundRequestStatus.UserCancelled; - break; - default: - status = RefundRequestStatus.Error; - break; - } - - s_storedCallbacks?.OnRefundRequestCompleted?.Invoke( - new RefundRequestCompletedEventArgs(productIdentifier, status)); + case "SUCCESS": + status = RefundRequestStatus.Success; + break; + case "USERCANCELLED": + case "USER_CANCELLED": + status = RefundRequestStatus.UserCancelled; + break; + default: + status = RefundRequestStatus.Error; + break; } - break; - case "onFeedbackSurveyCompleted": - if (!string.IsNullOrEmpty(payload)) - { - s_storedCallbacks?.OnFeedbackSurveyCompleted?.Invoke( - new FeedbackSurveyCompletedEventArgs(payload)); - } - break; + s_storedCallbacks?.OnRefundRequestCompleted?.Invoke( + new RefundRequestCompletedEventArgs(productIdentifier, status)); + } + break; + + case "onFeedbackSurveyCompleted": + if (!string.IsNullOrEmpty(payload)) + { + s_storedCallbacks?.OnFeedbackSurveyCompleted?.Invoke( + new FeedbackSurveyCompletedEventArgs(payload)); + } + break; + + case "onManagementOptionSelected": + if (!string.IsNullOrEmpty(payload)) + { + var data = JSON.Parse(payload); + var option = data["option"]?.Value; + var url = data["url"]?.Value; - case "onManagementOptionSelected": - if (!string.IsNullOrEmpty(payload)) + CustomerCenterManagementOption optionEnum; + switch (option?.ToLowerInvariant()) { - var data = JSON.Parse(payload); - var option = data["option"]?.Value; - var url = data["url"]?.Value; - - CustomerCenterManagementOption optionEnum; - switch (option?.ToLowerInvariant()) - { - case "cancel": - optionEnum = CustomerCenterManagementOption.Cancel; - break; - case "custom_url": - optionEnum = CustomerCenterManagementOption.CustomUrl; - break; - case "missing_purchase": - optionEnum = CustomerCenterManagementOption.MissingPurchase; - break; - case "refund_request": - optionEnum = CustomerCenterManagementOption.RefundRequest; - break; - case "change_plans": - optionEnum = CustomerCenterManagementOption.ChangePlans; - break; - default: - optionEnum = CustomerCenterManagementOption.Unknown; - break; - } - - s_storedCallbacks?.OnManagementOptionSelected?.Invoke( - new ManagementOptionSelectedEventArgs(optionEnum, url)); + case "cancel": + optionEnum = CustomerCenterManagementOption.Cancel; + break; + case "custom_url": + optionEnum = CustomerCenterManagementOption.CustomUrl; + break; + case "missing_purchase": + optionEnum = CustomerCenterManagementOption.MissingPurchase; + break; + case "refund_request": + optionEnum = CustomerCenterManagementOption.RefundRequest; + break; + case "change_plans": + optionEnum = CustomerCenterManagementOption.ChangePlans; + break; + default: + optionEnum = CustomerCenterManagementOption.Unknown; + break; } - break; - case "onCustomActionSelected": - if (!string.IsNullOrEmpty(payload)) - { - var data = JSON.Parse(payload); - var actionId = data["actionId"]?.Value; - var purchaseIdentifier = data["purchaseIdentifier"]?.Value; - - s_storedCallbacks?.OnCustomActionSelected?.Invoke( - new CustomActionSelectedEventArgs(actionId, purchaseIdentifier)); - } - break; + s_storedCallbacks?.OnManagementOptionSelected?.Invoke( + new ManagementOptionSelectedEventArgs(optionEnum, url)); + } + break; + + case "onCustomActionSelected": + if (!string.IsNullOrEmpty(payload)) + { + var data = JSON.Parse(payload); + var actionId = data["actionId"]?.Value; + var purchaseIdentifier = data["purchaseIdentifier"]?.Value; - default: - Debug.LogWarning($"[RevenueCatUI][iOS] Unknown customer center event: {eventName}"); - break; - } - } - catch (Exception e) - { - Debug.LogError($"[RevenueCatUI][iOS] Error handling customer center event '{eventName}': {e.Message}"); + s_storedCallbacks?.OnCustomActionSelected?.Invoke( + new CustomActionSelectedEventArgs(actionId, purchaseIdentifier)); + } + break; + + default: + Debug.LogWarning($"[RevenueCatUI][iOS] Unknown customer center event: {eventName}"); + break; } } } From 71a8b60b34b613b7f4d46a9e97b80745fd19d30b Mon Sep 17 00:00:00 2001 From: Cesar de la Vega <664544+vegaro@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:05:39 +0200 Subject: [PATCH 11/24] log clean up --- .../Scripts/CustomerCenterCallbacks.cs | 3 + .../Android/AndroidCustomerCenterPresenter.cs | 3 +- .../iOS/IOSCustomerCenterPresenter.cs | 2 - Subtester/Assets/Scripts/PurchasesListener.cs | 2726 ++++++++--------- 4 files changed, 1349 insertions(+), 1385 deletions(-) diff --git a/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs b/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs index 0079c895..4e599b31 100644 --- a/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs +++ b/RevenueCatUI/Scripts/CustomerCenterCallbacks.cs @@ -97,6 +97,9 @@ public CustomActionSelectedEventArgs(string actionId, string purchaseIdentifier /// /// Callbacks for Customer Center events. + /// + /// IMPORTANT: All callbacks execute on a background thread, not Unity's main thread. + /// You CANNOT directly call Unity APIs (GameObject, Transform, UI components, etc.) from these callbacks. /// public sealed class CustomerCenterCallbacks { diff --git a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs index da943acd..39bc63b6 100644 --- a/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/Android/AndroidCustomerCenterPresenter.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using RevenueCat; using RevenueCat.SimpleJSON; -using RevenueCatUI.Internal; using UnityEngine; using UnityEngine.Android; @@ -27,6 +26,7 @@ public AndroidCustomerCenterPresenter() catch (Exception e) { Debug.LogError($"[RevenueCatUI][Android] Failed to initialize RevenueCatUI plugin for Customer Center: {e.Message}"); + Debug.LogException(e); } } @@ -87,7 +87,6 @@ private void OnCustomerCenterDismissed() try { - Debug.Log("[RevenueCatUI][Android] Customer Center dismissed."); _current.TrySetResult(true); } catch (Exception e) diff --git a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs index 5c3ef992..d0d6ac51 100644 --- a/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/iOS/IOSCustomerCenterPresenter.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using RevenueCat; using RevenueCat.SimpleJSON; -using RevenueCatUI.Internal; using UnityEngine; namespace RevenueCatUI.Platforms @@ -67,7 +66,6 @@ private static void OnDismissed() try { - Debug.Log("[RevenueCatUI][iOS] Customer Center dismissed."); s_current.TrySetResult(true); } catch (Exception e) diff --git a/Subtester/Assets/Scripts/PurchasesListener.cs b/Subtester/Assets/Scripts/PurchasesListener.cs index 507446eb..6675727a 100644 --- a/Subtester/Assets/Scripts/PurchasesListener.cs +++ b/Subtester/Assets/Scripts/PurchasesListener.cs @@ -1,1381 +1,1345 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEditor; -using UnityEngine; -using UnityEngine.Events; -using UnityEngine.UI; - -public class PurchasesListener : Purchases.UpdatedCustomerInfoListener -{ - public RectTransform parentPanel; - public GameObject buttonPrefab; - public Text infoLabel; - - private bool simulatesAskToBuyInSandbox; - - private int minYOffsetForButtons = 40; // values lower than these don't work great with devices - // with safe areas on iOS - - private int minXOffsetForButtons = 20; - - private int xPaddingForButtons = 10; - private int yPaddingForButtons = 5; - - private int maxButtonsPerRow = 2; - private int currentButtons = 0; - - private Purchases.ProrationMode prorationMode = Purchases.ProrationMode.UnknownSubscriptionUpgradeDowngradePolicy; - private string currentProductId = ""; - - // Use this for initialization - private void Start() - { - CreateButton("Get Customer Info", GetCustomerInfo); - CreateButton("Get Offerings", GetOfferings); - CreateButton("Get Current Offering For Onboarding Placement", GetCurrentOfferingForPlacement); - CreateButton("Sync Attributes and Offerings", SyncAttributesAndOfferingsIfNeeded); - CreateButton("Sync Purchases", SyncPurchases); - CreateButton("Restore Purchases", RestorePurchases); - CreateButton("Can Make Payments", CanMakePayments); - CreateButton("Set Subs Attributes", SetSubscriberAttributes); - CreateButton("Log in as \"test\"", LogInAsTest); - CreateButton("Log in as random id", LogInAsRandomId); - CreateButton("Log out", LogOut); - CreateButton("Check Intro Eligibility", CheckIntroEligibility); - CreateButton("Get Promo Offer", GetPromotionalOffers); - CreateButton("Buy package w/discount", BuyFirstPackageWithDiscount); - CreateButton("Buy product w/discount", BuyFirstProductWithDiscount); - CreateButton("Code redemption sheet", PresentCodeRedemptionSheet); - CreateButton("Invalidate customer info cache", InvalidateCustomerInfoCache); - CreateButton("Get all products", GetAllProducts); - CreateButton("Toggle simulatesAskToBuyInSandbox", ToggleSimulatesAskToBuyInSandbox); - CreateButton("Is Anonymous", IsAnonymous); - CreateButton("Is Configured", IsConfigured); - CreateButton("Get AppUserId", GetAppUserId); - CreateButton("Show In-App Messages", ShowInAppMessages); - CreateButton("Get Amazon LWAConsentStatus", GetAmazonLWAConsentStatus); - CreateProrationModeButtons(); - CreatePurchasePackageButtons(); - CreatePurchasePackageForPlacementButtons(); - CreateButton("Get Virtual Currencies", GetVirtualCurrencies); - CreateButton("Get Cached Virtual Currencies", GetCachedVirtualCurrencies); - CreateButton("Invalidate Virtual Currencies Cache", InvalidateVirtualCurrenciesCache); - CreateButton("Purchase Product For WinBack Testing", PurchaseProductForWinBackTesting); - CreateButton("Fetch & Redeem WinBack for Product", FetchAndRedeemWinBackForProduct); - CreateButton("Purchase Package For WinBack Testing", PurchasePackageForWinBackTesting); - CreateButton("Fetch & Redeem WinBack for Package", FetchAndRedeemWinBackForPackage); - CreateButton("Get Storefront", GetStorefront); - CreateButton("Present Paywall", PresentPaywallResult); - CreateButton("Present Paywall with Options", PresentPaywallWithOptions); - CreateButton("Present Paywall for Offering", PresentPaywallForOffering); - CreateButton("Present Paywall If Needed", PresentPaywallIfNeeded); - CreateButton("Present Customer Center", PresentCustomerCenter); - - var purchases = GetComponent(); - purchases.SetLogLevel(Purchases.LogLevel.Verbose); - purchases.EnableAdServicesAttributionTokenCollection(); - } - - private void CreateProrationModeButtons() - { - foreach (Purchases.ProrationMode mode in Enum.GetValues(typeof(Purchases.ProrationMode))) - { - CreateButton("Proration mode: " + mode, () => - { - infoLabel.text = "ProrationMode set to " + mode; - prorationMode = mode; - }); - } - } - - private void GetStorefront() - { - var purchases = GetComponent(); - purchases.GetStorefront((storefront) => - { - if (storefront == null) - { - infoLabel.text = "Storefront is null"; - } - else - { - infoLabel.text = "Storefront: " + storefront.CountryCode; - } - }); - } - - private void CreatePurchasePackageButtons() - { - var purchases = GetComponent(); - purchases.GetOfferings((offerings, error) => - { - if (error != null) - { - LogError(error); - } - else - { - Debug.Log("offerings received " + offerings.ToString()); - - foreach (var package in offerings.Current.AvailablePackages) - { - Debug.Log("Package " + package); - if (package == null) continue; - var label = package.PackageType + " " + package.StoreProduct.PriceString; - CreateButton("Buy as Package: " + label, () => PurchasePackageButtonClicked(package)); - - CreateButton("Buy as Product: " + label, () => PurchaseProductButtonClicked(package.StoreProduct)); - - var options = package.StoreProduct.SubscriptionOptions; - if (options is not null) { - foreach (var subscriptionOption in options) { - List parts = new List(); - var label2 = package.PackageType; - - var phases = subscriptionOption.PricingPhases; - if (phases is not null) { - foreach (var pricingPhase in phases) { - var period = pricingPhase.BillingPeriod; - var price = pricingPhase.Price; - if (period is not null && price is not null) { - parts.Add(price.Formatted + " for " + period.ISO8601); - } - } - } else { - parts.Add("ITS SO NULL"); - } - var info = String.Join(" -> ", parts.ToArray()); - CreateButton(info, () => PurchaseSubscriptionOptionButtonClicked(subscriptionOption)); - } - } - } - } - }); - } - - private void CreatePurchasePackageForPlacementButtons() - { - var purchases = GetComponent(); - purchases.GetCurrentOfferingForPlacement("pizza", (offering, error) => - { - if (error != null) - { - LogError(error); - } - else - { - Debug.Log("offering for placement received " + offering.ToString()); - - foreach (var package in offering.AvailablePackages) - { - Debug.Log("Placement Package " + package); - if (package == null) continue; - var label = package.PackageType + " " + package.StoreProduct.PriceString; - CreateButton("Buy as Placement Package: " + label, () => PurchasePackageButtonClicked(package)); - - var options = package.StoreProduct.SubscriptionOptions; - if (options is not null) { - foreach (var subscriptionOption in options) { - List parts = new List(); - var label2 = package.PackageType; - - var phases = subscriptionOption.PricingPhases; - if (phases is not null) { - foreach (var pricingPhase in phases) { - var period = pricingPhase.BillingPeriod; - var price = pricingPhase.Price; - if (period is not null && price is not null) { - parts.Add(price.Formatted + " for " + period.ISO8601); - } - } - } else { - parts.Add("ITS SO NULL"); - } - var info = "PCMNT: " + String.Join(" -> ", parts.ToArray()); - CreateButton(info, () => PurchaseSubscriptionOptionButtonClicked(subscriptionOption)); - } - } - } - } - }); - } - - void PresentPaywallResult() - { - Debug.Log("Subtester: launching paywall"); - if (infoLabel != null) infoLabel.text = "Launching paywall..."; - StartCoroutine(PresentPaywallCoroutine()); - } - - void PresentPaywallWithOptions() - { - Debug.Log("Subtester: launching paywall with options"); - if (infoLabel != null) infoLabel.text = "Launching paywall with options..."; - StartCoroutine(PresentPaywallWithOptionsCoroutine()); - } - - void PresentPaywallForOffering() - { - Debug.Log("Subtester: launching paywall for specific offering"); - if (infoLabel != null) infoLabel.text = "Launching paywall for offering..."; - StartCoroutine(PresentPaywallForOfferingCoroutine()); - } - - void PresentPaywallIfNeeded() - { - Debug.Log("Subtester: launching paywall if needed for test entitlement"); - if (infoLabel != null) infoLabel.text = "Checking entitlement and launching paywall if needed..."; - StartCoroutine(PresentPaywallIfNeededCoroutine()); - } - - void PresentCustomerCenter() - { - Debug.Log("Subtester: launching customer center"); - if (infoLabel != null) infoLabel.text = "Launching Customer Center..."; - StartCoroutine(PresentCustomerCenterCoroutine()); - } - - private System.Collections.IEnumerator PresentPaywallCoroutine() - { - var task = RevenueCatUI.PaywallsPresenter.Present(); - while (!task.IsCompleted) { yield return null; } - - var result = task.Result; - Debug.Log("Subtester: paywall result = " + result); - - if (infoLabel != null) - { - string status = GetPaywallResultStatus(result); - - if (result.Result == RevenueCatUI.PaywallResultType.Purchased || - result.Result == RevenueCatUI.PaywallResultType.Restored) - { - GetComponent().GetCustomerInfo((customerInfo, error) => { - if (error != null) - { - Debug.LogError("Subtester: Error refreshing customer info after " + result.Result + ": " + error); - } - else - { - Debug.Log("Subtester: Refreshed customer info after " + result.Result); - DisplayCustomerInfo(customerInfo); - } - }); - } - - infoLabel.text = $"Paywall result: {status}"; - Debug.Log($"Subtester: {status}"); - } - } - - private System.Collections.IEnumerator PresentCustomerCenterCoroutine() - { - var callbacks = new RevenueCatUI.CustomerCenterCallbacks - { - OnFeedbackSurveyCompleted = (args) => - { - Debug.Log($"Subtester: OnFeedbackSurveyCompleted - Option ID: {args.FeedbackSurveyOptionId}"); - if (infoLabel != null) - { - infoLabel.text = $"Feedback survey completed: {args.FeedbackSurveyOptionId}"; - } - }, - OnShowingManageSubscriptions = () => - { - Debug.Log("Subtester: OnShowingManageSubscriptions"); - if (infoLabel != null) - { - infoLabel.text = "Showing manage subscriptions"; - } - }, - OnRestoreCompleted = (args) => - { - Debug.Log($"Subtester: OnRestoreCompleted - CustomerInfo: {args.CustomerInfo}"); - if (infoLabel != null) - { - DisplayCustomerInfo(args.CustomerInfo); - } - }, - OnRestoreFailed = (args) => - { - Debug.Log($"Subtester: OnRestoreFailed - Error: {args.Error}"); - if (infoLabel != null) - { - LogError(args.Error); - } - }, - OnRestoreStarted = () => - { - Debug.Log("Subtester: OnRestoreStarted"); - if (infoLabel != null) - { - infoLabel.text = "Restore started..."; - } - }, - OnRefundRequestStarted = (args) => - { - Debug.Log($"Subtester: OnRefundRequestStarted - Product: {args.ProductIdentifier}"); - if (infoLabel != null) - { - infoLabel.text = $"Refund request started for: {args.ProductIdentifier}"; - } - }, - OnRefundRequestCompleted = (args) => - { - Debug.Log($"Subtester: OnRefundRequestCompleted - Product: {args.ProductIdentifier}, Status: {args.RefundRequestStatus}"); - if (infoLabel != null) - { - infoLabel.text = $"Refund request completed for {args.ProductIdentifier}: {args.RefundRequestStatus}"; - } - }, - OnManagementOptionSelected = (args) => - { - string urlInfo = args.Url != null ? $", URL: {args.Url}" : ""; - Debug.Log($"Subtester: OnManagementOptionSelected - Option: {args.Option}{urlInfo}"); - if (infoLabel != null) - { - infoLabel.text = $"Management option selected: {args.Option}{urlInfo}"; - } - }, - OnCustomActionSelected = (args) => - { - string purchaseInfo = args.PurchaseIdentifier != null ? $", Purchase: {args.PurchaseIdentifier}" : ""; - Debug.Log($"Subtester: OnCustomActionSelected - Action: {args.ActionId}{purchaseInfo}"); - if (infoLabel != null) - { - infoLabel.text = $"Custom action selected: {args.ActionId}{purchaseInfo}"; - } - } - }; - - var task = RevenueCatUI.CustomerCenterPresenter.Present(callbacks); - while (!task.IsCompleted) { yield return null; } - - Debug.Log("Subtester: customer center finished."); - - if (infoLabel != null) - { - infoLabel.text = "Customer Center finished"; - } - } - - private System.Collections.IEnumerator PresentPaywallWithOptionsCoroutine() - { - var options = new RevenueCatUI.PaywallOptions(displayCloseButton: false); - - var task = RevenueCatUI.PaywallsPresenter.Present(options); - while (!task.IsCompleted) { yield return null; } - - var result = task.Result; - Debug.Log("Subtester: paywall with options result = " + result); - - if (infoLabel != null) - { - infoLabel.text = $"Paywall with options result: {GetPaywallResultStatus(result)}"; - } - } - - private System.Collections.IEnumerator PresentPaywallForOfferingCoroutine() - { - // First get available offerings to use one as an example - var purchases = GetComponent(); - var offeringsTask = new System.Threading.Tasks.TaskCompletionSource(); - - purchases.GetOfferings((offerings, error) => - { - if (error != null) - { - offeringsTask.SetException(new System.Exception(error.ToString())); - } - else - { - offeringsTask.SetResult(offerings); - } - }); - - while (!offeringsTask.Task.IsCompleted) { yield return null; } - - if (offeringsTask.Task.IsFaulted) - { - Debug.LogError("Subtester: Error getting offerings: " + offeringsTask.Task.Exception.GetBaseException().Message); - if (infoLabel != null) - { - infoLabel.text = "Error getting offerings: " + offeringsTask.Task.Exception.GetBaseException().Message; - } - yield break; - } - - var offerings = offeringsTask.Task.Result; - - // Random offering from available offerings - Purchases.Offering randomOffering = null; - if (offerings?.All?.Count > 0) - { - var allOfferings = offerings.All.Values.ToList(); - var randomIndex = UnityEngine.Random.Range(0, allOfferings.Count); - randomOffering = allOfferings[randomIndex]; - } - else if (offerings?.Current != null) - { - randomOffering = offerings.Current; - } - - Debug.Log($"Subtester: Presenting paywall for offering: {randomOffering?.Identifier ?? "current"}"); - - var options = randomOffering != null - ? new RevenueCatUI.PaywallOptions(randomOffering, displayCloseButton: true) - : new RevenueCatUI.PaywallOptions(displayCloseButton: true); - var task = RevenueCatUI.PaywallsPresenter.Present(options); - while (!task.IsCompleted) { yield return null; } - - var result = task.Result; - Debug.Log("Subtester: paywall for offering result = " + result); - - if (infoLabel != null) - { - infoLabel.text = $"Paywall for offering '{randomOffering?.Identifier ?? "current"}' result: {GetPaywallResultStatus(result)}"; - } - } - - private System.Collections.IEnumerator PresentPaywallIfNeededCoroutine() - { - // First get available offerings to use one as an example - var purchases = GetComponent(); - var offeringsTask = new System.Threading.Tasks.TaskCompletionSource(); - - purchases.GetOfferings((offerings, error) => - { - if (error != null) - { - offeringsTask.SetException(new System.Exception(error.ToString())); - } - else - { - offeringsTask.SetResult(offerings); - } - }); - - while (!offeringsTask.Task.IsCompleted) { yield return null; } - - if (offeringsTask.Task.IsFaulted) - { - Debug.LogError("Subtester: Error getting offerings: " + offeringsTask.Task.Exception.GetBaseException().Message); - if (infoLabel != null) - { - infoLabel.text = "Error getting offerings: " + offeringsTask.Task.Exception.GetBaseException().Message; - } - yield break; - } - - var offerings = offeringsTask.Task.Result; - // Random offering from available offerings - Purchases.Offering randomOffering = null; - if (offerings?.All?.Count > 0) - { - var allOfferings = offerings.All.Values.ToList(); - var randomIndex = UnityEngine.Random.Range(0, allOfferings.Count); - randomOffering = allOfferings[randomIndex]; - } - else if (offerings?.Current != null) - { - randomOffering = offerings.Current; - } - - // Test with a real entitlement - change this to test different scenarios - var testEntitlement = "pro_level_b"; // User should have this, so paywall should NOT be presented - - Debug.Log($"Subtester: Testing presentPaywallIfNeeded for entitlement: {testEntitlement}, offering: {randomOffering?.Identifier ?? "current"}"); - - var options = randomOffering != null - ? new RevenueCatUI.PaywallOptions(randomOffering, displayCloseButton: true) - : new RevenueCatUI.PaywallOptions(displayCloseButton: true); - var task = RevenueCatUI.PaywallsPresenter.PresentIfNeeded(testEntitlement, options); - while (!task.IsCompleted) { yield return null; } - - var result = task.Result; - Debug.Log("Subtester: paywall if needed result = " + result); - - if (infoLabel != null) - { - var status = GetPaywallResultStatus(result); - var message = $"PaywallIfNeeded for '{testEntitlement}' result: {status}"; - if (result.Result == RevenueCatUI.PaywallResultType.NotPresented) - { - message += " (User already has entitlement)"; - } - infoLabel.text = message; - } - } - - private string GetPaywallResultStatus(RevenueCatUI.PaywallResult result) - { - switch (result.Result) - { - case RevenueCatUI.PaywallResultType.Purchased: - return "PURCHASED - User completed a purchase"; - case RevenueCatUI.PaywallResultType.Restored: - return "RESTORED - User restored previous purchases"; - case RevenueCatUI.PaywallResultType.Cancelled: - return "CANCELLED - User dismissed the paywall"; - case RevenueCatUI.PaywallResultType.Error: - return "ERROR - An error occurred during paywall"; - case RevenueCatUI.PaywallResultType.NotPresented: - return "NOT PRESENTED - Paywall was not needed"; - default: - return $"UNKNOWN - Received: {result}"; - } - } - - private void CreateButton(string label, UnityAction action) - { - var button = Instantiate(buttonPrefab, parentPanel, false); - var buttonTransform = (RectTransform)button.transform; - - var rect = buttonTransform.rect; - var height = rect.height; - var width = rect.width; - - var yPos = -1 // unity counts from the bottom left, so negative values give you buttons that are - // lower in the screen - * (currentButtons / maxButtonsPerRow // how many buttons are on top of this one - * (height + - yPaddingForButtons) // distance from start of the first button to the start of the second - + minYOffsetForButtons // min distance to the top of the container - + height / 2); // y position starts from the center - var xPos = (currentButtons % maxButtonsPerRow) // 0 for first column, 1 for second column - * (width + xPaddingForButtons) // distance from start of the first button to the start of the second - + minXOffsetForButtons + (width / 2); // x position starts from the center - - // anchors position calculation to make it easier to reason about - var newButtonTransform = (RectTransform)button.transform; - newButtonTransform.anchorMin = new Vector2(0, 1); - newButtonTransform.anchorMax = new Vector2(0, 1); - - newButtonTransform.anchoredPosition = new Vector2(xPos, yPos); - - var tempButton = button.GetComponent