Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions content/docs/android/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions content/docs/android/guides/advanced/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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` | `<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />` (API 33+) | Devices below Android 13 do not require a runtime permission; the SDK reports `granted` immediately. |
| Location (Foreground) | `location` | `<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />` | Also covers coarse location because FINE implies COARSE. |
| Location (Background) | `background_location` | Foreground entry above **and** `<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />` (API 29+) | The SDK first ensures foreground access, then escalates to background. |
| Photos / Images | `read_images` | `<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />` (API 33+) or `READ_EXTERNAL_STORAGE` for older OS versions | Automatically picks the right permission at runtime. |
| Videos | `read_video` | `<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />` (API 33+) or `READ_EXTERNAL_STORAGE` pre-33 | |
| Contacts | `contacts` | `<uses-permission android:name="android.permission.READ_CONTACTS" />` | |
| Camera | `camera` | `<uses-permission android:name="android.permission.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.
2 changes: 1 addition & 1 deletion content/docs/android/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<SdkLatestVersion version="2.6.5" repoUrl="https://github.com/superwall/Superwall-Android" />
<SdkLatestVersion version="2.6.7" repoUrl="https://github.com/superwall/Superwall-Android" />
40 changes: 40 additions & 0 deletions content/docs/android/quickstart/tracking-subscription-state.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
33 changes: 33 additions & 0 deletions content/docs/android/sdk-reference/Superwall.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,39 @@ Superwall.instance.setIntegrationAttributes(
)
```

## Observe customer info (2.6.6+)

Superwall now exposes purchase history and entitlement snapshots via a `StateFlow<CustomerInfo>`. 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
Expand Down
84 changes: 83 additions & 1 deletion content/docs/android/sdk-reference/SuperwallDelegate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ interface SuperwallDelegate {
from: SubscriptionStatus,
to: SubscriptionStatus
) {}

fun customerInfoDidChange(
from: CustomerInfo,
to: CustomerInfo
) {}

fun userAttributesDidChange(newAttributes: Map<String, Any>) {}

fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {}

Expand Down Expand Up @@ -55,12 +62,37 @@ public interface SuperwallDelegateJava {
SubscriptionStatus from,
SubscriptionStatus to
) {}

default void customerInfoDidChange(
CustomerInfo from,
CustomerInfo to
) {}

default void userAttributesDidChange(Map<String, Object> 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<String, Object> info,
Throwable error
) {}
}
```

Expand All @@ -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<String, Any>",
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.",
Expand Down Expand Up @@ -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<String, Any>) {
// 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) {
Expand Down Expand Up @@ -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<String, Object> newAttributes) {
analytics.identify(newAttributes);
}
}
```
39 changes: 38 additions & 1 deletion content/docs/android/sdk-reference/SuperwallEvent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
}
}
```
2 changes: 1 addition & 1 deletion content/docs/android/sdk-reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<SdkLatestVersion version="2.6.5" repoUrl="https://github.com/superwall/Superwall-Android" />
<SdkLatestVersion version="2.6.7" repoUrl="https://github.com/superwall/Superwall-Android" />