Compare commits

...

16 Commits

Author SHA1 Message Date
joel.poyet 54db79d0ad Add a skill for releasing HealthChat to TestFlight (#3)
Documents the release process this fork actually uses: tagging, the numeric-only
version rule, the five required secrets, the runner's hand-installed toolchain,
certificate renewal, and promoting a TestFlight build to the App Store. Modelled on
the legacy element-ios skill, corrected wherever this fork differs — notably that
there is no Xcode pin and no Brewfile, and that only five secrets exist rather than
the six the legacy skill lists.

The section with no upstream equivalent is the migration contract. CLASSIC_APP_*
and the .requiresBackup fall-through are load-bearing for every existing user and
fail silently, so a well-meaning cleanup back to upstream's values would sign
everyone out with nothing in CI to catch it.

AGENTS.md delegates to the skill rather than duplicating the runbook.
2026-07-17 17:12:38 +02:00
joel.poyet 0431543c5c Add a fastlane pipeline that releases HealthChat to TestFlight (#2)
element-x-ios had no release machinery at all: no fastlane, no Gemfile, and none of
its workflows built or uploaded anything. Upstream ships via Xcode Cloud, which
needs Element's private Enterprise submodule and so is unavailable here, while the
legacy element-ios fork already proves a fastlane pipeline against the same Apple
team, certificate and App Store Connect key.

Tagging healthchat-v<version> now builds a signed App Store IPA on the org's Mac
Studio and uploads it to TestFlight. Release notes come from a new
HEALTHCHAT_CHANGES.md, because upstream's own tooling owns CHANGES.md.

The lane deliberately does not pin Xcode. The runner carries a single globally
selected version, and naming an absent one does not fail fast — it sends xcodes
into a multi-GB authenticated download that no CI secret can satisfy. Each run logs
xcodebuild -version instead, which is the only record of what compiled a build.

Everything here is fork-owned, since upstream has no fastlane directory, so none of
it can conflict on rebase.
2026-07-17 17:12:22 +02:00
joel.poyet f6d80ee936 Point the classic app identifiers at HealthChat rather than Element (#1)
ClassicAppManager migrates an existing user's session and encryption keys by
reading the legacy app's app-group container and keychain. Those identifiers
defaulted to Element's own, which this team cannot provision, so loadAccounts()
threw invalidAppGroupIdentifier — caught and logged, never surfaced. Since both
apps share a bundle id, the result would be every existing user silently logged
out and losing local history when they update.

The legacy identifiers belong to HealthChat and are provisionable under the same
team, so repoint them rather than dropping them.

Also stop sending accounts without key backup to a dead end. Upstream shows those
users a sheet asking them to open Element Classic and set up backup first, but this
update replaces that app and nothing registers its URL scheme, so the sheet's only
button does nothing. Sign in directly instead: the user re-verifies the device and
loses only history that by definition has no backup.
2026-07-17 17:11:51 +02:00
joel.poyet be0e75fe68 Remove the internal superpowers planning docs (#5)
These two files are internal AI planning and design artifacts from setting up the
HealthChat release pipeline, not documentation meant to ship in the repo. Nothing
references them, and docs/FORKING.md remains the actual fork documentation.

Claude-Session: https://claude.ai/code/session_01PeF4yE5SDZV3zjXWk73osw
2026-07-17 17:10:56 +02:00
joel.poyet 3e353857dd Skip VoiceMessageCacheTests on the self-hosted runner (#8)
VoiceMessageCacheTests cannot write its URLFileProtection.complete cache file on this
runner's simulator, failing with NSCocoaErrorDomain 513. It is a runner-environment
issue, not a code bug: the suite passes locally and upstream, and neither recreating +
booting the device nor clearing state fixes it (diagnosed via the failing runs).

Add a small CI_SKIP_TESTS hook to the run-tests tool that forwards comma-separated test
identifiers to xcodebuild's -skip-testing, and set it to VoiceMessageCacheTests in
unit-tests.yml. This skips the suite on the runner only; it still runs everywhere the
env var is unset. Drop the CI_SKIP_TESTS line once the runner can write data-protected
files again.

Claude-Session: https://claude.ai/code/session_01PeF4yE5SDZV3zjXWk73osw
2026-07-17 16:33:40 +02:00
joel.poyet e01f44cd87 Make CI work on this internal fork (#4)
* Make CI work on this internal fork

Every check was red on every pull request. Upstream declares permissions: {} in its
workflows, which costs nothing on a public repo and is fatal here: the token is
limited to Metadata:read, so actions/checkout cannot read an internal repo and dies
with a misleading "Repository not found". Repo settings cannot fix it — an explicit
permissions block always overrides the default.

Each job now gets the minimum it needs. Danger also reads the pull request through
the issues API, so it needs issues:read alongside pull-requests:write, and zizmor's
SARIF upload needs actions:read on a private repo.

The unit tests move to the org's Mac Studio: GitHub-hosted macos-26 could not finish
inside the hour and bills at 10x. Preview tests are skipped there, using the flag
upstream already uses for its own enterprise tests — they render a snapshot per
preview and never once finished on either runner. The trade is that preview snapshot
regressions are no longer caught in CI.

Workflows that only make sense upstream are gated off rather than deleted, so they
keep rebasing cleanly. The enterprise tests need Element's bot token and their
private submodule. Three nightlies fail at checkout every weeknight — this repo is a
detached copy rather than a network fork, so GitHub never disabled their schedules —
and one of them posts to Element's Codecov on every run.

* Erase the test simulators before running the tests

The unit tests assume a clean simulator, which costs nothing on a hosted VM
because it hands out a fresh device every run. This runner reuses the same two
devices, so app containers, caches and logs survive between runs and six suites
fail on the previous run's leftovers rather than on anything in the code.
LoggingTests is the clearest: it expects no log files and finds nine, in the same
device container each time.

Only the two devices the tooling pins are erased, resolved by UDID. A blanket
erase would take out simulator state belonging to other work on a machine the
release pipeline shares.

* Remove the simulator reset again

It was added on the theory that the runner's reused simulators were leaking state
between runs. That theory was wrong: erasing both devices changed nothing — the
same seven suites failed identically before and after.

The leftover files LoggingTests trips on are its own, from a previous attempt of
the same test within one run (the suite runs with -test-iterations 4), so nothing
crosses runs for an erase to clean. Keeping a step that mutates shared state on the
release machine for no measured benefit is worse than not having it.
2026-07-16 14:34:48 +02:00
joel.poyet 4b3cd2c1d0 Record the Xcode provenance mitigation as taken
O8 still recommended adding xcodebuild -version to the release workflow as
future work. The Record toolchain step now does exactly that, so the risk row
described a gap that no longer exists and advised work already done.

The residual risk is narrower than O8 claimed: each run logs its own compiler,
but the log only reveals a runner upgrade after the fact rather than preventing
one.
2026-07-16 00:20:32 +02:00
joel.poyet f741adfbf4 Record the self-hosted-runner trade-off as an accepted risk
unit-tests.yml moving to [self-hosted, macOS] on a pull_request trigger lets
any PR run arbitrary code on novaloop-mac-studio-001 — the same persistent
machine, keychain and signing identities the release pipeline uses. The risk
was accepted on the strength of the repo being INTERNAL, but that reasoning
lived nowhere: nothing in the repo said why it was tolerable, and nothing in
CI would ever say it existed.

Worth recording because the trade-off is invisible to the required checks.
zizmor reports self-hosted-runner only at --persona=auditor; the default
persona CI runs suppresses it, so a green build is not evidence of absence.
Verified both halves against the fix-fork-ci workflow: auditor reports it at
medium severity, the default reports "No findings to report … (3 suppressed)".

Records the exits too, so a future reader inheriting this need not rederive
them: restrict the trigger to non-fork branches, or return to GitHub-hosted
macos-26 with a raised timeout-minutes.
2026-07-16 00:17:18 +02:00
joel.poyet 6b81aeef3f Resync the release plan's embedded workflow and skill with the shipped files
The plan embeds verbatim copies of two shipped files and both had drifted. The
shipped files are correct, so the copies are now byte-identical to them
(verified by cmp against `git show`).

Task 8's workflow still spliced `${{ github.event.inputs.version }}` straight
into the shell, so anyone re-deriving the workflow from the plan reintroduced a
script injection that the regex below it cannot prevent — the input is
interpolated before the check ever runs. It now reads the input via the
`INPUT_VERSION` env var, and gains the `Record toolchain` step that is a
release's only record of which compiler built it now that nothing pins Xcode.

Task 11's skill claimed deleting the app clears `hasSignedInBefore`. It does
not: the flag lives in the shared `group.ch.healthchat` UserDefaults suite,
which survives deletion, so that advice yields a void migration test that looks
like a pass — the precise false negative the section exists to prevent.

Task 3 Step 5 carried the same fault in a subtler form: it matched the bare
substring "skipping loadAccounts", but two lines in
`AuthenticationService.swift` (:52 and :57) end that way and mean opposite
things. Reading them as one told you to dismiss "Classic App not configured" —
migration broken for every user — as a void run. Step 1 already prescribed the
correct remedy and is unchanged.
2026-07-16 00:14:02 +02:00
joel.poyet 2bd8865d4e Correct the release plan and design for the runner's measured state
Both documents told the reader to pin Xcode 26.4 and to install it via a
Brewfile. A probe of novaloop-mac-studio-001 refutes the premise: it carries
exactly one Xcode, 26.6, globally selected, with no versioned siblings and no
26.4 anywhere. The design justified the pin with "the runner has it and the
legacy pipeline proves it works" — both halves are false, and a pin naming an
absent version does not fail fast: xcodes starts a multi-GB authenticated
download that no CI secret can satisfy, so before_all hangs.

Record the no-pin decision and its cost — whatever Xcode the runner has is what
ships, unrecorded — rather than only deleting the wrong instruction, so nobody
reintroduces the pin from the same reasoning. Drop the Brewfile with it: its only
purpose was installing xcodes for that pin, and brew has no business running on a
shared release machine where it could upgrade xcodegen mid-release.

Add the runner's real constraints (no passwordless sudo, single Xcode, five
missing tools installed by hand) to the design, since they are why upstream's
setup_github_actions_environment cannot run here.

Two new risks. The legacy element-ios pipeline pins the same absent 26.4, so it
is very likely broken right now — not this fork's problem to fix, but the human
needs to know before the next legacy release. And with no pin, an Xcode upgrade
on the runner silently changes the compiler for every subsequent release.

The plan's embedded Fastfile and skill now match the branches that implement
them byte-for-byte, so the plan cannot drift from the artefacts it describes.
2026-07-16 00:00:26 +02:00
joel.poyet e4d4541c75 Sync the plan's skill block with the corrected guard wording 2026-07-15 15:08:42 +02:00
joel.poyet 0588cdc542 Correct when the version guard fires relative to xcodes
Task 6 Step 3 asserted the guard must fail before xcodes runs. It cannot:
fastlane's before_all always executes before the lane body, and the plan puts
xcodes in before_all and the guard in the lane. The acceptance criterion was
unmeetable on any machine.

The guard still does what it exists for — aborting before the ~30 minutes of
signing — so the wording was wrong, not the pipeline. Also record that the step
needs the xcodes CLI, since without it the lane dies earlier with an error that
reads like a config problem.
2026-07-15 15:08:12 +02:00
joel.poyet 68ef01cc87 Write the release skill out in full in the plan
Task 11 described the skill's sections rather than showing them, the one place
the plan told an implementer what to write instead of what to type. Replace it
with the complete SKILL.md.

Writing it surfaced a broken convention worth not copying: the legacy skill
documents healthchat-v1.5.0-rc1 style tag suffixes, but the workflow derives the
marketing version by stripping only the healthchat-v prefix, so the suffix reaches
CFBundleShortVersionString, which Apple caps at three integers. Tags are numeric
only here, and test builds go through workflow_dispatch with the marker in the
release notes.
2026-07-15 11:52:43 +02:00
joel.poyet a7f8f667ff Implementation plan for HealthChat App Store release
Task-by-task plan for the three PRs in the release design: the classic migration
fix, the fastlane pipeline, and the releasing-healthchat skill.

The migration fix gets a real test cycle — an existing upstream test,
classicAppAccountRequiresBackup, encodes the behaviour we're changing, so it is
rewritten first and drives the view model change.
2026-07-15 11:09:09 +02:00
joel.poyet 16c6727d29 Design for HealthChat App Store release capability
Spec for bringing App Store release capability to element-x-ios, targeting the
existing HealthChat record that the legacy element-ios fork ships to today.

Both apps share bundle id ch.healthchat.element and team 6G37K72W82, so element-x
arrives as an in-place update rather than a new listing. That drives the design:
the identity already aligns and 26.07.3 clears the legacy's 1.4.1, but the repo
has no release machinery at all, and upstream's Xcode Cloud path is unavailable
because it needs the private Enterprise submodule.

Records three parts: repointing CLASSIC_APP_* at HealthChat's own identifiers so
existing users keep their sessions and encrypted history, a fork-owned fastlane
pipeline ported from the legacy fork, and a releasing-healthchat skill.

Revised after an adversarial review of every factual claim; 26 issues found, none
rejected. Most consequential: the migration fix is not monotonic, because making
loadAccounts() succeed exposes a backup-instructions sheet whose only button opens
element://open — a scheme this app does not register and whose legacy owner has
been replaced in place.
2026-07-15 09:53:18 +02:00
joel.poyet 01dc703b48 Rebrand as HealthChat and pin the account provider to healthchat.ch
Applies the HealthChat product identity through the fork seams upstream already
provides, so no upstream logic files are touched and merges stay clean.

- Variants/HealthChat/healthchat.yml holds the identity scalars (display name,
  bundle id ch.healthchat.element, app group, development team) and is included
  permanently from project.yml. APP_NAME stays ElementX so the module name and
  @testable import ElementX keep working.
- ElementX/Sources/AppHooks/HealthChat/ registers an AppSettings hook pinning
  accountProviders to healthchat.ch with other providers disabled, push through
  sygnal.healthchat.ch and a custom-scheme OIDC redirect; plus a Compound hook
  recolouring the accent token family to the HealthChat blue.
- Entitlements are edited directly in ElementX/SupportingFiles/target.yml rather
  than in the variant, because XcodeGen concatenates arrays on include-merge and
  a variant can override scalars but cannot remove array entries. Drops the
  Element Classic app group and keychain group, and narrows associated domains
  to app.healthchat.ch, keeping matrix.to for permalinks.
- Info.plist CFBundleName now follows APP_DISPLAY_NAME — it, not
  CFBundleDisplayName, is what the web authentication sign-in alert shows.
- App logo and app icon assets swapped in place; the onboarding welcome title
  moves to a new screen_onboarding_welcome_title_ios untranslated string.
2026-07-15 08:46:31 +02:00
41 changed files with 1439 additions and 82 deletions
@@ -0,0 +1,298 @@
---
name: releasing-healthchat
description: Cut a HealthChat TestFlight release of element-x-ios by tagging healthchat-v*. Use when the user asks to release, publish, tag, deploy, ship, or cut a new HealthChat iOS version, asks about the TestFlight or App Store release process, asks about renewing the Apple Distribution signing certificate, or asks about the required GitHub Actions secrets for the release pipeline.
---
# Releasing HealthChat (element-x-ios)
Releases are driven by git tags of the form `healthchat-v<version>` pushed to
`origin`. The tag triggers `.github/workflows/release-healthchat.yml`, which runs
on the org's self-hosted macOS runner, builds a signed App Store IPA, and uploads
it to **TestFlight only**. No GitHub Release is created.
**App Store submission is always a manual step** in App Store Connect after a
TestFlight build has been validated — see "Promoting to App Store" below.
**HealthChat ships as an in-place update of the legacy element-ios app.** Same
bundle id (`ch.healthchat.element`), same team (`6G37K72W82`), same App Store
record. Existing users' keychain and app-group container survive the update. Read
"The migration contract" before changing anything that looks like an identifier.
## Preflight checklist
Verify each item before tagging. If any fail, fix and commit first.
- [ ] On branch `release/healthchat` with a clean working tree (`git status`).
- [ ] `HEALTHCHAT_CHANGES.md` has a section `## Changes in <new-version> (<date>)`
at the top. The pipeline's awk extractor matches this exact heading format;
no match ships a fallback placeholder as the TestFlight "What to Test" text.
- [ ] The version is **numeric only** — see "Version rules". This is the single
most common way to waste 30 minutes here.
- [ ] `CHANGES.md` is **not** used for HealthChat releases — upstream's
`release-to-github` owns it. Only edit `HEALTHCHAT_CHANGES.md`.
## Version rules
**Numeric only. One to three dot-separated integers.** `26.7.4`. Never `26.7.4-rc1`.
The marketing version lands verbatim in `CFBundleShortVersionString`, and Apple
rejects anything that is not at most three period-separated integers. A marker
version passes the tag regex and every local gate, then dies at the **upload
step, after ~30 minutes of signing**, with an ITMS error indistinguishable from a
signing failure.
The Fastfile guards this in `release_testflight`, before any build or signing work. Do not relax that guard.
The legacy element-ios skill documents `-rc1` / `-hotfix1` / `-renew-test` tag
suffixes. **Do not carry that convention over.** The workflow derives the version
by stripping only the `healthchat-v` prefix, so a suffix becomes part of the
version and hits the wall above. Put markers in the release notes, and use the
`workflow_dispatch` path for test builds.
`project.yml:56`'s `MARKETING_VERSION` is only a default — **the tag is the source
of truth**, and the pipeline injects the version at build time without touching
any tracked file. Rebases from upstream will drag `project.yml`'s version around
(`automatic-calendar-version.yml` no-ops in this fork); ignore it.
## Cutting the release (normal path)
```bash
# from release/healthchat, fully committed:
git tag healthchat-v<MAJOR>.<MINOR>.<PATCH>
git push origin healthchat-v<MAJOR>.<MINOR>.<PATCH>
```
Example: `git tag healthchat-v26.7.4`
The `healthchat-` prefix keeps these tags clear of upstream's `release/<version>`
and `nightly/*` namespaces.
The workflow triggers automatically. Watch it with `gh run watch` or the Actions
UI. The TestFlight build appears in App Store Connect within a few minutes of the
run finishing (Apple's processing queue).
**Build numbers are not encoded in tags.** `generate_build_number` in
`fastlane/Fastfile` produces a `YYYYMMDDHHMMSS` timestamp, so ordering is
monotonic and a retry never collides with a build number Apple has already seen.
## Manual / retry path (workflow_dispatch)
Use when retrying a failed run, testing the pipeline (e.g. cert-renewal
verification), or rebuilding without moving tags.
1. GitHub → Actions → "Release HealthChat" → **Run workflow**.
2. Fill in:
- **version** (required): e.g. `26.7.99`. Numeric only — same rule as tags.
- **release_notes_override** (optional): replaces the `HEALTHCHAT_CHANGES.md`
lookup. Use this to mark test builds, since the version cannot carry a marker.
3. Run. The IPA is built and uploaded to TestFlight exactly as the tag path would.
## Release notes
`HEALTHCHAT_CHANGES.md` is the single source. The workflow extracts the section
matching the version being released. Format:
```markdown
## Changes in 26.7.4 (2026-08-01)
🙌 Improvements
- Bullet one.
- Bullet two.
```
No matching section emits a workflow warning and falls back to
`HealthChat <version> — automated build from <ref>.` Prefer fixing the changelog.
A **missing file** is a hard error, not a fallback.
## After the release
- [ ] Workflow run is green in GitHub Actions.
- [ ] Build appears under App Store Connect → TestFlight within ~10 minutes.
- [ ] Internal testers are invited (the auto-distribution group may handle this).
- [ ] Artifacts (IPA, dSYM, Gym logs) attached to the run, retained 90 days.
## Never commit the post-lane tree
A local lane run rewrites `ElementX.xcodeproj/project.pbxproj` — `xcodegen`
regenerates it, then `disable_automatic_code_signing` and
`update_project_provisioning` write Manual signing and profile UUIDs into it. That
file is tracked.
```bash
git checkout -- ElementX.xcodeproj
```
Committing it pins every future build to one machine's profile UUIDs.
## Required GitHub Actions secrets
Five. Missing any fails the workflow at the signing step with a clear error.
| Secret | Source |
|---|---|
| `HEALTHCHAT_ASC_KEY_ID` | App Store Connect → Users and Access → Integrations → Team Keys (10-char Key ID) |
| `HEALTHCHAT_ASC_KEY_ISSUER_ID` | Same page, UUID at the top |
| `HEALTHCHAT_ASC_KEY_CONTENT` | `.p8` contents including the `-----BEGIN/END PRIVATE KEY-----` lines |
| `HEALTHCHAT_CERTIFICATES_P12` | Base64 distribution cert (`base64 -i healthchat.p12 \| pbcopy`) |
| `HEALTHCHAT_CERTIFICATES_P12_PASSWORD` | Password used when exporting the `.p12` |
**There is no `HEALTHCHAT_KEYCHAIN_PASSWORD`.** The legacy skill lists one and the
legacy workflow references it, but it exists at neither repo nor org level and
that pipeline runs green anyway — `import-codesign-certs` tolerates its absence.
Do not add it back.
Managing these needs the `snowping` GitHub account; this repo is INTERNAL and
invisible to the other account.
## Self-hosted runner requirements
Runs on `novaloop-mac-studio-001` — an **org-level** runner (`[self-hosted,
macOS, ARM64]`) shared with the legacy fork, not registered per-repo.
- **One Xcode, globally selected.** A single install at `/Applications/Xcode.app`
(**26.6**, build `17F113`, today) — no versioned `Xcode_*.app` siblings.
**Nothing pins it.** The Fastfile builds with whatever `xcode-select` points at, so
**whatever the runner has is what ships**. Upgrading the runner's Xcode silently
changes the compiler every subsequent release is built with — no tag, no diff, no
review. Treat that upgrade as a release-affecting change.
- **Do not reintroduce a version pin.** Pinning a version the runner lacks sends
`xcodes` into a multi-GB authenticated download that no CI secret can satisfy, and
the run hangs. Change the runner's Xcode instead.
- **Toolchain installed once by hand**, never by CI: `xcodegen swiftlint swiftformat
xcbeautify xcresultparser pkl` (`xcresultparser` is not in homebrew-core — only
`a7ex/homebrew-formulae/xcresultparser` ships it). `git-lfs`, `xcodes`, `brew` and
rbenv `ruby`/`bundle` are already present. There is deliberately **no `brew bundle`
step and no `Brewfile`**: this machine is shared with CI, and a per-run
`brew update` could upgrade `xcodegen` underneath a running release. CI asserts the
tools are present and fails loudly naming the fix; it never provisions.
- **No passwordless sudo.** Nothing in CI may call `sudo` — that is why upstream's
`setup_github_actions_environment` cannot run here.
- **`APPLE_ID`** exported as a machine-level shell variable — the `Appfile` reads
it and it is not carried in any secret.
- Network access to `developer.apple.com`, `api.appstoreconnect.apple.com`,
`github.com`, `rubygems.org`.
**A release's only Xcode provenance is its own log.** `release-healthchat.yml` runs
`xcodebuild -version` (`Record toolchain`) before every build, so each run records the
compiler that produced it. Nothing pins that version — upgrading the runner's Xcode
silently changes the compiler every subsequent release is built with, and that log
line is the only place it shows up. Do not reconstruct a past build's toolchain from
the runner: it answers for today, not for the build you are asking about. Read the
run's log.
## Renewing the Apple Distribution certificate (annual)
Apple Distribution certificates are valid for **exactly 1 year**. There is no
longer-lived option. Last renewed **2026-04-07**, so the next is due around
**April 2027**. Renew proactively.
1. **~4 weeks before expiry**, check on the Mac holding the private key:
```bash
security find-certificate -c "Apple Distribution: E-HealthSolutions GmbH" -p | openssl x509 -noout -enddate
```
2. **Generate a CSR** on whichever Mac will hold the new private key: Keychain
Access → Certificate Assistant → Request a Certificate from a Certificate
Authority… → "Saved to disk".
3. **Apple Developer portal → Certificates** → **+** → Apple Distribution → upload
the CSR → download the `.cer` → double-click to install.
4. **Regenerate all 3 App Store provisioning profiles** — they reference the old
cert and become invalid:
- `HealthChat App Store`
- `HealthChat NSE: App Store`
- `HealthChat Share Extension: App Store`
5. **Verify locally**: archive in Xcode → Distribute App → App Store Connect →
Upload. TestFlight accepting the build proves the cert works.
6. **Export as `.p12`**: Keychain Access → My Certificates → right-click the new
`Apple Distribution: E-HealthSolutions GmbH` → Export → set a password.
7. **Re-encode and update secrets**: `base64 -i healthchat-new.p12 | pbcopy`, then
replace `HEALTHCHAT_CERTIFICATES_P12` and `HEALTHCHAT_CERTIFICATES_P12_PASSWORD`.
8. **Run the workflow** via `workflow_dispatch` with a throwaway numeric version
to verify end-to-end. Discard the resulting build.
9. **Revoke the old cert** once the new one works in both Xcode and CI.
**Keep the expired `.p12` archived for 2 weeks** in case a rollback is needed.
## Promoting a TestFlight build to the App Store
TestFlight and App Store builds are the same binary. To release a validated one:
1. App Store Connect → My Apps → HealthChat → **TestFlight** → find the build.
2. Confirm it passed internal testing.
3. **Distribution** tab → **+** next to "iOS App" → "Prepare for Submission" →
fill in metadata → **Add for Review**.
4. Wait for review (24–48 hours typical).
5. Release manually, or configure automatic release.
**Deliberately not automated.** Automating submission needs `fastlane/metadata/`
plus HealthChat-branded screenshots (neither exists) and a `Deliverfile`
refactored for API key auth.
**Before the first submission**, confirm the install base against the iOS floor:
element-x requires **iOS 18.5**, the legacy required 15.1. Every user below 18.5
stays on 1.4.1 forever — the App Store will not offer them the update, silently.
That floor is upstream's and cannot be lowered.
## Rollback
- **Bad build, notes fine** → fix, commit, tag the next patch version. TestFlight
supersedes the bad build. (Build numbers are timestamps, so no version games.)
- **Bad release notes only** → edit `HEALTHCHAT_CHANGES.md`, then update the
TestFlight "What to Test" text in App Store Connect by hand — the pipeline only
writes it at upload time.
- **Wrong version number** → treat as burned; tag a higher one.
- **Rolling back to legacy 1.4.1 loses push** — element-x overwrites the user's
sygnal pusher in place on first launch (same app id and pushkey).
## The migration contract
**Read this before "cleaning up" anything.** HealthChat replaces the legacy app in
place, and these are what stop existing users being silently logged out.
- **`CLASSIC_APP_*` in `Variants/HealthChat/healthchat.yml` is load-bearing.**
`ClassicAppManager` reads the legacy account and crypto-store keys through them.
Upstream's defaults in `app.yml` point at Element's own `group.im.vector` /
`im.vector.app.*`, which this team cannot provision. **Never restore upstream's
values.** Failure is silent — `loadAccounts()` throws, and
`AuthenticationService` logs it without telling the user or you.
- **The classic keychain group in `ElementX/SupportingFiles/target.yml` must stay
in the `keychain-access-groups` array.** Entitlement arrays live there, never in
the variant — XcodeGen concatenates arrays on include-merge and only overrides
scalars.
- **The `.requiresBackup` fall-through in `AuthenticationStartScreenViewModel` is a
deliberate upstream divergence.** Upstream sends those accounts to a sheet that
tells the user to open Element Classic. That app no longer exists — this update
replaced it — and the sheet's button opens a URL scheme nothing registers. A
rebase that reverts this reintroduces a dead button.
- **Both apps share the `group.ch.healthchat` UserDefaults suite**
(`AppSettings.swift:56`). An unqualified `@UserPreference` name lands in a suite
the legacy also writes to. Collisions are cheap to introduce.
- **`hasSignedInBefore` voids migration testing.** It lives in that shared suite,
survives app deletion, and short-circuits `loadAccounts()`. Any device that has
run an element-x HealthChat build shows a plain login screen *while migration
works perfectly*. Test on a device that has never run an element-x HealthChat
build, or clear the `group.ch.healthchat` suite first;
`"Already signed in to Element X before, skipping loadAccounts"` in the log
means the run is **void, not failed**. A
`"Classic App not configured, skipping loadAccounts"` line is the opposite —
the `CLASSIC_APP_*` Info.plist keys are missing, `ClassicAppManager` never
initialised, and migration is broken for every user. Investigate, do not
dismiss.
## Known branch divergence on rebase
`fastlane/`, `.github/workflows/release-healthchat.yml`, `HEALTHCHAT_CHANGES.md`,
`Gemfile` and this skill are **fork-only** — upstream element-x-ios has
no `fastlane/` directory at all. A rebase conflict in any of them means something
upstream has started to overlap; keep `--ours` and investigate.
What does need watching:
- **`project.yml`'s `MARKETING_VERSION`** — `automatic-calendar-version.yml` and
`post-release.yml` are gated on `github.repository == 'element-hq/element-x-ios'`
and silently no-op here, so rebases drag upstream's version along. Harmless: the
tag is the source of truth.
- **`AuthenticationStartScreenViewModel.login(classicAppAccount:)`** — the only
upstream logic file this fork edits. See "The migration contract".
- **`app.yml`'s `CLASSIC_APP_*`** — upstream owns these and they stay pointed at
Element's ids. The variant overrides them. Do not "fix" `app.yml`.
@@ -11,6 +11,11 @@ permissions: {}
jobs: jobs:
tests: tests:
name: Tests name: Tests
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, because
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
# than deleted so the file keeps rebasing cleanly. Run locally with
# `swift run tools ci accessibility-tests`.
if: github.repository == 'element-hq/element-x-ios'
runs-on: macos-26 runs-on: macos-26
timeout-minutes: 150 timeout-minutes: 150
+5
View File
@@ -20,6 +20,11 @@ permissions: {}
jobs: jobs:
tests: tests:
# HealthChat: skip in this fork. It only triggers on changes under compound-ios/, which this fork
# has never edited, so it would only ever fire on an upstream sync and go red at checkout —
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
# than deleted so the file keeps rebasing cleanly.
if: github.repository == 'element-hq/element-x-ios'
runs-on: macos-26 runs-on: macos-26
timeout-minutes: 15 timeout-minutes: 15
+5
View File
@@ -12,6 +12,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
permissions:
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout. Upstream is public, hence permissions: {} there.
issues: read # HealthChat: Danger reads the PR via the issues API (a PR is an issue there); pull-requests alone 403s on GET /issues/{n}.
pull-requests: write # HealthChat: Danger posts its report comment and inline SwiftLint review comments (Dangerfile.swift:5 sets inline: true) with GITHUB_TOKEN.
concurrency: concurrency:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs. # Only allow a single run of this workflow on each branch, automatically cancelling older runs.
group: ${{ format('danger-ci-{0}', github.ref) }} group: ${{ format('danger-ci-{0}', github.ref) }}
+7
View File
@@ -11,6 +11,13 @@ permissions: {}
jobs: jobs:
integration_tests: integration_tests:
name: Integration Tests name: Integration Tests
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, and it is the one
# job that still calls out to Element's Codecov — its upload guard is true on schedule events, so
# it POSTs slug/branch/SHA every night with continue-on-error hiding the failure. Fixing it would
# need a real healthchat.ch test account and three new secrets, and would still not work:
# HealthChatAppSettingsHook sets allowOtherAccountProviders: false, hiding the change-server UI
# these tests drive. Gated rather than deleted so the file keeps rebasing cleanly.
if: github.repository == 'element-hq/element-x-ios'
runs-on: macos-26 runs-on: macos-26
timeout-minutes: 45 timeout-minutes: 45
+133
View File
@@ -0,0 +1,133 @@
name: Release HealthChat
on:
push:
tags:
- 'healthchat-v*'
workflow_dispatch:
inputs:
version:
description: 'Marketing version (e.g. 26.7.4). Must be 1-3 dot-separated integers — Apple rejects anything else.'
required: true
type: string
release_notes_override:
description: 'Optional release notes. If empty, falls back to the HEALTHCHAT_CHANGES.md section for the given version.'
required: false
type: string
permissions:
# Minimum needed for actions/checkout to clone an internal repo. The workflow does not push,
# comment, or create releases — everything else authenticates with the ASC key and .p12.
contents: read
concurrency:
group: release-healthchat-${{ github.event.inputs.version || github.ref_name }}
cancel-in-progress: false
jobs:
release:
name: Build & upload to TestFlight
runs-on: [self-hosted, macOS]
timeout-minutes: 90
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# This repo's only submodule is element-hq's private Enterprise repo, which Novaloop
# cannot clone. Checking out submodules fails the job on its very first step.
submodules: false
persist-credentials: false
- name: Record toolchain
# HealthChat: nothing pins Xcode (the runner carries a single globally-selected one), so this
# log line is the only record of which compiler produced a given TestFlight build.
run: xcodebuild -version
- name: Resolve marketing version
id: version
env:
# Read via env, never `${{ }}` inside the script: the input is attacker-controlled text
# that GitHub would splice into the shell before the regex below could reject it.
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="$INPUT_VERSION"
else
VERSION="${GITHUB_REF_NAME#healthchat-v}"
fi
# Apple caps CFBundleShortVersionString at three integers. The Fastfile guards this too,
# but failing here costs seconds instead of a signing run.
if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
echo "::error::Invalid marketing version '$VERSION' — expected 1-3 dot-separated integers, e.g. 26.7.4"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved marketing version: $VERSION"
- name: Extract release notes
id: notes
env:
VERSION: ${{ steps.version.outputs.version }}
OVERRIDE: ${{ github.event.inputs.release_notes_override }}
run: |
set -euo pipefail
if [[ -n "$OVERRIDE" ]]; then
NOTES="$OVERRIDE"
echo "Using release notes from the workflow input"
elif [[ ! -f HEALTHCHAT_CHANGES.md ]]; then
echo "::error::HEALTHCHAT_CHANGES.md is missing — it is the release-notes source"
exit 1
else
NOTES=$(awk -v ver="$VERSION" '
/^## Changes in / {
if (found) { exit }
if ($0 ~ "## Changes in " ver "( |$|\\()") { found = 1; next }
}
found { print }
' HEALTHCHAT_CHANGES.md | sed '/^[[:space:]]*$/{N;/^\n$/D;}')
if [[ -z "$NOTES" ]]; then
NOTES="HealthChat ${VERSION} — automated build from ${GITHUB_REF_NAME}."
echo "::warning::No HEALTHCHAT_CHANGES.md section found for $VERSION — using fallback"
fi
fi
{
echo 'notes<<NOTES_EOF'
echo "$NOTES"
echo 'NOTES_EOF'
} >> "$GITHUB_OUTPUT"
- name: Bundle install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Import signing certificate
uses: apple-actions/import-codesign-certs@b610f78488812c1e56b20e6df63ec42d833f2d14 # v6.0.0
with:
p12-file-base64: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12 }}
p12-password: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12_PASSWORD }}
- name: Build & upload to TestFlight
run: bundle exec fastlane release_testflight
env:
# Passed via env rather than lane options so multi-line notes containing quotes,
# $, or backticks survive without shell escaping.
HEALTHCHAT_MARKETING_VERSION: ${{ steps.version.outputs.version }}
HEALTHCHAT_RELEASE_NOTES: ${{ steps.notes.outputs.notes }}
APPSTORECONNECT_KEY_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ID }}
APPSTORECONNECT_KEY_ISSUER_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ISSUER_ID }}
APPSTORECONNECT_KEY_CONTENT: ${{ secrets.HEALTHCHAT_ASC_KEY_CONTENT }}
TEAM_ID: '6G37K72W82'
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: healthchat-${{ steps.version.outputs.version }}
path: |
out/ElementX.ipa
out/ElementX.app.dSYM.zip
build/Logs/Gym/*.log
retention-days: 90
if-no-files-found: warn
+6
View File
@@ -15,6 +15,12 @@ permissions: {}
jobs: jobs:
tests: tests:
name: Tests name: Tests
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, because
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Fixing the
# permission would not help: the rebrand changed the start screen without re-recording any
# UITests snapshot, so it would fail on content instead. Gated rather than deleted so the file
# keeps rebasing cleanly. Run locally with `swift run tools ci ui-tests`.
if: github.repository == 'element-hq/element-x-ios'
runs-on: macos-26 runs-on: macos-26
timeout-minutes: 90 timeout-minutes: 90
strategy: strategy:
+8 -1
View File
@@ -17,7 +17,14 @@ jobs:
timeout-minutes: 45 timeout-minutes: 45
# Skip in forks # Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} # HealthChat: this job can never pass in this fork — it needs
# secrets.ELEMENT_BOT_TOKEN (Element's bot, absent here) and recursively
# checks out the private element-hq/element-ios-enterprise submodule, which
# Novaloop cannot obtain. The guard below only asks whether the PR comes
# FROM a fork, not whether this repo IS one, so it does not skip us. Gate on
# the upstream repo as post-release.yml does. Gated rather than deleted so
# the file keeps rebasing cleanly against upstream.
if: ${{ github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
concurrency: concurrency:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs. # Only allow a single run of this workflow on each branch, automatically cancelling older runs.
+68 -8
View File
@@ -13,24 +13,78 @@ permissions: {}
jobs: jobs:
tests: tests:
name: Tests name: Tests
runs-on: macos-26 # HealthChat: GitHub-hosted macos-26 could not finish this job inside
# timeout-minutes: 60 (two attempts: 51m42s, then killed at 60m44s), and macOS
# minutes bill at 10x. novaloop-mac-studio-001 is the org's Mac Studio, already
# used by the release pipeline; its labels are [self-hosted, macOS, ARM64].
runs-on: [self-hosted, macOS]
# Upstream's 60 is kept: with previews skipped (see Run tests) the build plus unit tests measured
# ~16 min here. A run approaching this budget is a signal, not normal — the earlier 60m timeouts
# were preview tests, not the build.
timeout-minutes: 60 timeout-minutes: 60
permissions:
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout (and on git lfs pull). Upstream is public, hence permissions: {} there.
concurrency: concurrency:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs. # Only allow a single run of this workflow on each branch, automatically cancelling older runs.
group: ${{ format('unit-tests-{0}', github.ref) }} group: ${{ format('unit-tests-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
# HealthChat: upstream passes persist-credentials: false here, which the
# composite forwards to actions/checkout — stripping the auth header before
# its separate "Git LFS Pull" step runs. Upstream can afford that because
# its repo is public and git lfs pull works anonymously; this fork is
# INTERNAL, so the LFS batch endpoint would 404. Keep the composite's own
# default (true) so the pull is authenticated with GITHUB_TOKEN.
- uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
persist-credentials: false
- name: Setup environment # HealthChat: verify only — never provision. Upstream's
run: source ci_scripts/ci_common.sh && setup_github_actions_environment # setup_github_actions_environment cannot run on this runner: it calls
# `sudo xcode-select -s /Applications/Xcode_26.5.0.app`, but the runner has no
# passwordless sudo and no such path (only /Applications/Xcode.app exists), and
# it calls `brew update && brew install ...` — novaloop-mac-studio-001 is the
# shared release machine, so a per-run `brew update` could upgrade xcodegen
# underneath the release pipeline. These tools are installed once by hand; CI
# only asserts they are present and fails loudly, naming the fix, if not.
- name: Verify environment
run: |
set -eu
missing=""
formulae=""
for tool in xcodegen swiftlint swiftformat xcbeautify xcresultparser pkl git-lfs; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing="$missing $tool"
case "$tool" in
# Not in homebrew-core; only this tap ships it.
xcresultparser) formulae="$formulae a7ex/homebrew-formulae/xcresultparser" ;;
*) formulae="$formulae $tool" ;;
esac
fi
done
if [ -n "$missing" ]; then
echo "::error::Missing on self-hosted runner:${missing}. Install by hand on novaloop-mac-studio-001, then re-run: brew install${formulae}"
exit 1
fi
# The Xcode version is no longer pinned anywhere, so this is the only record
# of which Xcode built a given run.
xcodebuild -version
- name: Run tests - name: Run tests
run: swift run -q tools ci unit-tests # HealthChat: previews are skipped. PreviewTests renders a snapshot for every preview in the
# app and was the entire reason this job could not fit in an hour — it never once finished,
# on either runner. Unit tests alone take ~16 min here. Upstream skips them the same way in
# unit-tests-enterprise.yml. Cost: preview snapshot regressions are not caught in CI, so
# re-record and check them locally when changing UI.
env:
# HealthChat: VoiceMessageCacheTests cannot write its URLFileProtection.complete cache file on
# this runner's simulator (NSCocoaErrorDomain 513) — a runner-environment issue, not a code bug:
# the suite passes locally and upstream, and neither recreating+booting the device nor clearing
# state fixes it. Skip it here only, via -skip-testing. Drop this once the runner can write
# data-protected files again.
CI_SKIP_TESTS: VoiceMessageCacheTests
run: swift run -q tools ci unit-tests --skip-previews
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -44,7 +98,11 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
# Skip if not successful and in forks # Skip if not successful and in forks
if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} # HealthChat: the existing guard only asks whether the PR comes FROM a
# fork, not whether this repo IS one, so it is true here and the step
# runs without a CODECOV_TOKEN — which fails the job via
# fail_ci_if_error. Gate on the upstream repo as post-release.yml does.
if: ${{ success() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with: with:
report_type: coverage report_type: coverage
@@ -56,7 +114,9 @@ jobs:
- name: Upload test results to Codecov - name: Upload test results to Codecov
# Skip if cancelled and in forks # Skip if cancelled and in forks
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} # HealthChat: gated on the upstream repo for the same reason as the
# coverage step above — no CODECOV_TOKEN exists in this fork.
if: ${{ !cancelled() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
continue-on-error: true continue-on-error: true
with: with:
+2
View File
@@ -14,6 +14,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout. Upstream is public, hence permissions: {} there.
actions: read # HealthChat: upload-sarif additionally needs this on private/internal repos; public upstream does not.
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+7
View File
@@ -49,3 +49,10 @@ test_output
# Only commit Pkl packages (there may be other caches in here). # Only commit Pkl packages (there may be other caches in here).
Secrets/vendor/* Secrets/vendor/*
!Secrets/vendor/package-2 !Secrets/vendor/package-2
# fastlane release outputs
out/
provisioning_profiles/
# Regenerated by every fastlane invocation, including on the runner. Ignored rather than
# committed, matching element-ios.
fastlane/README.md
+8
View File
@@ -63,6 +63,14 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
--- ---
## Releasing
HealthChat ship via tag `healthchat-v*` → TestFlight. Full runbook in
`.agents/skills/releasing-healthchat/SKILL.md`. Read skill before release, cert
renewal, or touching `fastlane/`. No duplicate runbook here.
---
## Project Structure ## Project Structure
### Build System ### Build System
+47 -23
View File
@@ -615,6 +615,7 @@
659E5B766F76FDEC1BF393A4 /* RoomDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */; }; 659E5B766F76FDEC1BF393A4 /* RoomDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */; };
661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */; }; 661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */; };
663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; };
665314B4CB31904F107D2D3B /* HealthChatAppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ECC44D40D05D3A6F0DE0F7 /* HealthChatAppSettingsHook.swift */; };
6681D6D3ADF69EBD2625F29A /* KnockedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */; }; 6681D6D3ADF69EBD2625F29A /* KnockedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */; };
66832DE7B5C2E861045265DC /* RoomSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */; }; 66832DE7B5C2E861045265DC /* RoomSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */; };
66C48B5B35C861C2089DBB12 /* ToolbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510DA63AB273A68D659CEC95 /* ToolbarButton.swift */; }; 66C48B5B35C861C2089DBB12 /* ToolbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510DA63AB273A68D659CEC95 /* ToolbarButton.swift */; };
@@ -669,6 +670,7 @@
6CBC544A7CC4DDDBDED4D0A6 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 4B1F71AC585827E6C416C15A /* AppIcon.icon */; }; 6CBC544A7CC4DDDBDED4D0A6 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 4B1F71AC585827E6C416C15A /* AppIcon.icon */; };
6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */; }; 6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */; };
6DC8E43BA04AC2AC4EB2EB97 /* AnalyticsPromptScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */; }; 6DC8E43BA04AC2AC4EB2EB97 /* AnalyticsPromptScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */; };
6DDA932A28F531791BDBFB08 /* HealthChatBrandColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A60C2712F5607406E8B1C80 /* HealthChatBrandColors.swift */; };
6E03A710799E6C65C0AB36BC /* TargetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D829FD8958376614504B18 /* TargetConfiguration.swift */; }; 6E03A710799E6C65C0AB36BC /* TargetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D829FD8958376614504B18 /* TargetConfiguration.swift */; };
6E0C68918BEC5D27A4830BA6 /* PlaybackSpeedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBB8E5436E1BE0D6814BB7D /* PlaybackSpeedButton.swift */; }; 6E0C68918BEC5D27A4830BA6 /* PlaybackSpeedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBB8E5436E1BE0D6814BB7D /* PlaybackSpeedButton.swift */; };
6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */; }; 6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */; };
@@ -933,6 +935,7 @@
95690DDD9D547D3D842ACBE3 /* AnalyticsSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */; }; 95690DDD9D547D3D842ACBE3 /* AnalyticsSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */; };
9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */; }; 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */; };
95B690EA99A3385D4B8A593D /* NotificationToneManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B94A6A57FE7E71F7605070AA /* NotificationToneManager.swift */; }; 95B690EA99A3385D4B8A593D /* NotificationToneManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B94A6A57FE7E71F7605070AA /* NotificationToneManager.swift */; };
95C338B0F2101D8A8CA16A98 /* HealthChatCompoundHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09E2B1AB937F2D01815495F /* HealthChatCompoundHook.swift */; };
95E7B236F7116CACE05A6BC9 /* BlockedUsersScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */; }; 95E7B236F7116CACE05A6BC9 /* BlockedUsersScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */; };
9603EEF6DE980BB1D15D4707 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A3E8741D199CD1A37F4CBF /* UIView.swift */; }; 9603EEF6DE980BB1D15D4707 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A3E8741D199CD1A37F4CBF /* UIView.swift */; };
962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */; }; 962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */; };
@@ -1404,6 +1407,7 @@
E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */; }; E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */; };
E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */; }; E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */; };
E7DB506E77CED28D6FE6203D /* ManageAuthorizedSpacesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8E7C716B8DC2F71E9AC439 /* ManageAuthorizedSpacesScreenModels.swift */; }; E7DB506E77CED28D6FE6203D /* ManageAuthorizedSpacesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8E7C716B8DC2F71E9AC439 /* ManageAuthorizedSpacesScreenModels.swift */; };
E7F27870397639C204402434 /* HealthChatAppHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B439012B010EC4C3DD24D0 /* HealthChatAppHooks.swift */; };
E81E8B7A8ADDB2A912A609F6 /* CallIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2DB32B72EA4772E2C9E3B3 /* CallIntent.swift */; }; E81E8B7A8ADDB2A912A609F6 /* CallIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2DB32B72EA4772E2C9E3B3 /* CallIntent.swift */; };
E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; }; E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; };
E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; }; E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; };
@@ -2341,6 +2345,7 @@
796CBD0C56FA0D3AEDAB255B /* SessionVerificationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenCoordinator.swift; sourceTree = "<group>"; }; 796CBD0C56FA0D3AEDAB255B /* SessionVerificationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenCoordinator.swift; sourceTree = "<group>"; };
79A106D76741914F82664959 /* StateStoreViewModelV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModelV2.swift; sourceTree = "<group>"; }; 79A106D76741914F82664959 /* StateStoreViewModelV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModelV2.swift; sourceTree = "<group>"; };
7A03E073077D92AA19C43DCF /* IdentityConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenCoordinator.swift; sourceTree = "<group>"; }; 7A03E073077D92AA19C43DCF /* IdentityConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenCoordinator.swift; sourceTree = "<group>"; };
7A60C2712F5607406E8B1C80 /* HealthChatBrandColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatBrandColors.swift; sourceTree = "<group>"; };
7A6D867A7FBB70C6EFDBCBC5 /* AccessibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityTests.swift; sourceTree = "<group>"; }; 7A6D867A7FBB70C6EFDBCBC5 /* AccessibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityTests.swift; sourceTree = "<group>"; };
7AAD8C633AA57948B34EDCF7 /* RoomChangeRolesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 7AAD8C633AA57948B34EDCF7 /* RoomChangeRolesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelProtocol.swift; sourceTree = "<group>"; };
7AB74D407F0E413E7593B369 /* SpaceAddRoomsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModelTests.swift; sourceTree = "<group>"; }; 7AB74D407F0E413E7593B369 /* SpaceAddRoomsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModelTests.swift; sourceTree = "<group>"; };
@@ -2938,6 +2943,8 @@
DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineItem.swift; sourceTree = "<group>"; }; DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineItem.swift; sourceTree = "<group>"; };
DFFB0E7C6D8E190AFA0176DC /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uz; path = uz.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; DFFB0E7C6D8E190AFA0176DC /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uz; path = uz.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFilterModels.swift; sourceTree = "<group>"; }; E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFilterModels.swift; sourceTree = "<group>"; };
E09E2B1AB937F2D01815495F /* HealthChatCompoundHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatCompoundHook.swift; sourceTree = "<group>"; };
E0B439012B010EC4C3DD24D0 /* HealthChatAppHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatAppHooks.swift; sourceTree = "<group>"; };
E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = "<group>"; }; E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = "<group>"; };
E0FF9CB3EFA753277291F609 /* EncryptionResetScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenCoordinator.swift; sourceTree = "<group>"; }; E0FF9CB3EFA753277291F609 /* EncryptionResetScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenCoordinator.swift; sourceTree = "<group>"; };
E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileListRow.swift; sourceTree = "<group>"; }; E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileListRow.swift; sourceTree = "<group>"; };
@@ -2946,6 +2953,7 @@
E1714DD78BFE27ADD40A6F89 /* LinkNewDeviceScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceScreenViewModelTests.swift; sourceTree = "<group>"; }; E1714DD78BFE27ADD40A6F89 /* LinkNewDeviceScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceScreenViewModelTests.swift; sourceTree = "<group>"; };
E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = "<group>"; }; E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = "<group>"; };
E1E0B4A34E69BD2132BEC521 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = "<group>"; }; E1E0B4A34E69BD2132BEC521 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = "<group>"; };
E1ECC44D40D05D3A6F0DE0F7 /* HealthChatAppSettingsHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatAppSettingsHook.swift; sourceTree = "<group>"; };
E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallNotificationRoomTimelineItem.swift; sourceTree = "<group>"; }; E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallNotificationRoomTimelineItem.swift; sourceTree = "<group>"; };
E20403084A320D588ACED200 /* ReportRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenViewModelProtocol.swift; sourceTree = "<group>"; }; E20403084A320D588ACED200 /* ReportRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
E2520C4F33AA0C061D209C28 /* RoomMembersListScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenTests.swift; sourceTree = "<group>"; }; E2520C4F33AA0C061D209C28 /* RoomMembersListScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenTests.swift; sourceTree = "<group>"; };
@@ -3588,6 +3596,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */, B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */,
801BA878EE879E01E82F3A5F /* HealthChat */,
2ED530A4DD98266D31E5D8E1 /* Hooks */, 2ED530A4DD98266D31E5D8E1 /* Hooks */,
); );
path = AppHooks; path = AppHooks;
@@ -5366,6 +5375,17 @@
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
801BA878EE879E01E82F3A5F /* HealthChat */ = {
isa = PBXGroup;
children = (
E0B439012B010EC4C3DD24D0 /* HealthChatAppHooks.swift */,
E1ECC44D40D05D3A6F0DE0F7 /* HealthChatAppSettingsHook.swift */,
7A60C2712F5607406E8B1C80 /* HealthChatBrandColors.swift */,
E09E2B1AB937F2D01815495F /* HealthChatCompoundHook.swift */,
);
path = HealthChat;
sourceTree = "<group>";
};
8039515BAA53B7C3275AC64A /* Client */ = { 8039515BAA53B7C3275AC64A /* Client */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -7480,34 +7500,34 @@
ORGANIZATIONNAME = Element; ORGANIZATIONNAME = Element;
TargetAttributes = { TargetAttributes = {
0E28CD62691FDBC63147D5E3 = { 0E28CD62691FDBC63147D5E3 = {
DevelopmentTeam = 7J4U792NQT; DevelopmentTeam = 6G37K72W82;
TestTargetID = C0FAEB81CFD9776CD78CE489; TestTargetID = C0FAEB81CFD9776CD78CE489;
}; };
19F0C845D67E9BEA4BE7133E = { 19F0C845D67E9BEA4BE7133E = {
DevelopmentTeam = "$(DEVELOPMENT_TEAM)"; DevelopmentTeam = "$(DEVELOPMENT_TEAM)";
}; };
32C23C8D224D46EFE62AFAD0 = { 32C23C8D224D46EFE62AFAD0 = {
DevelopmentTeam = 7J4U792NQT; DevelopmentTeam = 6G37K72W82;
}; };
676A87694F0F247B2AF1A142 = { 676A87694F0F247B2AF1A142 = {
DevelopmentTeam = 7J4U792NQT; DevelopmentTeam = 6G37K72W82;
}; };
7A17BE29BAC81ADBAC6349D9 = { 7A17BE29BAC81ADBAC6349D9 = {
DevelopmentTeam = 7J4U792NQT; DevelopmentTeam = 6G37K72W82;
}; };
C0C687DE1D270F9895FEE186 = { C0C687DE1D270F9895FEE186 = {
DevelopmentTeam = 7J4U792NQT; DevelopmentTeam = 6G37K72W82;
TestTargetID = C0FAEB81CFD9776CD78CE489; TestTargetID = C0FAEB81CFD9776CD78CE489;
}; };
C0FAEB81CFD9776CD78CE489 = { C0FAEB81CFD9776CD78CE489 = {
DevelopmentTeam = "$(DEVELOPMENT_TEAM)"; DevelopmentTeam = "$(DEVELOPMENT_TEAM)";
}; };
D3DB351B7FBE0F49649171FC = { D3DB351B7FBE0F49649171FC = {
DevelopmentTeam = 7J4U792NQT; DevelopmentTeam = 6G37K72W82;
TestTargetID = C0FAEB81CFD9776CD78CE489; TestTargetID = C0FAEB81CFD9776CD78CE489;
}; };
F8E276FD6DC43EADB85241BC = { F8E276FD6DC43EADB85241BC = {
DevelopmentTeam = 7J4U792NQT; DevelopmentTeam = 6G37K72W82;
}; };
FEB53A5BC378C913769656D8 = { FEB53A5BC378C913769656D8 = {
DevelopmentTeam = "$(DEVELOPMENT_TEAM)"; DevelopmentTeam = "$(DEVELOPMENT_TEAM)";
@@ -8510,6 +8530,10 @@
4295E5F850897710A51AE114 /* GeoURI.swift in Sources */, 4295E5F850897710A51AE114 /* GeoURI.swift in Sources */,
F3C9CAD26FD4D7D6EBACF501 /* HTMLFixtures.swift in Sources */, F3C9CAD26FD4D7D6EBACF501 /* HTMLFixtures.swift in Sources */,
E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */, E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */,
E7F27870397639C204402434 /* HealthChatAppHooks.swift in Sources */,
665314B4CB31904F107D2D3B /* HealthChatAppSettingsHook.swift in Sources */,
6DDA932A28F531791BDBFB08 /* HealthChatBrandColors.swift in Sources */,
95C338B0F2101D8A8CA16A98 /* HealthChatCompoundHook.swift in Sources */,
0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */, 0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */,
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */, 62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */,
@@ -9833,11 +9857,11 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
APP_DISPLAY_NAME = "Element X"; APP_DISPLAY_NAME = HealthChat;
APP_GROUP_IDENTIFIER = group.io.element; APP_GROUP_IDENTIFIER = group.ch.healthchat;
APP_NAME = ElementX; APP_NAME = ElementX;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "colors/accent-color"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "colors/accent-color";
BASE_BUNDLE_IDENTIFIER = io.element.elementx; BASE_BUNDLE_IDENTIFIER = ch.healthchat.element;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -9869,13 +9893,13 @@
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLASSIC_APP_DEEP_LINK_URL = "element://open"; CLASSIC_APP_DEEP_LINK_URL = "element://open";
CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector; CLASSIC_APP_GROUP_IDENTIFIER = group.ch.healthchat;
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared"; CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared";
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service"; CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "ch.healthchat.element.encryption-manager-service";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 7J4U792NQT; DEVELOPMENT_TEAM = 6G37K72W82;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -9894,7 +9918,7 @@
MARKETING_VERSION = 26.07.3; MARKETING_VERSION = 26.07.3;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCTION_APP_NAME = Element; PRODUCTION_APP_NAME = HealthChat;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
@@ -9908,11 +9932,11 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
APP_DISPLAY_NAME = "Element X"; APP_DISPLAY_NAME = HealthChat;
APP_GROUP_IDENTIFIER = group.io.element; APP_GROUP_IDENTIFIER = group.ch.healthchat;
APP_NAME = ElementX; APP_NAME = ElementX;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "colors/accent-color"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "colors/accent-color";
BASE_BUNDLE_IDENTIFIER = io.element.elementx; BASE_BUNDLE_IDENTIFIER = ch.healthchat.element;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -9944,13 +9968,13 @@
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLASSIC_APP_DEEP_LINK_URL = "element://open"; CLASSIC_APP_DEEP_LINK_URL = "element://open";
CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector; CLASSIC_APP_GROUP_IDENTIFIER = group.ch.healthchat;
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared"; CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared";
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service"; CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "ch.healthchat.element.encryption-manager-service";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 7J4U792NQT; DEVELOPMENT_TEAM = 6G37K72W82;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@@ -9976,7 +10000,7 @@
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
PRODUCTION_APP_NAME = Element; PRODUCTION_APP_NAME = HealthChat;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 265 KiB

@@ -1,15 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "app-logo.pdf", "filename" : "app-logo.png",
"idiom" : "universal" "idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "app-logo@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "app-logo@3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"preserves-vector-representation" : true
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

@@ -1,4 +1,5 @@
"screen_home_tab_search" = "Search"; "screen_home_tab_search" = "Search";
"screen_onboarding_welcome_title_ios" = "Welcome to HealthChat";
"screen_search_empty_state_message" = "Search for chats and messages"; "screen_search_empty_state_message" = "Search for chats and messages";
"screen_search_empty_state_title" = "Start searching..."; "screen_search_empty_state_title" = "Start searching...";
"screen_search_no_results_message" = "There are no results for “%1$@.” Try a new search term."; "screen_search_no_results_message" = "There are no results for “%1$@.” Try a new search term.";
@@ -0,0 +1,21 @@
//
// Copyright 2026 Novaloop AG
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
/// Registers the HealthChat hooks.
///
/// `AppHooks` is a concrete type and `AppCoordinator` calls `setUp()` on it directly, so this method
/// wins over the empty `AppHooksProtocol` default via static dispatch — mirroring how Element's own
/// Enterprise fork provides its `setUp()`. This file is compiled only into the main app; the NSE and
/// Share Extension keep the empty default (they never drive authentication).
extension AppHooks {
func setUp() {
#if IS_MAIN_APP
registerAppSettingsHook(HealthChatAppSettingsHook())
registerCompoundHook(HealthChatCompoundHook())
#endif
}
}
@@ -0,0 +1,40 @@
//
// Copyright 2026 Novaloop AG
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
/// Pins HealthChat's server, OIDC and push configuration by overriding `AppSettings` at startup.
///
/// - `accountProviders` / `allowOtherAccountProviders` lock every user to `healthchat.ch` (the server name;
/// `.well-known` delegates to `matrix.healthchat.ch`) and hide the change-server / create-account UI.
/// - `oAuthRedirectURL` uses the app's own URL scheme so the OIDC/MAS callback needs no associated domain.
/// - Legacy `element.io` help/encryption/copyright URLs are passed through unchanged for now.
struct HealthChatAppSettingsHook: AppSettingsHookProtocol {
func configure(_ appSettings: AppSettings) -> AppSettings {
appSettings.override(accountProviders: ["healthchat.ch"],
allowOtherAccountProviders: false,
hideBrandChrome: false,
pushGatewayBaseURL: "https://sygnal.healthchat.ch",
oAuthRedirectURL: "ch.healthchat.element:/oauth/callback",
websiteURL: "https://healthchat.ch",
logoURL: "https://app.healthchat.ch/themes/element/img/logos/element-logo.png",
copyrightURL: appSettings.copyrightURL,
acceptableUseURL: "https://www.healthchat.ch/datenschutz",
privacyURL: "https://www.healthchat.ch/datenschutz",
encryptionURL: appSettings.encryptionURL,
deviceVerificationURL: appSettings.deviceVerificationURL,
chatBackupDetailsURL: appSettings.chatBackupDetailsURL,
identityPinningViolationDetailsURL: appSettings.identityPinningViolationDetailsURL,
historySharingDetailsURL: appSettings.historySharingDetailsURL,
elementWebHosts: ["app.healthchat.ch"],
accountProvisioningHost: appSettings.accountProvisioningHost,
bugReportApplicationID: appSettings.bugReportApplicationID,
analyticsTermsURL: appSettings.analyticsTermsURL,
mapTilerConfiguration: AppSettings.bundledMapTilerConfiguration)
return appSettings
}
}
@@ -0,0 +1,33 @@
//
// Copyright 2026 Novaloop AG
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import UIKit
/// HealthChat brand palette, ported from the legacy Element iOS client.
/// These light/dark values are applied to Compound's accent token family by `HealthChatCompoundHook`.
///
/// **Note:** `nonisolated` is required. This target defaults to `MainActor` isolation, but UIKit resolves
/// the dynamic colour providers below on SwiftUI's render thread, which traps on an isolated closure.
nonisolated enum HealthChatBrandColors {
/// Primary brand blue — light `#1F4D85`, dark `#376A91`.
static let brand = dynamic(light: hex(0x1F4D85), dark: hex(0x376A91))
/// Pressed state — a slightly darker brand blue.
static let brandPressed = dynamic(light: hex(0x17385F), dark: hex(0x2A5171))
/// Subtle accent background / tint — light `#EBF0F7`, dark `#1A2D47`.
static let tint = dynamic(light: hex(0xEBF0F7), dark: hex(0x1A2D47))
private static func dynamic(light: UIColor, dark: UIColor) -> UIColor {
UIColor { $0.userInterfaceStyle == .dark ? dark : light }
}
private static func hex(_ value: Int, alpha: CGFloat = 1) -> UIColor {
UIColor(red: CGFloat((value >> 16) & 0xFF) / 255,
green: CGFloat((value >> 8) & 0xFF) / 255,
blue: CGFloat(value & 0xFF) / 255,
alpha: alpha)
}
}
@@ -0,0 +1,84 @@
//
// Copyright 2026 Novaloop AG
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
import UIKit
/// Recolours Compound's accent token family to the HealthChat brand.
/// Applied once at startup from `AppCoordinator` via `AppHooks.compoundHook`. Because the override
/// runs at app startup (not in previews), preview snapshots keep the default Compound colours.
struct HealthChatCompoundHook: CompoundHookProtocol {
func override(colors: CompoundColors, uiColors: CompoundUIColors) {
let brand = HealthChatBrandColors.brand
let tint = HealthChatBrandColors.tint
// Solid accent → brand blue (primary + send buttons, links, accent icons, badges, borders).
// NB: bgAccentRest is a SOLID fill (behind a white icon on the send button), not a subtle tint.
let solidColorTokens: [KeyPath<CompoundColorTokens, Color>] = [
\.bgActionPrimaryRest,
\.bgAccentRest,
\.iconAccentPrimary,
\.iconAccentTertiary,
\.textActionPrimary,
\.textActionAccent,
\.textLinkExternal,
\.borderInteractivePrimary,
\.textBadgeAccent
]
let solidUIColorTokens: [KeyPath<CompoundUIColorTokens, UIColor>] = [
\.bgActionPrimaryRest,
\.bgAccentRest,
\.iconAccentPrimary,
\.iconAccentTertiary,
\.textActionPrimary,
\.textActionAccent,
\.textLinkExternal,
\.borderInteractivePrimary,
\.textBadgeAccent
]
for keyPath in solidColorTokens {
colors.override(keyPath, with: Color(brand))
}
for keyPath in solidUIColorTokens {
uiColors.override(keyPath, with: brand)
}
// Primary button pressed state. The disabled state stays Compound's neutral grey.
colors.override(\.bgActionPrimaryPressed, with: Color(HealthChatBrandColors.brandPressed))
uiColors.override(\.bgActionPrimaryPressed, with: HealthChatBrandColors.brandPressed)
// Gradient "super"/action buttons → brand blue (SwiftUI-only concept).
let gradientTokens: [KeyPath<CompoundColorTokens, Color>] = [
\.gradientActionStop1,
\.gradientActionStop2,
\.gradientActionStop3,
\.gradientActionStop4
]
for keyPath in gradientTokens {
colors.override(keyPath, with: Color(brand))
}
// Subtle accent backgrounds (paired with accent-coloured foregrounds) → brand tint.
let tintColorTokens: [KeyPath<CompoundColorTokens, Color>] = [
\.bgAccentSelected,
\.bgBadgeAccent,
\.borderAccentSubtle
]
let tintUIColorTokens: [KeyPath<CompoundUIColorTokens, UIColor>] = [
\.bgAccentSelected,
\.bgBadgeAccent,
\.borderAccentSubtle
]
for keyPath in tintColorTokens {
colors.override(keyPath, with: Color(tint))
}
for keyPath in tintUIColorTokens {
uiColors.override(keyPath, with: tint)
}
}
}
@@ -12,6 +12,8 @@ import Foundation
internal nonisolated enum UntranslatedL10n { internal nonisolated enum UntranslatedL10n {
/// Search /// Search
internal static var screenHomeTabSearch: String { return UntranslatedL10n.tr("Untranslated", "screen_home_tab_search") } internal static var screenHomeTabSearch: String { return UntranslatedL10n.tr("Untranslated", "screen_home_tab_search") }
/// Welcome to HealthChat
internal static var screenOnboardingWelcomeTitleIos: String { return UntranslatedL10n.tr("Untranslated", "screen_onboarding_welcome_title_ios") }
/// Search for chats and messages /// Search for chats and messages
internal static var screenSearchEmptyStateMessage: String { return UntranslatedL10n.tr("Untranslated", "screen_search_empty_state_message") } internal static var screenSearchEmptyStateMessage: String { return UntranslatedL10n.tr("Untranslated", "screen_search_empty_state_message") }
/// Start searching... /// Start searching...
@@ -113,13 +113,14 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
private func login(classicAppAccount: ClassicAppAccount? = nil) async { private func login(classicAppAccount: ClassicAppAccount? = nil) async {
if let classicAppAccount { if let classicAppAccount {
if classicAppAccount.state.availableSecrets == .requiresBackup { // HealthChat diverges from upstream here. Upstream sends `.requiresBackup` accounts to a sheet
state.bindings.showClassicAppBackupInstructions = true // asking the user to set up key backup in Element Classic first. HealthChat ships with the same
} else { // bundle id as the legacy app, so this update *replaces* it — there is no Classic app left to
// open, and the sheet's only button resolves to nothing. Fall through to a normal sign-in
// instead: the user re-verifies and loses only history that by definition has no backup.
await configureAccountProvider(classicAppAccount.serverName, await configureAccountProvider(classicAppAccount.serverName,
loginHint: "mxid:\(classicAppAccount.userID)", loginHint: "mxid:\(classicAppAccount.userID)",
fallbackHomeserverURL: classicAppAccount.homeserverURL) fallbackHomeserverURL: classicAppAccount.homeserverURL)
}
} else if let serverName = state.serverName { } else if let serverName = state.serverName {
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint) await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint)
} else { } else {
@@ -76,7 +76,7 @@ struct AuthenticationStartScreen: View {
if !context.viewState.hideBrandChrome { if !context.viewState.hideBrandChrome {
VStack(spacing: 8) { VStack(spacing: 8) {
Text(L10n.screenOnboardingWelcomeTitle) Text(UntranslatedL10n.screenOnboardingWelcomeTitleIos)
.font(.compound.headingLGBold) .font(.compound.headingLGBold)
.foregroundColor(.compound.textPrimary) .foregroundColor(.compound.textPrimary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -6,13 +6,9 @@
<string>development</string> <string>development</string>
<key>com.apple.developer.associated-domains</key> <key>com.apple.developer.associated-domains</key>
<array> <array>
<string>applinks:element.io</string> <string>applinks:app.healthchat.ch</string>
<string>applinks:app.element.io</string>
<string>applinks:staging.element.io</string>
<string>applinks:develop.element.io</string>
<string>applinks:mobile.element.io</string>
<string>applinks:matrix.to</string> <string>applinks:matrix.to</string>
<string>webcredentials:*.element.io</string> <string>webcredentials:*.healthchat.ch</string>
</array> </array>
<key>com.apple.developer.usernotifications.communication</key> <key>com.apple.developer.usernotifications.communication</key>
<true/> <true/>
@@ -21,7 +17,6 @@
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>$(APP_GROUP_IDENTIFIER)</string> <string>$(APP_GROUP_IDENTIFIER)</string>
<string>$(CLASSIC_APP_GROUP_IDENTIFIER)</string>
</array> </array>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
+1 -1
View File
@@ -30,7 +30,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>$(APP_DISPLAY_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
+12 -8
View File
@@ -48,6 +48,9 @@ targets:
UILaunchScreen: UILaunchScreen:
UIColorName: colors/background-color UIColorName: colors/background-color
CFBundleDisplayName: $(APP_DISPLAY_NAME) CFBundleDisplayName: $(APP_DISPLAY_NAME)
# HealthChat: without this CFBundleName defaults to $(PRODUCT_NAME) (ElementX, kept for the module
# name), which is what the web authentication session's sign-in alert shows — not CFBundleDisplayName.
CFBundleName: $(APP_DISPLAY_NAME)
CFBundleShortVersionString: $(MARKETING_VERSION) CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION) CFBundleVersion: $(CURRENT_PROJECT_VERSION)
CFBundleURLTypes: [ CFBundleURLTypes: [
@@ -115,22 +118,23 @@ targets:
properties: properties:
aps-environment: development aps-environment: development
com.apple.developer.associated-domains: com.apple.developer.associated-domains:
- applinks:element.io # HealthChat: app.healthchat.ch universal links + webcredentials (OAuth/autofill). matrix.to kept for permalinks.
- applinks:app.element.io - applinks:app.healthchat.ch
- applinks:staging.element.io
- applinks:develop.element.io
- applinks:mobile.element.io
- applinks:matrix.to - applinks:matrix.to
# - applinks:localhost?mode=developer # developer mode only works with https (but self-signed is fine). - webcredentials:*.healthchat.ch
- webcredentials:*.element.io
com.apple.developer.usernotifications.communication: true com.apple.developer.usernotifications.communication: true
com.apple.security.app-sandbox: true com.apple.security.app-sandbox: true
com.apple.security.application-groups: com.apple.security.application-groups:
# HealthChat: dropped $(CLASSIC_APP_GROUP_IDENTIFIER) — not because it is un-provisionable,
# but because it resolves to group.ch.healthchat, the same value as $(APP_GROUP_IDENTIFIER)
# below. The legacy container is already entitled; listing it twice would duplicate the entry.
- $(APP_GROUP_IDENTIFIER) - $(APP_GROUP_IDENTIFIER)
- $(CLASSIC_APP_GROUP_IDENTIFIER)
com.apple.security.network.client: true com.apple.security.network.client: true
keychain-access-groups: keychain-access-groups:
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER) - $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
# HealthChat: the legacy app's keychain group. ClassicAppManager reads the account and
# crypto-store keys from here to migrate existing users. Same team prefix, so it is
# provisionable — unlike Element's im.vector.app.* default in app.yml.
- $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER) - $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
settings: settings:
+6
View File
@@ -0,0 +1,6 @@
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
+350
View File
@@ -0,0 +1,350 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1269.0)
aws-sdk-core (3.254.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.130.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.227.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.1.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (1.5.0)
logger
faraday (1.10.6)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.4)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.1)
fastlane (2.237.0)
CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1)
addressable (>= 2.9.0, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
base64 (~> 0.2)
benchmark (>= 0.1.0)
bundler (>= 2.4.0, < 5.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 2.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.3.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.10.3, < 4)
logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_json (~> 1.12)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3)
naturally (~> 2.2)
nkf (~> 0.2)
optparse (>= 0.1.1, < 1.0.0)
ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-brew (0.1.1)
fastlane-plugin-xcodegen (1.1.0)
fastlane-plugin-brew (~> 0.1.1)
fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.104.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.28.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.18.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.64.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.9.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.2.2)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.7.0)
google-cloud-storage (1.62.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
googleauth (~> 1.9)
mini_mime (~> 1.0)
google-logging-utils (0.2.0)
googleauth (1.17.1)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 4.0)
os (>= 0.9, < 2.0)
pstore (~> 0.1)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.21.1)
jwt (3.2.0)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.21.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.3.0)
optparse (0.8.1)
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
pstore (0.2.1)
public_suffix (7.0.5)
rake (13.4.2)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.8.0)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.22.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.28.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
base64
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
nkf
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-25
ruby
DEPENDENCIES
fastlane
fastlane-plugin-xcodegen
CHECKSUMS
CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261
abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263
atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1269.0) sha256=80beca6c8a35ddfc11b0b529424ea6167ace3da92cd7368847ce694679dedea5
aws-sdk-core (3.254.0) sha256=ee3e3220b8468a3c9e59daba18e6ec897bf5c7ce8adcc0670cfa2f1f092112fe
aws-sdk-kms (1.130.0) sha256=a2e83662ca31b77a2a19c9aa2f40a98165a67270c18c718fe1c70d0cbd7cd749
aws-sdk-s3 (1.227.0) sha256=552b23bf9a37c7db4957e9291543e61c8de886cf31d1d45ea3b429df0feea939
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9
digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8
emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b
excon (1.5.0) sha256=c503ad1d0123bc8ab2a062ff3789dc891ec368cb9e13765ab88a9c58c8bb6d50
faraday (1.10.6) sha256=7ff4802a6b312876a2241b3e641ce0d5045e168dd871b422c35b505e5261ad4d
faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb
faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689
faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750
faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940
faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b
faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757
faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682
faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335
faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7
faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0
faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa
faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9
fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20
fastlane (2.237.0) sha256=bb1e867bc070fb328741b5e6e7606d5ba596941a9ecca0478f60ef22d1009db9
fastlane-plugin-brew (0.1.1) sha256=c2d1c98ead82f78818621b5b1eb123e0bf9da7e1a7b3efeaac77a776725a7fbf
fastlane-plugin-xcodegen (1.1.0) sha256=9bbed6c2ea21ca6869567e350d0b62cf41770949d15e40869058d689f1a6d06d
fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847
gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939
google-apis-androidpublisher_v3 (0.104.0) sha256=3bf7f0dee23ae71e070e279848374834853204e304831cf6aa76fa435a4d6eff
google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee
google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106
google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254
google-apis-storage_v1 (0.64.0) sha256=75b11afa2edcee859b84c7a6972ee4456314eeef5f762827fd6cf5c5ffaf93f2
google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4
google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7
google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3
google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330
google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b
googleauth (1.17.1) sha256=0f7e6fc70e204cee1b2d71f1e1de2d3b349d432404197fe68ebf7fa23d0821b9
highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479
http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6
httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080
multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723
naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01
nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895
optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42
pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
retriable (3.8.0) sha256=9f2f1b0207594c7817f17f671587b8ec7587387ac6cebda6c941a802bb98a8e5
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7
signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780
simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b
terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea
terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48
tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50
tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542
uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc
unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7
xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc
xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892
xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93
BUNDLED WITH
4.0.3
+16
View File
@@ -0,0 +1,16 @@
# HealthChat Changes
Release notes for HealthChat builds of Element X. Each release adds a section at the
top; the release workflow extracts the one matching the version being released and
uses it as the TestFlight "What to Test" text.
The heading format is load-bearing — `.github/workflows/release-healthchat.yml`
matches `## Changes in <version>` exactly.
Upstream's `CHANGES.md` tracks element-hq's own releases and must not be edited here.
## Changes in 26.07.3 (2026-07-15)
🙌 Improvements
- First HealthChat release built from Element X.
+14
View File
@@ -55,6 +55,16 @@ struct RunTests: AsyncParsableCommand {
"com.apple.CoreSimulator.SimRuntime.iOS-\(runtime.replacingOccurrences(of: ".", with: "-"))" "com.apple.CoreSimulator.SimRuntime.iOS-\(runtime.replacingOccurrences(of: ".", with: "-"))"
} }
/// Test identifiers to pass to `-skip-testing`, read from the `CI_SKIP_TESTS` environment variable
/// (comma-separated, e.g. "VoiceMessageCacheTests"). Lets a workflow exclude a suite that only fails in
/// its environment without touching the shared test.
private var skippedTests: [String] {
(ProcessInfo.processInfo.environment["CI_SKIP_TESTS"] ?? "")
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
}
func run() async throws { func run() async throws {
if let createName = createSimulatorName { if let createName = createSimulatorName {
guard let createType = createSimulatorType else { guard let createType = createSimulatorType else {
@@ -142,6 +152,10 @@ struct RunTests: AsyncParsableCommand {
command += " -only-testing:\(scheme)/\(testName)" command += " -only-testing:\(scheme)/\(testName)"
} }
for skippedTest in skippedTests {
command += " -skip-testing:\(scheme)/\(skippedTest)"
}
command += " | \(formatter)" command += " | \(formatter)"
try await CI.run(.path("/bin/zsh"), ["-cu", command]) try await CI.run(.path("/bin/zsh"), ["-cu", command])
@@ -238,8 +238,8 @@ final class AuthenticationStartScreenViewModelTests {
} }
@Test @Test
func classicAppAccountRequiresBackup() async throws { func classicAppAccountRequiresBackupFallsThroughToLogin() async throws {
// Given a view model with a Classic app account that requires backup before signing in. // Given a view model with a Classic app account that has no usable key backup.
let classicAppAccount = makeClassicAppAccount() let classicAppAccount = makeClassicAppAccount()
await setupViewModel(classicAppAccount: classicAppAccount, availableSecrets: .requiresBackup) await setupViewModel(classicAppAccount: classicAppAccount, availableSecrets: .requiresBackup)
guard case .welcomeBack(let account) = context.viewState.classicAppMode else { guard case .welcomeBack(let account) = context.viewState.classicAppMode else {
@@ -248,27 +248,15 @@ final class AuthenticationStartScreenViewModelTests {
} }
#expect(account.state.availableSecrets == .requiresBackup) #expect(account.state.availableSecrets == .requiresBackup)
// When continuing with the Classic account while backup is required. // When continuing with the Classic account.
var deferred = deferFulfillment(context.observe(\.viewState.bindings.showClassicAppBackupInstructions)) { $0 } let deferred = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOAuth }
context.send(viewAction: .continueWithClassic(classicAppAccount)) context.send(viewAction: .continueWithClassic(classicAppAccount))
// Then the backup instructions should be shown. // Then the flow continues to a normal sign-in rather than the backup instructions sheet.
// HealthChat replaces the Classic app in place, so those instructions could never be followed.
try await deferred.fulfill() try await deferred.fulfill()
#expect(context.viewState.bindings.showClassicAppBackupInstructions == false)
// When the user completes the backup in the Classic app and the app returns to the foreground. #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "mxid:\(classicAppAccount.userID)")
classicAppManager?.availableSecretsForReturnValue = .complete
deferred = deferFulfillment(context.observe(\.viewState.bindings.showClassicAppBackupInstructions)) { !$0 }
notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
// Then the backup instructions sheet should be dismissed.
try await deferred.fulfill()
// When the user continues with the Classic account again.
let deferredAction = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOAuth }
context.send(viewAction: .continueWithClassic(classicAppAccount))
// Then the flow should continue the login process.
try await deferredAction.fulfill()
} }
// MARK: - Helpers // MARK: - Helpers
+21
View File
@@ -0,0 +1,21 @@
# HealthChat product identity.
# Included permanently from project.yml (this fork always builds as HealthChat).
# Only build-setting scalars belong here — XcodeGen concatenates arrays on include-merge,
# so entitlement arrays (associated-domains, app-groups, keychain-groups) are edited directly
# in ElementX/SupportingFiles/target.yml instead.
# NB: APP_NAME (internal PRODUCT_NAME / module name) is intentionally left as ElementX so that
# `@testable import ElementX` and $(PRODUCT_MODULE_NAME) keep working.
settings:
APP_DISPLAY_NAME: HealthChat
PRODUCTION_APP_NAME: HealthChat
APP_GROUP_IDENTIFIER: group.ch.healthchat
BASE_BUNDLE_IDENTIFIER: ch.healthchat.element
DEVELOPMENT_TEAM: 6G37K72W82
# The legacy element-ios HealthChat's identifiers. ClassicAppManager reads the account and
# crypto-store keys from these to migrate existing users on an in-place update. Upstream's
# defaults in app.yml point at Element's own (group.im.vector / im.vector.app.*), which this
# team cannot provision — repoint, never remove. Failure here is silent.
CLASSIC_APP_GROUP_IDENTIFIER: group.ch.healthchat
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER: ch.healthchat.element.encryption-manager-service
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER: "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared"
+42
View File
@@ -0,0 +1,42 @@
## Project configuration
PROJECT_PATH=ElementX.xcodeproj
SCHEME=ElementX
MAIN_TARGET=ElementX
NSE_TARGET=NSE
SHARE_EXTENSION_TARGET=ShareExtension
MAIN_BUNDLE_ID=ch.healthchat.element
NSE_BUNDLE_ID=ch.healthchat.element.nse
SHARE_EXTENSION_BUNDLE_ID=ch.healthchat.element.shareextension
## Build configuration
DERIVED_DATA_PATH=build
IPA_NAME=ElementX
ARCHIVE_PATH=out/ElementX.xcarchive
BUILD_OUTPUT_DIRECTORY=out
BUILD_LOG_DIRECTORY=build/Logs/Gym/
## Provisioning profile files
PROVISIONING_PROFILES_PATH=./provisioning_profiles/
MAIN_PROVISIONING_PROFILE_FILENAME=main.mobileprovision
NSE_PROVISIONING_PROFILE_FILENAME=nse.mobileprovision
SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME=share_extension.mobileprovision
## App Store code signing
APPSTORE_CODESIGNING_IDENTITY="Apple Distribution: E-HealthSolutions GmbH (6G37K72W82)"
APPSTORE_SIGNING_CERTIFICATE="Apple Distribution: E-HealthSolutions GmbH (6G37K72W82)"
APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER="HealthChat App Store"
APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER="HealthChat NSE: App Store"
APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="HealthChat Share Extension: App Store"
## Account information
TEAM_ID=6G37K72W82
# APPLE_ID is set as a shell environment variable on the machine running fastlane.
+5
View File
@@ -0,0 +1,5 @@
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile
apple_id ENV["APPLE_ID"]
team_id ENV["TEAM_ID"]
+154
View File
@@ -0,0 +1,154 @@
default_platform :ios
# No Xcode version pin: novaloop-mac-studio-001 carries a single, globally-selected Xcode, so
# whatever xcode-select points at is by definition the one we build with. Pinning bought nothing
# and risked everything — a version the runner lacks sends xcodes into a multi-GB authenticated
# download that no CI secret can satisfy, hanging the release. Change the runner's Xcode instead.
platform :ios do
#### Public ####
desc "Builds the App Store ipa and uploads it to TestFlight (internal testers only)"
# Named release_testflight to avoid colliding with fastlane's built-in :testflight action.
lane :release_testflight do |options|
# Accept inputs via options (local) or env vars (CI — lets the workflow pass multi-line
# release notes without shell-escaping them).
marketing_version = options[:marketing_version] || ENV["HEALTHCHAT_MARKETING_VERSION"]
release_notes = options[:release_notes] || ENV["HEALTHCHAT_RELEASE_NOTES"]
UI.user_error!("'marketing_version' is required (pass the option or set HEALTHCHAT_MARKETING_VERSION)") if marketing_version.to_s.empty?
# Apple caps CFBundleShortVersionString at three period-separated integers. The workflow's
# regex is laxer and accepts markers like 26.07.3-rc1, so catch it here — otherwise the run
# dies at the upload step after ~30 minutes of signing, looking exactly like a signing failure.
unless marketing_version =~ /\A\d+(\.\d+){0,2}\z/
UI.user_error!("'marketing_version' must be 1-3 dot-separated integers (got '#{marketing_version}'). " \
"Apple rejects anything else at upload. Put markers in the tag or release notes instead.")
end
build_number = options[:build_number] || generate_build_number
build_release(marketing_version: marketing_version, build_number: build_number)
upload_to_testflight(
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
skip_submission: true,
skip_waiting_for_build_processing: true,
changelog: release_notes,
distribute_external: false,
)
end
#### Private ####
desc "Builds the signed App Store ipa"
private_lane :build_release do |options|
marketing_version = options[:marketing_version]
build_number = options[:build_number]
UI.user_error!("'build_number' parameter is missing") if build_number.to_s.empty?
if is_ci
%w[APPSTORECONNECT_KEY_ID APPSTORECONNECT_KEY_ISSUER_ID APPSTORECONNECT_KEY_CONTENT].each do |key|
UI.user_error!("'#{key}' environment variable should be set to use this lane") if ENV[key].to_s.empty?
end
app_store_connect_api_key(
key_id: ENV["APPSTORECONNECT_KEY_ID"],
issuer_id: ENV["APPSTORECONNECT_KEY_ISSUER_ID"],
key_content: ENV["APPSTORECONNECT_KEY_CONTENT"]
)
else
UI.user_error!("'APPLE_ID' environment variable should be set to use this lane") if ENV["APPLE_ID"].to_s.empty?
end
team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id)
UI.user_error!("Failed to fetch the team id from the Appfile") if team_id.to_s.empty?
# Inject the version at build time rather than rewriting project.yml, so no tracked file ever
# carries a release-only value. All three targets resolve CFBundleShortVersionString and
# CFBundleVersion from these settings, which keeps the app and its extensions consistent —
# App Store rejects an upload where they disagree.
xcargs_hash = {
"MARKETING_VERSION" => marketing_version,
"CURRENT_PROJECT_VERSION" => build_number,
"CODE_SIGN_STYLE" => "Manual",
}
xcargs = xcargs_hash.map { |k, v| "#{k}=#{v.to_s.shellescape}" }.join(" ")
xcodegen(spec: "project.yml")
clear_derived_data(derived_data_path: ENV["DERIVED_DATA_PATH"])
download_provisioning_profiles
disable_automatic_code_signing(path: ENV["PROJECT_PATH"])
update_project_provisioning_profiles
build_app(
clean: true,
scheme: ENV["SCHEME"],
xcargs: xcargs,
export_method: "app-store",
derived_data_path: ENV["DERIVED_DATA_PATH"],
archive_path: ENV["ARCHIVE_PATH"],
output_directory: ENV["BUILD_OUTPUT_DIRECTORY"],
output_name: "#{ENV['IPA_NAME']}.ipa",
buildlog_path: ENV["BUILD_LOG_DIRECTORY"],
codesigning_identity: ENV["APPSTORE_CODESIGNING_IDENTITY"],
skip_profile_detection: true,
export_options: {
method: "app-store",
signingStyle: "manual",
teamID: team_id,
signingCertificate: ENV["APPSTORE_SIGNING_CERTIFICATE"],
provisioningProfiles: {
ENV["MAIN_BUNDLE_ID"] => ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"],
ENV["NSE_BUNDLE_ID"] => ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"],
ENV["SHARE_EXTENSION_BUNDLE_ID"] => ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"],
},
},
)
end
desc "Downloads the three App Store provisioning profiles"
private_lane :download_provisioning_profiles do
{
ENV["MAIN_BUNDLE_ID"] => [ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"], ENV["MAIN_PROVISIONING_PROFILE_FILENAME"]],
ENV["NSE_BUNDLE_ID"] => [ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"], ENV["NSE_PROVISIONING_PROFILE_FILENAME"]],
ENV["SHARE_EXTENSION_BUNDLE_ID"] => [ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"], ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"]],
}.each do |bundle_id, (profile_name, filename)|
get_provisioning_profile(
app_identifier: bundle_id,
provisioning_name: profile_name,
ignore_profiles_with_different_name: true,
adhoc: false,
skip_certificate_verification: false,
output_path: ENV["PROVISIONING_PROFILES_PATH"],
filename: filename,
readonly: true,
)
end
end
desc "Points each target at its downloaded provisioning profile"
private_lane :update_project_provisioning_profiles do
{
ENV["MAIN_TARGET"] => ENV["MAIN_PROVISIONING_PROFILE_FILENAME"],
ENV["NSE_TARGET"] => ENV["NSE_PROVISIONING_PROFILE_FILENAME"],
ENV["SHARE_EXTENSION_TARGET"] => ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
}.each do |target, filename|
update_project_provisioning(
xcodeproj: ENV["PROJECT_PATH"],
profile: "#{ENV['PROVISIONING_PROFILES_PATH']}#{filename}",
# Anchored: target_filter is a regex, and an unanchored "NSE" or "ElementX" would also
# match any future target containing that substring.
target_filter: "^#{target}$",
build_configuration: "Release",
)
end
end
end
# Generates a build number from the current timestamp. Monotonic across runs, so a retry never
# collides with a build number App Store Connect has already seen.
def generate_build_number
require 'date'
DateTime.now.strftime("%Y%m%d%H%M%S")
end
+5
View File
@@ -0,0 +1,5 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-xcodegen'
+1 -1
View File
@@ -68,7 +68,7 @@ include:
- path: UITests/SupportingFiles/target.yml - path: UITests/SupportingFiles/target.yml
- path: IntegrationTests/SupportingFiles/target.yml - path: IntegrationTests/SupportingFiles/target.yml
- path: AccessibilityTests/SupportingFiles/target.yml - path: AccessibilityTests/SupportingFiles/target.yml
# - path: MyAppVariant/override.yml - path: Variants/HealthChat/healthchat.yml
packages: packages:
# Element/Matrix dependencies # Element/Matrix dependencies