PılPılı ← All posts

navigator.share on iOS silently drops your text: detection and the workaround

PilPili is a browser-game arcade of small games, built by one person directing AI for about two hours a day. A share button is the highest-value control in a free browser game — word of mouth is the growth model, and every share is a player doing your marketing with their own credibility. Ours took three rounds to get right, and in each round the platform actively concealed what was wrong. The middle mistake is the one I'd most like to save you from.

Round one: the URL that arrived alone

The web share API takes a title, text, and a URL as separate fields. Fill them all in — obviously — and on many share targets the message arrives as a bare link, your carefully written "I scored 2384 — can you beat it?" silently discarded. Some targets keep only the URL field when both are present.

The fix is inelegant and works everywhere: put the score and the link in the one text field, and never set the URL field at all. Chat apps detect the URL inside the text and still build their preview card, so folding it in costs nothing — and the text can no longer be dropped separately from the link, because there is no "separately."

One adjacent lesson: we render each result as a small image — a share card — because a picture reads in a chat list without anyone tapping and survives forwarding. It must be built when the run ends, not when share is tapped: the share call has to run inside the user's tap gesture, and awaiting an image encode puts it outside one. Build early, share instantly.

Round two: canShare() says yes and the platform ignores you

Then shares from one game arrived with the picture and no text, while another game's arrived complete. Same code. The difference was the platform: on iOS, handing the share sheet a file makes most targets drop the accompanying text — the notes app and the clipboard both do.

canShare() returns true for the file-plus-text payload. The API accepts everything; the target discards the text afterwards, past the point any code can detect. There is no feature check for "will the words survive," because the discarding isn't the API's doing. It answers "will the call succeed," which is not "will the user's message arrive."

So we did the obvious defensive thing: on iOS, drop the image and share text-plus-link, so the words always survive.

That was the middle mistake.

Round three: the fix that stole the card from every iPhone

Dropping the image on iOS quietly took the share card away from every iOS player — the picture that does the actual selling in a group chat, gone from the platform with half the audience. No test caught it. I caught it, sharing from two of our games on my own phone either side of that deploy — one arrived with a card and one didn't, and the difference between them was nothing but the date they were last built.

The final shape accepts the platform as it is: the card ships on every platform, the URL is printed on the card itself, and text-plus-link are attached as well for the platforms that carry them. On iOS you cannot reliably have both words and picture — so the picture wins, and it carries the words that matter baked into its pixels, where no share target can strip them. A link someone must retype from an image is worse than a tappable one — but it beats a share with no card, and every other platform still gets both.

The bug underneath: the games that never had a card at all

While verifying all this in real browsers, we found something the automated harness had structurally missed: four of our older games — the ones predating the shared game-over engine — had never built a share card at all. Not broken; never wired.

Why did no automated check catch it? The headless test harness fakes the canvas — drawing is recorded, not rasterised — so the image-encode call that produces a card never completes under it. A share card cannot exist in that environment, which means its absence cannot fail a test. The only way this class of defect gets caught is a real browser and a real share sheet — so those games got test seams, and "verify sharing" moved permanently onto the short list of things checked on actual devices.

What survives contact

Fold the URL into the text and never set the URL field. The one payload that cannot be split is the one that arrives whole.

When you degrade a feature on one platform, name what every user of that platform just lost. Our iOS "fix" was locally correct and globally a regression; it was caught not by a test but by a phone in a hand.

Know what your test double can't see. Anything that terminates in a real encoder, a real share sheet, a real anything — a mocked environment will happily report its absence as a pass, forever.