# Paywall screen

> A complete subscription paywall: a monthly and yearly toggle with a savings badge, plan cards whose prices roll digit by digit when the period changes, a feature list, a trial note that always states the price, restore purchases and an async subscribe button.

- Type: Component (screens)
- Access: Pro
- Page: https://goodui.dev/native/components/paywall-screen
- Markdown: https://goodui.dev/native/components/paywall-screen.md
- Registry item: https://goodui.dev/native/r/paywall-screen.json
- Dependencies: react-native-reanimated, react-native-svg
- Keywords: react native paywall, expo subscription screen, revenuecat paywall ui, in app purchase screen, pricing plans monthly yearly, free trial screen

## When to use

- Selling a subscription with one to three plans and a monthly or yearly choice.
- After onboarding, or when a person taps a locked feature.

## When not to use

- One-time purchases or tips: use a sheet with a single button.
- Comparing many plans feature by feature: a table on the web works better.
- Checkout with a card form: this screen hands off to the store purchase sheet.

## Installation

With the shadcn CLI (needs a components.json in the Expo app), then `npx expo install --fix` so native dependencies match your Expo SDK. The foundation comes along the first time.

```bash
npx shadcn@latest add @goodui-native-pro/paywall-screen
npx expo install --fix
```

Pro: add `"@goodui-native-pro": { "url": "https://goodui.dev/native/r/pro/{name}.json", "headers": { "Authorization": "Bearer ${GOODUI_LICENSE_KEY}" } }` to the `registries` of components.json and put your license key in .env.local.

The component lands in `components/goodui-native/paywall-screen/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs badge, button, icon-button, locale, rolling-number, segmented-control.

Built-in strings follow `<GooduiLocaleProvider>` (one [language pack](https://goodui.dev/native/r/locale-tr.json) translates the whole kit); the `labels` prop overrides them for one instance.

## Usage

```tsx
// app/paywall.tsx (presented as a modal route)
import { router } from "expo-router";
import Purchases from "react-native-purchases";
import { PaywallScreen } from "@/components/goodui-native/paywall-screen/paywall-screen";
import { packageFor } from "@/lib/purchases";

export default function PaywallRoute() {
  return (
    <PaywallScreen
      appName="Stride"
      heroImage={require("@/assets/paywall.jpg")}
      currency="USD"
      onSubscribe={(planId, period) => Purchases.purchasePackage(packageFor(planId, period))}
      onSubscribed={() => router.back()}
      onRestore={async () => {
        const info = await Purchases.restorePurchases();
        if (!info.entitlements.active.pro) throw new Error("Nothing to restore");
      }}
      onClose={router.back}
      onOpenTerms={() => router.push("/legal/terms")}
      onOpenPrivacy={() => router.push("/legal/privacy")}
    />
  );
}
```

## Examples

### Euro prices, no trial

```tsx
<PaywallScreen
  currency="EUR"
  locale="de-DE"
  trialDays={0}
  plans={[
    { id: "plus", name: "Plus", description: "Unlimited projects", price: { monthly: 4.99, yearly: 39.99 } },
    { id: "team", name: "Team", description: "Up to 10 people", price: { monthly: 12.99, yearly: 119 }, badge: "Best value" },
  ]}
  onSubscribe={buy}
