Purchases in the iOS app

Plans are sold through Stripe (Store/Order, /Store/Plans, /Store/Subscribe). App Review guideline 3.1.1 does not allow an iOS app to offer a purchase — a buy, subscribe or upgrade button, a price, a link to a payment page — that does not go through Apple's in-app purchase. So the iOS native build of the Memex app (app/react-native, Platform.OS === "ios") shows none of them. The web, the Mac app (which wraps the web build) and the Blazor portal are unchanged.

Free acquisition keeps working in the iOS app: the free plan's one-click Get, a plan the person already holds (one click acquires a covered package, or it is acquired on arrival), Open, Start and the install step. Content bought on the web is simply there.

One capability, declared by the client

The shared renderer (clients/react) has a ClientCapabilities context with one field, purchasesAllowed (default true). The React Native app declares it once, in app/react-native/src/purchases.ts:

export const clientCapabilities = { purchasesAllowed: purchasesAllowedFor(Platform.OS) };

purchasesAllowedFor(os) is false for ios only. There is no other if ios about purchases anywhere: every rule below reads that one flag.

What the renderer drops when purchases are not allowed

All of it is central — ControlRenderer, the HTML seam, the menu and the shell — so a new layout area inherits it by marking its controls, not by testing the platform.

  1. Any control marked mw-purchase, with its subtree. The server marks a purchase call-to-action whose destination the client cannot read off the control — a server click action, a price line, a pitch that names a paid plan — with PurchaseSurface.Class (Store/Core/Source/PurchaseSurface.cs): control.AsPurchase(), or PurchaseSurface.With(existingClass) to keep a class such as cta-pill. The mirror on the client is PURCHASE_CLASS.
  2. Any link, nav link, menu item, button or redirect whose href is a purchase route (isPurchaseRoute).
  3. In rendered HTML (Markdown, Html controls): an anchor carrying the marker is removed with its text (it is a call-to-action, such as the Store hero's plans pill); an anchor to a purchase route keeps its text and loses the link, so a sentence such as "comes with the Pro plan — see the plans" stays readable (stripPurchaseLinks).
  4. Navigation to a purchase route is refused by the app shell (refusesNavigation): a tapped link, a server NavigationRequest, a pasted or opened link (also one naming another mesh — no mesh switch, no sign-in), and a resolved target whose node and area together name one (isPurchaseTarget: Store + Subscribe). The person stays on the page they were on.
  5. A left-menu entry that leads to a purchase route is not listed — by its href, or by the target it resolves to on the current node (an area-only Subscribe entry on Store).

Routes are compared as a browser opens them: //host/… is an absolute URL, \ reads as /, and the path is percent-decoded with empty, . and .. segments resolved (/Store/./Subscribe is the checkout).

Purchase routes

Route Why it is one
/Store/Plans (and below) The plans page — prices and Subscribe links (Subscriptions.PlansPath).
/Store/Subscribe (and below) The plan checkout (Subscriptions.CheckoutPath).
checkout.stripe.com, billing.stripe.com, buy.stripe.com A payment provider's page.

Deliberately not purchase routes: /{plugin}/Subscribe — the paywall is also where a gated page redirects and where the free plan's Get lands, so its paid parts are marked instead — and /Store/MyPlan, which shows the plan a person already holds (its billing buttons are marked).

What is marked on the server

Surface Marked
Cover CTA row (UnentitledCta) Subscribe and Contact sales (CoverCtaButton.Purchase), also when folded into a menu. Not the free Get, and not a Subscribe the viewer's live plan already covers (it opens the paywall's no-charge unlock).
Cover purchase panel The price line, the plan pitch, Subscribe, Contact sales. Not Get free.
Paywall (/{plugin}/Subscribe) Price line, plan pitch, See the plans, the coupon row, the contact-sales form, the checkout line and its redirect, the billing form and the pay button. Not the plan-covered Get, not Open.
Store landing The hero's plans pill.
Catalog cards Subscribe (an unbought paid or contact-sales card), a non-zero price, "Let's talk". Not the free Get, not a card the viewer's plan covers, not a bought card's start verb, not the plan label.
Plans page and plan checkout The whole plans page; See the plans after a payment.
My plan (/Store/MyPlan) The price, Manage billing, the billing-portal link and hint. Cancel stays.

Coupons are hidden deliberately. Guideline 3.1.1 also rules out an app's own mechanism for unlocking content — license keys, codes — so the paywall's coupon row is a purchase surface even when the code is free. A code is redeemed on the web; what it unlocks is then simply there in the app. A key the person already HOLDS (one click unlocks a package it covers) is not a code entry and stays.

Manage after a payment links to the person's own profile (Subscriptions.ManageLink), not to a payment page, so it stays; on that page the billing controls are marked.

The "which plan includes X" refusal an install request answers with (InstallRequestControlPlane) is Markdown with a link to /Store/Plans; rule 3 turns it into plain text.

Tests

Adding a purchase surface

Mark it: .AsPurchase() on the control (or PurchaseSurface.With(...) beside another class), or link it to a purchase route. Never test the platform in a layout area — the server does not know which client renders it, and the one decision belongs to the client.