This document describes the behavior that is implemented today. It is not a roadmap.
The canonical variant list lives in the registry, the
public callback contract in types, and the gallery route in
[paywall].tsx.
- Every experience renders on a calibrated
390 × 844reference canvas. Resizing is uniform; it does not create additional responsive breakpoints inside a replica. initialVariantchooses the initial registered visual state. Missing or unsupported route variants fall back to the registry default.- Changing
replayKeyresets or remounts the experience. A repeated, unchanged key does not replay anything. onClosePress,onPurchasePress, andonRestorePressare host callbacks. The demo route navigates back for close and displays alerts for purchase and restore; the replicas do not contain store SDKs or real legal-link navigation.onActionPress(id)receives the semantic IDs listed below when a host supplies it. Haptics and any local/fallback behavior run before that callback.onVariantChange(value)is called only by implemented transitions between registered compositions or tiers. Local plan choices emit semantic action IDs instead.- Every
ReplicaPressablesupplies brief opacity/scale press feedback. The motion notes below describe additional animation only.
The root route / opens the invisible-switching gallery. /:paywall opens one replica.
The route accepts these query parameters:
| Parameter | Behavior |
|---|---|
variant |
Used only when it exactly matches a registered variant for that paywall; otherwise the default is used. |
replay |
Opaque string passed as replayKey; change it to reset/replay the current experience. Defaults to 0. |
Examples:
/chatgpt
/chatgpt?variant=go
/flo?variant=all-plans
/picturethis?variant=comparison&replay=1
/strava?variant=improvements&replay=qa-2
/yazio?replay=2
The equivalent custom-scheme form uses the configured paywall-showcase scheme, for
example paywall-showcase:///strava?variant=bundle&replay=3. An unknown paywall path
falls back to ChatGPT; an invalid variant falls back to that paywall's default.
| Paywall | Default | Registered variants |
|---|---|---|
| ChatGPT | plus |
plus, go |
| Cleanup | smart-cleanup |
smart-cleanup, unlimited |
| Facetune | trial |
trial, vip |
| Flo | plans |
plans, all-plans |
| Learna | personalized |
personalized, trial |
| MyFitnessPal | standard |
standard, offer |
| Perplexity | pro |
pro, max |
| PictureThis | trial |
trial, comparison |
| Strava | bundle |
bundle, improvements |
| YAZIO | default |
default |
State: tier = plus | go, initialized from the registered variant. The segmented
control moves between both tiers, updates the feature table and pricing, calls
onVariantChange(tier), and resets to initialVariant on replay.
- Motion: a
210 mstier transition moves the segment pill, crossfades marks/content, and resizes the feature viewport. There is no mount animation and this transition is not reduced-motion gated. - Scroll/sticky: only the feature comparison viewport scrolls vertically. The lower fade, upgrade button, web-purchase button, and renewal copy remain fixed.
- Modal/unsupported: no modal, no restore UI, and no autonomous tier paging.
- Actions:
chatgpt.select-go,chatgpt.select-plus,chatgpt.close,chatgpt.upgrade-go,chatgpt.upgrade-plus,chatgpt.purchase-on-web,chatgpt.learn-more(visible on Go only).
Registered variants are externally selected; there is no in-screen switch between
smart-cleanup and unlimited.
smart-cleanup has no local selection state. On mount/replay, its storage fill grows
for 680 ms. unlimited starts with plan = weekly; selecting lifetime or weekly
crossfades the selection treatment over 190 ms, emits the corresponding semantic
action, and changes the CTA. Its storage meter also fills over 680 ms on mount/replay.
- Scroll/sticky: both variants use a full-canvas, non-bouncing vertical
ScrollView. There is no sticky footer, horizontal scrolling, or modal. - Motion limitations: neither storage nor plan motion is reduced-motion gated; there is no cross-variant animation.
- Actions, smart cleanup:
cleanup.restore,cleanup.try-free,cleanup.privacy-policy,cleanup.terms-of-use. - Actions, unlimited:
cleanup.close,cleanup.select-weekly,cleanup.select-lifetime,cleanup.start-trial,cleanup.buy-lifetime,cleanup.terms,cleanup.restore.
State: stage = vip | trial. vip starts with plan = yearly; the user may select
weekly or yearly. trial is a timeline explainer. Its Show all plans action is
a one-way trial → vip transition; there is no in-screen route back to the timeline.
Replay restores the requested initial stage.
- Motion:
trial → vipcrossfades over230 ms; VIP plan selection crossfades over190 ms. The stage transition callsonVariantChange('vip'); plan selection emits typed semantic actions. Motion is not reduced-motion gated. - Scroll/sticky/modal: each stage uses a non-bouncing full-canvas vertical
ScrollView. The VIP color palette is a real staticBlurView; it is not an interactive modal. There is no sticky purchase region. - Actions:
facetune.restore,facetune.close,facetune.start-yearly-trial,facetune.show-all-plans,facetune.select-yearly,facetune.select-weekly,facetune.continue-yearly,facetune.continue-weekly.
State: mode = plans | all-plans. The default plans sheet starts with
plan = yearly and trialEnabled = false; it offers yearly and family.
all-plans retains the current plan/trial selection and adds monthly. Opening it changes
the registered mode and calls onVariantChange('all-plans'). Go back, backdrop tap,
or a completed downward dismissal returns to plans and calls
onVariantChange('plans'). Because monthly is not visible in the featured sheet,
dismissing with monthly selected restores the visible yearly selection.
- Motion: the testimonial pager supports native horizontal swipes and advances every
4.8 s. The all-plans sheet enters over310 mswith a dimmed backdrop and exits over260 ms. A downward pan dismisses after78 ptor720 pt/s; otherwise it springs back. The all-plans trial switch animates for180 ms. None is reduced-motion gated. - Scroll/sticky/modal: the fixed lower default sheet is itself a non-bouncing vertical
ScrollView. All plans is a modal bottom sheet with its own non-bouncing vertical scroll, backdrop dismissal, and pan gesture. The testimonial pager is horizontal. - Deliberately unsupported: no swipe between registered variants and no real App Store checkout.
- Actions:
flo.close,flo.restore,flo.select-yearly,flo.select-family,flo.select-monthly,flo.toggle-trial,flo.continue-yearly,flo.continue-yearly-trial,flo.continue-family,flo.continue-family-trial,flo.continue-monthly,flo.continue-monthly-trial,flo.view-all-plans,flo.dismiss-all-plans,flo.go-back,flo.subscribe-app-store,flo.limited-access.
State: externally selected personalized | trial. Both variants share the same close,
restore, purchase, and legal controls. Changing initialVariant or replayKey resets
the visual variant; there is no in-screen variant transition or local plan state.
- Motion/scroll/modal: fixed layout only. There is no
ScrollView, mount animation, sticky region, modal, or carousel beyond normal press feedback. - Actions:
learna.close,learna.restore,learna.try-free,learna.privacy,learna.terms.
State: experience = standard | offer. standard starts with plan = annual and the
learn-more sheet closed. The user may select monthly or annual. Tapping the standard
close control intentionally performs standard → offer; it does not call the host
close callback. The offer is a fixed promotional annual product. No thanks is the
actual dismiss action. Replay restores the requested initial experience, annual plan,
and closed sheet.
- Motion:
standard → offerfades/translates over220 ms. The learn-more sheet opens over190 msand closes over170 ms. These transitions become immediate when reduced motion is enabled. Plan-card selection itself changes immediately. - Scroll/sticky/modal: no scrolling. Learn more is a modal sheet with a tappable backdrop and Got it dismissal. There is no sticky footer.
- Deliberately unsupported: no route from the promotional offer back to standard and no restore control.
- Actions:
myfitnesspal.select-annual,myfitnesspal.select-monthly,myfitnesspal.close-to-offer,myfitnesspal.try-free-annual,myfitnesspal.try-free-monthly,myfitnesspal.upgrade-promo,myfitnesspal.select-promo-annual,myfitnesspal.no-thanks,myfitnesspal.learn-more,myfitnesspal.dismiss-learn-more.
State: tier = pro | max; Pro additionally has proPlan = yearly | monthly and starts
yearly. Tier selection calls onVariantChange(tier). Replay restores the requested
tier, yearly Pro plan, and the appropriate model-strip offset.
- Motion: tier changes over
225 ms, moving the segment pill and crossfading feature and pricing layers. The horizontal model strip automatically scrolls to the tier's offset. Both behaviors become immediate under reduced motion. Pro price-card selection is immediate. - Scroll/sticky/modal: only the model strip scrolls horizontally; beginning a manual
drag emits
perplexity.scroll-models. There is no vertical scroll, modal, or sticky region. The feature card and top controls use real static blur. - Actions:
perplexity.select-pro,perplexity.select-max,perplexity.select-yearly,perplexity.select-monthly,perplexity.select-max-monthly,perplexity.close,perplexity.restore,perplexity.get-pro-yearly,perplexity.get-pro-monthly,perplexity.get-max,perplexity.scroll-models.
Registered variants are externally selected; there is no visible pager or transition
between trial and comparison.
trial starts with plan = annual (the seven-day trial product) and reminder off.
Users can select the monthly product, toggle the code-native reminder switch, purchase, or
open the four legal/restore actions. Replay resets annual selection and the reminder.
comparison has no local selection state: Get Plus and Get Pro are direct
purchase actions.
- Motion: trial plan surfaces crossfade over
180 ms, becoming immediate under reduced motion. The custom reminder track/thumb transition runs for190 msand also becomes immediate under reduced motion. There is no mount animation. - Scroll/sticky/modal: both variants are fixed layouts with no scroll, sticky sheet, blur, or modal.
- Actions, trial:
picturethis.select-annual,picturethis.select-monthly,picturethis.reminder-on,picturethis.reminder-off,picturethis.continue-annual,picturethis.continue-monthly,picturethis.cancel,picturethis.terms,picturethis.subscription-terms,picturethis.privacy,picturethis.restore. - Actions, comparison:
picturethis.close,picturethis.get-plus,picturethis.get-pro,picturethis.terms,picturethis.subscription-terms,picturethis.privacy,picturethis.restore.
Registered variants are externally selected; there is no visible switch between
bundle and improvements. Both share a fixed toolbar and Skip action.
bundle starts with plan = annual; the user can select the Strava + Runna bundle or
annual Strava product. Replay restores annual. improvements has no local selection
state and exposes separate web-checkout and in-app-purchase actions.
- Motion: bundle selection crossfades over
180 ms, becoming immediate under reduced motion. There is no mount/hero animation or cross-variant motion. - Scroll/sticky/modal: both bodies are fixed layouts behind a fixed header and an opaque, shadowed bottom purchase sheet. The sheets are not blurred or translucent. There is no scrolling, horizontal carousel, or modal.
- Actions, common:
strava.skip. - Actions, bundle:
strava.select-bundle,strava.select-annual,strava.start-free-trial-bundle,strava.start-free-trial-annual. - Actions, improvements:
strava.start-free-trial-web,strava.pay-in-app.
State: one registered default variant with plan = annual | quarterly, initialized
to annual. Replay resets annual, scrolls the vertical body and review rail back to zero,
and replays the supported entrance fade.
- Motion: the screen fades in over
350 ms; plan surfaces crossfade over180 ms. Both become immediate under reduced motion. Vertical and horizontal scroll use native motion. There is no looping mascot/food animation. - Scroll/sticky/modal: the body scrolls vertically. A nested testimonial rail scrolls
horizontally and snaps every
260 pt. The close control and lower CTA footer remain fixed; the footer uses an alpha white gradient, not blur. - Deliberately unsupported: close only invokes the host callback. The discount-wheel flow that can follow this paywall is not implemented because it has no supplied replica state.
- Actions:
yazio.select-annual,yazio.select-quarterly,yazio.close,yazio.continue-annual,yazio.continue-quarterly.
The semantic IDs describe intent; they do not perform commerce, authentication,
external navigation, or legal-document routing on their own. A production host should
map onActionPress and the purchase/restore/close callbacks to its own services. Visual
variant selection remains invisible inside PictureThis, Strava, Cleanup, and Learna by
design; use initialVariant or the validated route query instead of adding controls to
those replicas.