crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 68a938ea504cfb33e2bd38a64f7195d7c85ee57d
parent 8307c563f88c16299284dc0edb042887a711ff6e
Author: Michael Camilleri <[email protected]>
Date:   Fri, 26 Jun 2026 14:04:13 +0900

Mint the notification content key at share time

Prior to this commit, the per-game content key the Notification Service
Extension needs to decrypt the payload was minted lazily on the first
push — ensurePushCredentials only ran from the push resolvers — so a
game that had never sent a push had no key at all. The sending device
minted the key at that first push and wrote it onto the Game record, but
the push travelled via the Worker while the key travelled via CloudKit
sync, so it could (and almost certainly wouldn)t outrun its own
propagation.

Now the key is minted eagerly when a game becomes shared. The
onShareSaved handler calls ensurePushCredentials immediately after
markShared, so the key is written onto the Game record and has time to
reach participants before any encrypted notification is sent. The call
is idempotent, so it also backfills an already-shared game that never
acquired a key. It changes only the value carried in the Game record's
notification field, not its shape, so no CloudKit schema change is
required.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Services/AppServices.swift | 9+++++++++
1 file changed, 9 insertions(+), 0 deletions(-)

diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift @@ -841,6 +841,15 @@ final class AppServices { shareController.onShareSaved = { [weak self] gameID in guard let self else { return } self.store.markShared(gameID: gameID) + // Mint the game's notification content key now, at share time, + // rather than lazily on the first push. The key rides the Game + // record (`setNotification` enqueues its push), so minting here + // gives it time to propagate to participants before any encrypted + // notification is sent. Lazy minting let the first push for a game + // with no prior activity (e.g. an immediate resign on a game with + // no moves) outrun the key's sync, leaving the recipient unable to + // decrypt and falling back to the generic alert. Idempotent. + self.store.ensurePushCredentials(for: gameID) // Register this device under the newly-shared game's derived push // address so peers can reach it. Task { @MainActor [weak self] in