From eb17a619f16736cc640f0be3b6d24377febae6da Mon Sep 17 00:00:00 2001 From: "mathieu J." Date: Wed, 4 Dec 2024 19:00:49 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20(AccountKit):=20Add?= =?UTF-8?q?=20ConsentInfo=20to=20RootAccount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/ConsentInfo+Codable.swift | 21 +++++++++++++++++++ .../Extensions/RootAccount+Codable.swift | 2 ++ .../ConsentInfo/ConsentInfo+Init.swift | 14 +++++++++++++ .../RootAccount/ConsentInfo/ConsentInfo.swift | 19 +++++++++++++++++ .../Models/RootAccount/RootAccount+Init.swift | 4 +++- .../Models/RootAccount/RootAccount.swift | 2 ++ 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 Modules/AccountKit/Sources/Extensions/ConsentInfo+Codable.swift create mode 100644 Modules/AccountKit/Sources/Models/RootAccount/ConsentInfo/ConsentInfo+Init.swift create mode 100644 Modules/AccountKit/Sources/Models/RootAccount/ConsentInfo/ConsentInfo.swift diff --git a/Modules/AccountKit/Sources/Extensions/ConsentInfo+Codable.swift b/Modules/AccountKit/Sources/Extensions/ConsentInfo+Codable.swift new file mode 100644 index 0000000000..10a0c34e22 --- /dev/null +++ b/Modules/AccountKit/Sources/Extensions/ConsentInfo+Codable.swift @@ -0,0 +1,21 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension ConsentInfo: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.policyVersion = try container.decode(String.self, forKey: .policyVersion) + self.acceptedAt = try container.decode(Date.self, forKey: .acceptedAt) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.policyVersion, forKey: .policyVersion) + try container.encode(self.acceptedAt, forKey: .acceptedAt) + } +} diff --git a/Modules/AccountKit/Sources/Extensions/RootAccount+Codable.swift b/Modules/AccountKit/Sources/Extensions/RootAccount+Codable.swift index c877c6d222..5078c6a806 100644 --- a/Modules/AccountKit/Sources/Extensions/RootAccount+Codable.swift +++ b/Modules/AccountKit/Sources/Extensions/RootAccount+Codable.swift @@ -7,9 +7,11 @@ import SwiftUI public extension RootAccount { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent(String.self, forKey: .id) self.rootOwnerUid = try container.decode(String.self, forKey: .rootOwnerUid) self.library = try container.decodeIfPresent(Library.self, forKey: .library) ?? Library() + self.consentInfo = try container.decode([ConsentInfo].self, forKey: .consentInfo) self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) self.lastEditedAt = try container.decodeIfPresent(Date.self, forKey: .lastEditedAt) } diff --git a/Modules/AccountKit/Sources/Models/RootAccount/ConsentInfo/ConsentInfo+Init.swift b/Modules/AccountKit/Sources/Models/RootAccount/ConsentInfo/ConsentInfo+Init.swift new file mode 100644 index 0000000000..53d88b68b3 --- /dev/null +++ b/Modules/AccountKit/Sources/Models/RootAccount/ConsentInfo/ConsentInfo+Init.swift @@ -0,0 +1,14 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public extension ConsentInfo { + init( + policyVersion: String + ) { + self.policyVersion = policyVersion + self.acceptedAt = Date() + } +} diff --git a/Modules/AccountKit/Sources/Models/RootAccount/ConsentInfo/ConsentInfo.swift b/Modules/AccountKit/Sources/Models/RootAccount/ConsentInfo/ConsentInfo.swift new file mode 100644 index 0000000000..54f575138a --- /dev/null +++ b/Modules/AccountKit/Sources/Models/RootAccount/ConsentInfo/ConsentInfo.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public struct ConsentInfo: Codable { + // MARK: Public + + public var policyVersion: String + public var acceptedAt: Date + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case policyVersion = "policy_version" + case acceptedAt = "accepted_at" + } +} diff --git a/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+Init.swift b/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+Init.swift index 87e8afa00f..fcc514214a 100644 --- a/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+Init.swift +++ b/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+Init.swift @@ -8,10 +8,12 @@ public extension RootAccount { init( id: String = "", rootOwnerUid: String = "", - library: Library = Library() + library: Library = Library(), + consentInfo: [ConsentInfo] = [] ) { self.id = id self.rootOwnerUid = rootOwnerUid self.library = library + self.consentInfo = consentInfo } } diff --git a/Modules/AccountKit/Sources/Models/RootAccount/RootAccount.swift b/Modules/AccountKit/Sources/Models/RootAccount/RootAccount.swift index 0b78d4488d..5f2a937bf9 100644 --- a/Modules/AccountKit/Sources/Models/RootAccount/RootAccount.swift +++ b/Modules/AccountKit/Sources/Models/RootAccount/RootAccount.swift @@ -13,6 +13,7 @@ public struct RootAccount: AccountDocument { @ServerTimestamp public var lastEditedAt: Date? public var rootOwnerUid: String public var library: Library + public var consentInfo: [ConsentInfo] // MARK: Internal @@ -20,6 +21,7 @@ public struct RootAccount: AccountDocument { case id = "uuid" case rootOwnerUid = "root_owner_uid" case library + case consentInfo = "consent_info" case createdAt = "created_at" case lastEditedAt = "last_edited_at" } From efb95b4c57fa19f8b6591f3aade0d481a0a0dc37 Mon Sep 17 00:00:00 2001 From: "mathieu J." Date: Wed, 4 Dec 2024 22:46:54 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20(LekaApp):=20Add=20ConsentView?= =?UTF-8?q?=20in=20SignUp=20flow=20w/=20policy=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountCreation/AccountCreationView.swift | 19 ++++- .../Views/ConsentView/ConsentView.swift | 64 +++++++++++++++++ .../Views/ConsentView/PrivacyPolicyView.swift | 70 +++++++++++++++++++ .../Extensions/ConsentInfo+Codable.swift | 21 ------ 4 files changed, 151 insertions(+), 23 deletions(-) create mode 100644 Apps/LekaApp/Sources/Views/ConsentView/ConsentView.swift create mode 100644 Apps/LekaApp/Sources/Views/ConsentView/PrivacyPolicyView.swift delete mode 100644 Modules/AccountKit/Sources/Extensions/ConsentInfo+Codable.swift diff --git a/Apps/LekaApp/Sources/Views/AccountCreation/AccountCreationView.swift b/Apps/LekaApp/Sources/Views/AccountCreation/AccountCreationView.swift index 0da55b9b23..8011c470b8 100644 --- a/Apps/LekaApp/Sources/Views/AccountCreation/AccountCreationView.swift +++ b/Apps/LekaApp/Sources/Views/AccountCreation/AccountCreationView.swift @@ -39,7 +39,7 @@ struct AccountCreationView: View { .disableAutocorrection(true) Button { - self.submitForm() + self.showConsentView = true } label: { Text(String(l10n.AccountCreationView.connectionButton.characters)) .loadingIndicator( @@ -52,7 +52,8 @@ struct AccountCreationView: View { } .onChange(of: self.authManagerViewModel.userAuthenticationState) { newValue in if newValue == .loggedIn { - self.rootAccountManager.createRootAccount(rootAccount: RootAccount()) + let rootAccount = RootAccount(consentInfo: [self.userConsentInfo!]) + self.rootAccountManager.createRootAccount(rootAccount: rootAccount) self.rootAccountManager.initializeRootAccountListener() self.isVerificationEmailAlertPresented = true } @@ -71,6 +72,18 @@ struct AccountCreationView: View { self.navigation.navigateToAccountCreationProcess = true }) } + .sheet(isPresented: self.$showConsentView) { + ConsentView( + onCancel: { + self.showConsentView = false + }, + onAccept: { + self.userConsentInfo = ConsentInfo(policyVersion: "1.0.0") + self.showConsentView = false + self.submitForm() + } + ) + } } // MARK: Private @@ -81,6 +94,8 @@ struct AccountCreationView: View { @ObservedObject private var navigation: Navigation = .shared @State private var isVerificationEmailAlertPresented: Bool = false + @State private var showConsentView: Bool = false + @State private var userConsentInfo: ConsentInfo? private var authManager = AuthManager.shared private var rootAccountManager = RootAccountManager.shared diff --git a/Apps/LekaApp/Sources/Views/ConsentView/ConsentView.swift b/Apps/LekaApp/Sources/Views/ConsentView/ConsentView.swift new file mode 100644 index 0000000000..96b630fbd5 --- /dev/null +++ b/Apps/LekaApp/Sources/Views/ConsentView/ConsentView.swift @@ -0,0 +1,64 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct ConsentView: View { + // MARK: Internal + + let onCancel: () -> Void + let onAccept: () -> Void + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + ScrollView { + PrivacyPolicyView() + .padding(.bottom) + } + .frame(maxWidth: .infinity, alignment: .leading) + + VStack(spacing: 14) { + Divider() + Group { + Toggle(isOn: self.$isConsentGiven) { + Label("I have read and agree to the terms and privacy policy.", systemImage: "checkmark.shield.fill") + } + + Button("Continue", action: self.onAccept) + .buttonStyle(.borderedProminent) + .disabled(!self.isConsentGiven) + } + .padding([.bottom, .horizontal]) + } + .background(Color(.systemGray6)) + .edgesIgnoringSafeArea(.bottom) + } + .navigationTitle("Privacy Policy") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel", action: self.onCancel) + } + } + } + } + + // MARK: Private + + @State private var isConsentGiven: Bool = false +} + +// MARK: - ConsentView_Previews + +#Preview { + ConsentView( + onCancel: { + print("Content was declined") + }, + onAccept: { + print("Consent was given") + } + ) +} diff --git a/Apps/LekaApp/Sources/Views/ConsentView/PrivacyPolicyView.swift b/Apps/LekaApp/Sources/Views/ConsentView/PrivacyPolicyView.swift new file mode 100644 index 0000000000..3610914e92 --- /dev/null +++ b/Apps/LekaApp/Sources/Views/ConsentView/PrivacyPolicyView.swift @@ -0,0 +1,70 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import MarkdownUI +import SwiftUI + +struct PrivacyPolicyView: View { + // MARK: Internal + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Markdown(self.markdownText) + .markdownTheme(.gitHub) + .padding() + .multilineTextAlignment(.leading) + } + } + + // MARK: Private + + // swiftlint:disable line_length + private let markdownText = """ + # Who we are + The address of our site: [https://leka.io](https://leka.io) + + ## Comments + When you leave a comment on our site, the data entered in the comment form as well as your IP address and your browser's user-agent are collected to help us detect undesirable comments. + + An anonymized string created from your email address (also called a hash) may be sent to the Gravatar service to verify your use of the service. The Gravatar service privacy policy is available [here](https://automattic.com/privacy/). After validation of your comment, your profile picture will be publicly visible beside your comment. + + ## Media + If you upload images to the site, we advise you to avoid uploading images that contain EXIF data of GPS coordinates. People visiting your site may download and extract location data from these images. + + ## Cookies + If you leave a comment on our site, you will be asked to save your name, email address, and site in cookies. This is only for your convenience so that you do not have to enter this information if you leave another comment later. These cookies expire after one year. + + If you go to the login page, a temporary cookie will be created to determine if your browser accepts cookies. It does not contain any personal information and will be deleted automatically when you close your browser. + + When you log in, we will set a number of cookies to store your login information and screen preferences. The lifetime of a login cookie is two days, and the lifetime of a screen option cookie is one year. If you tick “Remember me”, your login cookie will be kept for two weeks. If you log out of your account, the login cookie will be deleted. + + By editing or publishing a post, an additional cookie will be stored in your browser. This cookie does not include any personal information. It simply indicates the ID of the publication you have just modified. It expires after one day. + + ## Embedded content from other sites + Articles on this site may include embedded content (e.g. videos, images, articles, etc.). Embedded content from other sites behaves in the same way as if the visitor were on that other site. + + These websites may collect data about you, use cookies, embed third-party tracking tools, and track your interactions with such embedded content if you have an account with their website. + + ## Use and disclosure of your personal data + If you request a password reset, your IP address will be included in the reset email. + + ## How long we store your data + If you leave a comment, the comment and its metadata are kept indefinitely. This allows us to automatically recognize and approve subsequent comments instead of leaving them in the moderation queue. + + For accounts that register with our site (if any), we also store the personal data listed in their profile. All accounts can view, edit, or delete their personal information at any time (except for their user ID). Site managers can also view and edit this information. + + ## The rights you have over your data + If you have an account or if you have left comments on the site, you can request to receive a file containing all the personal data that we have about you, including those that you have provided to us. You may also request the deletion of your personal data. This does not include data stored for administrative, legal, or security purposes. + + ## Disclosure of your personal data + Visitors' comments can be checked using an automated service to detect undesirable comments. + """ + // swiftlint:enable line_length +} + +// MARK: - PrivacyPolicyView_Previews + +#Preview { + PrivacyPolicyView() +} diff --git a/Modules/AccountKit/Sources/Extensions/ConsentInfo+Codable.swift b/Modules/AccountKit/Sources/Extensions/ConsentInfo+Codable.swift deleted file mode 100644 index 10a0c34e22..0000000000 --- a/Modules/AccountKit/Sources/Extensions/ConsentInfo+Codable.swift +++ /dev/null @@ -1,21 +0,0 @@ -// Leka - iOS Monorepo -// Copyright APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -import SwiftUI - -extension ConsentInfo: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.policyVersion = try container.decode(String.self, forKey: .policyVersion) - self.acceptedAt = try container.decode(Date.self, forKey: .acceptedAt) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(self.policyVersion, forKey: .policyVersion) - try container.encode(self.acceptedAt, forKey: .acceptedAt) - } -}