Skip to content

iOS companion: finish getting it onto TestFlight #203

Description

@arzafran

Where this stands

The iOS companion is not on TestFlight. No build has ever reached a tester
through CI — ios-testflight.yml existed for weeks and had never run once.

That lane now ships by itself on any push to main that touches the phone app
(#201), and pairing was collapsed from two hand-copied strings to one QR scan
(#202). Both are merged. The lane is wired, verified, and currently skipping every
run because the Apple signing material is not on the repo.

Three things left. Only the first blocks shipping.


1. Add the Apple signing secrets (blocking, needs Apple account access)

ios-testflight.yml runs on every ios/** push to main and stops at
preflight with configured=false. It warns and skips rather than failing, so
main stays green and nothing reaches testers. Two consecutive runs have skipped
this way — that is the designed behaviour, not a bug, but it means the companion
is not shipping.

Add under Settings → Secrets and variables → Actions:

secret what it is
APPLE_IOS_DIST_CERT_BASE64 Apple Distribution cert as .p12, base64. A Developer ID or Apple Development cert cannot sign for TestFlight — the build script rejects them explicitly.
APPLE_IOS_DIST_CERT_PASSWORD the .p12 export password
APPLE_IOS_APP_PROFILE_BASE64 App Store profile for com.darkroom.programa.spike
APPLE_IOS_WIDGET_PROFILE_BASE64 App Store profile for com.darkroom.programa.spike.widgets
APPSTORE_CONNECT_KEY_ID App Store Connect → Users and Access → Integrations
APPSTORE_CONNECT_ISSUER_ID same page
APPSTORE_CONNECT_KEY_P8_BASE64 the .p8 key, base64

Encode each with base64 -i <file> | pbcopy.

Regenerate both provisioning profiles — do not reuse the existing ones.
plans/golden-tumbling-gray.md:490-493 records that the iOS profiles were minted
before the iCloud container was attached to the App ID, so they carry no
containers. scripts/build-ios-testflight.sh also hard-fails when the app profile
has aps-environment != production, which is what happened on the first manual
upload. Mint them with Push Notifications enabled on the App ID.

Verify, in order:

  1. gh workflow run ios-testflight.yml -f upload=false → run completes
    successfully with build no longer skipped, and the "Verifying signed
    entitlements" step printing ok aps-environment = production,
    ok get-task-allow = false,
    ok com.apple.developer.icloud-container-environment = Production.
  2. Only once that passes: gh workflow run ios-testflight.yml -f upload=false
    again is unnecessary — push any ios/** change to main and confirm the
    upload step runs and the build appears in App Store Connect after processing.

2. Localize the iOS strings

ios/ProgramaSpike/ has no String Catalog and its convention is bare English
literals, so the strings added in #202 (PairConnectView, QRScannerView,
AppStore) are unlocalized. The Mac side of the same feature is localized in
Resources/Localizable.xcstrings (en + ja).

CLAUDE.md's localization rule is repo-wide, so this is a real gap rather than a
target-local style choice. It needs a String Catalog added to the iOS target and
the existing literals migrated — worth its own change, and worth doing before
Japanese testers get a build.

Verify: every user-facing string in ios/ProgramaSpike/ProgramaSpike/*.swift
reaches the UI through String(localized:defaultValue:), with en + ja entries,
and the app builds:
xcodebuild -project ios/ProgramaSpike/ProgramaSpike.xcodeproj -scheme ProgramaSpike -configuration Release -destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO build
** BUILD SUCCEEDED **

3. Optional: make the pairing QR an https:// universal link

Today the QR encodes programa-pair://pair?v=1&t=<ticket>&k=<token>, a custom
scheme. It works, but a tester has to open Programa on the phone, find "Scan QR
Code", then scan — three steps.

pingdotgg/t3code encodes a real https:// URL instead
(packages/shared/src/remote.ts:165-170), so any camera app opens it directly:
point, tap, done. Their in-app scanner is a convenience layer rather than the only
route in.

Cost: an apple-app-site-association file on a domain we control. A static file
on an existing site is not "a server we host", so it does not break that
constraint, but it is real setup.

If we do this, the token must move into the URL fragment (#token=), not the
query string — that is exactly why t3code puts it there. A query string reaches
the web origin and its access logs; a fragment does not. This does not apply to
the current custom scheme, which never touches a server, which is why it was left
alone in #202.

Defer until the companion is in testers' hands and we know whether the two extra
taps actually matter.


Not in scope, noted so it is not rediscovered

  • The bundle id is still com.darkroom.programa.spike. Renaming means a new App
    ID, a new App Store Connect record, and re-attaching the iCloud container.
    Testers only ever see the display name "Programa", so this is not worth doing.
  • MARKETING_VERSION in ios/ProgramaSpike/project.yml is 0.1.
  • The two pairing-code parsers (Sources/MobileBridge/MobileBridgePairingCode.swift
    and ios/ProgramaSpike/ProgramaSpike/PairingCode.swift) are separate copies kept
    in sync by hand, because the two targets share no module. Both validate v=1 and
    reject anything else. Any format change must land on the phone before the Mac
    starts emitting it, since the companion ships through TestFlight and will lag.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions