Consolidate onboarding into single login page with native styling and full localization

Merge the two-page welcome/login flow into a single page with the app icon,
title, subtitle, and login inputs all on one screen. Replace the custom blue
background and white-on-blue styling with native iOS system colors and
button styles. Add missing translations (de, es, fr) for all onboarding
strings and fix localization by using LocalizedStringKey and String(localized:).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 10:44:51 +01:00
parent ce2a814e5a
commit c8d9ab7397
4 changed files with 4178 additions and 3886 deletions

View File

@@ -12,14 +12,14 @@ import WebKit
enum V2LoginStage: LoginStage {
case login, validate
func next() -> V2LoginStage {
switch self {
case .login: return .validate
case .validate: return .validate
}
}
func previous() -> V2LoginStage {
switch self {
case .login: return .login
@@ -33,105 +33,101 @@ enum V2LoginStage: LoginStage {
struct V2LoginView: View {
@Binding var showAlert: Bool
@Binding var alertMessage: String
@State var loginStage: V2LoginStage = .login
@State var loginRequest: LoginV2Request? = nil
@State var presentBrowser = false
// TextField handling
enum Field {
case server
case username
case token
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
ServerAddressField()
CollapsibleView {
VStack(alignment: .leading) {
Text("Make sure to enter the server address in the form 'example.com', or \n'<server address>:<port>'\n when a non-standard port is used.")
.padding(.bottom)
Text("The 'Login' button will open a web browser. Please follow the login instructions provided there.\nAfter a successful login, return to this application and press 'Validate'.")
.padding(.bottom)
Text("If the login button does not open your browser, use the 'Copy Link' button and paste the link in your browser manually.")
}
} title: {
Text("Show help")
.foregroundColor(.white)
.font(.headline)
}.padding()
if loginRequest != nil {
Button("Copy Link") {
UIPasteboard.general.string = loginRequest!.login
}
.font(.headline)
.foregroundStyle(.white)
.padding()
VStack(alignment: .leading, spacing: 16) {
ServerAddressField()
CollapsibleView(titleColor: .secondary) {
VStack(alignment: .leading, spacing: 8) {
Text("Make sure to enter the server address in the form 'example.com', or '<server address>:<port>' when a non-standard port is used.")
Text("The 'Login' button will open a web browser. Please follow the login instructions provided there. After a successful login, return to this application and press 'Validate'.")
Text("If the login button does not open your browser, use the 'Copy Link' button and paste the link in your browser manually.")
}
HStack {
Button {
if UserSettings.shared.serverAddress == "" {
alertMessage = "Please enter a valid server address."
showAlert = true
return
}
Task {
let error = await sendLoginV2Request()
if let error = error {
alertMessage = "A network error occured (\(error.localizedDescription))."
showAlert = true
}
if let loginRequest = loginRequest {
presentBrowser = true
//await UIApplication.shared.open(URL(string: loginRequest.login)!)
} else {
alertMessage = "Unable to reach server. Please check your server address and internet connection."
showAlert = true
}
}
loginStage = loginStage.next()
} label: {
Text("Login")
.foregroundColor(.white)
.font(.headline)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white, lineWidth: 2)
.foregroundColor(.clear)
)
}.padding()
if loginStage == .validate {
Spacer()
Button {
// fetch login v2 response
Task {
let (response, error) = await fetchLoginV2Response()
checkLogin(response: response, error: error)
}
} label: {
Text("Validate")
.foregroundColor(.white)
.font(.headline)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white, lineWidth: 2)
.foregroundColor(.clear)
)
}
.disabled(loginRequest == nil ? true : false)
.padding()
}
.font(.footnote)
.foregroundStyle(.secondary)
} title: {
Text("Show help")
.font(.subheadline)
}
if loginRequest != nil {
Button {
UIPasteboard.general.string = loginRequest!.login
} label: {
Label("Copy Link", systemImage: "doc.on.doc")
.font(.subheadline)
}
}
HStack(spacing: 12) {
Button {
if UserSettings.shared.serverAddress == "" {
alertMessage = String(localized: "Please enter a valid server address.")
showAlert = true
return
}
Task {
let error = await sendLoginV2Request()
if let error = error {
alertMessage = String(localized: "A network error occurred. Please try again.")
showAlert = true
}
if let _ = loginRequest {
presentBrowser = true
} else {
alertMessage = String(localized: "Unable to reach server. Please check your server address and internet connection.")
showAlert = true
}
}
loginStage = loginStage.next()
} label: {
Label("Login", systemImage: "person.badge.key")
.font(.subheadline)
.fontWeight(.medium)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.foregroundStyle(Color.nextcloudBlue)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.nextcloudBlue.opacity(0.1))
)
}
if loginStage == .validate {
Button {
Task {
let (response, error) = await fetchLoginV2Response()
checkLogin(response: response, error: error)
}
} label: {
Label("Validate", systemImage: "checkmark.circle.fill")
.font(.subheadline)
.fontWeight(.medium)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.foregroundStyle(Color.nextcloudBlue)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.nextcloudBlue.opacity(0.1))
)
}
.disabled(loginRequest == nil)
}
}
.padding(.top, 4)
}
.sheet(isPresented: $presentBrowser, onDismiss: {
Task {
@@ -144,26 +140,26 @@ struct V2LoginView: View {
}
}
}
func sendLoginV2Request() async -> NetworkError? {
let (req, error) = await NextcloudApi.loginV2Request()
self.loginRequest = req
return error
}
func fetchLoginV2Response() async -> (LoginV2Response?, NetworkError?) {
guard let loginRequest = loginRequest else { return (nil, .invalidRequest) }
return await NextcloudApi.loginV2Response(req: loginRequest)
}
func checkLogin(response: LoginV2Response?, error: NetworkError?) {
if let error = error {
alertMessage = "Login failed. Please login via the browser and try again. (\(error.localizedDescription))"
alertMessage = String(localized: "Login failed. Please login via the browser and try again.")
showAlert = true
return
}
guard let response = response else {
alertMessage = "Login failed. Please login via the browser and try again."
alertMessage = String(localized: "Login failed. Please login via the browser and try again.")
showAlert = true
return
}