commit 8f191f2af629623a73ebd5d3f0a92b5c311f3610
parent 11e31ab4918b656b4b415d1af43c2a7eb7c6d7f5
Author: Michael Camilleri <[email protected]>
Date: Thu, 30 Apr 2026 12:43:57 +0900
Remove remote-alert notification path
Crossmate now delivers shared-puzzle activity alerts by syncing SessionPing
records and scheduling local notifications in the host app. As a result, the
old CloudKit query-subscription alert path is no longer needed.
This commit removes the NotificationService extension target, its
plist/entitlements/source, the embedded extension wiring, and the stale scheme
entry. It also deletes SessionPingSubscriber and the remaining SyncEngine hooks
that tried to maintain per-zone query subscriptions in the shared database.
The local notification copy is now consistently phrased as "<player> has
updated the puzzle <title>", with fallback variants for missing player or title
metadata.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
11 files changed, 11 insertions(+), 404 deletions(-)
diff --git a/Crossmate.xcodeproj/project.pbxproj b/Crossmate.xcodeproj/project.pbxproj
@@ -35,14 +35,12 @@
77556FD9473A3F10FADF5E4E /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC295195602B3DDF7BB3895 /* PersistenceController.swift */; };
78802AFDF6273231781CC0DC /* AppServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDC81CA6A9C80EB31E7F493 /* AppServices.swift */; };
7C0AAD1DD6086E76A6DA806B /* NameBroadcasterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2289E9F6A78502581886CD /* NameBroadcasterTests.swift */; };
- 7D4A56FBB1C5D5F89271B77F /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507B4DC893CE8AC4778CBACE /* NotificationService.swift */; };
7E54EC2E507C3BFD615FD621 /* MoveLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7422F19AA1F1692A98E3602 /* MoveLog.swift */; };
7FFEACFC672925A0968ACC1C /* XD.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9031A1574C21866940F6A2C /* XD.swift */; };
818B1F2693962832BE14578E /* GameListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DDAD9D6470A894C3FD6F90 /* GameListView.swift */; };
82918A74836E5076CBFA1592 /* SyncEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73DDDED719CFFDD6035C3B48 /* SyncEngine.swift */; };
8478F0BC0CA624C78DC0A3B5 /* ImportedBrowseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B1BB8AB6309AF111671CB5 /* ImportedBrowseView.swift */; };
849970A21D62C34EC382A27E /* GameShareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ABB557BA10CBE9909056882 /* GameShareItem.swift */; };
- 88BACA1689459AC9AED20D43 /* NotificationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D2FD896D75863554E31654C /* NotificationState.swift */; };
8F5CB2F94E083D06D7E04280 /* PlayerSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B331CC55827FEF3420ABCE /* PlayerSession.swift */; };
905D79CFE454D2E4374544C7 /* GameStoreSnapshotPruningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F61F35F4B4AEF51C02276A3 /* GameStoreSnapshotPruningTests.swift */; };
9789150602A3321D2E1E7E81 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BF60C84D92A9024AC1A53FC /* Media.xcassets */; };
@@ -61,7 +59,6 @@
BCB9A4D5E06EE5006186465D /* ShareController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C74683332956B0D1CA37589 /* ShareController.swift */; };
BE57957589423497338EBD37 /* ShareRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68072F4F3EB5D5A78E03D408 /* ShareRoutingTests.swift */; };
C30C0C4E54E4209A22843872 /* CrossmateModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F93AC31640C40FCC039570A3 /* CrossmateModel.xcdatamodeld */; };
- C511387D9FFBCC2E2F5EF699 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 51318FC5DAE02D35CB005729 /* NotificationService.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C7370BCAD585EEFD366204E3 /* GridThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BB7D9759D27F7BA6734FDE /* GridThumbnailView.swift */; };
C944A5BD871C6ECC64DE8A5B /* CalendarDayCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CAA5E17BD406AFEEF96196 /* CalendarDayCell.swift */; };
CC250D6BA9B41CB722D8A62E /* CloudService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BC76178319D0D669CD50FF /* CloudService.swift */; };
@@ -81,18 +78,10 @@
F46733AB3C72749A4A992667 /* SyncState+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A49C3C31F49A85764B84A15 /* SyncState+Helpers.swift */; };
F77177F48728ECEACD3B28B3 /* KeyboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D28E8CBB1AFFD801E87D4E3 /* KeyboardView.swift */; };
F8DDA34AC1A6B6499C5D222E /* PlayerPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46148CF0F4D719692F81A6EC /* PlayerPreferences.swift */; };
- F9B17DFD6A460AA3266B34B6 /* SessionPingSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3145BFE3D25B6025BA277D9 /* SessionPingSubscriber.swift */; };
FFBE2EC8A3A60E119A0D314F /* NYTBrowseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2BE43E18B1CC6AAD27DC6D /* NYTBrowseView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
- 0751E0359C340223ADBA2B05 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 9167165F088B7698D1319D3C /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 350366A68B75DC4BDA91F8E5;
- remoteInfo = NotificationService;
- };
F0122CF3E216720C4437CE6A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9167165F088B7698D1319D3C /* Project object */;
@@ -102,20 +91,6 @@
};
/* End PBXContainerItemProxy section */
-/* Begin PBXCopyFilesBuildPhase section */
- 2768812850886EB633B6C27C /* Embed Foundation Extensions */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 13;
- files = (
- C511387D9FFBCC2E2F5EF699 /* NotificationService.appex in Embed Foundation Extensions */,
- );
- name = "Embed Foundation Extensions";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
/* Begin PBXFileReference section */
07C57DEE9E0EFA684D8BD00B /* NYTLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYTLoginView.swift; sourceTree = "<group>"; };
0B73A791FD061430AE286E11 /* morning.xd */ = {isa = PBXFileReference; path = morning.xd; sourceTree = "<group>"; };
@@ -141,9 +116,7 @@
465F2BB469EFE84CF3733398 /* Game.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = "<group>"; };
4803C2E84FC5B3BFE593171D /* NameBroadcaster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameBroadcaster.swift; sourceTree = "<group>"; };
4DC7784917397BCD6B8D679D /* PuzzleCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PuzzleCatalog.swift; sourceTree = "<group>"; };
- 507B4DC893CE8AC4778CBACE /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
50992CDA4082429EBB17F65C /* garden.xd */ = {isa = PBXFileReference; path = garden.xd; sourceTree = "<group>"; };
- 51318FC5DAE02D35CB005729 /* NotificationService.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
52B8E26067849A63758DDEA4 /* MoveBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveBuffer.swift; sourceTree = "<group>"; };
543481AA9FA32BF14076EB1C /* MoveLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveLogTests.swift; sourceTree = "<group>"; };
56BC76178319D0D669CD50FF /* CloudService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudService.swift; sourceTree = "<group>"; };
@@ -161,7 +134,6 @@
7D28E8CBB1AFFD801E87D4E3 /* KeyboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardView.swift; sourceTree = "<group>"; };
7E4DEAF9F7887CBB46A99E8E /* RecordSerializerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordSerializerTests.swift; sourceTree = "<group>"; };
87B1BB8AB6309AF111671CB5 /* ImportedBrowseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedBrowseView.swift; sourceTree = "<group>"; };
- 88E8AACB638FE5724B534B41 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
927186458ED03FD0C5660765 /* CrossmateModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CrossmateModel.xcdatamodel; sourceTree = "<group>"; };
93EE5BA78566EDED68D846AB /* GameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameStore.swift; sourceTree = "<group>"; };
9447F0FE34C63810C6F1D8BE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -176,7 +148,6 @@
B0938B0ACB40772EE522D77C /* NYTPuzzleFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYTPuzzleFetcher.swift; sourceTree = "<group>"; };
B135C285570F91181595B405 /* CellMark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellMark.swift; sourceTree = "<group>"; };
B1F1471BE4D6D84361DD692B /* AuthorIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorIdentity.swift; sourceTree = "<group>"; };
- B3D873ABDF871E14794A2845 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = "<group>"; };
B689A7138429641E61E9E558 /* Crossmate.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Crossmate.app; sourceTree = BUILT_PRODUCTS_DIR; };
B9031A1574C21866940F6A2C /* XD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XD.swift; sourceTree = "<group>"; };
BAC1B64755AE15CF45350DBB /* MoveBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveBufferTests.swift; sourceTree = "<group>"; };
@@ -186,7 +157,6 @@
C54223FED97577A593B7964E /* NYTToXDConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYTToXDConverterTests.swift; sourceTree = "<group>"; };
CAB4BB9E160C3A59C653E7A9 /* GridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridView.swift; sourceTree = "<group>"; };
CBDC81CA6A9C80EB31E7F493 /* AppServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppServices.swift; sourceTree = "<group>"; };
- D3145BFE3D25B6025BA277D9 /* SessionPingSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPingSubscriber.swift; sourceTree = "<group>"; };
D97CBA409832A24D64DF0F5C /* Crossmate Unit Tests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = "Crossmate Unit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
D9BB7D9759D27F7BA6734FDE /* GridThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridThumbnailView.swift; sourceTree = "<group>"; };
DB55FC337CF72C650373210A /* PlayerColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerColor.swift; sourceTree = "<group>"; };
@@ -219,7 +189,6 @@
F7422F19AA1F1692A98E3602 /* MoveLog.swift */,
19D51F4A1CAEB849A09EC2C1 /* PresencePublisher.swift */,
0C0A7348E1283E7CD2486E2A /* RecordSerializer.swift */,
- D3145BFE3D25B6025BA277D9 /* SessionPingSubscriber.swift */,
5C74683332956B0D1CA37589 /* ShareController.swift */,
73DDDED719CFFDD6035C3B48 /* SyncEngine.swift */,
AC00F4D5060EDA4859A6B3F1 /* SyncMonitor.swift */,
@@ -233,7 +202,6 @@
children = (
D97CBA409832A24D64DF0F5C /* Crossmate Unit Tests.xctest */,
B689A7138429641E61E9E558 /* Crossmate.app */,
- 51318FC5DAE02D35CB005729 /* NotificationService.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -360,7 +328,6 @@
children = (
5770CE69DB2B0B7462FACE53 /* Crossmate */,
6F470E54D9E6E99FCEA893D1 /* Generated */,
- D7910EDB740AFF963BDCA6CE /* NotificationService */,
9BF7383FE2AB07F12434C013 /* Shared */,
01B07D8724DEA04C3E74558E /* Support */,
212DB6FCF46C41F81C41D232 /* Unit */,
@@ -368,16 +335,6 @@
);
sourceTree = "<group>";
};
- D7910EDB740AFF963BDCA6CE /* NotificationService */ = {
- isa = PBXGroup;
- children = (
- 88E8AACB638FE5724B534B41 /* Info.plist */,
- B3D873ABDF871E14794A2845 /* NotificationService.entitlements */,
- 507B4DC893CE8AC4778CBACE /* NotificationService.swift */,
- );
- path = NotificationService;
- sourceTree = "<group>";
- };
D8F0E3376B2616B4E917129C /* Services */ = {
isa = PBXGroup;
children = (
@@ -407,35 +364,16 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- 350366A68B75DC4BDA91F8E5 /* NotificationService */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 97D15A585690B87E7C17FF8A /* Build configuration list for PBXNativeTarget "NotificationService" */;
- buildPhases = (
- 6D4D7955C5F70F9F18D7C1F0 /* Sources */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = NotificationService;
- packageProductDependencies = (
- );
- productName = NotificationService;
- productReference = 51318FC5DAE02D35CB005729 /* NotificationService.appex */;
- productType = "com.apple.product-type.app-extension";
- };
7708D1C8A0145D43BD15DEB7 /* Crossmate */ = {
isa = PBXNativeTarget;
buildConfigurationList = AB7D49875A042FD78EDD157A /* Build configuration list for PBXNativeTarget "Crossmate" */;
buildPhases = (
C17B62906BBF281D006D8DC2 /* Sources */,
C475EFB2B47245175F9B415C /* Resources */,
- 2768812850886EB633B6C27C /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
- 0638C125467274AA03088E07 /* PBXTargetDependency */,
);
name = Crossmate;
packageProductDependencies = (
@@ -471,10 +409,6 @@
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
- 350366A68B75DC4BDA91F8E5 = {
- DevelopmentTeam = 7TD7PZBNXP;
- ProvisioningStyle = Automatic;
- };
7708D1C8A0145D43BD15DEB7 = {
DevelopmentTeam = 7TD7PZBNXP;
ProvisioningStyle = Automatic;
@@ -501,7 +435,6 @@
targets = (
7708D1C8A0145D43BD15DEB7 /* Crossmate */,
C38EBD1A6B9D37EF81FF3511 /* Crossmate Unit Tests */,
- 350366A68B75DC4BDA91F8E5 /* NotificationService */,
);
};
/* End PBXProject section */
@@ -521,15 +454,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
- 6D4D7955C5F70F9F18D7C1F0 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 7D4A56FBB1C5D5F89271B77F /* NotificationService.swift in Sources */,
- 88BACA1689459AC9AED20D43 /* NotificationState.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
931E2DAAD4EC47B06F7AB60A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -601,7 +525,6 @@
E91FB8101E1927CA567DE825 /* PuzzleSource.swift in Sources */,
2F43F24C98D7FF00CA486753 /* PuzzleView.swift in Sources */,
CF0CA17ABE211DAE4DD35AFD /* RecordSerializer.swift in Sources */,
- F9B17DFD6A460AA3266B34B6 /* SessionPingSubscriber.swift in Sources */,
54464FDFB8C71B0D3B4B61A2 /* SettingsView.swift in Sources */,
BCB9A4D5E06EE5006186465D /* ShareController.swift in Sources */,
AF4F1AE2A1F94E92C785C524 /* Square.swift in Sources */,
@@ -616,11 +539,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
- 0638C125467274AA03088E07 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 350366A68B75DC4BDA91F8E5 /* NotificationService */;
- targetProxy = 0751E0359C340223ADBA2B05 /* PBXContainerItemProxy */;
- };
42035D5EEE61A5D459E1D46D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 7708D1C8A0145D43BD15DEB7 /* Crossmate */;
@@ -729,23 +647,6 @@
};
name = Release;
};
- 68A4416DD8D2ADB28F35E46C /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
- CODE_SIGN_STYLE = Automatic;
- INFOPLIST_FILE = NotificationService/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@executable_path/../../Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = net.inqk.crossmate.notificationservice;
- SDKROOT = iphoneos;
- TARGETED_DEVICE_FAMILY = 1;
- };
- name = Release;
- };
8BC97916898B0BF1E6951C48 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -782,23 +683,6 @@
};
name = Debug;
};
- C32934743A6313F80D8A4C76 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
- CODE_SIGN_STYLE = Automatic;
- INFOPLIST_FILE = NotificationService/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@executable_path/../../Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = net.inqk.crossmate.notificationservice;
- SDKROOT = iphoneos;
- TARGETED_DEVICE_FAMILY = 1;
- };
- name = Debug;
- };
E7B092DD549FA4FFED8BC20E /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 26397B9DBC57DCF7B58899D4 /* BuildNumber.xcconfig */;
@@ -882,15 +766,6 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
- 97D15A585690B87E7C17FF8A /* Build configuration list for PBXNativeTarget "NotificationService" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- C32934743A6313F80D8A4C76 /* Debug */,
- 68A4416DD8D2ADB28F35E46C /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Debug;
- };
9A436EF03A8593C66A18A832 /* Build configuration list for PBXProject "Crossmate" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/Crossmate.xcodeproj/xcuserdata/pyrmont.xcuserdatad/xcschemes/xcschememanagement.plist b/Crossmate.xcodeproj/xcuserdata/pyrmont.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -9,11 +9,6 @@
<key>orderHint</key>
<integer>0</integer>
</dict>
- <key>NotificationService.xcscheme_^#shared#^_</key>
- <dict>
- <key>orderHint</key>
- <integer>1</integer>
- </dict>
</dict>
</dict>
</plist>
diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift
@@ -249,11 +249,13 @@ final class AppServices {
let content = UNMutableNotificationContent()
content.title = "Crossmate"
if !ping.playerName.isEmpty, !ping.puzzleTitle.isEmpty {
- content.body = "\(ping.playerName) made an edit to \(ping.puzzleTitle)"
+ content.body = "\(ping.playerName) has updated the puzzle \(ping.puzzleTitle)"
} else if !ping.playerName.isEmpty {
- content.body = "\(ping.playerName) made an edit"
+ content.body = "\(ping.playerName) has updated a puzzle"
+ } else if !ping.puzzleTitle.isEmpty {
+ content.body = "A player has updated the puzzle \(ping.puzzleTitle)"
} else {
- content.body = "A player made an edit"
+ content.body = "A player has updated a puzzle"
}
content.sound = .default
content.userInfo = ["crossmateGameID": ping.gameID.uuidString]
diff --git a/Crossmate/Sync/RecordSerializer.swift b/Crossmate/Sync/RecordSerializer.swift
@@ -159,9 +159,9 @@ enum RecordSerializer {
/// Builds a freshly-minted SessionPing record. SessionPings are
/// write-once — they have no Core Data equivalent and no system-fields
/// archive.
- /// - `authorID` lets the subscription predicate filter out self-sends.
- /// - `playerName` and `puzzleTitle` are read by the receiver's NSE to
- /// render the alert body (e.g. "Alice made an edit to Sunday Crossword").
+ /// - `authorID` lets receivers filter out self-sends.
+ /// - `playerName` and `puzzleTitle` let receivers render the alert body
+ /// (e.g. "Alice has updated the puzzle Sunday Crossword").
static func sessionPingRecord(
gameID: UUID,
authorID: String,
diff --git a/Crossmate/Sync/SessionPingSubscriber.swift b/Crossmate/Sync/SessionPingSubscriber.swift
@@ -1,93 +0,0 @@
-import CloudKit
-import Foundation
-
-/// Registers and tracks `CKQuerySubscription`s for `SessionPing` records in
-/// the shared CloudKit database. One subscription per shared zone, with a
-/// predicate that filters out pings authored by the local user so the sender
-/// doesn't receive their own push.
-///
-/// Registered zone names are mirrored to `UserDefaults` so we don't pay the
-/// network cost of re-saving an identical subscription on every launch.
-actor SessionPingSubscriber {
- private let container: CKContainer
- private let identityProvider: @Sendable () async -> String?
- private let tracer: (@Sendable (String) -> Void)?
-
- private static let registeredKey = "crossmate.sessionPingSubscribedZones"
-
- init(
- container: CKContainer,
- identityProvider: @escaping @Sendable () async -> String?,
- tracer: (@Sendable (String) -> Void)? = nil
- ) {
- self.container = container
- self.identityProvider = identityProvider
- self.tracer = tracer
- }
-
- /// Saves a SessionPing query subscription for `zoneID` to the shared
- /// database, if one isn't already on file. Idempotent — safe to call
- /// every time the zone surfaces via fetched-database-changes.
- func ensureSubscribed(zoneID: CKRecordZone.ID) async {
- if isRegistered(zoneName: zoneID.zoneName) { return }
- guard let authorID = await identityProvider() else {
- tracer?("session-ping: skip subscribe — no authorID for \(zoneID.zoneName)")
- return
- }
-
- let predicate = NSPredicate(format: "authorID != %@", authorID)
- let subscriptionID = "sessionping-\(zoneID.zoneName)"
- let subscription = CKQuerySubscription(
- recordType: "SessionPing",
- predicate: predicate,
- subscriptionID: subscriptionID,
- options: .firesOnRecordCreation
- )
- subscription.zoneID = zoneID
-
- let info = CKSubscription.NotificationInfo()
- // Placeholder body — the NSE rewrites it from `playerName` and
- // `puzzleTitle` so a partner-readable banner shows.
- info.alertBody = "A player made an edit"
- info.shouldSendMutableContent = true
- info.desiredKeys = ["authorID", "playerName", "puzzleTitle"]
- subscription.notificationInfo = info
-
- do {
- _ = try await container.sharedCloudDatabase.save(subscription)
- markRegistered(zoneName: zoneID.zoneName)
- tracer?("session-ping: subscribed \(subscriptionID)")
- } catch let error as CKError where error.code == .serverRejectedRequest {
- // Subscription already exists server-side — likely from a prior
- // install whose UserDefaults didn't survive. Treat as success.
- markRegistered(zoneName: zoneID.zoneName)
- tracer?("session-ping: \(subscriptionID) already on server")
- } catch {
- tracer?("session-ping: failed to subscribe \(subscriptionID) — \(error.localizedDescription)")
- }
- }
-
- /// Removes a zone from the registered set so that, if the share is
- /// re-accepted later, we re-subscribe. The server-side subscription is
- /// cleaned up by CloudKit when the zone disappears.
- func forgetZone(zoneName: String) {
- var set = registeredZones()
- guard set.remove(zoneName) != nil else { return }
- UserDefaults.standard.set(Array(set), forKey: Self.registeredKey)
- }
-
- private func isRegistered(zoneName: String) -> Bool {
- registeredZones().contains(zoneName)
- }
-
- private func markRegistered(zoneName: String) {
- var set = registeredZones()
- set.insert(zoneName)
- UserDefaults.standard.set(Array(set), forKey: Self.registeredKey)
- }
-
- private func registeredZones() -> Set<String> {
- let arr = UserDefaults.standard.stringArray(forKey: Self.registeredKey) ?? []
- return Set(arr)
- }
-}
diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift
@@ -70,11 +70,6 @@ actor SyncEngine {
private var onGameAccessRevoked: (@MainActor @Sendable (UUID) async -> Void)?
private var onSnapshotsSaved: (@MainActor @Sendable ([String]) async -> Void)?
private var tracer: (@MainActor @Sendable (String) -> Void)?
- private var sessionPingSubscriber: SessionPingSubscriber?
-
- func setSessionPingSubscriber(_ subscriber: SessionPingSubscriber) {
- sessionPingSubscriber = subscriber
- }
func setTracer(_ t: @MainActor @Sendable @escaping (String) -> Void) {
tracer = t
@@ -781,13 +776,6 @@ actor SyncEngine {
// For the shared engine, create placeholder game entities for new
// zones (populated fully when the Game record arrives), and mark
// games access-revoked when the owner removes us from the share.
- let newZoneIDs: [CKRecordZone.ID] = event.modifications
- .map(\.zoneID)
- .filter { $0.zoneName.hasPrefix("game-") }
- let removedZoneNames: [String] = event.deletions
- .map(\.zoneID.zoneName)
- .filter { $0.hasPrefix("game-") }
-
let ctx = persistence.container.newBackgroundContext()
let revokedIDs: [UUID] = ctx.performAndWait {
var ids: [UUID] = []
@@ -830,15 +818,6 @@ actor SyncEngine {
for id in revokedIDs {
if let cb = onGameAccessRevoked { await cb(id) }
}
-
- if let subscriber = sessionPingSubscriber {
- for zoneID in newZoneIDs {
- await subscriber.ensureSubscribed(zoneID: zoneID)
- }
- for zoneName in removedZoneNames {
- await subscriber.forgetZone(zoneName: zoneName)
- }
- }
}
private func handleFetchedRecordZoneChanges(
diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>CFBundleDevelopmentRegion</key>
- <string>$(DEVELOPMENT_LANGUAGE)</string>
- <key>CFBundleDisplayName</key>
- <string>Crossmate Notifications</string>
- <key>CFBundleExecutable</key>
- <string>$(EXECUTABLE_NAME)</string>
- <key>CFBundleIdentifier</key>
- <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
- <key>CFBundleInfoDictionaryVersion</key>
- <string>6.0</string>
- <key>CFBundleName</key>
- <string>$(PRODUCT_NAME)</string>
- <key>CFBundlePackageType</key>
- <string>XPC!</string>
- <key>CFBundleShortVersionString</key>
- <string>1.0.0</string>
- <key>CFBundleVersion</key>
- <string>$(CURRENT_PROJECT_VERSION)</string>
- <key>NSExtension</key>
- <dict>
- <key>NSExtensionPointIdentifier</key>
- <string>com.apple.usernotifications.service</string>
- <key>NSExtensionPrincipalClass</key>
- <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
- </dict>
-</dict>
-</plist>
diff --git a/NotificationService/NotificationService.entitlements b/NotificationService/NotificationService.entitlements
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.security.application-groups</key>
- <array>
- <string>group.net.inqk.crossmate</string>
- </array>
-</dict>
-</plist>
diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift
@@ -1,83 +0,0 @@
-import CloudKit
-import UserNotifications
-
-/// Notification Service Extension. Intercepts incoming alert pushes from the
-/// CloudKit `SessionPing` subscription and decides how prominently to show
-/// them.
-///
-/// Suppression policy:
-/// - If the user is currently viewing the puzzle the ping refers to, demote
-/// to `.passive` (notification appears in Notification Center only — no
-/// banner, no sound).
-/// - If a ping for the same game was already shown within `dedupWindow`,
-/// demote to `.passive`.
-/// - Otherwise pass through and record the timestamp so subsequent pings for
-/// the same game are demoted.
-///
-/// Foreground suppression (returning `[]` from `willPresent`) is handled by
-/// the host app's UNUserNotificationCenter delegate, since NSEs cannot fully
-/// hide a notification.
-final class NotificationService: UNNotificationServiceExtension {
- private var contentHandler: ((UNNotificationContent) -> Void)?
- private var bestAttemptContent: UNMutableNotificationContent?
-
- override func didReceive(
- _ request: UNNotificationRequest,
- withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
- ) {
- self.contentHandler = contentHandler
- self.bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
-
- guard let content = bestAttemptContent else {
- contentHandler(request.content)
- return
- }
-
- Self.rewriteBody(into: content, from: request.content.userInfo)
-
- if let gameID = Self.gameID(from: request.content.userInfo) {
- if NotificationState.shouldSuppress(gameID: gameID) {
- content.interruptionLevel = .passive
- content.sound = nil
- }
- NotificationState.recordShown(gameID: gameID)
- }
-
- contentHandler(content)
- }
-
- /// Replaces the placeholder alert body with "<player> made an edit to
- /// <title>" using fields delivered in the CloudKit query notification.
- /// Falls back to the original body if either field is missing.
- private static func rewriteBody(
- into content: UNMutableNotificationContent,
- from userInfo: [AnyHashable: Any]
- ) {
- guard let note = CKNotification(fromRemoteNotificationDictionary: userInfo)
- as? CKQueryNotification,
- let fields = note.recordFields
- else { return }
- let player = (fields["playerName"] as? String) ?? ""
- let title = (fields["puzzleTitle"] as? String) ?? ""
- guard !player.isEmpty, !title.isEmpty else { return }
- content.body = "\(player) made an edit to \(title)"
- }
-
- override func serviceExtensionTimeWillExpire() {
- if let contentHandler, let bestAttemptContent {
- contentHandler(bestAttemptContent)
- }
- }
-
- /// CloudKit query subscriptions deliver the affected zone in
- /// `userInfo["ck"]["qry"]["zid"]`. Our zone names encode the gameID as
- /// `game-<UUID>`.
- private static func gameID(from userInfo: [AnyHashable: Any]) -> UUID? {
- guard let ck = userInfo["ck"] as? [AnyHashable: Any],
- let qry = ck["qry"] as? [AnyHashable: Any],
- let zoneName = qry["zid"] as? String,
- zoneName.hasPrefix("game-")
- else { return nil }
- return UUID(uuidString: String(zoneName.dropFirst("game-".count)))
- }
-}
diff --git a/Shared/NotificationState.swift b/Shared/NotificationState.swift
@@ -1,14 +1,13 @@
import Foundation
-/// State shared between the host app and the Notification Service Extension
-/// via App Group UserDefaults. Both targets compile this file directly.
+/// Notification suppression state persisted via App Group UserDefaults.
///
/// Two pieces of state are tracked:
/// - `activePuzzleID` — set by the app while the user is viewing a puzzle so
-/// the NSE can quietly drop notifications for that same puzzle.
+/// local notifications for that same puzzle can be skipped.
/// - `shownByGame` — a `[gameID: Date]` map used to debounce repeat
/// notifications. Once a SessionPing for game X has been shown, further
-/// pings for X within `dedupWindow` are demoted to `.passive`.
+/// pings for X within `dedupWindow` are suppressed.
enum NotificationState {
static let appGroup = "group.net.inqk.crossmate"
diff --git a/project.yml b/project.yml
@@ -67,32 +67,6 @@ targets:
CODE_SIGN_ENTITLEMENTS: Crossmate/Crossmate.entitlements
TARGETED_DEVICE_FAMILY: "1"
CODE_SIGN_STYLE: Automatic
- dependencies:
- - target: NotificationService
- embed: true
- codeSign: true
-
- NotificationService:
- type: app-extension
- platform: iOS
- sources:
- - NotificationService
- - Shared
- info:
- path: NotificationService/Info.plist
- properties:
- CFBundleDisplayName: Crossmate Notifications
- CFBundleShortVersionString: "1.0.0"
- CFBundleVersion: $(CURRENT_PROJECT_VERSION)
- NSExtension:
- NSExtensionPointIdentifier: com.apple.usernotifications.service
- NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).NotificationService
- settings:
- PRODUCT_BUNDLE_IDENTIFIER: net.inqk.crossmate.notificationservice
- INFOPLIST_FILE: NotificationService/Info.plist
- CODE_SIGN_ENTITLEMENTS: NotificationService/NotificationService.entitlements
- TARGETED_DEVICE_FAMILY: "1"
- CODE_SIGN_STYLE: Automatic
Crossmate Unit Tests:
type: bundle.unit-test