{
  "schema_version": 1,
  "builds": [
    {
      "platform": "ios",
      "version": "v0.0.57",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.57",
      "commit": "e54ebb65b89fc3819f717d21b72c030abb8fe5c7",
      "commit_short": "e54eb",
      "built_at": "2026-05-23T20:06:58Z",
      "description": "Release v0.0.57",
      "issue": {
        "number": 172,
        "title": "Release",
        "url": "https://github.com/onymchat/onym-ios/issues/172",
        "body": "### Tag\n\nv0.0.57\n\n### Release notes (optional)\n\n_No response_\n\n### Test results\n\n<!-- UI_TESTS_START -->\n_Discovered 79 XCTestCase classes — boxes flip to `[x]` once `Release` finishes._\n\n- [ ] OnymIOSTests.AnchorSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [ ] OnymIOSTests.AnchorsPickerFlowTests\n- [ ] OnymIOSTests.ApproveRequestsFlowTests\n- [ ] OnymIOSTests.CachingChainStateReaderTests — Tests for the cache + retry decorator that tames the launch-time `get_commitment` storm and survives transient relayer throttling.\n- [ ] OnymIOSTests.CanonicalFrTests — Regression coverage for the bls12-381 canonical-Fr predicate + rejection sampler that `CreateGroupInteractor` uses to mint `groupID`.\n- [ ] OnymIOSTests.ChatBubbleCellTests\n- [ ] OnymIOSTests.ChatGroupTests\n- [ ] OnymIOSTests.ChatInputPanelViewTests\n- [ ] OnymIOSTests.ChatMessagePayloadTests — Wire-format pin for `ChatMessagePayload`.\n- [ ] OnymIOSTests.ChatThreadViewControllerTests\n- [ ] OnymIOSTests.ContractsManifestDecodingTests — Pin the wire format + the unknown-enum-dropping behaviour of the manifest decoder.\n- [ ] OnymIOSTests.ContractsManifestFetcherTests — Hits the production fetcher with `StubURLProtocol` (the reusable scaffolding from PR #18) in front.\n- [ ] OnymIOSTests.ContractsRepositoryTests — Repository against the in-memory fakes — focused on the binding resolution rules (the part chains will read forever) plus the reactive surface, selection persistence, and silent-on-error background start.\n- [ ] OnymIOSTests.CreateGroupFlowTests\n- [ ] OnymIOSTests.CreateGroupInteractorTests\n- [ ] OnymIOSTests.DeeplinkCaptureTests — Pure-XCTest tests for `DeeplinkCapture.introCapability(from:)`.\n- [ ] OnymIOSTests.GroupAvatarImageTests — `GroupAvatarImage` is the single funnel every avatar source passes through, so these lock the two invariants the wire format depends on: the output is a 256×256 square, and it never exceeds the byte budget — even for a high-entropy image that resists JPEG.\n- [ ] OnymIOSTests.GroupAvatarPayloadTests — Wire-format pin for `GroupAvatarPayload`.\n- [ ] OnymIOSTests.GroupCommitmentBuilderTests — Unit tests for `GroupCommitmentBuilder`.\n- [ ] OnymIOSTests.GroupInvitationPayloadTests — Wire-format pin for `GroupInvitationPayload`.\n- [ ] OnymIOSTests.GroupInviteOfferPayloadTests\n  - The dispatcher relies on `inviter_alias` + `intro_pub` being unique to this type.\n- [ ] OnymIOSTests.GroupProofGeneratorTests — Real proof generation against the OnymSDK Tyranny circuit.\n- [ ] OnymIOSTests.GroupRepositoryTests — Reactive-surface tests for `GroupRepository`.\n- [ ] OnymIOSTests.GroupStateRefreshRequestCodecTests\n  - An invite offer must not decode as a refresh request (disjoint required keys keep the dispatcher's trial-decode unambiguous).\n  - The dispatcher trial-decodes the offer (fast-path 0) and the refresh request (fast-path 0.5) BEFORE the announcement / invitation / join-request / chat-message paths.\n- [ ] OnymIOSTests.GroupStateVerifierTests\n  - A snapshot whose admin we can't reach (no admin entry in the shipped roster) is recorded as `.unreachable` and surfaced to the user — not silently dropped, never materialized.\n  - The admin must not answer a refresh request from a non-member — the reply carries the salt.\n  - If this device isn't the group's admin, refuse even a real member — only the admin holds and should disclose the current salt.\n  - Full loop: a pending verification is cleared once the fresh snapshot materializes the group.\n  - The 30/60s timer transitions a sent-but-unanswered request from `.verifying` to `.unreachable` (tiny timeout for the test).\n- [ ] OnymIOSTests.IdentitiesFlowTests\n- [ ] OnymIOSTests.IdentityIDTests\n- [ ] OnymIOSTests.IdentityKeychainStoreTests — Per-identity keychain store tests.\n- [ ] OnymIOSTests.IdentityRepositoryInvitationDecryptTests — Real X25519 + AES-GCM round-trip against `IdentityRepository`.\n- [ ] OnymIOSTests.IdentityRepositoryMultiIdentityTests — Multi-identity API surface added in PR-2.\n- [ ] OnymIOSTests.IdentityRepositorySealInvitationTests — Sender side of the invitation envelope.\n- [ ] OnymIOSTests.IdentityRepositoryTests — Each test uses its own Keychain service so test runs are isolated and do not collide with the production identity item or with each other.\n  - **Derivation fixture.** Locks in derivation against the canonical BIP39 test mnemonic so any change to a salt / info string (HKDF for nostr, BLS, Stellar Ed25519, X25519, or the `sep-inbox-v1` SHA-256 tag) breaks this test loudly.\n- [ ] OnymIOSTests.InboxFanoutInteractorTests\n- [ ] OnymIOSTests.IncomingInvitationsInteractorTests — Pump tests for the seam-A → interactor → seam-B pattern.\n- [ ] OnymIOSTests.IncomingInvitationsRepositoryTests — Repository contract on top of the in-memory store fake — fast, focused on the reactive surface (snapshots emit current value on subscribe + a fresh value after every successful mutation), the mutator semantics, dedup behaviour, and post-#58 the per-identity filter + `removeForOwner` hook.\n- [ ] OnymIOSTests.IncomingMessageDispatcherChatMessageTests\n- [ ] OnymIOSTests.IncomingMessageDispatcherTests\n- [ ] OnymIOSTests.CreateGroupOneOnOneE2ETests\n  - Happy path.\n- [ ] OnymIOSTests.CreateGroupTyrannyE2ETests\n  - Happy path with zero invitees.\n  - Happy path with one invitee.\n- [ ] OnymIOSTests.IntroCapabilityInteropTests — Cross-platform wire-format pin.\n  - `intro_pub` is 32 bytes of `0x01`, `group_id` is 32 bytes of `0x02`.\n  - Confirms UTF-8 round-trips through both platforms' JSON readers — group names like \"Семья\" or \"👨‍👩‍👧\" must survive the wire intact.\n  - Encoder smoke check: decode our own emit + reconstruct.\n- [ ] OnymIOSTests.IntroCapabilityTests — Wire-format pin for `IntroCapability`.\n- [ ] OnymIOSTests.IntroInboxPumpTests — Behavioral tests for `IntroInboxPump`.\n- [ ] OnymIOSTests.InvitationDecryptorTests — Interactor tests against `FakeInvitationEnvelopeDecrypter` — fast, no real crypto.\n- [ ] OnymIOSTests.InviteIntroducerTests — Unit tests for `InviteIntroducer` + `IntroKeyStore` contract.\n- [ ] OnymIOSTests.JoinFlowTests\n- [ ] OnymIOSTests.JoinRequestApproverTests\n- [ ] OnymIOSTests.JoinRequestPayloadTests — Wire-format pin for `JoinRequestPayload`.\n- [ ] OnymIOSTests.KnownRelayersFetcherTests — Hits the production `URLSession`-backed fetcher with `StubURLProtocol` in front so we can pin the wire format (the `relayers.json` shape we promise to publish) and the error-path behaviour (cache fallback happens at the repository layer; the fetcher itself just throws on any failure).\n- [ ] OnymIOSTests.LocalizationCatalogTests — Sanity tests that every chain-layer enum case has a non-empty, genuinely-localised `displayName`.\n  - Reads the source `.xcstrings` directly off disk (fragile under SPM / CI rebuilds where source files live elsewhere — but on the dev machine and the project's GH Actions runner this works fine and gives a one-shot sweep that catches missing translations across the entire catalog, not just chain enums).\n- [ ] OnymIOSTests.MemberAnnouncementPayloadTests — Wire-format pin for `MemberAnnouncementPayload`.\n- [ ] OnymIOSTests.MemberProfileTests — Wire-format pin for `MemberProfile`.\n- [ ] OnymIOSTests.MessageRepositoryTests — Reactive-surface tests for `MessageRepository`.\n- [ ] OnymIOSTests.NostrEventTests — Tests for the NIP-01 wire format and the integrity check that `NostrRelayConnection` runs on every inbound event.\n- [ ] OnymIOSTests.NostrInboxTransportTests — Covers the pure event-building and filter-shape paths of the inbox adapter.\n- [ ] OnymIOSTests.NostrMessageTransportTests — Covers the pure event-building path of the broadcast adapter.\n- [ ] OnymIOSTests.NostrRelaySettingsFlowTests\n- [ ] OnymIOSTests.NostrRelaysRepositoryTests — Behavioral tests for `NostrRelaysRepository` covering the first-launch seed, mutation idempotency, and reset-to-default.\n- [ ] OnymIOSTests.OnymAccentSenderColorTests — Tests for the per-sender accent derivation that drives chat sender-differentiation.\n- [ ] OnymIOSTests.OnymNostrSignerTests — Hits OnymSDK's BIP340 / secp256k1 FFI through `OnymNostrSigner` — no mocks.\n- [ ] OnymIOSTests.RecoveryPhraseBackupFlowTests\n- [ ] OnymIOSTests.RelayerConfigurationTests — Pure tests for `RelayerConfiguration.selectURL` — the resolver chain interactors will call per request.\n- [ ] OnymIOSTests.RelayerEndpointSchemaTests — Pin the wire-format compat between the iOS app and the published `relayers.json` (and any saves from earlier app versions).\n- [ ] OnymIOSTests.RelayerRepositoryTests — Repository against the in-memory fakes — fast, focused on: - construction loads cached state from the store, - list-shaped mutators (add / remove / setPrimary / setStrategy) persist via the store and push a fresh snapshot, - addEndpoint dedupes on URL (idempotent + updates metadata), - removeEndpoint clears the primary marker if it pointed at the removed endpoint, - setPrimary is a no-op when the URL isn't in the configured list, - selectURL respects the strategy stored in the configuration, - background `start()` is idempotent (no double fetch) and silent on error.\n- [ ] OnymIOSTests.RelayerSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [ ] OnymIOSTests.RelayerSettingsFlowTests\n- [ ] OnymIOSTests.SEPContractClientTests — Pure-Swift unit tests for `SEPContractClient` — no real HTTP.\n- [ ] OnymIOSTests.SealedEnvelopeTests — Pin the SealedEnvelope wire format.\n- [ ] OnymIOSTests.SendMessageInteractorTests\n- [ ] OnymIOSTests.SettingsQRCodeTests — Producer-side checks for the inbox-key invite URL emitted by Settings → Invite Key.\n- [ ] OnymIOSTests.ShareInviteFlowTests\n- [ ] OnymIOSTests.SmokeTests\n  - Smoke test: the OnymSDK SwiftPM dep links and the FFI is reachable from app code.\n- [ ] OnymIOSTests.StorageEncryptionTests — Pin AES-GCM roundtrip semantics + Keychain key stability.\n- [ ] OnymIOSTests.SwiftDataGroupStoreTests — Round-trip tests for `SwiftDataGroupStore`.\n- [ ] OnymIOSTests.SwiftDataInvitationStoreTests — Exercises the real SwiftData backend (in-memory `ModelContainer` so each test gets a fresh, isolated store).\n- [ ] OnymIOSTests.SwiftDataMessageStoreTests — Round-trip tests for `SwiftDataMessageStore`.\n- [ ] OnymIOSUITests.AnchorsUITests — End-to-end UI tests for Settings → Anchors.\n  - When the manifest publishes no Mainnet contracts, the Mainnet row renders disabled (no NavigationLink) while Testnet stays tappable.\n- [ ] OnymIOSUITests.IdentityManagementUITests — End-to-end coverage of the multi-identity surface that landed in PR #56 (the 5-PR multi-identity stack: keychain → repository → group filter → transport fan-out → UI).\n  - 1) Settings → Identities row → list appears with the bootstrapped identity marked Active.\n  - 2) Add Identity sheet → name in → submit → list grows by one.\n  - 3) Picker on Chats switches identities; nav title flips.\n  - 4) Remove flow — name-confirm gate blocks until exact match, list shrinks back after confirmed removal.\n- [ ] OnymIOSUITests.RecoveryPhraseBackupUITests — End-to-end coverage of the recovery-phrase backup flow, driven through the live SwiftUI views via `XCUIApplication`.\n  - Full happy path: open Backup → tap Reveal → see a 12-word phrase → answer all three verification rounds correctly → land on the Done screen.\n  - Picking a wrong word during verification keeps the user on the same round and surfaces the inline error message — no silent advance, no false-positive completion.\n  - Launching with `language: \"ru\"` renders Russian copy on Settings (nav title + Backup row) and on the recovery-phrase Intro screen — confirms the localized catalog wires through end to end.\n  - A fresh-keychain launch generates a valid 12-word phrase: every word is non-empty, all-lowercase letters, and the phrase has at least 6 unique words (a sanity guard against a stuck/zeroed seed).\n- [ ] OnymIOSUITests.RelayerSettingsUITests — End-to-end UI tests for Settings → Relayer.\n  - On a clean first launch, the Configured-relayers list pre-populates from the published manifest fixture (testnet + mainnet rows visible without any user action).\n  - On a clean first launch, the relayer-selection strategy defaults to Random — the segmented control's \"Random\" segment is the selected one.\n  - Tapping a row's star to mark it primary, then switching the strategy segment from Random to Primary, persists through the UI: the Primary segment is the selected one when the dust settles.\n  - Typing a URL into the custom-relayer field and tapping Add makes the new row appear in the Configured list.\n\n<!-- UI_TESTS_END -->\n\n\n### Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Fresh install via TestFlight or AltStore signs in cleanly\n- [ ] First-run BIP39 generation produces a 12-word phrase\n- [ ] Backup → Reveal → Verify (3 rounds) reaches the Done screen\n- [ ] App launches without splash flicker on cold start\n\n### Identity\n- [ ] Restore from a known mnemonic produces the expected Stellar address\n- [ ] Inbox key in Settings → Advanced matches across reinstalls of the same mnemonic\n\n### Create Group — Tyranny\n- [ ] Step 1 → Step 2 → Create reaches Success against testnet\n- [ ] Group lands in the Chats tab as published-on-chain\n- [ ] Inviting one peer (paste their inbox key) sends an invitation that the receiver can accept\n\n### Create Group — 1-on-1\n- [ ] \"1-on-1\" governance card is selectable on Step 1 (no \"Soon\" pill)\n- [ ] Step 2 requires exactly one peer; CTA reads \"Start dialog\"\n- [ ] Created dialog lands in Chats; both parties see the same group\n\n### Theme + locale\n- [ ] Light theme renders Create Group flow correctly\n- [ ] Dark theme renders the same screens correctly\n- [ ] Russian locale shows localized strings on Settings + Backup\n\n### Anchors + Relayer\n- [ ] Settings → Anchors picker resolves to the latest contracts manifest\n- [ ] Settings → Relayer can add a custom URL and switch primary\n\n### Release artifacts\n- [ ] App Store screenshots refreshed via fastlane (if any UI changed materially)\n- [ ] Push notification entitlement still validates against the bundle ID\n- [ ] IPA installs cleanly through AltStore (the only currently shipped channel)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.57/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.57/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.57/manifest.plist",
      "size_bytes": 5204532,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/e54ebb65b89fc3819f717d21b72c030abb8fe5c7",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/26342071809"
    },
    {
      "platform": "android",
      "version": "v0.0.55",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.55",
      "commit": "65e3cad5b9911a2b16de03cffc2b6cdd4153e32d",
      "commit_short": "65e3c",
      "built_at": "2026-05-23T19:48:10Z",
      "description": "Release v0.0.55",
      "issue": {
        "number": 173,
        "title": "Release: vX.Y.Z",
        "url": "https://github.com/onymchat/onym-android/issues/173",
        "body": "### Release tag\n\nv0.0.55\n\n### Release notes (optional)\n\n_No response_\n\n### Checklist\n\n## UI tests\n\n<!-- UI_TESTS_START -->\n\n- [x] app.onym.android.chain.GroupProofGeneratorFfiTest\n- [x] app.onym.android.group.CreateGroupInteractorTest\n- [x] app.onym.android.group.GroupAvatarImageTest\n- [x] app.onym.android.group.GroupCommitmentBuilderFfiTest\n- [x] app.onym.android.identity.IdentityRepositoryInvitationDecryptTest\n- [x] app.onym.android.identity.IdentityRepositorySealInvitationTest\n- [x] app.onym.android.identity.IdentityRepositoryTest\n- [x] app.onym.android.inbox.IncomingMessageDispatcherConvergeForwardFfiTest\n- [x] app.onym.android.integration.CreateGroupAnarchyE2ETest\n- [x] app.onym.android.integration.CreateGroupOneOnOneE2ETest\n- [x] app.onym.android.integration.CreateGroupTyrannyE2ETest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupScreenTest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupViewModelTest\n- [x] app.onym.android.transport.nostr.OnymNostrSignerTest\n- [x] app.onym.android.uitests.AnchorsUITest\n- [x] app.onym.android.uitests.IdentitiesUITest\n- [x] app.onym.android.uitests.RelayerSettingsUITest\n\n<!-- UI_TESTS_END -->\n\n## Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Sideload the signed APK on a real device (Android 14+ recommended)\n- [ ] First-launch onboarding flow renders without crash\n- [ ] Bootstrap a new identity, then restart the app and confirm it persists\n- [ ] Restore identity from a 12-word mnemonic, confirm pubkey matches\n\n### Create Group — Tyranny\n- [ ] Create an empty Tyranny group against testnet, confirm it lands on the Stellar contract\n- [ ] Create a Tyranny group with 1 invitee, confirm invitee receives sealed envelope\n- [ ] Open Settings → toggle Mainnet, confirm next create resolves the mainnet contract binding\n\n### Create Group — 1-on-1\n- [ ] Pick the 1-on-1 governance card, paste an invitee inbox key, confirm \"Start 1-on-1\" CTA enables only at exactly 1 invitee\n- [ ] Create the 1-on-1 group, confirm anchor on testnet\n- [ ] Confirm invitee receives sealed envelope carrying the ephemeral `sk_1`\n\n### Settings + relayer + anchors\n- [ ] Settings → Relayer Settings → swipe-delete an endpoint, confirm primary marker clears if deleted\n- [ ] Settings → Anchors → drill into Testnet → Tyranny, pick an older release, confirm persistence\n- [ ] Add a custom relayer URL, confirm it lands as `custom` network endpoint\n\n### Theming + chrome\n- [ ] Toggle system dark mode, every Create Group screen renders correctly in both themes\n- [ ] Bottom-tab Chats / Settings / Search switching works without flicker\n- [ ] Recovery-phrase backup flow (Settings → Backup) walks Intro → Reveal → Verify → Done\n\n### Production sanity\n- [ ] APK installs without \"package conflicts with existing\" prompt over the prior release\n- [ ] Network Settings shows the deployed `relayer.onym.chat` endpoint by default after auto-populate\n- [ ] AltStore source updates with the new IPA URL once notarization mirrors land (separate workflow — leave unchecked if N/A)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "https://onym.app/qa/android/v0.0.55/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.55/onym.apk",
      "size_bytes": 37471129,
      "commit_url": "https://github.com/onymchat/onym-android/commit/65e3cad5b9911a2b16de03cffc2b6cdd4153e32d",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/26341724806"
    },
    {
      "platform": "ios",
      "version": "v0.0.54",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.54",
      "commit": "1def1bb9e3df674f8a0506ce912f130fcdf69087",
      "commit_short": "1def1",
      "built_at": "2026-05-23T15:59:36Z",
      "description": "Release v0.0.54",
      "issue": {
        "number": 167,
        "title": "Release",
        "url": "https://github.com/onymchat/onym-ios/issues/167",
        "body": "### Tag\n\nv0.0.54\n\n### Release notes (optional)\n\n_No response_\n\n### Test results\n\n<!-- UI_TESTS_START -->\n_Discovered 78 XCTestCase classes — boxes flip to `[x]` once `Release` finishes._\n\n- [x] OnymIOSTests.AnchorSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [x] OnymIOSTests.AnchorsPickerFlowTests\n- [x] OnymIOSTests.ApproveRequestsFlowTests\n- [x] OnymIOSTests.CanonicalFrTests — Regression coverage for the bls12-381 canonical-Fr predicate + rejection sampler that `CreateGroupInteractor` uses to mint `groupID`.\n- [x] OnymIOSTests.ChatBubbleCellTests\n- [x] OnymIOSTests.ChatGroupTests\n- [x] OnymIOSTests.ChatInputPanelViewTests\n- [x] OnymIOSTests.ChatMessagePayloadTests — Wire-format pin for `ChatMessagePayload`.\n- [x] OnymIOSTests.ChatThreadViewControllerTests\n- [x] OnymIOSTests.ContractsManifestDecodingTests — Pin the wire format + the unknown-enum-dropping behaviour of the manifest decoder.\n- [x] OnymIOSTests.ContractsManifestFetcherTests — Hits the production fetcher with `StubURLProtocol` (the reusable scaffolding from PR #18) in front.\n- [x] OnymIOSTests.ContractsRepositoryTests — Repository against the in-memory fakes — focused on the binding resolution rules (the part chains will read forever) plus the reactive surface, selection persistence, and silent-on-error background start.\n- [x] OnymIOSTests.CreateGroupFlowTests\n- [x] OnymIOSTests.CreateGroupInteractorTests\n- [x] OnymIOSTests.DeeplinkCaptureTests — Pure-XCTest tests for `DeeplinkCapture.introCapability(from:)`.\n- [x] OnymIOSTests.GroupAvatarImageTests — `GroupAvatarImage` is the single funnel every avatar source passes through, so these lock the two invariants the wire format depends on: the output is a 256×256 square, and it never exceeds the byte budget — even for a high-entropy image that resists JPEG.\n- [x] OnymIOSTests.GroupAvatarPayloadTests — Wire-format pin for `GroupAvatarPayload`.\n- [x] OnymIOSTests.GroupCommitmentBuilderTests — Unit tests for `GroupCommitmentBuilder`.\n- [x] OnymIOSTests.GroupInvitationPayloadTests — Wire-format pin for `GroupInvitationPayload`.\n- [x] OnymIOSTests.GroupInviteOfferPayloadTests\n  - The dispatcher relies on `inviter_alias` + `intro_pub` being unique to this type.\n- [x] OnymIOSTests.GroupProofGeneratorTests — Real proof generation against the OnymSDK Tyranny circuit.\n- [x] OnymIOSTests.GroupRepositoryTests — Reactive-surface tests for `GroupRepository`.\n- [x] OnymIOSTests.GroupStateRefreshRequestCodecTests\n  - An invite offer must not decode as a refresh request (disjoint required keys keep the dispatcher's trial-decode unambiguous).\n  - The dispatcher trial-decodes the offer (fast-path 0) and the refresh request (fast-path 0.5) BEFORE the announcement / invitation / join-request / chat-message paths.\n- [x] OnymIOSTests.GroupStateVerifierTests\n  - A snapshot whose admin we can't reach (no admin entry in the shipped roster) is recorded as `.unreachable` and surfaced to the user — not silently dropped, never materialized.\n  - The admin must not answer a refresh request from a non-member — the reply carries the salt.\n  - If this device isn't the group's admin, refuse even a real member — only the admin holds and should disclose the current salt.\n  - Full loop: a pending verification is cleared once the fresh snapshot materializes the group.\n  - The 30/60s timer transitions a sent-but-unanswered request from `.verifying` to `.unreachable` (tiny timeout for the test).\n- [x] OnymIOSTests.IdentitiesFlowTests\n- [x] OnymIOSTests.IdentityIDTests\n- [x] OnymIOSTests.IdentityKeychainStoreTests — Per-identity keychain store tests.\n- [x] OnymIOSTests.IdentityRepositoryInvitationDecryptTests — Real X25519 + AES-GCM round-trip against `IdentityRepository`.\n- [x] OnymIOSTests.IdentityRepositoryMultiIdentityTests — Multi-identity API surface added in PR-2.\n- [x] OnymIOSTests.IdentityRepositorySealInvitationTests — Sender side of the invitation envelope.\n- [x] OnymIOSTests.IdentityRepositoryTests — Each test uses its own Keychain service so test runs are isolated and do not collide with the production identity item or with each other.\n  - **Derivation fixture.** Locks in derivation against the canonical BIP39 test mnemonic so any change to a salt / info string (HKDF for nostr, BLS, Stellar Ed25519, X25519, or the `sep-inbox-v1` SHA-256 tag) breaks this test loudly.\n- [x] OnymIOSTests.InboxFanoutInteractorTests\n- [x] OnymIOSTests.IncomingInvitationsInteractorTests — Pump tests for the seam-A → interactor → seam-B pattern.\n- [x] OnymIOSTests.IncomingInvitationsRepositoryTests — Repository contract on top of the in-memory store fake — fast, focused on the reactive surface (snapshots emit current value on subscribe + a fresh value after every successful mutation), the mutator semantics, dedup behaviour, and post-#58 the per-identity filter + `removeForOwner` hook.\n- [x] OnymIOSTests.IncomingMessageDispatcherChatMessageTests\n- [x] OnymIOSTests.IncomingMessageDispatcherTests\n- [x] OnymIOSTests.CreateGroupOneOnOneE2ETests\n  - Happy path.\n- [x] OnymIOSTests.CreateGroupTyrannyE2ETests\n  - Happy path with zero invitees.\n  - Happy path with one invitee.\n- [x] OnymIOSTests.IntroCapabilityInteropTests — Cross-platform wire-format pin.\n  - `intro_pub` is 32 bytes of `0x01`, `group_id` is 32 bytes of `0x02`.\n  - Confirms UTF-8 round-trips through both platforms' JSON readers — group names like \"Семья\" or \"👨‍👩‍👧\" must survive the wire intact.\n  - Encoder smoke check: decode our own emit + reconstruct.\n- [x] OnymIOSTests.IntroCapabilityTests — Wire-format pin for `IntroCapability`.\n- [x] OnymIOSTests.IntroInboxPumpTests — Behavioral tests for `IntroInboxPump`.\n- [x] OnymIOSTests.InvitationDecryptorTests — Interactor tests against `FakeInvitationEnvelopeDecrypter` — fast, no real crypto.\n- [x] OnymIOSTests.InviteIntroducerTests — Unit tests for `InviteIntroducer` + `IntroKeyStore` contract.\n- [x] OnymIOSTests.JoinFlowTests\n- [x] OnymIOSTests.JoinRequestApproverTests\n- [x] OnymIOSTests.JoinRequestPayloadTests — Wire-format pin for `JoinRequestPayload`.\n- [x] OnymIOSTests.KnownRelayersFetcherTests — Hits the production `URLSession`-backed fetcher with `StubURLProtocol` in front so we can pin the wire format (the `relayers.json` shape we promise to publish) and the error-path behaviour (cache fallback happens at the repository layer; the fetcher itself just throws on any failure).\n- [x] OnymIOSTests.LocalizationCatalogTests — Sanity tests that every chain-layer enum case has a non-empty, genuinely-localised `displayName`.\n  - Reads the source `.xcstrings` directly off disk (fragile under SPM / CI rebuilds where source files live elsewhere — but on the dev machine and the project's GH Actions runner this works fine and gives a one-shot sweep that catches missing translations across the entire catalog, not just chain enums).\n- [x] OnymIOSTests.MemberAnnouncementPayloadTests — Wire-format pin for `MemberAnnouncementPayload`.\n- [x] OnymIOSTests.MemberProfileTests — Wire-format pin for `MemberProfile`.\n- [x] OnymIOSTests.MessageRepositoryTests — Reactive-surface tests for `MessageRepository`.\n- [x] OnymIOSTests.NostrEventTests — Tests for the NIP-01 wire format and the integrity check that `NostrRelayConnection` runs on every inbound event.\n- [x] OnymIOSTests.NostrInboxTransportTests — Covers the pure event-building and filter-shape paths of the inbox adapter.\n- [x] OnymIOSTests.NostrMessageTransportTests — Covers the pure event-building path of the broadcast adapter.\n- [x] OnymIOSTests.NostrRelaySettingsFlowTests\n- [x] OnymIOSTests.NostrRelaysRepositoryTests — Behavioral tests for `NostrRelaysRepository` covering the first-launch seed, mutation idempotency, and reset-to-default.\n- [x] OnymIOSTests.OnymAccentSenderColorTests — Tests for the per-sender accent derivation that drives chat sender-differentiation.\n- [x] OnymIOSTests.OnymNostrSignerTests — Hits OnymSDK's BIP340 / secp256k1 FFI through `OnymNostrSigner` — no mocks.\n- [x] OnymIOSTests.RecoveryPhraseBackupFlowTests\n- [x] OnymIOSTests.RelayerConfigurationTests — Pure tests for `RelayerConfiguration.selectURL` — the resolver chain interactors will call per request.\n- [x] OnymIOSTests.RelayerEndpointSchemaTests — Pin the wire-format compat between the iOS app and the published `relayers.json` (and any saves from earlier app versions).\n- [x] OnymIOSTests.RelayerRepositoryTests — Repository against the in-memory fakes — fast, focused on: - construction loads cached state from the store, - list-shaped mutators (add / remove / setPrimary / setStrategy) persist via the store and push a fresh snapshot, - addEndpoint dedupes on URL (idempotent + updates metadata), - removeEndpoint clears the primary marker if it pointed at the removed endpoint, - setPrimary is a no-op when the URL isn't in the configured list, - selectURL respects the strategy stored in the configuration, - background `start()` is idempotent (no double fetch) and silent on error.\n- [x] OnymIOSTests.RelayerSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [x] OnymIOSTests.RelayerSettingsFlowTests\n- [x] OnymIOSTests.SEPContractClientTests — Pure-Swift unit tests for `SEPContractClient` — no real HTTP.\n- [x] OnymIOSTests.SealedEnvelopeTests — Pin the SealedEnvelope wire format.\n- [x] OnymIOSTests.SendMessageInteractorTests\n- [x] OnymIOSTests.SettingsQRCodeTests — Producer-side checks for the inbox-key invite URL emitted by Settings → Invite Key.\n- [x] OnymIOSTests.ShareInviteFlowTests\n- [x] OnymIOSTests.SmokeTests\n  - Smoke test: the OnymSDK SwiftPM dep links and the FFI is reachable from app code.\n- [x] OnymIOSTests.StorageEncryptionTests — Pin AES-GCM roundtrip semantics + Keychain key stability.\n- [x] OnymIOSTests.SwiftDataGroupStoreTests — Round-trip tests for `SwiftDataGroupStore`.\n- [x] OnymIOSTests.SwiftDataInvitationStoreTests — Exercises the real SwiftData backend (in-memory `ModelContainer` so each test gets a fresh, isolated store).\n- [x] OnymIOSTests.SwiftDataMessageStoreTests — Round-trip tests for `SwiftDataMessageStore`.\n- [x] OnymIOSUITests.AnchorsUITests — End-to-end UI tests for Settings → Anchors.\n  - When the manifest publishes no Mainnet contracts, the Mainnet row renders disabled (no NavigationLink) while Testnet stays tappable.\n- [x] OnymIOSUITests.IdentityManagementUITests — End-to-end coverage of the multi-identity surface that landed in PR #56 (the 5-PR multi-identity stack: keychain → repository → group filter → transport fan-out → UI).\n  - 1) Settings → Identities row → list appears with the bootstrapped identity marked Active.\n  - 2) Add Identity sheet → name in → submit → list grows by one.\n  - 3) Picker on Chats switches identities; nav title flips.\n  - 4) Remove flow — name-confirm gate blocks until exact match, list shrinks back after confirmed removal.\n- [x] OnymIOSUITests.RecoveryPhraseBackupUITests — End-to-end coverage of the recovery-phrase backup flow, driven through the live SwiftUI views via `XCUIApplication`.\n  - Full happy path: open Backup → tap Reveal → see a 12-word phrase → answer all three verification rounds correctly → land on the Done screen.\n  - Picking a wrong word during verification keeps the user on the same round and surfaces the inline error message — no silent advance, no false-positive completion.\n  - Launching with `language: \"ru\"` renders Russian copy on Settings (nav title + Backup row) and on the recovery-phrase Intro screen — confirms the localized catalog wires through end to end.\n  - A fresh-keychain launch generates a valid 12-word phrase: every word is non-empty, all-lowercase letters, and the phrase has at least 6 unique words (a sanity guard against a stuck/zeroed seed).\n- [x] OnymIOSUITests.RelayerSettingsUITests — End-to-end UI tests for Settings → Relayer.\n  - On a clean first launch, the Configured-relayers list pre-populates from the published manifest fixture (testnet + mainnet rows visible without any user action).\n  - On a clean first launch, the relayer-selection strategy defaults to Random — the segmented control's \"Random\" segment is the selected one.\n  - Tapping a row's star to mark it primary, then switching the strategy segment from Random to Primary, persists through the UI: the Primary segment is the selected one when the dust settles.\n  - Typing a URL into the custom-relayer field and tapping Add makes the new row appear in the Configured list.\n\n<!-- UI_TESTS_END -->\n\n\n### Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Fresh install via TestFlight or AltStore signs in cleanly\n- [ ] First-run BIP39 generation produces a 12-word phrase\n- [ ] Backup → Reveal → Verify (3 rounds) reaches the Done screen\n- [ ] App launches without splash flicker on cold start\n\n### Identity\n- [ ] Restore from a known mnemonic produces the expected Stellar address\n- [ ] Inbox key in Settings → Advanced matches across reinstalls of the same mnemonic\n\n### Create Group — Tyranny\n- [ ] Step 1 → Step 2 → Create reaches Success against testnet\n- [ ] Group lands in the Chats tab as published-on-chain\n- [ ] Inviting one peer (paste their inbox key) sends an invitation that the receiver can accept\n\n### Create Group — 1-on-1\n- [ ] \"1-on-1\" governance card is selectable on Step 1 (no \"Soon\" pill)\n- [ ] Step 2 requires exactly one peer; CTA reads \"Start dialog\"\n- [ ] Created dialog lands in Chats; both parties see the same group\n\n### Theme + locale\n- [ ] Light theme renders Create Group flow correctly\n- [ ] Dark theme renders the same screens correctly\n- [ ] Russian locale shows localized strings on Settings + Backup\n\n### Anchors + Relayer\n- [ ] Settings → Anchors picker resolves to the latest contracts manifest\n- [ ] Settings → Relayer can add a custom URL and switch primary\n\n### Release artifacts\n- [ ] App Store screenshots refreshed via fastlane (if any UI changed materially)\n- [ ] Push notification entitlement still validates against the bundle ID\n- [ ] IPA installs cleanly through AltStore (the only currently shipped channel)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.54/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.54/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.54/manifest.plist",
      "size_bytes": 5189884,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/1def1bb9e3df674f8a0506ce912f130fcdf69087",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/26336959207"
    },
    {
      "platform": "android",
      "version": "v0.0.53",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.53",
      "commit": "a230fcf8c283c02d7b69bfa91dd6dc7b4fd3aa16",
      "commit_short": "a230f",
      "built_at": "2026-05-23T15:58:06Z",
      "description": "Release v0.0.53",
      "issue": {
        "number": 167,
        "title": "Release: vX.Y.Z",
        "url": "https://github.com/onymchat/onym-android/issues/167",
        "body": "### Release tag\n\nv0.0.53\n\n### Release notes (optional)\n\n_No response_\n\n### Checklist\n\n## UI tests\n\n<!-- UI_TESTS_START -->\n\n- [x] app.onym.android.chain.GroupProofGeneratorFfiTest\n- [x] app.onym.android.group.CreateGroupInteractorTest\n- [x] app.onym.android.group.GroupAvatarImageTest\n- [x] app.onym.android.group.GroupCommitmentBuilderFfiTest\n- [x] app.onym.android.identity.IdentityRepositoryInvitationDecryptTest\n- [x] app.onym.android.identity.IdentityRepositorySealInvitationTest\n- [x] app.onym.android.identity.IdentityRepositoryTest\n- [x] app.onym.android.inbox.IncomingMessageDispatcherConvergeForwardFfiTest\n- [x] app.onym.android.integration.CreateGroupAnarchyE2ETest\n- [x] app.onym.android.integration.CreateGroupOneOnOneE2ETest\n- [x] app.onym.android.integration.CreateGroupTyrannyE2ETest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupScreenTest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupViewModelTest\n- [x] app.onym.android.transport.nostr.OnymNostrSignerTest\n- [x] app.onym.android.uitests.AnchorsUITest\n- [x] app.onym.android.uitests.IdentitiesUITest\n- [x] app.onym.android.uitests.RelayerSettingsUITest\n\n<!-- UI_TESTS_END -->\n\n## Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Sideload the signed APK on a real device (Android 14+ recommended)\n- [ ] First-launch onboarding flow renders without crash\n- [ ] Bootstrap a new identity, then restart the app and confirm it persists\n- [ ] Restore identity from a 12-word mnemonic, confirm pubkey matches\n\n### Create Group — Tyranny\n- [ ] Create an empty Tyranny group against testnet, confirm it lands on the Stellar contract\n- [ ] Create a Tyranny group with 1 invitee, confirm invitee receives sealed envelope\n- [ ] Open Settings → toggle Mainnet, confirm next create resolves the mainnet contract binding\n\n### Create Group — 1-on-1\n- [ ] Pick the 1-on-1 governance card, paste an invitee inbox key, confirm \"Start 1-on-1\" CTA enables only at exactly 1 invitee\n- [ ] Create the 1-on-1 group, confirm anchor on testnet\n- [ ] Confirm invitee receives sealed envelope carrying the ephemeral `sk_1`\n\n### Settings + relayer + anchors\n- [ ] Settings → Relayer Settings → swipe-delete an endpoint, confirm primary marker clears if deleted\n- [ ] Settings → Anchors → drill into Testnet → Tyranny, pick an older release, confirm persistence\n- [ ] Add a custom relayer URL, confirm it lands as `custom` network endpoint\n\n### Theming + chrome\n- [ ] Toggle system dark mode, every Create Group screen renders correctly in both themes\n- [ ] Bottom-tab Chats / Settings / Search switching works without flicker\n- [ ] Recovery-phrase backup flow (Settings → Backup) walks Intro → Reveal → Verify → Done\n\n### Production sanity\n- [ ] APK installs without \"package conflicts with existing\" prompt over the prior release\n- [ ] Network Settings shows the deployed `relayer.onym.chat` endpoint by default after auto-populate\n- [ ] AltStore source updates with the new IPA URL once notarization mirrors land (separate workflow — leave unchecked if N/A)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "https://onym.app/qa/android/v0.0.53/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.53/onym.apk",
      "size_bytes": 37467033,
      "commit_url": "https://github.com/onymchat/onym-android/commit/a230fcf8c283c02d7b69bfa91dd6dc7b4fd3aa16",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/26336952583"
    },
    {
      "platform": "android",
      "version": "v0.0.50",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.50",
      "commit": "a7d5e6edae5d4cb537631482afec964030e576d9",
      "commit_short": "a7d5e",
      "built_at": "2026-05-23T10:52:35Z",
      "description": "Release v0.0.50",
      "issue": {
        "number": 161,
        "title": "Release: vX.Y.Z",
        "url": "https://github.com/onymchat/onym-android/issues/161",
        "body": "### Release tag\n\nv0.0.50\n\n### Release notes (optional)\n\n_No response_\n\n### Checklist\n\n## UI tests\n\n<!-- UI_TESTS_START -->\n\n- [x] app.onym.android.chain.GroupProofGeneratorFfiTest\n- [x] app.onym.android.group.CreateGroupInteractorTest\n- [x] app.onym.android.group.GroupCommitmentBuilderFfiTest\n- [x] app.onym.android.identity.IdentityRepositoryInvitationDecryptTest\n- [x] app.onym.android.identity.IdentityRepositorySealInvitationTest\n- [x] app.onym.android.identity.IdentityRepositoryTest\n- [x] app.onym.android.inbox.IncomingMessageDispatcherConvergeForwardFfiTest\n- [ ] app.onym.android.integration.CreateGroupAnarchyE2ETest\n- [ ] app.onym.android.integration.CreateGroupOneOnOneE2ETest\n- [ ] app.onym.android.integration.CreateGroupTyrannyE2ETest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupScreenTest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupViewModelTest\n- [x] app.onym.android.transport.nostr.OnymNostrSignerTest\n- [x] app.onym.android.uitests.AnchorsUITest\n- [x] app.onym.android.uitests.IdentitiesUITest\n- [x] app.onym.android.uitests.RelayerSettingsUITest\n\n<!-- UI_TESTS_END -->\n\n## Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Sideload the signed APK on a real device (Android 14+ recommended)\n- [ ] First-launch onboarding flow renders without crash\n- [ ] Bootstrap a new identity, then restart the app and confirm it persists\n- [ ] Restore identity from a 12-word mnemonic, confirm pubkey matches\n\n### Create Group — Tyranny\n- [ ] Create an empty Tyranny group against testnet, confirm it lands on the Stellar contract\n- [ ] Create a Tyranny group with 1 invitee, confirm invitee receives sealed envelope\n- [ ] Open Settings → toggle Mainnet, confirm next create resolves the mainnet contract binding\n\n### Create Group — 1-on-1\n- [ ] Pick the 1-on-1 governance card, paste an invitee inbox key, confirm \"Start 1-on-1\" CTA enables only at exactly 1 invitee\n- [ ] Create the 1-on-1 group, confirm anchor on testnet\n- [ ] Confirm invitee receives sealed envelope carrying the ephemeral `sk_1`\n\n### Settings + relayer + anchors\n- [ ] Settings → Relayer Settings → swipe-delete an endpoint, confirm primary marker clears if deleted\n- [ ] Settings → Anchors → drill into Testnet → Tyranny, pick an older release, confirm persistence\n- [ ] Add a custom relayer URL, confirm it lands as `custom` network endpoint\n\n### Theming + chrome\n- [ ] Toggle system dark mode, every Create Group screen renders correctly in both themes\n- [ ] Bottom-tab Chats / Settings / Search switching works without flicker\n- [ ] Recovery-phrase backup flow (Settings → Backup) walks Intro → Reveal → Verify → Done\n\n### Production sanity\n- [ ] APK installs without \"package conflicts with existing\" prompt over the prior release\n- [ ] Network Settings shows the deployed `relayer.onym.chat` endpoint by default after auto-populate\n- [ ] AltStore source updates with the new IPA URL once notarization mirrors land (separate workflow — leave unchecked if N/A)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "https://onym.app/qa/android/v0.0.50/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.50/onym.apk",
      "size_bytes": 37450649,
      "commit_url": "https://github.com/onymchat/onym-android/commit/a7d5e6edae5d4cb537631482afec964030e576d9",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/26329414592"
    },
    {
      "platform": "ios",
      "version": "v0.0.51",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.51",
      "commit": "100ba243bf614ba1c8c7c01f26125382e6e2d9e3",
      "commit_short": "100ba",
      "built_at": "2026-05-22T21:13:03Z",
      "description": "Release v0.0.51",
      "issue": {
        "number": 160,
        "title": "Release",
        "url": "https://github.com/onymchat/onym-ios/issues/160",
        "body": "### Tag\n\nv0.0.51\n\n### Release notes (optional)\n\n_No response_\n\n### Test results\n\n<!-- UI_TESTS_START -->\n_Discovered 75 XCTestCase classes — boxes flip to `[x]` once `Release` finishes._\n\n- [x] OnymIOSTests.AnchorSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [x] OnymIOSTests.AnchorsPickerFlowTests\n- [x] OnymIOSTests.ApproveRequestsFlowTests\n- [x] OnymIOSTests.CanonicalFrTests — Regression coverage for the bls12-381 canonical-Fr predicate + rejection sampler that `CreateGroupInteractor` uses to mint `groupID`.\n- [x] OnymIOSTests.ChatBubbleCellTests\n- [x] OnymIOSTests.ChatGroupTests\n- [x] OnymIOSTests.ChatInputPanelViewTests\n- [x] OnymIOSTests.ChatMessagePayloadTests — Wire-format pin for `ChatMessagePayload`.\n- [x] OnymIOSTests.ChatThreadViewControllerTests\n- [x] OnymIOSTests.ContractsManifestDecodingTests — Pin the wire format + the unknown-enum-dropping behaviour of the manifest decoder.\n- [x] OnymIOSTests.ContractsManifestFetcherTests — Hits the production fetcher with `StubURLProtocol` (the reusable scaffolding from PR #18) in front.\n- [x] OnymIOSTests.ContractsRepositoryTests — Repository against the in-memory fakes — focused on the binding resolution rules (the part chains will read forever) plus the reactive surface, selection persistence, and silent-on-error background start.\n- [x] OnymIOSTests.CreateGroupFlowTests\n- [x] OnymIOSTests.CreateGroupInteractorTests\n- [x] OnymIOSTests.DeeplinkCaptureTests — Pure-XCTest tests for `DeeplinkCapture.introCapability(from:)`.\n- [x] OnymIOSTests.GroupCommitmentBuilderTests — Unit tests for `GroupCommitmentBuilder`.\n- [x] OnymIOSTests.GroupInvitationPayloadTests — Wire-format pin for `GroupInvitationPayload`.\n- [x] OnymIOSTests.GroupInviteOfferPayloadTests\n  - The dispatcher relies on `inviter_alias` + `intro_pub` being unique to this type.\n- [x] OnymIOSTests.GroupProofGeneratorTests — Real proof generation against the OnymSDK Tyranny circuit.\n- [x] OnymIOSTests.GroupRepositoryTests — Reactive-surface tests for `GroupRepository`.\n- [x] OnymIOSTests.GroupStateRefreshRequestCodecTests\n  - An invite offer must not decode as a refresh request (disjoint required keys keep the dispatcher's trial-decode unambiguous).\n  - The dispatcher trial-decodes the offer (fast-path 0) and the refresh request (fast-path 0.5) BEFORE the announcement / invitation / join-request / chat-message paths.\n- [x] OnymIOSTests.GroupStateVerifierTests\n  - A snapshot whose admin we can't reach (no admin entry in the shipped roster) is recorded as `.unreachable` and surfaced to the user — not silently dropped, never materialized.\n  - The admin must not answer a refresh request from a non-member — the reply carries the salt.\n  - If this device isn't the group's admin, refuse even a real member — only the admin holds and should disclose the current salt.\n  - Full loop: a pending verification is cleared once the fresh snapshot materializes the group.\n  - The 30/60s timer transitions a sent-but-unanswered request from `.verifying` to `.unreachable` (tiny timeout for the test).\n- [x] OnymIOSTests.IdentitiesFlowTests\n- [x] OnymIOSTests.IdentityIDTests\n- [x] OnymIOSTests.IdentityKeychainStoreTests — Per-identity keychain store tests.\n- [x] OnymIOSTests.IdentityRepositoryInvitationDecryptTests — Real X25519 + AES-GCM round-trip against `IdentityRepository`.\n- [x] OnymIOSTests.IdentityRepositoryMultiIdentityTests — Multi-identity API surface added in PR-2.\n- [x] OnymIOSTests.IdentityRepositorySealInvitationTests — Sender side of the invitation envelope.\n- [x] OnymIOSTests.IdentityRepositoryTests — Each test uses its own Keychain service so test runs are isolated and do not collide with the production identity item or with each other.\n  - **Derivation fixture.** Locks in derivation against the canonical BIP39 test mnemonic so any change to a salt / info string (HKDF for nostr, BLS, Stellar Ed25519, X25519, or the `sep-inbox-v1` SHA-256 tag) breaks this test loudly.\n- [x] OnymIOSTests.InboxFanoutInteractorTests\n- [x] OnymIOSTests.IncomingInvitationsInteractorTests — Pump tests for the seam-A → interactor → seam-B pattern.\n- [x] OnymIOSTests.IncomingInvitationsRepositoryTests — Repository contract on top of the in-memory store fake — fast, focused on the reactive surface (snapshots emit current value on subscribe + a fresh value after every successful mutation), the mutator semantics, dedup behaviour, and post-#58 the per-identity filter + `removeForOwner` hook.\n- [x] OnymIOSTests.IncomingMessageDispatcherChatMessageTests\n- [x] OnymIOSTests.IncomingMessageDispatcherTests\n- [x] OnymIOSTests.CreateGroupOneOnOneE2ETests\n  - Happy path.\n- [x] OnymIOSTests.CreateGroupTyrannyE2ETests\n  - Happy path with zero invitees.\n  - Happy path with one invitee.\n- [x] OnymIOSTests.IntroCapabilityInteropTests — Cross-platform wire-format pin.\n  - `intro_pub` is 32 bytes of `0x01`, `group_id` is 32 bytes of `0x02`.\n  - Confirms UTF-8 round-trips through both platforms' JSON readers — group names like \"Семья\" or \"👨‍👩‍👧\" must survive the wire intact.\n  - Encoder smoke check: decode our own emit + reconstruct.\n- [x] OnymIOSTests.IntroCapabilityTests — Wire-format pin for `IntroCapability`.\n- [x] OnymIOSTests.IntroInboxPumpTests — Behavioral tests for `IntroInboxPump`.\n- [x] OnymIOSTests.InvitationDecryptorTests — Interactor tests against `FakeInvitationEnvelopeDecrypter` — fast, no real crypto.\n- [x] OnymIOSTests.InviteIntroducerTests — Unit tests for `InviteIntroducer` + `IntroKeyStore` contract.\n- [x] OnymIOSTests.JoinFlowTests\n- [x] OnymIOSTests.JoinRequestApproverTests\n- [x] OnymIOSTests.JoinRequestPayloadTests — Wire-format pin for `JoinRequestPayload`.\n- [x] OnymIOSTests.KnownRelayersFetcherTests — Hits the production `URLSession`-backed fetcher with `StubURLProtocol` in front so we can pin the wire format (the `relayers.json` shape we promise to publish) and the error-path behaviour (cache fallback happens at the repository layer; the fetcher itself just throws on any failure).\n- [x] OnymIOSTests.LocalizationCatalogTests — Sanity tests that every chain-layer enum case has a non-empty, genuinely-localised `displayName`.\n  - Reads the source `.xcstrings` directly off disk (fragile under SPM / CI rebuilds where source files live elsewhere — but on the dev machine and the project's GH Actions runner this works fine and gives a one-shot sweep that catches missing translations across the entire catalog, not just chain enums).\n- [x] OnymIOSTests.MemberAnnouncementPayloadTests — Wire-format pin for `MemberAnnouncementPayload`.\n- [x] OnymIOSTests.MemberProfileTests — Wire-format pin for `MemberProfile`.\n- [x] OnymIOSTests.MessageRepositoryTests — Reactive-surface tests for `MessageRepository`.\n- [x] OnymIOSTests.NostrEventTests — Tests for the NIP-01 wire format and the integrity check that `NostrRelayConnection` runs on every inbound event.\n- [x] OnymIOSTests.NostrInboxTransportTests — Covers the pure event-building and filter-shape paths of the inbox adapter.\n- [x] OnymIOSTests.NostrMessageTransportTests — Covers the pure event-building path of the broadcast adapter.\n- [x] OnymIOSTests.NostrRelaySettingsFlowTests\n- [x] OnymIOSTests.NostrRelaysRepositoryTests — Behavioral tests for `NostrRelaysRepository` covering the first-launch seed, mutation idempotency, and reset-to-default.\n- [x] OnymIOSTests.OnymNostrSignerTests — Hits OnymSDK's BIP340 / secp256k1 FFI through `OnymNostrSigner` — no mocks.\n- [x] OnymIOSTests.RecoveryPhraseBackupFlowTests\n- [x] OnymIOSTests.RelayerConfigurationTests — Pure tests for `RelayerConfiguration.selectURL` — the resolver chain interactors will call per request.\n- [x] OnymIOSTests.RelayerEndpointSchemaTests — Pin the wire-format compat between the iOS app and the published `relayers.json` (and any saves from earlier app versions).\n- [x] OnymIOSTests.RelayerRepositoryTests — Repository against the in-memory fakes — fast, focused on: - construction loads cached state from the store, - list-shaped mutators (add / remove / setPrimary / setStrategy) persist via the store and push a fresh snapshot, - addEndpoint dedupes on URL (idempotent + updates metadata), - removeEndpoint clears the primary marker if it pointed at the removed endpoint, - setPrimary is a no-op when the URL isn't in the configured list, - selectURL respects the strategy stored in the configuration, - background `start()` is idempotent (no double fetch) and silent on error.\n- [x] OnymIOSTests.RelayerSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [x] OnymIOSTests.RelayerSettingsFlowTests\n- [x] OnymIOSTests.SEPContractClientTests — Pure-Swift unit tests for `SEPContractClient` — no real HTTP.\n- [x] OnymIOSTests.SealedEnvelopeTests — Pin the SealedEnvelope wire format.\n- [x] OnymIOSTests.SendMessageInteractorTests\n- [x] OnymIOSTests.SettingsQRCodeTests — Producer-side checks for the inbox-key invite URL emitted by Settings → Invite Key.\n- [x] OnymIOSTests.ShareInviteFlowTests\n- [x] OnymIOSTests.SmokeTests\n  - Smoke test: the OnymSDK SwiftPM dep links and the FFI is reachable from app code.\n- [x] OnymIOSTests.StorageEncryptionTests — Pin AES-GCM roundtrip semantics + Keychain key stability.\n- [x] OnymIOSTests.SwiftDataGroupStoreTests — Round-trip tests for `SwiftDataGroupStore`.\n- [x] OnymIOSTests.SwiftDataInvitationStoreTests — Exercises the real SwiftData backend (in-memory `ModelContainer` so each test gets a fresh, isolated store).\n- [x] OnymIOSTests.SwiftDataMessageStoreTests — Round-trip tests for `SwiftDataMessageStore`.\n- [x] OnymIOSUITests.AnchorsUITests — End-to-end UI tests for Settings → Anchors.\n  - When the manifest publishes no Mainnet contracts, the Mainnet row renders disabled (no NavigationLink) while Testnet stays tappable.\n- [x] OnymIOSUITests.IdentityManagementUITests — End-to-end coverage of the multi-identity surface that landed in PR #56 (the 5-PR multi-identity stack: keychain → repository → group filter → transport fan-out → UI).\n  - 1) Settings → Identities row → list appears with the bootstrapped identity marked Active.\n  - 2) Add Identity sheet → name in → submit → list grows by one.\n  - 3) Picker on Chats switches identities; nav title flips.\n  - 4) Remove flow — name-confirm gate blocks until exact match, list shrinks back after confirmed removal.\n- [x] OnymIOSUITests.RecoveryPhraseBackupUITests — End-to-end coverage of the recovery-phrase backup flow, driven through the live SwiftUI views via `XCUIApplication`.\n  - Full happy path: open Backup → tap Reveal → see a 12-word phrase → answer all three verification rounds correctly → land on the Done screen.\n  - Picking a wrong word during verification keeps the user on the same round and surfaces the inline error message — no silent advance, no false-positive completion.\n  - Launching with `language: \"ru\"` renders Russian copy on Settings (nav title + Backup row) and on the recovery-phrase Intro screen — confirms the localized catalog wires through end to end.\n  - A fresh-keychain launch generates a valid 12-word phrase: every word is non-empty, all-lowercase letters, and the phrase has at least 6 unique words (a sanity guard against a stuck/zeroed seed).\n- [x] OnymIOSUITests.RelayerSettingsUITests — End-to-end UI tests for Settings → Relayer.\n  - On a clean first launch, the Configured-relayers list pre-populates from the published manifest fixture (testnet + mainnet rows visible without any user action).\n  - On a clean first launch, the relayer-selection strategy defaults to Random — the segmented control's \"Random\" segment is the selected one.\n  - Tapping a row's star to mark it primary, then switching the strategy segment from Random to Primary, persists through the UI: the Primary segment is the selected one when the dust settles.\n  - Typing a URL into the custom-relayer field and tapping Add makes the new row appear in the Configured list.\n\n<!-- UI_TESTS_END -->\n\n\n### Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Fresh install via TestFlight or AltStore signs in cleanly\n- [ ] First-run BIP39 generation produces a 12-word phrase\n- [ ] Backup → Reveal → Verify (3 rounds) reaches the Done screen\n- [ ] App launches without splash flicker on cold start\n\n### Identity\n- [ ] Restore from a known mnemonic produces the expected Stellar address\n- [ ] Inbox key in Settings → Advanced matches across reinstalls of the same mnemonic\n\n### Create Group — Tyranny\n- [ ] Step 1 → Step 2 → Create reaches Success against testnet\n- [ ] Group lands in the Chats tab as published-on-chain\n- [ ] Inviting one peer (paste their inbox key) sends an invitation that the receiver can accept\n\n### Create Group — 1-on-1\n- [ ] \"1-on-1\" governance card is selectable on Step 1 (no \"Soon\" pill)\n- [ ] Step 2 requires exactly one peer; CTA reads \"Start dialog\"\n- [ ] Created dialog lands in Chats; both parties see the same group\n\n### Theme + locale\n- [ ] Light theme renders Create Group flow correctly\n- [ ] Dark theme renders the same screens correctly\n- [ ] Russian locale shows localized strings on Settings + Backup\n\n### Anchors + Relayer\n- [ ] Settings → Anchors picker resolves to the latest contracts manifest\n- [ ] Settings → Relayer can add a custom URL and switch primary\n\n### Release artifacts\n- [ ] App Store screenshots refreshed via fastlane (if any UI changed materially)\n- [ ] Push notification entitlement still validates against the bundle ID\n- [ ] IPA installs cleanly through AltStore (the only currently shipped channel)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.51/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.51/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.51/manifest.plist",
      "size_bytes": 5161979,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/100ba243bf614ba1c8c7c01f26125382e6e2d9e3",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/26311700180"
    },
    {
      "platform": "android",
      "version": "v0.0.48",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.48",
      "commit": "b70d8177e41fed7f2e0a68ddbf9810ceb66691a6",
      "commit_short": "b70d8",
      "built_at": "2026-05-22T21:11:47Z",
      "description": "Release v0.0.48",
      "issue": {
        "number": 156,
        "title": "Release: vX.Y.Z",
        "url": "https://github.com/onymchat/onym-android/issues/156",
        "body": "### Release tag\n\nv0.0.48\n\n### Release notes (optional)\n\n_No response_\n\n### Checklist\n\n## UI tests\n\n<!-- UI_TESTS_START -->\n\n- [x] app.onym.android.chain.GroupProofGeneratorFfiTest\n- [x] app.onym.android.group.CreateGroupInteractorTest\n- [x] app.onym.android.group.GroupCommitmentBuilderFfiTest\n- [x] app.onym.android.identity.IdentityRepositoryInvitationDecryptTest\n- [x] app.onym.android.identity.IdentityRepositorySealInvitationTest\n- [x] app.onym.android.identity.IdentityRepositoryTest\n- [x] app.onym.android.inbox.IncomingMessageDispatcherConvergeForwardFfiTest\n- [x] app.onym.android.integration.CreateGroupAnarchyE2ETest\n- [x] app.onym.android.integration.CreateGroupOneOnOneE2ETest\n- [x] app.onym.android.integration.CreateGroupTyrannyE2ETest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupScreenTest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupViewModelTest\n- [x] app.onym.android.transport.nostr.OnymNostrSignerTest\n- [x] app.onym.android.uitests.AnchorsUITest\n- [x] app.onym.android.uitests.IdentitiesUITest\n- [x] app.onym.android.uitests.RelayerSettingsUITest\n\n<!-- UI_TESTS_END -->\n\n## Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Sideload the signed APK on a real device (Android 14+ recommended)\n- [ ] First-launch onboarding flow renders without crash\n- [ ] Bootstrap a new identity, then restart the app and confirm it persists\n- [ ] Restore identity from a 12-word mnemonic, confirm pubkey matches\n\n### Create Group — Tyranny\n- [ ] Create an empty Tyranny group against testnet, confirm it lands on the Stellar contract\n- [ ] Create a Tyranny group with 1 invitee, confirm invitee receives sealed envelope\n- [ ] Open Settings → toggle Mainnet, confirm next create resolves the mainnet contract binding\n\n### Create Group — 1-on-1\n- [ ] Pick the 1-on-1 governance card, paste an invitee inbox key, confirm \"Start 1-on-1\" CTA enables only at exactly 1 invitee\n- [ ] Create the 1-on-1 group, confirm anchor on testnet\n- [ ] Confirm invitee receives sealed envelope carrying the ephemeral `sk_1`\n\n### Settings + relayer + anchors\n- [ ] Settings → Relayer Settings → swipe-delete an endpoint, confirm primary marker clears if deleted\n- [ ] Settings → Anchors → drill into Testnet → Tyranny, pick an older release, confirm persistence\n- [ ] Add a custom relayer URL, confirm it lands as `custom` network endpoint\n\n### Theming + chrome\n- [ ] Toggle system dark mode, every Create Group screen renders correctly in both themes\n- [ ] Bottom-tab Chats / Settings / Search switching works without flicker\n- [ ] Recovery-phrase backup flow (Settings → Backup) walks Intro → Reveal → Verify → Done\n\n### Production sanity\n- [ ] APK installs without \"package conflicts with existing\" prompt over the prior release\n- [ ] Network Settings shows the deployed `relayer.onym.chat` endpoint by default after auto-populate\n- [ ] AltStore source updates with the new IPA URL once notarization mirrors land (separate workflow — leave unchecked if N/A)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "https://onym.app/qa/android/v0.0.48/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.48/onym.apk",
      "size_bytes": 36523626,
      "commit_url": "https://github.com/onymchat/onym-android/commit/b70d8177e41fed7f2e0a68ddbf9810ceb66691a6",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/26311811441"
    },
    {
      "platform": "android",
      "version": "v0.0.47-5174f",
      "kind": "branch",
      "branch": "pr-chat-a10-thread-polish",
      "tag": "v0.0.47",
      "commit": "5174f1e79cc5654ea417a6271a817e1e84ac0ef1",
      "commit_short": "5174f",
      "built_at": "2026-05-19T20:14:46Z",
      "description": "PR 10/N chat stack: chat-thread polish (nav subtitle, status, retry, send, input panel, message list)",
      "issue": {
        "number": 150,
        "title": "feat(chats): chat-thread nav subtitle (PR 10/N chat stack)",
        "url": "https://github.com/onymchat/onym-android/issues/150",
        "body": "**Stacked on #149.** Mirrors the nav-subtitle piece of [onym-ios PR #156](https://github.com/onymchat/onym-ios/pull/156). The empty-state (\"No messages yet. Say hi.\") that iOS PR #156 also added already shipped on Android in PR A6's `EmptyThread` composable, so this PR is just the nav title.\n\n## What's in here\n\n`ChatThreadTitle` composable replaces the single `TopAppBar` title `Text` with a `Column`:\n- Group name on top (`typography.titleMedium`).\n- \"N members\" subtitle below (`typography.bodySmall`, `onSurfaceVariant`).\n- Subtitle is hidden when `memberCount <= 1` — singleton groups (just the creator) and not-yet-loaded groups would render an awkward \"0 members\" / \"1 member\" otherwise. Same gate as iOS.\n\n`memberCountSubtitle(memberCount)` is a pure helper — the `<= 1` gate + the \"N members\" pluralization live in one place and the unit test pins both without standing up Compose.\n\nMember count comes straight from `group.memberProfiles.size` on the VM's existing `group: StateFlow<ChatGroup?>` — when an admin admits a new joiner and the dispatcher's `MemberAnnouncementPayload` fast path lands, the bar updates live without any new VM API surface.\n\n## Divergence from iOS / out of scope\n\n- **iOS bundled the empty-state into this PR**; on Android it landed in PR A6 because the Compose mirror of iOS PR #152's message-list rendering was a single-screen edit that naturally included the empty branch.\n- **Group avatar in nav: deferred.** Would need either reaching into `OnymGroupAvatar` (a Create-Group surface today) or building a chat-tailored variant. iOS deferred for the same reason.\n- **`ChatsScreen` last-message preview + unread dot: deferred** — needs a per-group \"last message\" query on `MessageRepository` plus a `PersistedGroup` schema bump for `lastReadMessageID`. Worth its own focused PR.\n\n## Tests (4 new)\n\n**`ChatThreadTitleTest`**:\n- `memberCountSubtitle(0)` → `null` (hide on pre-load)\n- `memberCountSubtitle(1)` → `null` (singleton group)\n- `memberCountSubtitle(2)` → `\"2 members\"`\n- `memberCountSubtitle(100)` → `\"100 members\"`\n\nFull `:app:testDebugUnitTest` suite — **533 / 533 pass**, 0 failures, 0 errors, 0 regressions. `assembleDebug` clean.\n\n## Plan context\n\nPR 10 of N in the Android chat stack. **PR A1** (#141) payload • **PR A2** (#142) persistence • **PR A3** (#143) Ed25519 sender pubkey • **PR A4** (#144) dispatcher + send interactor • **PR A5** (#145) Compose chat-thread shell • **PR A6** (#146) message list rendering • **PR A7** (#147) input panel + keyboard avoidance • **PR A8** (#148) wire Send button • **PR A9** (#149) status indicator + retry.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)"
      },
      "install_url": "https://onym.app/qa/android/v0.0.47-5174f/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.47-5174f/onym.apk",
      "size_bytes": 36474474,
      "commit_url": "https://github.com/onymchat/onym-android/commit/5174f1e79cc5654ea417a6271a817e1e84ac0ef1",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/26122474196"
    },
    {
      "platform": "ios",
      "version": "v0.0.50-8abbd",
      "kind": "branch",
      "branch": "pr-chat-10-thread-polish",
      "tag": "v0.0.50",
      "commit": "8abbda048b1b0896e563657b22c38195ddfa12a9",
      "commit_short": "8abbd",
      "built_at": "2026-05-19T20:03:43Z",
      "description": "PR #156: chat-thread polish — nav subtitle + empty state",
      "issue": {
        "number": 156,
        "title": "feat(chats): chat-thread polish — nav subtitle + empty state (PR 10/11 chat stack)",
        "url": "https://github.com/onymchat/onym-ios/issues/156",
        "body": "Two view-only improvements that make the chat thread feel complete.\n\n## Stacked on\nBase: \\`pr-chat-9-status-indicator\\` (#155).\n\n## What's in here\n\n**Nav title subtitle.** Replaces the single centered title with a vertical stack: name on top, \"N members\" below. Hidden when `memberCount <= 1` (singleton groups don't need the count; nothing-known groups would render \"0 members\" awkwardly).\n\n**Empty state.** Centered \"No messages yet. Say hi.\" label overlaid on the table view. Toggled on when `update(messages:)` lands an empty array and off as soon as the first row arrives. Lives behind the table (added as a sibling) so the keyboard layout guide affects it the same way as the message list.\n\n## Wire-up\n- `ChatThreadViewController.update(groupName:memberCount:)` replaces the old single-arg overload.\n- The SwiftUI bridge computes `memberCount` from `chatsFlow.groups.first { $0.id == groupID }?.memberProfiles.count` so an admin admitting a new joiner updates the bar live as the announcement lands.\n- The `emptyStateLabel` toggle lives inside the existing `update(messages:)` flow — no new hook surface.\n\n## What's NOT in here — deferred to a focused follow-up\n\nThe plan's polish bucket also included:\n- **Group avatar in the nav.** Would need a UIKit equivalent of `OnymGroupAvatar` or a `UIHostingController` bridge — both heavier than the rest of PR 10.\n- **`ChatsView` last-message preview + timestamp + unread dot.** Needs a per-group \"last message\" query in `MessageRepository` *and* a `PersistedGroup.lastReadMessageID` schema bump. Worth a focused follow-up rather than absorbing here.\n\nPR 10 stays small + view-only so PR 11 (E2E + CI smoke) can ship on the same chain.\n\n## Tests (5 new, suite 622 / 622)\n\n**`ChatThreadViewControllerTests`** +5:\n- `memberCount` 0 / 1 → subtitle hidden\n- `memberCount >= 2` → subtitle visible, pluralized as \"N members\"\n- empty list → empty state visible\n- non-empty list → empty state hidden\n- list cleared again → empty state re-appears (symmetric toggle)\n\nUpdated two pre-existing tests for the `update(groupName:memberCount:)` signature.\n\n## Plan context\nPR 10 of 11. **PR 11 next**: E2E integration test + CI smoke — two-identity round-trip through real `IdentityRepository` + in-memory fake transport, plus a UI smoke that opens a chat and verifies a typed message round-trips.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.50-8abbd/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.50-8abbd/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.50-8abbd/manifest.plist",
      "size_bytes": 5087507,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/8abbda048b1b0896e563657b22c38195ddfa12a9",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/26121956620"
    },
    {
      "platform": "ios",
      "version": "v0.0.50",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.50",
      "commit": "c8380c13df950b14c77bc63b7fb6f12b82d9c623",
      "commit_short": "c8380",
      "built_at": "2026-05-16T06:24:17Z",
      "description": "Release v0.0.50",
      "issue": {
        "number": 145,
        "title": "Release",
        "url": "https://github.com/onymchat/onym-ios/issues/145",
        "body": "### Tag\n\nv0.0.50\n\n### Release notes (optional)\n\n_No response_\n\n### Test results\n\n<!-- UI_TESTS_START -->\n_Discovered 63 XCTestCase classes — boxes flip to `[x]` once `Release` finishes._\n\n- [x] OnymIOSTests.AnchorSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [x] OnymIOSTests.AnchorsPickerFlowTests\n- [x] OnymIOSTests.ApproveRequestsFlowTests\n- [x] OnymIOSTests.CanonicalFrTests — Regression coverage for the bls12-381 canonical-Fr predicate + rejection sampler that `CreateGroupInteractor` uses to mint `groupID`.\n- [x] OnymIOSTests.ChatGroupTests\n- [x] OnymIOSTests.ContractsManifestDecodingTests — Pin the wire format + the unknown-enum-dropping behaviour of the manifest decoder.\n- [x] OnymIOSTests.ContractsManifestFetcherTests — Hits the production fetcher with `StubURLProtocol` (the reusable scaffolding from PR #18) in front.\n- [x] OnymIOSTests.ContractsRepositoryTests — Repository against the in-memory fakes — focused on the binding resolution rules (the part chains will read forever) plus the reactive surface, selection persistence, and silent-on-error background start.\n- [x] OnymIOSTests.CreateGroupFlowTests\n- [x] OnymIOSTests.CreateGroupInteractorTests\n- [x] OnymIOSTests.DeeplinkCaptureTests — Pure-XCTest tests for `DeeplinkCapture.introCapability(from:)`.\n- [x] OnymIOSTests.GroupCommitmentBuilderTests — Unit tests for `GroupCommitmentBuilder`.\n- [x] OnymIOSTests.GroupInvitationPayloadTests — Wire-format pin for `GroupInvitationPayload`.\n- [x] OnymIOSTests.GroupProofGeneratorTests — Real proof generation against the OnymSDK Tyranny circuit.\n- [x] OnymIOSTests.GroupRepositoryTests — Reactive-surface tests for `GroupRepository`.\n- [x] OnymIOSTests.IdentitiesFlowTests\n- [x] OnymIOSTests.IdentityIDTests\n- [x] OnymIOSTests.IdentityKeychainStoreTests — Per-identity keychain store tests.\n- [x] OnymIOSTests.IdentityRepositoryInvitationDecryptTests — Real X25519 + AES-GCM round-trip against `IdentityRepository`.\n- [x] OnymIOSTests.IdentityRepositoryMultiIdentityTests — Multi-identity API surface added in PR-2.\n- [x] OnymIOSTests.IdentityRepositorySealInvitationTests — Sender side of the invitation envelope.\n- [x] OnymIOSTests.IdentityRepositoryTests — Each test uses its own Keychain service so test runs are isolated and do not collide with the production identity item or with each other.\n  - **Derivation fixture.** Locks in derivation against the canonical BIP39 test mnemonic so any change to a salt / info string (HKDF for nostr, BLS, Stellar Ed25519, X25519, or the `sep-inbox-v1` SHA-256 tag) breaks this test loudly.\n- [x] OnymIOSTests.InboxFanoutInteractorTests\n- [x] OnymIOSTests.IncomingInvitationsInteractorTests — Pump tests for the seam-A → interactor → seam-B pattern.\n- [x] OnymIOSTests.IncomingInvitationsRepositoryTests — Repository contract on top of the in-memory store fake — fast, focused on the reactive surface (snapshots emit current value on subscribe + a fresh value after every successful mutation), the mutator semantics, dedup behaviour, and post-#58 the per-identity filter + `removeForOwner` hook.\n- [x] OnymIOSTests.IncomingMessageDispatcherTests\n- [x] OnymIOSTests.CreateGroupOneOnOneE2ETests\n  - Happy path.\n- [x] OnymIOSTests.CreateGroupTyrannyE2ETests\n  - Happy path with zero invitees.\n  - Happy path with one invitee.\n- [x] OnymIOSTests.IntroCapabilityInteropTests — Cross-platform wire-format pin.\n  - `intro_pub` is 32 bytes of `0x01`, `group_id` is 32 bytes of `0x02`.\n  - Confirms UTF-8 round-trips through both platforms' JSON readers — group names like \"Семья\" or \"👨‍👩‍👧\" must survive the wire intact.\n  - Encoder smoke check: decode our own emit + reconstruct.\n- [x] OnymIOSTests.IntroCapabilityTests — Wire-format pin for `IntroCapability`.\n- [x] OnymIOSTests.IntroInboxPumpTests — Behavioral tests for `IntroInboxPump`.\n- [x] OnymIOSTests.InvitationDecryptorTests — Interactor tests against `FakeInvitationEnvelopeDecrypter` — fast, no real crypto.\n- [x] OnymIOSTests.InviteIntroducerTests — Unit tests for `InviteIntroducer` + `IntroKeyStore` contract.\n- [x] OnymIOSTests.JoinFlowTests\n- [x] OnymIOSTests.JoinRequestApproverTests\n- [x] OnymIOSTests.JoinRequestPayloadTests — Wire-format pin for `JoinRequestPayload`.\n- [x] OnymIOSTests.KnownRelayersFetcherTests — Hits the production `URLSession`-backed fetcher with `StubURLProtocol` in front so we can pin the wire format (the `relayers.json` shape we promise to publish) and the error-path behaviour (cache fallback happens at the repository layer; the fetcher itself just throws on any failure).\n- [x] OnymIOSTests.LocalizationCatalogTests — Sanity tests that every chain-layer enum case has a non-empty, genuinely-localised `displayName`.\n  - Reads the source `.xcstrings` directly off disk (fragile under SPM / CI rebuilds where source files live elsewhere — but on the dev machine and the project's GH Actions runner this works fine and gives a one-shot sweep that catches missing translations across the entire catalog, not just chain enums).\n- [x] OnymIOSTests.MemberAnnouncementPayloadTests — Wire-format pin for `MemberAnnouncementPayload`.\n- [x] OnymIOSTests.NostrEventTests — Tests for the NIP-01 wire format and the integrity check that `NostrRelayConnection` runs on every inbound event.\n- [x] OnymIOSTests.NostrInboxTransportTests — Covers the pure event-building and filter-shape paths of the inbox adapter.\n- [x] OnymIOSTests.NostrMessageTransportTests — Covers the pure event-building path of the broadcast adapter.\n- [x] OnymIOSTests.NostrRelaySettingsFlowTests\n- [x] OnymIOSTests.NostrRelaysRepositoryTests — Behavioral tests for `NostrRelaysRepository` covering the first-launch seed, mutation idempotency, and reset-to-default.\n- [x] OnymIOSTests.OnymNostrSignerTests — Hits OnymSDK's BIP340 / secp256k1 FFI through `OnymNostrSigner` — no mocks.\n- [x] OnymIOSTests.RecoveryPhraseBackupFlowTests\n- [x] OnymIOSTests.RelayerConfigurationTests — Pure tests for `RelayerConfiguration.selectURL` — the resolver chain interactors will call per request.\n- [x] OnymIOSTests.RelayerEndpointSchemaTests — Pin the wire-format compat between the iOS app and the published `relayers.json` (and any saves from earlier app versions).\n- [x] OnymIOSTests.RelayerRepositoryTests — Repository against the in-memory fakes — fast, focused on: - construction loads cached state from the store, - list-shaped mutators (add / remove / setPrimary / setStrategy) persist via the store and push a fresh snapshot, - addEndpoint dedupes on URL (idempotent + updates metadata), - removeEndpoint clears the primary marker if it pointed at the removed endpoint, - setPrimary is a no-op when the URL isn't in the configured list, - selectURL respects the strategy stored in the configuration, - background `start()` is idempotent (no double fetch) and silent on error.\n- [x] OnymIOSTests.RelayerSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [x] OnymIOSTests.RelayerSettingsFlowTests\n- [x] OnymIOSTests.SEPContractClientTests — Pure-Swift unit tests for `SEPContractClient` — no real HTTP.\n- [x] OnymIOSTests.SealedEnvelopeTests — Pin the SealedEnvelope wire format.\n- [x] OnymIOSTests.SettingsQRCodeTests — Producer-side checks for the inbox-key invite URL emitted by Settings → Invite Key.\n- [x] OnymIOSTests.ShareInviteFlowTests\n- [x] OnymIOSTests.SmokeTests\n  - Smoke test: the OnymSDK SwiftPM dep links and the FFI is reachable from app code.\n- [x] OnymIOSTests.StorageEncryptionTests — Pin AES-GCM roundtrip semantics + Keychain key stability.\n- [x] OnymIOSTests.SwiftDataGroupStoreTests — Round-trip tests for `SwiftDataGroupStore`.\n- [x] OnymIOSTests.SwiftDataInvitationStoreTests — Exercises the real SwiftData backend (in-memory `ModelContainer` so each test gets a fresh, isolated store).\n- [x] OnymIOSUITests.AnchorsUITests — End-to-end UI tests for Settings → Anchors.\n  - When the manifest publishes no Mainnet contracts, the Mainnet row renders disabled (no NavigationLink) while Testnet stays tappable.\n- [x] OnymIOSUITests.IdentityManagementUITests — End-to-end coverage of the multi-identity surface that landed in PR #56 (the 5-PR multi-identity stack: keychain → repository → group filter → transport fan-out → UI).\n  - 1) Settings → Identities row → list appears with the bootstrapped identity marked Active.\n  - 2) Add Identity sheet → name in → submit → list grows by one.\n  - 3) Picker on Chats switches identities; nav title flips.\n  - 4) Remove flow — name-confirm gate blocks until exact match, list shrinks back after confirmed removal.\n- [x] OnymIOSUITests.RecoveryPhraseBackupUITests — End-to-end coverage of the recovery-phrase backup flow, driven through the live SwiftUI views via `XCUIApplication`.\n  - Full happy path: open Backup → tap Reveal → see a 12-word phrase → answer all three verification rounds correctly → land on the Done screen.\n  - Picking a wrong word during verification keeps the user on the same round and surfaces the inline error message — no silent advance, no false-positive completion.\n  - Launching with `language: \"ru\"` renders Russian copy on Settings (nav title + Backup row) and on the recovery-phrase Intro screen — confirms the localized catalog wires through end to end.\n  - A fresh-keychain launch generates a valid 12-word phrase: every word is non-empty, all-lowercase letters, and the phrase has at least 6 unique words (a sanity guard against a stuck/zeroed seed).\n- [x] OnymIOSUITests.RelayerSettingsUITests — End-to-end UI tests for Settings → Relayer.\n  - On a clean first launch, the Configured-relayers list pre-populates from the published manifest fixture (testnet + mainnet rows visible without any user action).\n  - On a clean first launch, the relayer-selection strategy defaults to Random — the segmented control's \"Random\" segment is the selected one.\n  - Tapping a row's star to mark it primary, then switching the strategy segment from Random to Primary, persists through the UI: the Primary segment is the selected one when the dust settles.\n  - Typing a URL into the custom-relayer field and tapping Add makes the new row appear in the Configured list.\n\n<!-- UI_TESTS_END -->\n\n\n### Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Fresh install via TestFlight or AltStore signs in cleanly\n- [ ] First-run BIP39 generation produces a 12-word phrase\n- [ ] Backup → Reveal → Verify (3 rounds) reaches the Done screen\n- [ ] App launches without splash flicker on cold start\n\n### Identity\n- [ ] Restore from a known mnemonic produces the expected Stellar address\n- [ ] Inbox key in Settings → Advanced matches across reinstalls of the same mnemonic\n\n### Create Group — Tyranny\n- [ ] Step 1 → Step 2 → Create reaches Success against testnet\n- [ ] Group lands in the Chats tab as published-on-chain\n- [ ] Inviting one peer (paste their inbox key) sends an invitation that the receiver can accept\n\n### Create Group — 1-on-1\n- [ ] \"1-on-1\" governance card is selectable on Step 1 (no \"Soon\" pill)\n- [ ] Step 2 requires exactly one peer; CTA reads \"Start dialog\"\n- [ ] Created dialog lands in Chats; both parties see the same group\n\n### Theme + locale\n- [ ] Light theme renders Create Group flow correctly\n- [ ] Dark theme renders the same screens correctly\n- [ ] Russian locale shows localized strings on Settings + Backup\n\n### Anchors + Relayer\n- [ ] Settings → Anchors picker resolves to the latest contracts manifest\n- [ ] Settings → Relayer can add a custom URL and switch primary\n\n### Release artifacts\n- [ ] App Store screenshots refreshed via fastlane (if any UI changed materially)\n- [ ] Push notification entitlement still validates against the bundle ID\n- [ ] IPA installs cleanly through AltStore (the only currently shipped channel)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.50/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.50/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.50/manifest.plist",
      "size_bytes": 5023819,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/c8380c13df950b14c77bc63b7fb6f12b82d9c623",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25954720853"
    },
    {
      "platform": "ios",
      "version": "v0.0.1",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.1",
      "commit": "b2799bcf8767d9608c7d08503df1973242699226",
      "commit_short": "b2799",
      "built_at": "2026-05-16T05:58:00Z",
      "description": "Release v0.0.1",
      "issue": {
        "number": 143,
        "title": "Release",
        "url": "https://github.com/onymchat/onym-ios/issues/143",
        "body": "### Tag\n\nv0.0.1\n\n### Release notes (optional)\n\n_No response_\n\n### Test results\n\n<!-- UI_TESTS_START -->\n_Discovered 63 XCTestCase classes — boxes flip to `[x]` once `Release` finishes._\n\n- [x] OnymIOSTests.AnchorSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [x] OnymIOSTests.AnchorsPickerFlowTests\n- [x] OnymIOSTests.ApproveRequestsFlowTests\n- [x] OnymIOSTests.CanonicalFrTests — Regression coverage for the bls12-381 canonical-Fr predicate + rejection sampler that `CreateGroupInteractor` uses to mint `groupID`.\n- [x] OnymIOSTests.ChatGroupTests\n- [x] OnymIOSTests.ContractsManifestDecodingTests — Pin the wire format + the unknown-enum-dropping behaviour of the manifest decoder.\n- [x] OnymIOSTests.ContractsManifestFetcherTests — Hits the production fetcher with `StubURLProtocol` (the reusable scaffolding from PR #18) in front.\n- [x] OnymIOSTests.ContractsRepositoryTests — Repository against the in-memory fakes — focused on the binding resolution rules (the part chains will read forever) plus the reactive surface, selection persistence, and silent-on-error background start.\n- [x] OnymIOSTests.CreateGroupFlowTests\n- [x] OnymIOSTests.CreateGroupInteractorTests\n- [x] OnymIOSTests.DeeplinkCaptureTests — Pure-XCTest tests for `DeeplinkCapture.introCapability(from:)`.\n- [x] OnymIOSTests.GroupCommitmentBuilderTests — Unit tests for `GroupCommitmentBuilder`.\n- [x] OnymIOSTests.GroupInvitationPayloadTests — Wire-format pin for `GroupInvitationPayload`.\n- [x] OnymIOSTests.GroupProofGeneratorTests — Real proof generation against the OnymSDK Tyranny circuit.\n- [x] OnymIOSTests.GroupRepositoryTests — Reactive-surface tests for `GroupRepository`.\n- [x] OnymIOSTests.IdentitiesFlowTests\n- [x] OnymIOSTests.IdentityIDTests\n- [x] OnymIOSTests.IdentityKeychainStoreTests — Per-identity keychain store tests.\n- [x] OnymIOSTests.IdentityRepositoryInvitationDecryptTests — Real X25519 + AES-GCM round-trip against `IdentityRepository`.\n- [x] OnymIOSTests.IdentityRepositoryMultiIdentityTests — Multi-identity API surface added in PR-2.\n- [x] OnymIOSTests.IdentityRepositorySealInvitationTests — Sender side of the invitation envelope.\n- [x] OnymIOSTests.IdentityRepositoryTests — Each test uses its own Keychain service so test runs are isolated and do not collide with the production identity item or with each other.\n  - **Derivation fixture.** Locks in derivation against the canonical BIP39 test mnemonic so any change to a salt / info string (HKDF for nostr, BLS, Stellar Ed25519, X25519, or the `sep-inbox-v1` SHA-256 tag) breaks this test loudly.\n- [x] OnymIOSTests.InboxFanoutInteractorTests\n- [x] OnymIOSTests.IncomingInvitationsInteractorTests — Pump tests for the seam-A → interactor → seam-B pattern.\n- [x] OnymIOSTests.IncomingInvitationsRepositoryTests — Repository contract on top of the in-memory store fake — fast, focused on the reactive surface (snapshots emit current value on subscribe + a fresh value after every successful mutation), the mutator semantics, dedup behaviour, and post-#58 the per-identity filter + `removeForOwner` hook.\n- [x] OnymIOSTests.IncomingMessageDispatcherTests\n- [x] OnymIOSTests.CreateGroupOneOnOneE2ETests\n  - Happy path.\n- [x] OnymIOSTests.CreateGroupTyrannyE2ETests\n  - Happy path with zero invitees.\n  - Happy path with one invitee.\n- [x] OnymIOSTests.IntroCapabilityInteropTests — Cross-platform wire-format pin.\n  - `intro_pub` is 32 bytes of `0x01`, `group_id` is 32 bytes of `0x02`.\n  - Confirms UTF-8 round-trips through both platforms' JSON readers — group names like \"Семья\" or \"👨‍👩‍👧\" must survive the wire intact.\n  - Encoder smoke check: decode our own emit + reconstruct.\n- [x] OnymIOSTests.IntroCapabilityTests — Wire-format pin for `IntroCapability`.\n- [x] OnymIOSTests.IntroInboxPumpTests — Behavioral tests for `IntroInboxPump`.\n- [x] OnymIOSTests.InvitationDecryptorTests — Interactor tests against `FakeInvitationEnvelopeDecrypter` — fast, no real crypto.\n- [x] OnymIOSTests.InviteIntroducerTests — Unit tests for `InviteIntroducer` + `IntroKeyStore` contract.\n- [x] OnymIOSTests.JoinFlowTests\n- [x] OnymIOSTests.JoinRequestApproverTests\n- [x] OnymIOSTests.JoinRequestPayloadTests — Wire-format pin for `JoinRequestPayload`.\n- [x] OnymIOSTests.KnownRelayersFetcherTests — Hits the production `URLSession`-backed fetcher with `StubURLProtocol` in front so we can pin the wire format (the `relayers.json` shape we promise to publish) and the error-path behaviour (cache fallback happens at the repository layer; the fetcher itself just throws on any failure).\n- [x] OnymIOSTests.LocalizationCatalogTests — Sanity tests that every chain-layer enum case has a non-empty, genuinely-localised `displayName`.\n  - Reads the source `.xcstrings` directly off disk (fragile under SPM / CI rebuilds where source files live elsewhere — but on the dev machine and the project's GH Actions runner this works fine and gives a one-shot sweep that catches missing translations across the entire catalog, not just chain enums).\n- [x] OnymIOSTests.MemberAnnouncementPayloadTests — Wire-format pin for `MemberAnnouncementPayload`.\n- [x] OnymIOSTests.NostrEventTests — Tests for the NIP-01 wire format and the integrity check that `NostrRelayConnection` runs on every inbound event.\n- [x] OnymIOSTests.NostrInboxTransportTests — Covers the pure event-building and filter-shape paths of the inbox adapter.\n- [x] OnymIOSTests.NostrMessageTransportTests — Covers the pure event-building path of the broadcast adapter.\n- [x] OnymIOSTests.NostrRelaySettingsFlowTests\n- [x] OnymIOSTests.NostrRelaysRepositoryTests — Behavioral tests for `NostrRelaysRepository` covering the first-launch seed, mutation idempotency, and reset-to-default.\n- [x] OnymIOSTests.OnymNostrSignerTests — Hits OnymSDK's BIP340 / secp256k1 FFI through `OnymNostrSigner` — no mocks.\n- [x] OnymIOSTests.RecoveryPhraseBackupFlowTests\n- [x] OnymIOSTests.RelayerConfigurationTests — Pure tests for `RelayerConfiguration.selectURL` — the resolver chain interactors will call per request.\n- [x] OnymIOSTests.RelayerEndpointSchemaTests — Pin the wire-format compat between the iOS app and the published `relayers.json` (and any saves from earlier app versions).\n- [x] OnymIOSTests.RelayerRepositoryTests — Repository against the in-memory fakes — fast, focused on: - construction loads cached state from the store, - list-shaped mutators (add / remove / setPrimary / setStrategy) persist via the store and push a fresh snapshot, - addEndpoint dedupes on URL (idempotent + updates metadata), - removeEndpoint clears the primary marker if it pointed at the removed endpoint, - setPrimary is a no-op when the URL isn't in the configured list, - selectURL respects the strategy stored in the configuration, - background `start()` is idempotent (no double fetch) and silent on error.\n- [x] OnymIOSTests.RelayerSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [x] OnymIOSTests.RelayerSettingsFlowTests\n- [x] OnymIOSTests.SEPContractClientTests — Pure-Swift unit tests for `SEPContractClient` — no real HTTP.\n- [x] OnymIOSTests.SealedEnvelopeTests — Pin the SealedEnvelope wire format.\n- [x] OnymIOSTests.SettingsQRCodeTests — Producer-side checks for the inbox-key invite URL emitted by Settings → Invite Key.\n- [x] OnymIOSTests.ShareInviteFlowTests\n- [x] OnymIOSTests.SmokeTests\n  - Smoke test: the OnymSDK SwiftPM dep links and the FFI is reachable from app code.\n- [x] OnymIOSTests.StorageEncryptionTests — Pin AES-GCM roundtrip semantics + Keychain key stability.\n- [x] OnymIOSTests.SwiftDataGroupStoreTests — Round-trip tests for `SwiftDataGroupStore`.\n- [x] OnymIOSTests.SwiftDataInvitationStoreTests — Exercises the real SwiftData backend (in-memory `ModelContainer` so each test gets a fresh, isolated store).\n- [x] OnymIOSUITests.AnchorsUITests — End-to-end UI tests for Settings → Anchors.\n  - When the manifest publishes no Mainnet contracts, the Mainnet row renders disabled (no NavigationLink) while Testnet stays tappable.\n- [x] OnymIOSUITests.IdentityManagementUITests — End-to-end coverage of the multi-identity surface that landed in PR #56 (the 5-PR multi-identity stack: keychain → repository → group filter → transport fan-out → UI).\n  - 1) Settings → Identities row → list appears with the bootstrapped identity marked Active.\n  - 2) Add Identity sheet → name in → submit → list grows by one.\n  - 3) Picker on Chats switches identities; nav title flips.\n  - 4) Remove flow — name-confirm gate blocks until exact match, list shrinks back after confirmed removal.\n- [x] OnymIOSUITests.RecoveryPhraseBackupUITests — End-to-end coverage of the recovery-phrase backup flow, driven through the live SwiftUI views via `XCUIApplication`.\n  - Full happy path: open Backup → tap Reveal → see a 12-word phrase → answer all three verification rounds correctly → land on the Done screen.\n  - Picking a wrong word during verification keeps the user on the same round and surfaces the inline error message — no silent advance, no false-positive completion.\n  - Launching with `language: \"ru\"` renders Russian copy on Settings (nav title + Backup row) and on the recovery-phrase Intro screen — confirms the localized catalog wires through end to end.\n  - A fresh-keychain launch generates a valid 12-word phrase: every word is non-empty, all-lowercase letters, and the phrase has at least 6 unique words (a sanity guard against a stuck/zeroed seed).\n- [x] OnymIOSUITests.RelayerSettingsUITests — End-to-end UI tests for Settings → Relayer.\n  - On a clean first launch, the Configured-relayers list pre-populates from the published manifest fixture (testnet + mainnet rows visible without any user action).\n  - On a clean first launch, the relayer-selection strategy defaults to Random — the segmented control's \"Random\" segment is the selected one.\n  - Tapping a row's star to mark it primary, then switching the strategy segment from Random to Primary, persists through the UI: the Primary segment is the selected one when the dust settles.\n  - Typing a URL into the custom-relayer field and tapping Add makes the new row appear in the Configured list.\n\n<!-- UI_TESTS_END -->\n\n\n### Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Fresh install via TestFlight or AltStore signs in cleanly\n- [ ] First-run BIP39 generation produces a 12-word phrase\n- [ ] Backup → Reveal → Verify (3 rounds) reaches the Done screen\n- [ ] App launches without splash flicker on cold start\n\n### Identity\n- [ ] Restore from a known mnemonic produces the expected Stellar address\n- [ ] Inbox key in Settings → Advanced matches across reinstalls of the same mnemonic\n\n### Create Group — Tyranny\n- [ ] Step 1 → Step 2 → Create reaches Success against testnet\n- [ ] Group lands in the Chats tab as published-on-chain\n- [ ] Inviting one peer (paste their inbox key) sends an invitation that the receiver can accept\n\n### Create Group — 1-on-1\n- [ ] \"1-on-1\" governance card is selectable on Step 1 (no \"Soon\" pill)\n- [ ] Step 2 requires exactly one peer; CTA reads \"Start dialog\"\n- [ ] Created dialog lands in Chats; both parties see the same group\n\n### Theme + locale\n- [ ] Light theme renders Create Group flow correctly\n- [ ] Dark theme renders the same screens correctly\n- [ ] Russian locale shows localized strings on Settings + Backup\n\n### Anchors + Relayer\n- [ ] Settings → Anchors picker resolves to the latest contracts manifest\n- [ ] Settings → Relayer can add a custom URL and switch primary\n\n### Release artifacts\n- [ ] App Store screenshots refreshed via fastlane (if any UI changed materially)\n- [ ] Push notification entitlement still validates against the bundle ID\n- [ ] IPA installs cleanly through AltStore (the only currently shipped channel)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.1/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.1/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.1/manifest.plist",
      "size_bytes": 5023816,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/b2799bcf8767d9608c7d08503df1973242699226",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25954203601"
    },
    {
      "platform": "android",
      "version": "v0.0.47",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.47",
      "commit": "9f203cafd480077c219b1326b968fce87602e1ae",
      "commit_short": "9f203",
      "built_at": "2026-05-16T05:54:16Z",
      "description": "Release v0.0.47",
      "issue": {
        "number": 140,
        "title": "Release: vX.Y.Z",
        "url": "https://github.com/onymchat/onym-android/issues/140",
        "body": "### Release tag\n\nv0.0.47\n\n### Release notes (optional)\n\n_No response_\n\n### Checklist\n\n## UI tests\n\n<!-- UI_TESTS_START -->\n\n- [x] app.onym.android.chain.GroupProofGeneratorFfiTest\n- [x] app.onym.android.group.CreateGroupInteractorTest\n- [x] app.onym.android.group.GroupCommitmentBuilderFfiTest\n- [x] app.onym.android.identity.IdentityRepositoryInvitationDecryptTest\n- [x] app.onym.android.identity.IdentityRepositorySealInvitationTest\n- [x] app.onym.android.identity.IdentityRepositoryTest\n- [x] app.onym.android.integration.CreateGroupAnarchyE2ETest\n- [x] app.onym.android.integration.CreateGroupOneOnOneE2ETest\n- [x] app.onym.android.integration.CreateGroupTyrannyE2ETest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupScreenTest\n- [x] app.onym.android.recovery.RecoveryPhraseBackupViewModelTest\n- [x] app.onym.android.transport.nostr.OnymNostrSignerTest\n- [x] app.onym.android.uitests.AnchorsUITest\n- [x] app.onym.android.uitests.IdentitiesUITest\n- [x] app.onym.android.uitests.RelayerSettingsUITest\n\n<!-- UI_TESTS_END -->\n\n## Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Sideload the signed APK on a real device (Android 14+ recommended)\n- [ ] First-launch onboarding flow renders without crash\n- [ ] Bootstrap a new identity, then restart the app and confirm it persists\n- [ ] Restore identity from a 12-word mnemonic, confirm pubkey matches\n\n### Create Group — Tyranny\n- [ ] Create an empty Tyranny group against testnet, confirm it lands on the Stellar contract\n- [ ] Create a Tyranny group with 1 invitee, confirm invitee receives sealed envelope\n- [ ] Open Settings → toggle Mainnet, confirm next create resolves the mainnet contract binding\n\n### Create Group — 1-on-1\n- [ ] Pick the 1-on-1 governance card, paste an invitee inbox key, confirm \"Start 1-on-1\" CTA enables only at exactly 1 invitee\n- [ ] Create the 1-on-1 group, confirm anchor on testnet\n- [ ] Confirm invitee receives sealed envelope carrying the ephemeral `sk_1`\n\n### Settings + relayer + anchors\n- [ ] Settings → Relayer Settings → swipe-delete an endpoint, confirm primary marker clears if deleted\n- [ ] Settings → Anchors → drill into Testnet → Tyranny, pick an older release, confirm persistence\n- [ ] Add a custom relayer URL, confirm it lands as `custom` network endpoint\n\n### Theming + chrome\n- [ ] Toggle system dark mode, every Create Group screen renders correctly in both themes\n- [ ] Bottom-tab Chats / Settings / Search switching works without flicker\n- [ ] Recovery-phrase backup flow (Settings → Backup) walks Intro → Reveal → Verify → Done\n\n### Production sanity\n- [ ] APK installs without \"package conflicts with existing\" prompt over the prior release\n- [ ] Network Settings shows the deployed `relayer.onym.chat` endpoint by default after auto-populate\n- [ ] AltStore source updates with the new IPA URL once notarization mirrors land (separate workflow — leave unchecked if N/A)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "https://onym.app/qa/android/v0.0.47/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.47/onym.apk",
      "size_bytes": 36425322,
      "commit_url": "https://github.com/onymchat/onym-android/commit/9f203cafd480077c219b1326b968fce87602e1ae",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/25954197941"
    },
    {
      "platform": "ios",
      "version": "v0.0.48-c9a8f",
      "kind": "branch",
      "branch": "agent/issue-112",
      "tag": "v0.0.48",
      "commit": "c9a8f49b6c3e89a229556b683721f4e08df31f89",
      "commit_short": "c9a8f",
      "built_at": "2026-05-15T15:53:59Z",
      "description": "fix(create-group): render generated name as placeholder, not real input (PR #135)",
      "issue": {
        "number": 135,
        "title": "fix(create-group): render generated name as placeholder, not real input (closes #112)",
        "url": "https://github.com/onymchat/onym-ios/issues/135",
        "body": "The Step 1 group-name field was seeded with the generated \"Adjective\nNoun\" suggestion as the actual TextField value, so it rendered in the\nfull-opacity input color and looked indistinguishable from text the\nuser had typed. Users could miss that they were supposed to (or could)\nchange it, and unknowingly create groups called \"Silver Atlas\".\n\nLeave `name` empty at init and reset so the TextField's `prompt` —\nalready styled with the muted `OnymTokens.text3` color — surfaces as a\nproper iOS placeholder. The existing `effectiveName` fallback already\nsubstitutes `generatedName` at submit when the user leaves the field\nblank, so the \"hit Create immediately\" path still works.\n\nThe first-focus-clears-placeholder dance is no longer needed and is\nremoved along with its tests.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.48-c9a8f/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.48-c9a8f/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.48-c9a8f/manifest.plist",
      "size_bytes": 5120823,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/c9a8f49b6c3e89a229556b683721f4e08df31f89",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25926709845"
    },
    {
      "platform": "ios",
      "version": "v0.0.48-ed01c",
      "kind": "branch",
      "branch": "agent/issue-137",
      "tag": "v0.0.48",
      "commit": "ed01c96f8a4a97571b638a40dcd36f69fe00a170",
      "commit_short": "ed01c",
      "built_at": "2026-05-15T14:59:23Z",
      "description": "PR #138: surface invite QR on Chats toolbar; fix dead-end hint (closes #137)",
      "issue": {
        "number": 138,
        "title": "feat(chats): surface invite QR on Chats toolbar; fix dead-end hint (closes #137)",
        "url": "https://github.com/onymchat/onym-ios/issues/138",
        "body": "Adds a QR toolbar icon to the Chats tab that opens ShareKeyView, so users\ncan share their invite without drilling into Settings. Updates the Invite\nby Key hint in Create Group to point at the new entry point instead of the\nnon-existent \"Settings -> Advanced\" path. Renames settingsInviteURL's\nparameter from blsPublicKey: to inboxPublicKey: to match what it actually\ncarries (X25519 inbox key, not the BLS governance key) and adds a doc note\nto keep the next contributor from re-introducing the drift.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.48-ed01c/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.48-ed01c/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.48-ed01c/manifest.plist",
      "size_bytes": 5123838,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/ed01c96f8a4a97571b638a40dcd36f69fe00a170",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25922151059"
    },
    {
      "platform": "android",
      "version": "v0.0.45",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.45",
      "commit": "3dabed106ec0cf08430eb78ff929112a56b30e6c",
      "commit_short": "3dabe",
      "built_at": "2026-05-10T21:20:59Z",
      "description": "Release v0.0.45",
      "issue": {
        "number": 135,
        "title": "Release: vX.Y.Z",
        "url": "https://github.com/onymchat/onym-android/issues/135",
        "body": "### Release tag\n\nv0.0.45\n\n### Release notes (optional)\n\n_No response_\n\n### Checklist\n\n## UI tests\n\n<!-- UI_TESTS_START -->\n\n- [x] chat.onym.android.chain.GroupProofGeneratorFfiTest\n- [x] chat.onym.android.group.CreateGroupInteractorTest\n- [x] chat.onym.android.group.GroupCommitmentBuilderFfiTest\n- [x] chat.onym.android.identity.IdentityRepositoryInvitationDecryptTest\n- [x] chat.onym.android.identity.IdentityRepositorySealInvitationTest\n- [x] chat.onym.android.identity.IdentityRepositoryTest\n- [x] chat.onym.android.integration.CreateGroupAnarchyE2ETest\n- [x] chat.onym.android.integration.CreateGroupOneOnOneE2ETest\n- [x] chat.onym.android.integration.CreateGroupTyrannyE2ETest\n- [x] chat.onym.android.recovery.RecoveryPhraseBackupScreenTest\n- [x] chat.onym.android.recovery.RecoveryPhraseBackupViewModelTest\n- [x] chat.onym.android.transport.nostr.OnymNostrSignerTest\n- [x] chat.onym.android.uitests.AnchorsUITest\n- [x] chat.onym.android.uitests.IdentitiesUITest\n- [x] chat.onym.android.uitests.RelayerSettingsUITest\n\n<!-- UI_TESTS_END -->\n\n## Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Sideload the signed APK on a real device (Android 14+ recommended)\n- [ ] First-launch onboarding flow renders without crash\n- [ ] Bootstrap a new identity, then restart the app and confirm it persists\n- [ ] Restore identity from a 12-word mnemonic, confirm pubkey matches\n\n### Create Group — Tyranny\n- [ ] Create an empty Tyranny group against testnet, confirm it lands on the Stellar contract\n- [ ] Create a Tyranny group with 1 invitee, confirm invitee receives sealed envelope\n- [ ] Open Settings → toggle Mainnet, confirm next create resolves the mainnet contract binding\n\n### Create Group — 1-on-1\n- [ ] Pick the 1-on-1 governance card, paste an invitee inbox key, confirm \"Start 1-on-1\" CTA enables only at exactly 1 invitee\n- [ ] Create the 1-on-1 group, confirm anchor on testnet\n- [ ] Confirm invitee receives sealed envelope carrying the ephemeral `sk_1`\n\n### Settings + relayer + anchors\n- [ ] Settings → Relayer Settings → swipe-delete an endpoint, confirm primary marker clears if deleted\n- [ ] Settings → Anchors → drill into Testnet → Tyranny, pick an older release, confirm persistence\n- [ ] Add a custom relayer URL, confirm it lands as `custom` network endpoint\n\n### Theming + chrome\n- [ ] Toggle system dark mode, every Create Group screen renders correctly in both themes\n- [ ] Bottom-tab Chats / Settings / Search switching works without flicker\n- [ ] Recovery-phrase backup flow (Settings → Backup) walks Intro → Reveal → Verify → Done\n\n### Production sanity\n- [ ] APK installs without \"package conflicts with existing\" prompt over the prior release\n- [ ] Network Settings shows the deployed `relayer.onym.chat` endpoint by default after auto-populate\n- [ ] AltStore source updates with the new IPA URL once notarization mirrors land (separate workflow — leave unchecked if N/A)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "https://onym.app/qa/android/v0.0.45/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.45/onym.apk",
      "size_bytes": 36425322,
      "commit_url": "https://github.com/onymchat/onym-android/commit/3dabed106ec0cf08430eb78ff929112a56b30e6c",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/25639876199"
    },
    {
      "platform": "ios",
      "version": "v0.0.49",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.49",
      "commit": "95091664ba834ba05bfbb3a5191122f3d80d72e3",
      "commit_short": "95091",
      "built_at": "2026-05-10T21:20:04Z",
      "description": "Release v0.0.49",
      "issue": {
        "number": 133,
        "title": "Release",
        "url": "https://github.com/onymchat/onym-ios/issues/133",
        "body": "### Tag\n\nv0.0.49\n\n### Release notes (optional)\n\n_No response_\n\n### Test results\n\n<!-- UI_TESTS_START -->\n_Discovered 63 XCTestCase classes — boxes flip to `[x]` once `Release` finishes._\n\n- [ ] OnymIOSTests.AnchorSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [ ] OnymIOSTests.AnchorsPickerFlowTests\n- [ ] OnymIOSTests.ApproveRequestsFlowTests\n- [ ] OnymIOSTests.CanonicalFrTests — Regression coverage for the bls12-381 canonical-Fr predicate + rejection sampler that `CreateGroupInteractor` uses to mint `groupID`.\n- [ ] OnymIOSTests.ChatGroupTests\n- [ ] OnymIOSTests.ContractsManifestDecodingTests — Pin the wire format + the unknown-enum-dropping behaviour of the manifest decoder.\n- [ ] OnymIOSTests.ContractsManifestFetcherTests — Hits the production fetcher with `StubURLProtocol` (the reusable scaffolding from PR #18) in front.\n- [ ] OnymIOSTests.ContractsRepositoryTests — Repository against the in-memory fakes — focused on the binding resolution rules (the part chains will read forever) plus the reactive surface, selection persistence, and silent-on-error background start.\n- [ ] OnymIOSTests.CreateGroupFlowTests\n- [ ] OnymIOSTests.CreateGroupInteractorTests\n- [ ] OnymIOSTests.DeeplinkCaptureTests — Pure-XCTest tests for `DeeplinkCapture.introCapability(from:)`.\n- [ ] OnymIOSTests.GroupCommitmentBuilderTests — Unit tests for `GroupCommitmentBuilder`.\n- [ ] OnymIOSTests.GroupInvitationPayloadTests — Wire-format pin for `GroupInvitationPayload`.\n- [ ] OnymIOSTests.GroupProofGeneratorTests — Real proof generation against the OnymSDK Tyranny circuit.\n- [ ] OnymIOSTests.GroupRepositoryTests — Reactive-surface tests for `GroupRepository`.\n- [ ] OnymIOSTests.IdentitiesFlowTests\n- [ ] OnymIOSTests.IdentityIDTests\n- [ ] OnymIOSTests.IdentityKeychainStoreTests — Per-identity keychain store tests.\n- [ ] OnymIOSTests.IdentityRepositoryInvitationDecryptTests — Real X25519 + AES-GCM round-trip against `IdentityRepository`.\n- [ ] OnymIOSTests.IdentityRepositoryMultiIdentityTests — Multi-identity API surface added in PR-2.\n- [ ] OnymIOSTests.IdentityRepositorySealInvitationTests — Sender side of the invitation envelope.\n- [ ] OnymIOSTests.IdentityRepositoryTests — Each test uses its own Keychain service so test runs are isolated and do not collide with the production identity item or with each other.\n  - **Cross-platform interop fixture.** Locks in derivation against the canonical BIP39 test mnemonic so any change to a salt / info string (HKDF for nostr, BLS, Stellar Ed25519, X25519, or the `sep-inbox-v1` SHA-256 tag) breaks this test loudly.\n- [ ] OnymIOSTests.InboxFanoutInteractorTests\n- [ ] OnymIOSTests.IncomingInvitationsInteractorTests — Pump tests for the seam-A → interactor → seam-B pattern.\n- [ ] OnymIOSTests.IncomingInvitationsRepositoryTests — Repository contract on top of the in-memory store fake — fast, focused on the reactive surface (snapshots emit current value on subscribe + a fresh value after every successful mutation), the mutator semantics, dedup behaviour, and post-#58 the per-identity filter + `removeForOwner` hook.\n- [ ] OnymIOSTests.IncomingMessageDispatcherTests\n- [ ] OnymIOSTests.CreateGroupOneOnOneE2ETests\n  - Happy path.\n- [ ] OnymIOSTests.CreateGroupTyrannyE2ETests\n  - Happy path with zero invitees.\n  - Happy path with one invitee.\n- [ ] OnymIOSTests.IntroCapabilityInteropTests — Cross-platform wire-format pin.\n  - `intro_pub` is 32 bytes of `0x01`, `group_id` is 32 bytes of `0x02`.\n  - Confirms UTF-8 round-trips through both platforms' JSON readers — group names like \"Семья\" or \"👨‍👩‍👧\" must survive the wire intact.\n  - Encoder smoke check: decode our own emit + reconstruct.\n- [ ] OnymIOSTests.IntroCapabilityTests — Wire-format pin for `IntroCapability`.\n- [ ] OnymIOSTests.IntroInboxPumpTests — Behavioral tests for `IntroInboxPump`.\n- [ ] OnymIOSTests.InvitationDecryptorTests — Interactor tests against `FakeInvitationEnvelopeDecrypter` — fast, no real crypto.\n- [ ] OnymIOSTests.InviteIntroducerTests — Unit tests for `InviteIntroducer` + `IntroKeyStore` contract.\n- [ ] OnymIOSTests.JoinFlowTests\n- [ ] OnymIOSTests.JoinRequestApproverTests\n- [ ] OnymIOSTests.JoinRequestPayloadTests — Wire-format pin for `JoinRequestPayload`.\n- [ ] OnymIOSTests.KnownRelayersFetcherTests — Hits the production `URLSession`-backed fetcher with `StubURLProtocol` in front so we can pin the wire format (the `relayers.json` shape we promise to publish) and the error-path behaviour (cache fallback happens at the repository layer; the fetcher itself just throws on any failure).\n- [ ] OnymIOSTests.LocalizationCatalogTests — Sanity tests that every chain-layer enum case has a non-empty, genuinely-localised `displayName`.\n  - Reads the source `.xcstrings` directly off disk (fragile under SPM / CI rebuilds where source files live elsewhere — but on the dev machine and the project's GH Actions runner this works fine and gives a one-shot sweep that catches missing translations across the entire catalog, not just chain enums).\n- [ ] OnymIOSTests.MemberAnnouncementPayloadTests — Wire-format pin for `MemberAnnouncementPayload`.\n- [ ] OnymIOSTests.NostrEventTests — Tests for the NIP-01 wire format and the integrity check that `NostrRelayConnection` runs on every inbound event.\n- [ ] OnymIOSTests.NostrInboxTransportTests — Covers the pure event-building and filter-shape paths of the inbox adapter.\n- [ ] OnymIOSTests.NostrMessageTransportTests — Covers the pure event-building path of the broadcast adapter.\n- [ ] OnymIOSTests.NostrRelaySettingsFlowTests\n- [ ] OnymIOSTests.NostrRelaysRepositoryTests — Behavioral tests for `NostrRelaysRepository` covering the first-launch seed, mutation idempotency, and reset-to-default.\n- [ ] OnymIOSTests.OnymNostrSignerTests — Hits OnymSDK's BIP340 / secp256k1 FFI through `OnymNostrSigner` — no mocks.\n- [ ] OnymIOSTests.RecoveryPhraseBackupFlowTests\n- [ ] OnymIOSTests.RelayerConfigurationTests — Pure tests for `RelayerConfiguration.selectURL` — the resolver chain interactors will call per request.\n- [ ] OnymIOSTests.RelayerEndpointSchemaTests — Pin the wire-format compat between the iOS app and the published `relayers.json` (and any saves from earlier app versions).\n- [ ] OnymIOSTests.RelayerRepositoryTests — Repository against the in-memory fakes — fast, focused on: - construction loads cached state from the store, - list-shaped mutators (add / remove / setPrimary / setStrategy) persist via the store and push a fresh snapshot, - addEndpoint dedupes on URL (idempotent + updates metadata), - removeEndpoint clears the primary marker if it pointed at the removed endpoint, - setPrimary is a no-op when the URL isn't in the configured list, - selectURL respects the strategy stored in the configuration, - background `start()` is idempotent (no double fetch) and silent on error.\n- [ ] OnymIOSTests.RelayerSelectionStoreTests — UserDefaults round-trip with per-test suite isolation.\n- [ ] OnymIOSTests.RelayerSettingsFlowTests\n- [ ] OnymIOSTests.SEPContractClientTests — Pure-Swift unit tests for `SEPContractClient` — no real HTTP.\n- [ ] OnymIOSTests.SealedEnvelopeTests — Pin the SealedEnvelope wire format.\n- [ ] OnymIOSTests.SettingsQRCodeTests — Producer-side checks for the inbox-key invite URL emitted by Settings → Invite Key.\n- [ ] OnymIOSTests.ShareInviteFlowTests\n- [ ] OnymIOSTests.SmokeTests\n  - Smoke test: the OnymSDK SwiftPM dep links and the FFI is reachable from app code.\n- [ ] OnymIOSTests.StorageEncryptionTests — Pin AES-GCM roundtrip semantics + Keychain key stability.\n- [ ] OnymIOSTests.SwiftDataGroupStoreTests — Round-trip tests for `SwiftDataGroupStore`.\n- [ ] OnymIOSTests.SwiftDataInvitationStoreTests — Exercises the real SwiftData backend (in-memory `ModelContainer` so each test gets a fresh, isolated store).\n- [ ] OnymIOSUITests.AnchorsUITests — End-to-end UI tests for Settings → Anchors.\n  - When the manifest publishes no Mainnet contracts, the Mainnet row renders disabled (no NavigationLink) while Testnet stays tappable.\n- [ ] OnymIOSUITests.IdentityManagementUITests — End-to-end coverage of the multi-identity surface that landed in PR #56 (the 5-PR multi-identity stack: keychain → repository → group filter → transport fan-out → UI).\n  - 1) Settings → Identities row → list appears with the bootstrapped identity marked Active.\n  - 2) Add Identity sheet → name in → submit → list grows by one.\n  - 3) Picker on Chats switches identities; nav title flips.\n  - 4) Remove flow — name-confirm gate blocks until exact match, list shrinks back after confirmed removal.\n- [ ] OnymIOSUITests.RecoveryPhraseBackupUITests — End-to-end coverage of the recovery-phrase backup flow, driven through the live SwiftUI views via `XCUIApplication`.\n  - Full happy path: open Backup → tap Reveal → see a 12-word phrase → answer all three verification rounds correctly → land on the Done screen.\n  - Picking a wrong word during verification keeps the user on the same round and surfaces the inline error message — no silent advance, no false-positive completion.\n  - Launching with `language: \"ru\"` renders Russian copy on Settings (nav title + Backup row) and on the recovery-phrase Intro screen — confirms the localized catalog wires through end to end.\n  - A fresh-keychain launch generates a valid 12-word phrase: every word is non-empty, all-lowercase letters, and the phrase has at least 6 unique words (a sanity guard against a stuck/zeroed seed).\n- [ ] OnymIOSUITests.RelayerSettingsUITests — End-to-end UI tests for Settings → Relayer.\n  - On a clean first launch, the Configured-relayers list pre-populates from the published manifest fixture (testnet + mainnet rows visible without any user action).\n  - On a clean first launch, the relayer-selection strategy defaults to Random — the segmented control's \"Random\" segment is the selected one.\n  - Tapping a row's star to mark it primary, then switching the strategy segment from Random to Primary, persists through the UI: the Primary segment is the selected one when the dust settles.\n  - Typing a URL into the custom-relayer field and tapping Add makes the new row appear in the Configured list.\n\n<!-- UI_TESTS_END -->\n\n\n### Manual QA\n\n<!-- MANUAL_QA_START -->\n### Install + onboarding\n- [ ] Fresh install via TestFlight or AltStore signs in cleanly\n- [ ] First-run BIP39 generation produces a 12-word phrase\n- [ ] Backup → Reveal → Verify (3 rounds) reaches the Done screen\n- [ ] App launches without splash flicker on cold start\n\n### Identity\n- [ ] Restore from a known mnemonic produces the expected Stellar address\n- [ ] Inbox key in Settings → Advanced matches across reinstalls of the same mnemonic\n\n### Create Group — Tyranny\n- [ ] Step 1 → Step 2 → Create reaches Success against testnet\n- [ ] Group lands in the Chats tab as published-on-chain\n- [ ] Inviting one peer (paste their inbox key) sends an invitation that the receiver can accept\n\n### Create Group — 1-on-1\n- [ ] \"1-on-1\" governance card is selectable on Step 1 (no \"Soon\" pill)\n- [ ] Step 2 requires exactly one peer; CTA reads \"Start dialog\"\n- [ ] Created dialog lands in Chats; both parties see the same group\n\n### Theme + locale\n- [ ] Light theme renders Create Group flow correctly\n- [ ] Dark theme renders the same screens correctly\n- [ ] Russian locale shows localized strings on Settings + Backup\n\n### Anchors + Relayer\n- [ ] Settings → Anchors picker resolves to the latest contracts manifest\n- [ ] Settings → Relayer can add a custom URL and switch primary\n\n### Release artifacts\n- [ ] App Store screenshots refreshed via fastlane (if any UI changed materially)\n- [ ] Push notification entitlement still validates against the bundle ID\n- [ ] IPA installs cleanly through AltStore (the only currently shipped channel)\n<!-- MANUAL_QA_END -->"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.49/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.49/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.49/manifest.plist",
      "size_bytes": 5121129,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/95091664ba834ba05bfbb3a5191122f3d80d72e3",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25639881136"
    },
    {
      "platform": "android",
      "version": "v0.0.39-22c33",
      "kind": "branch",
      "branch": "agent/issue-86",
      "tag": "v0.0.39",
      "commit": "22c33bfcc8f491f69abb05115948f3ef2b295b95",
      "commit_short": "22c33",
      "built_at": "2026-05-10T16:43:48Z",
      "description": "Restore identity from 12-word recovery phrase (PR #94)",
      "issue": {
        "number": 94,
        "title": "ux: restore identity from 12-word recovery phrase (closes #86)",
        "url": "https://github.com/onymchat/onym-android/issues/94"
      },
      "install_url": "https://onym.app/qa/android/v0.0.39-22c33/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.39-22c33/onym.apk",
      "size_bytes": 36449898,
      "commit_url": "https://github.com/onymchat/onym-android/commit/22c33bfcc8f491f69abb05115948f3ef2b295b95",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/25634119585"
    },
    {
      "platform": "android",
      "version": "v0.0.43",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.43",
      "commit": "84c714c4fd5d37667878257968dc5fa96441a7f2",
      "commit_short": "84c71",
      "built_at": "2026-05-10T10:56:06Z",
      "description": "Release v0.0.43",
      "issue": {
        "number": 132,
        "title": "Release: vX.Y.Z",
        "url": "https://github.com/onymchat/onym-android/issues/132"
      },
      "install_url": "https://onym.app/qa/android/v0.0.43/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.43/onym.apk",
      "size_bytes": 36425322,
      "commit_url": "https://github.com/onymchat/onym-android/commit/84c714c4fd5d37667878257968dc5fa96441a7f2",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/25626728231"
    },
    {
      "platform": "ios",
      "version": "v0.0.45",
      "kind": "tag",
      "branch": null,
      "tag": "v0.0.45",
      "commit": "2d2f35ace1fe28880bf329d9054192bbf42eb721",
      "commit_short": "2d2f3",
      "built_at": "2026-05-10T10:43:57Z",
      "description": "Release v0.0.45",
      "issue": {
        "number": 128,
        "title": "Release",
        "url": "https://github.com/onymchat/onym-ios/issues/128"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.45/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.45/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.45/manifest.plist",
      "size_bytes": 5102057,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/2d2f35ace1fe28880bf329d9054192bbf42eb721",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25626478081"
    },
    {
      "platform": "ios",
      "version": "v0.0.37-20044",
      "kind": "branch",
      "branch": "agent/issue-115",
      "tag": "v0.0.37",
      "commit": "20044282f65618c12d5de047a6f013de83252f79",
      "commit_short": "20044",
      "built_at": "2026-05-09T21:44:31Z",
      "description": "PR #117: scan QR on Invite by Inbox Key (closes #115)",
      "issue": {
        "number": 117,
        "title": "feat(create-group): scan QR on Invite by Inbox Key (closes #115)",
        "url": "https://github.com/onymchat/onym-ios/issues/117"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.37-20044/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.37-20044/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.37-20044/manifest.plist",
      "size_bytes": 5116693,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/20044282f65618c12d5de047a6f013de83252f79",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25612534418"
    },
    {
      "platform": "android",
      "version": "v0.0.39-daf55",
      "kind": "branch",
      "branch": "agent/issue-82",
      "tag": "v0.0.39",
      "commit": "daf55b620130a0d8855fbac7f84051f5531264af",
      "commit_short": "daf55",
      "built_at": "2026-05-09T21:38:24Z",
      "description": "PR #93: drop tap-to-reveal overlay from recovery-phrase screen",
      "issue": {
        "number": 93,
        "title": "fix: drop tap-to-reveal overlay from recovery-phrase screen (#82)",
        "url": "https://github.com/onymchat/onym-android/issues/93"
      },
      "install_url": "https://onym.app/qa/android/v0.0.39-daf55/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.39-daf55/onym.apk",
      "size_bytes": 36425322,
      "commit_url": "https://github.com/onymchat/onym-android/commit/daf55b620130a0d8855fbac7f84051f5531264af",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/25612364898"
    },
    {
      "platform": "ios",
      "version": "v0.0.36-a1bc9",
      "kind": "branch",
      "branch": "agent/issue-115",
      "tag": "v0.0.36",
      "commit": "a1bc9bd36fb45dcb691c63d32d07a3abdfd9ef77",
      "commit_short": "a1bc9",
      "built_at": "2026-05-09T21:16:35Z",
      "description": "PR #117 — Scan QR on Invite by Inbox Key (closes #115)",
      "issue": {
        "number": 115,
        "title": "[BUG] No QR code scanner on the invitation/add people screen",
        "url": "https://github.com/onymchat/onym-ios/issues/115"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.36-a1bc9/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.36-a1bc9/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.36-a1bc9/manifest.plist",
      "size_bytes": 5116692,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/a1bc9bd36fb45dcb691c63d32d07a3abdfd9ef77",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25611980830"
    },
    {
      "platform": "android",
      "version": "v0.0.16-e69db",
      "kind": "branch",
      "branch": "agent/issue-117",
      "tag": "v0.0.16",
      "commit": "e69db1504538b08a5194f2f06034fa4e0a5b54c0",
      "commit_short": "e69db",
      "built_at": "2026-05-08T23:01:39Z",
      "description": "PR #119: actionable error when relayer rejects un-allowlisted contract (issue #117)",
      "issue": {
        "number": 117,
        "title": "Contract not allowlisted for tyranny on testnet when creating group",
        "url": "https://github.com/onymchat/onym-android/issues/117"
      },
      "install_url": "https://onym.app/qa/android/v0.0.16-e69db/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.16-e69db/onym.apk",
      "size_bytes": 36359786,
      "commit_url": "https://github.com/onymchat/onym-android/commit/e69db1504538b08a5194f2f06034fa4e0a5b54c0",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/25583640961"
    },
    {
      "platform": "ios",
      "version": "v0.0.14-563f0",
      "kind": "branch",
      "branch": "chore/secret-leak-linter",
      "tag": "v0.0.14",
      "commit": "563f01af471b1e7b9ae8470b9de24248e62272c5",
      "commit_short": "563f0",
      "built_at": "2026-05-06T19:40:04Z",
      "description": "Tyranny join flow",
      "issue": {
        "number": 95,
        "title": "QA: end-to-end test for new-member announcement stack (#75–#94)",
        "url": "https://github.com/onymchat/onym-ios/issues/95"
      },
      "install_url": "itms-services://?action=download-manifest&url=https://onym.app/qa/ios/v0.0.14-563f0/manifest.plist",
      "ipa_url": "https://onym.app/qa/ios/v0.0.14-563f0/OnymIOS.ipa",
      "manifest_url": "https://onym.app/qa/ios/v0.0.14-563f0/manifest.plist",
      "size_bytes": 5099075,
      "commit_url": "https://github.com/onymchat/onym-ios/commit/563f01af471b1e7b9ae8470b9de24248e62272c5",
      "workflow_url": "https://github.com/onymchat/onym-ios/actions/runs/25456934927"
    },
    {
      "platform": "android",
      "version": "v0.0.16-d8d71",
      "kind": "branch",
      "branch": "chore/secret-leak-linter",
      "tag": "v0.0.16",
      "commit": "d8d71e0893028fb3c602f4915d6b5737b2a93a67",
      "commit_short": "d8d71",
      "built_at": "2026-05-06T19:30:05Z",
      "description": "Tyranny group join flow",
      "issue": {
        "number": 116,
        "title": "QA: end-to-end test for new-member announcement stack (#95–#115)",
        "url": "https://github.com/onymchat/onym-android/issues/116"
      },
      "install_url": "https://onym.app/qa/android/v0.0.16-d8d71/onym.apk",
      "apk_url": "https://onym.app/qa/android/v0.0.16-d8d71/onym.apk",
      "size_bytes": 36425322,
      "commit_url": "https://github.com/onymchat/onym-android/commit/d8d71e0893028fb3c602f4915d6b5737b2a93a67",
      "workflow_url": "https://github.com/onymchat/onym-android/actions/runs/25456024429"
    }
  ],
  "generated_at": "2026-05-23T20:06:59Z"
}
