Skip to content

Commit

Permalink
Export Consent Forms (Markdown) (#40)
Browse files Browse the repository at this point in the history
<!--

This source file is part of the Stanford Spezi open-source project

SPDX-FileCopyrightText: 2022 Stanford University and the project authors
(see CONTRIBUTORS.md)

SPDX-License-Identifier: MIT

-->

# *Export Consent Forms (Markdown)*

## ♻️ Current situation & Problem
Until now, users could view and sign a consent document in the Spezi
template application using the `SpeziOnboarding` package. However, the
app did not offer any provision to save or export the signed consent
document.
In the medical domain, the consent form collection is especially
important in order to serve as legal cover.
See StanfordSpezi/SpeziOnboarding#7.

Utilizes: StanfordSpezi/SpeziFirebase#17


## ⚙️ Release Notes 
- The template application now utilizes the `OnboardingConsentView` of
`SpeziOnboarding`, providing an export and share functionality of the
signed consent form as a PDF.


## 📚 Documentation
Incorporated where necessary.
Associated with StanfordSpezi/SpeziOnboarding#26


## ✅ Testing
--


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [X] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

---------

Co-authored-by: Paul Schmiedmayer <[email protected]>
  • Loading branch information
philippzagar and PSchmiedmayer authored Oct 16, 2023
1 parent 692c7c9 commit 5f4e95c
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 71 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Template Application Contributors

* [Paul Schmiedmayer](https://github.com/PSchmiedmayer)
* [Andreas Bauer](https://github.com/Supereg)
* [Philipp Zagar](https://github.com/philippzagar)
Binary file modified Figures/TemplateOnboardingFlow/Consent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 20 additions & 4 deletions TemplateApplication.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
653A256228338800005D4D48 /* TemplateApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A256128338800005D4D48 /* TemplateApplicationTests.swift */; };
653A256C28338800005D4D48 /* SchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A256B28338800005D4D48 /* SchedulerTests.swift */; };
9733CFC62A8066DE001B7ABC /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8029EDD91D004B9AB4 /* SpeziOnboarding */; };
9739A0C62AD7B5730084BEA5 /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 9739A0C52AD7B5730084BEA5 /* FirebaseStorage */; };
97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */; };
97D8B2A42A7653E600715F50 /* ProcessInfo+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97D8B2A32A7653E600715F50 /* ProcessInfo+PreviewSimulator.swift */; };
97D8B2AB2A769A6E00715F50 /* OnboardingFlow+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97D8B2AA2A769A6E00715F50 /* OnboardingFlow+PreviewSimulator.swift */; };
A92525A72ABC4B5F00640379 /* AccountUpdateModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92525A62ABC4B5F00640379 /* AccountUpdateModifier.swift */; };
Expand Down Expand Up @@ -155,6 +157,7 @@
9733CFC62A8066DE001B7ABC /* SpeziOnboarding in Frameworks */,
2FE5DC6429EDD883004B9AB4 /* SpeziAccount in Frameworks */,
2FB099AF2A875DF100B20952 /* FirebaseAuth in Frameworks */,
97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */,
2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */,
2FE5DC8429EDD934004B9AB4 /* SpeziQuestionnaire in Frameworks */,
2FB099B32A875DF100B20952 /* FirebaseFirestoreSwift in Frameworks */,
Expand All @@ -163,6 +166,7 @@
2FE5DC8A29EDD972004B9AB4 /* SpeziLocalStorage in Frameworks */,
2FE5DC8C29EDD972004B9AB4 /* SpeziSecureStorage in Frameworks */,
2FE5DC7529EDD8E6004B9AB4 /* SpeziFirebaseAccount in Frameworks */,
9739A0C62AD7B5730084BEA5 /* FirebaseStorage in Frameworks */,
2FF53D8B2A8725DE00042B76 /* SpeziMockWebService in Frameworks */,
2FE5DC7229EDD8D3004B9AB4 /* SpeziHealthKit in Frameworks */,
2F49B7762980407C00BCB272 /* Spezi in Frameworks */,
Expand Down Expand Up @@ -223,10 +227,10 @@
2FE5DC2829EDD398004B9AB4 /* Onboarding */ = {
isa = PBXGroup;
children = (
2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */,
2FE5DC3129EDD7CA004B9AB4 /* OnboardingFlow.swift */,
2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */,
2FE5DC3229EDD7CA004B9AB4 /* InterestingModules.swift */,
2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */,
2FE5DC2F29EDD7CA004B9AB4 /* Consent.swift */,
2FE5DC3029EDD7CA004B9AB4 /* HealthKitPermissions.swift */,
2F65B44D2A3B8B0600A36932 /* NotificationPermissions.swift */,
Expand Down Expand Up @@ -401,6 +405,8 @@
2FB099B02A875DF100B20952 /* FirebaseFirestore */,
2FB099B22A875DF100B20952 /* FirebaseFirestoreSwift */,
2FB099B52A875E2B00B20952 /* HealthKitOnFHIR */,
9739A0C52AD7B5730084BEA5 /* FirebaseStorage */,
97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */,
);
productName = TemplateApplication;
productReference = 653A254D283387FE005D4D48 /* TemplateApplication.app */;
Expand Down Expand Up @@ -1138,7 +1144,7 @@
repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.5.1;
minimumVersion = 0.6.0;
};
};
2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = {
Expand All @@ -1162,7 +1168,7 @@
repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.5.0;
minimumVersion = 0.5.1;
};
};
2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
Expand Down Expand Up @@ -1210,7 +1216,7 @@
repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.5.1;
minimumVersion = 0.6.0;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down Expand Up @@ -1325,6 +1331,16 @@
package = 2FE750CA2A87240100723EAE /* XCRemoteSwiftPackageReference "SpeziMockWebService" */;
productName = SpeziMockWebService;
};
9739A0C52AD7B5730084BEA5 /* FirebaseStorage */ = {
isa = XCSwiftPackageProductDependency;
package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseStorage;
};
97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */ = {
isa = XCSwiftPackageProductDependency;
package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */;
productName = SpeziFirebaseStorage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 653A2545283387FE005D4D48 /* Project object */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
{
"identity" : "healthkitonfhir",
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git",
"location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR",
"state" : {
"revision" : "fdf8e4543718a940643598e4bd5e750e9c4c5540",
"version" : "0.2.4"
Expand Down Expand Up @@ -176,8 +176,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziFirebase.git",
"state" : {
"revision" : "7e31453c06f76ab3f2d0e7d3cff2e186d562dfd3",
"version" : "0.5.1"
"revision" : "56dd7fb9204f9ec48f54c5c867f9755bec50b8ac",
"version" : "0.6.0"
}
},
{
Expand All @@ -203,8 +203,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziOnboarding",
"state" : {
"revision" : "e0b11e101a920fa01f92e0ebf77fa0bb1c0265ef",
"version" : "0.5.1"
"revision" : "0ea46a66c17615e1a933481a07434bfd41717c54",
"version" : "0.6.0"
}
},
{
Expand Down Expand Up @@ -237,10 +237,10 @@
{
"identity" : "speziviews",
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziViews.git",
"location" : "https://github.com/StanfordSpezi/SpeziViews",
"state" : {
"revision" : "626914fa7554aa2b994257541c0794eb930520ba",
"version" : "0.5.0"
"revision" : "4b7cc423fd823123d354ec1d541ca7d2e0a9d6e3",
"version" : "0.5.1"
}
},
{
Expand Down
11 changes: 3 additions & 8 deletions TemplateApplication/Onboarding/Consent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SpeziOnboarding
import SwiftUI


/// - Note: The `OnboardingConsentView` exports the signed consent form as PDF to the Spezi `Standard`, necessitating the conformance of the `Standard` to the `OnboardingConstraint`.
struct Consent: View {
@EnvironmentObject private var onboardingNavigationPath: OnboardingNavigationPath

Expand All @@ -24,14 +25,8 @@ struct Consent: View {


var body: some View {
ConsentView(
header: {
OnboardingTitleView(
title: "CONSENT_TITLE",
subtitle: "CONSENT_SUBTITLE"
)
},
asyncMarkdown: {
OnboardingConsentView(
markdown: {
consentDocument
},
action: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ import SwiftUI
/// Defines onboarding views that are shown in the Xcode preview simulator
extension OnboardingFlow {
static let previewSimulatorViews: [any View] = {
[Welcome(), InterestingModules(), Consent(), AccountOnboarding(), HealthKitPermissions(), NotificationPermissions()]
[Welcome(), InterestingModules(), AccountOnboarding(), Consent(), HealthKitPermissions(), NotificationPermissions()]
}()
}
8 changes: 4 additions & 4 deletions TemplateApplication/Onboarding/OnboardingFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ struct OnboardingFlow: View {
Welcome()
InterestingModules()

#if !(targetEnvironment(simulator) && (arch(i386) || arch(x86_64)))
Consent()
#endif

if !FeatureFlags.disableFirebase {
AccountOnboarding()
}

#if !(targetEnvironment(simulator) && (arch(i386) || arch(x86_64)))
Consent()
#endif

if HKHealthStore.isHealthDataAvailable() && !healthKitAuthorization {
HealthKitPermissions()
}
Expand Down
1 change: 1 addition & 0 deletions TemplateApplication/Onboarding/Welcome.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct Welcome: View {
onboardingNavigationPath.nextStep()
}
)
.padding(.top, 24)
}
}

Expand Down
21 changes: 0 additions & 21 deletions TemplateApplication/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -86,27 +86,6 @@
}
}
},
"CONSENT_SUBTITLE" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Spezi can collect consent from a user. You can provide the consent document using a markdown file."
}
}
}
},
"CONSENT_TITLE" : {
"comment" : "MARK: Consent",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Consent Example"
}
}
}
},
"CONTACTS_NAVIGATION_TITLE" : {
"localizations" : {
"en" : {
Expand Down
4 changes: 4 additions & 0 deletions TemplateApplication/TemplateAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import Spezi
import SpeziAccount
import SpeziFirebaseAccount
import SpeziFirebaseStorage
import SpeziFirestore
import SpeziHealthKit
import SpeziMockWebService
import SpeziOnboarding
import SpeziQuestionnaire
import SpeziScheduler
import SwiftUI
Expand All @@ -33,13 +35,15 @@ class TemplateAppDelegate: SpeziAppDelegate {
FirebaseAccountConfiguration()
}
firestore
FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199))
}
if HKHealthStore.isHealthDataAvailable() {
healthKit
}
QuestionnaireDataSource()
MockWebService()
TemplateApplicationScheduler()
OnboardingDataSource()
}
}

