mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-09 07:20:07 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 558802b552 | |||
| 7a37282d0b | |||
| 728ace6f81 | |||
| e4d4541c75 | |||
| 0588cdc542 | |||
| 68ef01cc87 | |||
| a7f8f667ff | |||
| 16c6727d29 | |||
| 01dc703b48 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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) }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -13,24 +13,71 @@ 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.
|
||||||
|
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 +91,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 +107,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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -9875,7 +9899,7 @@
|
|||||||
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;
|
||||||
@@ -9950,7 +9974,7 @@
|
|||||||
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.
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...
|
||||||
|
|||||||
+1
-1
@@ -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,14 +17,12 @@
|
|||||||
<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/>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
||||||
<string>$(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,23 +118,19 @@ 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) (group.im.vector) — owned by Element's team, not provisionable here.
|
||||||
- $(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:
|
||||||
|
# HealthChat: dropped $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER) (im.vector.app.*) — not provisionable here.
|
||||||
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
||||||
- $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,445 @@
|
|||||||
|
# HealthChat (element-x-ios) — App Store release capability
|
||||||
|
|
||||||
|
**Date:** 2026-07-15
|
||||||
|
**Branch:** `release/healthchat`
|
||||||
|
**Status:** Design approved; revised after adversarial review (26 issues, 0 rejected)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`Novaloop-AG/element-x-ios` is the HealthChat rebrand of Element X. The rebrand landed in `01dc703b4`
|
||||||
|
(branding, fixed homeserver, variant seam). What it cannot do is ship: the repo has **no release machinery
|
||||||
|
of any kind** — no `fastlane/`, no `Gemfile`, and none of its 17 GitHub workflows build or upload anything.
|
||||||
|
|
||||||
|
The legacy fork `Novaloop-AG/element-ios` ships HealthChat to the App Store today via a proven pipeline
|
||||||
|
(last green run 2026-04-15, version 1.4.1). This design brings equivalent capability to element-x-ios and
|
||||||
|
captures the process in a skill.
|
||||||
|
|
||||||
|
Both apps use **the same bundle id** (`ch.healthchat.element`) and **the same Apple team** (`6G37K72W82`).
|
||||||
|
element-x therefore reaches the App Store as an **in-place update** of the existing HealthChat record. That
|
||||||
|
single fact drives most of this design — including the fact that the two apps can never coexist on a
|
||||||
|
device, which upstream's migration UX assumes they can.
|
||||||
|
|
||||||
|
### Why upstream's own release path is unavailable
|
||||||
|
|
||||||
|
element-hq ships element-x via Xcode Cloud. `ci_scripts/ci_post_clone.sh` branches on `$CI_WORKFLOW` and
|
||||||
|
calls `swift run pipeline update-foss-secrets` (`Tools/Sources/Commands/CI/ConfigureProduction.swift:8`).
|
||||||
|
`pipeline` lives only in the private `Enterprise` submodule (`.gitmodules` → `element-hq/element-ios-enterprise`;
|
||||||
|
`Enterprise/` is empty here and Novaloop cannot obtain it). Separately `ci_scripts/ci_post_xcodebuild.sh:13`
|
||||||
|
unconditionally uploads dSYMs to Element's Sentry org. Xcode Cloud is a dead end for this fork.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| # | Decision | Rationale |
|
||||||
|
|---|---|---|
|
||||||
|
| D1 | **In-place update** of the existing App Store record | Identity already aligns. `26.07.3` already clears the legacy's `1.4.1`, so no version bump is needed. |
|
||||||
|
| D2 | **Preserve** legacy sessions + E2EE keys | The migration code already ships; the fix is 3 scalars + 1 entitlement, all HealthChat's own provisionable ids. |
|
||||||
|
| D3 | **Port the legacy fastlane** (slim, purpose-built) | Xcode Cloud is blocked. The legacy pipeline is proven and reuses the identical team, cert and ASC key. |
|
||||||
|
| D4 | Migration fix ships **first, as its own PR** | It corrects a defect in `01dc703b4`. **Note: it is _not_ monotonic — see D9.** |
|
||||||
|
| D5 | Skill lives at `.agents/skills/releasing-healthchat/SKILL.md` | Reachable via the existing `.claude/skills -> ../.agents/skills` symlink. A new subdirectory cannot conflict on rebase. |
|
||||||
|
| D6 | Reuse the legacy's `HEALTHCHAT_*` secrets | Same team, cert and ASC key. **Reject fastlane match** — the legacy deliberately has none. |
|
||||||
|
| D7 | Keep the `healthchat-v*` tag namespace | Upstream owns `release/<version>` (`ReleaseToGithub.swift:83-85`) and `nightly/*`. |
|
||||||
|
| D8 | **Create** `HEALTHCHAT_CHANGES.md` as the release-notes source | element-x's `CHANGES.md` is owned by upstream's `release-to-github`. The file does **not exist** in this repo — it must be created, not carried over. |
|
||||||
|
| D9 | Fix the `.requiresBackup` dead-end with a **small upstream edit** | Accepts the first-ever edit to an upstream logic file. Required for D4 to be safe — see Part 1. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1 — Classic migration fix (PR1, `pr-bugfix`)
|
||||||
|
|
||||||
|
### The defect
|
||||||
|
|
||||||
|
`01dc703b4` dropped `$(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)` from the entitlements with the
|
||||||
|
rationale "not provisionable here". That was correct *about Element's identifiers* — but the fix is to
|
||||||
|
**repoint them at HealthChat's own**, not to remove them.
|
||||||
|
|
||||||
|
Left as-is, every existing user who updates is silently logged out and loses local encrypted history
|
||||||
|
unless they had key backup. `loadAccounts()` throws `invalidAppGroupIdentifier`, caught and logged only —
|
||||||
|
`AuthenticationService.swift:59-63`: *"No need to alert the user of the failure, just log it."*
|
||||||
|
|
||||||
|
### Why the fix is small
|
||||||
|
|
||||||
|
The plumbing already ships. `ElementX/SupportingFiles/target.yml:78-81` maps the values into `Info.plist`;
|
||||||
|
`InfoPlistReader.swift:28-31,129-143` reads them. `ClassicAppManager` reads exactly `accountIv`,
|
||||||
|
`accountAesKey`, `cryptoSDKStoreKey` (`ClassicAppManager.swift:31-35,61-68`) — and the legacy
|
||||||
|
`EncryptionKeyManager` writes exactly those three names, to service
|
||||||
|
`ch.healthchat.element.encryption-manager-service`, in access group
|
||||||
|
`$(AppIdentifierPrefix)ch.healthchat.element.keychain.shared`
|
||||||
|
(`element-ios/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift:17,20-25,27`).
|
||||||
|
|
||||||
|
Only the scalars are wrong: `app.yml:11-13` points them at Element's `group.im.vector` / `im.vector.app.*`,
|
||||||
|
and `healthchat.yml` does not override them.
|
||||||
|
|
||||||
|
**Verified mechanics** (these were the design's load-bearing assumptions):
|
||||||
|
- **XcodeGen scalar precedence.** `healthchat.yml` is included after `app.yml` (`project.yml:71` vs `:61`)
|
||||||
|
and wins — proven by the already-generated pbxproj, where `APP_GROUP_IDENTIFIER = group.ch.healthchat`
|
||||||
|
and `BASE_BUNDLE_IDENTIFIER = ch.healthchat.element` beat app.yml's Element values.
|
||||||
|
- **No trailing-dot bug.** `$(AppIdentifierPrefix)` carries its own trailing dot, so the legacy resolves to
|
||||||
|
`6G37K72W82.ch.healthchat.element.keychain.shared`. `$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared`
|
||||||
|
is byte-equivalent and matches upstream's own convention (`app.yml:13`).
|
||||||
|
|
||||||
|
### The change
|
||||||
|
|
||||||
|
In `Variants/HealthChat/healthchat.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
In `ElementX/SupportingFiles/target.yml`, restore the classic keychain group to `keychain-access-groups`
|
||||||
|
and correct the misleading comment at `:132`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
keychain-access-groups:
|
||||||
|
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
||||||
|
# HealthChat: the legacy app's keychain group — ClassicAppManager reads the account + crypto store
|
||||||
|
# keys from here to migrate existing users. Same team prefix, so it is provisionable.
|
||||||
|
- $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do not** re-add `$(CLASSIC_APP_GROUP_IDENTIFIER)` to `com.apple.security.application-groups`: it resolves
|
||||||
|
to `group.ch.healthchat`, already the value of `$(APP_GROUP_IDENTIFIER)`, so it would duplicate the entry.
|
||||||
|
Correct the comment at `:128` to say this rather than implying the group was dropped as un-provisionable.
|
||||||
|
|
||||||
|
`CLASSIC_APP_DEEP_LINK_URL` needs no override — legacy HealthChat registers the `element` scheme
|
||||||
|
(`element-ios/Config/AppIdentifiers.xcconfig:22`) — but see the `.requiresBackup` fix below, which makes it
|
||||||
|
moot.
|
||||||
|
|
||||||
|
**Finally: run `xcodegen` and commit both `ElementX.xcodeproj/project.pbxproj` and
|
||||||
|
`ElementX/SupportingFiles/ElementX.entitlements`.** Both are tracked and not ignored. Without this the
|
||||||
|
checked-in entitlements still lack the classic group and anyone building the branch gets the un-fixed,
|
||||||
|
silently-failing behaviour.
|
||||||
|
|
||||||
|
### The `.requiresBackup` fix (D9) — why Part 1 is not monotonic
|
||||||
|
|
||||||
|
`availableSecrets` maps `.withoutBackup` / `.unusedBackup` → `.requiresBackup`
|
||||||
|
(`ClassicAppManager.swift:82-85`). `login(classicAppAccount:)` routes that to
|
||||||
|
`state.bindings.showClassicAppBackupInstructions`, whose sheet's only button fires `.openClassicApp` →
|
||||||
|
`appMediator.open(element://open)` (`AuthenticationStartScreenViewModel.swift:106`).
|
||||||
|
|
||||||
|
**That URL resolves to nothing.** element-x registers only `$(BASE_BUNDLE_IDENTIFIER)` and `matrix`
|
||||||
|
(`ElementX/SupportingFiles/target.yml:60-63`) — no `element` scheme — and the legacy app that *did*
|
||||||
|
register it has been replaced in place by D1's same-bundle-id update. Upstream's UX assumes Element Classic
|
||||||
|
(`im.vector.app`) and Element X (`io.element.elementx`) coexist as separate apps. HealthChat cannot.
|
||||||
|
|
||||||
|
So making `loadAccounts()` succeed switches on a UI branch that is worse than the status quo for
|
||||||
|
`.requiresBackup` users: today they see a working login screen; after the fix they see a modal urging them
|
||||||
|
to open an app that no longer exists. The sheet is dismissible (`@Environment(\.dismiss)`) and `otherOptions`
|
||||||
|
(`AuthenticationClassicAppAccountView.swift:106`) is an escape hatch, so it is not a hard lockout — but it
|
||||||
|
is a knowingly broken call-to-action.
|
||||||
|
|
||||||
|
**Fix:** in `AuthenticationStartScreenViewModel.login(classicAppAccount:)`, treat `.requiresBackup` the same
|
||||||
|
way `.unavailable` is already treated — skip the sheet and fall through to
|
||||||
|
`configureAccountProvider(account.serverName, loginHint: "mxid:\(account.userID)", fallbackHomeserverURL:)`.
|
||||||
|
Those users sign into their own account and re-verify the device, losing only history that by definition has
|
||||||
|
no backup.
|
||||||
|
|
||||||
|
This is the **first edit to an upstream logic file** in this fork, ending the rebrand's "no upstream logic
|
||||||
|
edits" property. It is ~3 lines in a method upstream rarely touches. Record it in the skill's divergence
|
||||||
|
section so a future rebase does not silently revert it.
|
||||||
|
|
||||||
|
### Precondition for D2 (verified, but confirm on device)
|
||||||
|
|
||||||
|
`AuthenticationStartScreenViewModel:46` gates the whole classic flow on
|
||||||
|
`appSettings.accountProviders.contains(serverName)`, where `serverName` is derived from the **MXID domain**
|
||||||
|
(`ClassicAppAccountManager.swift:107-111`), not the homeserver URL. HealthChat pins
|
||||||
|
`accountProviders: ["healthchat.ch"]`.
|
||||||
|
|
||||||
|
`curl https://healthchat.ch/.well-known/matrix/server` → `{"m.server": "matrix.healthchat.ch:8448"}`. This is
|
||||||
|
*server-name delegation*: the server is **named** `healthchat.ch` and merely reachable at
|
||||||
|
`matrix.healthchat.ch:8448`. Legacy MXIDs are therefore `@user:healthchat.ch`, the gate passes, and D2 holds.
|
||||||
|
Confirm against a real legacy MXID during device testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2 — Release pipeline (PR2, `pr-build`)
|
||||||
|
|
||||||
|
### Design note: this fork owns `fastlane/` outright
|
||||||
|
|
||||||
|
Upstream element-x-ios has **no `fastlane/` directory**. The legacy skill devotes a whole section to rebase
|
||||||
|
divergence in `fastlane/.env.default` and `fastlane/Fastfile` precisely because those are upstream-owned in
|
||||||
|
element-ios. Here that class of conflict does not exist — which is why this is a slim purpose-built Fastfile
|
||||||
|
(~120 lines), not a copy of Vector's ~600-line multi-variant one.
|
||||||
|
|
||||||
|
**Dropped:** cocoapods (element-x is SPM), diawi, sentry (HealthChat has no Sentry), and the `adhoc` /
|
||||||
|
`alpha` / `setup_app_variant` lanes. Siri-intents and broadcast-upload have no counterpart target here.
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `fastlane/Fastfile` | `before_all` (Xcode pin) + public `release_testflight` + private helpers |
|
||||||
|
| `fastlane/Appfile` | `apple_id ENV["APPLE_ID"]`, `team_id ENV["TEAM_ID"]` |
|
||||||
|
| `fastlane/.env.default` | Project/target/bundle-id/profile configuration (full key list below) |
|
||||||
|
| `fastlane/Pluginfile` | **`fastlane-plugin-xcodegen` only** |
|
||||||
|
| `Gemfile` + `Gemfile.lock` | `fastlane` + plugins. No cocoapods, no slather. **Commit the lock** — the workflow's gem cache keys on `hashFiles('**/Gemfile.lock')`; without it the key is constant and fastlane silently re-resolves between two runs of the same tag. |
|
||||||
|
| `Brewfile` | `xcodesorg/made/xcodes`. **Not** `xcodegen` — see runner notes. |
|
||||||
|
| `HEALTHCHAT_CHANGES.md` | **New file** (D8). Seed with a `## Changes in 26.07.3 (<date>)` section matching the awk pattern. |
|
||||||
|
| `.gitignore` | Add `out/` and **`provisioning_profiles/`** (signing material — security-relevant). `build` (`:35`) and `fastlane/report.xml` (`:28`) are already covered upstream. |
|
||||||
|
|
||||||
|
**Drop `fastlane-plugin-versioning`.** The legacy carries it but never uses it — its `update_marketing_version`
|
||||||
|
is raw `File.read`/`gsub!`, and no `increment_version_number` / `get_version_number` / `update_info_plist`
|
||||||
|
call exists anywhere. Copying it into a file we call "slim and purpose-built" would propagate dead weight.
|
||||||
|
|
||||||
|
### `.env.default`
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
||||||
|
DERIVED_DATA_PATH=build
|
||||||
|
IPA_NAME=ElementX
|
||||||
|
ARCHIVE_PATH=out/ElementX.xcarchive
|
||||||
|
BUILD_OUTPUT_DIRECTORY=out
|
||||||
|
BUILD_LOG_DIRECTORY=build/Logs/Gym/
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
TEAM_ID=6G37K72W82
|
||||||
|
# APPLE_ID is a machine-level shell variable on the runner, not a repo secret.
|
||||||
|
```
|
||||||
|
|
||||||
|
`APPSTORE_CODESIGNING_IDENTITY` and `APPSTORE_SIGNING_CERTIFICATE` are **distinct keys** in the legacy — both
|
||||||
|
are needed. The `out/ElementX.ipa` artifact path is only true because `BUILD_OUTPUT_DIRECTORY`/`IPA_NAME` are
|
||||||
|
set here.
|
||||||
|
|
||||||
|
Verified: bundle ids from `NSE/SupportingFiles/target.yml:75` and `ShareExtension/SupportingFiles/target.yml:77`;
|
||||||
|
schemes/targets from `ElementX/SupportingFiles/target.yml:3-4,41`, `NSE/SupportingFiles/target.yml:3-4,30`,
|
||||||
|
`ShareExtension/SupportingFiles/target.yml:3-4,30`.
|
||||||
|
|
||||||
|
### `release_testflight` lane
|
||||||
|
|
||||||
|
Mirrors `element-ios/fastlane/Fastfile:199-224`: accept inputs via options (local) or env vars (CI, avoiding
|
||||||
|
shell-escaping of multi-line notes), then:
|
||||||
|
|
||||||
|
1. `app_store_connect_api_key(...)` from `APPSTORECONNECT_KEY_{ID,ISSUER_ID,CONTENT}` when `is_ci`.
|
||||||
|
2. `generate_build_number` → `YYYYMMDDHHMMSS` (ported from `Fastfile:562`). Monotonic; makes retries free.
|
||||||
|
3. `xcodegen(spec: "project.yml")`.
|
||||||
|
4. `download_provisioning_profiles` → three profiles (main, NSE, ShareExtension).
|
||||||
|
5. `disable_automatic_code_signing` + `update_project_provisioning` per target.
|
||||||
|
6. `build_app`, `export_method: "app-store"`, `CODE_SIGN_STYLE=Manual`.
|
||||||
|
7. `upload_to_testflight(skip_submission: true, distribute_external: false, changelog: release_notes)`.
|
||||||
|
|
||||||
|
**Version injection.** element-x holds `MARKETING_VERSION: 26.07.3` / `CURRENT_PROJECT_VERSION: 1` in
|
||||||
|
`project.yml:56-57`, consumed by XcodeGen at *generate* time (unlike the legacy's `AppVersion.xcconfig`, read
|
||||||
|
at *build* time). Pass both via `xcargs` to `build_app` instead of rewriting `project.yml`. Verified sound:
|
||||||
|
NSE (`:51-52,76-77`) and ShareExtension (`:46-47,78-79`) both resolve their version keys from
|
||||||
|
`$(MARKETING_VERSION)`/`$(CURRENT_PROJECT_VERSION)` at build time, and xcodebuild command-line settings
|
||||||
|
override project *and* target level uniformly — so all three targets stay version-consistent. This matters:
|
||||||
|
App Store rejects an upload whose extension version disagrees with the app's.
|
||||||
|
|
||||||
|
This spares `project.yml`, but **it does not mean "no tracked file is mutated"** — `ElementX.xcodeproj/project.pbxproj`
|
||||||
|
is tracked and steps 3 and 5 rewrite it (xcodegen regenerates; `disable_automatic_code_signing` /
|
||||||
|
`update_project_provisioning` write Manual signing and profile UUIDs). The pbxproj footgun **moved, it did not
|
||||||
|
vanish**. The skill must say: never commit the post-lane tree; `git checkout -- ElementX.xcodeproj`.
|
||||||
|
|
||||||
|
### GitHub workflow
|
||||||
|
|
||||||
|
`.github/workflows/release-healthchat.yml` — near-copy of the legacy's, which is already well-built (pinned
|
||||||
|
action SHAs, `contents: read`, concurrency group, 90-minute timeout, awk notes extraction, artifact upload).
|
||||||
|
|
||||||
|
Changes from the legacy:
|
||||||
|
|
||||||
|
- **`submodules: false`** — critical. element-x-ios's only submodule is the **private**
|
||||||
|
`element-hq/element-ios-enterprise` over SSH; the legacy's `submodules: 'true'` would fail checkout on step
|
||||||
|
one of every run.
|
||||||
|
- Drop the CocoaPods cache and `rm -rf Pods/MatrixSDK`.
|
||||||
|
- Drop `MX_GIT_BRANCH` (legacy `:41`) — no `mx_git_branch` helper exists in the slim Fastfile.
|
||||||
|
- Re-evaluate `fetch-depth: 0` (legacy `:48`); keep only if something needs history.
|
||||||
|
- Keep the explicit `brew bundle` step (legacy `:115`).
|
||||||
|
- `bundle exec fastlane release_testflight`; artifacts `out/ElementX.ipa`, `out/ElementX.app.dSYM.zip`.
|
||||||
|
- Harden the notes step with `[[ -f HEALTHCHAT_CHANGES.md ]]`. The legacy's "warning fallback" only fires on
|
||||||
|
an *empty match*, not an absent file — under `set -euo pipefail` a missing file aborts the job.
|
||||||
|
|
||||||
|
**Do not carry `HEALTHCHAT_KEYCHAIN_PASSWORD` forward as mandatory.** The legacy skill lists it among six
|
||||||
|
required secrets and the legacy workflow references it, but it exists at neither repo nor org level — and the
|
||||||
|
pipeline runs green regardless. `apple-actions/import-codesign-certs` tolerates its absence. **Five secrets is
|
||||||
|
the real requirement.**
|
||||||
|
|
||||||
|
### Runner
|
||||||
|
|
||||||
|
`novaloop-mac-studio-001` — org-level, online, labels `[self-hosted, macOS, ARM64]`; already satisfies
|
||||||
|
`runs-on: [self-hosted, macOS]`. Confirm the org runner group's visibility includes element-x-ios (two
|
||||||
|
`Default` groups exist, one `all`, one `selected`).
|
||||||
|
|
||||||
|
**Xcode pin: use `"26.4"`.** The runner has it and the legacy pipeline proves it works.
|
||||||
|
`ci_scripts/ci_common.sh:34` (`sudo xcode-select -s /Applications/Xcode_26.5.0.app`) is an **Xcode Cloud image
|
||||||
|
path, not an `xcodes` version identifier**, and is not evidence the runner has 26.5. If element-x turns out to
|
||||||
|
need 26.5, install it on the runner manually **first** — otherwise `xcodes` attempts a multi-GB authenticated
|
||||||
|
download needing `XCODES_USERNAME`/`XCODES_PASSWORD`, which none of the five secrets supply, and hangs in
|
||||||
|
`before_all`.
|
||||||
|
|
||||||
|
Also required on the runner: `xcodegen` on PATH (installed manually once, deliberately **not** in `Brewfile`
|
||||||
|
so `brew bundle` cannot churn it), Homebrew, Ruby + bundler, and `APPLE_ID` exported as a machine-level shell
|
||||||
|
variable (the `Appfile` reads it; it is not repo-carried).
|
||||||
|
|
||||||
|
### Secrets to copy
|
||||||
|
|
||||||
|
element-x-ios has **zero** repository secrets. Copy these five from `Novaloop-AG/element-ios`:
|
||||||
|
`HEALTHCHAT_ASC_KEY_ID`, `HEALTHCHAT_ASC_KEY_ISSUER_ID`, `HEALTHCHAT_ASC_KEY_CONTENT`,
|
||||||
|
`HEALTHCHAT_CERTIFICATES_P12`, `HEALTHCHAT_CERTIFICATES_P12_PASSWORD`.
|
||||||
|
|
||||||
|
Requires the `snowping` GitHub account — `Novaloop-AG/element-x-ios` is INTERNAL and invisible to
|
||||||
|
`joel-poyet_GALGH`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3 — Release skill (PR3, `pr-doc`)
|
||||||
|
|
||||||
|
`.agents/skills/releasing-healthchat/SKILL.md`, reachable as a project skill through the existing
|
||||||
|
`.claude/skills -> ../.agents/skills` symlink.
|
||||||
|
|
||||||
|
Mirror the legacy skill's shape — it is a good model: two-field frontmatter (`name`, `description`), terse
|
||||||
|
imperative prose, checklists and tables, **rationale inline on every prohibition**. Sections: preflight,
|
||||||
|
cutting a release, `workflow_dispatch` retry path, tag format, release notes, post-release checks, required
|
||||||
|
secrets, runner requirements, certificate renewal, TestFlight → App Store promotion, rollback, divergence.
|
||||||
|
|
||||||
|
Corrected for this repo:
|
||||||
|
|
||||||
|
- **Five** required secrets, not six.
|
||||||
|
- Version source is `project.yml:56`, not `Config/AppVersion.xcconfig`.
|
||||||
|
- Three profiles (main, NSE, **ShareExtension**), not main/NSE/broadcast.
|
||||||
|
- **Never commit the post-lane tree** — `git checkout -- ElementX.xcodeproj` after a local run.
|
||||||
|
- **Test versions must be numeric-only** (see Verification) — this trap costs 30 minutes per hit.
|
||||||
|
- The divergence section shrinks: `fastlane/` and the release workflow are fork-only here, so the legacy's
|
||||||
|
`--ours` guidance for `.env.default`/`Fastfile` does not apply. What *does* need calling out:
|
||||||
|
`automatic-calendar-version.yml` and `post-release.yml` are gated on
|
||||||
|
`github.repository == 'element-hq/element-x-ios'` and silently no-op in the fork, so rebases drag
|
||||||
|
`project.yml`'s version along — **the tag is the source of truth**.
|
||||||
|
- **New section — the migration contract** (no legacy equivalent): `CLASSIC_APP_*` in `healthchat.yml` is
|
||||||
|
load-bearing for existing users and fails *silently*; it must never be "cleaned up" to upstream's values.
|
||||||
|
The `.requiresBackup` edit in `AuthenticationStartScreenViewModel` is a deliberate upstream divergence — a
|
||||||
|
rebase that reverts it reintroduces a broken CTA. Both apps share the `group.ch.healthchat` UserDefaults
|
||||||
|
suite (`AppSettings.swift:56`), so unqualified `@UserPreference` property names land in a suite the legacy
|
||||||
|
also writes to — future key collisions are cheap to introduce.
|
||||||
|
|
||||||
|
Add a 2-line delegation in `AGENTS.md` pointing at the skill rather than duplicating the runbook — the legacy
|
||||||
|
does exactly this (`element-ios/CLAUDE.md:288-290`). element-x's `AGENTS.md` has no release section today, and
|
||||||
|
per its own "keep current" rule this is a new fact about the repo, so it belongs there, in caveman voice.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Landing plan
|
||||||
|
|
||||||
|
`Dangerfile.swift:50-51` **fails** the PR unless it carries exactly one `pr-` label. (The empty-body rule
|
||||||
|
`:24` and the >1000-additions rule `:18-19` are `warn`s, not fails.)
|
||||||
|
|
||||||
|
| PR | Contents | Label |
|
||||||
|
|---|---|---|
|
||||||
|
| PR1 | Part 1 — `CLASSIC_APP_*`, entitlement, `.requiresBackup` edit, regenerated pbxproj | `pr-bugfix` |
|
||||||
|
| PR2 | Part 2 — fastlane, workflow, Gemfile(.lock), Brewfile, .gitignore, HEALTHCHAT_CHANGES.md | `pr-build` |
|
||||||
|
| PR3 | Part 3 — skill + AGENTS.md delegation | `pr-doc` |
|
||||||
|
|
||||||
|
AGENTS.md's 500-addition guidance argues for keeping these separate rather than combining PR2 and PR3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Apple Developer portal prerequisites
|
||||||
|
|
||||||
|
Manual, one-time, **blocking** — no signed archive is possible until all three are done.
|
||||||
|
|
||||||
|
1. **Create App ID `ch.healthchat.element.shareextension`** with App Groups + Keychain Sharing, then the
|
||||||
|
`HealthChat Share Extension: App Store` profile. Genuinely new: the legacy provisioned
|
||||||
|
`ch.healthchat.element.shareExtension` — **camelCase, a different string** — and disabled the target anyway
|
||||||
|
(`element-ios/fastlane/.env.default`: `SHARE_EXTENSION_TARGET=`). element-x embeds ShareExtension
|
||||||
|
unconditionally (`ElementX/SupportingFiles/target.yml:227-228`).
|
||||||
|
2. **Add `com.apple.developer.usernotifications.communication`** to `ch.healthchat.element`; regenerate
|
||||||
|
`HealthChat App Store`. New vs. legacy (absent from `Riot/SupportingFiles/Riot.entitlements`).
|
||||||
|
3. **Add `com.apple.developer.usernotifications.filtering`** to `ch.healthchat.element.nse`; regenerate
|
||||||
|
`HealthChat NSE: App Store`. New vs. legacy (`RiotNSE.entitlements` has only app-groups + keychain).
|
||||||
|
|
||||||
|
The `HealthChat Broadcast Upload Extension: App Store` profile becomes orphaned — leave it; the legacy
|
||||||
|
pipeline still uses it.
|
||||||
|
|
||||||
|
Certificate: **not due.** `HEALTHCHAT_CERTIFICATES_P12` was updated 2026-04-07 and the workflow ran green
|
||||||
|
2026-04-15. Apple Distribution certs last one year → renewal due around **April 2027**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Part 1 — device-only
|
||||||
|
|
||||||
|
**Step 0 (or the test silently false-negatives).** `AuthenticationService.swift:49-52` short-circuits
|
||||||
|
`loadAccounts()` entirely when `appSettings.hasSignedInBefore` — which lives in the `group.ch.healthchat`
|
||||||
|
UserDefaults suite (`AppSettings.swift:56`), survives app deletion via the shared container, and is never
|
||||||
|
cleared. **Any dev device that has previously run a HealthChat element-x build will show a plain login screen
|
||||||
|
while the fix works perfectly.** Delete the app before step 1. A `"skipping loadAccounts"` log line means the
|
||||||
|
run is **void, not failed**.
|
||||||
|
|
||||||
|
1. Install the *shipping* legacy HealthChat on a physical device. Sign in. Send an encrypted message so the
|
||||||
|
crypto store is populated.
|
||||||
|
2. Build element-x with the fix and install **over it** — same bundle id → in-place update. **Do not delete
|
||||||
|
the app first**; that destroys the keychain and container and invalidates the test.
|
||||||
|
3. Expected: the app finds the account and migrates without a re-login.
|
||||||
|
4. Confirm the legacy MXID is `@user:healthchat.ch` (the D2 precondition).
|
||||||
|
5. Exercise **both** secrets paths if possible: `.complete` (migrates) and `.requiresBackup` (must fall
|
||||||
|
through to sign-in, never the backup sheet).
|
||||||
|
6. Watch the log for `ClassicAppManagerError` — the failure is silent in the UI.
|
||||||
|
7. **Send a push notification to the migrated device.** Part 2 adds two brand-new notification entitlements
|
||||||
|
and a first-ever NSE profile; nothing else exercises delivery, and upload proves signing, not push.
|
||||||
|
|
||||||
|
### Part 2 — pipeline
|
||||||
|
|
||||||
|
Use `workflow_dispatch` with a throwaway version, as the legacy skill prescribes for cert renewal:
|
||||||
|
|
||||||
|
1. Run "Release HealthChat" manually with version **`26.7.99`** and a release-notes override.
|
||||||
|
**The version must be numeric-only.** It lands verbatim in `CFBundleShortVersionString` (`Info.plist:36-37`)
|
||||||
|
and Apple requires ≤3 period-separated integers. A marker like `26.07.3-pipeline-test` passes the
|
||||||
|
workflow's own regex (`:59`) but earns an **ITMS rejection at the final step, after ~30 minutes of
|
||||||
|
signing** — indistinguishable from a real signing failure. Put markers in the tag or release notes only.
|
||||||
|
2. Green run → build appears in App Store Connect → TestFlight within ~10 minutes.
|
||||||
|
3. Discard the build.
|
||||||
|
|
||||||
|
Do **not** verify by submitting to the App Store. TestFlight acceptance proves signing, entitlements and
|
||||||
|
upload; review proves nothing extra about the pipeline.
|
||||||
|
|
||||||
|
`swiftformat --lint .` and SwiftLint run on build and are currently clean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks & open questions
|
||||||
|
|
||||||
|
| # | Risk | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| **O1** | **Was the legacy build on the Rust crypto store?** `cryptoSDKStoreKey` is generated unconditionally (`EncryptionKeyManager.swift:42`), but whether the store is Rust-backed decides if `SecretsBundleWithUserId.fromDatabase` can read it. If it was still Olm, D2 collapses to "forced re-login" and needs product sign-off. **Settle on a real device before the first submission.** |
|
||||||
|
| **O2** | **iOS floor rises 15.1 → 18.5** (`element-ios/Config/Project.xcconfig:29` vs `project.yml:14-15`). Every user below 18.5 silently stays on 1.4.1 forever — the App Store will not offer the update. Not fixable (upstream's floor); check the install base before shipping. |
|
||||||
|
| **O3** | **Pusher overwrite.** element-x's `pusherAppID` and device pushkey are identical to the legacy's, so first launch overwrites the user's sygnal pusher in place and the format changes to `.eventIdOnly`. Benign under D1 — but a rollback to 1.4.1 loses push. |
|
||||||
|
| **O4** | **Runner group visibility** — confirm `novaloop-mac-studio-001` is reachable from element-x-ios. |
|
||||||
|
| **O5** | `Secrets/Secrets.swift` holds placeholders; `Secrets/Secrets.pkl:12-17` wants MAPLIBRE_API_KEY among others. Maps degrade without it. Not a release blocker — track separately. |
|
||||||
|
| **O6** | Cosmetic Element strings ship in a HealthChat binary, e.g. `BGTaskSchedulerPermittedIdentifiers: io.element.elementx.background.refresh` (`ElementX/SupportingFiles/target.yml:102-104`). Working; cosmetic only. |
|
||||||
|
|
||||||
|
**Closed:** the app-group collision question — container paths are disjoint, and the shared
|
||||||
|
`group.ch.healthchat` UserDefaults suite has a verified-empty key intersection. Captured as a standing hazard
|
||||||
|
in the Part 3 migration contract rather than an open risk.
|
||||||
|
|
||||||
|
### Corrected prior belief
|
||||||
|
|
||||||
|
An earlier session recorded that a legacy app-lock PIN could lock the new build out via a shared keychain
|
||||||
|
group. **Refuted.** The two differ in *group, service and account* — `…keychain.shared` vs `…element`;
|
||||||
|
`…pin-service` vs `…keychain.sessions`; `pin` vs `appLockPINCode`
|
||||||
|
(`element-ios/Riot/Modules/SetPinCode/PinCodePreferences.swift:20-24,37-38` vs
|
||||||
|
`ElementX/.../KeychainController.swift:21-23,34-37`). The groups were never shared. The conclusion
|
||||||
|
("harmless") was right; the mechanism was wrong — and the wrong mechanism is what motivated dropping the
|
||||||
|
classic keychain group in `01dc703b4`.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
App Store submission automation (needs `fastlane/metadata/` + HealthChat screenshots, neither exists), German
|
||||||
|
localisation, the `to.healthchat.ch` links service, blue outgoing bubbles.
|
||||||
+1
-1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user