chore(mobile): ota backport for runtime 1.6.2 - #262
Open
GSTJ wants to merge 13 commits into
Open
Conversation
The stored tap is now keyed by the notification id and consumed once. Both listeners claim the tap before handling it, so the mount path and the app wide listener cannot both report the same one. Hand ported: this base has no pending dog profile store, so the usePendingDogProfile mock, harness and its five tests are left out and the suite covers the replay cases only. The re-engagement push kind is on this base, so the kind passed through to the reported open is kept. Backport of #231.
Title Case is an English convention. Portuguese capitalises the first word and proper nouns only, so the interface read as a translation rather than as Portuguese. Hand ported: only the pt-BR translation keys that exist on this base are changed. The web.json part is dropped, since the sections it rewrote (footer and waitlist) are not on this base. Backport of #252.
The empty state column runs past the fold on a short phone, and the last thing in it is the preferences button the copy points at, so it scrolls now. The notify opt-in stops being a disabled button once it is taken: a disabled button paints its label at half opacity, 1.7:1 on the light background, with nothing saying why it would not respond. A check and a full contrast label read as an answer instead. The preferences tap is counted, so the empty deck funnel can see the action the copy actually asks for. Hand ported: the SharePromptCard the original renders inside this column belongs to the native share work, which is not on this base, so the card, its import, its test mock and the three tests that assert on it are left out. The reachability, done state, analytics and copy changes stand on their own. Backport of #248.
GSTJ
force-pushed
the
release/ota-1.6.2
branch
from
September 4, 2026 22:15
e4bb526 to
7736f26
Compare
The floor is per platform now, and the client sends the platform with the version so the server can answer with the right one. Without this a 1.6.2 client sends no platform header and ignores the per platform floors, so there is no way to retire the old runtime without cutting off both platforms at once. The update wall also clears the stack before it navigates, sets its flag only once the navigation went through, and no longer takes the back gesture, so a single back swipe cannot put someone back into the app the floor just locked. Hand ported: this base has no pending dog profile store, so that import is left out of the root layout. The packages/api half is kept because the mobile tRPC types infer from the router and the echo tests cover the floor logic; it is inert here, since the API ships with the server and production already runs main. Backport of #274.
feat(mobile): say whether an upgrade was a real purchase
(cherry picked from commit b5f804e)
(cherry picked from commit 42a75c4)
…plies (cherry picked from commit 750ed21)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is a backport branch for the users still on runtime 1.6.2. It is based on
37fa5e2, the commit behind the last EAS update those users actually received, not on thev1.6.2tag. CI kept publishing updates frommainto runtime 1.6.2 until the universal links merge bumped the version to 1.7.1, so a 1.6.2 phone today is already running37fa5e2, not the tag.Do not merge this into
main. It exists only to publish over the air updates to the 1.6.2 channel. Merging it would replay commits that are already onmain.Details
Ported on top of
37fa5e2, each as its own commit:fix(mobile): stop the swipe screen replaying the last notification tap(backport of fix(mobile): stop the swipe screen replaying the last notification tap #231). The stored tap is keyed by the notification id and consumed once, so the mount path and the app wide listener cannot both report it. The pending dog profile store does not exist on this base, so its mock, harness and five tests are left out; the suite covers the replay cases only. The re-engagement push kind is on this base, so the kind carried into the reported open is kept.fix(shared): put the Portuguese interface in sentence case(backport of fix(shared): put the Portuguese interface in sentence case #252). Only the pt-BR translation keys present on this base change: 29 values, no keys added or removed. Theweb.jsonhalf is dropped, because the footer and waitlist sections it rewrote are not on this base.fix(mobile): make the empty deck reachable and its done state readable(backport of fix(mobile): stop the sign in banner from cutting the hero headline #248). The empty state column scrolls, the notify opt in stops being a half opacity disabled button once taken, and the preferences tap is counted. TheSharePromptCardthis column renders upstream belongs to the native share work, which is not on this base, so the card, its import, its test mock and the three tests that assert on it are left out. Everything else stands on its own.feat: minimum app version per platform(backport of feat: minimum app version per platform #274). The minimum supported version becomes a per platform floor, and the client now sends its platform alongside its version so the server can answer with the right one. This is what makes retiring 1.6.2 possible: without it a 1.6.2 client sends no platform header and ignores the per platform floors, so the only way to move the floor would be to cut iOS and Android off together. The update wall also clears the stack before it navigates, sets its seen flag only once the navigation actually went through, and no longer accepts the back gesture, so one back swipe cannot drop someone back into the app the floor just locked. The pending dog profile import the original adds to the root layout is left out, since that store is not on this base. Thepackages/apihalf comes along because the mobile tRPC types infer from the router and the echo tests cover the floor logic; it is inert for this branch, since the API ships with the server and production already runsmain.feat(mobile): tag analytics with the running update(backport of feat(mobile): tag analytics with the running update #285). Every PostHog event now carries the update the device is actually running:ota_update_id,ota_is_embedded,runtime_version,ota_channelandota_created_at, registered once as super properties so they ride along on exceptions too, not just onanalytics.trackcalls. This is what makes "did the update reach anyone?" answerable for the people stuck on the 1.6.2 binary, whose$app_versionsays 1.6.2 forever. The metrics script half of the original is dropped:scripts/metrics/does not exist on this base, and the readout runs frommain.fix(mobile): keep a signed in user out of the sign in loop(backport of fix(mobile): keep a signed in user out of the sign in loop #283). The launch query now retries under the shared transient policy with a four second ceiling per attempt and a ten second ceiling on the whole decision, and a device that already holds a token routes into the app instead of being dropped back on sign in when the network is slow. Both commits of the original come across. Thepackages/apihalf, which turns the already logged in throw into a coded CONFLICT, is left out: it ships with the server and production already runsmain, and the mobile side only calls the existing echo route, so nothing here depends on it.fix(mobile): re-upload profile photos when the grant went stale(backport of fix(api): let profile photos outlive a slow first form #284). Saving a profile whose upload grant expired now puts the photos back in the bucket and saves once more, exactly one extra attempt, and falls back to a toast in Portuguese and English telling the person to add the photos again. The two translation keys come along because they ship inside the bundle. Thepackages/apihalf, which lengthens the grant to an hour, is left out for the same reason as above, and it already reached production frommain. The client side is the floor under that fix, not a replacement for it.fix(mobile): look again when the keychain is not ready at launch(backport of fix(mobile): look again when the keychain is not ready at launch #286). The token read at launch now asks the keychain up to three times with a growing pause between attempts. iOS answers that read with "a required entitlement isn't present" when the app process is not attached yet, which is what a launch nobody started looks like: the system prewarming the app, or a notification waking it while the phone is still locked. A phone with no token is not affected, because an absent item comes back as an empty answer rather than a failure and never costs a retry, and a read that keeps failing still fails, so a real misconfiguration stays visible. Worst case this adds 450ms, and only to a launch that was already failing. Both halves of the original cherry pick clean onto this base, so nothing is left out. This one matters most here: every event in the audit came from the store build, so the 1.6.2 users are the only ones it reaches.feat(mobile): say whether an upgrade was a real purchase(backport of the mobile half of fix(ci): show what upgrades bought and where subscription events go missing #290). A successful upgrade now carriessourceandis_sandboxnext to the package it bought, so an App Store charge, a sandbox receipt and the premium grant the end to end flows hand themselves stop counting as the same thing in the readout. Only the store purchase produces a subscription event on the server, which is how successful upgrades could show up with nothing behind them.is_sandboxis null rather than false when the entitlement cannot be read, so a receipt nobody could open is not filed as real money. The server half of the original is left out: it ships with the server, and production already runsmain.feat(api): make the free daily like limit configurable(backport of feat(api): make the free daily like limit configurable #295). The free daily like allowance stops being a number compiled into the app. The launch query and the foreground check both readfreeDailyLikeLimitoff the echo route, and the refusal the server sends when someone runs out carries the allowance it actually enforced, so the limit sheet copy and theLike Limit Reachedevent report the number the server used rather than the number this binary was built with. A payload without the field leaves the shipped default of ten in place, which is what an older server answers with, so nothing regresses if the two sides disagree. All three commits cherry pick clean onto this base. Thepackages/apihalf comes along because the mobile tRPC types infer from the router; it is inert on this branch, since the server ships frommain, where the same change already landed.Deliberately not ported:
mainbefore the version bump, so backporting them would be a no op at best.Measurement. The baseline from #188 is 800 re-engagement pushes sent this week with 0 opens recorded, because 1.6.2 does not track opens at all. The readout is the PostHog event
Push Notification Openedfiltered to app version 1.6.2.Publish record, newest first:
Seventh publish: update group
d2d03804-1c3d-4010-88ab-36e6fd594605from commit5f280c2(server driven free daily like limit, feat(api): make the free daily like limit configurable #295) on channelmainat runtime 1.6.2, iOS update01a07b68-6aac-716b-b627-a6c11a576ff0. Preflight group06eb7d5d-181c-4fb2-99e1-225d325b7ffd, iOS update01a07b65-3d7e-7469-bccb-508f706ee4fa, was verified first on a release simulator build pinned to theota-preflightchannel and built before the preflight group was published. The binary carriedexpo-channel-name: ota-preflightin its update request headers at runtime 1.6.2. On the first launch the device loggeddidFinishBackgroundUpdateWithStatus=NewUpdateLoadedagainst that update id, and on the second it came backcheckCompleteUnavailablewithisUpToDate=1and no update left to fetch. Its updates database recorded two successful launches and zero failed ones against that update id, and zero launches against the embedded bundle, so the published bundle is what ran both times. No crash was logged and the app rendered its screen past the splash on both starts. Rollback of this one alone iseas update:rollback d2d03804-1c3d-4010-88ab-36e6fd594605, which republishesfd7ca41a.Sixth publish: update group
fd7ca41a-bef1-4d2d-8a7c-25433753643efrom commit29696e0(upgrade event properties, mobile half of fix(ci): show what upgrades bought and where subscription events go missing #290 by way of feat(mobile): say whether an upgrade was a real purchase #291) on channelmainat runtime 1.6.2, iOS update01a076f5-ed18-71d3-a992-31fc2e4df878. Preflight groupe4fba8a7-4dc9-44e8-b8a7-eb990fbb8668, iOS update01a076f2-8bce-76d5-b2e7-db21ceaf2ca9, was verified first on a release simulator build pinned to theota-preflightchannel and built before the preflight group was published. The binary carriedexpo-channel-name: ota-preflightin its update request headers, and the row the device stored for the published update id carries the same header back. That row came out ready with two successful launches and zero failed ones across two cold starts, against one launch for the embedded bundle, so the published bundle is what ran. No crash report was written, and the app rendered its screen past sign in both times. Rollback of this one alone iseas update:rollback fd7ca41a-bef1-4d2d-8a7c-25433753643e, which republishesc240833f.Fifth publish: update group
c240833f-e043-4762-987d-6c16623b188afrom commitac36c13(keychain read retry at launch, fix(mobile): look again when the keychain is not ready at launch #286) on channelmainat runtime 1.6.2, iOS update01a071b6-cbd0-7f8c-b371-73a5c17bcf16. Preflight groupe1ba57f2-d6b5-4be3-946e-5e0a4fd4c3fe, iOS update01a071b2-b2ca-74e9-8d8e-2dd5ab9a1af4, was verified first on a release simulator build pinned to theota-preflightchannel and built before the preflight group was published. The device loggeddidStartLoadingUpdateagainst that update id, pulled all 52 assets with none failed, finisheddidFinishBackgroundUpdateWithStatus=NewUpdateLoaded, and came backcheckCompleteUnavailablewithisUpToDate=1on the next launch. Its updates database recorded two successful launches and zero failed ones against that update id, and none at all against the embedded one, so the published bundle is what ran. The app rendered the sign in screen both times, which is the launch routing path this change touches. Rollback of this one alone iseas update:rollback c240833f-e043-4762-987d-6c16623b188a, which republishesba2798f2.Fourth publish: update group
ba2798f2-6c48-49d3-a1e7-245f0197b5ddfrom commit52c913a(sign in loop and stale upload grant, fix(mobile): keep a signed in user out of the sign in loop #283 and fix(api): let profile photos outlive a slow first form #284) on channelmainat runtime 1.6.2. Preflight group408792c9-e140-4408-9c18-d548b1ee52aawas verified on the same release simulator build first: it downloaded, loggedNewUpdateLoaded, came backisUpToDateon the next launch, recorded two successful launches and zero failed ones, and the app rendered the sign in screen past the splash, which is the launch routing path this change touches. Rollback of this one alone iseas update:rollback ba2798f2-6c48-49d3-a1e7-245f0197b5dd, which republishes19f2351e.Third publish: update group
19f2351e-5cb2-4a06-9027-05cd054c68c5from commit5b74b2e(analytics tagged with the running update, feat(mobile): tag analytics with the running update #285) on channelmainat runtime 1.6.2. Preflight group48cb7ff0-88f3-4abf-9d88-d3cd7624c036was verified on a release simulator build pinned to theota-preflightchannel first: the device downloaded it, loggedNewUpdateLoadedand thenisUpToDateon the next launch, recorded two successful launches and zero failed ones, and PostHog came up registeringota_update_idequal to the update the launcher actually loaded. Rollback of this one alone iseas update:rollback 19f2351e-5cb2-4a06-9027-05cd054c68c5, which republishesbc9927dc.Second publish: update group
bc9927dc-b7ce-4e0a-83d5-ac3f48a40c09from commitb356e90(minimum app version gate per platform, feat: minimum app version per platform #274) at 01:03 UTC on 2026-09-05, preflight groupf86b0b0c-f6e4-4708-bbf1-0655ffcac1baverified on device. Rollback of this one alone iseas update:rollback bc9927dc-b7ce-4e0a-83d5-ac3f48a40c09, which republishes85e75f28.Rollback chain, newest to oldest:
d2d03804tofd7ca41atoc240833ftoba2798f2to19f2351etobc9927dcto85e75f28to0eebf8e3.Lesson recorded: build the preflight binary first, publish the preflight group second, or the binary refuses the older update.
Testing steps
npx oxfmt --checkover the 22 changed TypeScript files. All correctly formatted.apps/mobile/app.config.tsstill says version1.6.2with theappVersionruntime policy, and thatapps/mobile/package.jsonandpnpm-lock.yamlare byte identical to37fa5e2. No dependency moves, so the runtime stays compatible with the binary already on people's phones.expo-sharing,react-native-view-shot,expo-clipboard,associatedDomains,intentFilters,DogShareOptionsandusePendingDogProfile. Zero hits, so nothing here needs a native rebuild.ota-preflightchannel and open a release build of 1.6.2 pointed at that channel. The update downloads, the app relaunches on it, and the screenshots below come from that build.Screenshots
Paywall, yearly preselected with the saving badge:
Full paywall, no free trial copy anywhere in the column:
Portuguese in sentence case, sign in:
Portuguese in sentence case, create profile: