Sprint M7 — second rejection from the apple store - the fix was not in the mobile app


The first rejection was a form field. I wrote about that last sprint. We filled the form in, sent the reply, and waited.

The second rejection arrived on September 2nd and said something stranger: the app uses a third-party login service without offering an equivalent option that meets three privacy requirements. Guideline 4.8. Apple even names the compliant option in the text — Sign in with Apple.

The instinct, again, was to go and change the app. And again it was wrong, for a reason it took most of a day to see clearly.

Domi’s mobile app does not have a login screen.

It has one button. The button opens the system browser at our own website, the website authenticates you, and the browser hands a token back. There is no list of sign-in providers anywhere in the binary. Nothing in the app knows that Google exists.

So the thing Apple rejected — “Continue with Google” — was on a web page. And that page is rendered by our server, which means it is the same page for every version of the app ever installed. A new build would have shipped with exactly the login screen that had just been rejected.

That is a strange feeling: a rejection you cannot fix by fixing the thing that was rejected. What fixed it was deploying Sign in with Apple to the website. I checked it by fetching the live page rather than trusting anyone’s word, including my own team’s — Apple’s button above Google’s, both drawn with byte-identical styling, which is what Apple means by an equivalent option being offered with equivalent prominence.

The version number that nearly shipped a broken app

Partway through the sprint I asked for an over-the-air update. Domi can push JavaScript to phones without going through the App Store, and there were four real fixes sitting on the main branch. It seemed obvious.

The agent refused, and it was right.

Over-the-air updates only replace the JavaScript. The native code — the compiled parts — comes from whatever binary is installed. Our updates are bucketed by version number, and the version had not changed. But the native surface had: seventeen Expo package upgrades had landed since that binary was built.

So the update would have been accepted, delivered, and handed JavaScript compiled against newer native modules to an app that does not have them. To every phone on that version. Including the one sitting in Apple’s review queue.

The check that catches this is a fingerprint — a hash of everything native in the project. Ours had moved from 51cd6c07… to 026be634…. Nothing in the tooling would have stopped the publish; the version number is the only gate, and the version number said they matched.

It caught us three times in one sprint. Two of the three ended in a version bump nobody wanted to make and everybody was glad about afterwards.

The reviewer that kept being right

Every change in this repo now gets reviewed by a second AI — a different model family from the one writing the code, on the theory that a reviewer from the same family shares the author’s blind spots.

It found roughly twenty defects this sprint. All but one were real. The patterns are more interesting than the number.

One rule, broken four times in a single change. The app promises never to show a queued write as though it had been saved. A new “add an event” button broke that promise, and it took four rounds to fix — because each round fixed the screen being looked at rather than the rule. The agenda got its indicator; the month view did not. Then the month view got one but the failure state was quietly dropped. Then a piece of pre-existing code — correct for months — turned out to be wrong the moment the indicator existed, because adding an indicator makes its absence a claim. The fix that finally held guards the rule: every write path on that screen, found automatically, rather than the ones somebody thought of.

Three tests narrower than the bugs they guarded. My favourite: a regression test written to catch a specific colour bug matched one spelling of it and missed the other, so a change whose entire subject was that bug shipped an instance of it past its own test. A test that catches one spelling of a defect reports the other as clean.

One finding was wrong, and holding it was the right call. The reviewer twice asked us to write all-day events as ending at the following midnight — which is the calendar-industry convention, and genuinely what most systems do. Ours does not. Our own server’s test says so in one line. Complying would have stored every all-day event one day too long. The agent refused twice, then did something better than arguing: it moved the server’s test, quoted, into the code at the exact line someone would change. The finding did not come back.

That is the lesson I want to keep from this sprint. Not “the reviewer was usually right”, though it was. Put the evidence where the next reader will be standing — not in a comment thread underneath, not in a pull request description that will go stale. In the code, at the decision.

The colour that was never a colour

Somewhere in the middle of all this, an agent read the database schema instead of the screen and found that calendar events had never been the right colour.

Events store a colour nameblue, teal, graphite — and the app was handing that name straight to the phone as if it were a colour code. Nine of the ten names happen to be valid CSS colours, so nine of ten events rendered a plausible, confident, wrong colour. graphite is not a CSS colour, so those rendered as nothing at all. And the function deciding whether to draw the label in black or white tested for a colour code, never matched a name, and always guessed the same way.

It had been like that since the calendar shipped. Nobody noticed, because wrong-but-plausible colours look like a design decision.

What actually shipped

Twenty-five pull requests. Tests from 454 to 523. Two new builds — one that was made and never used, one now on TestFlight as version 1.0.3.

The app got: multi-day events drawn as a single bar across the days they span instead of repeated once per day; a way to add an event with a button, which it genuinely did not have before; “Ask Domi”, a plain-sentence box on the home screen that turns “dentist Tuesday 2pm” into something to confirm; a fix for dark mode rendering the light-mode grey app-wide at a contrast ratio below the accessibility floor, across 93 places; and two authentication fixes, one of which had a phone quietly retrying a dead session twenty-two times in ninety seconds.

What I am not claiming

None of it has been seen on a phone.

Two new screens, a calendar layout, French labels that run a third longer than English, tap targets that must clear a minimum size — all verified by tests, and tests cannot see any of it. This repo has already shipped two defects that rendered perfectly and did nothing at all. Passing is not the same as working, and the next sprint opens on the device pass none of this has had.

The app is not in the store. But the reason it is not in the store is now a queue position rather than a mistake, which is a better place to be than we were a week ago.