From b9db5c370d577e0f69529bce9d1da24bd00b2f33 Mon Sep 17 00:00:00 2001 From: bryant-jimenez Date: Thu, 22 Feb 2024 04:06:00 -0800 Subject: [PATCH 01/15] Client handlers for notification received timestamp Handlers for receiving a notification in the background or foreground, call methods in the PrismaStandard for setting the time received in Firestore. --- Prisma.xcodeproj/project.pbxproj | 8 +++- .../PushNotifications/PushNotifications.swift | 20 +++++++++ Prisma/Standard/PrismaModule.swift | 6 ++- .../Standard/PrismaStandard+Extension.swift | 2 +- .../PrismaStandard+PushNotifications.swift | 43 +++++++++++++++++++ Prisma/Standard/PrismaStandard.swift | 19 +++++--- 6 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 Prisma/Standard/PrismaStandard+PushNotifications.swift diff --git a/Prisma.xcodeproj/project.pbxproj b/Prisma.xcodeproj/project.pbxproj index 68aa227..324fe3a 100644 --- a/Prisma.xcodeproj/project.pbxproj +++ b/Prisma.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 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 */; }; + 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 */; }; 653A2551283387FE005D4D48 /* Prisma.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* Prisma.swift */; }; @@ -72,9 +73,9 @@ A9D83F962B083794000D0C78 /* SpeziFirebaseAccountStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A9D83F952B083794000D0C78 /* SpeziFirebaseAccountStorage */; }; A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; }; A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */; }; - E4C766262B72D50500C1DEDA /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C766252B72D50500C1DEDA /* WebView.swift */; }; AC69903E2B6C5A2F00D92970 /* PrivacyControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903D2B6C5A2F00D92970 /* PrivacyControls.swift */; }; AC6990402B6C627100D92970 /* ToggleTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903F2B6C627100D92970 /* ToggleTestView.swift */; }; + E4C766262B72D50500C1DEDA /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C766252B72D50500C1DEDA /* WebView.swift */; }; F8AF6F9A2B5F2B1A0011C32D /* AppIcon-NoBG.png in Resources */ = {isa = PBXBuildFile; fileRef = F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */; }; F8AF6F9F2B5F35400011C32D /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF6F9E2B5F35400011C32D /* ChatView.swift */; }; F8AF6FA52B5F3AE70011C32D /* EventContextCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF6FA42B5F3AE70011C32D /* EventContextCard.swift */; }; @@ -141,6 +142,7 @@ 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 = ""; }; + 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; }; 653A2550283387FE005D4D48 /* Prisma.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prisma.swift; sourceTree = ""; }; @@ -152,9 +154,9 @@ A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = ""; }; A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = ""; }; - E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; AC69903D2B6C5A2F00D92970 /* PrivacyControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyControls.swift; sourceTree = ""; }; AC69903F2B6C627100D92970 /* ToggleTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleTestView.swift; sourceTree = ""; }; + E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon-NoBG.png"; sourceTree = ""; }; F8AF6F9E2B5F35400011C32D /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; F8AF6FA42B5F3AE70011C32D /* EventContextCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventContextCard.swift; sourceTree = ""; }; @@ -438,6 +440,7 @@ isa = PBXGroup; children = ( 2FF53D8C2A8729D600042B76 /* PrismaStandard.swift */, + 5FBBD2B52B875DB800B75E9F /* PrismaStandard+PushNotifications.swift */, F8AF6FB82B5F72650011C32D /* PrismaStandard+HealthKit.swift */, F8AF6FB32B5F6EDC0011C32D /* PrismaModule.swift */, F8AF6FB52B5F71460011C32D /* PrismaStandard+Extension.swift */, @@ -657,6 +660,7 @@ 2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */, 2FE5DCB129EE6107004B9AB4 /* AccountOnboarding.swift in Sources */, 2F4FC8D729EE69D300BFFE26 /* MockUpload.swift in Sources */, + 5FBBD2B62B875DB800B75E9F /* PrismaStandard+PushNotifications.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, 2FE5DC3829EDD7CA004B9AB4 /* Features.swift in Sources */, E4C766262B72D50500C1DEDA /* WebView.swift in Sources */, diff --git a/Prisma/PushNotifications/PushNotifications.swift b/Prisma/PushNotifications/PushNotifications.swift index 73fd84c..3e6470b 100644 --- a/Prisma/PushNotifications/PushNotifications.swift +++ b/Prisma/PushNotifications/PushNotifications.swift @@ -78,4 +78,24 @@ class PrismaPushNotifications: NSObject, Module, LifecycleHandler, MessagingDele } } } + + + /// This function processes incoming remote notifications for the Prisma app. + /// The system calls this method when Prisma is running either in the foreground or background. When a + /// remote notification is received, we write a timestamp to the notification document in Firestore indicating that + /// the notification was received. + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { + print(userInfo) + // get current time + let currentTime = Date().localISOFormat() + Task { + await standard.addNotificationReceivedTimestamp(timestamp: currentTime) + } + + // In the future, if different actions desired to be completed in the background based on notification data received, + // handle that functionality and return any of .newData, .noData, .failed. For now, no new data retrieved + // from the background fetch. + return .noData + } + } diff --git a/Prisma/Standard/PrismaModule.swift b/Prisma/Standard/PrismaModule.swift index e88848c..dbdf9ce 100644 --- a/Prisma/Standard/PrismaModule.swift +++ b/Prisma/Standard/PrismaModule.swift @@ -9,12 +9,14 @@ import HealthKit -/// A `Module` is a type of data that can be uploaded to the Firestore. +/// A `PrismaModule` is a type of data that can be uploaded to the Firestore database. enum PrismaModule { /// The questionnaire type with the `String` id. case questionnaire(String) /// The health type with the `HKQuantityTypeIdentifier` as a String. case health(String) + /// The notification type with the timestamp as a `String`. + case notifications(String) /// The `String` description of the module. var description: String { @@ -23,6 +25,8 @@ enum PrismaModule { return "questionnaire" case .health: return "health" + case .notifications: + return "notifications" } } } diff --git a/Prisma/Standard/PrismaStandard+Extension.swift b/Prisma/Standard/PrismaStandard+Extension.swift index 03966c8..e50acbb 100644 --- a/Prisma/Standard/PrismaStandard+Extension.swift +++ b/Prisma/Standard/PrismaStandard+Extension.swift @@ -23,7 +23,7 @@ extension String { } extension Date { - /// converts Date obejct to local time. + /// converts Date object to local time. func localISOFormat() -> String { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate, .withTime, .withColonSeparatorInTime, .withFractionalSeconds] diff --git a/Prisma/Standard/PrismaStandard+PushNotifications.swift b/Prisma/Standard/PrismaStandard+PushNotifications.swift new file mode 100644 index 0000000..58744d7 --- /dev/null +++ b/Prisma/Standard/PrismaStandard+PushNotifications.swift @@ -0,0 +1,43 @@ +// +// 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 +// +// Created by Bryant Jimenez on 2/22/24. +// + +import FirebaseFirestore +import Foundation + +extension PrismaStandard { + /// Stores the timestamp when a notification was received by + /// the user's device to the specific notification document. + /// + /// - Parameter timestamp: The time which the notification was received by the device + func addNotificationReceivedTimestamp(timestamp: String) async { + // path = user_id/notifications/YYYY-MM-DDThh:mm:ss.mss + let path: String + do { + path = try await getPath(module: .notifications(timestamp)) + } catch { + print("Failed to define path: \(error.localizedDescription)") + return + } + + // try push to Firestore. + do { + try await Firestore.firestore().document(path).setData(["received": timestamp]) + } catch { + print("Failed to set data in Firestore: \(error.localizedDescription)") + } + } + + + /// Stores the timestamp when a notification was opened by + /// the user to the specific notification document. + func addNotificationOpenedTimestamp() { + + } +} diff --git a/Prisma/Standard/PrismaStandard.swift b/Prisma/Standard/PrismaStandard.swift index 0f593db..f2c66be 100644 --- a/Prisma/Standard/PrismaStandard.swift +++ b/Prisma/Standard/PrismaStandard.swift @@ -86,12 +86,15 @@ actor PrismaStandard: Standard, EnvironmentAccessible, HealthKitConstraint, Onbo var moduleText: String switch module { - case .questionnaire(let type): - // Questionnaire responses - moduleText = "\(module.description)/\(type)" - case .health(let type): - // HealthKit observations - moduleText = "\(module.description)/\(type.healthKitDescription)" + case .questionnaire(let type): + // Questionnaire responses + moduleText = "\(module.description)/\(type)" + case .health(let type): + // HealthKit observations + moduleText = "\(module.description)/\(type.healthKitDescription)" + case .notifications(let timestamp): + // notifications for user + moduleText = "\(module.description)/\(timestamp)" } // studies/STUDY_ID/users/USER_ID/MODULE_NAME/SUB_TYPE/... @@ -107,6 +110,10 @@ actor PrismaStandard: Standard, EnvironmentAccessible, HealthKitConstraint, Onbo } } + + /// Stores the user device APNs Token in the user's document directory. + /// + /// - Parameter token: The specific device token to be stored as a `String`. func storeToken(token: String?) async { struct FirebaseDocumentTokenData: Codable { let apnsToken: String? From 820dd0c9f24cb28d10bcb494ea8d5f9f5e875d8f Mon Sep 17 00:00:00 2001 From: EvelynBunnyDev Date: Wed, 28 Feb 2024 23:03:47 -0800 Subject: [PATCH 02/15] update ChatView to incorporate JWT token auth --- Prisma/Chat/ChatView.swift | 59 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index 34e0165..25cfdbf 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI import WebKit - +import Firebase struct ChatView: View { @Binding var presentingAccount: Bool @@ -17,7 +17,7 @@ struct ChatView: View { var body: some View { NavigationStack { GeometryReader { geometry in - if let url = URL(string: "http://localhost:3000") { + if let url = URL(string: "http://localhost:3000?token=\(generateJWT() ?? "")") { WebView(url: url) .navigationTitle("Chat") .frame( @@ -29,6 +29,9 @@ struct ChatView: View { } } } + .onAppear { + signInWithFirebase() + } } init(presentingAccount: Binding) { @@ -36,6 +39,58 @@ struct ChatView: View { } } +func generateJWT(completion: @escaping (String?) -> Void ) { + if let currentUser = Auth.auth().currentUser { + currentUser.getIDTokenForcingRefresh(true) { token, error in + if let error = error { + print("Error getting ID token: \(error.localizedDescription)") + completion(nil) + } else if let token = token { + print("JWT is: \(token)") + sendTokenToBackend(token: token, completion: completion) + } else { + print("No token received.") + completion(nil) + } + } + } else { + print("No current user") + completion(nil) + } +} + +func sendTokenToBackend(token: String, completion: @escaping (String?) -> Void) { + let url = URL(string: "https://your-backend-url.com/api/endpoint")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + // Handle response from backend + if let data = data, let responseString = String(data: data, encoding: .utf8) { + print("Response from backend: \(responseString)") + completion(responseString) + } else if let error = error { + print("Error sending token to backend: \(error.localizedDescription)") + completion(nil) + } else { + print("No response received from backend") + completion(nil) + } + } + task.resume() +} + +func signInWithFirebase() { + Auth.auth().signInAnonymously { (authResult, error) in + if let error = error { + print("Error signing in anonymously.") + return + } + } +} + + #if DEBUG struct ChatView_Previews: PreviewProvider { From 24b479e211a0544d25968eec0a70ef894305e3a1 Mon Sep 17 00:00:00 2001 From: EvelynBunnyDev Date: Wed, 28 Feb 2024 23:53:45 -0800 Subject: [PATCH 03/15] Update ChatView.swift --- Prisma/Chat/ChatView.swift | 93 +++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index 25cfdbf..adba09f 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -13,58 +13,79 @@ import Firebase struct ChatView: View { @Binding var presentingAccount: Bool - + @State private var token: String? = nil + var body: some View { NavigationStack { GeometryReader { geometry in - if let url = URL(string: "http://localhost:3000?token=\(generateJWT() ?? "")") { - WebView(url: url) - .navigationTitle("Chat") - .frame( - width: geometry.size.width, - height: geometry.size.height - ) + // Fetch JWT token asynchronously + if let token = token { + if let url = URL(string: "http://localhost:3000?token=\(token)") { + WebView(url: url) + .navigationTitle("Chat") + .frame( + width: geometry.size.width, + height: geometry.size.height + ) + } else { + Text("Invalid URL") + } } else { - Text("Invalid URL") + // Handle case where token is nil + Text("Failed to get JWT token") } } - } - .onAppear { - signInWithFirebase() + .onAppear { + self.generateJWT { token in + self.token = token + } + self.signInWithFirebase() + } } } init(presentingAccount: Binding) { self._presentingAccount = presentingAccount } + } -func generateJWT(completion: @escaping (String?) -> Void ) { - if let currentUser = Auth.auth().currentUser { - currentUser.getIDTokenForcingRefresh(true) { token, error in + +extension ChatView { + func signInWithFirebase() { + Auth.auth().signInAnonymously { (authResult, error) in if let error = error { - print("Error getting ID token: \(error.localizedDescription)") - completion(nil) - } else if let token = token { - print("JWT is: \(token)") - sendTokenToBackend(token: token, completion: completion) - } else { - print("No token received.") - completion(nil) + print("Error signing in anonymously.") + return } } - } else { - print("No current user") - completion(nil) } -} - -func sendTokenToBackend(token: String, completion: @escaping (String?) -> Void) { - let url = URL(string: "https://your-backend-url.com/api/endpoint")! + + func generateJWT(completion: @escaping (String?) -> Void ) { + if let currentUser = Auth.auth().currentUser { + currentUser.getIDTokenForcingRefresh(true) { token, error in + if let error = error { + print("Error getting ID token: \(error.localizedDescription)") + completion(nil) + } else if let token = token { + print("JWT is: \(token)") + sendTokenToBackend(token: token, completion: completion) + } else { + print("No token received.") + completion(nil) + } + } + } else { + print("No current user") + completion(nil) + } + } + + func sendTokenToBackend(token: String, completion: @escaping (String?) -> Void) { + let url = URL(string: "http://localhost:3000")! // Replace with actual website, once created var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - let task = URLSession.shared.dataTask(with: request) { data, response, error in // Handle response from backend if let data = data, let responseString = String(data: data, encoding: .utf8) { @@ -79,18 +100,8 @@ func sendTokenToBackend(token: String, completion: @escaping (String?) -> Void) } } task.resume() -} - -func signInWithFirebase() { - Auth.auth().signInAnonymously { (authResult, error) in - if let error = error { - print("Error signing in anonymously.") - return - } } } - - #if DEBUG struct ChatView_Previews: PreviewProvider { From 9834138b8d72b259ee0fb1373a52ca26d609dd74 Mon Sep 17 00:00:00 2001 From: EvelynBunnyDev Date: Thu, 29 Feb 2024 23:49:28 -0800 Subject: [PATCH 04/15] updates to firebase sign-in --- Prisma/Chat/ChatView.swift | 37 +++++++++---------- Prisma/Resources/Localizable.xcstrings | 7 ++-- .../Standard/PrismaStandard+HealthKit.swift | 4 +- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index adba09f..71eb360 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -6,21 +6,21 @@ // SPDX-License-Identifier: MIT // +import Firebase import Foundation import SwiftUI import WebKit -import Firebase struct ChatView: View { @Binding var presentingAccount: Bool - @State private var token: String? = nil + @State private var token: String? var body: some View { NavigationStack { GeometryReader { geometry in // Fetch JWT token asynchronously if let token = token { - if let url = URL(string: "http://localhost:3000?token=\(token)") { + if let url = URL(string: "http://localhost:3000?token=\(token)") { // this needs to be sent to the frontend WebView(url: url) .navigationTitle("Chat") .frame( @@ -32,14 +32,19 @@ struct ChatView: View { } } else { // Handle case where token is nil - Text("Failed to get JWT token") + // Text("Failed to get JWT token") + ProgressView() } } - .onAppear { + .task { + guard await ((try? self.signInWithFirebase()) != nil) else { + print("Firebase Auth failed") + return + } + self.generateJWT { token in self.token = token } - self.signInWithFirebase() } } } @@ -47,27 +52,21 @@ struct ChatView: View { init(presentingAccount: Binding) { self._presentingAccount = presentingAccount } - } - extension ChatView { - func signInWithFirebase() { - Auth.auth().signInAnonymously { (authResult, error) in - if let error = error { - print("Error signing in anonymously.") - return - } - } + func signInWithFirebase() async throws { + try await Auth.auth().signIn(withCustomToken: token ?? "") } func generateJWT(completion: @escaping (String?) -> Void ) { if let currentUser = Auth.auth().currentUser { - currentUser.getIDTokenForcingRefresh(true) { token, error in + // Generating JWT Token + currentUser.getIDTokenForcingRefresh(true) { (token, error) in if let error = error { print("Error getting ID token: \(error.localizedDescription)") completion(nil) - } else if let token = token { + } else if let token = token { // Setting the JWT token and send it to chat print("JWT is: \(token)") sendTokenToBackend(token: token, completion: completion) } else { @@ -82,11 +81,11 @@ extension ChatView { } func sendTokenToBackend(token: String, completion: @escaping (String?) -> Void) { - let url = URL(string: "http://localhost:3000")! // Replace with actual website, once created + let url = URL(string: "http://localhost:5000")! // Replace with actual website, once created var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - let task = URLSession.shared.dataTask(with: request) { data, response, error in + let task = URLSession.shared.dataTask(with: request) { data, _, error in // Handle response from backend if let data = data, let responseString = String(data: data, encoding: .utf8) { print("Response from backend: \(responseString)") diff --git a/Prisma/Resources/Localizable.xcstrings b/Prisma/Resources/Localizable.xcstrings index a3fa31d..11d236b 100644 --- a/Prisma/Resources/Localizable.xcstrings +++ b/Prisma/Resources/Localizable.xcstrings @@ -75,9 +75,6 @@ } } } - }, - "chat" : { - }, "Chat" : { @@ -314,7 +311,6 @@ } } }, - "Invalid URL" : {}, "Include Active Energy Burned" : { }, @@ -341,6 +337,9 @@ }, "Include Walking Heart Rate Average" : { + }, + "Invalid URL" : { + }, "JAMES_LANDAY_BIO" : { "localizations" : { diff --git a/Prisma/Standard/PrismaStandard+HealthKit.swift b/Prisma/Standard/PrismaStandard+HealthKit.swift index e584294..9622747 100644 --- a/Prisma/Standard/PrismaStandard+HealthKit.swift +++ b/Prisma/Standard/PrismaStandard+HealthKit.swift @@ -39,7 +39,7 @@ extension PrismaStandard { return } - var sampleToToggleNameMapping: [HKQuantityType?: String] = [ + let sampleToToggleNameMapping: [HKQuantityType?: String] = [ HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned): "includeActiveEnergyBurned", HKQuantityType.quantityType(forIdentifier: .stepCount): "includeStepCountUpload", HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning): "includeDistanceWalkingRunning", @@ -50,7 +50,7 @@ extension PrismaStandard { HKQuantityType.quantityType(forIdentifier: .respiratoryRate): "includeRespiratoryRate", HKQuantityType.quantityType(forIdentifier: .walkingHeartRateAverage): "includeWalkingHeartRateAverage" ] - var toggleNameToBoolMapping: [String: Bool] = PrivacyModule().getCurrentToggles() + let toggleNameToBoolMapping: [String: Bool] = PrivacyModule().getCurrentToggles() if let variableName = sampleToToggleNameMapping[quantityType] { let response: Bool = toggleNameToBoolMapping[variableName] ?? false From 3c4aa3b0a5af816c9a1b917af925d95e4b607771 Mon Sep 17 00:00:00 2001 From: EvelynBunnyDev Date: Fri, 1 Mar 2024 13:13:10 -0800 Subject: [PATCH 05/15] Update ChatView.swift --- Prisma/Chat/ChatView.swift | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index 71eb360..42ab71e 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -8,12 +8,15 @@ import Firebase import Foundation +import SpeziAccount import SwiftUI import WebKit struct ChatView: View { @Binding var presentingAccount: Bool @State private var token: String? + + @Environment(Account.self) private var account var body: some View { NavigationStack { @@ -36,14 +39,20 @@ struct ChatView: View { ProgressView() } } - .task { - guard await ((try? self.signInWithFirebase()) != nil) else { - print("Firebase Auth failed") + .onChange(of: account.signedIn) { + guard account.signedIn else { return } - self.generateJWT { token in - self.token = token + Task { + try await self.signInWithFirebase() + } + } + .task { + do { + try await self.signInWithFirebase() + } catch { + print("Firebase Auth failed \(error)") } } } @@ -56,7 +65,7 @@ struct ChatView: View { extension ChatView { func signInWithFirebase() async throws { - try await Auth.auth().signIn(withCustomToken: token ?? "") + token = try await Auth.auth().currentUser?.getIDToken() } func generateJWT(completion: @escaping (String?) -> Void ) { From fde6d3df2b2e41deabbe99157c4143edefa75d26 Mon Sep 17 00:00:00 2001 From: bryant-jimenez Date: Fri, 1 Mar 2024 18:08:23 -0800 Subject: [PATCH 06/15] updat package dependencies --- .../xcshareddata/swiftpm/Package.resolved | 72 +++++++++---------- .../PushNotifications/PushNotifications.swift | 53 ++++++++++---- 2 files changed, 74 insertions(+), 51 deletions(-) diff --git a/Prisma.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Prisma.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c6a59ae..87c237b 100644 --- a/Prisma.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Prisma.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "5746b2d35c91c50581590ed97abe4c06b5037274", - "version" : "10.18.0" + "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", + "version" : "10.18.1" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "c60c958e707c50a9cf8bcb7cfd7d51c566d726c5", - "version" : "10.19.1" + "revision" : "f91c8167141d0279726c6f6d9d4a47c026785cbc", + "version" : "10.21.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "6b332152355c372ace9966d8ee76ed191f97025e", - "version" : "10.17.0" + "revision" : "cb8617fab75d181270a1d8f763f26b15c73e2e1e", + "version" : "10.21.0" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "115f75e43851774934d695449a4836123c3246e1", - "version" : "3.2.0" + "revision" : "76135c9f4e1ac85459d5fec61b6f76ac47ab3a4c", + "version" : "3.3.1" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", - "version" : "2.3.1" + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKit", "state" : { - "revision" : "cf79a15c7d8c436f98937fe93e72e880dd2f73e4", - "version" : "2.2.20" + "revision" : "15f06cf7c1d2d22805b7b939823536bc78ad63a6", + "version" : "2.2.25" } }, { @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKitOnFHIR", "state" : { - "revision" : "7dc09f7acd7fb19673594e0fdd4d72d0869ee006", - "version" : "1.0.0" + "revision" : "300fbc0038df28f53a9b653298931f71aa6f0bb5", + "version" : "1.1.1" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/Spezi", "state" : { - "revision" : "c4bf0e99de40acfdd2baf0fa02769f06a4c3f0eb", - "version" : "1.1.0" + "revision" : "0ced3efbc2af9513c07ac913ad762c773a00a6c8", + "version" : "1.2.1" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "revision" : "714f01ae1e67bf9c1c0e7c07624380f9bea772b7", - "version" : "1.1.0" + "revision" : "a7d289ef3be54de62b25dc92e8f7ff1a0f093906", + "version" : "1.2.1" } }, { @@ -185,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFoundation.git", "state" : { - "revision" : "683c66f922a4cfe0882c4a86a43854f613b48541", - "version" : "1.0.0" + "revision" : "0346857e2f1d6fd4b1d950d271be6c82df97107f", + "version" : "1.0.2" } }, { @@ -194,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziHealthKit.git", "state" : { - "revision" : "d882734a4ed31fce1bffd7b9977e2669080f21de", - "version" : "0.5.0" + "revision" : "b40695ffa4d1c9d58c5a0ee277640c2343fb5516", + "version" : "0.5.1" } }, { @@ -212,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziOnboarding", "state" : { - "revision" : "ae7b18a18453557cd95c7adeb8f75846f48c343c", - "version" : "1.0.0" + "revision" : "91463ae190611bd14ef52b0657e8db3bf53c9ae8", + "version" : "1.1.0" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziQuestionnaire.git", "state" : { - "revision" : "930a4099db1aca9db0b6ed4e77687141c4780052", - "version" : "1.0.0" + "revision" : "f25580e95bfdad02383980dcb94406cf97b08ea8", + "version" : "1.0.2" } }, { @@ -230,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziScheduler.git", "state" : { - "revision" : "adf793cb47dc199f8ae88f5c719f4d3ba06a4c4e", - "version" : "0.8.0" + "revision" : "ba391084109a9a16622b07e9dcefe2ab1552d2a2", + "version" : "0.8.1" } }, { @@ -248,8 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "0137e69d156bf4001a8d6bf5661c9a37b2bbd0aa", - "version" : "1.0.0" + "revision" : "d49f716e4a4d634604bb0dcd6d53df679b6c1358", + "version" : "1.3.0" } }, { @@ -266,8 +266,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", - "version" : "1.0.6" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { @@ -293,8 +293,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTestExtensions.git", "state" : { - "revision" : "388a6d6a5be48eff5d98a2c45e0b50f30ed21dc3", - "version" : "0.4.7" + "revision" : "1fe9b8e76aeb7a132af37bfa0892160c9b662dcc", + "version" : "0.4.10" } }, { @@ -311,8 +311,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTRuntimeAssertions", "state" : { - "revision" : "bb2a287c2544aa846e53670d1ece35e5949567be", - "version" : "1.0.0" + "revision" : "51da3403f128b120705571ce61e0fe190f8889e6", + "version" : "1.0.1" } } ], diff --git a/Prisma/PushNotifications/PushNotifications.swift b/Prisma/PushNotifications/PushNotifications.swift index 3e6470b..2c28e0c 100644 --- a/Prisma/PushNotifications/PushNotifications.swift +++ b/Prisma/PushNotifications/PushNotifications.swift @@ -19,7 +19,30 @@ import SpeziFirebaseConfiguration import SwiftUI -class PrismaPushNotifications: NSObject, Module, LifecycleHandler, MessagingDelegate, UNUserNotificationCenterDelegate, EnvironmentAccessible { +class PrismaPushNotifications: NSObject, Module, NotificationHandler, MessagingDelegate, UNUserNotificationCenterDelegate, EnvironmentAccessible { + func handleNotificationAction(_ response: UNNotificationResponse) async { + <#code#> + } + + func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? { + <#code#> + } + + /// Handle remote notification when the app is running in background. For now upload a timestamp of when received to firestore. + func receiveRemoteNotification(_ remoteNotification: [AnyHashable : Any]) async -> BackgroundFetchResult { + print(remoteNotification) + // get current time + let currentTime = Date().localISOFormat() + Task { + await standard.addNotificationReceivedTimestamp(timestamp: currentTime) + } + + // In the future, if different actions desired to be completed in the background based on notification data received, + // handle that functionality and return any of .newData, .noData, .failed. For now, no new data retrieved + // from the background fetch. + return BackgroundFetchResult.noData + } + @StandardActor var standard: PrismaStandard @Dependency private var configureFirebaseApp: ConfigureFirebaseApp @@ -84,18 +107,18 @@ class PrismaPushNotifications: NSObject, Module, LifecycleHandler, MessagingDele /// The system calls this method when Prisma is running either in the foreground or background. When a /// remote notification is received, we write a timestamp to the notification document in Firestore indicating that /// the notification was received. - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { - print(userInfo) - // get current time - let currentTime = Date().localISOFormat() - Task { - await standard.addNotificationReceivedTimestamp(timestamp: currentTime) - } - - // In the future, if different actions desired to be completed in the background based on notification data received, - // handle that functionality and return any of .newData, .noData, .failed. For now, no new data retrieved - // from the background fetch. - return .noData - } - +// func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { +// print(userInfo) +// // get current time +// let currentTime = Date().localISOFormat() +// Task { +// await standard.addNotificationReceivedTimestamp(timestamp: currentTime) +// } +// +// // In the future, if different actions desired to be completed in the background based on notification data received, +// // handle that functionality and return any of .newData, .noData, .failed. For now, no new data retrieved +// // from the background fetch. +// return .noData +// } + } From a214a6a17fa7ab660de1ef425d4af31f0edac17d Mon Sep 17 00:00:00 2001 From: EvelynBunnyDev Date: Sun, 3 Mar 2024 23:55:55 -0800 Subject: [PATCH 07/15] Update ChatView.swift --- Prisma/Chat/ChatView.swift | 48 ++++---------------------------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index 42ab71e..39c3c61 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -34,8 +34,6 @@ struct ChatView: View { Text("Invalid URL") } } else { - // Handle case where token is nil - // Text("Failed to get JWT token") ProgressView() } } @@ -54,6 +52,10 @@ struct ChatView: View { } catch { print("Firebase Auth failed \(error)") } + guard await ((try? self.signInWithFirebase()) != nil) else { + print("Firebase Auth failed") + return + } } } } @@ -67,48 +69,6 @@ extension ChatView { func signInWithFirebase() async throws { token = try await Auth.auth().currentUser?.getIDToken() } - - func generateJWT(completion: @escaping (String?) -> Void ) { - if let currentUser = Auth.auth().currentUser { - // Generating JWT Token - currentUser.getIDTokenForcingRefresh(true) { (token, error) in - if let error = error { - print("Error getting ID token: \(error.localizedDescription)") - completion(nil) - } else if let token = token { // Setting the JWT token and send it to chat - print("JWT is: \(token)") - sendTokenToBackend(token: token, completion: completion) - } else { - print("No token received.") - completion(nil) - } - } - } else { - print("No current user") - completion(nil) - } - } - - func sendTokenToBackend(token: String, completion: @escaping (String?) -> Void) { - let url = URL(string: "http://localhost:5000")! // Replace with actual website, once created - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - let task = URLSession.shared.dataTask(with: request) { data, _, error in - // Handle response from backend - if let data = data, let responseString = String(data: data, encoding: .utf8) { - print("Response from backend: \(responseString)") - completion(responseString) - } else if let error = error { - print("Error sending token to backend: \(error.localizedDescription)") - completion(nil) - } else { - print("No response received from backend") - completion(nil) - } - } - task.resume() - } } #if DEBUG From a401e6679fa5390f4cc733df5d7e5d3956a43d43 Mon Sep 17 00:00:00 2001 From: bryant-jimenez Date: Tue, 5 Mar 2024 18:24:36 -0800 Subject: [PATCH 08/15] Conforming to Spezi 1.2 Notification handling --- Prisma.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 64 ++++---- .../Onboarding/NotificationPermissions.swift | 2 +- Prisma/PrismaDelegate.swift | 12 +- .../PushNotifications/PushNotifications.swift | 137 +++++++++++------- Prisma/Resources/Localizable.xcstrings | 7 +- Prisma/SharedContext/FeatureFlags.swift | 2 +- .../Standard/PrismaStandard+Extension.swift | 11 +- .../Standard/PrismaStandard+HealthKit.swift | 2 +- .../PrismaStandard+PushNotifications.swift | 2 +- .../PrismaStandard+Questionnaire.swift | 2 +- 11 files changed, 137 insertions(+), 106 deletions(-) diff --git a/Prisma.xcodeproj/project.pbxproj b/Prisma.xcodeproj/project.pbxproj index 324fe3a..60ec7e6 100644 --- a/Prisma.xcodeproj/project.pbxproj +++ b/Prisma.xcodeproj/project.pbxproj @@ -1243,7 +1243,7 @@ repositoryURL = "https://github.com/StanfordSpezi/Spezi"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.1.0; + minimumVersion = 1.2.0; }; }; 2FB099B42A875E2B00B20952 /* XCRemoteSwiftPackageReference "HealthKitOnFHIR" */ = { diff --git a/Prisma.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Prisma.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 87c237b..43bd650 100644 --- a/Prisma.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Prisma.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", - "version" : "10.18.1" + "revision" : "5746b2d35c91c50581590ed97abe4c06b5037274", + "version" : "10.18.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "f91c8167141d0279726c6f6d9d4a47c026785cbc", - "version" : "10.21.0" + "revision" : "c60c958e707c50a9cf8bcb7cfd7d51c566d726c5", + "version" : "10.19.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "cb8617fab75d181270a1d8f763f26b15c73e2e1e", - "version" : "10.21.0" + "revision" : "6b332152355c372ace9966d8ee76ed191f97025e", + "version" : "10.17.0" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "76135c9f4e1ac85459d5fec61b6f76ac47ab3a4c", - "version" : "3.3.1" + "revision" : "115f75e43851774934d695449a4836123c3246e1", + "version" : "3.2.0" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" + "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", + "version" : "2.3.1" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKit", "state" : { - "revision" : "15f06cf7c1d2d22805b7b939823536bc78ad63a6", - "version" : "2.2.25" + "revision" : "cf79a15c7d8c436f98937fe93e72e880dd2f73e4", + "version" : "2.2.20" } }, { @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKitOnFHIR", "state" : { - "revision" : "300fbc0038df28f53a9b653298931f71aa6f0bb5", - "version" : "1.1.1" + "revision" : "7dc09f7acd7fb19673594e0fdd4d72d0869ee006", + "version" : "1.0.0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "revision" : "a7d289ef3be54de62b25dc92e8f7ff1a0f093906", - "version" : "1.2.1" + "revision" : "714f01ae1e67bf9c1c0e7c07624380f9bea772b7", + "version" : "1.1.0" } }, { @@ -185,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFoundation.git", "state" : { - "revision" : "0346857e2f1d6fd4b1d950d271be6c82df97107f", - "version" : "1.0.2" + "revision" : "01af5b91a54f30ddd121258e81aff2ddc2a99ff9", + "version" : "1.0.4" } }, { @@ -194,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziHealthKit.git", "state" : { - "revision" : "b40695ffa4d1c9d58c5a0ee277640c2343fb5516", - "version" : "0.5.1" + "revision" : "d882734a4ed31fce1bffd7b9977e2669080f21de", + "version" : "0.5.0" } }, { @@ -212,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziOnboarding", "state" : { - "revision" : "91463ae190611bd14ef52b0657e8db3bf53c9ae8", - "version" : "1.1.0" + "revision" : "ae7b18a18453557cd95c7adeb8f75846f48c343c", + "version" : "1.0.0" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziQuestionnaire.git", "state" : { - "revision" : "f25580e95bfdad02383980dcb94406cf97b08ea8", - "version" : "1.0.2" + "revision" : "930a4099db1aca9db0b6ed4e77687141c4780052", + "version" : "1.0.0" } }, { @@ -230,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziScheduler.git", "state" : { - "revision" : "ba391084109a9a16622b07e9dcefe2ab1552d2a2", - "version" : "0.8.1" + "revision" : "adf793cb47dc199f8ae88f5c719f4d3ba06a4c4e", + "version" : "0.8.0" } }, { @@ -248,8 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "d49f716e4a4d634604bb0dcd6d53df679b6c1358", - "version" : "1.3.0" + "revision" : "0137e69d156bf4001a8d6bf5661c9a37b2bbd0aa", + "version" : "1.0.0" } }, { @@ -266,8 +266,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", + "version" : "1.0.6" } }, { @@ -293,8 +293,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTestExtensions.git", "state" : { - "revision" : "1fe9b8e76aeb7a132af37bfa0892160c9b662dcc", - "version" : "0.4.10" + "revision" : "388a6d6a5be48eff5d98a2c45e0b50f30ed21dc3", + "version" : "0.4.7" } }, { diff --git a/Prisma/Onboarding/NotificationPermissions.swift b/Prisma/Onboarding/NotificationPermissions.swift index 4ae41e1..217a338 100644 --- a/Prisma/Onboarding/NotificationPermissions.swift +++ b/Prisma/Onboarding/NotificationPermissions.swift @@ -49,7 +49,7 @@ struct NotificationPermissions: View { if ProcessInfo.processInfo.isPreviewSimulator { try await _Concurrency.Task.sleep(for: .seconds(5)) } else { - try await pushNotifications.requestNotificationAuthorization() + try await pushNotifications.handleNotificationsAllowed() } } catch { print("Could not request notification permissions.") diff --git a/Prisma/PrismaDelegate.swift b/Prisma/PrismaDelegate.swift index 1a8d4c5..6dd5134 100644 --- a/Prisma/PrismaDelegate.swift +++ b/Prisma/PrismaDelegate.swift @@ -105,10 +105,10 @@ class PrismaDelegate: SpeziAppDelegate { /// We assign the APNs token received from Apple to the apnsToken property of the /// Messaging class provided by the Firebase SDK. Firebase uses this token to communicate with /// APNs and send notifications to the device. - func application( - _ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - Messaging.messaging().apnsToken = deviceToken - } +// override func application( +// _ application: UIApplication, +// didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data +// ) { +// Messaging.messaging().apnsToken = deviceToken +// } } diff --git a/Prisma/PushNotifications/PushNotifications.swift b/Prisma/PushNotifications/PushNotifications.swift index 2c28e0c..77463a6 100644 --- a/Prisma/PushNotifications/PushNotifications.swift +++ b/Prisma/PushNotifications/PushNotifications.swift @@ -19,37 +19,17 @@ import SpeziFirebaseConfiguration import SwiftUI -class PrismaPushNotifications: NSObject, Module, NotificationHandler, MessagingDelegate, UNUserNotificationCenterDelegate, EnvironmentAccessible { - func handleNotificationAction(_ response: UNNotificationResponse) async { - <#code#> - } - - func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? { - <#code#> - } - - /// Handle remote notification when the app is running in background. For now upload a timestamp of when received to firestore. - func receiveRemoteNotification(_ remoteNotification: [AnyHashable : Any]) async -> BackgroundFetchResult { - print(remoteNotification) - // get current time - let currentTime = Date().localISOFormat() - Task { - await standard.addNotificationReceivedTimestamp(timestamp: currentTime) - } - - // In the future, if different actions desired to be completed in the background based on notification data received, - // handle that functionality and return any of .newData, .noData, .failed. For now, no new data retrieved - // from the background fetch. - return BackgroundFetchResult.noData - } - +class PrismaPushNotifications: NSObject, Module, NotificationHandler, NotificationTokenHandler, MessagingDelegate, + UNUserNotificationCenterDelegate, EnvironmentAccessible { + @Application(\.registerRemoteNotifications) + var registerRemoteNotifications + @StandardActor var standard: PrismaStandard @Dependency private var configureFirebaseApp: ConfigureFirebaseApp @AppStorage(StorageKeys.pushNotificationsAllowed) var pushNotificationsAllowed = false - override init() {} @@ -57,21 +37,69 @@ class PrismaPushNotifications: NSObject, Module, NotificationHandler, MessagingD Messaging.messaging().delegate = self } + /// + func handleNotificationsAllowed() async throws { + let deviceToken = try await registerRemoteNotifications() + print("APNS registration token: \(String(describing: deviceToken))") +// +// let tokenDict: [String: Data] = ["apns_token": deviceToken ?? ""] +// NotificationCenter.default.post( +// name: Notification.Name("FCMToken"), +// object: nil, +// userInfo: tokenDict +// ) + +// await standard.storeToken(token: deviceToken) + + } - /// Prompts the user to allow notifications on their device, storing that result on disk to reference on app startup. - func requestNotificationAuthorization() async throws { - let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] - // prompt the user to allow notifications - if try await UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { - self.pushNotificationsAllowed = true - // Generate apns token, triggers didRegisterForRemoteNotificationsWithDeviceToken() - await UIApplication.shared.registerForRemoteNotifications() - } else { - self.pushNotificationsAllowed = false - } + func receiveUpdatedDeviceToken(_ deviceToken: Data) { + Messaging.messaging().apnsToken = deviceToken + } + + + func receiveRemoteNotification(_ remoteNotification: [AnyHashable : Any]) async -> BackgroundFetchResult { + .noData + } + + func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? { + nil + } + + func handleNotificationAction(_ response: UNNotificationResponse) async { + print("...") } +// /// Handle remote notification when the app is running in background. For now uploads a timestamp of when notification received to firestore. +// func receiveRemoteNotification(_ remoteNotification: [AnyHashable : Any]) async -> BackgroundFetchResult { +// print(remoteNotification) +// // get current time and upload to firestore +// // let currentTime = Date().toISOFormat() +// // Task { +// // await standard.addNotificationReceivedTimestamp(timestamp: currentTime) +// // } +// // +// // // In the future, if different actions desired to be completed in the background based on notification data received, +// // // handle that functionality and return any of .newData, .noData, .failed. For now, no new data retrieved +// // // from the background fetch. +// return BackgroundFetchResult.noData +// } + +// func handleNotificationAction(_ response: UNNotificationResponse) async { +// print("handleNotifAction") +// } +// +// +// func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? { +// return [.badge, .banner, .sound, .list] +// } + + + + + + /// This function listens for token refreshes and updates the specific user token to Firestore. /// This callback is fired at each app startup and whenever a new token is generated. /// @@ -80,26 +108,25 @@ class PrismaPushNotifications: NSObject, Module, NotificationHandler, MessagingD /// - the user uninstalls/reinstall the app /// - the user clears app data. func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { - if pushNotificationsAllowed { - print("Firebase registration token: \(String(describing: fcmToken))") - - let tokenDict: [String: String] = ["apns_token": fcmToken ?? ""] - NotificationCenter.default.post( - name: Notification.Name("FCMToken"), - object: nil, - userInfo: tokenDict - ) - - // Update the token in Firestore: - - // The standard is an actor, which protects against data races and conforms to - // immutable data practice - - // get into new asynchronous context and execute - Task { - await standard.storeToken(token: fcmToken) - } + + print("Firebase registration token: \(String(describing: fcmToken))") + let tokenDict: [String: String] = ["apns_token": fcmToken ?? ""] + NotificationCenter.default.post( + name: Notification.Name("FCMToken"), + object: nil, + userInfo: tokenDict + ) + + // Update the token in Firestore: + + // The standard is an actor, which protects against data races and conforms to + // immutable data practice + + // get into new asynchronous context and execute + Task { + await standard.storeToken(token: fcmToken) } + } diff --git a/Prisma/Resources/Localizable.xcstrings b/Prisma/Resources/Localizable.xcstrings index a3fa31d..11d236b 100644 --- a/Prisma/Resources/Localizable.xcstrings +++ b/Prisma/Resources/Localizable.xcstrings @@ -75,9 +75,6 @@ } } } - }, - "chat" : { - }, "Chat" : { @@ -314,7 +311,6 @@ } } }, - "Invalid URL" : {}, "Include Active Energy Burned" : { }, @@ -341,6 +337,9 @@ }, "Include Walking Heart Rate Average" : { + }, + "Invalid URL" : { + }, "JAMES_LANDAY_BIO" : { "localizations" : { diff --git a/Prisma/SharedContext/FeatureFlags.swift b/Prisma/SharedContext/FeatureFlags.swift index 5239ff0..1295dcf 100644 --- a/Prisma/SharedContext/FeatureFlags.swift +++ b/Prisma/SharedContext/FeatureFlags.swift @@ -16,7 +16,7 @@ enum FeatureFlags { 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 + static let useFirebaseEmulator = false #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") diff --git a/Prisma/Standard/PrismaStandard+Extension.swift b/Prisma/Standard/PrismaStandard+Extension.swift index e50acbb..427ff45 100644 --- a/Prisma/Standard/PrismaStandard+Extension.swift +++ b/Prisma/Standard/PrismaStandard+Extension.swift @@ -23,11 +23,16 @@ extension String { } extension Date { - /// converts Date object to local time. - func localISOFormat() -> String { + /// 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] - formatter.timeZone = TimeZone.current + if let timezone = timezone { + formatter.timeZone = timezone + } else { + formatter.timeZone = TimeZone.current + } return formatter.string(from: self) } diff --git a/Prisma/Standard/PrismaStandard+HealthKit.swift b/Prisma/Standard/PrismaStandard+HealthKit.swift index e584294..773c07c 100644 --- a/Prisma/Standard/PrismaStandard+HealthKit.swift +++ b/Prisma/Standard/PrismaStandard+HealthKit.swift @@ -68,7 +68,7 @@ extension PrismaStandard { let identifier = quantityType.identifier // convert the startDate of the HKSample to local time - let effectiveTimestamp = sample.startDate.localISOFormat() + let effectiveTimestamp = sample.startDate.toISOFormat() // path = HEALTH_KIT_PATH/raw/YYYY-MM-DDThh:mm:ss.mss do { diff --git a/Prisma/Standard/PrismaStandard+PushNotifications.swift b/Prisma/Standard/PrismaStandard+PushNotifications.swift index 58744d7..60c7b58 100644 --- a/Prisma/Standard/PrismaStandard+PushNotifications.swift +++ b/Prisma/Standard/PrismaStandard+PushNotifications.swift @@ -17,7 +17,7 @@ extension PrismaStandard { /// /// - Parameter timestamp: The time which the notification was received by the device func addNotificationReceivedTimestamp(timestamp: String) async { - // path = user_id/notifications/YYYY-MM-DDThh:mm:ss.mss + // path = user_id/notifications/data/logs/YYYY-MM-DDThh:mm:ss.mss let path: String do { path = try await getPath(module: .notifications(timestamp)) diff --git a/Prisma/Standard/PrismaStandard+Questionnaire.swift b/Prisma/Standard/PrismaStandard+Questionnaire.swift index 15e6dee..6480bae 100644 --- a/Prisma/Standard/PrismaStandard+Questionnaire.swift +++ b/Prisma/Standard/PrismaStandard+Questionnaire.swift @@ -24,7 +24,7 @@ extension PrismaStandard { // extracts the first item as that is the id. let rootTag = String("\(tag)".split(separator: "/")[0]) - let effectiveTimestamp = Date().localISOFormat() + let effectiveTimestamp = Date().toISOFormat() let path: String do { From 7cae1b599974beba153c880d57881c03be72fc69 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 5 Mar 2024 18:47:14 -0800 Subject: [PATCH 09/15] Fix Push Notifications --- Prisma.xcodeproj/project.pbxproj | 2 +- .../Onboarding/NotificationPermissions.swift | 2 - Prisma/Onboarding/OnboardingFlow.swift | 5 +- Prisma/PrismaDelegate.swift | 16 --- .../PushNotifications/PushNotifications.swift | 104 ++---------------- Prisma/SharedContext/StorageKeys.swift | 2 - 6 files changed, 10 insertions(+), 121 deletions(-) diff --git a/Prisma.xcodeproj/project.pbxproj b/Prisma.xcodeproj/project.pbxproj index 60ec7e6..8c98e7b 100644 --- a/Prisma.xcodeproj/project.pbxproj +++ b/Prisma.xcodeproj/project.pbxproj @@ -1019,7 +1019,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 637867499T; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Prisma/Supporting Files/Info.plist"; diff --git a/Prisma/Onboarding/NotificationPermissions.swift b/Prisma/Onboarding/NotificationPermissions.swift index 217a338..3240f70 100644 --- a/Prisma/Onboarding/NotificationPermissions.swift +++ b/Prisma/Onboarding/NotificationPermissions.swift @@ -18,8 +18,6 @@ struct NotificationPermissions: View { @State private var notificationProcessing = false - @AppStorage(StorageKeys.pushNotificationsAllowed) var pushNotificationsAllowed = false - var body: some View { OnboardingView( diff --git a/Prisma/Onboarding/OnboardingFlow.swift b/Prisma/Onboarding/OnboardingFlow.swift index 142a2fe..eb43e80 100644 --- a/Prisma/Onboarding/OnboardingFlow.swift +++ b/Prisma/Onboarding/OnboardingFlow.swift @@ -19,7 +19,6 @@ struct OnboardingFlow: View { @Environment(PrismaScheduler.self) private var scheduler @AppStorage(StorageKeys.onboardingFlowComplete) private var completedOnboardingFlow = false - @AppStorage(StorageKeys.pushNotificationsAllowed) var pushNotificationsAllowed = false private var healthKitAuthorization: Bool { @@ -44,9 +43,7 @@ struct OnboardingFlow: View { if HKHealthStore.isHealthDataAvailable() && !healthKitAuthorization { HealthKitPermissions() } - if !pushNotificationsAllowed { - NotificationPermissions() - } + NotificationPermissions() } .interactiveDismissDisabled(!completedOnboardingFlow) } diff --git a/Prisma/PrismaDelegate.swift b/Prisma/PrismaDelegate.swift index 6dd5134..81b90af 100644 --- a/Prisma/PrismaDelegate.swift +++ b/Prisma/PrismaDelegate.swift @@ -95,20 +95,4 @@ class PrismaDelegate: SpeziAppDelegate { ) } } - - - /// When the app successfully registers for remote notifications, it receives a device - /// token from Apple's push notification service (APNs). The deviceToken parameter - /// contains a unique identifier for the device, which the app uses to receive remote - /// notifications. - /// - /// We assign the APNs token received from Apple to the apnsToken property of the - /// Messaging class provided by the Firebase SDK. Firebase uses this token to communicate with - /// APNs and send notifications to the device. -// override func application( -// _ application: UIApplication, -// didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data -// ) { -// Messaging.messaging().apnsToken = deviceToken -// } } diff --git a/Prisma/PushNotifications/PushNotifications.swift b/Prisma/PushNotifications/PushNotifications.swift index 77463a6..40c0a66 100644 --- a/Prisma/PushNotifications/PushNotifications.swift +++ b/Prisma/PushNotifications/PushNotifications.swift @@ -19,16 +19,12 @@ import SpeziFirebaseConfiguration import SwiftUI -class PrismaPushNotifications: NSObject, Module, NotificationHandler, NotificationTokenHandler, MessagingDelegate, +class PrismaPushNotifications: NSObject, Module, NotificationTokenHandler, MessagingDelegate, UNUserNotificationCenterDelegate, EnvironmentAccessible { - @Application(\.registerRemoteNotifications) - var registerRemoteNotifications - + @Application(\.registerRemoteNotifications) var registerRemoteNotifications @StandardActor var standard: PrismaStandard - @Dependency private var configureFirebaseApp: ConfigureFirebaseApp - @AppStorage(StorageKeys.pushNotificationsAllowed) var pushNotificationsAllowed = false override init() {} @@ -37,69 +33,18 @@ class PrismaPushNotifications: NSObject, Module, NotificationHandler, Notificati Messaging.messaging().delegate = self } - /// func handleNotificationsAllowed() async throws { - let deviceToken = try await registerRemoteNotifications() - print("APNS registration token: \(String(describing: deviceToken))") -// -// let tokenDict: [String: Data] = ["apns_token": deviceToken ?? ""] -// NotificationCenter.default.post( -// name: Notification.Name("FCMToken"), -// object: nil, -// userInfo: tokenDict -// ) - -// await standard.storeToken(token: deviceToken) - + let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] + // prompt the user to allow notifications + if try await UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { + try await registerRemoteNotifications() + } } func receiveUpdatedDeviceToken(_ deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken } - - func receiveRemoteNotification(_ remoteNotification: [AnyHashable : Any]) async -> BackgroundFetchResult { - .noData - } - - func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? { - nil - } - - func handleNotificationAction(_ response: UNNotificationResponse) async { - print("...") - } - - -// /// Handle remote notification when the app is running in background. For now uploads a timestamp of when notification received to firestore. -// func receiveRemoteNotification(_ remoteNotification: [AnyHashable : Any]) async -> BackgroundFetchResult { -// print(remoteNotification) -// // get current time and upload to firestore -// // let currentTime = Date().toISOFormat() -// // Task { -// // await standard.addNotificationReceivedTimestamp(timestamp: currentTime) -// // } -// // -// // // In the future, if different actions desired to be completed in the background based on notification data received, -// // // handle that functionality and return any of .newData, .noData, .failed. For now, no new data retrieved -// // // from the background fetch. -// return BackgroundFetchResult.noData -// } - -// func handleNotificationAction(_ response: UNNotificationResponse) async { -// print("handleNotifAction") -// } -// -// -// func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? { -// return [.badge, .banner, .sound, .list] -// } - - - - - - /// This function listens for token refreshes and updates the specific user token to Firestore. /// This callback is fired at each app startup and whenever a new token is generated. /// @@ -108,44 +53,11 @@ class PrismaPushNotifications: NSObject, Module, NotificationHandler, Notificati /// - the user uninstalls/reinstall the app /// - the user clears app data. func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { - - print("Firebase registration token: \(String(describing: fcmToken))") - let tokenDict: [String: String] = ["apns_token": fcmToken ?? ""] - NotificationCenter.default.post( - name: Notification.Name("FCMToken"), - object: nil, - userInfo: tokenDict - ) - // Update the token in Firestore: - // The standard is an actor, which protects against data races and conforms to - // immutable data practice - - // get into new asynchronous context and execute + // immutable data practice get into new asynchronous context and execute Task { await standard.storeToken(token: fcmToken) } - } - - - /// This function processes incoming remote notifications for the Prisma app. - /// The system calls this method when Prisma is running either in the foreground or background. When a - /// remote notification is received, we write a timestamp to the notification document in Firestore indicating that - /// the notification was received. -// func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { -// print(userInfo) -// // get current time -// let currentTime = Date().localISOFormat() -// Task { -// await standard.addNotificationReceivedTimestamp(timestamp: currentTime) -// } -// -// // In the future, if different actions desired to be completed in the background based on notification data received, -// // handle that functionality and return any of .newData, .noData, .failed. For now, no new data retrieved -// // from the background fetch. -// return .noData -// } - } diff --git a/Prisma/SharedContext/StorageKeys.swift b/Prisma/SharedContext/StorageKeys.swift index aa90aa6..dca4566 100644 --- a/Prisma/SharedContext/StorageKeys.swift +++ b/Prisma/SharedContext/StorageKeys.swift @@ -13,8 +13,6 @@ enum StorageKeys { static let onboardingFlowComplete = "onboardingFlow.complete" /// A `Step` flag indicating the current step in the onboarding process. static let onboardingFlowStep = "onboardingFlow.step" - /// A `Bool` flag indicating whether or not push notifications are allowed. - static let pushNotificationsAllowed = "pushNotifications.allowed" // MARK: - Home /// The currently selected home tab. From f4ba1ed38b5dd4e64d54f7c86575bb39c3b464ef Mon Sep 17 00:00:00 2001 From: EvelynBunnyDev Date: Wed, 6 Mar 2024 17:46:41 -0800 Subject: [PATCH 10/15] passing JWT token from iOS to web updating the packages, ChatView adding getFirebaseIDToken, updating the GOogleService plist. --- Prisma.xcodeproj/project.pbxproj | 8 ++--- Prisma/Chat/ChatView.swift | 35 +++++++++---------- Prisma/Onboarding/OnboardingFlow.swift | 3 ++ Prisma/SharedContext/FeatureFlags.swift | 2 +- .../Supporting Files/GoogleService-Info.plist | 34 ------------------ 5 files changed, 25 insertions(+), 57 deletions(-) delete mode 100644 Prisma/Supporting Files/GoogleService-Info.plist diff --git a/Prisma.xcodeproj/project.pbxproj b/Prisma.xcodeproj/project.pbxproj index 8c98e7b..cbfde10 100644 --- a/Prisma.xcodeproj/project.pbxproj +++ b/Prisma.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 2F4E23832989D51F0013F3D9 /* PrismaTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* PrismaTestingSetup.swift */; }; 2F4FC8D729EE69D300BFFE26 /* MockUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4FC8D629EE69D300BFFE26 /* MockUpload.swift */; }; 2F5E32BD297E05EA003432F8 /* PrismaDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5E32BC297E05EA003432F8 /* PrismaDelegate.swift */; }; - 2F6025CB29BBE70F0045459E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */; }; 2FA0BFED2ACC977500E0EF83 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */; }; 2FB099AF2A875DF100B20952 /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099AE2A875DF100B20952 /* FirebaseAuth */; }; 2FB099B12A875DF100B20952 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B02A875DF100B20952 /* FirebaseFirestore */; }; @@ -75,6 +74,7 @@ A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */; }; AC69903E2B6C5A2F00D92970 /* PrivacyControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903D2B6C5A2F00D92970 /* PrivacyControls.swift */; }; AC6990402B6C627100D92970 /* ToggleTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903F2B6C627100D92970 /* ToggleTestView.swift */; }; + E4A114EB2B994F1000B6B5C4 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = E4A114EA2B994F1000B6B5C4 /* GoogleService-Info.plist */; }; E4C766262B72D50500C1DEDA /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C766252B72D50500C1DEDA /* WebView.swift */; }; F8AF6F9A2B5F2B1A0011C32D /* AppIcon-NoBG.png in Resources */ = {isa = PBXBuildFile; fileRef = F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */; }; F8AF6F9F2B5F35400011C32D /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF6F9E2B5F35400011C32D /* ChatView.swift */; }; @@ -114,7 +114,6 @@ 2F4E23822989D51F0013F3D9 /* PrismaTestingSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrismaTestingSetup.swift; sourceTree = ""; }; 2F4FC8D629EE69D300BFFE26 /* MockUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUpload.swift; sourceTree = ""; }; 2F5E32BC297E05EA003432F8 /* PrismaDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrismaDelegate.swift; sourceTree = ""; }; - 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 2FAEC07F297F583900C11C42 /* Prisma.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Prisma.entitlements; sourceTree = ""; }; 2FC94CD4298B0A1D009C8209 /* Prisma.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Prisma.xctestplan; sourceTree = ""; }; @@ -156,6 +155,7 @@ A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = ""; }; AC69903D2B6C5A2F00D92970 /* PrivacyControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyControls.swift; sourceTree = ""; }; AC69903F2B6C627100D92970 /* ToggleTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleTestView.swift; sourceTree = ""; }; + E4A114EA2B994F1000B6B5C4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../../../Downloads/GoogleService-Info.plist"; sourceTree = ""; }; E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon-NoBG.png"; sourceTree = ""; }; F8AF6F9E2B5F35400011C32D /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; @@ -235,7 +235,7 @@ children = ( 2FAEC07F297F583900C11C42 /* Prisma.entitlements */, 653A258928339462005D4D48 /* Info.plist */, - 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */, + E4A114EA2B994F1000B6B5C4 /* GoogleService-Info.plist */, 2F1AC9DE2B4E840E00C24973 /* Prisma.docc */, ); path = "Supporting Files"; @@ -610,7 +610,7 @@ 653A255528338800005D4D48 /* Assets.xcassets in Resources */, F8AF6FAC2B5F42C40011C32D /* afternoon-en-US.json in Resources */, 2FA0BFED2ACC977500E0EF83 /* Localizable.xcstrings in Resources */, - 2F6025CB29BBE70F0045459E /* GoogleService-Info.plist in Resources */, + E4A114EB2B994F1000B6B5C4 /* GoogleService-Info.plist in Resources */, F8AF6FAE2B5F42C40011C32D /* morning-en-US.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index 39c3c61..83334c1 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -15,9 +15,8 @@ import WebKit struct ChatView: View { @Binding var presentingAccount: Bool @State private var token: String? - - @Environment(Account.self) private var account + var body: some View { NavigationStack { GeometryReader { geometry in @@ -37,37 +36,37 @@ struct ChatView: View { ProgressView() } } - .onChange(of: account.signedIn) { - guard account.signedIn else { - return - } - - Task { - try await self.signInWithFirebase() - } - } + /* + .onChange(of: account.signedIn) { + guard account.signedIn else { + return + } + + Task { + try await self.signInWithFirebase() + } + } + */ .task { do { - try await self.signInWithFirebase() + try await self.getFirebaseIDToken() } catch { print("Firebase Auth failed \(error)") } - guard await ((try? self.signInWithFirebase()) != nil) else { - print("Firebase Auth failed") - return - } } } } - + init(presentingAccount: Binding) { self._presentingAccount = presentingAccount } } extension ChatView { - func signInWithFirebase() async throws { + func getFirebaseIDToken() async throws { token = try await Auth.auth().currentUser?.getIDToken() + + 1+1 } } diff --git a/Prisma/Onboarding/OnboardingFlow.swift b/Prisma/Onboarding/OnboardingFlow.swift index eb43e80..e0b2415 100644 --- a/Prisma/Onboarding/OnboardingFlow.swift +++ b/Prisma/Onboarding/OnboardingFlow.swift @@ -43,7 +43,10 @@ struct OnboardingFlow: View { if HKHealthStore.isHealthDataAvailable() && !healthKitAuthorization { HealthKitPermissions() } + + #if !targetEnvironment(simulator) NotificationPermissions() + #endif } .interactiveDismissDisabled(!completedOnboardingFlow) } diff --git a/Prisma/SharedContext/FeatureFlags.swift b/Prisma/SharedContext/FeatureFlags.swift index 1295dcf..5239ff0 100644 --- a/Prisma/SharedContext/FeatureFlags.swift +++ b/Prisma/SharedContext/FeatureFlags.swift @@ -16,7 +16,7 @@ enum FeatureFlags { 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 = false + 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") diff --git a/Prisma/Supporting Files/GoogleService-Info.plist b/Prisma/Supporting Files/GoogleService-Info.plist deleted file mode 100644 index cb638f0..0000000 --- a/Prisma/Supporting Files/GoogleService-Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - CLIENT_ID - CLIENT_ID - REVERSED_CLIENT_ID - REVERSED_CLIENT_ID - API_KEY - API_KEY________________________________ - GCM_SENDER_ID - GCM_SENDER_ID - PLIST_VERSION - 1 - BUNDLE_ID - edu.stanford.cs342.2024.behavior - PROJECT_ID - prism-627d5 - STORAGE_BUCKET - prism-627d5.appspot.com - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:123456789012:ios:1234567890123456789012 - - \ No newline at end of file From 412e7954c0ce1d9233669baf0cf1af7423f65534 Mon Sep 17 00:00:00 2001 From: EvelynBunnyDev Date: Thu, 7 Mar 2024 11:35:59 -0800 Subject: [PATCH 11/15] updating feature flags to not use the emulator --- Prisma/Chat/ChatView.swift | 3 +-- Prisma/SharedContext/FeatureFlags.swift | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index 83334c1..5ccb731 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -65,8 +65,7 @@ struct ChatView: View { extension ChatView { func getFirebaseIDToken() async throws { token = try await Auth.auth().currentUser?.getIDToken() - - 1+1 + print(token) } } diff --git a/Prisma/SharedContext/FeatureFlags.swift b/Prisma/SharedContext/FeatureFlags.swift index 5239ff0..3a2eeb7 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) + //#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 + //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") } From 574345f8fab942bc555783b0186f48926bdbda90 Mon Sep 17 00:00:00 2001 From: EvelynBunnyDev Date: Sun, 10 Mar 2024 20:06:03 -0700 Subject: [PATCH 12/15] enable firebase emulator --- Prisma.xcodeproj/xcshareddata/xcschemes/Prisma.xcscheme | 2 +- Prisma/Chat/ChatView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Prisma.xcodeproj/xcshareddata/xcschemes/Prisma.xcscheme b/Prisma.xcodeproj/xcshareddata/xcschemes/Prisma.xcscheme index e2a133b..7859492 100644 --- a/Prisma.xcodeproj/xcshareddata/xcschemes/Prisma.xcscheme +++ b/Prisma.xcodeproj/xcshareddata/xcschemes/Prisma.xcscheme @@ -95,7 +95,7 @@ + isEnabled = "YES"> diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index 5ccb731..9e99065 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -65,7 +65,7 @@ struct ChatView: View { extension ChatView { func getFirebaseIDToken() async throws { token = try await Auth.auth().currentUser?.getIDToken() - print(token) + print("token is:", token) } } From b4bb5549584be429d1dd88b1963e385984822df0 Mon Sep 17 00:00:00 2001 From: dhruvna1k Date: Mon, 11 Mar 2024 02:02:49 -0700 Subject: [PATCH 13/15] swiftlint fixes --- Prisma/PushNotifications/PushNotifications.swift | 2 +- Prisma/SharedContext/FeatureFlags.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Prisma/PushNotifications/PushNotifications.swift b/Prisma/PushNotifications/PushNotifications.swift index ac6a5e5..bccf51e 100644 --- a/Prisma/PushNotifications/PushNotifications.swift +++ b/Prisma/PushNotifications/PushNotifications.swift @@ -48,7 +48,7 @@ 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. - let actionIdentifier = response.actionIdentifier + _ = 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) diff --git a/Prisma/SharedContext/FeatureFlags.swift b/Prisma/SharedContext/FeatureFlags.swift index 3a2eeb7..d0f118a 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") } From b98e68d21606a7f2909483049d4a125a29ee9efd Mon Sep 17 00:00:00 2001 From: dhruvna1k Date: Mon, 11 Mar 2024 13:15:22 -0700 Subject: [PATCH 14/15] swiftline unwrapping --- Prisma/Chat/ChatView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prisma/Chat/ChatView.swift b/Prisma/Chat/ChatView.swift index 9e99065..54dbaa9 100644 --- a/Prisma/Chat/ChatView.swift +++ b/Prisma/Chat/ChatView.swift @@ -65,7 +65,7 @@ struct ChatView: View { extension ChatView { func getFirebaseIDToken() async throws { token = try await Auth.auth().currentUser?.getIDToken() - print("token is:", token) + print("token is:", token ?? "") } } From ac9e4523cff369bb4c7113019cfa47bba766425b Mon Sep 17 00:00:00 2001 From: dhruvna1k Date: Mon, 11 Mar 2024 13:23:37 -0700 Subject: [PATCH 15/15] fixed google plist adding --- Prisma.xcodeproj/project.pbxproj | 10 +++--- .../Supporting Files/GoogleService-Info.plist | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 Prisma/Supporting Files/GoogleService-Info.plist diff --git a/Prisma.xcodeproj/project.pbxproj b/Prisma.xcodeproj/project.pbxproj index e5508aa..c1d91ed 100644 --- a/Prisma.xcodeproj/project.pbxproj +++ b/Prisma.xcodeproj/project.pbxproj @@ -73,10 +73,11 @@ A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; }; A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */; }; AC69903E2B6C5A2F00D92970 /* PrivacyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */; }; + ACB5DFBA2B9F9E76004F28E6 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = ACB5DFB92B9F9E76004F28E6 /* GoogleService-Info.plist */; }; D8027E912B90655700BB9466 /* ManageDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8027E902B90655700BB9466 /* ManageDataView.swift */; }; D8F136C52B85CEED000BA7AE /* DeleteDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */; }; E4C766262B72D50500C1DEDA /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C766252B72D50500C1DEDA /* WebView.swift */; }; - F83B7CBE2B8FFE6800662914 /* PrismaStandard+TimeIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83B7CBD2B8FFE6800662914 /* PrismaStandard+TimeIndex.swift */; }; + F83B7CBE2B8FFE6800662914 /* PrismaStandard+TimeIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83B7CBD2B8FFE6800662914 /* PrismaStandard+TimeIndex.swift */; }; F8AF6F9A2B5F2B1A0011C32D /* AppIcon-NoBG.png in Resources */ = {isa = PBXBuildFile; fileRef = F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */; }; F8AF6F9F2B5F35400011C32D /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF6F9E2B5F35400011C32D /* ChatView.swift */; }; F8AF6FA52B5F3AE70011C32D /* EventContextCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF6FA42B5F3AE70011C32D /* EventContextCard.swift */; }; @@ -155,10 +156,11 @@ A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = ""; }; A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = ""; }; AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyModule.swift; sourceTree = ""; }; + ACB5DFB92B9F9E76004F28E6 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; D8027E902B90655700BB9466 /* ManageDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageDataView.swift; sourceTree = ""; }; D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteDataView.swift; sourceTree = ""; }; E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; - F83B7CBD2B8FFE6800662914 /* PrismaStandard+TimeIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PrismaStandard+TimeIndex.swift"; sourceTree = ""; }; + F83B7CBD2B8FFE6800662914 /* PrismaStandard+TimeIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PrismaStandard+TimeIndex.swift"; sourceTree = ""; }; F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon-NoBG.png"; sourceTree = ""; }; F8AF6F9E2B5F35400011C32D /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; F8AF6FA42B5F3AE70011C32D /* EventContextCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventContextCard.swift; sourceTree = ""; }; @@ -235,9 +237,9 @@ 2FC9759D2978E30800BA99FE /* Supporting Files */ = { isa = PBXGroup; children = ( + ACB5DFB92B9F9E76004F28E6 /* GoogleService-Info.plist */, 2FAEC07F297F583900C11C42 /* Prisma.entitlements */, 653A258928339462005D4D48 /* Info.plist */, - E4A114EA2B994F1000B6B5C4 /* GoogleService-Info.plist */, 2F1AC9DE2B4E840E00C24973 /* Prisma.docc */, ); path = "Supporting Files"; @@ -609,12 +611,12 @@ files = ( 2FC3439229EE634B002D773C /* ConsentDocument.md in Resources */, F8AF6F9A2B5F2B1A0011C32D /* AppIcon-NoBG.png in Resources */, + ACB5DFBA2B9F9E76004F28E6 /* GoogleService-Info.plist in Resources */, F8AF6FAF2B5F42C40011C32D /* mid-day-en-US.json in Resources */, F8AF6FAD2B5F42C40011C32D /* end-of-day-en-US.json in Resources */, 653A255528338800005D4D48 /* Assets.xcassets in Resources */, F8AF6FAC2B5F42C40011C32D /* afternoon-en-US.json in Resources */, 2FA0BFED2ACC977500E0EF83 /* Localizable.xcstrings in Resources */, - E4A114EB2B994F1000B6B5C4 /* GoogleService-Info.plist in Resources */, F8AF6FAE2B5F42C40011C32D /* morning-en-US.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Prisma/Supporting Files/GoogleService-Info.plist b/Prisma/Supporting Files/GoogleService-Info.plist new file mode 100644 index 0000000..cb638f0 --- /dev/null +++ b/Prisma/Supporting Files/GoogleService-Info.plist @@ -0,0 +1,34 @@ + + + + + CLIENT_ID + CLIENT_ID + REVERSED_CLIENT_ID + REVERSED_CLIENT_ID + API_KEY + API_KEY________________________________ + GCM_SENDER_ID + GCM_SENDER_ID + PLIST_VERSION + 1 + BUNDLE_ID + edu.stanford.cs342.2024.behavior + PROJECT_ID + prism-627d5 + STORAGE_BUCKET + prism-627d5.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:123456789012:ios:1234567890123456789012 + + \ No newline at end of file