diff --git a/content/docs/android/changelog.mdx b/content/docs/android/changelog.mdx
index bb4f8dc..57365ca 100644
--- a/content/docs/android/changelog.mdx
+++ b/content/docs/android/changelog.mdx
@@ -3,6 +3,17 @@ title: "Changelog"
description: "Release notes for the Superwall Android SDK"
---
+## 2.6.7
+
+### Enhancements
+- Adds permission granting and callbacks to/from paywalls
+- Adds `PaywallPreloadStart` and `PaywallPreloadComplete` events
+
+### Fixes
+- Fix handling of deep links when paywall is detached
+- Enables permission granting from paywall and callbacks
+- Fix crash when handling drawer style paywalls with 100% height
+
## 2.6.6
## Enhancements
diff --git a/content/docs/android/guides/advanced/meta.json b/content/docs/android/guides/advanced/meta.json
index 3b4222e..00ce388 100644
--- a/content/docs/android/guides/advanced/meta.json
+++ b/content/docs/android/guides/advanced/meta.json
@@ -6,6 +6,7 @@
"using-the-presentation-handler",
"viewing-purchased-products",
"custom-paywall-actions",
+ "request-permissions-from-paywalls",
"observer-mode",
"direct-purchasing",
"game-controller-support",
diff --git a/content/docs/android/guides/advanced/request-permissions-from-paywalls.mdx b/content/docs/android/guides/advanced/request-permissions-from-paywalls.mdx
new file mode 100644
index 0000000..0d2850b
--- /dev/null
+++ b/content/docs/android/guides/advanced/request-permissions-from-paywalls.mdx
@@ -0,0 +1,74 @@
+---
+title: "Request permissions from paywalls"
+description: "Trigger Android runtime permission dialogs directly from a Superwall paywall action."
+---
+
+## Overview
+
+Use the **Request permission** action in the paywall editor when you want to gate features behind Android permissions without bouncing users back to native screens. When the user taps the element, the SDK:
+
+- Presents the corresponding Android system dialog.
+- Emits analytics events (`permission_requested`, `permission_granted`, `permission_denied`).
+- Sends the result back to the paywall so you can branch the UI (for example, swap a checklist item for a success state).
+
+## Add the action in the editor
+
+1. Open your paywall, select the button (or any element) that should prompt the permission, and set its action to **Request permission**.
+2. Choose the permission you want to request. You can wire multiple buttons if you need to prime several permissions in a single flow.
+3. Republish the paywall. No extra SDK configuration is required beyond having the proper `AndroidManifest.xml` entries.
+
+## Declare the permissions in `AndroidManifest.xml`
+
+| Editor option | `permission_type` sent from the paywall | Required manifest entries | Notes |
+|---------------|-----------------------------------------|---------------------------|-------|
+| Notifications | `notification` | `` (API 33+) | Devices below Android 13 do not require a runtime permission; the SDK reports `granted` immediately. |
+| Location (Foreground) | `location` | `` | Also covers coarse location because FINE implies COARSE. |
+| Location (Background) | `background_location` | Foreground entry above **and** `` (API 29+) | The SDK first ensures foreground access, then escalates to background. |
+| Photos / Images | `read_images` | `` (API 33+) or `READ_EXTERNAL_STORAGE` for older OS versions | Automatically picks the right permission at runtime. |
+| Videos | `read_video` | `` (API 33+) or `READ_EXTERNAL_STORAGE` pre-33 | |
+| Contacts | `contacts` | `` | |
+| Camera | `camera` | `` | |
+
+If a manifest entry is missing—or the permission is unsupported on the current OS level—the SDK responds with an `unsupported` status so you can show fallback copy.
+
+## Analytics and delegate callbacks
+
+Forward the new events through `SuperwallDelegate.handleSuperwallEvent` to keep your analytics platform and feature flags in sync:
+
+```kotlin
+override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
+ when (val event = eventInfo.event) {
+ is SuperwallEvent.PermissionRequested -> {
+ analytics.track("permission_requested", mapOf(
+ "permission" to event.permissionName,
+ "paywall_id" to event.paywallIdentifier
+ ))
+ }
+ is SuperwallEvent.PermissionGranted -> {
+ FeatureFlags.unlock(event.permissionName)
+ }
+ is SuperwallEvent.PermissionDenied -> {
+ Alerts.showPermissionDeclinedSheet(event.permissionName)
+ }
+ else -> Unit
+ }
+}
+```
+
+You can also log the newer [`customerInfoDidChange`](/android/sdk-reference/SuperwallDelegate#customerinfodidchangefrom-customerinfo-to-customerinfo) callback if the permission subsequently unlocks new paywalls that grant entitlements.
+
+## Status values returned to the paywall
+
+The paywall receives a `permission_result` web event with:
+
+- `granted` – The system dialog reported success (or no dialog was needed).
+- `denied` – The user denied the request or previously denied it.
+- `unsupported` – The platform or manifest doesn't allow the requested permission.
+
+Use Liquid or custom Javascript inside the paywall to branch on these statuses—for example, replace a “Grant notification access” button with a checkmark when the result equals `granted`.
+
+## Troubleshooting
+
+- Seeing `unsupported`? Double-check the manifest entries above and confirm the permission exists on the device's API level (for example, notification permissions only apply on Android 13+).
+- Nothing happens when you tap the button? Ensure the action is set to **Request permission** in the released paywall version.
+- Want to provide next steps after a denial? Listen for `PermissionDenied` in your delegate to deep-link users into Settings or show educational copy.
diff --git a/content/docs/android/index.mdx b/content/docs/android/index.mdx
index 32fee8f..83c4f0a 100644
--- a/content/docs/android/index.mdx
+++ b/content/docs/android/index.mdx
@@ -34,4 +34,4 @@ If you have feedback on any of our docs, please leave a rating and message at th
If you have any issues with the SDK, please [open an issue on GitHub](https://github.com/superwall/superwall-android/issues).
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/content/docs/android/quickstart/tracking-subscription-state.mdx b/content/docs/android/quickstart/tracking-subscription-state.mdx
index 14b733c..6d7507f 100644
--- a/content/docs/android/quickstart/tracking-subscription-state.mdx
+++ b/content/docs/android/quickstart/tracking-subscription-state.mdx
@@ -101,6 +101,46 @@ fun ContentScreen() {
}
```
+## Reading detailed purchase history (2.6.6+)
+
+When you need more context than `SubscriptionStatus` provides (for example, to show the full transaction history or mix web redemptions with Google Play receipts), subscribe to `Superwall.instance.customerInfo`. The flow emits a `CustomerInfo` object that merges device, web, and external purchase controller data.
+
+```kotlin
+class BillingDashboardFragment : Fragment() {
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ Superwall.instance.customerInfo.collect { info ->
+ val subscriptions = info.subscriptions.map { it.productId to it.expiresDate }
+ val nonSubscriptions = info.nonSubscriptions.map { it.productId to it.purchaseDate }
+ val entitlementIds = info.entitlements.filter { it.isActive }.map { it.id }
+
+ renderCustomerInfo(
+ activeProducts = info.activeSubscriptionProductIds,
+ entitlements = entitlementIds,
+ subscriptions = subscriptions,
+ oneTimePurchases = nonSubscriptions
+ )
+ }
+ }
+ }
+}
+```
+
+Need the latest value immediately (for example, during cold start)? Call `Superwall.instance.getCustomerInfo()` to synchronously read the most recent snapshot before collecting the flow:
+
+```kotlin
+val cached = Superwall.instance.getCustomerInfo()
+renderCustomerInfo(
+ activeProducts = cached.activeSubscriptionProductIds,
+ entitlements = cached.entitlements.filter { it.isActive }.map { it.id },
+ subscriptions = cached.subscriptions.map { it.productId to it.purchaseDate },
+ oneTimePurchases = cached.nonSubscriptions.map { it.productId to it.purchaseDate }
+)
+```
+
+After you start collecting, you can also watch for [`SuperwallDelegate.customerInfoDidChange(from:to:)`](/android/sdk-reference/SuperwallDelegate#customerinfodidchangefrom-customerinfo-to-customerinfo) to run analytics or sync other systems whenever purchases change.
+
## Checking for specific entitlements
If your app has multiple subscription tiers (e.g., Bronze, Silver, Gold), you can check for specific entitlements:
diff --git a/content/docs/android/sdk-reference/Superwall.mdx b/content/docs/android/sdk-reference/Superwall.mdx
index d47cf0b..72aaf63 100644
--- a/content/docs/android/sdk-reference/Superwall.mdx
+++ b/content/docs/android/sdk-reference/Superwall.mdx
@@ -132,6 +132,39 @@ Superwall.instance.setIntegrationAttributes(
)
```
+## Observe customer info (2.6.6+)
+
+Superwall now exposes purchase history and entitlement snapshots via a `StateFlow`. Each emission contains merged device, web, and external purchase controller data so you can react to subscription changes without wiring up your own polling layer.
+
+```kotlin
+lifecycleScope.launch {
+ Superwall.instance.customerInfo.collect { info ->
+ val activeProductIds = info.activeSubscriptionProductIds
+ val activeEntitlementIds = info.entitlements
+ .filter { it.isActive }
+ .map { it.id }
+
+ updateUi(
+ subscriptions = activeProductIds,
+ entitlements = activeEntitlementIds
+ )
+ }
+}
+```
+
+Need an immediate snapshot (for example during cold start)? Call `Superwall.instance.getCustomerInfo()` to synchronously read the latest cached value, or wire both together:
+
+```kotlin
+val cachedInfo = Superwall.instance.getCustomerInfo()
+render(cachedInfo)
+
+lifecycleScope.launch {
+ Superwall.instance.customerInfo.collect { render(it) }
+}
+```
+
+Pair the flow with [`SuperwallDelegate.customerInfoDidChange(from:to:)`](/android/sdk-reference/SuperwallDelegate#customerinfodidchangefrom-customerinfo-to-customerinfo) when you need to mirror changes into analytics.
+
Java usage:
```java
// Access the instance
diff --git a/content/docs/android/sdk-reference/SuperwallDelegate.mdx b/content/docs/android/sdk-reference/SuperwallDelegate.mdx
index 45cc0ee..8a92719 100644
--- a/content/docs/android/sdk-reference/SuperwallDelegate.mdx
+++ b/content/docs/android/sdk-reference/SuperwallDelegate.mdx
@@ -21,6 +21,13 @@ interface SuperwallDelegate {
from: SubscriptionStatus,
to: SubscriptionStatus
) {}
+
+ fun customerInfoDidChange(
+ from: CustomerInfo,
+ to: CustomerInfo
+ ) {}
+
+ fun userAttributesDidChange(newAttributes: Map) {}
fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {}
@@ -55,12 +62,37 @@ public interface SuperwallDelegateJava {
SubscriptionStatus from,
SubscriptionStatus to
) {}
+
+ default void customerInfoDidChange(
+ CustomerInfo from,
+ CustomerInfo to
+ ) {}
+
+ default void userAttributesDidChange(Map newAttributes) {}
default void handleSuperwallEvent(SuperwallEventInfo eventInfo) {}
default void handleCustomPaywallAction(String name) {}
- // ... other methods
+ default void willDismissPaywall(PaywallInfo paywallInfo) {}
+
+ default void willPresentPaywall(PaywallInfo paywallInfo) {}
+
+ default void didDismissPaywall(PaywallInfo paywallInfo) {}
+
+ default void didPresentPaywall(PaywallInfo paywallInfo) {}
+
+ default void paywallWillOpenURL(String url) {}
+
+ default void paywallWillOpenDeepLink(String url) {}
+
+ default void handleLog(
+ LogLevel level,
+ LogScope scope,
+ String message,
+ Map info,
+ Throwable error
+ ) {}
}
```
@@ -84,6 +116,16 @@ All methods are optional to implement. Key methods include:
description: "Called when user taps elements with `data-pw-custom` tags.",
required: true,
},
+ userAttributesDidChange: {
+ type: "newAttributes: Map",
+ description: "Called whenever paywall actions mutate user attributes (for example, forms or surveys).",
+ required: true,
+ },
+ customerInfoDidChange: {
+ type: "from: CustomerInfo, to: CustomerInfo",
+ description: "Raised when Superwall merges device, web, and external purchase data into a new CustomerInfo snapshot.",
+ required: true,
+ },
willPresentPaywall: {
type: "paywallInfo: PaywallInfo",
description: "Called before paywall presentation.",
@@ -135,6 +177,32 @@ override fun subscriptionStatusDidChange(
}
```
+Mirror merged purchase history:
+```kotlin
+override fun customerInfoDidChange(
+ from: CustomerInfo,
+ to: CustomerInfo
+) {
+ if (from.activeSubscriptionProductIds != to.activeSubscriptionProductIds) {
+ Analytics.track("customer_info_updated", mapOf(
+ "old_products" to from.activeSubscriptionProductIds.joinToString(),
+ "new_products" to to.activeSubscriptionProductIds.joinToString()
+ ))
+ }
+
+ refreshEntitlementBadge(to.entitlements.filter { it.isActive }.map { it.id })
+}
+```
+
+Capture remote attribute changes:
+```kotlin
+override fun userAttributesDidChange(newAttributes: Map) {
+ // Paywall forms or surveys can set attributes directly.
+ // Forward them to your analytics platform or local cache.
+ analytics.identify(newAttributes)
+}
+```
+
Forward analytics events:
```kotlin
override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
@@ -200,5 +268,19 @@ public class MainActivity extends AppCompatActivity implements SuperwallDelegate
System.out.println("Subscription changed from " + from + " to " + to);
updateUI(to);
}
+
+ @Override
+ public void customerInfoDidChange(
+ CustomerInfo from,
+ CustomerInfo to
+ ) {
+ Logger.i("Superwall", "Customer info updated: " + to.getActiveSubscriptionProductIds());
+ syncUserPurchases(to);
+ }
+
+ @Override
+ public void userAttributesDidChange(Map newAttributes) {
+ analytics.identify(newAttributes);
+ }
}
```
diff --git a/content/docs/android/sdk-reference/SuperwallEvent.mdx b/content/docs/android/sdk-reference/SuperwallEvent.mdx
index 3996729..ec91ebb 100644
--- a/content/docs/android/sdk-reference/SuperwallEvent.mdx
+++ b/content/docs/android/sdk-reference/SuperwallEvent.mdx
@@ -106,4 +106,41 @@ This is a sealed class that represents different event types. Events are receive
## Usage
-These events are received via [`SuperwallDelegate.handleSuperwallEvent(eventInfo)`](/android/sdk-reference/SuperwallDelegate) for forwarding to your analytics platform.
\ No newline at end of file
+These events are received via [`SuperwallDelegate.handleSuperwallEvent(eventInfo)`](/android/sdk-reference/SuperwallDelegate) for forwarding to your analytics platform.
+
+## New events in 2.6.6+
+
+- `CustomerInfoDidChange` fires whenever the SDK merges device, web, and external purchase controller data into a new [`CustomerInfo`](/android/quickstart/tracking-subscription-state#reading-detailed-purchase-history-2-6-6) snapshot. The event includes the previous and next objects so you can diff entitlements or transactions.
+- `PermissionRequested`, `PermissionGranted`, and `PermissionDenied` correspond to the new **Request permission** action in the paywall editor. Each event carries the `permissionName` and `paywallIdentifier`.
+- `PaywallPreloadStart` and `PaywallPreloadComplete` track when preloading kicks off and how many paywalls finished warming the cache.
+
+Example handler:
+
+```kotlin
+override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
+ when (val event = eventInfo.event) {
+ is SuperwallEvent.CustomerInfoDidChange -> {
+ analytics.track("customer_info_updated", mapOf(
+ "old_products" to event.from.activeSubscriptionProductIds.joinToString(),
+ "new_products" to event.to.activeSubscriptionProductIds.joinToString()
+ ))
+ }
+ is SuperwallEvent.PermissionRequested -> {
+ analytics.track("permission_requested", mapOf(
+ "permission" to event.permissionName,
+ "paywall_id" to event.paywallIdentifier
+ ))
+ }
+ is SuperwallEvent.PermissionGranted -> {
+ featureFlags.unlock(event.permissionName)
+ }
+ is SuperwallEvent.PermissionDenied -> {
+ showPermissionHelpSheet(event.permissionName)
+ }
+ is SuperwallEvent.PaywallPreloadComplete -> {
+ Logger.i("Superwall", "Preloaded ${event.paywallCount} paywalls")
+ }
+ else -> Unit
+ }
+}
+```
\ No newline at end of file
diff --git a/content/docs/android/sdk-reference/index.mdx b/content/docs/android/sdk-reference/index.mdx
index 2004395..4073e85 100644
--- a/content/docs/android/sdk-reference/index.mdx
+++ b/content/docs/android/sdk-reference/index.mdx
@@ -15,4 +15,4 @@ If you have feedback on any of our docs, please leave a rating and message at th
If you have any issues with the SDK, please [open an issue on GitHub](https://github.com/superwall/superwall-android/issues).
-
\ No newline at end of file
+
\ No newline at end of file