From 17af26250901cf47e7e7aad1aba079ef3b873fed Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Fri, 18 Oct 2024 16:45:53 -0400 Subject: [PATCH 1/4] #1599: Account Success screen in onboarding --- Nos.xcodeproj/project.pbxproj | 4 + Nos/Assets/Localization/Localizable.xcstrings | 48 ++++++ Nos/Views/Onboarding/AccountSuccessView.swift | 150 ++++++++++++++++++ Nos/Views/Onboarding/DisplayNameView.swift | 1 + Nos/Views/Onboarding/OnboardingView.swift | 16 ++ Nos/Views/Onboarding/UsernameView.swift | 3 +- 6 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 Nos/Views/Onboarding/AccountSuccessView.swift diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 5012074a4..faddbb204 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -99,6 +99,7 @@ 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 */; }; + 03A241BD2CC2F458007EA31B /* AccountSuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A241BC2CC2F458007EA31B /* AccountSuccessView.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 */; }; @@ -658,6 +659,7 @@ 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 = ""; }; + 03A241BC2CC2F458007EA31B /* AccountSuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSuccessView.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 = ""; }; @@ -1482,6 +1484,7 @@ 3FB5E64F299D288E00386527 /* Onboarding */ = { isa = PBXGroup; children = ( + 03A241BC2CC2F458007EA31B /* AccountSuccessView.swift */, 030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */, 039F09582CC051EE00FEEC81 /* CreateAccountView.swift */, 030E570C2CC2A05B00A4A51E /* DisplayNameView.swift */, @@ -2495,6 +2498,7 @@ 5BCA95D22C8A5F0D00A52D1A /* PreviewEventRepository.swift in Sources */, C95D68A6299E6F9E00429F86 /* ProfileHeader.swift in Sources */, 0365CD872C4016A200622A1A /* EventKind.swift in Sources */, + 03A241BD2CC2F458007EA31B /* AccountSuccessView.swift in Sources */, C9BAB09B2996FBA10003A84E /* EventProcessor.swift in Sources */, C9B5C78E2C24AF650070445B /* MockRelaySubscriptionManager.swift in Sources */, C960C57129F3236200929990 /* LikeButton.swift in Sources */, diff --git a/Nos/Assets/Localization/Localizable.xcstrings b/Nos/Assets/Localization/Localizable.xcstrings index d12fd5ee2..33fa511fe 100644 --- a/Nos/Assets/Localization/Localizable.xcstrings +++ b/Nos/Assets/Localization/Localizable.xcstrings @@ -601,6 +601,30 @@ } } }, + "accountPartialSuccessDescription" : { + "comment" : "description for the account success screen when something isn't quite right", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The app couldn't save your chosen Display Name or Username. You can set these up later in your Profile." + } + } + } + }, + "accountPartialSuccessHeadline" : { + "comment" : "headline for the account success screen when something isn't quite right", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You've finished setting up your account, but..." + } + } + } + }, "accountsIFollow" : { "extractionState" : "manual", "localizations" : { @@ -654,6 +678,30 @@ } } }, + "accountSuccessDescription" : { + "comment" : "description for the account success screen when everything is right", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Now that you know who you are on Nostr, let’s find other people to follow!" + } + } + } + }, + "accountSuccessHeadline" : { + "comment" : "headline for the account success screen when everything is right", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You've finished setting up your account!" + } + } + } + }, "activity" : { "extractionState" : "manual", "localizations" : { diff --git a/Nos/Views/Onboarding/AccountSuccessView.swift b/Nos/Views/Onboarding/AccountSuccessView.swift new file mode 100644 index 000000000..c8ce69a74 --- /dev/null +++ b/Nos/Views/Onboarding/AccountSuccessView.swift @@ -0,0 +1,150 @@ +import Dependencies +import SwiftUI + +/// The Account Success view in the onboarding. +struct AccountSuccessView: 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) { + accountSuccessStack + + ScrollView { + accountSuccessStack + } + } + } + .navigationBarHidden(true) + } + + var accountSuccessStack: some View { + VStack(alignment: .leading, spacing: 20) { + Text(emoji) + .font(.system(size: 60)) + Text(headline) + .font(.clarityBold(.title)) + .foregroundStyle(Color.primaryTxt) + .padding(.bottom, 10) + CompletedStepsView(state) + .padding(.horizontal, 10) + Spacer() + Text(description) + .foregroundStyle(Color.secondaryTxt) + BigActionButton("next") { + state.step = .buildYourNetwork + } + } + .padding(40) + .readabilityPadding() + } + + var emoji: String { + state.allStepsSucceeded ? "🎉" : "🤔" + } + + var headline: LocalizedStringKey { + state.allStepsSucceeded ? "accountSuccessHeadline" : "accountPartialSuccessHeadline" + } + + var description: LocalizedStringKey { + state.allStepsSucceeded ? "accountSuccessDescription" : "accountPartialSuccessDescription" + } +} + +/// The four numbered steps with their corresponding text. +fileprivate struct CompletedStepsView: View { + let state: OnboardingState + + init(_ state: OnboardingState) { + self.state = state + } + + var body: some View { + VStack(alignment: .leading, spacing: 50) { + StepView(completed: true, label: "privateKeyHeadline") + StepView(completed: true, label: "publicKeyHeadline") + StepView(completed: state.displayNameSucceeded, label: "displayNameHeadline") + StepView(completed: state.usernameSucceeded, 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 StepView: View { + let completed: Bool + let label: LocalizedStringKey + + var body: some View { + HStack(alignment: .center, spacing: 20) { + Group { + if completed { + Image(systemName: "checkmark") + } else { + Image(systemName: "xmark") + } + } + .fontWeight(.bold) + .foregroundStyle(Color.primaryTxt) + .frame(width: 16) + .background( + Group { + if completed { + Circle() + .fill(LinearGradient.verticalAccentPrimary) + } else { + 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("All steps completed") { + var state = OnboardingState() + state.displayNameSucceeded = true + state.usernameSucceeded = true + + return AccountSuccessView() + .environment(state) + .inject(previewData: PreviewData()) +} + +#Preview("Some steps failed") { + AccountSuccessView() + .environment(OnboardingState()) + .inject(previewData: PreviewData()) +} diff --git a/Nos/Views/Onboarding/DisplayNameView.swift b/Nos/Views/Onboarding/DisplayNameView.swift index 7d7a8d712..82ea68bf0 100644 --- a/Nos/Views/Onboarding/DisplayNameView.swift +++ b/Nos/Views/Onboarding/DisplayNameView.swift @@ -79,6 +79,7 @@ struct DisplayNameView: View { do { try viewContext.save() try await currentUser.publishMetadata() + state.displayNameSucceeded = true nextStep() } catch { crashReporting.report(error) diff --git a/Nos/Views/Onboarding/OnboardingView.swift b/Nos/Views/Onboarding/OnboardingView.swift index 5b795fe54..4b23be2ae 100644 --- a/Nos/Views/Onboarding/OnboardingView.swift +++ b/Nos/Views/Onboarding/OnboardingView.swift @@ -1,5 +1,6 @@ import SwiftUI +/// The state of onboarding, tracking the flow, steps, and success or failure of a few specific tasks. @Observable final class OnboardingState { var flow: OnboardingFlow = .createAccount var step: OnboardingStep = .onboardingStart { @@ -8,6 +9,17 @@ import SwiftUI } } var path = NavigationPath() + + /// Whether the user succeeded in setting their display name + var displayNameSucceeded = false + + /// Whether the user succeeded in setting their username + var usernameSucceeded = false + + /// Whether the user succeeded in all steps of onboarding + var allStepsSucceeded: Bool { + displayNameSucceeded && usernameSucceeded + } } enum OnboardingFlow { @@ -24,6 +36,7 @@ enum OnboardingStep { case publicKey case displayName case username + case accountSuccess case buildYourNetwork case login } @@ -65,6 +78,9 @@ struct OnboardingView: View { case .username: UsernameView() .environment(state) + case .accountSuccess: + AccountSuccessView() + .environment(state) case .login: OnboardingLoginView(completion: completion) case .buildYourNetwork: diff --git a/Nos/Views/Onboarding/UsernameView.swift b/Nos/Views/Onboarding/UsernameView.swift index e22b61996..48bc160b7 100644 --- a/Nos/Views/Onboarding/UsernameView.swift +++ b/Nos/Views/Onboarding/UsernameView.swift @@ -119,7 +119,7 @@ struct UsernameView: View { } func nextStep() { - state.step = .buildYourNetwork + state.step = .accountSuccess } /// Checks whether the username is available and saves it. Updates `usernameState` based on the result. @@ -170,6 +170,7 @@ struct UsernameView: View { relays: relays ) usernameState = .claimed + state.usernameSucceeded = true nextStep() } catch { crashReporting.report(error) From bbb68de0436b49c2650692c63e8d73642b09846c Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Fri, 18 Oct 2024 16:48:09 -0400 Subject: [PATCH 2/4] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede67ba98..dbf0bdb6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) - Added the Username onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1598](https://github.com/planetary-social/nos/issues/1598) +- Added the Account Success onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1599](https://github.com/planetary-social/nos/issues/1599) ## [0.2.2] - 2024-10-11Z From 1ff706ab825950b82426fa42da82c6038dca7d17 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Fri, 18 Oct 2024 16:56:27 -0400 Subject: [PATCH 3/4] remove unneeded var --- Nos/Views/Onboarding/UsernameView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Nos/Views/Onboarding/UsernameView.swift b/Nos/Views/Onboarding/UsernameView.swift index 48bc160b7..8942f780d 100644 --- a/Nos/Views/Onboarding/UsernameView.swift +++ b/Nos/Views/Onboarding/UsernameView.swift @@ -22,7 +22,6 @@ struct UsernameView: View { @State private var username = "" @State private var usernameState: UsernameViewState = .idle - @State private var saveError: SaveProfileError? private var showAlert: Binding { Binding { From 5be391de520ffe8e965821742ac92b98d6e26c99 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Fri, 18 Oct 2024 17:00:19 -0400 Subject: [PATCH 4/4] fix alerts to use message instead of title --- Nos/Views/Onboarding/DisplayNameView.swift | 4 +++- Nos/Views/Onboarding/UsernameView.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Nos/Views/Onboarding/DisplayNameView.swift b/Nos/Views/Onboarding/DisplayNameView.swift index 82ea68bf0..604e38ac6 100644 --- a/Nos/Views/Onboarding/DisplayNameView.swift +++ b/Nos/Views/Onboarding/DisplayNameView.swift @@ -25,12 +25,14 @@ struct DisplayNameView: View { } } .navigationBarHidden(true) - .alert("errorConnecting", isPresented: $showError) { + .alert("", isPresented: $showError) { Button { nextStep() } label: { Text("skipForNow") } + } message: { + Text("errorConnecting") } } diff --git a/Nos/Views/Onboarding/UsernameView.swift b/Nos/Views/Onboarding/UsernameView.swift index 8942f780d..df8235d8d 100644 --- a/Nos/Views/Onboarding/UsernameView.swift +++ b/Nos/Views/Onboarding/UsernameView.swift @@ -56,12 +56,14 @@ struct UsernameView: View { } } .navigationBarHidden(true) - .alert("errorConnecting", isPresented: showAlert) { + .alert("", isPresented: showAlert) { Button { nextStep() } label: { Text("skipForNow") } + } message: { + Text("errorConnecting") } }