Expand Down
49 changes: 48 additions & 1 deletion TemplateApplication/TemplateApplicationStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@
// SPDX-License-Identifier: MIT
//

import FirebaseAuth
import FirebaseFirestore
import FirebaseFirestoreSwift
import FirebaseStorage
import HealthKitOnFHIR
import OSLog
import PDFKit
import Spezi
import SpeziAccount
import SpeziFirestore
import SpeziHealthKit
import SpeziMockWebService
import SpeziOnboarding
import SpeziQuestionnaire
import SwiftUI


actor TemplateApplicationStandard: Standard, ObservableObject, ObservableObjectProvider, HealthKitConstraint,
QuestionnaireConstraint, AccountNotifyStandard {
QuestionnaireConstraint, AccountNotifyStandard, OnboardingConstraint {
enum TemplateApplicationStandardError: Error {
case userNotAuthenticatedYet
}
Expand All @@ -41,6 +45,15 @@ actor TemplateApplicationStandard: Standard, ObservableObject, ObservableObjectP
return Firestore.firestore().collection("users").document(details.userId)
}
}

private var userBucketReference: StorageReference {
get async throws {
guard let uid = Auth.auth().currentUser?.uid else {
throw TemplateApplicationStandardError.userNotAuthenticatedYet
}
return Storage.storage().reference().child("users/\(uid)")
}
}


func updateAccount(details: AccountDetails) async {
Expand Down Expand Up @@ -125,4 +138,38 @@ actor TemplateApplicationStandard: Standard, ObservableObject, ObservableObjectP
logger.error("Could not delete user document: \(error)")
}
}

/// Stores the given consent form in the user's document directory with a unique timestamped filename.
///
/// - Parameter consent: The consent form's data to be stored as a `PDFDocument`.
func store(consent: PDFDocument) async {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd_HHmmss"
let dateString = formatter.string(from: Date())

guard !FeatureFlags.disableFirebase else {
guard let basePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
logger.error("Could not create path for writing consent form to user document directory.")
return
}

let filePath = basePath.appending(path: "consentForm_\(dateString).pdf")
consent.write(to: filePath)

return
}

do {
guard let consentData = consent.dataRepresentation() else {
logger.error("Could not store consent form.")
return
}

let metadata = StorageMetadata()
metadata.contentType = "application/pdf"
_ = try await userBucketReference.child("consent/\(dateString).pdf").putDataAsync(consentData, metadata: metadata)
} catch {
logger.error("Could not store consent form: \(error)")
}
}
}
Loading

0 comments on commit 5f4e95c

Please sign in to comment.