From 49396325426ccf2afef01bc45df9d2da635ca7fa Mon Sep 17 00:00:00 2001 From: Bryant Jimenez <84168212+bryant-jimenez@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:56:38 -0700 Subject: [PATCH] Handling Background Notifications (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # *Handling Background Notifications* ## :recycle: Current situation & Problem - https://github.com/CS342/2024-Prisma/issues/31 - https://github.com/CS342/2024-Prisma/pull/36 Currently, Prisma is only configured to handle notifications in the foreground. Handling notifications requires writing timestamps to Firestore when notifications are received and opened on the user's device, but this was not possible with the current configuration of background notification handling due to Apple throttling the amount of requests per hour [(see this link)](https://developer.apple.com/documentation/usernotifications/pushing-background-updates-to-your-app). ## :gear: Release Notes As a workaround, we utilize the UNNotificationServiceExtension class to provide an entry-point for pushing updates to Firestore on the arrival of background notifications. Normally we would use this extension to modify the notification’s content or download content related to the extension, but we are taking advantage of the fact that we are allowed a 30 second period to do so, in order to write to Firestore. Doing so required [creating a shared keychain for cross-app authentication](https://firebase.google.com/docs/auth/ios/single-sign-on), allowing access of the auth state for Firebase between the application and the extension, utilized for authorizing writes to Firestore using the same instance of Firebase as the application upon notification arrival. The following additions/changes were made to the Prisma application: - Added the Notification Service Extension `PrismaPushNotificationsExtension/NotificationsService.swift` - `didReceive` implements functionality for writing to Firestore, using the logs collection path passed in the notification payload. Because the main application and app extension run independently of each other, we needed to introduce an authorization mechanism that is checked in the function before actually doing any writes. - We also added a keychain access group, allowing for keychain sharing between the main app and the extension. We subsequently implemented the authorization function `authorizeAccessGroupForCurrentUser()` which checks for the shared access group instance for a user, documented in `PrismaStandard.swift`. - This function is called within both the Home view and the AccountOnboarding flow, ensuring that a user will always need to be authorized and logged in in order to receive background notification updates and have those writable to Firestore. ## :books: Documentation *Please ensure that you properly document any additions in conformance to [Spezi Documentation Guide](https://github.com/StanfordSpezi/.github/blob/main/DOCUMENTATIONGUIDE.md).* *You can use this section to describe your solution, but we encourage contributors to document your reasoning and changes using in-line documentation.* ## :white_check_mark: Testing *Please ensure that the PR meets the testing requirements set by CodeCov and that new functionality is appropriately tested.* *This section describes important information about the tests and why some elements might not be testable.* ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md): - [ x] I agree to follow the [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md). --- Prisma.xcodeproj/project.pbxproj | 386 +++++++++++++++++- Prisma/Home.swift | 5 + Prisma/Onboarding/AccountOnboarding.swift | 16 +- .../PushNotifications/PushNotifications.swift | 11 +- Prisma/SharedContext/FeatureFlags.swift | 10 +- .../PrismaStandard+PushNotifications.swift | 9 +- Prisma/Standard/PrismaStandard.swift | 33 ++ Prisma/Supporting Files/Prisma.entitlements | 4 + PrismaPushNotificationsExtension/Info.plist | 13 + .../Info.plist.license | 6 + .../NotificationService.swift | 66 +++ ...smaPushNotificationsExtension.entitlements | 10 + ...otificationsExtension.entitlements.license | 6 + 13 files changed, 555 insertions(+), 20 deletions(-) create mode 100644 PrismaPushNotificationsExtension/Info.plist create mode 100644 PrismaPushNotificationsExtension/Info.plist.license create mode 100644 PrismaPushNotificationsExtension/NotificationService.swift create mode 100644 PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements create mode 100644 PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements.license diff --git a/Prisma.xcodeproj/project.pbxproj b/Prisma.xcodeproj/project.pbxproj index c1d91ed..c0e34fc 100644 --- a/Prisma.xcodeproj/project.pbxproj +++ b/Prisma.xcodeproj/project.pbxproj @@ -59,6 +59,32 @@ 5661552E2AB854C000209B80 /* PackageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5661552D2AB854C000209B80 /* PackageHelper.swift */; }; 5680DD392AB8983D004E6D4A /* PackageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5680DD382AB8983D004E6D4A /* PackageCell.swift */; }; 56F6F2A02AB441930022FE5A /* ContributionsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F6F29F2AB441930022FE5A /* ContributionsList.swift */; }; + 5F5ECC492B9F3B5C00B666BC /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F5ECC482B9F3B5C00B666BC /* NotificationService.swift */; }; + 5F5ECC582B9F3EE900B666BC /* PrismaPushNotificationsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5F5ECC462B9F3B5C00B666BC /* PrismaPushNotificationsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 5F5ECC5E2B9F3F9000B666BC /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC5D2B9F3F9000B666BC /* FirebaseFirestore */; }; + 5F5ECC622B9F53F600B666BC /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC612B9F53F600B666BC /* FirebaseAuth */; }; + 5F5ECC652B9F548300B666BC /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC642B9F548300B666BC /* Spezi */; }; + 5F5ECC672B9F548300B666BC /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC662B9F548300B666BC /* SpeziAccount */; }; + 5F5ECC692B9F548300B666BC /* SpeziContact in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC682B9F548300B666BC /* SpeziContact */; }; + 5F5ECC6B2B9F548300B666BC /* SpeziHealthKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC6A2B9F548300B666BC /* SpeziHealthKit */; }; + 5F5ECC6D2B9F548300B666BC /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC6C2B9F548300B666BC /* SpeziFirebaseAccount */; }; + 5F5ECC6F2B9F548300B666BC /* SpeziFirebaseAccountStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC6E2B9F548300B666BC /* SpeziFirebaseAccountStorage */; }; + 5F5ECC712B9F548300B666BC /* SpeziFirebaseConfiguration in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC702B9F548300B666BC /* SpeziFirebaseConfiguration */; }; + 5F5ECC732B9F548300B666BC /* SpeziFirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC722B9F548300B666BC /* SpeziFirebaseStorage */; }; + 5F5ECC752B9F548300B666BC /* SpeziFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC742B9F548300B666BC /* SpeziFirestore */; }; + 5F5ECC772B9F548300B666BC /* SpeziQuestionnaire in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC762B9F548300B666BC /* SpeziQuestionnaire */; }; + 5F5ECC792B9F548300B666BC /* SpeziLocalStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC782B9F548300B666BC /* SpeziLocalStorage */; }; + 5F5ECC7B2B9F548300B666BC /* SpeziSecureStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC7A2B9F548300B666BC /* SpeziSecureStorage */; }; + 5F5ECC7D2B9F548300B666BC /* SpeziViews in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC7C2B9F548300B666BC /* SpeziViews */; }; + 5F5ECC7F2B9F548300B666BC /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC7E2B9F548300B666BC /* FirebaseFirestoreSwift */; }; + 5F5ECC812B9F548300B666BC /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC802B9F548300B666BC /* FirebaseMessaging */; }; + 5F5ECC832B9F548300B666BC /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC822B9F548300B666BC /* FirebaseStorage */; }; + 5F5ECC852B9F548300B666BC /* SpeziScheduler in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC842B9F548300B666BC /* SpeziScheduler */; }; + 5F5ECC872B9F548300B666BC /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC862B9F548300B666BC /* SpeziOnboarding */; }; + 5F5ECC892B9F548300B666BC /* SpeziMockWebService in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC882B9F548300B666BC /* SpeziMockWebService */; }; + 5F5ECC8B2B9F548300B666BC /* HealthKitOnFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC8A2B9F548300B666BC /* HealthKitOnFHIR */; }; + 5F5ECC8D2B9F548300B666BC /* SwiftPackageList in Frameworks */ = {isa = PBXBuildFile; productRef = 5F5ECC8C2B9F548300B666BC /* SwiftPackageList */; }; + 5FA07FE52B9FF33000A3D38D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */; }; 5FBBD2B62B875DB800B75E9F /* PrismaStandard+PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FBBD2B52B875DB800B75E9F /* PrismaStandard+PushNotifications.swift */; }; 5FECE9562B6C9A5F00C06B13 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FECE9552B6C9A5F00C06B13 /* PushNotifications.swift */; }; 5FECE9592B6CCF0B00C06B13 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 5FECE9582B6CCF0B00C06B13 /* FirebaseMessaging */; }; @@ -93,6 +119,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 5F5ECC592B9F3EE900B666BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 653A2545283387FE005D4D48 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5F5ECC452B9F3B5C00B666BC; + remoteInfo = PrismaPushNotificationsExtension; + }; 653A255E28338800005D4D48 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 653A2545283387FE005D4D48 /* Project object */; @@ -109,6 +142,20 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 5F5ECC5B2B9F3EE900B666BC /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 5F5ECC582B9F3EE900B666BC /* PrismaPushNotificationsExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 27FA298F2A388E9B009CAC45 /* ModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalView.swift; sourceTree = ""; }; 2F1AC9DE2B4E840E00C24973 /* Prisma.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Prisma.docc; sourceTree = ""; }; @@ -143,6 +190,10 @@ 5661552D2AB854C000209B80 /* PackageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageHelper.swift; sourceTree = ""; }; 5680DD382AB8983D004E6D4A /* PackageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageCell.swift; sourceTree = ""; }; 56F6F29F2AB441930022FE5A /* ContributionsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributionsList.swift; sourceTree = ""; }; + 5F5ECC462B9F3B5C00B666BC /* PrismaPushNotificationsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PrismaPushNotificationsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 5F5ECC482B9F3B5C00B666BC /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 5F5ECC4A2B9F3B5C00B666BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5FA07FE72BA0BABF00A3D38D /* PrismaPushNotificationsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PrismaPushNotificationsExtension.entitlements; sourceTree = ""; }; 5FBBD2B52B875DB800B75E9F /* PrismaStandard+PushNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PrismaStandard+PushNotifications.swift"; sourceTree = ""; }; 5FECE9552B6C9A5F00C06B13 /* PushNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotifications.swift; sourceTree = ""; }; 653A254D283387FE005D4D48 /* Prisma.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Prisma.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -176,6 +227,36 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 5F5ECC432B9F3B5C00B666BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F5ECC772B9F548300B666BC /* SpeziQuestionnaire in Frameworks */, + 5F5ECC892B9F548300B666BC /* SpeziMockWebService in Frameworks */, + 5F5ECC672B9F548300B666BC /* SpeziAccount in Frameworks */, + 5F5ECC792B9F548300B666BC /* SpeziLocalStorage in Frameworks */, + 5F5ECC6F2B9F548300B666BC /* SpeziFirebaseAccountStorage in Frameworks */, + 5F5ECC8B2B9F548300B666BC /* HealthKitOnFHIR in Frameworks */, + 5F5ECC6D2B9F548300B666BC /* SpeziFirebaseAccount in Frameworks */, + 5F5ECC7B2B9F548300B666BC /* SpeziSecureStorage in Frameworks */, + 5F5ECC6B2B9F548300B666BC /* SpeziHealthKit in Frameworks */, + 5F5ECC7F2B9F548300B666BC /* FirebaseFirestoreSwift in Frameworks */, + 5F5ECC652B9F548300B666BC /* Spezi in Frameworks */, + 5F5ECC812B9F548300B666BC /* FirebaseMessaging in Frameworks */, + 5F5ECC8D2B9F548300B666BC /* SwiftPackageList in Frameworks */, + 5F5ECC7D2B9F548300B666BC /* SpeziViews in Frameworks */, + 5F5ECC832B9F548300B666BC /* FirebaseStorage in Frameworks */, + 5F5ECC852B9F548300B666BC /* SpeziScheduler in Frameworks */, + 5F5ECC872B9F548300B666BC /* SpeziOnboarding in Frameworks */, + 5F5ECC712B9F548300B666BC /* SpeziFirebaseConfiguration in Frameworks */, + 5F5ECC5E2B9F3F9000B666BC /* FirebaseFirestore in Frameworks */, + 5F5ECC752B9F548300B666BC /* SpeziFirestore in Frameworks */, + 5F5ECC732B9F548300B666BC /* SpeziFirebaseStorage in Frameworks */, + 5F5ECC692B9F548300B666BC /* SpeziContact in Frameworks */, + 5F5ECC622B9F53F600B666BC /* FirebaseAuth in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 653A254A283387FE005D4D48 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -324,6 +405,16 @@ path = Contributions; sourceTree = ""; }; + 5F5ECC472B9F3B5C00B666BC /* PrismaPushNotificationsExtension */ = { + isa = PBXGroup; + children = ( + 5FA07FE72BA0BABF00A3D38D /* PrismaPushNotificationsExtension.entitlements */, + 5F5ECC482B9F3B5C00B666BC /* NotificationService.swift */, + 5F5ECC4A2B9F3B5C00B666BC /* Info.plist */, + ); + path = PrismaPushNotificationsExtension; + sourceTree = ""; + }; 5FECE9542B6C9A3E00C06B13 /* PushNotifications */ = { isa = PBXGroup; children = ( @@ -339,6 +430,7 @@ 653A254F283387FE005D4D48 /* Prisma */, 653A256028338800005D4D48 /* PrismaTests */, 653A256A28338800005D4D48 /* PrismaUITests */, + 5F5ECC472B9F3B5C00B666BC /* PrismaPushNotificationsExtension */, 653A254E283387FE005D4D48 /* Products */, 653A258B283395A7005D4D48 /* Frameworks */, ); @@ -350,6 +442,7 @@ 653A254D283387FE005D4D48 /* Prisma.app */, 653A255D28338800005D4D48 /* PrismaTests.xctest */, 653A256728338800005D4D48 /* PrismaUITests.xctest */, + 5F5ECC462B9F3B5C00B666BC /* PrismaPushNotificationsExtension.appex */, ); name = Products; sourceTree = ""; @@ -458,6 +551,48 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 5F5ECC452B9F3B5C00B666BC /* PrismaPushNotificationsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5F5ECC522B9F3B5C00B666BC /* Build configuration list for PBXNativeTarget "PrismaPushNotificationsExtension" */; + buildPhases = ( + 5F5ECC422B9F3B5C00B666BC /* Sources */, + 5F5ECC432B9F3B5C00B666BC /* Frameworks */, + 5F5ECC442B9F3B5C00B666BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PrismaPushNotificationsExtension; + packageProductDependencies = ( + 5F5ECC5D2B9F3F9000B666BC /* FirebaseFirestore */, + 5F5ECC612B9F53F600B666BC /* FirebaseAuth */, + 5F5ECC642B9F548300B666BC /* Spezi */, + 5F5ECC662B9F548300B666BC /* SpeziAccount */, + 5F5ECC682B9F548300B666BC /* SpeziContact */, + 5F5ECC6A2B9F548300B666BC /* SpeziHealthKit */, + 5F5ECC6C2B9F548300B666BC /* SpeziFirebaseAccount */, + 5F5ECC6E2B9F548300B666BC /* SpeziFirebaseAccountStorage */, + 5F5ECC702B9F548300B666BC /* SpeziFirebaseConfiguration */, + 5F5ECC722B9F548300B666BC /* SpeziFirebaseStorage */, + 5F5ECC742B9F548300B666BC /* SpeziFirestore */, + 5F5ECC762B9F548300B666BC /* SpeziQuestionnaire */, + 5F5ECC782B9F548300B666BC /* SpeziLocalStorage */, + 5F5ECC7A2B9F548300B666BC /* SpeziSecureStorage */, + 5F5ECC7C2B9F548300B666BC /* SpeziViews */, + 5F5ECC7E2B9F548300B666BC /* FirebaseFirestoreSwift */, + 5F5ECC802B9F548300B666BC /* FirebaseMessaging */, + 5F5ECC822B9F548300B666BC /* FirebaseStorage */, + 5F5ECC842B9F548300B666BC /* SpeziScheduler */, + 5F5ECC862B9F548300B666BC /* SpeziOnboarding */, + 5F5ECC882B9F548300B666BC /* SpeziMockWebService */, + 5F5ECC8A2B9F548300B666BC /* HealthKitOnFHIR */, + 5F5ECC8C2B9F548300B666BC /* SwiftPackageList */, + ); + productName = PrismaPushNotificationsExtension; + productReference = 5F5ECC462B9F3B5C00B666BC /* PrismaPushNotificationsExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 653A254C283387FE005D4D48 /* Prisma */ = { isa = PBXNativeTarget; buildConfigurationList = 653A257128338800005D4D48 /* Build configuration list for PBXNativeTarget "Prisma" */; @@ -466,11 +601,13 @@ 653A254A283387FE005D4D48 /* Frameworks */, 653A254B283387FE005D4D48 /* Resources */, 2F5B528D29BD237B002020B7 /* ShellScript */, + 5F5ECC5B2B9F3EE900B666BC /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( 566155222AB83CF200209B80 /* PBXTargetDependency */, + 5F5ECC5A2B9F3EE900B666BC /* PBXTargetDependency */, ); name = Prisma; packageProductDependencies = ( @@ -550,9 +687,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1340; + LastSwiftUpdateCheck = 1530; LastUpgradeCheck = 1520; TargetAttributes = { + 5F5ECC452B9F3B5C00B666BC = { + CreatedOnToolsVersion = 15.3; + }; 653A254C283387FE005D4D48 = { CreatedOnToolsVersion = 13.4; }; @@ -600,11 +740,20 @@ 653A254C283387FE005D4D48 /* Prisma */, 653A255C28338800005D4D48 /* PrismaTests */, 653A256628338800005D4D48 /* PrismaUITests */, + 5F5ECC452B9F3B5C00B666BC /* PrismaPushNotificationsExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 5F5ECC442B9F3B5C00B666BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5FA07FE52B9FF33000A3D38D /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 653A254B283387FE005D4D48 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -659,6 +808,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 5F5ECC422B9F3B5C00B666BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F5ECC492B9F3B5C00B666BC /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 653A2549283387FE005D4D48 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -735,6 +892,11 @@ isa = PBXTargetDependency; productRef = 566155212AB83CF200209B80 /* SwiftPackageListJSONPlugin */; }; + 5F5ECC5A2B9F3EE900B666BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5F5ECC452B9F3B5C00B666BC /* PrismaPushNotificationsExtension */; + targetProxy = 5F5ECC592B9F3EE900B666BC /* PBXContainerItemProxy */; + }; 653A255F28338800005D4D48 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 653A254C283387FE005D4D48 /* Prisma */; @@ -897,6 +1059,101 @@ }; name = Test; }; + 5F5ECC4F2B9F3B5C00B666BC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PrismaPushNotificationsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PrismaPushNotificationsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.cs342.2024.behavior.PrismaPushNotificationsExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5F5ECC502B9F3B5C00B666BC /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PrismaPushNotificationsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PrismaPushNotificationsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.cs342.2024.behavior.PrismaPushNotificationsExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Test; + }; + 5F5ECC512B9F3B5C00B666BC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PrismaPushNotificationsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PrismaPushNotificationsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.cs342.2024.behavior.PrismaPushNotificationsExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 653A256F28338800005D4D48 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1027,7 +1284,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Prisma/Supporting Files/Info.plist"; @@ -1195,6 +1452,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 5F5ECC522B9F3B5C00B666BC /* Build configuration list for PBXNativeTarget "PrismaPushNotificationsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5F5ECC4F2B9F3B5C00B666BC /* Debug */, + 5F5ECC502B9F3B5C00B666BC /* Test */, + 5F5ECC512B9F3B5C00B666BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 653A2548283387FE005D4D48 /* Build configuration list for PBXProject "Prisma" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1483,6 +1750,121 @@ package = 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */; productName = "plugin:SwiftPackageListJSONPlugin"; }; + 5F5ECC5D2B9F3F9000B666BC /* FirebaseFirestore */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseFirestore; + }; + 5F5ECC612B9F53F600B666BC /* FirebaseAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAuth; + }; + 5F5ECC642B9F548300B666BC /* Spezi */ = { + isa = XCSwiftPackageProductDependency; + package = 2F49B7742980407B00BCB272 /* XCRemoteSwiftPackageReference "Spezi" */; + productName = Spezi; + }; + 5F5ECC662B9F548300B666BC /* SpeziAccount */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC6229EDD883004B9AB4 /* XCRemoteSwiftPackageReference "SpeziAccount" */; + productName = SpeziAccount; + }; + 5F5ECC682B9F548300B666BC /* SpeziContact */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */; + productName = SpeziContact; + }; + 5F5ECC6A2B9F548300B666BC /* SpeziHealthKit */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC7029EDD8D3004B9AB4 /* XCRemoteSwiftPackageReference "SpeziHealthKit" */; + productName = SpeziHealthKit; + }; + 5F5ECC6C2B9F548300B666BC /* SpeziFirebaseAccount */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirebaseAccount; + }; + 5F5ECC6E2B9F548300B666BC /* SpeziFirebaseAccountStorage */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirebaseAccountStorage; + }; + 5F5ECC702B9F548300B666BC /* SpeziFirebaseConfiguration */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirebaseConfiguration; + }; + 5F5ECC722B9F548300B666BC /* SpeziFirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirebaseStorage; + }; + 5F5ECC742B9F548300B666BC /* SpeziFirestore */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirestore; + }; + 5F5ECC762B9F548300B666BC /* SpeziQuestionnaire */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */; + productName = SpeziQuestionnaire; + }; + 5F5ECC782B9F548300B666BC /* SpeziLocalStorage */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC8829EDD972004B9AB4 /* XCRemoteSwiftPackageReference "SpeziStorage" */; + productName = SpeziLocalStorage; + }; + 5F5ECC7A2B9F548300B666BC /* SpeziSecureStorage */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC8829EDD972004B9AB4 /* XCRemoteSwiftPackageReference "SpeziStorage" */; + productName = SpeziSecureStorage; + }; + 5F5ECC7C2B9F548300B666BC /* SpeziViews */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC8D29EDD980004B9AB4 /* XCRemoteSwiftPackageReference "SpeziViews" */; + productName = SpeziViews; + }; + 5F5ECC7E2B9F548300B666BC /* FirebaseFirestoreSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseFirestoreSwift; + }; + 5F5ECC802B9F548300B666BC /* FirebaseMessaging */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseMessaging; + }; + 5F5ECC822B9F548300B666BC /* FirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseStorage; + }; + 5F5ECC842B9F548300B666BC /* SpeziScheduler */ = { + isa = XCSwiftPackageProductDependency; + package = 2F3D4ABA2A4E7C290068FB2F /* XCRemoteSwiftPackageReference "SpeziScheduler" */; + productName = SpeziScheduler; + }; + 5F5ECC862B9F548300B666BC /* SpeziOnboarding */ = { + isa = XCSwiftPackageProductDependency; + package = 97F466E62A76BBEE005DC9B4 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */; + productName = SpeziOnboarding; + }; + 5F5ECC882B9F548300B666BC /* SpeziMockWebService */ = { + isa = XCSwiftPackageProductDependency; + package = 2FE750CA2A87240100723EAE /* XCRemoteSwiftPackageReference "SpeziMockWebService" */; + productName = SpeziMockWebService; + }; + 5F5ECC8A2B9F548300B666BC /* HealthKitOnFHIR */ = { + isa = XCSwiftPackageProductDependency; + package = 2FB099B42A875E2B00B20952 /* XCRemoteSwiftPackageReference "HealthKitOnFHIR" */; + productName = HealthKitOnFHIR; + }; + 5F5ECC8C2B9F548300B666BC /* SwiftPackageList */ = { + isa = XCSwiftPackageProductDependency; + package = 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */; + productName = SwiftPackageList; + }; 5FECE9582B6CCF0B00C06B13 /* FirebaseMessaging */ = { isa = XCSwiftPackageProductDependency; productName = FirebaseMessaging; diff --git a/Prisma/Home.swift b/Prisma/Home.swift index ffda150..cd067e5 100644 --- a/Prisma/Home.swift +++ b/Prisma/Home.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import FirebaseAuth import SpeziAccount import SpeziMockWebService import SwiftUI @@ -26,6 +27,7 @@ struct HomeView: View { @AppStorage(StorageKeys.homeTabSelection) private var selectedTab = Tabs.schedule + @Environment(PrismaStandard.self) private var standard @State private var presentingAccount = false @@ -66,6 +68,9 @@ struct HomeView: View { AccountSheet() } .verifyRequiredAccountDetails(Self.accountEnabled) + .task { + await standard.authorizeAccessGroupForCurrentUser() + } } } diff --git a/Prisma/Onboarding/AccountOnboarding.swift b/Prisma/Onboarding/AccountOnboarding.swift index 731acea..71fcab7 100644 --- a/Prisma/Onboarding/AccountOnboarding.swift +++ b/Prisma/Onboarding/AccountOnboarding.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import FirebaseAuth import SpeziAccount import SpeziOnboarding import SwiftUI @@ -20,6 +21,7 @@ struct AccountOnboarding: View { var body: some View { AccountSetup { _ in Task { + await standard.authorizeAccessGroupForCurrentUser() // Placing the nextStep() call inside this task will ensure that the sheet dismiss animation is // played till the end before we navigate to the next step. await standard.setAccountTimestamp() @@ -44,11 +46,11 @@ struct AccountOnboarding: View { OnboardingStack { AccountOnboarding() } - .previewWith { - AccountConfiguration { - MockUserIdPasswordAccountService() - } + .previewWith { + AccountConfiguration { + MockUserIdPasswordAccountService() } + } } #Preview("Account Onboarding") { @@ -59,8 +61,8 @@ struct AccountOnboarding: View { return OnboardingStack { AccountOnboarding() } - .previewWith { - AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) - } + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } #endif diff --git a/Prisma/PushNotifications/PushNotifications.swift b/Prisma/PushNotifications/PushNotifications.swift index bccf51e..1193b09 100644 --- a/Prisma/PushNotifications/PushNotifications.swift +++ b/Prisma/PushNotifications/PushNotifications.swift @@ -26,7 +26,6 @@ class PrismaPushNotifications: NSObject, Module, NotificationHandler, Notificati @StandardActor var standard: PrismaStandard @Dependency private var configureFirebaseApp: ConfigureFirebaseApp - override init() {} @@ -48,7 +47,6 @@ class PrismaPushNotifications: NSObject, Module, NotificationHandler, Notificati func handleNotificationAction(_ response: UNNotificationResponse) async { // right now the default action is when a user taps on the notification. functionality can be expanded in the future. - _ = response.actionIdentifier if let sentTimestamp = response.notification.request.content.userInfo["sent_timestamp"] as? String { let openedTimestamp = Date().toISOFormat(timezone: TimeZone(abbreviation: "UTC")) await standard.addNotificationOpenedTimestamp(timeSent: sentTimestamp, timeOpened: openedTimestamp) @@ -71,7 +69,14 @@ class PrismaPushNotifications: NSObject, Module, NotificationHandler, Notificati } func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) async -> BackgroundFetchResult { - print("bg") + let receivedTimestamp = Date().toISOFormat(timezone: TimeZone(abbreviation: "UTC")) + if let sentTimestamp = remoteNotification["sent_timestamp"] as? String { + Task { + await standard.addNotificationReceivedTimestamp(timeSent: sentTimestamp, timeReceived: receivedTimestamp) + } + } else { + print("Sent timestamp is not a string or is nil") + } return .noData } diff --git a/Prisma/SharedContext/FeatureFlags.swift b/Prisma/SharedContext/FeatureFlags.swift index d0f118a..60558f9 100644 --- a/Prisma/SharedContext/FeatureFlags.swift +++ b/Prisma/SharedContext/FeatureFlags.swift @@ -14,13 +14,13 @@ enum FeatureFlags { static let showOnboarding = CommandLine.arguments.contains("--showOnboarding") /// Disables the Firebase interactions, including the login/sign-up step and the Firebase Firestore upload. static let disableFirebase = CommandLine.arguments.contains("--disableFirebase") - // if targetEnvironment(simulator) - // Defines if the application should connect to the local firebase emulator. Always set to true when using the iOS simulator. - // static let useFirebaseEmulator = true - // else +#if targetEnvironment(simulator) + /// Defines if the application should connect to the local firebase emulator. Always set to true when using the iOS simulator. + static let useFirebaseEmulator = true + #else /// Defines if the application should connect to the local firebase emulator. Always set to true when using the iOS simulator. static let useFirebaseEmulator = CommandLine.arguments.contains("--useFirebaseEmulator") - // endif + #endif /// Adds a test task to the schedule at the current time static let testSchedule = CommandLine.arguments.contains("--testSchedule") } diff --git a/Prisma/Standard/PrismaStandard+PushNotifications.swift b/Prisma/Standard/PrismaStandard+PushNotifications.swift index bed0526..afd5c6a 100644 --- a/Prisma/Standard/PrismaStandard+PushNotifications.swift +++ b/Prisma/Standard/PrismaStandard+PushNotifications.swift @@ -28,7 +28,7 @@ extension PrismaStandard { // try push to Firestore. do { - try await Firestore.firestore().document(path).setData(["received": timeReceived]) + try await Firestore.firestore().document(path).setData(["received": timeReceived], merge: true) } catch { print("Failed to set data in Firestore: \(error.localizedDescription)") } @@ -36,7 +36,10 @@ extension PrismaStandard { /// Stores the timestamp when a notification was opened by - /// the user to the specific notification document. + /// the user's device to the specific notification document. + /// + /// - Parameter timeSent: The time which the notification was sent, used for the path in Firestore. + /// - Parameter timeOpened: The time which the notification was opened, generated when the user opens the notification. func addNotificationOpenedTimestamp(timeSent: String, timeOpened: String) async { // path = user_id/notifications/data/logs/YYYY-MM-DDThh:mm:ss.mss let path: String @@ -49,7 +52,7 @@ extension PrismaStandard { // try push to Firestore. do { - try await Firestore.firestore().document(path).setData(["opened": timeOpened]) + try await Firestore.firestore().document(path).setData(["opened": timeOpened], merge: true) } catch { print("Failed to set data in Firestore: \(error.localizedDescription)") } diff --git a/Prisma/Standard/PrismaStandard.swift b/Prisma/Standard/PrismaStandard.swift index 4328b1e..c11f05f 100644 --- a/Prisma/Standard/PrismaStandard.swift +++ b/Prisma/Standard/PrismaStandard.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import FirebaseAuth import FirebaseFirestore import FirebaseMessaging import FirebaseStorage @@ -221,4 +222,36 @@ actor PrismaStandard: Standard, EnvironmentAccessible, HealthKitConstraint, Onbo } try await accountStorage.delete(identifier) } + + /// Authorizes access to the Prisma keychain access group for the currently signed-in user. + /// + /// If the current user is signed in, this function authorizes their access to the Prisma notifications keychain access group identifier. + /// If the user is not signed in, or if an error occurs during the authorization process, appropriate error handling is performed, and the user may be logged out. + /// + /// - Parameters: + /// - user: The current user object. + /// - accessGroup: The identifier of the access group to authorize. + /// + /// - Throws: An error if an issue occurs during the authorization process. + func authorizeAccessGroupForCurrentUser() async { + guard let user = Auth.auth().currentUser else { + print("No signed in user.") + return + } + let accessGroup = "637867499T.edu.stanford.cs342.2024.behavior" + + guard (try? Auth.auth().getStoredUser(forAccessGroup: accessGroup)) == nil else { + print("Access group already shared ...") + return + } + + do { + try Auth.auth().useUserAccessGroup(accessGroup) + try await Auth.auth().updateCurrentUser(user) + } catch let error as NSError { + print("Error changing user access group: %@", error) + // log out the user if fails + try? Auth.auth().signOut() + } + } } diff --git a/Prisma/Supporting Files/Prisma.entitlements b/Prisma/Supporting Files/Prisma.entitlements index f7ac7db..8043ec3 100644 --- a/Prisma/Supporting Files/Prisma.entitlements +++ b/Prisma/Supporting Files/Prisma.entitlements @@ -14,5 +14,9 @@ com.apple.developer.healthkit.background-delivery + keychain-access-groups + + 637867499T.edu.stanford.cs342.2024.behavior + diff --git a/PrismaPushNotificationsExtension/Info.plist b/PrismaPushNotificationsExtension/Info.plist new file mode 100644 index 0000000..57421eb --- /dev/null +++ b/PrismaPushNotificationsExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/PrismaPushNotificationsExtension/Info.plist.license b/PrismaPushNotificationsExtension/Info.plist.license new file mode 100644 index 0000000..79fa51c --- /dev/null +++ b/PrismaPushNotificationsExtension/Info.plist.license @@ -0,0 +1,6 @@ + +This source file is part of the Stanford Prisma Application based on the Stanford Spezi Template Application project + +SPDX-FileCopyrightText: 2023 Stanford University + +SPDX-License-Identifier: MIT diff --git a/PrismaPushNotificationsExtension/NotificationService.swift b/PrismaPushNotificationsExtension/NotificationService.swift new file mode 100644 index 0000000..67b7016 --- /dev/null +++ b/PrismaPushNotificationsExtension/NotificationService.swift @@ -0,0 +1,66 @@ +// +// This source file is part of the Stanford Prisma Application based on the Stanford Spezi Template Application project. +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// +// This file implements an extension to the Notification Service class, which is used to upload timestamps to +// Firestore on receival of background notifications. +// +// Created by Bryant Jimenez on 2/1/24. +// + +import Firebase +import FirebaseAuth +import FirebaseCore +import FirebaseFirestore +import UserNotifications + +class NotificationService: UNNotificationServiceExtension { + var contentHandler: ((UNNotificationContent) -> Void)? + var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + FirebaseApp.configure() + + let accessGroup = "637867499T.edu.stanford.cs342.2024.behavior" + do { + try Auth.auth().useUserAccessGroup(accessGroup) + } catch let error as NSError { + print("Error changing user access group: %@", error) + } + + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + if let bestAttemptContent = bestAttemptContent { + let path = request.content.userInfo["logs_path"] as? String ?? "" + let receivedTimestamp = Date().toISOFormat(timezone: TimeZone(abbreviation: "UTC")) + Firestore.firestore().document(path).setData(["received": receivedTimestamp], merge: true) + contentHandler(bestAttemptContent) + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } +} + +extension Date { + /// converts Date object to ISO Format string. Can optionally pass in a time zone to convert it to. + /// If no timezone is passed, it converts the Date object using the local time zone. + func toISOFormat(timezone: TimeZone? = nil) -> String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withFullDate, .withTime, .withColonSeparatorInTime, .withFractionalSeconds] + if let timezone = timezone { + formatter.timeZone = timezone + } else { + formatter.timeZone = TimeZone.current + } + return formatter.string(from: self) + } +} diff --git a/PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements b/PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements new file mode 100644 index 0000000..24e9fe2 --- /dev/null +++ b/PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + 637867499T.edu.stanford.cs342.2024.behavior + + + diff --git a/PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements.license b/PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements.license new file mode 100644 index 0000000..79fa51c --- /dev/null +++ b/PrismaPushNotificationsExtension/PrismaPushNotificationsExtension.entitlements.license @@ -0,0 +1,6 @@ + +This source file is part of the Stanford Prisma Application based on the Stanford Spezi Template Application project + +SPDX-FileCopyrightText: 2023 Stanford University + +SPDX-License-Identifier: MIT