# Paywall above-the-fold — React Native build notes · 5 Aug 2026

**Goal: minimum possible dev time.** Everything here is decided — no design calls left to make.
Scope is **the fold only**: from under the existing coral header down to (and including) the docked
CTA bar. **Do not touch the coral "Upgrade to Premium" header, and do not rebuild anything below
"WHAT YOU GET"** — the new fold hands off into the existing feature section, retitled "What you get".

## The three files

| File | What it is |
|---|---|
| `PaywallScreen.tsx` | Drop-in component. All styles, copy, layout, selection logic done. |
| `PAYWALL_ABOVE_FOLD_REDESIGN_2026-08-05.html` (one folder up) | The interactive visual spec — open in a browser, tap the tiers, that behaviour is the acceptance behaviour. Measurements table inside. |
| `assets/members-strip.png` | 480×98 avatar strip (from `Group-1000004751.png`, live on the site). Serve @1x/2x/3x. |
| `assets/members-wall.webp` | 960×438 member mosaic (checkout pack, Aga-approved) — after the guarantee seal. |
| `assets/partner-logos.png` | 798×71 white press marks: Red Bull · Forbes · NewsWatch · AMC — after the wall, 86% width, 60% opacity. |

## What the devs wire — this is the entire job

1. **Products:** fetch RevenueCat offerings; map to the `tiers` prop in order `[annual, quarterly,
   monthly]` (the monthly product `monthly` @ $24.99 ALREADY EXISTS — display-only). Every displayed
   string comes from `product.priceString` / derived figures — **per-day denominators: 365 / 91 /
   30.4**; per-week (annual) = price/52; `zeroPriceString` =
   `Intl.NumberFormat(locale,{style:'currency',currency:product.currencyCode}).format(0)` (Android
   Hermes: enable the Intl flag). **If offerings fail, show the retry state — never a hardcoded
   price.**
   🔴 **Also read the trial length off the product's introductory-offer period** and pass it as
   `trialDays`. One number drives four strings — "Try it FREE for N days", the dock, the "Day N"
   timeline labels and the trial-end date. Hardcoding 7 means a trial change in App Store Connect
   silently makes the screen lie about when the customer is charged. ~15 min, and it removes the
   whole failure class.
2. **Actions:** `onPurchase` → `Purchases.purchasePackage(pkg)` · `onRestore` →
   `Purchases.restorePurchases()` · `onTerms`/`onPrivacy` → existing webview/links.
3. **Analytics:** `paywall_viewed` on mount, `tier_selected`, `purchase_started` (already called in
   the component — just point `track` at the existing pipe).

That's it. Copy, layout, colours, selection behaviour, timeline binding are all in the component.

## Start here — the screen runs before you wire anything

```tsx
import PaywallScreen from './PaywallScreen';
import {mockProps} from './fixtures/tiers.mock';

<PaywallScreen {...mockProps('full')} />   // or 'off50' / 'off30'
```

That renders the finished screen in a simulator with working tier selection, dock re-pricing and
timeline binding. **Check layout and start QA before touching RevenueCat.**

### Then swap the fixture for the real thing — this is the whole wiring job

```tsx
const offerings = await Purchases.getOfferings();
const pkgs = offerings.current!.availablePackages;
const fmt = (v: number, p: PurchasesPackage) =>
  new Intl.NumberFormat(undefined, {style:'currency', currency:p.product.currencyCode}).format(v);

const toTier = (key: 'annual'|'quarterly', p: PurchasesPackage) => {
  const isYear = key === 'annual', price = p.product.price;
  return {
    key, pkg: p,
    label: isYear ? '12-Month plan' : '3-Month plan',
    priceString: p.product.priceString,              // NEVER build this yourself
    perDay:   fmt(price / (isYear ? 365 : 91), p),
    perWeek:  fmt(price / (isYear ? 52  : 13), p),
    perMonth: fmt(price / (isYear ? 12  : 3),  p),
    billedLine: isYear ? `${p.product.priceString} billed once a year`
                       : `${p.product.priceString} every 3 months`,
    zeroPriceString: fmt(0, p),
    // promo: only when a REAL configured offer exists on the product (rule 2)
  };
};

// trialDays from the intro offer — drives four strings, never hardcode it
const trialDays = periodToDays(annualPkg.product.introPrice?.periodNumberOfUnits,
                               annualPkg.product.introPrice?.periodUnit);
```

⚠️ Android/Hermes: enable the Intl build flag or `Intl.NumberFormat` falls back to en-US.

## Hard rules (why the old screen is being replaced)

1. **No price literal anywhere.** The old screen hardcoded `$8 / week` and `$6.4 / month`; they were
   byte-identical on GB and PH devices while the charged price changed underneath.
2. **Version B's discounts must be real configured offers** (StoreKit promotional offer / intro
   offer resolving to $34.99 and $79.99 tiers) — never UI arithmetic.
3. **Version B has NO trial copy** — one introductory offer per subscription group; the discount IS
   the introductory offer. ⚠️ Verify against our StoreKit config before building B.
