Skip to content

feat(app): show legacy plan labels and descriptions on iOS Settings - #69

Merged
cursor[bot] merged 10 commits into
pricing-update-sept-2026from
cursor/pricing-legacy-label-5707
Sep 5, 2026
Merged

feat(app): show legacy plan labels and descriptions on iOS Settings#69
cursor[bot] merged 10 commits into
pricing-update-sept-2026from
cursor/pricing-legacy-label-5707

Conversation

@formed2forge

@formed2forge formed2forge commented Sep 5, 2026

Copy link
Copy Markdown
Owner

What changed and why

#68 wired (Legacy Plan), the early-supporter note, and current-plan descriptions on macOS/Windows. iOS Settings still dropped legacy/description from 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 from current_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_client remapped only MOBILE_PLAN_TYPES (plus, pro_v2). Unlimited-v2 is keep-until-cancel, not a sold mobile SKU, so GET /v1/users/me/subscription left plan=unlimited_v2 on the wire, UserSubscriptionResponse rejected it (500), and Flutter Settings fell through to “You are currently on the free tier.” The remap now uses WIRE_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 can APPLE_DEVELOPMENT_TEAM=… bash setup.sh ios: dynamic DEVELOPMENT_TEAM on 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.

cd backend && python -m pytest \
  tests/unit/test_subscription_plans.py \
  tests/unit/test_user_subscription_wire_contract.py \
  tests/routers/test_users.py::test_unlimited_v2_subscription_does_not_500_current_ios_into_free
# 32 passed
cd app && bash test.sh test/utils/subscription_plan_presentation_test.dart
# 14 passed
bash app/test/shell/detect_apple_team_id_test.sh
bash app/test/shell/google_reverse_client_id_test.sh

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 team 9536L8KLMP, line count unchanged.

iOS tester path:

PROVIDER_MODE=offline make dev-up
make seed-pricing-scenario SCENARIO=plan_catalog_matrix
cd app && APPLE_DEVELOPMENT_TEAM=<10-char-team> bash setup.sh ios

Physical iPhone: OMI_DEV_HOST=<Mac LAN> before both make dev-up and setup.sh ios.

uid Title Note Description
pricing_plus / pricing_pro_v2 Plus / Pro, no Legacy suffix none non-empty
pricing_unlimited Neo (Legacy Plan) supporter note non-empty Neo entitlements
pricing_architect / pricing_operator / pricing_unlimited_v2 {Title} (Legacy Plan) supporter note non-empty
pricing_basic Free, no Legacy none non-empty

Do not expand OpenAPI PlanType. Do not put unlimited_v2 in MOBILE_PLAN_TYPES. Do not 3-way-merge project.pbxproj as a whole file.

Tests

  • Regression (unlimited_v2 Free): test_unlimited_v2_subscription_does_not_500_current_ios_into_free
  • test_wire_plan_remaps_fallback_tiers_for_clients_without_the_enum
  • Flutter: Unlimited-v2 recovered from remapped plan=unlimited + price_uv2_m is not Free
  • Windows billing remapped-wire title; macOS XCTest not run on this Linux VM
  • Community signing: app/test/shell/detect_apple_team_id_test.sh, google_reverse_client_id_test.sh

Failure class (fixes)

Failure-Class: none

Open in Web Open in Cursor 

cursoragent and others added 10 commits September 5, 2026 15:58
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>
@cursor
cursor Bot merged commit 5c0ea21 into pricing-update-sept-2026 Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants