Android SDK
Native Kotlin SDK for presenting AgentWallie paywalls in Android apps. Renders paywall schemas using Jetpack Compose and handles purchases via Google Play Billing.
Minimum API level: 26 (Android 8.0+)
Installation
Gradle
Add the dependency to your module's build.gradle.kts:
dependencies {
implementation("com.agentwallie:sdk:0.1.0")
}Configuration
Configure the SDK in your Application class:
import com.agentwallie.sdk.AgentWallie
import com.agentwallie.sdk.AgentWallieOptions
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
AgentWallie.configure(this, apiKey = "pk_your_public_key")
}
}With Options
val options = AgentWallieOptions(
baseUrl = "https://agentwallie.com/api",
loggingEnabled = true,
automaticPurchaseHandling = true,
configRefreshIntervalMs = 300_000L // 5 minutes
)
AgentWallie.configure(this, apiKey = "pk_your_public_key", options = options)configure() must be called exactly once before using any other SDK methods. Calling it again is a no-op.
User Management
Identify
Call identify after login or when you know the user identity:
AgentWallie.identify("user_123")This triggers a config re-fetch to apply user-specific state.
Set User Attributes
Provide attributes for audience targeting:
AgentWallie.setUserAttributes(mapOf(
"plan" to "free",
"onboarding_complete" to true,
"session_count" to 5,
"age" to 28
))Reset
Clear user identity, attributes, and experiment assignments:
AgentWallie.reset()Presenting Paywalls
Register a Placement
The primary API for showing paywalls:
AgentWallie.register(placement = "feature_gate") {
// Handler runs if user has entitlement (no paywall needed)
showProFeature()
}When called, the SDK:
- Evaluates all campaigns with a matching placement
- Matches audiences top-to-bottom using filter rules
- Checks experiment assignment for the matched audience
- Presents the paywall as a Compose-rendered dialog/sheet
Programmatic Presentation
Present a specific paywall by ID:
AgentWallie.presentPaywall(id = "pw_abc123")presentPaywall requires a foreground Activity. The SDK uses ActivityLifecycleCallbacks to track the current activity. If no activity is available, the call is a no-op with a log warning.
Get Paywall Without Presenting
Retrieve the paywall schema for custom rendering:
val schema = AgentWallie.getPaywall(forPlacement = "upgrade")
if (schema != null) {
// Use schema for custom rendering
}Subscription Status and Entitlements
// Set subscription status
AgentWallie.subscriptionStatus = SubscriptionStatus.ACTIVE
// Set entitlements
AgentWallie.entitlements = setOf("pro", "premium_content")SubscriptionStatus Values
| Value | Description |
|---|---|
UNKNOWN | Status not yet determined |
FREE | Free tier user |
FREE_TRIAL | Active free trial |
ACTIVE | Active paid subscription |
EXPIRED | Subscription expired |
GRACE_PERIOD | In billing grace period |
Purchase Handling
Default (Google Play Billing)
When automaticPurchaseHandling is true (default), the SDK initializes PlayBillingManager and handles purchases via Google Play Billing Library.
Custom PurchaseController
For apps using RevenueCat, custom billing, or server-side validation:
import com.agentwallie.sdk.purchasing.PurchaseController
class MyPurchaseController : PurchaseController {
override suspend fun purchase(productId: String): PurchaseResult {
// Custom purchase logic
return PurchaseResult.Purchased
}
override suspend fun restorePurchases(): RestoreResult {
return RestoreResult.Restored
}
}
// Set before any paywall presentation
AgentWallie.setPurchaseController(MyPurchaseController())Event Tracking
Track custom events:
AgentWallie.trackEvent(
name = "workout_completed",
properties = mapOf(
"duration" to 45,
"type" to "strength"
)
)Events are batched using EventTracker (which observes app lifecycle) and sent periodically to the events API.
Delegate Callbacks
Implement AgentWallieDelegate to receive lifecycle events:
import com.agentwallie.sdk.AgentWallieDelegate
class MyDelegate : AgentWallieDelegate {
override fun onPaywallWillPresent(paywallId: String) {
Log.d("AgentWallie", "Will present: $paywallId")
}
override fun onPaywallDidPresent(paywallId: String) {
Log.d("AgentWallie", "Did present: $paywallId")
}
override fun onPaywallDidDismiss(paywallId: String) {
Log.d("AgentWallie", "Dismissed: $paywallId")
}
override fun onPurchaseCompleted(productId: String) {
AgentWallie.subscriptionStatus = SubscriptionStatus.ACTIVE
}
override fun onCustomAction(name: String) {
when (name) {
"show_onboarding" -> showOnboarding()
}
}
}
// Set delegate
AgentWallie.delegate = MyDelegate()Deep Links
Handle deep links to trigger placements:
// In your Activity
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.let { uri ->
AgentWallie.handleDeepLink(uri)
}
}Deep link format: agentwallie://paywall?placement=feature_gate
Custom Fonts
The theme supports a font_families map for granular typography control. Define up to four font roles, then optionally override on individual components.
Theme-Level Font Families
{
"theme": {
"font_families": {
"display": "Playfair Display",
"heading": "Inter",
"body": "Inter",
"mono": "JetBrains Mono"
}
}
}The SDK resolves fonts in this order:
- Component-level
font_familyoverride (instyle) - Theme role mapping (
displayfortitle1/largeTitle,headingfortitle2/title3/headline,bodyfor everything else) - System default
Per-Component Override
Any component with a style block can specify font_family to override the theme:
{
"type": "text",
"id": "promo_title",
"props": { "content": "Limited Offer", "text_style": "title1" },
"style": { "font_family": "Lobster" }
}Loading Custom Fonts
Fonts referenced in the schema must be bundled in your app's res/font directory. The SDK converts the font family name to a lowercase snake_case resource lookup (e.g., "Playfair Display" resolves to R.font.playfair_display).
app/src/main/res/font/
├── playfair_display.ttf
├── inter.ttf
└── jetbrains_mono.ttfIf a font cannot be resolved, the SDK falls back to the system default and logs a warning when loggingEnabled is true.
Expression Resolution
Text content and style values support expression syntax using double curly braces. Expressions are resolved at render time against the current paywall context.
Supported Namespaces
| Namespace | Example | Description |
|---|---|---|
theme.* | {{ theme.primary }} | Theme color and style values |
user.* | {{ user.name }} | User attributes set via setUserAttributes |
products.* | {{ products.selected.price }} | Product fields from the selected product slot |
Examples
{
"type": "text",
"props": { "content": "Only {{ products.selected.price }}/year" }
}{
"type": "text",
"props": { "content": "Welcome back, {{ user.name }}!" }
}{
"style": { "color": "{{ theme.accent }}" }
}Expressions work in content, text, color values, and any string property within props or style.
Markdown Bold
Text components support inline bold via standard Markdown syntax. Wrap text in double asterisks to render as bold:
{
"type": "text",
"props": { "content": "Get **unlimited** access to **all** features" }
}The SDK parses **text** segments and renders them with FontWeight.Bold in Compose while keeping the surrounding text at its normal weight.
Background Gradient
Use background_gradient in settings to render a linear gradient behind the paywall content:
{
"settings": {
"background_gradient": {
"colors": ["#1A1A2E", "#16213E", "#0F3460"],
"start": "top",
"end": "bottom"
}
}
}The SDK renders this as a Compose Brush.linearGradient filling the paywall background. Supported directions: top/bottom, left/right, and diagonal combinations like topLeading/bottomTrailing.
Glow Effect
Add glow_color to any component's style to render a soft shadow glow around the component:
{
"type": "cta_button",
"id": "cta",
"props": { "text": "Subscribe Now", "action": "purchase" },
"style": {
"background_color": "#7C3AED",
"glow_color": "#7C3AED"
}
}The SDK applies a Compose shadow modifier with the specified color at 60% opacity, 12dp blur radius, and 4dp spread. This works on any component type.
Letter Spacing
Control letter spacing on text components via the letter_spacing style property. Value is in sp (scale-independent pixels):
{
"type": "text",
"props": { "content": "PREMIUM", "text_style": "headline" },
"style": { "letter_spacing": 2.5 }
}Close Button Style
The close_button_style setting controls how the paywall close button appears:
{
"settings": {
"close_button": true,
"close_button_style": "text"
}
}| Value | Description |
|---|---|
"icon" | (Default) Renders an X icon button in the top-right corner |
"text" | Renders a "Close" text button in the top-right corner |
Product Picker Cards
The product_picker component supports a "cards" layout that renders each product as a distinct card with pricing details and savings badges:
{
"type": "product_picker",
"id": "products",
"props": {
"layout": "cards",
"show_savings_badge": true,
"savings_badge_text": "Save {{ products.primary.savings_percent }}%"
},
"style": {
"selected_border_color": "{{ theme.primary }}",
"corner_radius": 16,
"padding": 16
}
}Each card displays:
- Product label and price (resolved from the product slot)
- Per-unit price breakdown (e.g., "$4.99/mo" for yearly plans)
- Savings badge when
show_savings_badgeistrue - Selected state with a colored border
The "cards" layout renders each product in a Card composable inside a Column, compared to the default "horizontal" layout which uses a LazyRow.
Feature List Per-Row Styling
Feature list rows support individual styling overrides for background, corner_radius, padding, and border_color:
{
"type": "feature_list",
"id": "features",
"props": {
"items": [
{
"icon": "star.fill",
"text": "Priority support",
"background": "#F0F9FF",
"corner_radius": 12,
"padding": 16,
"border_color": "#3B82F6"
},
{
"icon": "bolt.fill",
"text": "Unlimited exports",
"background": "#FFF7ED",
"corner_radius": 12,
"padding": 16,
"border_color": "#F97316"
}
]
}
}When border_color is set, the SDK renders a 3dp left-side accent bar on the row using that color. The background, corner_radius, and padding apply to the row's container Box.
Debug Overlay
The SDK includes a built-in debug overlay for inspecting the live state of paywalls, experiments, and events.
Show the Debugger
AgentWallie.showDebugger()This launches DebugActivity which displays the following tabs:
| Tab | Description |
|---|---|
| Status | SDK version, config status, API connectivity, last refresh timestamp |
| User | Current user ID, attributes, subscription status, entitlements |
| Products | Resolved product slots, prices, and StoreKit/Play Billing mapping |
| Placements | Placement evaluator -- test any placement to see which campaign/audience/paywall matches |
| Event Log | Real-time log of tracked events, config fetches, and purchase attempts |
| Assignments | Current experiment assignments with variant details |
AndroidManifest Setup
Add DebugActivity to your AndroidManifest.xml:
<activity
android:name="com.agentwallie.sdk.debug.DebugActivity"
android:theme="@style/Theme.Material3.DayNight"
android:exported="false" />Only include DebugActivity in debug builds. Use a Gradle build-type manifest or feature flag to exclude it from release builds.
Debug Deep Link
You can also launch the debugger via deep link:
agentwallie://debugHandle it in your activity:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.let { uri ->
if (uri.toString() == "agentwallie://debug") {
AgentWallie.showDebugger()
} else {
AgentWallie.handleDeepLink(uri)
}
}
}Jetpack Compose Rendering
The SDK renders paywall schemas as Compose UI. Each component type in the schema maps to a Compose composable:
| Schema Component | Compose Implementation |
|---|---|
text | Text with styled attributes, expression resolution, markdown bold |
image | AsyncImage / Image |
cta_button | Button with action handler, glow support |
product_picker | LazyRow (horizontal), Column of Card (cards layout) |
feature_list | Column with icon + text rows, per-row styling |
stack | Row (horizontal) / Column (vertical) |
carousel | HorizontalPager |
countdown_timer | Custom ticking composable |
Options Reference
AgentWallieOptions
| Property | Type | Default | Description |
|---|---|---|---|
baseUrl | String | Production URL | API base URL |
loggingEnabled | Boolean | false | Enable debug logging |
automaticPurchaseHandling | Boolean | true | Use built-in Play Billing |
configRefreshIntervalMs | Long | 300000 | Config refresh interval (ms) |
Supported Features
All paywall schema features are fully supported in the current Android SDK:
- Product picker:
"cards","horizontal", and"vertical"layouts withshow_savings_badge,savings_text,show_price,selected_border_color, and glow - Expression resolution:
{{ products.selected.price }}and other product expressions resolve reactively, with fallback to configured display prices when Play Billing fetch fails - Feature list: Per-row styling with
background_color,corner_radius,padding, andborder_color - CTA button: Full styling including
glow_colorshadow effect - Text:
letter_spacing, markdown bold - Close button: Configurable via
close_button_style - Shimmer animation: Supported via
"shimmer"animation type
Full Example
import android.app.Application
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.*
import com.agentwallie.sdk.*
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
AgentWallie.configure(
context = this,
apiKey = "pk_live_abc123",
options = AgentWallieOptions(loggingEnabled = true)
)
AgentWallie.delegate = object : AgentWallieDelegate {
override fun onPurchaseCompleted(productId: String) {
AgentWallie.subscriptionStatus = SubscriptionStatus.ACTIVE
AgentWallie.entitlements = setOf("pro")
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AgentWallie.identify("user_456")
AgentWallie.setUserAttributes(mapOf(
"plan" to "free",
"workouts_completed" to 12
))
setContent {
Button(onClick = {
AgentWallie.register(placement = "advanced_workout") {
// User has pro -- let them through
startAdvancedWorkout()
}
}) {
Text("Start Advanced Workout")
}
}
}
private fun startAdvancedWorkout() {
// Navigate to workout
}
}