4. **Timeline + dock bind to the selected tier** — selecting quarterly must read "Day 7 — $49.99
   charged". Already implemented in the component; don't simplify it away.
5. **The CTA's claim must match the configured offer exactly** — Apple's payment sheet is the real
   charge UI, and it appears immediately after the tap.

## iOS answers to questions already asked

- **Sticky bottom bar: YES, fully native.** A plain `View` sibling of the `ScrollView` — the App
  Store's own product-page pattern. ⚠️ Use `react-native-safe-area-context` for the bottom inset —
  RN's built-in `SafeAreaView` is a no-op on Android.
- **The hero's teal glow is iOS-only** (RN shadows don't render on Android) — Android ships the
  teal border + gradient fill as the selected state, by design. The screen-top radial glow from the
  web spec is likewise cut in RN.
- **Seal shield icon:** replace the emoji placeholder with the 34×34 teal SVG shield
  (`react-native-svg`; paths in the HTML spec's seal block).
- **Header untouched:** the existing coral gradient header stays. If the app's navigator draws it,
  delete the `nav` block from the component.
- **Colour ground is `#0E1526` navy** (not black) so the new fold blends seamlessly into the
  untouched sections below it.
- **App Review 3.1.2:** Terms of Use + Privacy Policy links are in the dock (required on the
  purchase screen); Restore is reachable; renewal price and term are in plain text pre-purchase.

## 🔴 THREE variants, not two — and they are ONE component

`variant: 'full' | 'off30' | 'off50'`. Same layout, same code path; only the price source changes.

| | 12-Month | 3-Month | Trial | First-year saving |
|---|---|---|---|---|
| **full** | $159.99 | $49.99 | ✅ 7 days | $39.97 |
| **off30** | $111.99 | $34.99 | ❌ none | $27.97 |
| **off50** | $79.99 | $24.99 | ❌ none | $19.97 |

**The percentage applies to BOTH tiers.** Never mix rates (a 50% annual beside a 30% quarterly reads as
two offers and invites the reader to work out which is better — they won't, they'll leave).

Every promo figure is an exact percentage of list, so each maps to a real store price point:
$159.99 × 0.5 = $79.99 · × 0.7 = $111.99 · $49.99 × 0.5 = $24.99 · × 0.7 = $34.99. **Renewal is always
the full list price**, stated on the card, in the timeline and in the dock.

⚠️ **Both promo variants need their offers configured in App Store Connect before they can be built** —
four promotional offers in total. Until they exist, build `full` only.

## Cut on Aga's ruling — do not build

- The **Free vs Premium comparison table** (5 Aug ruling: removed entirely). The scroll order is now:
  hero → ramps → timeline → guarantee seal → "What you get" (existing section, one-string copy change).

## Verify before ship (product/marketing side, not dev)

- [ ] "100,000+ members" — confirm the live figure
- [x] ✅ **60-day guarantee — SETTLED 5 Aug, ships as written.** Research: **only Apple can refund an
      in-app purchase** (customer requests at `reportaproblem.apple.com`, ~90 days, Apple decides), so
      the website's "one email gets you a full refund, no questions, no forms" was **removed as false
      in-app**. The trial state leans on the platform promise Apple enforces ("you won't be charged for
      7 days"); the promo states keep the promise with an honest mechanism ("email us and we'll make it
      right"). **Aga ruled YES on 5 Aug** that TMA honours it by whatever route works — Apple refund
      request, or compensation our side. **No further action for you; build the copy as supplied.**
      refunds. Confirm support honours the 60-day promise for store subscriptions (compensation path)
      before the seal ships in-app. The wording "one email gets you a full refund" must be true.
- [ ] Version B: promo products actually configured ($34.99 / $79.99) + campaign end date wired to
      the hero's "ends in {n} days". **No countdown timers.**
- [ ] **ALL "we remind you" claims (now ONE string — the Day-5 timeline line)** — the promo cards'
      "we remind you before it renews" was cut for the same reason — the toggle was CUT (Aga, 5 Aug),
      and neither store guarantees a reminder (Play emails before trial end; Apple's is not
      promisable). Pick one: (a) schedule ONE local notification at purchase success (~5 lines,
      existing push permission) → keep `COPY.day5` AND `COPY.remindA`; or (b) zero-build → swap to
      `COPY.day5Fallback` and `COPY.remindAFallback` ("cancel anytime before day 7"). Version B's
      renewal line already carries no reminder claim.

## Acceptance checklist (QA on device)

- [ ] Fresh install, offerings load → prices match App Store Connect for that storefront's currency
- [ ] Airplane mode → retry state, no prices shown
- [ ] Tap each tier → hero visibly deselects (✓ chip moves), dock + timeline re-price
- [ ] VoiceOver: tiers announce name + price + selected state; all targets ≥44pt
- [ ] CTA → Apple payment sheet shows the same trial/price the screen claimed
- [ ] Restore works signed-out → signed-in
- [ ] Terms/Privacy open
- [ ] Below the dock, the existing "What you get" content renders unchanged
