Skip to content

Commit

Permalink
Add Export Button
Browse files Browse the repository at this point in the history
  • Loading branch information
PSchmiedmayer committed Sep 26, 2023
1 parent 6ab3df7 commit cfe5c72
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 34 deletions.
8 changes: 8 additions & 0 deletions OwnYourData.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
2F0191F92A9E4CF100E9EB0E /* String+LocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0191F82A9E4CF100E9EB0E /* String+LocalizedError.swift */; };
2F0191FB2A9E579E00E9EB0E /* FHIRMultipleResourceInterpreter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0191FA2A9E579E00E9EB0E /* FHIRMultipleResourceInterpreter.swift */; };
2F0191FD2A9E57BD00E9EB0E /* Prompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0191FC2A9E57BD00E9EB0E /* Prompt.swift */; };
2F0608CF2AC3884100836556 /* ExportPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0608CE2AC3884100836556 /* ExportPackage.swift */; };
2F2146F42A82AF7F007CB929 /* SpeziOpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 2F2146F32A82AF7F007CB929 /* SpeziOpenAI */; };
2F2146F72A82AF9B007CB929 /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 2F2146F62A82AF9B007CB929 /* SpeziOnboarding */; };
2F2146FE2A82B236007CB929 /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 2F2146FD2A82B236007CB929 /* SpeziFirebaseAccount */; };
Expand All @@ -36,6 +37,7 @@
2F8537712A9DB6C8006994BB /* ModelsR4 in Frameworks */ = {isa = PBXBuildFile; productRef = 2F8537702A9DB6C8006994BB /* ModelsR4 */; };
2F8537742A9DB6E5006994BB /* HealthKitOnFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 2F8537732A9DB6E5006994BB /* HealthKitOnFHIR */; };
2F8537762A9DB781006994BB /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 2F8537752A9DB781006994BB /* SpeziAccount */; };
2F9652FE2AC388F300977083 /* URL+Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F9652FD2AC388F300977083 /* URL+Zip.swift */; };
2FA2023329CBCC0C0039C21A /* DocumentScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2023229CBCC0C0039C21A /* DocumentScanner.swift */; };
2FB2943929CBA29900EE91A0 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB2943329CBA29900EE91A0 /* ProfileView.swift */; };
2FB2943D29CBA29900EE91A0 /* ClinicalTrialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB2943729CBA29900EE91A0 /* ClinicalTrialsView.swift */; };
Expand Down Expand Up @@ -103,6 +105,7 @@
2F0191F82A9E4CF100E9EB0E /* String+LocalizedError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+LocalizedError.swift"; sourceTree = "<group>"; };
2F0191FA2A9E579E00E9EB0E /* FHIRMultipleResourceInterpreter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIRMultipleResourceInterpreter.swift; sourceTree = "<group>"; };
2F0191FC2A9E57BD00E9EB0E /* Prompt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Prompt.swift; sourceTree = "<group>"; };
2F0608CE2AC3884100836556 /* ExportPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportPackage.swift; sourceTree = "<group>"; };
2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = "<group>"; };
2F4E23822989D51F0013F3D9 /* TemplateAppTestingSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateAppTestingSetup.swift; sourceTree = "<group>"; };
2F5E32BC297E05EA003432F8 /* TemplateAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateAppDelegate.swift; sourceTree = "<group>"; };
Expand All @@ -112,6 +115,7 @@
2F8537672A9DB67E006994BB /* FHIRResource+Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FHIRResource+Search.swift"; sourceTree = "<group>"; };
2F8537682A9DB67E006994BB /* FHIR.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIR.swift; sourceTree = "<group>"; };
2F8537692A9DB67E006994BB /* FHIRResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIRResource.swift; sourceTree = "<group>"; };
2F9652FD2AC388F300977083 /* URL+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Zip.swift"; sourceTree = "<group>"; };
2FA2023229CBCC0C0039C21A /* DocumentScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentScanner.swift; sourceTree = "<group>"; };
2FAEC07F297F583900C11C42 /* TemplateApplication.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TemplateApplication.entitlements; sourceTree = "<group>"; };
2FB2943329CBA29900EE91A0 /* ProfileView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -294,6 +298,8 @@
isa = PBXGroup;
children = (
2FB2943329CBA29900EE91A0 /* ProfileView.swift */,
2F0608CE2AC3884100836556 /* ExportPackage.swift */,
2F9652FD2AC388F300977083 /* URL+Zip.swift */,
);
path = Profile;
sourceTree = "<group>";
Expand Down Expand Up @@ -601,12 +607,14 @@
2FA2023329CBCC0C0039C21A /* DocumentScanner.swift in Sources */,
FCD3FFB729CD0C31004D1E0E /* LogoView.swift in Sources */,
2FB2FCD029CBDDC00027D85A /* TemplateSignUp.swift in Sources */,
2F9652FE2AC388F300977083 /* URL+Zip.swift in Sources */,
2FB2FCD729CBDDC00027D85A /* Consent.swift in Sources */,
2FB2FCD329CBDDC00027D85A /* TemplateLogin.swift in Sources */,
2F8537652A9D1279006994BB /* HealthKitPermissions.swift in Sources */,
2FE573A729CD4672008EBBD4 /* PDFView.swift in Sources */,
2F0191F62A9E4CBD00E9EB0E /* OpenAIChatView.swift in Sources */,
2FE573A329CD4617008EBBD4 /* PDFDocument+Transferable.swift in Sources */,
2F0608CF2AC3884100836556 /* ExportPackage.swift in Sources */,
2F4E23832989D51F0013F3D9 /* TemplateAppTestingSetup.swift in Sources */,
2F5E32BD297E05EA003432F8 /* TemplateAppDelegate.swift in Sources */,
2FB2FCD129CBDDC00027D85A /* UserView.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions TemplateApplication/FHIR Standard/FHIR.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ actor FHIR: Standard, ObservableObject, ObservableObjectProvider, HealthKitConst
Array(_resources.values)
}

@MainActor var exportPackage: ExportPackage {
ExportPackage(resources: resources)
}

Check warning on line 40 in TemplateApplication/FHIR Standard/FHIR.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/FHIR Standard/FHIR.swift#L38-L40

Added lines #L38 - L40 were not covered by tests


init() {
guard HKHealthStore.isHealthDataAvailable() else {
Expand Down
61 changes: 61 additions & 0 deletions TemplateApplication/Profile/ExportPackage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// This source file is part of the Stanford OwnYourData Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import CoreTransferable
import OSLog


struct ExportPackage: Transferable {
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(
exportedContentType: .zip,
exporting: { document in
try await SentTransferredFile(document.zipRepresentation)
}
)
}

Check warning on line 21 in TemplateApplication/Profile/ExportPackage.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ExportPackage.swift#L14-L21

Added lines #L14 - L21 were not covered by tests


let resources: [FHIRResource]

private var directory: URL {
FileManager.default.temporaryDirectory.appendingPathComponent(
"edu.stanford.ownyourdate.export",
isDirectory: true
)
}

Check warning on line 31 in TemplateApplication/Profile/ExportPackage.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ExportPackage.swift#L26-L31

Added lines #L26 - L31 were not covered by tests

var zipRepresentation: URL {
get async throws {
if directory.exists {
try FileManager.default.removeItem(at: directory)
}
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)

for resource in resources {
guard let id = resource.id else {
os_log("Can not export resource named \(resource.displayName)")
continue
}

let resourceJSONData = Data(resource.jsonDescription.utf8)
try resourceJSONData.write(to: directory.appending(path: "\(id).json"))
}

let zipURL = try directory.zip()
try FileManager.default.removeItem(at: directory)

return zipURL
}

Check warning on line 54 in TemplateApplication/Profile/ExportPackage.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ExportPackage.swift#L34-L54

Added lines #L34 - L54 were not covered by tests
}


func deleteZipRepresentation() throws {
try FileManager.default.removeItem(at: directory.appendingPathExtension(".zip"))
}

Check warning on line 60 in TemplateApplication/Profile/ExportPackage.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ExportPackage.swift#L58-L60

Added lines #L58 - L60 were not covered by tests
}
103 changes: 69 additions & 34 deletions TemplateApplication/Profile/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,51 +19,86 @@ struct ProfileView: View {
@AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false

@EnvironmentObject var documentManager: DocumentManager
@EnvironmentObject var fhirStandard: FHIR


var body: some View {
VStack {
Image(systemName: "person.circle.fill")
.resizable()
.scaledToFit()
.accessibility(label: Text("profile image"))
.foregroundColor(Color("ButtonColor_dark"))
.frame(width: 120, height: 120)
.padding(.top, 80)
VStack(spacing: 10) {
Text("\(firstName) \(lastName)")
.font(.title2)
Text("Email: \(email)")
.font(.subheadline)
}
Spacer()
OwnYourDataButton(
title: "Log Out",
action: {
do {
try Auth.auth().signOut()

firstName = ""
lastName = ""
email = ""

completedOnboardingFlow = false

documentManager.removeAllDocuments()

print("Logged out.")
} catch {
print("Error signing out: \(error)")
GeometryReader { proxy in
ScrollView(.vertical) {
VStack {
Image(systemName: "person.circle.fill")
.resizable()
.scaledToFit()
.accessibility(label: Text("profile image"))
.foregroundColor(Color("ButtonColor_dark"))
.frame(width: 120, height: 120)
.padding(.top, 40)
VStack(spacing: 10) {
Text("\(firstName) \(lastName)")
.font(.title2)
Text("Email: \(email)")
.font(.subheadline)

Check warning on line 40 in TemplateApplication/Profile/ProfileView.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ProfileView.swift#L26-L40

Added lines #L26 - L40 were not covered by tests
}
sharebutton
Spacer(minLength: 64)
logoutButton

Check warning on line 44 in TemplateApplication/Profile/ProfileView.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ProfileView.swift#L42-L44

Added lines #L42 - L44 were not covered by tests
}
)
.padding(.bottom, 30)
.frame(minHeight: proxy.size.height)
}
.frame(width: proxy.size.width)

Check warning on line 49 in TemplateApplication/Profile/ProfileView.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ProfileView.swift#L46-L49

Added lines #L46 - L49 were not covered by tests
}
.padding(.bottom, 30)
.task {
fetchUserData()
}
}

@ViewBuilder private var sharebutton: some View {
VStack(alignment: .center, spacing: 8) {
ShareLink(
item: fhirStandard.exportPackage,
preview: SharePreview(
Text("FHIR JSON Export Package")
)
) {
HStack {
Image(systemName: "square.and.arrow.up")
.accessibilityHidden(true)
Text("Export")
}
}
.buttonStyle(.borderedProminent)
Text("Export your health records to share them with the OwnYourData team.")
.multilineTextAlignment(.center)
.foregroundStyle(Color.accentColor)
.font(.caption)
}
.padding()
}

Check warning on line 77 in TemplateApplication/Profile/ProfileView.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ProfileView.swift#L56-L77

Added lines #L56 - L77 were not covered by tests

@ViewBuilder private var logoutButton: some View {
OwnYourDataButton(
title: "Log Out",
action: {
do {
try Auth.auth().signOut()

firstName = ""
lastName = ""
email = ""

completedOnboardingFlow = false

documentManager.removeAllDocuments()

print("Logged out.")
} catch {
print("Error signing out: \(error)")
}
}
)
}

Check warning on line 100 in TemplateApplication/Profile/ProfileView.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/ProfileView.swift#L79-L100

Added lines #L79 - L100 were not covered by tests


private func fetchUserData() {
if let currentUser = Auth.auth().currentUser {
Expand Down
108 changes: 108 additions & 0 deletions TemplateApplication/Profile/URL+Zip.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// This source file is part of the Stanford OwnYourData Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import Foundation
import OSLog


extension URL {
var isDirectory: Bool {
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
}

Check warning on line 16 in TemplateApplication/Profile/URL+Zip.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/URL+Zip.swift#L14-L16

Added lines #L14 - L16 were not covered by tests

var exists: Bool {
FileManager.default.fileExists(atPath: path)
}

Check warning on line 20 in TemplateApplication/Profile/URL+Zip.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/URL+Zip.swift#L18-L20

Added lines #L18 - L20 were not covered by tests


func zip(to zipURL: URL? = nil) throws -> URL {
let logger = Logger(subsystem: "edu.stanford.spezi.zipcomponent", category: "ZipComponent")

let zipURL = zipURL ?? self.appendingPathExtension("zip")

let directory: URL
let temporaryDirectory: Bool
if isFileURL, exists, isDirectory {
directory = self
temporaryDirectory = false
} else {
// Crete a temporary folder
directory = FileManager.default.temporaryDirectory.appendingPathComponent(
"edu.stanford.spezi.zipcomponent/\(UUID().uuidString)",
isDirectory: true
)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
temporaryDirectory = true

// Copy the file to the new folder
let filePath = directory.appendingPathComponent(lastPathComponent)
try FileManager.default.copyItem(at: self, to: filePath)
}

// Clean up the folder at the end of the function independent of the outcome
defer {
if temporaryDirectory {
do {
try FileManager.default.removeItem(at: directory)
} catch {
logger.error("Could not remove temporary directory at \(directory)")
}
}
}

var zipError, copyError: NSError?

NSFileCoordinator().coordinate(
readingItemAt: directory,
options: .forUploading,
error: &zipError
) { zippedURL in
do {
if zipURL.exists {
try FileManager.default.removeItem(at: zipURL)
}
try FileManager.default.copyItem(at: zippedURL, to: zipURL)
} catch {
logger.debug("Copying the ziped file from \(zippedURL) to \(zipURL) failed.")
copyError = error as NSError
}
}

if let copyError {
throw copyError
}
if let zipError {
logger.debug("Could not zip the directory at \(directory): \(zipError)")
throw zipError
}

return zipURL
}

Check warning on line 85 in TemplateApplication/Profile/URL+Zip.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/URL+Zip.swift#L23-L85

Added lines #L23 - L85 were not covered by tests
}


extension Data {
var zip: Data {
get throws {
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(
"edu.stanford.spezi.zipcomponent/\(UUID().uuidString)",
isDirectory: true
)

try self.write(to: temporaryDirectory, options: .atomic)
let zipURL = try temporaryDirectory.zip()

let zippedData = try Data(contentsOf: zipURL)

try? FileManager.default.removeItem(at: temporaryDirectory)
try? FileManager.default.removeItem(at: zipURL)

return zippedData
}

Check warning on line 106 in TemplateApplication/Profile/URL+Zip.swift

View check run for this annotation

Codecov / codecov/patch

TemplateApplication/Profile/URL+Zip.swift#L91-L106

Added lines #L91 - L106 were not covered by tests
}
}

0 comments on commit cfe5c72

Please sign in to comment.