feat(app): show legacy plan labels and descriptions on iOS Settings - #69
Merged
cursor[bot] merged 10 commits intoSep 5, 2026
Merged
Conversation
Plan & Usage and the plans sheet now recover Plus/Pro from current_price_id, suffix keep-until-cancel titles with (Legacy Plan), and show each plan's description plus the early-supporter note so iOS testers can compare SKUs. Failure-Class: none Co-authored-by: Tselem <formed2forge@users.noreply.github.com>
Testers were pointed at desktop OMI_AUTH_API_URL. The phone app uses setup.sh ios / OMI_API_BASE_URL and the Auth emulator host. Failure-Class: none Co-authored-by: Tselem <formed2forge@users.noreply.github.com>
GET /v1/users/me/subscription 500'd for keep-until-cancel Unlimited-v2 because wire_plan_for_client only remapped MOBILE_PLAN_TYPES (plus, pro_v2). Unlimited-v2 is not a sold mobile SKU, so current iOS received plan=unlimited_v2, UserSubscriptionResponse rejected it, and Settings fell through to Free. Remap every WIRE_FALLBACK_PLAN_TYPES entry for unaware clients. Failure-Class: none Co-authored-by: Tselem <formed2forge@users.noreply.github.com>
…up, dev entitlements
Community developers cannot build the iOS app: signing credentials are
hardcoded to BasedHardware's Apple team, and the dev-flavor entitlements
request capabilities a personal team cannot provision.
Three blockers, three fixes:
1. DEVELOPMENT_TEAM. setup.sh gains detect_apple_team_id(), which resolves the
team from APPLE_DEVELOPMENT_TEAM, then a provisioning profile matching this
machine's bundle ID, then any profile with a valid signing cert in the
keychain, then an interactive prompt. The resolved value is written to
Custom.xcconfig, and the nine dev-flavor build configurations in
project.pbxproj read $(DEVELOPMENT_TEAM) instead of a literal team.
Prod/beta/raybanDat configurations are deliberately left on the literal team
— community builders cannot sign those anyway.
2. App group. The widget shares state with the app through an app group whose
name must track the (per-machine suffixed) bundle ID. Base.xcconfig carries
the unsuffixed default; setup.sh appends a suffixed APP_GROUP_IDENTIFIER for
dev builds only, so prod/beta keep today's literal group. The Runner and
BatteryWidget entitlements, both Info.plists, SharedDefaults.swift, and
AppDelegate.swift all read it indirectly, each with a fallback to the
original literal so a build without setup.sh still works.
3. Dev entitlements. RunnerDebug/Profile/Release-dev drop aps-environment,
associated-domains, HotspotConfiguration, and wifi-info — the capabilities a
free or personal Apple team cannot provision.
project.pbxproj is edited surgically: exactly nine DEVELOPMENT_TEAM lines
change and nothing else. Patching or 3-way merging this file does not work —
Xcode regenerates object IDs, so a merge silently adopts one whole side and
reverts unrelated upstream additions. An earlier attempt at this change did
exactly that, dropping ~670 lines of upstream Swift sources.
Verification (run locally on macOS 27, Aug 11 2026):
- detect_apple_team_id: APPLE_DEVELOPMENT_TEAM override returns the given team;
with no override and no discoverable profiles it fails fast rather than
hanging (see the following commit's regression test).
- End-to-end generate_ios_custom_config with a stub GoogleService-Info.plist:
dev -> APP_BUNDLE_IDENTIFIER=...ios12-mycomputer
APP_GROUP_IDENTIFIER=group....ios12-mycomputer
DEVELOPMENT_TEAM=98SC8JJDRG
beta -> APP_BUNDLE_IDENTIFIER=...ios12.beta, no APP_GROUP_IDENTIFIER line,
so it inherits the unsuffixed group from Base.xcconfig
- plutil -lint passes on project.pbxproj and all six touched plists/entitlements.
- bash -n passes on setup.sh.
- pbxproj audit: 9 dev-flavor configs now dynamic, 24 non-dev still literal,
9 + 24 == 33 == the pre-change count, so no config was added or lost.
Not exercised: a full `flutter build ios`. This machine has no provisioning
profiles and no Apple team configured, so signing cannot be attempted here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
detect_apple_team_id() ends in an interactive prompt. In any non-interactive context — CI, nested automation, a script whose stdin is an open-but-idle pipe — `read` never sees EOF, so setup.sh blocked forever at the prompt instead of failing with a usable message. Reproduced on this machine, which has zero provisioning profiles and therefore always reaches that branch: the call sat past a 120s deadline with no output. Both `read` sites now require a TTY. Without one, the function prints what to set (APPLE_DEVELOPMENT_TEAM) and returns non-zero, so setup.sh fails fast under its own `set -e`. Adds app/test/shell/ as the home for hermetic shell tests of setup.sh helpers, discovered by app/test.sh (which mobile-app-checks.yml already runs, so these execute in PR CI). The test drives the real function through two seams — $HOME, which is where the profile scan looks, and stdin — rather than asserting on source text, so it is behavioral coverage and not a static tripwire. It carries its own deadline instead of depending on GNU timeout(1) being installed. Verification: - With the guard removed, the test reports "hung waiting for input with no TTY" and exits 1. With the guard, both cases pass. Re-ran after restoring to confirm setup.sh was left byte-identical. - bash -n passes on the test, setup.sh, and test.sh. - The discovery loop in test.sh finds and runs the test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`detect_apple_team_id`'s fallback decided which teams to offer by collecting every
team named anywhere in `security find-identity -v -p codesigning` output. That is
wrong in both directions.
It accepted teams that cannot build: the list includes "Developer ID Application"
certificates, which sign Mac distribution builds and cannot sign an iOS
development build. And it matched as an unanchored substring, so a team ID
composed only of hex characters could match inside the 40-char SHA-1 that begins
every identity line.
More importantly, a certificate's common name is not authoritative about its
team. Apple keeps the original personal-team identifier in the common name when a
developer joins a paid team, so a certificate reading
"Apple Development: NAME (PERSONALTEAM)" can be issued under a different team
entirely — the real one is the OU, readable only by decoding the certificate.
Observed on the machine this was developed on: an identity whose common name says
(LW4P2T66Q4) but whose OU is 98SC8JJDRG, and which signs successfully for
98SC8JJDRG. A name-based check rejects exactly the team such a developer can use.
The fallback now asks the question Xcode asks when it picks a profile: does the
profile embed a certificate whose private key is on this machine? Fingerprints of
held iOS development identities are compared against the SHA-1 of each embedded
certificate, extracted with `plutil -extract DeveloperCertificates.<n> raw` and
`openssl x509`. The profile's own TeamIdentifier is authoritative for the team, so
that is what gets offered, and no inference is made from certificate names. The
identity list is still filtered to "Apple Development" and the legacy
"iPhone Developer" spelling, so a machine holding only a Mac Developer ID
certificate offers nothing.
Verified against the real profiles on this machine: the embedded certificates of
the installed profile have fingerprints 18309B14…, DBF7EBAE… and 8E1514B1…, of
which DBF7EBAE… and 8E1514B1… are held, so team 98SC8JJDRG is correctly offered.
Detection returns 98SC8JJDRG both through step 2's bundle match and — with the
bundle pattern forced not to match, so step 3 is reached — through this fallback.
Tests (app/test/shell/detect_apple_team_id_test.sh) generate real certificates
with openssl so a profile's embedded certificate and the stubbed identity list
agree on a genuine fingerprint:
- the profile's team is offered when we hold its embedded certificate, even though
that certificate's common name names a different team
- a team whose embedded certificate we do not hold is not offered
- a machine holding only a Developer ID Application certificate offers nothing
Verification (macOS 27.0, 2026-08-13): `bash -n` clean; all 5 cases in the file
pass. Against the previous name-matching implementation, case 1 fails
("expected PAIDTEAM01 …, got ''") — the regression this fixes. Cases 2 and 3 pass
either way; they guard this implementation against being too permissive rather
than covering the old defect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
Removing the hardcoded GOOGLE_REVERSE_CLIENT_ID from devDebug.xcconfig left
community builds with no value at all. Runner/Info.plist emits it as a
CFBundleURLSchemes entry, so the build shipped an empty URL scheme and the Google
Sign-In redirect had nowhere to land.
The chain failed in two places at once:
setup/prebuilt/GoogleService-Info-Local.plist no REVERSED_CLIENT_ID key
generate_ios_custom_config.sh extracted empty, wrote
"GOOGLE_REVERSE_CLIENT_ID="
Base.xcconfig no default to fall back to
devDebug.xcconfig includes Custom.xcconfig LAST,
so the empty value won
Runner/Info.plist:88 <string>$(GOOGLE_REVERSE_CLIENT_ID)</string>
Both halves are fixed, because either alone is insufficient: Base.xcconfig now
carries the default the June 26 review asked for, and the generator no longer
writes the key when the plist has no value — an empty assignment in
Custom.xcconfig overrides the default rather than deferring to it, since that
file is included afterwards. A plist that does carry REVERSED_CLIENT_ID still
overrides, so nobody silently builds against the checked-in fallback.
How this was missed twice: the PR review on Jun 26 asked to "verify
GOOGLE_REVERSE_CLIENT_ID has an equivalent fallback in Base.xcconfig", and both a
maintainer review comment (Aug 12) and my own earlier answer concluded it was
covered because the generator still writes the key. It does — it just writes
nothing useful for the community Firebase config. Running setup.sh's real config
path surfaced it immediately: the generated Custom.xcconfig read
"GOOGLE_REVERSE_CLIENT_ID=" with nothing after the equals sign. Asserting the
mechanism is not the same as asserting the value.
Tests (app/test/shell/google_reverse_client_id_test.sh) resolve the key the way
Xcode does — later includes win — and assert the resolved value, not the
mechanism:
- the community config resolves to a non-empty value
- the generator writes no empty assignment for a plist without the key
- a plist carrying the key still overrides the default
- Base.xcconfig carries a default at all
Verification (macOS 27.0, 2026-08-13): bash -n clean on the generator; all 4
cases pass. Reverting either half fails 2 cases — removing the Base default
fails "resolved to EMPTY" and "no default to fall back to"; restoring the
unconditional write fails "resolved to EMPTY" and "wrote an empty assignment".
Also confirmed by hand that a plist carrying REVERSED_CLIENT_ID produces that
value in Custom.xcconfig.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
detect_apple_team_id's real certificate-matching path shells out to `plutil -extract ... raw`, which is macOS-only. CI runs shell tests on an Ubuntu runner, so every candidate certificate silently failed to decode there and the function always reported "no match" — the two sub-tests expecting rejection passed for the wrong reason (a broken matcher rejects everything), while the one sub-test expecting a real match failed, which is what surfaced this in CI. Skip the whole candidate-filtering section when plutil is unavailable instead of asserting on a matcher that can't run. Verified both paths: unchanged pass with plutil on PATH, clean skip (exit 0) with a PATH that omits it. Failure-Class: none
Community-build signing from BasedHardware#7641 is now on this branch. Testers on a non-Omi Apple team pass APPLE_DEVELOPMENT_TEAM to setup.sh. Co-authored-by: Tselem <formed2forge@users.noreply.github.com>
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.
What changed and why
#68 wired
(Legacy Plan), the early-supporter note, and current-plan descriptions on macOS/Windows. iOS Settings still droppedlegacy/descriptionfrom the generated wire, labeled almost every paid plan as Unlimited, and showed a generic “You are on a paid plan.” Plan & Usage and the plans sheet now match desktop: recover Plus/Pro fromcurrent_price_id, suffix keep-until-cancel titles, and show each plan’s description.Follow-up: Unlimited-v2 (
pricing_unlimited_v2) was showing as Free on current iOS.wire_plan_for_clientremapped onlyMOBILE_PLAN_TYPES(plus,pro_v2). Unlimited-v2 is keep-until-cancel, not a sold mobile SKU, so GET/v1/users/me/subscriptionleftplan=unlimited_v2on the wire,UserSubscriptionResponserejected it (500), and Flutter Settings fell through to “You are currently on the free tier.” The remap now usesWIRE_FALLBACK_PLAN_TYPES.Follow-up: folded BasedHardware#7641 community-build iOS signing onto this branch (
origin/fix/ios-community-build@1240a53ab4, six commits cherry-picked). Personal/community Apple teams canAPPLE_DEVELOPMENT_TEAM=… bash setup.sh ios: dynamicDEVELOPMENT_TEAMon the nine dev-flavor pbxproj configs, suffixed app group, stripped personal-team-incompatible entitlements, Google reverse-client-id fallback. Did not fold LAN-auth (BasedHardware#12453) or the local-only team-chooser commits (personal bundle IDs scoped to team) — those SHAs are not on origin.Product invariants affected
none
How it was verified
Linux Cloud VM — no Xcode / iOS simulator. Backend + Flutter + Windows unit tests for the subscription remap; hermetic
app/test/shell/tests for community signing. Human iOS GUI / device signing pass is still required.detect_apple_team_id: override + fail-fast without TTY passed; candidate-filter skipped (no plutil). GOOGLE_REVERSE_CLIENT_ID: 4 passed. pbxproj audit: 9 dynamic
$(DEVELOPMENT_TEAM), 24 still literal Omi team9536L8KLMP, line count unchanged.iOS tester path:
Physical iPhone:
OMI_DEV_HOST=<Mac LAN>before bothmake dev-upandsetup.sh ios.pricing_plus/pricing_pro_v2pricing_unlimitedNeo (Legacy Plan)pricing_architect/pricing_operator/pricing_unlimited_v2{Title} (Legacy Plan)pricing_basicDo not expand OpenAPI
PlanType. Do not putunlimited_v2inMOBILE_PLAN_TYPES. Do not 3-way-mergeproject.pbxprojas a whole file.Tests
test_unlimited_v2_subscription_does_not_500_current_ios_into_freetest_wire_plan_remaps_fallback_tiers_for_clients_without_the_enumplan=unlimited+price_uv2_mis not Freeapp/test/shell/detect_apple_team_id_test.sh,google_reverse_client_id_test.shFailure class (fixes)
Failure-Class: none