Skip to content

Commit

Permalink
Merge pull request #1665 from planetary-social/onboarding-username
Browse files Browse the repository at this point in the history
#1598: Username screen in onboarding
  • Loading branch information
joshuatbrown authored Oct 18, 2024
2 parents 496bf7e + 82fb793 commit 6905591
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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)
- Added the Username onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1598](https://github.com/planetary-social/nos/issues/1598)

## [0.2.2] - 2024-10-11Z

Expand Down
4 changes: 4 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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 */; };
030E57292CC2B0D100A4A51E /* UsernameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E57282CC2B0D100A4A51E /* UsernameView.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 */; };
Expand Down Expand Up @@ -599,6 +600,7 @@
030E56F22CC2836D00A4A51E /* CopyKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyKeyView.swift; sourceTree = "<group>"; };
030E570C2CC2A05B00A4A51E /* DisplayNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameView.swift; sourceTree = "<group>"; };
030E571A2CC2ADDB00A4A51E /* SaveProfileError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveProfileError.swift; sourceTree = "<group>"; };
030E57282CC2B0D100A4A51E /* UsernameView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsernameView.swift; sourceTree = "<group>"; };
030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildYourNetworkView.swift; sourceTree = "<group>"; };
0314CF732C9C7DD00001A53B /* youTube_fortnight_short.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = youTube_fortnight_short.html; sourceTree = "<group>"; };
0314D5AB2C7D31060002E7F4 /* MediaService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1490,6 +1492,7 @@
3FB5E650299D28A200386527 /* OnboardingView.swift */,
038EF09C2CC16D640031F7F2 /* PrivateKeyView.swift */,
030E56C92CC1BC6200A4A51E /* PublicKeyView.swift */,
030E57282CC2B0D100A4A51E /* UsernameView.swift */,
030E56E52CC2835A00A4A51E /* Components */,
);
path = Onboarding;
Expand Down Expand Up @@ -2334,6 +2337,7 @@
03B4E6AE2C125D61006E5F59 /* FileStorageUploadResponseJSON.swift in Sources */,
5B79F6112B98AD0A002DA9BE /* ExcellentChoiceSheet.swift in Sources */,
C973AB612A323167002AED16 /* Author+CoreDataProperties.swift in Sources */,
030E57292CC2B0D100A4A51E /* UsernameView.swift in Sources */,
C9B678E129EEC41000303F33 /* SocialGraphCache.swift in Sources */,
034EBDBA2C24895E006BA35A /* CurrentUserError.swift in Sources */,
C93F488D2AC5C30C00900CEC /* NosFormField.swift in Sources */,
Expand Down
20 changes: 20 additions & 0 deletions Nos/Assets/Colors.xcassets/error.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x5D",
"green" : "0x19",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
72 changes: 60 additions & 12 deletions Nos/Assets/Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -5236,18 +5236,6 @@
}
}
},
"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",
Expand Down Expand Up @@ -5753,6 +5741,18 @@
}
}
},
"errorConnecting" : {
"comment" : "error message for when we can't set the display name or username 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."
}
}
}
},
"eventSource" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -19106,6 +19106,18 @@
}
}
},
"usernameDescription" : {
"comment" : "a description for the username screen in onboarding",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This is an easy to share alternative to your public key and allows people to search for you on Nos."
}
}
}
},
"usernameHeadline" : {
"comment" : "headline for the username screen in onboarding",
"extractionState" : "manual",
Expand All @@ -19118,6 +19130,42 @@
}
}
},
"usernameNIP05Parenthetical" : {
"comment" : "the word \"NIP-05\" in parentheses in English. \"NIP-05\" should not be translated, but the parentheses may",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "(NIP-05)"
}
}
}
},
"usernameNotAvailable" : {
"comment" : "error text to display when the username is unavailable",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This username is not available."
}
}
}
},
"usernamePlaceholder" : {
"comment" : "placeholder text for the username field in onboarding",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "username"
}
}
}
},
"usernameWarningMessage" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
6 changes: 3 additions & 3 deletions Nos/Views/Onboarding/DisplayNameView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct DisplayNameView: View {
}
}
.navigationBarHidden(true)
.alert("displayNameError", isPresented: $showError) {
.alert("errorConnecting", isPresented: $showError) {
Button {
nextStep()
} label: {
Expand All @@ -49,7 +49,7 @@ struct DisplayNameView: View {
prompt: Text("displayNamePlaceholder")
.foregroundStyle(Color.textFieldPlaceholder)
)
.textInputAutocapitalization(.none)
.textInputAutocapitalization(.never)
.foregroundStyle(Color.primaryTxt)
.fontWeight(.bold)
.autocorrectionDisabled()
Expand All @@ -65,7 +65,7 @@ struct DisplayNameView: View {
}

func nextStep() {
state.step = .buildYourNetwork
state.step = .username
}

/// Saves the display name locally and publishes the event to relays. Sets `showError` if it fails.
Expand Down
4 changes: 4 additions & 0 deletions Nos/Views/Onboarding/OnboardingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum OnboardingStep {
case privateKey
case publicKey
case displayName
case username
case buildYourNetwork
case login
}
Expand Down Expand Up @@ -61,6 +62,9 @@ struct OnboardingView: View {
case .displayName:
DisplayNameView()
.environment(state)
case .username:
UsernameView()
.environment(state)
case .login:
OnboardingLoginView(completion: completion)
case .buildYourNetwork:
Expand Down
185 changes: 185 additions & 0 deletions Nos/Views/Onboarding/UsernameView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import Dependencies
import Logger
import SwiftUI

/// The possible states of ``UsernameView``.
fileprivate enum UsernameViewState {
case idle
case loading
case verificationFailed
case claimed
case errorAlert
}

/// The Username view in the onboarding.
struct UsernameView: View {
@Environment(OnboardingState.self) private var state
@Environment(CurrentUser.self) private var currentUser
@Environment(\.managedObjectContext) private var viewContext

@Dependency(\.crashReporting) private var crashReporting
@Dependency(\.namesAPI) private var namesAPI

@State private var username = ""
@State private var usernameState: UsernameViewState = .idle
@State private var saveError: SaveProfileError?

private var showAlert: Binding<Bool> {
Binding {
usernameState == .errorAlert
} set: { _ in
usernameState = .idle
}
}

private var nextButtonDisabled: Bool {
if username.isEmpty {
return true
} else if usernameState == .loading {
return true
} else if usernameState == .claimed {
return true
} else {
return false
}
}

var body: some View {
ZStack {
Color.appBg
.ignoresSafeArea()
ViewThatFits(in: .vertical) {
displayNameStack

ScrollView {
displayNameStack
}
}
}
.navigationBarHidden(true)
.alert("errorConnecting", isPresented: showAlert) {
Button {
nextStep()
} label: {
Text("skipForNow")
}
}
}

var displayNameStack: some View {
VStack(alignment: .leading, spacing: 20) {
LargeNumberView(4)
HStack(alignment: .firstTextBaseline) {
Text("usernameHeadline")
.font(.clarityBold(.title))
.foregroundStyle(Color.primaryTxt)
Text("usernameNIP05Parenthetical")
.font(.clarityRegular(.title2))
.foregroundStyle(Color.secondaryTxt)
}
Text("usernameDescription")
.font(.body)
.foregroundStyle(Color.secondaryTxt)
HStack {
TextField(
"",
text: $username,
prompt: Text("usernamePlaceholder")
.foregroundStyle(Color.textFieldPlaceholder)
)
.textInputAutocapitalization(.never)
.foregroundStyle(Color.primaryTxt)
.fontWeight(.bold)
.autocorrectionDisabled()
.padding()
.withStyledBorder()

Text("@nos.social")
.fontWeight(.bold)
.foregroundStyle(Color.secondaryTxt)
}
if usernameState == .verificationFailed {
usernameAlreadyClaimedText
}
Spacer()
BigActionButton("next") {
await verifyAndSave()
}
.disabled(nextButtonDisabled)
}
.padding(40)
.readabilityPadding()
}

var usernameAlreadyClaimedText: some View {
Text("usernameNotAvailable")
.font(.subheadline)
.fontWeight(.medium)
.foregroundStyle(Color.error)
}

func nextStep() {
state.step = .buildYourNetwork
}

/// Checks whether the username is available and saves it. Updates `usernameState` based on the result.
func verifyAndSave() async {
usernameState = .loading

guard !username.isEmpty, let keyPair = currentUser.keyPair else {
usernameState = .errorAlert
return
}

do {
let verified = try await namesAPI.checkAvailability(
username: username,
publicKey: keyPair.publicKey
)
guard verified else {
usernameState = .verificationFailed
return
}
await save()
} catch {
Log.error(error.localizedDescription)
usernameState = .verificationFailed
}
}

/// Saves the username locally, publishes the metadata, and registers it.
func save() async {
usernameState = .loading

guard let author = await currentUser.author,
let keyPair = currentUser.keyPair else {
usernameState = .errorAlert
return
}

author.nip05 = "\(username)@nos.social"
do {
try viewContext.save()
try await currentUser.publishMetadata()
let relays = author.relays.compactMap {
$0.addressURL
}
try await namesAPI.register(
username: username,
keyPair: keyPair,
relays: relays
)
usernameState = .claimed
nextStep()
} catch {
crashReporting.report(error)
usernameState = .errorAlert
}
}
}

#Preview {
UsernameView()
.environment(OnboardingState())
.inject(previewData: PreviewData())
}

0 comments on commit 6905591

Please sign in to comment.