Add push notification service extension

This commit is contained in:
Justin Mazzocchi 2020-08-13 03:18:21 -07:00
parent b6704c1099
commit 39a7b24370
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
15 changed files with 497 additions and 48 deletions

View File

@ -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 {

View File

@ -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")
}
}

View File

@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.metabolist.metatext</string>
</array>
</dict>
</plist>

View File

@ -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 = "<group>"; };
D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesViewModel.swift; sourceTree = "<group>"; };
@ -254,13 +286,18 @@
D0DC175724D0130800A75C65 /* HTTPStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStubs.swift; sourceTree = "<group>"; };
D0DC175A24D0154F00A75C65 /* MastodonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAPI.swift; sourceTree = "<group>"; };
D0DC177624D0CF2600A75C65 /* MockKeychainService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychainService.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notification Service Extension.entitlements"; sourceTree = "<group>"; };
D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = "<group>"; };
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityService.swift; sourceTree = "<group>"; };
D0EC8DC424DF842700A08489 /* KeychainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainService.swift; sourceTree = "<group>"; };
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretsService.swift; sourceTree = "<group>"; };
D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitiesService.swift; sourceTree = "<group>"; };
D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
D0EC8DD324DFE38900A08489 /* AuthenticationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceTests.swift; sourceTree = "<group>"; };
D0EC8DD724E096C900A08489 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
D0EC8DD724E096C900A08489 /* UserNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationService.swift; sourceTree = "<group>"; };
D0EC8DDE24E09D7000A08489 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
D0EC8DE624E0BA6500A08489 /* Metatext.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
D0EC8DE724E21FEC00A08489 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 = "<group>";
};
D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */ = {
isa = PBXGroup;
children = (
D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */,
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */,
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */,
);
path = "Notification Service Extension";
sourceTree = "<group>";
};
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 */

View File

@ -0,0 +1,31 @@
<?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>Notification Service Extension</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>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</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>

View File

@ -0,0 +1,10 @@
<?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>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.metabolist.metatext</string>
</array>
</dict>
</plist>

View File

@ -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<SHA256>.deriveKey(
inputKeyMaterial: pseudoRandomKey,
salt: salt,
info: keyInfo,
outputByteCount: keyLength)
let nonce = HKDF<SHA256>.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
}
}

View File

@ -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";

View File

@ -29,7 +29,7 @@ struct MetatextApp: App {
RootView(
viewModel: RootViewModel(appDelegate: appDelegate,
identitiesService: identitiesService,
notificationService: NotificationService()))
userNotificationService: UserNotificationService()))
}
}
}

View File

@ -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
}

View File

@ -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<CFError>?
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<CFError>?
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]
}

View File

@ -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]
}

View File

@ -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<Bool, Error> {
getNotificationSettings()
.map(\.authorizationStatus)
.flatMap { status -> AnyPublisher<Bool, Error> in
.flatMap { [weak self] status -> AnyPublisher<Bool, Error> 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<UNNotificationSettings, Never> {
Future<UNNotificationSettings, Never> { promise in
userNotificationCenter.getNotificationSettings { promise(.success($0)) }
Future<UNNotificationSettings, Never> { [weak self] promise in
self?.userNotificationCenter.getNotificationSettings { promise(.success($0)) }
}
.eraseToAnyPublisher()
}
func requestProvisionalAuthorization() -> AnyPublisher<Bool, Error> {
Future<Bool, Error> { promise in
userNotificationCenter.requestAuthorization(
Future<Bool, Error> { [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)
}
}

View File

@ -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<AnyCancellable>()
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) }

View File

@ -10,5 +10,9 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.metabolist.metatext</string>
</array>
</dict>
</plist>