diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e447075..d5febe2bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Internal Changes - Migrate ObservableObject to @Observable where possible [#1458](https://github.com/planetary-social/nos/issues/1458) +- Added the Create Account onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1594](https://github.com/planetary-social/nos/issues/1594) ## [0.2.2] - 2024-10-11Z diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 285da9ce0..dd473f33e 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -91,6 +91,7 @@ 0393893D2CA49CE000698978 /* fonts-animated.gif in Resources */ = {isa = PBXBuildFile; fileRef = 0393893C2CA49CE000698978 /* fonts-animated.gif */; }; 039C961F2C480F4100A8EB39 /* unsupported_kinds.json in Resources */ = {isa = PBXBuildFile; fileRef = 039C961E2C480F4100A8EB39 /* unsupported_kinds.json */; }; 039C96292C48321E00A8EB39 /* long_form_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 039C96282C48321E00A8EB39 /* long_form_data.json */; }; + 039F09592CC051FF00FEEC81 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F09582CC051EE00FEEC81 /* CreateAccountView.swift */; }; 03A3AA3B2C5028FF008FE153 /* PublicKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A3AA3A2C5028FF008FE153 /* PublicKeyTests.swift */; }; 03A743452CC048C700893CAE /* GoToFeedTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A743442CC048C700893CAE /* GoToFeedTip.swift */; }; 03B4E6A22C125CA1006E5F59 /* nostr_build_nip96_upload_response.json in Resources */ = {isa = PBXBuildFile; fileRef = 03B4E6A12C125CA1006E5F59 /* nostr_build_nip96_upload_response.json */; }; @@ -641,6 +642,7 @@ 0393893C2CA49CE000698978 /* fonts-animated.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "fonts-animated.gif"; sourceTree = ""; }; 039C961E2C480F4100A8EB39 /* unsupported_kinds.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = unsupported_kinds.json; sourceTree = ""; }; 039C96282C48321E00A8EB39 /* long_form_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = long_form_data.json; sourceTree = ""; }; + 039F09582CC051EE00FEEC81 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = ""; }; 03A3AA3A2C5028FF008FE153 /* PublicKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyTests.swift; sourceTree = ""; }; 03A743442CC048C700893CAE /* GoToFeedTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoToFeedTip.swift; sourceTree = ""; }; 03AB2F7D2BF6609500B73DB1 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; @@ -1456,6 +1458,7 @@ isa = PBXGroup; children = ( 030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */, + 039F09582CC051EE00FEEC81 /* CreateAccountView.swift */, 3F30020629C237AB003D4F8B /* OnboardingAgeVerificationView.swift */, 3F30020C29C382EB003D4F8B /* OnboardingLoginView.swift */, 3F30020829C23895003D4F8B /* OnboardingNotOldEnoughView.swift */, @@ -2297,6 +2300,7 @@ 5BFF66B62A58A8A000AA79DD /* MutesView.swift in Sources */, C913DA0A2AEAF52B003BDD6D /* NoteWarningController.swift in Sources */, 3F30020929C23895003D4F8B /* OnboardingNotOldEnoughView.swift in Sources */, + 039F09592CC051FF00FEEC81 /* CreateAccountView.swift in Sources */, 042406F32C907A15008F2A21 /* NosToggle.swift in Sources */, 03B4E6AE2C125D61006E5F59 /* FileStorageUploadResponseJSON.swift in Sources */, 5B79F6112B98AD0A002DA9BE /* ExcellentChoiceSheet.swift in Sources */, diff --git a/Nos/Assets/Colors.xcassets/numbered-step-background.colorset/Contents.json b/Nos/Assets/Colors.xcassets/numbered-step-background.colorset/Contents.json new file mode 100644 index 000000000..bb7f36785 --- /dev/null +++ b/Nos/Assets/Colors.xcassets/numbered-step-background.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x5B", + "green" : "0x25", + "red" : "0x37" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nos/Assets/Localization/Localizable.xcstrings b/Nos/Assets/Localization/Localizable.xcstrings index 34ed42fb6..b65461f57 100644 --- a/Nos/Assets/Localization/Localizable.xcstrings +++ b/Nos/Assets/Localization/Localizable.xcstrings @@ -4030,6 +4030,42 @@ } } }, + "createAccountButton" : { + "comment" : "The title of the button to create an account", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Create Account" + } + } + } + }, + "createAccountDescription" : { + "comment" : "The description on the create account screen", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We've simplified management of your account credentials. Nos uses public-private key encryption to verify your identity." + } + } + } + }, + "createAccountHeadline" : { + "comment" : "headline for the create account screen", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Say goodbye to usernames and passwords!" + } + } + } + }, "dayAbbreviated" : { "extractionState" : "manual", "localizations" : { @@ -5164,6 +5200,18 @@ } } }, + "displayNameHeadline" : { + "comment" : "headline for the display name screen in onboarding", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display Name" + } + } + } + }, "done" : { "extractionState" : "manual", "localizations" : { @@ -5461,89 +5509,6 @@ } } }, - "enableAccountDeletion" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enable account deletion" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Activar eliminación de cuenta" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "계정 삭제 활성화" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "启用账户删除" - } - } - } - }, - "enableNewMediaDisplay" : { - "comment" : "Setting for new media feature flag", - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Neue Mediendarstellung aktivieren" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enable new media display" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Activar nuevo visor de medios" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Activer l'affichage des nouveaux médias" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "새로운 미디어 디스플레이 사용" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Washa onyesho jipya la midia" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "启用新的媒体显示" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "啓用新的媒體顯示" - } - } - } - }, "enableNewModerationFlow" : { "extractionState" : "manual", "localizations" : { @@ -14115,6 +14080,18 @@ } } }, + "privateKeyHeadline" : { + "comment" : "headline for the private key screen in onboarding", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Private Key" + } + } + } + }, "privateKeyPlaceholder" : { "extractionState" : "manual", "localizations" : { @@ -14554,6 +14531,18 @@ } } }, + "publicKeyHeadline" : { + "comment" : "headline for the public key screen in onboarding", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Public Key" + } + } + } + }, "quote" : { "extractionState" : "manual", "localizations" : { @@ -19025,6 +19014,18 @@ } } }, + "usernameHeadline" : { + "comment" : "headline for the username screen in onboarding", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Username" + } + } + } + }, "usernameWarningMessage" : { "extractionState" : "manual", "localizations" : { diff --git a/Nos/Service/FeatureFlags.swift b/Nos/Service/FeatureFlags.swift index 3a1b70dca..2040f4880 100644 --- a/Nos/Service/FeatureFlags.swift +++ b/Nos/Service/FeatureFlags.swift @@ -7,6 +7,11 @@ enum FeatureFlag { /// Whether the new moderation flow should be enabled or not. /// - Note: See [#1489](https://github.com/planetary-social/nos/issues/1489) for details on the new moderation flow. case newModerationFlow + + /// Whether the new onboarding flow should be enabled or not. + /// - Note: See [Figma](https://www.figma.com/design/6MeujQUXzC1AuviHEHCs0J/Nos---In-Progress?node-id=9221-8504) + /// for the new flow. + case newOnboardingFlow } /// The set of feature flags used by the app. @@ -31,6 +36,7 @@ protocol FeatureFlags { /// Feature flags and their values. private var featureFlags: [FeatureFlag: Bool] = [ .newModerationFlow: false, + .newOnboardingFlow: false ] /// Returns true if the feature is enabled. diff --git a/Nos/Views/Onboarding/CreateAccountView.swift b/Nos/Views/Onboarding/CreateAccountView.swift new file mode 100644 index 000000000..07262355c --- /dev/null +++ b/Nos/Views/Onboarding/CreateAccountView.swift @@ -0,0 +1,115 @@ +import Dependencies +import SwiftUI + +/// The Create Account view in the onboarding. +struct CreateAccountView: View { + @Environment(OnboardingState.self) private var state + @Environment(CurrentUser.self) var currentUser + + @Dependency(\.crashReporting) private var crashReporting + + var body: some View { + ZStack { + Color.appBg + .ignoresSafeArea() + ViewThatFits(in: .vertical) { + createAccountStack + + ScrollView { + createAccountStack + } + } + } + .navigationBarHidden(true) + } + + var createAccountStack: some View { + VStack(alignment: .leading, spacing: 20) { + Text("👋") + .font(.system(size: 60)) + Text("createAccountHeadline") + .font(.clarityBold(.title)) + .foregroundStyle(Color.primaryTxt) + Text("createAccountDescription") + .font(.body) + .foregroundStyle(Color.secondaryTxt) + .padding(.bottom, 10) + NumberedStepsView() + .padding(.horizontal, 10) + Spacer() + BigActionButton(title: "createAccountButton") { + do { + try await currentUser.createAccount() + } catch { + crashReporting.report(error) + } + state.step = .buildYourNetwork + } + } + .padding(40) + .readabilityPadding() + } +} + +/// The four numbered steps with their corresponding text. +fileprivate struct NumberedStepsView: View { + var body: some View { + VStack(alignment: .leading, spacing: 50) { + NumberedStepView(index: 1, label: "privateKeyHeadline") + NumberedStepView(index: 2, label: "publicKeyHeadline") + NumberedStepView(index: 3, label: "displayNameHeadline") + NumberedStepView(index: 4, label: "usernameHeadline") + } + .background( + ConnectingLine() + .offset(x: 8) + .stroke(Color.numberedStepBackground, lineWidth: 4), + alignment: .leading + ) + } +} + +/// A view containing an index with a circle background and some text. +fileprivate struct NumberedStepView: View { + let index: Int + let label: LocalizedStringKey + + var body: some View { + HStack(alignment: .center, spacing: 20) { + Text(index, format: .number) + .font(.clarityBold(.headline)) + .foregroundStyle(Color.primaryTxt) + .frame(width: 16) + .background( + Circle() + .fill(Color.numberedStepBackground) + .frame(width: 30, height: 30) + ) + + Text(label) + .font(.headline) + .fontWeight(.bold) + .foregroundStyle(Color.primaryTxt) + } + } +} + +/// Custom shape for the vertical connecting line +fileprivate struct ConnectingLine: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let startPoint = CGPoint(x: rect.minX, y: 0) + let endPoint = CGPoint(x: rect.minX, y: rect.maxY) + + path.move(to: startPoint) + path.addLine(to: endPoint) + + return path + } +} + +#Preview { + CreateAccountView() + .environment(OnboardingState()) + .inject(previewData: PreviewData()) +} diff --git a/Nos/Views/Onboarding/OnboardingAgeVerificationView.swift b/Nos/Views/Onboarding/OnboardingAgeVerificationView.swift index 7c4bcad0c..52231925b 100644 --- a/Nos/Views/Onboarding/OnboardingAgeVerificationView.swift +++ b/Nos/Views/Onboarding/OnboardingAgeVerificationView.swift @@ -4,9 +4,10 @@ import SwiftUI /// The Age Verification view in the onboarding. struct OnboardingAgeVerificationView: View { @Environment(OnboardingState.self) private var state + @Environment(CurrentUser.self) var currentUser @Dependency(\.crashReporting) private var crashReporting - @Dependency(\.currentUser) private var currentUser + @Dependency(\.featureFlags) private var featureFlags var body: some View { VStack { @@ -31,15 +32,12 @@ struct OnboardingAgeVerificationView: View { if state.flow == .loginToExistingAccount { state.step = .login } else { - // temporary; this will eventually move to the Create Account screen - do { - try await currentUser.createAccount() - } catch { - crashReporting.report(error) + if featureFlags.isEnabled(.newOnboardingFlow) { + state.step = .createAccount + } else { + await createAccount() + state.step = .buildYourNetwork } - // end temporary account creation - - state.step = .buildYourNetwork } } } @@ -49,4 +47,14 @@ struct OnboardingAgeVerificationView: View { .background(Color.appBg) .navigationBarHidden(true) } + + /// Create an account, logging any error to the crash reporting service. + /// - Note: This is a temporary solution for this screen and will eventually move to the Create Account screen. + func createAccount() async { + do { + try await currentUser.createAccount() + } catch { + crashReporting.report(error) + } + } } diff --git a/Nos/Views/Onboarding/OnboardingNotOldEnoughView.swift b/Nos/Views/Onboarding/OnboardingNotOldEnoughView.swift index 4b95fb282..0567e6d21 100644 --- a/Nos/Views/Onboarding/OnboardingNotOldEnoughView.swift +++ b/Nos/Views/Onboarding/OnboardingNotOldEnoughView.swift @@ -2,7 +2,7 @@ import SwiftUI struct OnboardingNotOldEnoughView: View { @Environment(OnboardingState.self) private var state - + var body: some View { VStack { Text(.localizable.notOldEnoughTitle) diff --git a/Nos/Views/Onboarding/OnboardingView.swift b/Nos/Views/Onboarding/OnboardingView.swift index a49d94802..d9d23092c 100644 --- a/Nos/Views/Onboarding/OnboardingView.swift +++ b/Nos/Views/Onboarding/OnboardingView.swift @@ -19,6 +19,7 @@ enum OnboardingStep { case onboardingStart case ageVerification case notOldEnough + case createAccount case buildYourNetwork case login } @@ -45,6 +46,9 @@ struct OnboardingView: View { case .notOldEnough: OnboardingNotOldEnoughView() .environment(state) + case .createAccount: + CreateAccountView() + .environment(state) case .login: OnboardingLoginView(completion: completion) case .buildYourNetwork: diff --git a/Nos/Views/Settings/SettingsView.swift b/Nos/Views/Settings/SettingsView.swift index 581c720f0..68f9993ad 100644 --- a/Nos/Views/Settings/SettingsView.swift +++ b/Nos/Views/Settings/SettingsView.swift @@ -301,6 +301,19 @@ extension SettingsView { private var newModerationFlowToggle: some View { NosToggle(isOn: isNewModerationFlowEnabled, labelText: .localizable.enableNewModerationFlow) } + + /// Whether the new onboarding flow is enabled. + private var isNewOnboardingFlowEnabled: Binding { + Binding( + get: { featureFlags.isEnabled(.newOnboardingFlow) }, + set: { featureFlags.setFeature(.newOnboardingFlow, enabled: $0) } + ) + } + + /// A toggle for the new moderation flow that allows the user to turn the feature on or off. + private var newOnboardingFlowToggle: some View { + NosToggle(isOn: isNewOnboardingFlowEnabled, labelText: "New Onboarding Flow") + } } #endif @@ -310,6 +323,7 @@ extension SettingsView { @MainActor private var stagingControls: some View { Group { newModerationFlowToggle + newOnboardingFlowToggle } } } @@ -321,6 +335,7 @@ extension SettingsView { @MainActor private var debugControls: some View { Group { newModerationFlowToggle + newOnboardingFlowToggle Text(.localizable.sampleDataInstructions) .foregroundColor(.primaryTxt) Button(String(localized: .localizable.loadSampleData)) { diff --git a/NosTests/Service/MockFeatureFlags.swift b/NosTests/Service/MockFeatureFlags.swift index 7d2eb5969..d64fc6a2e 100644 --- a/NosTests/Service/MockFeatureFlags.swift +++ b/NosTests/Service/MockFeatureFlags.swift @@ -3,6 +3,7 @@ class MockFeatureFlags: FeatureFlags { /// Mock feature flags and their values. private var featureFlags: [FeatureFlag: Bool] = [ .newModerationFlow: false, + .newOnboardingFlow: true ] func isEnabled(_ feature: FeatureFlag) -> Bool {