From fc6858db3999188b2bff21777f1e5c6cf5312545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Tr=C3=A9guier?= Date: Fri, 4 Oct 2024 17:19:05 +0200 Subject: [PATCH] Require accepting terms and privacy policy --- Fyreplace/Fakes/FakeClient.swift | 6 +- Fyreplace/Resources/Localizable.xcstrings | 10 ++++ Fyreplace/Views/Forms/DynamicForm.swift | 6 +- Fyreplace/Views/Forms/EnvironmentPicker.swift | 3 - Fyreplace/Views/Forms/LogoHeader.swift | 11 +--- Fyreplace/Views/Forms/SubmitOrCancel.swift | 2 - .../Views/Screens/AuthenticatingScreen.swift | 3 - Fyreplace/Views/Screens/LoginScreen.swift | 16 +---- Fyreplace/Views/Screens/RegisterScreen.swift | 59 ++++++++++++------- .../Screens/RegisterScreenProtocol.swift | 3 +- Fyreplace/Views/Screens/Screen.swift | 7 +-- Fyreplace/Views/Screens/SettingsScreen.swift | 2 +- .../Screens/RegisterScreenTests.swift | 24 ++++++-- 13 files changed, 81 insertions(+), 71 deletions(-) diff --git a/Fyreplace/Fakes/FakeClient.swift b/Fyreplace/Fakes/FakeClient.swift index 4e4d0f4..e7c39e4 100644 --- a/Fyreplace/Fakes/FakeClient.swift +++ b/Fyreplace/Fakes/FakeClient.swift @@ -270,9 +270,9 @@ extension FakeClient { static let passwordUsername = "password-username" static let goodUsername = "good-username" - static let badEmail = "bad-email" - static let usedEmail = "used-email" - static let goodEmail = "good-email" + static let badEmail = "bad@email" + static let usedEmail = "used@email" + static let goodEmail = "good@email" static let notImageBody = HTTPBody(stringLiteral: "Not") static let largeImageBody = HTTPBody(stringLiteral: "Large") diff --git a/Fyreplace/Resources/Localizable.xcstrings b/Fyreplace/Resources/Localizable.xcstrings index 4753166..8b09262 100644 --- a/Fyreplace/Resources/Localizable.xcstrings +++ b/Fyreplace/Resources/Localizable.xcstrings @@ -584,6 +584,16 @@ } } }, + "Register.TermsAcceptance" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "I accept the terms of service and the privacy policy" + } + } + } + }, "Register.Username" : { "localizations" : { "en" : { diff --git a/Fyreplace/Views/Forms/DynamicForm.swift b/Fyreplace/Views/Forms/DynamicForm.swift index d6fc865..846f11c 100644 --- a/Fyreplace/Views/Forms/DynamicForm.swift +++ b/Fyreplace/Views/Forms/DynamicForm.swift @@ -8,11 +8,9 @@ struct DynamicForm: View where Content: View { #if os(macOS) Form(content: content) .formStyle(.grouped) - .frame(minWidth: 360, maxWidth: 600) - .fixedSize() - .padding() + .frame(minWidth: 360) #else - HStack { + ZStack { Form(content: content) .frame(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 600 : nil) } diff --git a/Fyreplace/Views/Forms/EnvironmentPicker.swift b/Fyreplace/Views/Forms/EnvironmentPicker.swift index a759dc8..5575763 100644 --- a/Fyreplace/Views/Forms/EnvironmentPicker.swift +++ b/Fyreplace/Views/Forms/EnvironmentPicker.swift @@ -1,8 +1,6 @@ import SwiftUI struct EnvironmentPicker: View { - let namespace: Namespace.ID - @AppStorage("connection.environment") private var selectedEnvironment = ServerEnvironment.default @@ -19,6 +17,5 @@ struct EnvironmentPicker: View { } } .help("Environment.Help") - .matchedGeometryEffect(id: "environment-picker", in: namespace) } } diff --git a/Fyreplace/Views/Forms/LogoHeader.swift b/Fyreplace/Views/Forms/LogoHeader.swift index 5166462..cdad85e 100644 --- a/Fyreplace/Views/Forms/LogoHeader.swift +++ b/Fyreplace/Views/Forms/LogoHeader.swift @@ -1,8 +1,6 @@ import SwiftUI struct LogoHeader: View where ImageContent: View, TextContent: View { - let namespace: Namespace.ID - @ViewBuilder let imageContent: () -> ImageContent @@ -40,18 +38,11 @@ struct LogoHeader: View where ImageContent: View, Tex Spacer() } } - .matchedGeometryEffect(id: "header", in: namespace) } } -@available(macOS 14.0, *) -@available(iOS 17.0, *) #Preview { - @Previewable - @Namespace - var namespace - - LogoHeader(namespace: namespace) { + LogoHeader { Image(systemName: "photo") .resizable() .scaledToFit() diff --git a/Fyreplace/Views/Forms/SubmitOrCancel.swift b/Fyreplace/Views/Forms/SubmitOrCancel.swift index 6bb9156..6aa9c29 100644 --- a/Fyreplace/Views/Forms/SubmitOrCancel.swift +++ b/Fyreplace/Views/Forms/SubmitOrCancel.swift @@ -1,7 +1,6 @@ import SwiftUI struct SubmitOrCancel: View { - let namespace: Namespace.ID let submitLabel: LocalizedStringKey let canSubmit: Bool let canCancel: Bool @@ -16,7 +15,6 @@ struct SubmitOrCancel: View { action: submitAction ) .disabled(!canSubmit) - .matchedGeometryEffect(id: "submit", in: namespace) let cancel = Button(role: .cancel, action: cancelAction) { Text("Cancel").padding(.horizontal) diff --git a/Fyreplace/Views/Screens/AuthenticatingScreen.swift b/Fyreplace/Views/Screens/AuthenticatingScreen.swift index 9686655..ade5715 100644 --- a/Fyreplace/Views/Screens/AuthenticatingScreen.swift +++ b/Fyreplace/Views/Screens/AuthenticatingScreen.swift @@ -27,9 +27,6 @@ struct AuthenticatingScreen: View where Content: View { ) .navigationTitle("") .handlesExternalEvents(preferring: ["*"], allowing: ["action=connect"]) - #if os(macOS) - .animation(.snappy, value: choice) - #endif } else { content() } diff --git a/Fyreplace/Views/Screens/LoginScreen.swift b/Fyreplace/Views/Screens/LoginScreen.swift index 29b6c16..3cd9959 100644 --- a/Fyreplace/Views/Screens/LoginScreen.swift +++ b/Fyreplace/Views/Screens/LoginScreen.swift @@ -1,8 +1,6 @@ import SwiftUI struct LoginScreen: View, LoginScreenProtocol { - let namespace: Namespace.ID - @EnvironmentObject var eventBus: EventBus @@ -30,7 +28,7 @@ struct LoginScreen: View, LoginScreenProtocol { var body: some View { DynamicForm { Section { - EnvironmentPicker(namespace: namespace).disabled(isWaitingForRandomCode) + EnvironmentPicker().disabled(isWaitingForRandomCode) TextField( "Login.Identifier", @@ -41,7 +39,6 @@ struct LoginScreen: View, LoginScreenProtocol { .focused($focused, equals: .identifier) .disabled(isWaitingForRandomCode) .onSubmit(submit) - .matchedGeometryEffect(id: "first-field", in: namespace) #if !os(macOS) .textInputAutocapitalization(.never) .keyboardType(.asciiCapable) @@ -68,7 +65,6 @@ struct LoginScreen: View, LoginScreenProtocol { } SubmitOrCancel( - namespace: namespace, submitLabel: "Login.Submit", canSubmit: canSubmit, canCancel: isWaitingForRandomCode, @@ -77,7 +73,7 @@ struct LoginScreen: View, LoginScreenProtocol { cancelAction: cancel ) } header: { - LogoHeader(namespace: namespace) { + LogoHeader { Image("Logo", label: Text("Logo")).resizable() } textContent: { Text("Login.Header") @@ -122,15 +118,9 @@ struct LoginScreen: View, LoginScreenProtocol { } } -@available(macOS 14.0, *) -@available(iOS 17.0, *) #Preview { - @Previewable - @Namespace - var namespace - NavigationStack { - LoginScreen(namespace: namespace) + LoginScreen() } .environmentObject(EventBus()) } diff --git a/Fyreplace/Views/Screens/RegisterScreen.swift b/Fyreplace/Views/Screens/RegisterScreen.swift index a1def1c..84159f4 100644 --- a/Fyreplace/Views/Screens/RegisterScreen.swift +++ b/Fyreplace/Views/Screens/RegisterScreen.swift @@ -1,8 +1,6 @@ import SwiftUI struct RegisterScreen: View, RegisterScreenProtocol { - let namespace: Namespace.ID - @EnvironmentObject var eventBus: EventBus @@ -24,12 +22,18 @@ struct RegisterScreen: View, RegisterScreenProtocol { @AppStorage("account.isWaitingForRandomCode") var isWaitingForRandomCode = false + @AppStorage("account.hasAcceptedTerms") + var hasAcceptedTerms = false + @AppStorage("account.isRegistering") var isRegistering = false @KeychainStorage("connection.token") var token + @Environment(\.config) + private var config + @FocusState private var focused: FocusedField? @@ -46,7 +50,7 @@ struct RegisterScreen: View, RegisterScreenProtocol { DynamicForm { Section { - EnvironmentPicker(namespace: namespace).disabled(isWaitingForRandomCode) + EnvironmentPicker().disabled(isWaitingForRandomCode) TextField( "Register.Username", @@ -59,7 +63,6 @@ struct RegisterScreen: View, RegisterScreenProtocol { .disabled(isWaitingForRandomCode) .submitLabel(.next) .onSubmit { focused = .email } - .matchedGeometryEffect(id: "first-field", in: namespace) #if !os(macOS) .textInputAutocapitalization(.never) .keyboardType(.asciiCapable) @@ -100,18 +103,8 @@ struct RegisterScreen: View, RegisterScreenProtocol { .keyboardType(.asciiCapable) #endif } - - SubmitOrCancel( - namespace: namespace, - submitLabel: "Register.Submit", - canSubmit: canSubmit, - canCancel: isWaitingForRandomCode, - isLoading: isLoading, - submitAction: submit, - cancelAction: cancel - ) } header: { - LogoHeader(namespace: namespace) { + LogoHeader { Image("Logo", label: Text("Logo")).resizable() } textContent: { Text("Register.Header") @@ -132,6 +125,34 @@ struct RegisterScreen: View, RegisterScreenProtocol { focused = .email } } + + Section { + Toggle("Register.TermsAcceptance", isOn: $hasAcceptedTerms) + .disabled(isWaitingForRandomCode) + + Link(destination: config.app.info.termsOfService) { + Label("App.Help.TermsOfService", systemImage: "shield") + } + .foregroundStyle(.tint) + + Link(destination: config.app.info.privacyPolicy) { + Label("App.Help.PrivacyPolicy", systemImage: "lock") + } + .foregroundStyle(.tint) + } footer: { + Spacer() + } + + Section { + SubmitOrCancel( + submitLabel: "Register.Submit", + canSubmit: canSubmit, + canCancel: isWaitingForRandomCode, + isLoading: isLoading, + submitAction: submit, + cancelAction: cancel + ) + } } .disabled(isLoading) .animation(.default, value: isWaitingForRandomCode) @@ -161,15 +182,9 @@ struct RegisterScreen: View, RegisterScreenProtocol { } } -@available(macOS 14.0, *) -@available(iOS 17.0, *) #Preview { - @Previewable - @Namespace - var namespace - NavigationStack { - RegisterScreen(namespace: namespace) + RegisterScreen() } .environmentObject(EventBus()) } diff --git a/Fyreplace/Views/Screens/RegisterScreenProtocol.swift b/Fyreplace/Views/Screens/RegisterScreenProtocol.swift index e20f52c..a4c3870 100644 --- a/Fyreplace/Views/Screens/RegisterScreenProtocol.swift +++ b/Fyreplace/Views/Screens/RegisterScreenProtocol.swift @@ -6,6 +6,7 @@ protocol RegisterScreenProtocol: LoadingViewProtocol { var email: String { get nonmutating set } var randomCode: String { get nonmutating set } var isWaitingForRandomCode: Bool { get nonmutating set } + var hasAcceptedTerms: Bool { get nonmutating set } var isRegistering: Bool { get nonmutating set } var token: String { get nonmutating set } } @@ -15,7 +16,7 @@ extension RegisterScreenProtocol { var isUsernameValid: Bool { 3...50 ~= username.count } var isEmailValid: Bool { 3...254 ~= email.count && email.contains("@") } var canSubmit: Bool { - !isLoading + !isLoading && hasAcceptedTerms && (isWaitingForRandomCode ? randomCode.count >= 8 : isUsernameValid && isEmailValid) } diff --git a/Fyreplace/Views/Screens/Screen.swift b/Fyreplace/Views/Screens/Screen.swift index 00ad4dc..217848d 100644 --- a/Fyreplace/Views/Screens/Screen.swift +++ b/Fyreplace/Views/Screens/Screen.swift @@ -3,9 +3,6 @@ import SwiftUI struct Screen: View { let destination: Destination - @Namespace - private var namespace - var body: some View { switch destination { case .feed: @@ -21,9 +18,9 @@ struct Screen: View { case .settings: SettingsScreen() case .login: - LoginScreen(namespace: namespace) + LoginScreen() case .register: - RegisterScreen(namespace: namespace) + RegisterScreen() } } } diff --git a/Fyreplace/Views/Screens/SettingsScreen.swift b/Fyreplace/Views/Screens/SettingsScreen.swift index f27d661..af87197 100644 --- a/Fyreplace/Views/Screens/SettingsScreen.swift +++ b/Fyreplace/Views/Screens/SettingsScreen.swift @@ -35,7 +35,7 @@ struct SettingsScreen: View, SettingsScreenProtocol { #endif } } header: { - LogoHeader(namespace: namespace) { + LogoHeader { EditableAvatar(user: currentUser, avatarSelected: updateAvatar) } textContent: { Text("Settings.Header") diff --git a/FyreplaceTests/Screens/RegisterScreenTests.swift b/FyreplaceTests/Screens/RegisterScreenTests.swift index 4994405..5910198 100644 --- a/FyreplaceTests/Screens/RegisterScreenTests.swift +++ b/FyreplaceTests/Screens/RegisterScreenTests.swift @@ -11,6 +11,7 @@ struct RegisterScreenTests { var email = "" var randomCode = "" var isWaitingForRandomCode = false + var hasAcceptedTerms = false var isRegistering = false var token = "" } @@ -18,7 +19,8 @@ struct RegisterScreenTests { @Test("Username must have correct length") func usernameMustHaveCorrectLength() { let screen = FakeScreen(eventBus: .init(), api: .fake()) - screen.email = "email@example" + screen.email = FakeClient.goodEmail + screen.hasAcceptedTerms = true for i in 0..<3 { screen.username = .init(repeating: "a", count: i) @@ -37,7 +39,8 @@ struct RegisterScreenTests { @Test("Email must have correct length") func emailMustHaveCorrectLength() { let screen = FakeScreen(eventBus: .init(), api: .fake()) - screen.username = "Example" + screen.username = FakeClient.goodUsername + screen.hasAcceptedTerms = true for i in 0..<3 { screen.email = .init(repeating: "@", count: i) @@ -56,13 +59,25 @@ struct RegisterScreenTests { @Test("Email must have @") func emailMustHaveAtSign() { let screen = FakeScreen(eventBus: .init(), api: .fake()) - screen.username = "Example" + screen.username = FakeClient.goodUsername screen.email = "email" + screen.hasAcceptedTerms = true #expect(!screen.canSubmit) screen.email = "email@example" #expect(screen.canSubmit) } + @Test("Terms must be accepted") + func termsMustBeAccepted() { + let screen = FakeScreen(eventBus: .init(), api: .fake()) + screen.username = FakeClient.goodUsername + screen.email = FakeClient.goodEmail + screen.hasAcceptedTerms = false + #expect(!screen.canSubmit) + screen.hasAcceptedTerms = true + #expect(screen.canSubmit) + } + @Test("Invalid username produces a failure") func invalidUsernameProducesFailure() async { let eventBus = StoringEventBus() @@ -103,8 +118,9 @@ struct RegisterScreenTests { let screen = FakeScreen(eventBus: .init(), api: .fake()) screen.username = FakeClient.goodUsername screen.email = FakeClient.goodEmail - screen.isWaitingForRandomCode = true screen.randomCode = "abcd123" + screen.isWaitingForRandomCode = true + screen.hasAcceptedTerms = true #expect(!screen.canSubmit) screen.randomCode = "abcd1234" #expect(screen.canSubmit)