Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
788567f
Tweaks Stripe to point at app (#71)
anglinb Jan 14, 2026
d434153
Integration docs (#81)
DreamingInBinary Jan 14, 2026
ec8cde2
Guied (#84)
DreamingInBinary Jan 15, 2026
7eb91dc
Hotfix stripe app install instructions
anglinb Jan 16, 2026
ff0cebd
Add note about request permission action availability (#82)
dcrawbuck Jan 16, 2026
2e3dcf4
Fix fumadocs version (#87)
drvarmin Jan 17, 2026
ef19159
docs(expo): update for v1.0.1 (#80)
dcrawbuck Jan 17, 2026
3b01dcf
Added doc for intro offer eligibility override (#86)
drvarmin Jan 19, 2026
ed2438f
Update intro-offer-eligibility-override.mdx
yusuftor Jan 20, 2026
0650e8a
docs(flutter): update for 2.4.7 (#89)
dcrawbuck Jan 20, 2026
0a08002
Update requirements for introductory offer eligibilityhot
anglinb Jan 20, 2026
356127e
Make AL requirement clear (#90)
DreamingInBinary Jan 21, 2026
4d42c4b
Copy prompt docs (#92)
DreamingInBinary Jan 21, 2026
8e489d4
Dynamic change log (#93)
DreamingInBinary Jan 22, 2026
483a391
Add to toggle (#94)
DreamingInBinary Jan 22, 2026
e0afc02
Add PurchaseController support to intro offer override
yusuftor Jan 23, 2026
8bcb4e7
Remove paywall timeout references from docs (#95)
DreamingInBinary Jan 29, 2026
c6cbd76
Added new support documentation (#91)
drvarmin Jan 30, 2026
7d1678b
Remove all Paddle references from docs
anglinb Feb 5, 2026
f62d1f7
Docs: clarify Figma Auto Layout; add Codex MCP (#98)
DreamingInBinary Feb 5, 2026
c8ab54c
Update web_checkout_direct_to_stripe.jpg (#99)
DreamingInBinary Feb 6, 2026
43d3805
Add Mixedbread environment variables to PR preview deployments (#100)
drvarmin Feb 10, 2026
8152d0f
feat: update promo/referral code doc (#88)
dcrawbuck Feb 10, 2026
99714fe
docs(android): update for 2.7.0 (#102)
dcrawbuck Feb 12, 2026
5fc4385
Add Mixedbread vector search with SDK filtering (#97)
drvarmin Feb 12, 2026
784bc49
Demand score (#103)
DreamingInBinary Feb 12, 2026
45ea109
docs(ios): update for 4.12.11 (#79)
dcrawbuck Feb 12, 2026
bec43e1
Troubleshooting & FAQ docs (#107)
drvarmin Feb 18, 2026
0558410
New tweaks (#108)
DreamingInBinary Feb 18, 2026
1cdac17
docs(flutter): update for 2.4.8 (#105)
dcrawbuck Feb 19, 2026
69b5351
docs(expo): update for v1.0.2 (#106)
dcrawbuck Feb 19, 2026
ef47cc9
docs(ios): update for 4.13.0 (#104)
dcrawbuck Feb 19, 2026
e056b1d
Add SDK Setup page to Web Checkout guide
anglinb Jan 10, 2026
7a2b082
Fix SDK setup card icons and remove React Native card
dcrawbuck Feb 19, 2026
9034bbd
Pin OpenNext CLI to lockfile version in deploy workflows
dcrawbuck Feb 19, 2026
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
9 changes: 7 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ NEXT_PUBLIC_RB2B_KEY=<replace-with-rb2b-key>
NEXT_PUBLIC_PYLON_APP_ID=<replace-with-pylon-app-id>
PYLON_IDENTITY_SECRET=<replace-with-pylon-identity-secret>

# Search mode: 'fumadocs' (default, uses Fumadocs built-in search) or 'rag' (uses RAG endpoint at mcp.superwall.com)
SEARCH_MODE=fumadocs
# Search mode: 'fumadocs' (default, uses Fumadocs built-in search), 'rag' (uses RAG endpoint at mcp.superwall.com) or 'mixedbread' (vector search)
SEARCH_MODE=mixedbread

MIXEDBREAD_API_KEY=<your-api-key>
MIXEDBREAD_STORE_ID=<your-store-id>
MIXEDBREAD_TOPK=8 #defaults to 8
MIXEDBREAD_RERANK=8 #if empty, rerank is disabled
12 changes: 10 additions & 2 deletions .github/workflows/docs-deploy-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ jobs:
DOCS_NEXT_PUBLIC_RB2B_KEY: ${{ secrets.DOCS_NEXT_PUBLIC_RB2B_KEY }}
NEXT_PUBLIC_PYLON_APP_ID: ${{ secrets.NEXT_PUBLIC_PYLON_APP_ID }}
PYLON_IDENTITY_SECRET: ${{ secrets.PYLON_IDENTITY_SECRET }}
MIXEDBREAD_API_KEY: ${{ secrets.MIXEDBREAD_API_KEY }}
MIXEDBREAD_STORE_ID: ${{ secrets.MIXEDBREAD_STORE_ID }}
MIXEDBREAD_TOPK: ${{ secrets.MIXEDBREAD_TOPK }}
MIXEDBREAD_RERANK: ${{ secrets.MIXEDBREAD_RERANK }}
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -66,6 +70,10 @@ jobs:
SEARCH_MODE=${SEARCH_MODE}
NEXT_PUBLIC_PYLON_APP_ID=${NEXT_PUBLIC_PYLON_APP_ID}
PYLON_IDENTITY_SECRET=${PYLON_IDENTITY_SECRET}
MIXEDBREAD_API_KEY=${MIXEDBREAD_API_KEY}
MIXEDBREAD_STORE_ID=${MIXEDBREAD_STORE_ID}
MIXEDBREAD_TOPK=${MIXEDBREAD_TOPK}
MIXEDBREAD_RERANK=${MIXEDBREAD_RERANK}
EOF

- name: Generate Wrangler config for CI
Expand Down Expand Up @@ -103,7 +111,7 @@ jobs:
run: |
set -e
bun run build
bunx opennextjs-cloudflare build --config wrangler.jsonc -- --skipNextBuild
./node_modules/.bin/opennextjs-cloudflare build --config wrangler.jsonc -- --skipNextBuild

- name: Create GitHub Deployment
id: create_deployment
Expand All @@ -117,7 +125,7 @@ jobs:
id: deploy
run: |
set -e
OUTPUT=$(bunx opennextjs-cloudflare deploy --config wrangler.jsonc)
OUTPUT=$(./node_modules/.bin/opennextjs-cloudflare deploy --config wrangler.jsonc)
echo "$OUTPUT"
# Extract deployment URL if available
DEPLOYMENT_URL=$(echo "$OUTPUT" | grep -oE 'https://[^ ]+' | head -1)
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/docs-deploy-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ jobs:
DOCS_NEXT_PUBLIC_RB2B_KEY: ${{ secrets.DOCS_NEXT_PUBLIC_RB2B_KEY }}
NEXT_PUBLIC_PYLON_APP_ID: ${{ secrets.NEXT_PUBLIC_PYLON_APP_ID }}
PYLON_IDENTITY_SECRET: ${{ secrets.PYLON_IDENTITY_SECRET }}
MIXEDBREAD_API_KEY: ${{ secrets.MIXEDBREAD_API_KEY }}
MIXEDBREAD_STORE_ID: ${{ secrets.MIXEDBREAD_STORE_ID }}
MIXEDBREAD_TOPK: ${{ secrets.MIXEDBREAD_TOPK }}
MIXEDBREAD_RERANK: ${{ secrets.MIXEDBREAD_RERANK }}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -384,6 +388,10 @@ jobs:
SEARCH_MODE=${SEARCH_MODE}
NEXT_PUBLIC_PYLON_APP_ID=${NEXT_PUBLIC_PYLON_APP_ID}
PYLON_IDENTITY_SECRET=${PYLON_IDENTITY_SECRET}
MIXEDBREAD_API_KEY=${MIXEDBREAD_API_KEY}
MIXEDBREAD_STORE_ID=${MIXEDBREAD_STORE_ID}
MIXEDBREAD_TOPK=${MIXEDBREAD_TOPK}
MIXEDBREAD_RERANK=${MIXEDBREAD_RERANK}
EOF

- name: Generate Wrangler config for CI
Expand Down Expand Up @@ -424,7 +432,7 @@ jobs:
run: |
set -e
bun run build
bunx opennextjs-cloudflare build --config wrangler.jsonc -- --skipNextBuild
./node_modules/.bin/opennextjs-cloudflare build --config wrangler.jsonc -- --skipNextBuild
OUTPUT=$(npx wrangler@4.34.0 --config wrangler.jsonc versions upload --name ${CF_STAGING_WORKER_NAME} --preview-alias pr-$PR)
echo "$OUTPUT"
# Extract deployment URL
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.next
.source
next-env.d.ts

# thesse will be automatically filled with the resources from the docs
public/content
Expand Down
692 changes: 674 additions & 18 deletions bun.lock

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions content/docs/android/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,43 @@ title: "Changelog"
description: "Release notes for the Superwall Android SDK"
---

## 2.7.0

### Enhancements
- Enables paywall post-purchase action execution instead of dismissing
- Enables triggering custom callback requests from paywall
- Adds a new method to PaywallPresentationHandler called onCustomCallback that allows user to handle custom callback requests
- Adds retrieving of paywall state inside paywall info
- Adds support for new one time purchases with purchase options and offers
- Update Superscript to version 1.0.13, find more in the [Superscript changelog](https://github.com/superwall/superscript/releases/tag/1.0.13)

### Deprecations
- Deprecated `paywallWebviewLoad_timeout` - this event was causing confusion due to its naming, leading to it being deprecated

### Fixes
- Fixes late initialization authorization issue for Stripe checkouts
- Improves how Shimmer duration is measured
- Fixes wrong redemption type being displayed due to integration attributes

## 2.6.8

### Enhancements
- Adds microphone permission

### Fixes
- Fixes error when redeeming external purchases

## 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
88 changes: 88 additions & 0 deletions content/docs/android/guides/advanced/custom-callbacks.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: "Custom callbacks"
description: "Handle custom callback requests from paywalls to run app-side logic and return results."
---

<Info>
Available from Android SDK 2.7.0.
</Info>

## Overview

Custom callbacks let a paywall request arbitrary actions from your app and receive results that determine which branch (`onSuccess` / `onFailure`) executes inside the paywall. Common use cases include validating user input, fetching data, or running business logic that lives outside the paywall.

## How it works

1. In the paywall editor, attach a **Custom callback** action to an element (button, form submit, etc.) and give it a name (e.g. `validate_email`).
2. When the user triggers that element the SDK calls your `onCustomCallback` handler with a `CustomCallback` object.
3. Your handler runs whatever logic is needed and returns a `CustomCallbackResult` — either `.success()` or `.failure()` — with optional data.
4. The paywall receives the result and executes the matching `onSuccess` or `onFailure` branch.

## Setting up the handler

Register the handler on a `PaywallPresentationHandler` before calling `register`:

```kotlin
val handler = PaywallPresentationHandler()

handler.onCustomCallback { callback ->
when (callback.name) {
"validate_email" -> {
val email = callback.variables?.get("email") as? String
if (isValidEmail(email)) {
CustomCallbackResult.success(mapOf("validated" to true))
} else {
CustomCallbackResult.failure(mapOf("error" to "Invalid email"))
}
}
else -> CustomCallbackResult.failure()
}
}

Superwall.instance.register(placement = "campaign_trigger", handler = handler) {
// Feature launched
}
```

## CustomCallback

The `CustomCallback` data class is passed to your handler:

<TypeTable
type={{
name: {
type: "String",
description: "The name of the callback set in the paywall editor.",
required: true,
},
variables: {
type: "Map<String, Any>?",
description: "Optional key-value pairs sent from the paywall. Values are type-preserved (String, Number, Boolean).",
},
}}
/>

## CustomCallbackResult

Return one of the following from your handler to signal the outcome:

```kotlin
// Success — the paywall's onSuccess branch runs
CustomCallbackResult.success(data = mapOf("key" to "value"))

// Failure — the paywall's onFailure branch runs
CustomCallbackResult.failure(data = mapOf("error" to "Something went wrong"))
```

Both `success()` and `failure()` accept an optional `data` map whose values are sent back to the paywall and accessible as `callbacks.<name>.data.<key>`.

## Callback behavior

When configuring the custom callback action in the paywall editor you can choose between two behaviors:

- **Blocking** — the paywall waits for your handler to return before continuing the tap-action chain. Use this when the next step depends on the result (e.g. form validation).
- **Non-blocking** — the paywall continues immediately. The `onSuccess` / `onFailure` handlers still fire when the result arrives, but subsequent actions in the chain do not wait.

## Accessing returned data in the paywall

Inside the paywall you can reference the returned data using the pattern `callbacks.<callback_name>.data.<key>`. For example, if the callback named `validate_email` returns `mapOf("validated" to true)`, the paywall can access `callbacks.validate_email.data.validated`.
2 changes: 2 additions & 0 deletions content/docs/android/guides/advanced/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"using-the-presentation-handler",
"viewing-purchased-products",
"custom-paywall-actions",
"custom-callbacks",
"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,75 @@
---
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" />` | |
| Microphone | `microphone` | `<uses-permission android:name="android.permission.RECORD_AUDIO" />` | Added in 2.6.8. |

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.
6 changes: 6 additions & 0 deletions content/docs/android/guides/handling-deep-links.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: "Handling Deep Links"
description: "Use handleDeepLink and campaign rules to present paywalls from deep links without hardcoding logic in your app."
---

<include>../../../shared/handling-deep-links.mdx</include>
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.7.0" repoUrl="https://github.com/superwall/Superwall-Android" />
1 change: 1 addition & 0 deletions content/docs/android/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"guides/configuring",
"guides/using-superwall-delegate",
"guides/3rd-party-analytics",
"guides/handling-deep-links",

"---SDK Reference---",
"sdk-reference/index",
Expand Down
6 changes: 3 additions & 3 deletions content/docs/android/quickstart/install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ can find the [latest release here](https://github.com/superwall/Superwall-Androi
<CodeGroup>

```gradle build.gradle
implementation "com.superwall.sdk:superwall-android:2.6.5"
implementation "com.superwall.sdk:superwall-android:2.7.0"
```

```kotlin build.gradle.kts
implementation("com.superwall.sdk:superwall-android:2.6.5")
implementation("com.superwall.sdk:superwall-android:2.7.0")
```

```toml libs.version.toml
[libraries]
superwall-android = { group = "com.superwall.sdk", name = "superwall-android", version = "2.6.5" }
superwall-android = { group = "com.superwall.sdk", name = "superwall-android", version = "2.7.0" }

// And in your build.gradle.kts
dependencies {
Expand Down
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
Loading