mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 01:31:02 +00:00
Add push notification service extension
This commit is contained in:
parent
b6704c1099
commit
39a7b24370
15 changed files with 497 additions and 48 deletions
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 */
|
||||
|
|
31
Notification Service Extension/Info.plist
Normal file
31
Notification Service Extension/Info.plist
Normal 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>
|
|
@ -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>
|
166
Notification Service Extension/NotificationService.swift
Normal file
166
Notification Service Extension/NotificationService.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -29,7 +29,7 @@ struct MetatextApp: App {
|
|||
RootView(
|
||||
viewModel: RootViewModel(appDelegate: appDelegate,
|
||||
identitiesService: identitiesService,
|
||||
notificationService: NotificationService()))
|
||||
userNotificationService: UserNotificationService()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
23
Shared/Model/PushNotification.swift
Normal file
23
Shared/Model/PushNotification.swift
Normal 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
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue