Repository navigation
feat(payments): persist payment requests for offline - #1886
Merged
Merged
Conversation
Store each payment request in a payment_requests table (DB 48 -> 49) as the serialized PaymentRequestMetadata, with id, chat_id_hex, version, status, ends_at and created_at columns for the merge and per-chat reads. Private-chat text stays in its encrypted form. The contract merge runs as one conditional upsert: write when there is no row, the incoming version is greater, or at an equal version the incoming copy is EXPIRED over a stored OPEN one. PaymentRequests writes every ingest and applied update through to the table and loads a chat's stored requests before its first refetch, so cards and details draw offline. Rows are erased with the rest of the account cache on account deletion.
Store each private group's key envelope as the server returned it, so GroupContentCrypto can open encrypted content offline and after a restart. The unwrapped chat key is never written. Room 49 -> 50 adds group_key_envelopes, keyed by (chat id, own public key). On an in-memory miss the stored envelope is opened with the same scheme and wrapper checks as a fetched one; a stored envelope that fails a check is dropped and the network is used. The row follows the cache: replaced on fetch, on the member's self-wrapped re-wrap and on storeNewKey; dropped on an untrusted envelope, wrong-key invite bundle, NO_ENVELOPE and ALREADY_SET. Account deletion clears it with the other chat tables.
bmc08gt
added a commit
that referenced
this pull request
Oct 30, 2026
* feat(payments): persist payment requests for offline Store each payment request in a payment_requests table (DB 48 -> 49) as the serialized PaymentRequestMetadata, with id, chat_id_hex, version, status, ends_at and created_at columns for the merge and per-chat reads. Private-chat text stays in its encrypted form. The contract merge runs as one conditional upsert: write when there is no row, the incoming version is greater, or at an equal version the incoming copy is EXPIRED over a stored OPEN one. PaymentRequests writes every ingest and applied update through to the table and loads a chat's stored requests before its first refetch, so cards and details draw offline. Rows are erased with the rest of the account cache on account deletion. * feat(chat): persist group key envelopes for offline decryption Store each private group's key envelope as the server returned it, so GroupContentCrypto can open encrypted content offline and after a restart. The unwrapped chat key is never written. Room 49 -> 50 adds group_key_envelopes, keyed by (chat id, own public key). On an in-memory miss the stored envelope is opened with the same scheme and wrapper checks as a fetched one; a stored envelope that fails a check is dropped and the network is used. The row follows the cache: replaced on fetch, on the member's self-wrapped re-wrap and on storeNewKey; dropped on an untrusted envelope, wrong-key invite bundle, NO_ENVELOPE and ALREADY_SET. Account deletion clears it with the other chat tables. * fix(chat): drop a chat's key envelope and payment requests when it is removed
bmc08gt
added a commit
that referenced
this pull request
Oct 30, 2026
…ke-land * origin/code/cash: feat(payments): persist payment requests for offline (#1886) (#1892) feat(chat): add pin message contract scaffolding (#1884) # Conflicts: # apps/flipcash/shared/persistence/db/src/androidHostTest/kotlin/com/flipcash/app/persistence/dao/GroupKeyEnvelopeDaoTest.kt # apps/flipcash/shared/persistence/db/src/androidHostTest/kotlin/com/flipcash/app/persistence/dao/PaymentRequestDaoTest.kt # apps/flipcash/shared/persistence/db/src/commonMain/kotlin/com/flipcash/app/persistence/dao/GroupKeyEnvelopeDao.kt # apps/flipcash/shared/persistence/db/src/commonMain/kotlin/com/flipcash/app/persistence/dao/PaymentRequestDao.kt # apps/flipcash/shared/persistence/db/src/commonMain/kotlin/com/flipcash/app/persistence/entities/GroupKeyEnvelopeEntity.kt # apps/flipcash/shared/persistence/db/src/commonMain/kotlin/com/flipcash/app/persistence/entities/PaymentRequestEntity.kt # apps/flipcash/shared/persistence/sources/src/androidHostTest/kotlin/com/flipcash/app/persistence/sources/ChatRemovalDataTest.kt # apps/flipcash/shared/persistence/sources/src/commonMain/kotlin/com/flipcash/app/persistence/sources/GroupKeyEnvelopeDataSource.kt # apps/flipcash/shared/persistence/sources/src/commonMain/kotlin/com/flipcash/app/persistence/sources/PaymentRequestDataSource.kt # services/flipcash/src/androidHostTest/kotlin/com/flipcash/services/internal/network/extensions/PinMappingTest.kt # services/flipcash/src/androidHostTest/kotlin/com/flipcash/services/models/paymentrequest/PaymentRequestStorageTest.kt # services/flipcash/src/commonMain/kotlin/com/flipcash/services/chat/GroupKeyEnvelopeStore.kt # services/flipcash/src/commonMain/kotlin/com/flipcash/services/models/chat/Pin.kt # services/flipcash/src/commonMain/kotlin/com/flipcash/services/models/chat/PinSummaryStorage.kt # services/flipcash/src/commonMain/kotlin/com/flipcash/services/models/paymentrequest/PaymentRequestStorage.kt # services/flipcash/test-fixtures/src/commonMain/kotlin/com/flipcash/services/chat/InMemoryGroupKeyEnvelopeStore.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Payment requests lived only in memory in
PaymentRequests, so a chat's request cards were blank after a restart or offline until a refetch came back. This stores each request in Room and loads it before the network.Stacked on #1884, which takes Room to 48. Retarget to
code/cashonce that merges. iOS counterpart: code-payments/code-ios-app#1166.48 → 49(AutoMigration): newpayment_requeststable, one row per request.id_hex,chat_id_hex,version,status,ends_atandcreated_atare columns;requestholds the serializedPaymentRequestMetadata.INSERT … ON CONFLICT … DO UPDATE … WHEREinPaymentRequestDao.mergeIfNewer: a row is written when there is none, when the incoming version is greater, or at an equal version when the incoming status is EXPIRED and the stored one is OPEN. A stale fetch can't overwrite a newer row.ingest()(chat pages, fetch, refetch, create, close, mark paid,Createdupdates) is stored, and an appliedUpdatesstores the whole request at its new version. A failed write is logged and doesn't fail the fetch.loadChat,inChatandrequest(id)read the stored rows before the network call, through the existingopen()andmerge. A network failure leaves the stored copies showing.49 → 50: newgroup_key_envelopestable, keyed by chat and own public key, holding each private group's key envelope as fetched (scheme, nonce, ciphertext, wrapper). The unwrapped key is never written. On an in-memory miss,GroupKeyEnvelopes.key()opens the stored envelope with the same scheme and wrapper checks as a fetched one, so encrypted request text opens offline; nothing stored, or an envelope that fails a check, falls back to the network. A fetch,storeNewKeyor the member's own copy replaces the row.ALREADY_SET, an untrusted envelope, a wrong-key invite bundle andNO_ENVELOPEfor a member drop it.Splitkeeps the sealed form it was opened from (sealedText), and the encoder writes that, not the decrypted text.RealChatCoordinator.clearCache(), alongside the other chat caches, so they follow the same rule aschat_metadata: wiped on account deletion, kept across a plain logout because the DB is per account.ChatMetadataDataSource.removeFromChat. In one transaction it clears membership and deletes the chat'sgroup_key_envelopesandpayment_requestsrows. Thechat_metadatarow stays, since a left chat can still be shown. Hiding a DM is reversible and deletes nothing.missing,inFlight, retry state, and the payment intent id. A share paid by payment reads back as paid by payment.GroupKeyEnvelopesTest(stored open offline, replace, fallback, one case per invalidation point, stored bytes equal the fetched envelope),GroupKeyEnvelopeDaoTest,PaymentRequestOfflineDecryptTest(encrypted request text opens offline after a restart, over the real database andGroupContentCrypto),PaymentRequestDaoTest(merge cases, per-chat listing, delete),PaymentRequestStorageTest(proto round trip including sealed text and unparseable bytes),PaymentRequestsStorageTest(Robolectric with the real DB: offline restart, encrypted text at rest, write-through, stale update and stale fetch,clearStored).