From 39a7b2437013a7bd8a19ad24d6c7d55827184309 Mon Sep 17 00:00:00 2001
From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com>
Date: Thu, 13 Aug 2020 03:18:21 -0700
Subject: [PATCH] Add push notification service extension
---
Development Assets/DevelopmentModels.swift | 6 +-
Development Assets/MockKeychainService.swift | 4 +-
Metatext.entitlements | 4 +
Metatext.xcodeproj/project.pbxproj | 181 +++++++++++++++++-
Notification Service Extension/Info.plist | 31 +++
...otification Service Extension.entitlements | 10 +
.../NotificationService.swift | 166 ++++++++++++++++
Shared/Localizations/Localizable.strings | 1 +
Shared/MetatextApp.swift | 2 +-
Shared/Model/PushNotification.swift | 23 +++
Shared/Services/KeychainService.swift | 46 +++--
Shared/Services/SecretsService.swift | 21 +-
...ce.swift => UserNotificationService.swift} | 34 +++-
Shared/View Models/RootViewModel.swift | 12 +-
macOS/macOS.entitlements | 4 +
15 files changed, 497 insertions(+), 48 deletions(-)
create mode 100644 Notification Service Extension/Info.plist
create mode 100644 Notification Service Extension/Notification Service Extension.entitlements
create mode 100644 Notification Service Extension/NotificationService.swift
create mode 100644 Shared/Model/PushNotification.swift
rename Shared/Services/{NotificationService.swift => UserNotificationService.swift} (53%)
diff --git a/Development Assets/DevelopmentModels.swift b/Development Assets/DevelopmentModels.swift
index 2d46497..09330b4 100644
--- a/Development Assets/DevelopmentModels.swift
+++ b/Development Assets/DevelopmentModels.swift
@@ -69,15 +69,15 @@ extension IdentityService {
static let development = try! IdentitiesService.development.identityService(id: devIdentityID)
}
-extension NotificationService {
- static let development = NotificationService(userNotificationCenter: .current())
+extension UserNotificationService {
+ static let development = UserNotificationService(userNotificationCenter: .current())
}
extension RootViewModel {
static let development = RootViewModel(
appDelegate: AppDelegate(),
identitiesService: .development,
- notificationService: .development)
+ userNotificationService: .development)
}
extension AddIdentityViewModel {
diff --git a/Development Assets/MockKeychainService.swift b/Development Assets/MockKeychainService.swift
index 11049f5..341a7ff 100644
--- a/Development Assets/MockKeychainService.swift
+++ b/Development Assets/MockKeychainService.swift
@@ -23,11 +23,11 @@ extension MockKeychainService: KeychainService {
items[account]
}
- static func generateKeyAndReturnPublicKey(applicationTag: String) throws -> Data {
+ static func generateKeyAndReturnPublicKey(applicationTag: String, attributes: [String: Any]) throws -> Data {
fatalError("not implemented")
}
- static func getPrivateKey(applicationTag: String) throws -> Data? {
+ static func getPrivateKey(applicationTag: String, attributes: [String: Any]) throws -> Data? {
fatalError("not implemented")
}
}
diff --git a/Metatext.entitlements b/Metatext.entitlements
index 903def2..ac209bb 100644
--- a/Metatext.entitlements
+++ b/Metatext.entitlements
@@ -4,5 +4,9 @@
aps-environment
development
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.metabolist.metatext
+
diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj
index 73ce389..49465d1 100644
--- a/Metatext.xcodeproj/project.pbxproj
+++ b/Metatext.xcodeproj/project.pbxproj
@@ -132,6 +132,17 @@
D0DC176124D0171800A75C65 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = D0DC176024D0171800A75C65 /* Alamofire */; };
D0DC177724D0CF2600A75C65 /* MockKeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* MockKeychainService.swift */; };
D0DC177824D0CF2600A75C65 /* MockKeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* MockKeychainService.swift */; };
+ D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
+ D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ D0E5362524E3FE2300FB1CE1 /* SecretsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */; };
+ D0E5362624E3FE2C00FB1CE1 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC424DF842700A08489 /* KeychainService.swift */; };
+ D0E5362724E4047C00FB1CE1 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; };
+ D0E5362C24E534BD00FB1CE1 /* Unknowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847B24DBEA9F00CF380C /* Unknowable.swift */; };
+ D0E5362D24E5430F00FB1CE1 /* MastodonDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D019E6D624DF728400697C7D /* MastodonDecoder.swift */; };
+ D0E5362E24E5432000FB1CE1 /* MastodonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175A24D0154F00A75C65 /* MastodonAPI.swift */; };
+ D0E5363024E5436C00FB1CE1 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */; };
+ D0E5363124E5453E00FB1CE1 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */; };
+ D0E5363224E5453F00FB1CE1 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */; };
D0EC8DC224DF7D9C00A08489 /* IdentityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */; };
D0EC8DC324DF7D9C00A08489 /* IdentityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */; };
D0EC8DC524DF842700A08489 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DC424DF842700A08489 /* KeychainService.swift */; };
@@ -145,8 +156,8 @@
D0EC8DD424DFE38900A08489 /* AuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DD324DFE38900A08489 /* AuthenticationServiceTests.swift */; };
D0EC8DDF24E09D7000A08489 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DDE24E09D7000A08489 /* AppDelegate.swift */; };
D0EC8DE024E09D7000A08489 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DDE24E09D7000A08489 /* AppDelegate.swift */; };
- D0EC8DE424E0B44400A08489 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DD724E096C900A08489 /* NotificationService.swift */; };
- D0EC8DE524E0B44500A08489 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DD724E096C900A08489 /* NotificationService.swift */; };
+ D0EC8DE424E0B44400A08489 /* UserNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DD724E096C900A08489 /* UserNotificationService.swift */; };
+ D0EC8DE524E0B44500A08489 /* UserNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DD724E096C900A08489 /* UserNotificationService.swift */; };
D0EC8DE824E21FEC00A08489 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DE724E21FEC00A08489 /* Data+Extensions.swift */; };
D0EC8DE924E21FEC00A08489 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DE724E21FEC00A08489 /* Data+Extensions.swift */; };
D0EC8DEB24E26F1100A08489 /* PushSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC8DEA24E26F1100A08489 /* PushSubscription.swift */; };
@@ -180,8 +191,29 @@
remoteGlobalIDString = D047FA8B24C3E21200AF17C5;
remoteInfo = "Metatext (iOS)";
};
+ D0E5361E24E3EB4D00FB1CE1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = D047FA8024C3E21000AF17C5 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = D0E5361824E3EB4D00FB1CE1;
+ remoteInfo = "Notification Service Extension";
+ };
/* End PBXContainerItemProxy section */
+/* Begin PBXCopyFilesBuildPhase section */
+ D0E5362424E3EB4D00FB1CE1 /* Embed App Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */,
+ );
+ name = "Embed App Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
/* Begin PBXFileReference section */
D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesView.swift; sourceTree = ""; };
D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesViewModel.swift; sourceTree = ""; };
@@ -254,13 +286,18 @@
D0DC175724D0130800A75C65 /* HTTPStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStubs.swift; sourceTree = ""; };
D0DC175A24D0154F00A75C65 /* MastodonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAPI.swift; sourceTree = ""; };
D0DC177624D0CF2600A75C65 /* MockKeychainService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychainService.swift; sourceTree = ""; };
+ D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+ D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
+ D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notification Service Extension.entitlements"; sourceTree = ""; };
+ D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = ""; };
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityService.swift; sourceTree = ""; };
D0EC8DC424DF842700A08489 /* KeychainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainService.swift; sourceTree = ""; };
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretsService.swift; sourceTree = ""; };
D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitiesService.swift; sourceTree = ""; };
D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; };
D0EC8DD324DFE38900A08489 /* AuthenticationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceTests.swift; sourceTree = ""; };
- D0EC8DD724E096C900A08489 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
+ D0EC8DD724E096C900A08489 /* UserNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationService.swift; sourceTree = ""; };
D0EC8DDE24E09D7000A08489 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
D0EC8DE624E0BA6500A08489 /* Metatext.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = ""; };
D0EC8DE724E21FEC00A08489 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; };
@@ -306,6 +343,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D0E5361624E3EB4D00FB1CE1 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -375,7 +419,7 @@
D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */,
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */,
D0EC8DC424DF842700A08489 /* KeychainService.swift */,
- D0EC8DD724E096C900A08489 /* NotificationService.swift */,
+ D0EC8DD724E096C900A08489 /* UserNotificationService.swift */,
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */,
);
path = Services;
@@ -384,11 +428,12 @@
D047FA7F24C3E21000AF17C5 = {
isa = PBXGroup;
children = (
- D0EC8DE624E0BA6500A08489 /* Metatext.entitlements */,
D0ED1BB224CE3A1600B4899C /* Development Assets */,
D0666A7924C7745A00F3F04B /* Frameworks */,
D047FA8E24C3E21200AF17C5 /* iOS */,
D047FA9524C3E21200AF17C5 /* macOS */,
+ D0EC8DE624E0BA6500A08489 /* Metatext.entitlements */,
+ D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */,
D047FA8D24C3E21200AF17C5 /* Products */,
D047FA8424C3E21000AF17C5 /* Shared */,
D0666A2224C677B400F3F04B /* Tests */,
@@ -419,6 +464,7 @@
D047FA8C24C3E21200AF17C5 /* Metatext.app */,
D047FA9424C3E21200AF17C5 /* Metatext.app */,
D0666A2124C677B400F3F04B /* Tests.xctest */,
+ D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */,
);
name = Products;
sourceTree = "";
@@ -468,6 +514,7 @@
D0666A4D24C6C39600F3F04B /* Instance.swift */,
D0ED1BE224CFA84400B4899C /* MastodonError.swift */,
D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */,
+ D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */,
D0EC8DEA24E26F1100A08489 /* PushSubscription.swift */,
D0CD847524DBDF3C00CF380C /* Status.swift */,
D0CD847B24DBEA9F00CF380C /* Unknowable.swift */,
@@ -552,6 +599,16 @@
path = "Mastodon API Stubs";
sourceTree = "";
};
+ D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */ = {
+ isa = PBXGroup;
+ children = (
+ D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */,
+ D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */,
+ D0E5361D24E3EB4D00FB1CE1 /* Info.plist */,
+ );
+ path = "Notification Service Extension";
+ sourceTree = "";
+ };
D0EC8DD024DFE34F00A08489 /* Services */ = {
isa = PBXGroup;
children = (
@@ -610,10 +667,12 @@
D047FA8924C3E21200AF17C5 /* Frameworks */,
D047FA8A24C3E21200AF17C5 /* Resources */,
D0666A2E24C67E6700F3F04B /* ShellScript */,
+ D0E5362424E3EB4D00FB1CE1 /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
+ D0E5361F24E3EB4D00FB1CE1 /* PBXTargetDependency */,
);
name = "Metatext (iOS)";
packageProductDependencies = (
@@ -669,6 +728,25 @@
productReference = D0666A2124C677B400F3F04B /* Tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
+ D0E5361824E3EB4D00FB1CE1 /* Notification Service Extension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = D0E5362124E3EB4D00FB1CE1 /* Build configuration list for PBXNativeTarget "Notification Service Extension" */;
+ buildPhases = (
+ D0E5361524E3EB4D00FB1CE1 /* Sources */,
+ D0E5361624E3EB4D00FB1CE1 /* Frameworks */,
+ D0E5361724E3EB4D00FB1CE1 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "Notification Service Extension";
+ packageProductDependencies = (
+ );
+ productName = "Notification Service Extension";
+ productReference = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -690,6 +768,9 @@
LastSwiftMigration = 1200;
TestTargetID = D047FA8B24C3E21200AF17C5;
};
+ D0E5361824E3EB4D00FB1CE1 = {
+ CreatedOnToolsVersion = 12.0;
+ };
};
};
buildConfigurationList = D047FA8324C3E21000AF17C5 /* Build configuration list for PBXProject "Metatext" */;
@@ -714,6 +795,7 @@
D047FA8B24C3E21200AF17C5 /* Metatext (iOS) */,
D047FA9324C3E21200AF17C5 /* Metatext (macOS) */,
D0666A2024C677B400F3F04B /* Tests */,
+ D0E5361824E3EB4D00FB1CE1 /* Notification Service Extension */,
);
};
/* End PBXProject section */
@@ -744,6 +826,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D0E5361724E3EB4D00FB1CE1 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -830,11 +919,12 @@
D0DC175824D0130800A75C65 /* HTTPStubs.swift in Sources */,
D0DC177724D0CF2600A75C65 /* MockKeychainService.swift in Sources */,
D0EC8DC224DF7D9C00A08489 /* IdentityService.swift in Sources */,
+ D0E5363024E5436C00FB1CE1 /* PushNotification.swift in Sources */,
D0C963FB24CC359D003BD330 /* AlertItem.swift in Sources */,
D0DC174624CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */,
D019E6F024DF7C2F00697C7D /* DatabaseError.swift in Sources */,
D019E6D724DF728400697C7D /* MastodonEncoder.swift in Sources */,
- D0EC8DE524E0B44500A08489 /* NotificationService.swift in Sources */,
+ D0EC8DE524E0B44500A08489 /* UserNotificationService.swift in Sources */,
D0EC8DCB24DFA06700A08489 /* IdentitiesService.swift in Sources */,
D0091B7124DD68220040E8D2 /* PreferencesViewModel.swift in Sources */,
D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */,
@@ -872,6 +962,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ D0E5363124E5453E00FB1CE1 /* PushNotification.swift in Sources */,
D04FD73A24D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */,
D0DB6F0A24C65AC000D965FE /* AddIdentityViewModel.swift in Sources */,
D0CD847424DBDEC700CF380C /* MastodonPreferences.swift in Sources */,
@@ -891,7 +982,7 @@
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */,
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
D0666A4C24C6C37700F3F04B /* Identity.swift in Sources */,
- D0EC8DE424E0B44400A08489 /* NotificationService.swift in Sources */,
+ D0EC8DE424E0B44400A08489 /* UserNotificationService.swift in Sources */,
D0EC8DCC24DFA06700A08489 /* IdentitiesService.swift in Sources */,
D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */,
D019E6EE24DF7BF300697C7D /* IdentityDatabase.swift in Sources */,
@@ -957,6 +1048,21 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D0E5361524E3EB4D00FB1CE1 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D0E5363224E5453F00FB1CE1 /* PushNotification.swift in Sources */,
+ D0E5362C24E534BD00FB1CE1 /* Unknowable.swift in Sources */,
+ D0E5362D24E5430F00FB1CE1 /* MastodonDecoder.swift in Sources */,
+ D0E5362E24E5432000FB1CE1 /* MastodonAPI.swift in Sources */,
+ D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */,
+ D0E5362724E4047C00FB1CE1 /* NSError+Extensions.swift in Sources */,
+ D0E5362524E3FE2300FB1CE1 /* SecretsService.swift in Sources */,
+ D0E5362624E3FE2C00FB1CE1 /* KeychainService.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -965,6 +1071,11 @@
target = D047FA8B24C3E21200AF17C5 /* Metatext (iOS) */;
targetProxy = D0666A2624C677B400F3F04B /* PBXContainerItemProxy */;
};
+ D0E5361F24E3EB4D00FB1CE1 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = D0E5361824E3EB4D00FB1CE1 /* Notification Service Extension */;
+ targetProxy = D0E5361E24E3EB4D00FB1CE1 /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -1082,6 +1193,7 @@
D047FAB724C3E21200AF17C5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Metatext.entitlements;
@@ -1106,6 +1218,7 @@
D047FAB824C3E21200AF17C5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Metatext.entitlements;
@@ -1226,6 +1339,51 @@
};
name = Release;
};
+ D0E5362224E3EB4D00FB1CE1 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = "Notification Service Extension/Notification Service Extension.entitlements";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = 82HL67AXQ2;
+ INFOPLIST_FILE = "Notification Service Extension/Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.metatext.notification-service-extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ D0E5362324E3EB4D00FB1CE1 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = "Notification Service Extension/Notification Service Extension.entitlements";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = 82HL67AXQ2;
+ INFOPLIST_FILE = "Notification Service Extension/Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.metatext.notification-service-extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -1265,6 +1423,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ D0E5362124E3EB4D00FB1CE1 /* Build configuration list for PBXNativeTarget "Notification Service Extension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ D0E5362224E3EB4D00FB1CE1 /* Debug */,
+ D0E5362324E3EB4D00FB1CE1 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
diff --git a/Notification Service Extension/Info.plist b/Notification Service Extension/Info.plist
new file mode 100644
index 0000000..7393769
--- /dev/null
+++ b/Notification Service Extension/Info.plist
@@ -0,0 +1,31 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Notification Service Extension
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.usernotifications.service
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).NotificationService
+
+
+
diff --git a/Notification Service Extension/Notification Service Extension.entitlements b/Notification Service Extension/Notification Service Extension.entitlements
new file mode 100644
index 0000000..664ace1
--- /dev/null
+++ b/Notification Service Extension/Notification Service Extension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.metabolist.metatext
+
+
+
diff --git a/Notification Service Extension/NotificationService.swift b/Notification Service Extension/NotificationService.swift
new file mode 100644
index 0000000..5ee0e32
--- /dev/null
+++ b/Notification Service Extension/NotificationService.swift
@@ -0,0 +1,166 @@
+// Copyright © 2020 Metabolist. All rights reserved.
+
+import UserNotifications
+import CryptoKit
+
+class NotificationService: UNNotificationServiceExtension {
+
+ var contentHandler: ((UNNotificationContent) -> Void)?
+ var bestAttemptContent: UNMutableNotificationContent?
+
+ override func didReceive(
+ _ request: UNNotificationRequest,
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+ self.contentHandler = contentHandler
+ bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
+
+ guard let bestAttemptContent = bestAttemptContent else { return }
+
+ let pushNotification: PushNotification
+
+ do {
+ let decryptedJSON = try Self.extractAndDecrypt(userInfo: request.content.userInfo)
+
+ pushNotification = try MastodonDecoder().decode(PushNotification.self, from: decryptedJSON)
+ } catch {
+ contentHandler(bestAttemptContent)
+
+ return
+ }
+
+ bestAttemptContent.title = pushNotification.title
+ bestAttemptContent.body = pushNotification.body
+
+ let fileName = pushNotification.icon.lastPathComponent
+ let fileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(fileName)
+
+ do {
+ let iconData = try Data(contentsOf: pushNotification.icon)
+
+ try iconData.write(to: fileURL)
+ bestAttemptContent.attachments = [try UNNotificationAttachment(identifier: fileName, url: fileURL)]
+ } catch {
+ // no-op
+ }
+
+ contentHandler(bestAttemptContent)
+ }
+
+ override func serviceExtensionTimeWillExpire() {
+ if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
+ contentHandler(bestAttemptContent)
+ }
+ }
+}
+
+enum NotificationServiceError: Error {
+ case userInfoDataAbsent
+ case keychainDataAbsent
+}
+
+private extension NotificationService {
+ static let identityIDUserInfoKey = "i"
+ static let encryptedMessageUserInfoKey = "m"
+ static let saltUserInfoKey = "s"
+ static let serverPublicKeyUserInfoKey = "k"
+ static let keyLength = 16
+ static let nonceLength = 12
+ static let pseudoRandomKeyLength = 32
+ static let paddedByteCount = 2
+ static let curve = "P-256"
+
+ enum HKDFInfo: String {
+ case auth, aesgcm, nonce
+
+ var bytes: [UInt8] {
+ Array("Content-Encoding: \(self)\0".utf8)
+ }
+ }
+
+ static func extractAndDecrypt(userInfo: [AnyHashable: Any]) throws -> Data {
+ guard
+ let identityIDString = userInfo[identityIDUserInfoKey] as? String,
+ let identityID = UUID(uuidString: identityIDString),
+ let encryptedMessageBase64 = (userInfo[encryptedMessageUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
+ let encryptedMessage = Data(base64Encoded: encryptedMessageBase64),
+ let saltBase64 = (userInfo[saltUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
+ let salt = Data(base64Encoded: saltBase64),
+ let serverPublicKeyBase64 = (userInfo[serverPublicKeyUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
+ let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64)
+ else { throw NotificationServiceError.userInfoDataAbsent }
+
+ let secretsService = SecretsService(identityID: identityID, keychainServiceType: LiveKeychainService.self)
+
+ guard
+ let auth = try secretsService.getPushAuth(),
+ let pushKey = try secretsService.getPushKey()
+ else { throw NotificationServiceError.keychainDataAbsent }
+
+ return try decrypt(encryptedMessage: encryptedMessage,
+ privateKeyData: pushKey,
+ serverPublicKeyData: serverPublicKeyData,
+ auth: auth,
+ salt: salt)
+ }
+
+ static func decrypt(encryptedMessage: Data,
+ privateKeyData: Data,
+ serverPublicKeyData: Data,
+ auth: Data,
+ salt: Data) throws -> Data {
+ let privateKey = try P256.KeyAgreement.PrivateKey(x963Representation: privateKeyData)
+ let serverPublicKey = try P256.KeyAgreement.PublicKey(x963Representation: serverPublicKeyData)
+ let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: serverPublicKey)
+
+ var keyInfo = HKDFInfo.aesgcm.bytes
+ var nonceInfo = HKDFInfo.nonce.bytes
+ var context = Array(curve.utf8)
+ let publicKeyData = privateKey.publicKey.x963Representation
+
+ context.append(0)
+ context.append(0)
+ context.append(UInt8(publicKeyData.count))
+ context += Array(publicKeyData)
+ context.append(0)
+ context.append(UInt8(serverPublicKeyData.count))
+ context += Array(serverPublicKeyData)
+
+ keyInfo += context
+ nonceInfo += context
+
+ let pseudoRandomKey = sharedSecret.hkdfDerivedSymmetricKey(
+ using: SHA256.self,
+ salt: auth,
+ sharedInfo: HKDFInfo.auth.bytes,
+ outputByteCount: pseudoRandomKeyLength)
+ let key = HKDF.deriveKey(
+ inputKeyMaterial: pseudoRandomKey,
+ salt: salt,
+ info: keyInfo,
+ outputByteCount: keyLength)
+ let nonce = HKDF.deriveKey(
+ inputKeyMaterial: pseudoRandomKey,
+ salt: salt,
+ info: nonceInfo,
+ outputByteCount: nonceLength)
+
+ let sealedBox = try AES.GCM.SealedBox(combined: nonce.withUnsafeBytes(Array.init) + encryptedMessage)
+ let decrypted = try AES.GCM.open(sealedBox, using: key)
+ let unpadded = decrypted.suffix(from: paddedByteCount)
+
+ return Data(unpadded)
+ }
+}
+
+extension String {
+ func URLSafeBase64ToBase64() -> String {
+ var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
+ let countMod4 = count % 4
+
+ if countMod4 != 0 {
+ base64.append(String(repeating: "=", count: 4 - countMod4))
+ }
+
+ return base64
+ }
+}
diff --git a/Shared/Localizations/Localizable.strings b/Shared/Localizations/Localizable.strings
index 2ed5036..5b1a923 100644
--- a/Shared/Localizations/Localizable.strings
+++ b/Shared/Localizations/Localizable.strings
@@ -1,5 +1,6 @@
// Copyright © 2020 Metabolist. All rights reserved.
+"apns-default-message" = "New notification";
"add-identity.instance-url" = "Instance URL";
"add-identity.log-in" = "Log in";
"add-identity.browse-anonymously" = "Browse anonymously";
diff --git a/Shared/MetatextApp.swift b/Shared/MetatextApp.swift
index d639752..5634663 100644
--- a/Shared/MetatextApp.swift
+++ b/Shared/MetatextApp.swift
@@ -29,7 +29,7 @@ struct MetatextApp: App {
RootView(
viewModel: RootViewModel(appDelegate: appDelegate,
identitiesService: identitiesService,
- notificationService: NotificationService()))
+ userNotificationService: UserNotificationService()))
}
}
}
diff --git a/Shared/Model/PushNotification.swift b/Shared/Model/PushNotification.swift
new file mode 100644
index 0000000..924dfcd
--- /dev/null
+++ b/Shared/Model/PushNotification.swift
@@ -0,0 +1,23 @@
+// Copyright © 2020 Metabolist. All rights reserved.
+
+import Foundation
+
+struct PushNotification: Codable {
+ enum NotificationType: String, Codable, Unknowable {
+ case mention
+ case reblog
+ case favourite
+ case follow
+ case unknown
+
+ static var unknownCase: Self { .unknown }
+ }
+
+ let accessToken: String
+ let body: String
+ let title: String
+ let icon: URL
+ let notificationId: Int
+ let notificationType: NotificationType
+ let preferredLocale: String
+}
diff --git a/Shared/Services/KeychainService.swift b/Shared/Services/KeychainService.swift
index 64e9332..23310ce 100644
--- a/Shared/Services/KeychainService.swift
+++ b/Shared/Services/KeychainService.swift
@@ -6,8 +6,8 @@ protocol KeychainService {
static func setGenericPassword(data: Data, forAccount key: String, service: String) throws
static func deleteGenericPassword(account: String, service: String) throws
static func getGenericPassword(account: String, service: String) throws -> Data?
- static func generateKeyAndReturnPublicKey(applicationTag: String) throws -> Data
- static func getPrivateKey(applicationTag: String) throws -> Data?
+ static func generateKeyAndReturnPublicKey(applicationTag: String, attributes: [String: Any]) throws -> Data
+ static func getPrivateKey(applicationTag: String, attributes: [String: Any]) throws -> Data?
}
struct LiveKeychainService {}
@@ -52,13 +52,21 @@ extension LiveKeychainService: KeychainService {
}
}
- static func generateKeyAndReturnPublicKey(applicationTag: String) throws -> Data {
- var attributes = keyAttributes
+ static func generateKeyAndReturnPublicKey(applicationTag: String, attributes: [String: Any]) throws -> Data {
+ var attributes = attributes
var error: Unmanaged?
+ guard let accessControl = SecAccessControlCreateWithFlags(
+ kCFAllocatorDefault,
+ kSecAttrAccessibleAfterFirstUnlock,
+ [],
+ &error)
+ else { throw error?.takeRetainedValue() ?? NSError() }
+
attributes[kSecPrivateKeyAttrs as String] = [
kSecAttrIsPermanent as String: true,
- kSecAttrApplicationTag as String: Data(applicationTag.utf8)]
+ kSecAttrApplicationTag as String: Data(applicationTag.utf8),
+ kSecAttrAccessControl as String: accessControl]
guard
let key = SecKeyCreateRandomKey(attributes as CFDictionary, &error),
@@ -69,13 +77,27 @@ extension LiveKeychainService: KeychainService {
return publicKeyData
}
- static func getPrivateKey(applicationTag: String) throws -> Data? {
+ static func getPrivateKey(applicationTag: String, attributes: [String: Any]) throws -> Data? {
var result: AnyObject?
- let status = SecItemCopyMatching(keyQueryDictionary(applicationTag: applicationTag) as CFDictionary, &result)
+ var error: Unmanaged?
+ var query = keyQueryDictionary(applicationTag: applicationTag)
+
+ query.merge(attributes, uniquingKeysWith: { $1 })
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
+ query[kSecReturnRef as String] = kCFBooleanTrue
+
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
switch status {
case errSecSuccess:
- return result as? Data
+ // swiftlint:disable force_cast
+ let secKey = result as! SecKey
+ // swiftlint:enable force_cast
+ guard let data = SecKeyCopyExternalRepresentation(secKey, &error) else {
+ throw error?.takeRetainedValue() ?? NSError()
+ }
+
+ return data as Data
case errSecItemNotFound:
return nil
default:
@@ -85,8 +107,6 @@ extension LiveKeychainService: KeychainService {
}
private extension LiveKeychainService {
- static let keySizeInBits = 256
-
static func genericPasswordQueryDictionary(account: String, service: String) -> [String: Any] {
[kSecAttrService as String: service,
kSecAttrAccount as String: account,
@@ -96,13 +116,7 @@ private extension LiveKeychainService {
static func keyQueryDictionary(applicationTag: String) -> [String: Any] {
[kSecClass as String: kSecClassKey,
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
- kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
- kSecAttrKeySizeInBits as String: keySizeInBits,
kSecAttrApplicationTag as String: applicationTag,
kSecReturnRef as String: true]
}
-
- static let keyAttributes: [String: Any] = [
- kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
- kSecAttrKeySizeInBits as String: keySizeInBits]
}
diff --git a/Shared/Services/SecretsService.swift b/Shared/Services/SecretsService.swift
index c7405ec..29f5907 100644
--- a/Shared/Services/SecretsService.swift
+++ b/Shared/Services/SecretsService.swift
@@ -58,17 +58,21 @@ extension SecretsService {
}
func generatePushKeyAndReturnPublicKey() throws -> Data {
- try keychainServiceType.generateKeyAndReturnPublicKey(applicationTag: key(item: .pushKey))
+ try keychainServiceType.generateKeyAndReturnPublicKey(
+ applicationTag: key(item: .pushKey),
+ attributes: PushKey.attributes)
}
func getPushKey() throws -> Data? {
- try keychainServiceType.getPrivateKey(applicationTag: key(item: .pushKey))
+ try keychainServiceType.getPrivateKey(
+ applicationTag: key(item: .pushKey),
+ attributes: PushKey.attributes)
}
func generatePushAuth() throws -> Data {
- var bytes = [UInt8](repeating: 0, count: Self.authLength)
+ var bytes = [UInt8](repeating: 0, count: PushKey.authLength)
- _ = SecRandomCopyBytes(kSecRandomDefault, Self.authLength, &bytes)
+ _ = SecRandomCopyBytes(kSecRandomDefault, PushKey.authLength, &bytes)
let pushAuth = Data(bytes)
@@ -84,7 +88,6 @@ extension SecretsService {
private extension SecretsService {
static let keychainServiceName = "com.metabolist.metatext"
- private static let authLength = 16
func key(item: Item) -> String {
identityID.uuidString + "." + item.rawValue
@@ -110,3 +113,11 @@ extension String: SecretsStorable {
return string
}
}
+
+struct PushKey {
+ static let authLength = 16
+ static let sizeInBits = 256
+ static let attributes: [String: Any] = [
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
+ kSecAttrKeySizeInBits as String: sizeInBits]
+}
diff --git a/Shared/Services/NotificationService.swift b/Shared/Services/UserNotificationService.swift
similarity index 53%
rename from Shared/Services/NotificationService.swift
rename to Shared/Services/UserNotificationService.swift
index c16a467..ef65e2f 100644
--- a/Shared/Services/NotificationService.swift
+++ b/Shared/Services/UserNotificationService.swift
@@ -4,21 +4,27 @@ import Foundation
import Combine
import UserNotifications
-struct NotificationService {
+class UserNotificationService: NSObject {
private let userNotificationCenter: UNUserNotificationCenter
init(userNotificationCenter: UNUserNotificationCenter = .current()) {
self.userNotificationCenter = userNotificationCenter
+
+ super.init()
+
+ userNotificationCenter.delegate = self
}
}
-extension NotificationService {
+extension UserNotificationService {
func isAuthorized() -> AnyPublisher {
getNotificationSettings()
.map(\.authorizationStatus)
- .flatMap { status -> AnyPublisher in
+ .flatMap { [weak self] status -> AnyPublisher in
if status == .notDetermined {
- return requestProvisionalAuthorization().eraseToAnyPublisher()
+ return self?.requestProvisionalAuthorization()
+ .eraseToAnyPublisher()
+ ?? Empty().eraseToAnyPublisher()
}
return Just(status == .authorized || status == .provisional)
@@ -29,17 +35,17 @@ extension NotificationService {
}
}
-private extension NotificationService {
+private extension UserNotificationService {
func getNotificationSettings() -> AnyPublisher {
- Future { promise in
- userNotificationCenter.getNotificationSettings { promise(.success($0)) }
+ Future { [weak self] promise in
+ self?.userNotificationCenter.getNotificationSettings { promise(.success($0)) }
}
.eraseToAnyPublisher()
}
func requestProvisionalAuthorization() -> AnyPublisher {
- Future { promise in
- userNotificationCenter.requestAuthorization(
+ Future { [weak self] promise in
+ self?.userNotificationCenter.requestAuthorization(
options: [.alert, .sound, .badge, .provisional]) { granted, error in
if let error = error {
return promise(.failure(error))
@@ -51,3 +57,13 @@ private extension NotificationService {
.eraseToAnyPublisher()
}
}
+
+extension UserNotificationService: UNUserNotificationCenterDelegate {
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
+ print(notification.request.content.body)
+ completionHandler(.banner)
+ }
+}
diff --git a/Shared/View Models/RootViewModel.swift b/Shared/View Models/RootViewModel.swift
index 02aafed..f051064 100644
--- a/Shared/View Models/RootViewModel.swift
+++ b/Shared/View Models/RootViewModel.swift
@@ -10,17 +10,19 @@ class RootViewModel: ObservableObject {
private let appDelegate: AppDelegate
// swiftlint:enable weak_delegate
private let identitiesService: IdentitiesService
- private let notificationService: NotificationService
+ private let userNotificationService: UserNotificationService
private var cancellables = Set()
- init(appDelegate: AppDelegate, identitiesService: IdentitiesService, notificationService: NotificationService) {
+ init(appDelegate: AppDelegate,
+ identitiesService: IdentitiesService,
+ userNotificationService: UserNotificationService) {
self.appDelegate = appDelegate
self.identitiesService = identitiesService
- self.notificationService = notificationService
+ self.userNotificationService = userNotificationService
newIdentitySelected(id: identitiesService.mostRecentlyUsedIdentityID)
- notificationService.isAuthorized()
+ userNotificationService.isAuthorized()
.filter { $0 }
.zip(appDelegate.registerForRemoteNotifications())
.map { $1 }
@@ -62,7 +64,7 @@ extension RootViewModel {
func newIdentityCreated(id: UUID, instanceURL: URL) {
newIdentitySelected(id: id)
- notificationService.isAuthorized()
+ userNotificationService.isAuthorized()
.filter { $0 }
.zip(appDelegate.registerForRemoteNotifications())
.map { (id, instanceURL, $1, nil) }
diff --git a/macOS/macOS.entitlements b/macOS/macOS.entitlements
index b7b98df..2d82bbf 100644
--- a/macOS/macOS.entitlements
+++ b/macOS/macOS.entitlements
@@ -10,5 +10,9 @@
com.apple.security.network.client
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.metabolist.metatext
+