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; }