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.
- 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 — withPurchaseSurface.Class(Store/Core/Source/PurchaseSurface.cs):control.AsPurchase(), orPurchaseSurface.With(existingClass)to keep a class such ascta-pill. The mirror on the client isPURCHASE_CLASS. - Any link, nav link, menu item, button or redirect whose href is a purchase route
(
isPurchaseRoute). - In rendered HTML (Markdown,
Htmlcontrols): 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). - Navigation to a purchase route is refused by the app shell (
refusesNavigation): a tapped link, a serverNavigationRequest, 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. - 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
Subscribeentry onStore).
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
clients/react/src/render/clientCapabilities.test.tsx— the platform decision, the route list, the marker, hiding (and not hiding) controls, stripping links from HTML.app/react-native/src/purchases.test.tsx— the iOS capability isfalse, the shell refuses purchase navigation, a cover renders with Get free and Open but no purchase control, the menu lists no purchase entry.- In-mesh:
Store/Core(the marker),Store/Catalog(card Get, hero pill),Store/Plugin(cover CTA row, folded menu).
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.