/>
```

## API reference

### PaywallScreen

`import { PaywallScreen } from "@/components/goodui-native/paywall-screen/paywall-screen";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `appName` | `string` | `"Stride"` | Product name for the default copy. |
| `title` | `string` | – | Headline. Same as `labels.title`. |
| `subtitle` | `string` | – | One line under the headline. Same as `labels.subtitle`. |
| `heroImage` | `ImageSourcePropType` | – | Full-bleed art at the top: your own photo or illustration. Without it the screen starts with the headline. |
| `plans` | `PaywallPlan[]` | `PLANS` | Plans as cards, in order. One or more. |
| `defaultPlan` | `string` | – | Plan selected at first. Defaults to the first plan with a badge, then the first plan. |
| `defaultPeriod` | `"monthly" \| "yearly"` | `"yearly"` | Billing period at first. Default `yearly`. |
| `currency` | `string` | `"USD"` | ISO 4217 currency for every price. Default USD. |
| `locale` | `string` | – | BCP 47 locale for price formatting. Defaults to the device locale. |
| `features` | `string[]` | `FEATURES` | What the subscription unlocks, one short line each. |
| `featuresTitle` | `string` | – | Heading over the feature list. Default "Everything in {appName} Pro". Same as `labels.featuresTitle`. |
| `trialDays` | `number` | `7` | Free trial length in days. `0` means no trial: the button and the note change to a plain subscription. Default 7. |
| `savings` | `string \| null` | – | Badge on the yearly option. Defaults to the largest saving across plans ("Save 50%"); `null` hides it. |
| `onSubscribe` | `(planId: string, period: BillingPeriod) => unknown` | – | The subscribe button. Start the purchase here (RevenueCat, StoreKit, Play Billing) and return its promise: the button gathers into a spinner, then opens into a check, or shakes with "Try again" when it rejects. |
| `onSubscribed` | `(planId: string, period: BillingPeriod) => void` | – | Called about 900 ms after `onSubscribe` resolves, once the check has landed. Close the paywall here. |
| `onRestore` | `() => unknown` | – | Restore purchases. Return a promise; reject when nothing was found to show "Nothing to restore". |
| `onClose` | `() => void` | – | Close button at the top. Hidden when omitted. |
| `onOpenTerms` | `() => void` | – | Terms link in the footer. Hidden when omitted. |
| `onOpenPrivacy` | `() => void` | – | Privacy link in the footer. Hidden when omitted. |
| `labels` | `Partial<PaywallScreenLabels>` | – | Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. |
| `style` | `StyleProp<ViewStyle>` | – | Style for the outer container. |
| `testID` | `string` | – | Identifier for end-to-end tests. |

## Accessibility

- Plans are a radio group named 'Plan'; each card is a radio with its checked state. Native screen readers hear name, price per period, description and badge; the web reads the card's visible text.
- The billing toggle is a segmented control named 'Billing period' (from segmented-control). Prices read as their full formatted value (from rolling-number).
- The trial note is a polite live region on Android and the web, so a new price is announced there when the plan or period changes.
- Subscribe and Restore purchases are buttons that report busy and announce their result; Close is a labelled icon button.

## Motion

- Switching monthly and yearly rolls every price digit by digit, up or down with the change, and the per-month or per-year line under it rolls too.
- Selecting a plan springs the accent border and the radio dot in on the morph spring; cards sink to 98 % on touch down.
- Subscribe gathers into a spinner, then opens into 'Welcome to {appName} Pro' or shakes with 'Try again'.
- Reduced motion: prices change in place, the selection fades, no press scale or shake.

## Haptics

- select on the billing toggle (from segmented-control)
- tap, success and error from Subscribe and Restore purchases

## Performance

- Prices are rolling-number strips on the UI thread; switching the period re-renders only the cards and the note.
- The hero is one Image; pass an optimized local asset.

## Notes for AI

- Prices are numbers in currency units; pass currency (ISO 4217) and optionally locale. Do not pass formatted strings.
- Return the store purchase promise from onSubscribe; navigate away in onSubscribed. Return a promise from onRestore and reject when nothing was restored.
- The trial note is built from trialDays and the selected price; trialDays={0} switches the button to 'Subscribe for {price}'.
- savings defaults to the best yearly saving across plans; pass a string to override or null to hide it.
- heroImage comes in through props; the component bundles no images. Close, Terms, Privacy and Restore render only when their callbacks are passed.

## Related

- [Segmented control](https://goodui.dev/native/components/segmented-control.md): Two to five views of the same thing with an elastic highlight that stretches toward the new segment, can be dragged, and ticks at every boundary. Includes a billing toggle.
- [Rolling number](https://goodui.dev/native/components/rolling-number.md): A number whose digits roll in the direction of change, with digits that open and close in place and currency signs that glide instead of jumping.
- [Button](https://goodui.dev/native/components/button.md): A press-responsive button with haptics that runs async actions on its own: it gathers into a spinner, then opens into a success or error state.
- [Badge](https://goodui.dev/native/components/badge.md): A count, dot or status badge that springs in when it appears, pops when its value changes, caps at 99+ and shrinks away at zero; BadgeAnchor pins it to an icon or avatar.

## Guidance for AI tools

Paywall screen: A complete subscription paywall: a monthly and yearly toggle with a savings badge, plan cards whose prices roll digit by digit when the period changes, a feature list, a trial note that always states the price, restore purchases and an async subscribe button. Use only the documented props. Keep accessibility, reduced motion and both color schemes intact when adapting it.

Full library index: https://goodui.dev/native/llms.txt
