LIANA GRIGORY

Home / Writing

Shipping a web app through App Store review with Capacitor

Capacitor will wrap a working web application into a native binary in an afternoon. Review is the part that takes weeks, and almost none of it is about the wrapper. It is about the handful of things Apple checks that a website never had to answer for.

A wrapped site is not automatically rejected. A thin one is

The rule that gets quoted is that an app must not be a repackaged website. The rule as applied is narrower: the app must do something a Safari tab does not. Native session persistence, push, camera, location, offline behaviour, a home-screen presence with real state — any of these move it across the line. A binary whose entire content is a full-page WKWebView pointed at a public URL, with the same navigation chrome the site has, is the case that gets refused.

Never navigate with a link

This is the failure that produced the most confusing bug I have shipped. Inside a wrapped app, an <a href> to another page is a full document load. The tab bar disappears while it loads, the JavaScript context is destroyed, and if the network is slow or briefly absent, the user gets the connection-failure screen instead of the app. It looks like a crash and it is reported as one.

Inside the shell, every navigation must be a state change — switch the tab, render the view — and the only real page load is the one at launch. This is also what makes the app survive a lift lobby.

Draw no control that leads nowhere

A back arrow with nothing behind it is worse than no arrow, because a person taps it expecting to return. The naive fix — always draw it, call history.back(), and fall back to the site root when nothing happens — is a control that lies, and it lands the user somewhere they never asked to be.

The platform will not tell you the answer. history.length counts entries from before your app existed and never shrinks, and there is no canGoForward outside the Navigation API, which WebKit does not ship. So keep the bookkeeping yourself: stamp each entry with an index in history.state, remember the highest index reached in sessionStorage, and derive back from index > 0 and forward from index < highest. A new push truncates what was ahead, so pull the highest back down to match, or the forward arrow becomes a lie the moment someone branches.

Recompute on pageshow, not only on popstate. Returning to a page that was restored from the back-forward cache does not re-execute the document and does not fire popstate, so the arrows keep whatever they were showing when the page was frozen. That is the case that actually breaks, and it only reproduces on a device.

The four rejections that are administrative

Account deletion must be in the app. If a user can create an account inside the binary, they must be able to delete it and their data inside the binary too. Not a support email, not a web form — the same flow, in the app.

A reviewer account that works. A demo login that reaches the actual product, not a marketing screen, with a note saying exactly what to tap. Half of first-round rejections are a reviewer who could not get in.

The iPad build is reviewed as an iPad build. If the app is offered for iPad, someone will open it on one, and a login form pinned to the top-left of a 12.9-inch screen is a rejection. Either support the size class properly or declare iPhone only and mean it.

Privacy answers must match the binary. The data-collection declarations are checked against what the app actually sends. An analytics SDK you forgot is in there is a contradiction, and contradictions are the thing review is best at finding.

What review is really testing

Not novelty, and not architecture. Whether the thing behaves like an application to someone holding it for ninety seconds on an unreliable network. Every item above is a version of that same question.


Written by Liana Grigory, also written Liana Grigoryan — entrepreneur, technology founder and U.S. Army veteran in Los Angeles. More at Writing.