From 42441fd9ba9ee4676657a088171c523489776175 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Fri, 18 Oct 2024 10:56:44 -0400 Subject: [PATCH 1/4] #1597: Display Name screen in onboarding --- Nos.xcodeproj/project.pbxproj | 8 ++ .../Contents.json | 20 ++++ Nos/Assets/Localization/Localizable.xcstrings | 24 ++++ Nos/Service/SaveProfileError.swift | 16 +++ Nos/Views/Onboarding/DisplayNameView.swift | 108 ++++++++++++++++++ Nos/Views/Onboarding/OnboardingView.swift | 4 + Nos/Views/Onboarding/PublicKeyView.swift | 2 +- Nos/Views/Profile/Edit/ProfileEditView.swift | 20 +--- 8 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 Nos/Assets/Colors.xcassets/text-field-placeholder.colorset/Contents.json create mode 100644 Nos/Service/SaveProfileError.swift create mode 100644 Nos/Views/Onboarding/DisplayNameView.swift diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 984826b26..8654ffd2c 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 030E56CA2CC1BC6200A4A51E /* PublicKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E56C92CC1BC6200A4A51E /* PublicKeyView.swift */; }; 030E56E42CC1BF2900A4A51E /* CopyButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E56E32CC1BF2900A4A51E /* CopyButtonState.swift */; }; 030E56F32CC2836D00A4A51E /* CopyKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E56F22CC2836D00A4A51E /* CopyKeyView.swift */; }; + 030E570D2CC2A05B00A4A51E /* DisplayNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E570C2CC2A05B00A4A51E /* DisplayNameView.swift */; }; + 030E571B2CC2ADDB00A4A51E /* SaveProfileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E571A2CC2ADDB00A4A51E /* SaveProfileError.swift */; }; 030FECAB2CB5E0B900820014 /* BuildYourNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */; }; 0314CF742C9C7DD00001A53B /* youTube_fortnight_short.html in Resources */ = {isa = PBXBuildFile; fileRef = 0314CF732C9C7DD00001A53B /* youTube_fortnight_short.html */; }; 0314D5AC2C7D31060002E7F4 /* MediaService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0314D5AB2C7D31060002E7F4 /* MediaService.swift */; }; @@ -595,6 +597,8 @@ 030E56C92CC1BC6200A4A51E /* PublicKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyView.swift; sourceTree = ""; }; 030E56E32CC1BF2900A4A51E /* CopyButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyButtonState.swift; sourceTree = ""; }; 030E56F22CC2836D00A4A51E /* CopyKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyKeyView.swift; sourceTree = ""; }; + 030E570C2CC2A05B00A4A51E /* DisplayNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameView.swift; sourceTree = ""; }; + 030E571A2CC2ADDB00A4A51E /* SaveProfileError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveProfileError.swift; sourceTree = ""; }; 030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildYourNetworkView.swift; sourceTree = ""; }; 0314CF732C9C7DD00001A53B /* youTube_fortnight_short.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = youTube_fortnight_short.html; sourceTree = ""; }; 0314D5AB2C7D31060002E7F4 /* MediaService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaService.swift; sourceTree = ""; }; @@ -1478,6 +1482,7 @@ children = ( 030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */, 039F09582CC051EE00FEEC81 /* CreateAccountView.swift */, + 030E570C2CC2A05B00A4A51E /* DisplayNameView.swift */, 3F30020629C237AB003D4F8B /* OnboardingAgeVerificationView.swift */, 3F30020C29C382EB003D4F8B /* OnboardingLoginView.swift */, 3F30020829C23895003D4F8B /* OnboardingNotOldEnoughView.swift */, @@ -1658,6 +1663,7 @@ 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */, C936B4612A4CB01C00DF1EB9 /* PushNotificationService.swift */, C9A8015D2BD0177D006E29B2 /* ReportPublisher.swift */, + 030E571A2CC2ADDB00A4A51E /* SaveProfileError.swift */, 5B8805192A21027C00E21F06 /* SHA256Key.swift */, C9B678E029EEC41000303F33 /* SocialGraphCache.swift */, C9F64D8B29ED840700563F2B /* Zipper.swift */, @@ -2351,6 +2357,7 @@ C92E7F6D2C4EFF9B00B80638 /* WebSocketState.swift in Sources */, C95D68AB299E710F00429F86 /* Color+Hex.swift in Sources */, 037975EA2C0E695A00ADDF37 /* MockFeatureFlags.swift in Sources */, + 030E571B2CC2ADDB00A4A51E /* SaveProfileError.swift in Sources */, C94A5E182A72C84200B6EC5D /* ReportCategory.swift in Sources */, C9A8015E2BD0177D006E29B2 /* ReportPublisher.swift in Sources */, C9F0BB6B29A503D6000547FC /* PublicKey.swift in Sources */, @@ -2549,6 +2556,7 @@ C905B0772A619E99009B8A78 /* LPLinkViewRepresentable.swift in Sources */, C95D68A7299E6FF000429F86 /* KeyFixture.swift in Sources */, 0304D0B22C9B731F001D16C7 /* MockOpenGraphService.swift in Sources */, + 030E570D2CC2A05B00A4A51E /* DisplayNameView.swift in Sources */, C94437E629B0DB83004D8C86 /* NotificationsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Nos/Assets/Colors.xcassets/text-field-placeholder.colorset/Contents.json b/Nos/Assets/Colors.xcassets/text-field-placeholder.colorset/Contents.json new file mode 100644 index 000000000..b08220da1 --- /dev/null +++ b/Nos/Assets/Colors.xcassets/text-field-placeholder.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.500", + "blue" : "0xD8", + "green" : "0x88", + "red" : "0xA5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nos/Assets/Localization/Localizable.xcstrings b/Nos/Assets/Localization/Localizable.xcstrings index 1c628210d..e7c59c85f 100644 --- a/Nos/Assets/Localization/Localizable.xcstrings +++ b/Nos/Assets/Localization/Localizable.xcstrings @@ -5224,6 +5224,18 @@ } } }, + "displayNameDescription" : { + "comment" : "a description for the display name screen in onboarding", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This is how others will see you when they view your notes. You can always change it." + } + } + } + }, "displayNameHeadline" : { "comment" : "headline for the display name screen in onboarding", "extractionState" : "manual", @@ -5236,6 +5248,18 @@ } } }, + "displayNamePlaceholder" : { + "comment" : "placeholder text for the display name field in onboarding", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your display name" + } + } + } + }, "done" : { "extractionState" : "manual", "localizations" : { diff --git a/Nos/Service/SaveProfileError.swift b/Nos/Service/SaveProfileError.swift new file mode 100644 index 000000000..bf66f08cc --- /dev/null +++ b/Nos/Service/SaveProfileError.swift @@ -0,0 +1,16 @@ +import Foundation + +/// An error that may occur while saving a profile. +enum SaveProfileError: LocalizedError { + case unexpectedError + case unableToPublishChanges + + var errorDescription: String? { + switch self { + case .unexpectedError: + return "Something unexpected happened" + case .unableToPublishChanges: + return "We were unable to publish your changes to the network." + } + } +} diff --git a/Nos/Views/Onboarding/DisplayNameView.swift b/Nos/Views/Onboarding/DisplayNameView.swift new file mode 100644 index 000000000..becc22bcd --- /dev/null +++ b/Nos/Views/Onboarding/DisplayNameView.swift @@ -0,0 +1,108 @@ +import Dependencies +import SwiftUI + +/// The Display Name view in the onboarding. +struct DisplayNameView: View { + @Environment(OnboardingState.self) private var state + @Environment(CurrentUser.self) private var currentUser + @Environment(\.managedObjectContext) private var viewContext + + @Dependency(\.crashReporting) private var crashReporting + + @State private var displayName = "" + @State private var saveError: SaveProfileError? + + private var showAlert: Binding { + Binding { + saveError != nil + } set: { _ in + saveError = nil + } + } + + var body: some View { + ZStack { + Color.appBg + .ignoresSafeArea() + ViewThatFits(in: .vertical) { + displayNameStack + + ScrollView { + displayNameStack + } + } + } + .navigationBarHidden(true) + .alert(isPresented: showAlert, error: saveError) { + Button { + saveError = nil + Task { + await save() + } + } label: { + Text("retry") + } + Button { + saveError = nil + } label: { + Text("cancel") + } + } + } + + var displayNameStack: some View { + VStack(alignment: .leading, spacing: 20) { + LargeNumberView(3) + Text("displayNameHeadline") + .font(.clarityBold(.title)) + .foregroundStyle(Color.primaryTxt) + Text("displayNameDescription") + .font(.body) + .foregroundStyle(Color.secondaryTxt) + TextField( + "", + text: $displayName, + prompt: Text("displayNamePlaceholder") + .foregroundStyle(Color.textFieldPlaceholder) + ) + .textInputAutocapitalization(.none) + .foregroundStyle(Color.primaryTxt) + .fontWeight(.bold) + .autocorrectionDisabled() + .padding() + .withStyledBorder() + Spacer() + BigActionButton("next") { + await save() + } + } + .padding(40) + .readabilityPadding() + } + + /// Saves the display name locally and publishes the event to relays. Sets `saveError` if it fails. + func save() async { + guard let author = await currentUser.author else { + saveError = SaveProfileError.unexpectedError + return + } + + author.displayName = displayName + do { + try viewContext.save() + try await currentUser.publishMetadata() + state.step = .buildYourNetwork + } catch CurrentUserError.errorWhilePublishingToRelays { + saveError = SaveProfileError.unableToPublishChanges + } catch { + crashReporting.report(error) + saveError = SaveProfileError.unexpectedError + } + } +} + +#Preview { + DisplayNameView() + .environment(OnboardingState()) + .inject(previewData: PreviewData()) +} diff --git a/Nos/Views/Onboarding/OnboardingView.swift b/Nos/Views/Onboarding/OnboardingView.swift index 7cc9ea1da..0030dbd0e 100644 --- a/Nos/Views/Onboarding/OnboardingView.swift +++ b/Nos/Views/Onboarding/OnboardingView.swift @@ -22,6 +22,7 @@ enum OnboardingStep { case createAccount case privateKey case publicKey + case displayName case buildYourNetwork case login } @@ -57,6 +58,9 @@ struct OnboardingView: View { case .publicKey: PublicKeyView() .environment(state) + case .displayName: + DisplayNameView() + .environment(state) case .login: OnboardingLoginView(completion: completion) case .buildYourNetwork: diff --git a/Nos/Views/Onboarding/PublicKeyView.swift b/Nos/Views/Onboarding/PublicKeyView.swift index 91f58f2c3..2e07af803 100644 --- a/Nos/Views/Onboarding/PublicKeyView.swift +++ b/Nos/Views/Onboarding/PublicKeyView.swift @@ -44,7 +44,7 @@ struct PublicKeyView: View { CopyKeyView("copyPublicKey", keyString: $publicKeyString, copyButtonState: $copyButtonState) Spacer() BigActionButton("next") { - state.step = .buildYourNetwork + state.step = .displayName } } .padding(40) diff --git a/Nos/Views/Profile/Edit/ProfileEditView.swift b/Nos/Views/Profile/Edit/ProfileEditView.swift index 6bf48381e..3b44d9068 100644 --- a/Nos/Views/Profile/Edit/ProfileEditView.swift +++ b/Nos/Views/Profile/Edit/ProfileEditView.swift @@ -24,7 +24,7 @@ struct ProfileEditView: View { @State private var website: String = "" @State private var showNIP05Wizard = false @State private var showConfirmationDialog = false - @State private var saveError: SaveError? + @State private var saveError: SaveProfileError? @State private var isUploadingPhoto = false @@ -144,24 +144,10 @@ struct ProfileEditView: View { // Go back to profile page router.pop() } catch CurrentUserError.errorWhilePublishingToRelays { - saveError = SaveError.unableToPublishChanges + saveError = SaveProfileError.unableToPublishChanges } catch { crashReporting.report(error) - saveError = SaveError.unexpectedError - } - } - - enum SaveError: LocalizedError { - case unexpectedError - case unableToPublishChanges - - var errorDescription: String? { - switch self { - case .unexpectedError: - return "Something unexpected happened" - case .unableToPublishChanges: - return "We were unable to publish your changes in the network" - } + saveError = SaveProfileError.unexpectedError } } } From 7762c766a96ef7ab1334a2e208878bf988793f9a Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Fri, 18 Oct 2024 11:00:01 -0400 Subject: [PATCH 2/4] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c33c58506..de57804f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the Create Account onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1594](https://github.com/planetary-social/nos/issues/1594) - Added the Private Key onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1595](https://github.com/planetary-social/nos/issues/1595) - Added the Public Key onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1596](https://github.com/planetary-social/nos/issues/1596) +- Added the Display Name onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1597](https://github.com/planetary-social/nos/issues/1597) ## [0.2.2] - 2024-10-11Z From 2abd04a8085a814be1c127fe03c2f38a126da83b Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Fri, 18 Oct 2024 15:12:59 -0400 Subject: [PATCH 3/4] show error message and allow the user to skip --- Nos/Assets/Localization/Localizable.xcstrings | 24 ++++++++++++ Nos/Views/Onboarding/DisplayNameView.swift | 38 ++++++------------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/Nos/Assets/Localization/Localizable.xcstrings b/Nos/Assets/Localization/Localizable.xcstrings index e7c59c85f..a3fcf0d92 100644 --- a/Nos/Assets/Localization/Localizable.xcstrings +++ b/Nos/Assets/Localization/Localizable.xcstrings @@ -5236,6 +5236,18 @@ } } }, + "displayNameError" : { + "comment" : "error message for when we can't set the display name in onboarding", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "There is a problem connecting to the servers. You can skip for now and visit your Profile to update later." + } + } + } + }, "displayNameHeadline" : { "comment" : "headline for the display name screen in onboarding", "extractionState" : "manual", @@ -17516,6 +17528,18 @@ } } }, + "skipForNow" : { + "comment" : "button title that allows the user to skip this for now", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip for now" + } + } + } + }, "software" : { "extractionState" : "manual", "localizations" : { diff --git a/Nos/Views/Onboarding/DisplayNameView.swift b/Nos/Views/Onboarding/DisplayNameView.swift index becc22bcd..83e28ab06 100644 --- a/Nos/Views/Onboarding/DisplayNameView.swift +++ b/Nos/Views/Onboarding/DisplayNameView.swift @@ -10,15 +10,7 @@ struct DisplayNameView: View { @Dependency(\.crashReporting) private var crashReporting @State private var displayName = "" - @State private var saveError: SaveProfileError? - - private var showAlert: Binding { - Binding { - saveError != nil - } set: { _ in - saveError = nil - } - } + @State private var showError: Bool = false var body: some View { ZStack { @@ -33,19 +25,11 @@ struct DisplayNameView: View { } } .navigationBarHidden(true) - .alert(isPresented: showAlert, error: saveError) { - Button { - saveError = nil - Task { - await save() - } - } label: { - Text("retry") - } + .alert("displayNameError", isPresented: $showError) { Button { - saveError = nil + nextStep() } label: { - Text("cancel") + Text("skipForNow") } } } @@ -79,11 +63,15 @@ struct DisplayNameView: View { .padding(40) .readabilityPadding() } - + + func nextStep() { + state.step = .buildYourNetwork + } + /// Saves the display name locally and publishes the event to relays. Sets `saveError` if it fails. func save() async { guard let author = await currentUser.author else { - saveError = SaveProfileError.unexpectedError + showError = true return } @@ -91,12 +79,10 @@ struct DisplayNameView: View { do { try viewContext.save() try await currentUser.publishMetadata() - state.step = .buildYourNetwork - } catch CurrentUserError.errorWhilePublishingToRelays { - saveError = SaveProfileError.unableToPublishChanges + nextStep() } catch { crashReporting.report(error) - saveError = SaveProfileError.unexpectedError + showError = true } } } From 6b32a0ae3b2298e3422e8efffec15cdcac36cdaf Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Fri, 18 Oct 2024 15:32:07 -0400 Subject: [PATCH 4/4] fix doc comment Co-authored-by: Matt Lorentz --- Nos/Views/Onboarding/DisplayNameView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nos/Views/Onboarding/DisplayNameView.swift b/Nos/Views/Onboarding/DisplayNameView.swift index 83e28ab06..62c95f95a 100644 --- a/Nos/Views/Onboarding/DisplayNameView.swift +++ b/Nos/Views/Onboarding/DisplayNameView.swift @@ -68,7 +68,7 @@ struct DisplayNameView: View { state.step = .buildYourNetwork } - /// Saves the display name locally and publishes the event to relays. Sets `saveError` if it fails. + /// Saves the display name locally and publishes the event to relays. Sets `showError` if it fails. func save() async { guard let author = await currentUser.author else { showError = true