Skip to content

Latest commit

 

History

History
272 lines (225 loc) · 13.9 KB

File metadata and controls

272 lines (225 loc) · 13.9 KB

Interaction specification

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.

Shared runtime contract

  • Every experience renders on a calibrated 390 × 844 reference canvas. Resizing is uniform; it does not create additional responsive breakpoints inside a replica.
  • initialVariant chooses the initial registered visual state. Missing or unsupported route variants fall back to the registry default.
  • Changing replayKey resets or remounts the experience. A repeated, unchanged key does not replay anything.
  • onClosePress, onPurchasePress, and onRestorePress are 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 ReplicaPressable supplies brief opacity/scale press feedback. The motion notes below describe additional animation only.

Routes and query parameters

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.

Registered variants

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 machines

ChatGPT

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 ms tier 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).

Cleanup

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.

Facetune

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 → vip crossfades over 230 ms; VIP plan selection crossfades over 190 ms. The stage transition calls onVariantChange('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 static BlurView; 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.

Flo

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 over 310 ms with a dimmed backdrop and exits over 260 ms. A downward pan dismisses after 78 pt or 720 pt/s; otherwise it springs back. The all-plans trial switch animates for 180 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.

Learna

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.

MyFitnessPal

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 → offer fades/translates over 220 ms. The learn-more sheet opens over 190 ms and closes over 170 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.

Perplexity

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.

PictureThis

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 for 190 ms and 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.

Strava

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.

YAZIO

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 over 180 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.

Host integration boundary

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.