Skip to content

feat(chat): handle the E2EE contract additions and bump the contract packages - #870

Merged
bmc08gt merged 7 commits into
mainfrom
chore/sync-contracts-e3d25a85-9ebf55fe
Sep 25, 2026
Merged

bmc08gt merged 7 commits into
mainfrom
chore/sync-contracts-e3d25a85-9ebf55fe

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Blocked on publishing ocp-client-protocol 0.6.0 (code-payments/ocp-client-protocol#9) and flipcash2-client-protocol 0.12.0 (code-payments/flipcash2-client-protocol#18). Until both are tagged, the exact: requirements in FlipcashAPI/Package.swift have nothing to resolve. Built locally against both client branches with FLIPCASH_PROTO_LOCAL.

Handles the flipcash2 E2EE contract additions without implementing any encryption:

  • ENCRYPTION_NOT_ALLOWED is appended to ErrorSendMessage and ErrorEditMessage, so existing raw values don't shift.
  • Content.encrypted now maps to .encrypted(scheme:nonce:ciphertext:) instead of nil. Before, an encrypted DM message just disappeared. It renders as the deleted-message tombstone with "This message isn't supported on this version", and all three fields are stored in SQLite.
  • Content.asProto() switches exhaustively. ConversationController.retry is the only place stored content goes back to proto; .encrypted is written back byte for byte, while .cash / .deleted throw and get logged.
  • Conversation gains creator and useE2Ee, persisted in ConversationTable. useE2Ee changes no behaviour yet.
  • ChatMetadata.message moved into the message_ref oneof, which removed hasMessage. NotificationPayload now switches on messageRef. A push carrying only message_id falls back to the extension's existing fetch of recent messages; a direct GetMessage fetch is a TODO.

schemaVersion goes from 39 to 41, not 40: feat/chat-reactions also moves it to 40, and identical one-line edits merge silently. Either version triggers the same one-time store rebuild.

The ocp fiat_values_by_currency fields aren't wired in. BalanceService maps into TokenAmount, so exposing fiat values needs a modelling decision, not a sync.

Android counterpart: code-payments/code-android-app#1573

messaging_service.proto adds ENCRYPTION_NOT_ALLOWED to SendMessageResponse.Result
and EditMessageResponse.Result (EncryptedContent sent outside a DM). Add the case
to ErrorSendMessage/ErrorEditMessage after the existing cases so raw values still
line up with the server, and route it through the existing failure/classification
paths.

model.proto's Content.type oneof adds an encrypted case; map it through the same
unsupported-content path as media/system for now. Actual E2EE (X25519/HKDF/
XChaCha20) is a cross-platform parity hotspot and needs its own decision — this
only keeps decoding from crashing/dropping the switch.

chat/v1/model.proto's Metadata adds creator (group chats) and use_e2ee (DMs,
transitional migration flag). Mirror both onto Conversation; use_e2ee is stored
but not acted on yet.
push/v1/model.proto moved ChatMetadata.message into a message_ref oneof
alongside a new message_id, so payload.chatMetadata.hasMessage no longer
compiles. Switch on messageRef instead; the id-only case (long messages)
returns nil here rather than fetching via Messaging.GetMessage.

Not a regression in practice: NotificationService's transcript prefetch
(cachePreview) already fetches the chat's recent messages over its own
connection independently of the embedded payload, so an id-only push
still gets a rendered preview, just without the no-network fast path the
embedded-message case gives. Left a TODO on chatMessage(_:) naming the
proto field and the gap, in case that fast path turns out to matter later.
Content.encrypted decoded to nil, so an encrypted DM message vanished
from the transcript instead of being stored, unlike Android's
"unsupported message" placeholder.

Add ConversationMessage.Content.encrypted(scheme:nonce:ciphertext:),
persist all three fields in the SQLite store (kind 3, three new
nullable columns, schema version 40), and render it as the existing
.deleted tombstone bubble with new copy: "This message isn't supported
on this version". Decryption itself (X25519/HKDF/XChaCha20) is out of
scope; the content is kept verbatim, not decoded.

Wire the new case through every place that switches on Content so
nothing crashes or silently drops it: chat-list preview, reply
quoting, copy/link detection, and message capabilities (no
copy/edit/reply capability, matching a tombstone). .media/.system are
still dropped by design -- unchanged.
Every path that turns a domain ConversationMessage.Content back into
a proto Content assumed .text. The only such path is
ConversationController.retry(clientMessageID:in:), which pattern-
matched `case .text` and silently no-opped for anything else --
including the encrypted case just added, which this client can't
regenerate ciphertext for.

Give ConversationMessage.Content an asProto() that switches
exhaustively: .text encodes as before, .encrypted re-encodes the
stored scheme/nonce/ciphertext byte for byte (a round trip of bytes
this client never decrypted, not encryption), and .cash/.deleted
throw ConversationMessageContentEncodingError.unsupported -- neither
is ever resent this way (cash has its own send path, a tombstone is a
mutation result). No fatalError, preconditionFailure, or force-unwrap
on any branch.

retry() now switches on pending.content: .text sends as before;
.encrypted/.cash/.deleted call asProto() and log the thrown error
rather than resending, since there's no outbox path for them today.
Caller trace: retry(clientMessageID:in:) is the only place a stored
ConversationMessage.Content is converted back to a proto Content --
send/edit/reply all build a fresh .text Content directly rather than
converting an existing domain value.
chat.v1.Metadata's creator (group chats) and use_e2ee were mapped
from the proto onto Conversation but never written to or read back
from the SQLite store, so a conversation rebuilt from disk on cold
start lost both.

Add creator (nullable UUID) and useE2Ee (bool, default false) columns
to ConversationTable and wire them through writeConversation/
getConversations. No separate schema-version bump: the columns land
in version 40, the same rebuild already needed for the encrypted-
message columns, since the version can only be bumped once per
rebuild and splitting it across two commits would cost users a second
resync for no benefit.
The open feat/chat-reactions branch also moves schemaVersion from 39
to 40. Identical one-line edits merge without a conflict, so whichever
branch landed second would change the store with no new version, and
installs already at 40 would keep a store missing its columns.

Skipping 40 costs nothing: the launch check only compares the recorded
version against this one.
…rotocol to 0.12.0

Neither version is published yet, so this does not resolve until both
client packages are released.
@bmc08gt bmc08gt self-assigned this Sep 25, 2026
@bmc08gt
bmc08gt marked this pull request as ready for review September 25, 2026 17:31
@bmc08gt
bmc08gt merged commit 372cc78 into main Sep 25, 2026
1 check failed
bmc08gt added a commit that referenced this pull request Sep 26, 2026
* origin/main:
  feat(chat): send cash into a group chat as a cash link (#871)
  feat(chat): handle the E2EE contract additions and bump the contract packages (#870)
  docs(claude): drop think-harder lines and needless stops from agent instructions (#868)

# Conflicts:
#	FlipcashCore/Sources/FlipcashStore/Database+Conversations.swift
#	FlipcashCore/Sources/FlipcashStore/Database.swift
#	FlipcashCore/Sources/FlipcashStore/Schema.swift
bmc08gt added a commit that referenced this pull request Sep 26, 2026
…I pins

#870 moved FlipcashAPI's pins to ocp 0.6.0 and flipcash2 0.12.0 but left
Package.resolved on 0.5.0 and 0.11.0, so every build rewrote it.
@bmc08gt
bmc08gt deleted the chore/sync-contracts-e3d25a85-9ebf55fe branch October 5, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant