diff --git a/RevenueCat/Scripts/PresentedOfferingContext.cs b/RevenueCat/Scripts/PresentedOfferingContext.cs index 578580a9..55d14f6d 100644 --- a/RevenueCat/Scripts/PresentedOfferingContext.cs +++ b/RevenueCat/Scripts/PresentedOfferingContext.cs @@ -24,6 +24,13 @@ public PresentedOfferingContext(JSONNode response) } } + public PresentedOfferingContext(string offeringIdentifier) + { + OfferingIdentifier = offeringIdentifier; + PlacementIdentifier = null; + TargetingContext = null; + } + public override string ToString() { return $"{nameof(OfferingIdentifier)}: {OfferingIdentifier}\n" + diff --git a/RevenueCatUI/Scripts/PaywallOptions.cs b/RevenueCatUI/Scripts/PaywallOptions.cs index 2bd68362..cc61dc82 100644 --- a/RevenueCatUI/Scripts/PaywallOptions.cs +++ b/RevenueCatUI/Scripts/PaywallOptions.cs @@ -2,30 +2,57 @@ namespace RevenueCatUI { + internal abstract class OfferingSelection + { + internal sealed class OfferingType : OfferingSelection + { + public Purchases.Offering Offering { get; } + + public OfferingType(Purchases.Offering offering) + { + Offering = offering; + } + + internal override Purchases.Offering GetOffering() => Offering; + internal override string GetOfferingIdentifier() => Offering.Identifier; + internal override Purchases.PresentedOfferingContext GetPresentedOfferingContext() => + Offering.AvailablePackages != null && Offering.AvailablePackages.Count > 0 + ? Offering.AvailablePackages[0].PresentedOfferingContext + : null; + } + + internal sealed class IdOnly : OfferingSelection + { + public string OfferingId { get; } + private Purchases.PresentedOfferingContext _presentedOfferingContext; + + public IdOnly(string offeringId) + { + OfferingId = offeringId; + _presentedOfferingContext = new Purchases.PresentedOfferingContext(offeringId); + } + + internal override Purchases.Offering GetOffering() => null; + internal override string GetOfferingIdentifier() => OfferingId; + internal override Purchases.PresentedOfferingContext GetPresentedOfferingContext() => _presentedOfferingContext; + } + + internal abstract Purchases.Offering GetOffering(); + internal abstract string GetOfferingIdentifier(); + internal abstract Purchases.PresentedOfferingContext GetPresentedOfferingContext(); + } + /// /// Options for configuring paywall presentation. /// [Serializable] public class PaywallOptions { - /// - /// The offering to present. - /// If not provided, the current offering will be used. - /// - public Purchases.Offering Offering { get; set; } + internal readonly OfferingSelection _offeringSelection; - /// - /// Whether to display a close button on the paywall. - /// Only applicable for original template paywalls, ignored for V2 Paywalls. - /// - public bool DisplayCloseButton { get; set; } = false; - - internal string OfferingIdentifier => Offering?.Identifier; - - internal Purchases.PresentedOfferingContext PresentedOfferingContext => - Offering?.AvailablePackages != null && Offering.AvailablePackages.Count > 0 - ? Offering.AvailablePackages[0].PresentedOfferingContext - : null; + internal bool DisplayCloseButton { get; } + internal string OfferingIdentifier => _offeringSelection?.GetOfferingIdentifier(); + internal Purchases.PresentedOfferingContext PresentedOfferingContext => _offeringSelection?.GetPresentedOfferingContext(); /// /// Creates a new PaywallOptions instance. @@ -34,6 +61,7 @@ public class PaywallOptions /// Whether to display a close button. Only applicable for original template paywalls, ignored for V2 Paywalls. public PaywallOptions(bool displayCloseButton = false) { + _offeringSelection = null; DisplayCloseButton = displayCloseButton; } @@ -44,7 +72,13 @@ public PaywallOptions(bool displayCloseButton = false) /// Whether to display a close button. Only applicable for original template paywalls, ignored for V2 Paywalls. public PaywallOptions(Purchases.Offering offering, bool displayCloseButton = false) { - Offering = offering; + _offeringSelection = offering != null ? new OfferingSelection.OfferingType(offering) : null; + DisplayCloseButton = displayCloseButton; + } + + internal PaywallOptions(string offeringIdentifier, bool displayCloseButton = false) + { + _offeringSelection = !string.IsNullOrEmpty(offeringIdentifier) ? new OfferingSelection.IdOnly(offeringIdentifier) : null; DisplayCloseButton = displayCloseButton; } } diff --git a/RevenueCatUI/Scripts/PaywallsBehaviour.cs b/RevenueCatUI/Scripts/PaywallsBehaviour.cs index 08896c79..651e2684 100644 --- a/RevenueCatUI/Scripts/PaywallsBehaviour.cs +++ b/RevenueCatUI/Scripts/PaywallsBehaviour.cs @@ -1,32 +1,198 @@ +using System; using System.Threading.Tasks; using UnityEngine; +using UnityEngine.Events; namespace RevenueCatUI { /// - /// MonoBehaviour helper that forwards to the static PaywallsPresenter API so paywalls can be driven from scenes. + /// MonoBehaviour component for presenting RevenueCat Paywalls from the Unity Editor. + /// Provides an alternative to PaywallsPresenter for developers who prefer configuring + /// Paywalls through Unity's Inspector interface. /// + [AddComponentMenu("RevenueCat/Paywalls Behaviour")] public class PaywallsBehaviour : MonoBehaviour { - /// - /// Presents a paywall configured in the RevenueCat dashboard. - /// - /// Options for presenting the paywall. - /// A describing the outcome. - public async Task PresentPaywall(PaywallOptions options = null) + [Header("Paywall Options")] + [Tooltip("The identifier of the offering to present. Leave empty to use the current offering.")] + [SerializeField] private string offeringIdentifier; + + [Tooltip("Whether to display a close button on the paywall (only for original template RevenueCat Paywalls).")] + [SerializeField] private bool displayCloseButton = false; + + [Header("Conditional Presentation")] + [Tooltip("If set, the paywall will only be presented if the user doesn't have this entitlement.")] + [SerializeField] private string requiredEntitlementIdentifier; + + [Header("Events")] + [Tooltip("Invoked when the user completes a purchase and the paywall is dismissed.")] + public UnityEvent OnPurchased = new UnityEvent(); + + [Tooltip("Invoked when the user restores purchases and the paywall is dismissed.")] + public UnityEvent OnRestored = new UnityEvent(); + + [Tooltip("Invoked when the user cancels the paywall and the paywall is dismissed.")] + public UnityEvent OnCancelled = new UnityEvent(); + + [Tooltip("Invoked when the paywall was not presented, for example when the user already has the required entitlement).")] + public UnityEvent OnNotPresented = new UnityEvent(); + + [Tooltip("Invoked when an error occurs.")] + public UnityEvent OnError = new UnityEvent(); + + private bool isPresenting = false; + + public string OfferingIdentifier + { + get => offeringIdentifier; + set => offeringIdentifier = value; + } + + public bool DisplayCloseButton + { + get => displayCloseButton; + set => displayCloseButton = value; + } + + public string RequiredEntitlementIdentifier { - return await PaywallsPresenter.Present(options); + get => requiredEntitlementIdentifier; + set => requiredEntitlementIdentifier = value; } /// - /// Presents a paywall only if the user does not have the specified entitlement. + /// Presents the paywall with the configured options. + /// Can be called from Unity UI buttons or programmatically. /// - /// Entitlement identifier to check before presenting. - /// Options for presenting the paywall. - /// A describing the outcome. - public async Task PresentPaywallIfNeeded(string requiredEntitlementIdentifier, PaywallOptions options = null) + public async void PresentPaywall() { - return await PaywallsPresenter.PresentIfNeeded(requiredEntitlementIdentifier, options); + if (isPresenting) + { + Debug.LogWarning("[RevenueCatUI] Paywall is already being presented."); + return; + } + + isPresenting = true; + + try + { + var options = CreateOptions(); + PaywallResult result; + + if (!string.IsNullOrEmpty(requiredEntitlementIdentifier)) + { + result = await PaywallsPresenter.PresentIfNeeded(requiredEntitlementIdentifier, options); + } + else + { + result = await PaywallsPresenter.Present(options); + } + + HandleResult(result); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI] Exception in PaywallsBehaviour: {e.Message}"); + HandleResult(PaywallResult.Error); + } + finally + { + isPresenting = false; + } + } + + private async void PresentPaywallIfNeeded(string entitlementIdentifier) + { + if (string.IsNullOrEmpty(entitlementIdentifier)) + { + Debug.LogError("[RevenueCatUI] Entitlement identifier cannot be null or empty."); + HandleResult(PaywallResult.Error); + return; + } + + if (isPresenting) + { + Debug.LogWarning("[RevenueCatUI] Paywall is already being presented."); + return; + } + + isPresenting = true; + + try + { + var options = CreateOptions(); + var result = await PaywallsPresenter.PresentIfNeeded(entitlementIdentifier, options); + HandleResult(result); + } + catch (Exception e) + { + Debug.LogError($"[RevenueCatUI] Exception in PaywallsBehaviour: {e.Message}"); + HandleResult(PaywallResult.Error); + } + finally + { + isPresenting = false; + } + } + + private PaywallOptions CreateOptions() + { + if (string.IsNullOrEmpty(offeringIdentifier)) + { + return new PaywallOptions(displayCloseButton); + } + + return new PaywallOptions(offeringIdentifier, displayCloseButton); + } + + private void HandleResult(PaywallResult result) + { + if (result == null) + { + Debug.Log("[RevenueCatUI] Received null PaywallResult."); + OnError?.Invoke(); + return; + } + + switch (result.Result) + { + case PaywallResultType.Purchased: + if (OnPurchased.GetPersistentEventCount() == 0) + { + Debug.Log("[RevenueCatUI] Paywall purchase completed but OnPurchased event has no listeners."); + } + OnPurchased?.Invoke(); + break; + case PaywallResultType.Restored: + if (OnRestored.GetPersistentEventCount() == 0) + { + Debug.Log("[RevenueCatUI] Paywall restore completed but OnRestored event has no listeners."); + } + OnRestored?.Invoke(); + break; + case PaywallResultType.Cancelled: + if (OnCancelled.GetPersistentEventCount() == 0) + { + Debug.Log("[RevenueCatUI] Paywall cancelled but OnCancelled event has no listeners."); + } + OnCancelled?.Invoke(); + break; + case PaywallResultType.NotPresented: + if (OnNotPresented.GetPersistentEventCount() == 0) + { + Debug.Log("[RevenueCatUI] Paywall not presented but OnNotPresented event has no listeners."); + } + OnNotPresented?.Invoke(); + break; + case PaywallResultType.Error: + if (OnError.GetPersistentEventCount() == 0) + { + Debug.Log("[RevenueCatUI] Paywall error occurred but OnError event has no listeners."); + } + OnError?.Invoke(); + break; + } } } } + diff --git a/RevenueCatUI/Scripts/Platforms/Android/AndroidPaywallPresenter.cs b/RevenueCatUI/Scripts/Platforms/Android/AndroidPaywallPresenter.cs index 232fc121..f2a7e298 100644 --- a/RevenueCatUI/Scripts/Platforms/Android/AndroidPaywallPresenter.cs +++ b/RevenueCatUI/Scripts/Platforms/Android/AndroidPaywallPresenter.cs @@ -49,13 +49,14 @@ public Task PresentPaywallAsync(PaywallOptions options) _current = new TaskCompletionSource(); try { - var offering = options?.OfferingIdentifier; + var offeringIdentifier = options?.OfferingIdentifier; var displayCloseButton = options?.DisplayCloseButton ?? false; var presentedOfferingContextJson = options?.PresentedOfferingContext?.ToJsonString(); - Debug.Log($"[RevenueCatUI][Android] presentPaywall offering='{offering ?? ""}', displayCloseButton={displayCloseButton}"); + Debug.Log($"[RevenueCatUI][Android] presentPaywall offering='{offeringIdentifier ?? ""}', " + + $"displayCloseButton={displayCloseButton}"); var currentActivity = AndroidApplication.currentActivity; - _plugin.CallStatic("presentPaywall", new object[] { currentActivity, offering, presentedOfferingContextJson, displayCloseButton }); + _plugin.CallStatic("presentPaywall", new object[] { currentActivity, offeringIdentifier, presentedOfferingContextJson, displayCloseButton }); } catch (Exception e) { @@ -83,12 +84,13 @@ public Task PresentPaywallIfNeededAsync(string requiredEntitlemen _current = new TaskCompletionSource(); try { - var offering = options?.OfferingIdentifier; + var offeringIdentifier = options?.OfferingIdentifier; var displayCloseButton = options?.DisplayCloseButton ?? true; var presentedOfferingContextJson = options?.PresentedOfferingContext?.ToJsonString(); - Debug.Log($"[RevenueCatUI][Android] presentPaywallIfNeeded entitlement='{requiredEntitlementIdentifier}', offering='{offering ?? ""}', displayCloseButton={displayCloseButton}"); + Debug.Log($"[RevenueCatUI][Android] presentPaywallIfNeeded entitlement='{requiredEntitlementIdentifier}', '" + + $"offering='{offeringIdentifier ?? ""}', displayCloseButton={displayCloseButton}"); var currentActivity = AndroidApplication.currentActivity; - _plugin.CallStatic("presentPaywallIfNeeded", new object[] { currentActivity, requiredEntitlementIdentifier, offering, presentedOfferingContextJson, displayCloseButton }); + _plugin.CallStatic("presentPaywallIfNeeded", new object[] { currentActivity, requiredEntitlementIdentifier, offeringIdentifier, presentedOfferingContextJson, displayCloseButton }); } catch (Exception e) { diff --git a/Subtester/Assets/Scripts/PaywallResultHandler.cs b/Subtester/Assets/Scripts/PaywallResultHandler.cs new file mode 100644 index 00000000..7e9449b6 --- /dev/null +++ b/Subtester/Assets/Scripts/PaywallResultHandler.cs @@ -0,0 +1,53 @@ +using UnityEngine; +using UnityEngine.UI; +using RevenueCatUI; + +public class PaywallResultHandler : MonoBehaviour +{ + [SerializeField] private Text infoLabel; + + public void OnPaywallPurchased() + { + Debug.Log("User purchased!"); + if (infoLabel != null) + { + infoLabel.text = "PURCHASED - User completed a purchase"; + } + } + + public void OnPaywallRestored() + { + Debug.Log("User restored purchases"); + if (infoLabel != null) + { + infoLabel.text = "RESTORED - User restored previous purchases"; + } + } + + public void OnPaywallCancelled() + { + Debug.Log("User cancelled the paywall"); + if (infoLabel != null) + { + infoLabel.text = "CANCELLED - User dismissed the paywall"; + } + } + + public void OnPaywallNotPresented() + { + Debug.Log("Paywall not needed - user already has access"); + if (infoLabel != null) + { + infoLabel.text = "NOT PRESENTED - User already has entitlement"; + } + } + + public void OnPaywallError() + { + Debug.LogError("Error presenting paywall"); + if (infoLabel != null) + { + infoLabel.text = "ERROR - An error occurred during paywall"; + } + } +} diff --git a/Subtester/Assets/Scripts/PaywallResultHandler.cs.meta b/Subtester/Assets/Scripts/PaywallResultHandler.cs.meta new file mode 100644 index 00000000..8a8ea79e --- /dev/null +++ b/Subtester/Assets/Scripts/PaywallResultHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7b6da05e14f2d4ed2bf16f2d1fa80ed4 \ No newline at end of file diff --git a/Subtester/Assets/Scripts/PurchasesListener.cs b/Subtester/Assets/Scripts/PurchasesListener.cs index 1f927698..b2c26747 100644 --- a/Subtester/Assets/Scripts/PurchasesListener.cs +++ b/Subtester/Assets/Scripts/PurchasesListener.cs @@ -263,10 +263,7 @@ private System.Collections.IEnumerator PresentPaywallCoroutine() private System.Collections.IEnumerator PresentPaywallWithOptionsCoroutine() { - var options = new RevenueCatUI.PaywallOptions - { - DisplayCloseButton = false - }; + var options = new RevenueCatUI.PaywallOptions(displayCloseButton: false); var task = RevenueCatUI.PaywallsPresenter.Present(options); while (!task.IsCompleted) { yield return null; }