AppActor does not render a hosted paywall. Your app fetches offerings, maps packages into UI cards, and calls the purchase API for the package the customer selects.

Display rule

Use packages as your UI source of truth:
  • offering.packages for the full list and dashboard order
  • offering.monthly, offering.annual, or packageFor(...) for known slots
  • localizedPriceString for the primary price label
  • displayName, productName, productDescription, and metadata for copy
  • basePlanId and offerId for Android debugging or upgrade flows
Do not build the paywall by hardcoding store product IDs.

Keep product UI simple

Your paywall view model is app code, not an AppActor requirement. Keep the docs-level rule simple:
  • fetch the current offering
  • render the packages that exist
  • show the store-localized price where available
  • provide a recoverable fallback when there are no packages
  • keep the original package object attached to the CTA
If the current offering has no packages, do not crash or unlock access. Show a safe fallback, log enough context for QA, and check the catalog setup before changing SDK code.

Preserve package identity on tap

The selected UI card should keep the original AppActorPackage object. That package carries the backend package ID, offering ID, store product metadata, Android base plan, and offer information the purchase API needs.

Paywall QA checklist

  • The app can render the current offering.
  • Monthly and annual package slots exist if your UI references them directly.
  • Package display order matches the dashboard order.
  • Price labels use store-localized data, not hardcoded strings.
  • Metadata-driven badges have fallbacks.
  • Empty offerings produce a recoverable UI, not a crash or premium unlock.
  • The purchase CTA receives the original package object, not just a product ID.
Next, wire the CTA with Making Purchases.