Files
Nextcloud-Cookbook-iOS/Nextcloud Cookbook iOS Client/Views/Onboarding/OnboardingView.swift
Hendrik Hogertz c8d9ab7397 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>
2026-02-15 10:44:51 +01:00

193 lines
6.1 KiB
Swift

//
// OnboardingView.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 15.09.23.
//
import Foundation
import OSLog
import SwiftUI
struct OnboardingView: View {
@State var loginMethod: LoginMethod = .v2
// Login error alert
@State var showAlert: Bool = false
@State var alertMessage: String = String(localized: "Error: Could not connect to server.")
var body: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
Image("cookbook-icon")
.resizable()
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 18))
.padding(.top, 48)
Text("Cookbook Client")
.font(.largeTitle)
.bold()
.padding(.top, 10)
Text("Thanks for downloading! Sign in to your Nextcloud server to get started.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
.padding(.top, 6)
VStack(alignment: .leading, spacing: 16) {
Picker("Login Method", selection: $loginMethod) {
Text("Nextcloud Login").tag(LoginMethod.v2)
Text("App Token Login").tag(LoginMethod.token)
}
.pickerStyle(.segmented)
if loginMethod == .token {
TokenLoginView(
showAlert: $showAlert,
alertMessage: $alertMessage
)
} else if loginMethod == .v2 {
V2LoginView(
showAlert: $showAlert,
alertMessage: $alertMessage
)
}
}
.padding(.top, 28)
}
.fontDesign(.rounded)
.padding()
.alert(alertMessage, isPresented: $showAlert) {
Button("Ok", role: .cancel) { }
}
}
.background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea())
}
}
protocol LoginStage {
func next() -> Self
func previous() -> Self
}
enum LoginMethod {
case v2, token
}
enum TokenLoginStage: LoginStage {
case serverAddress, userName, appToken, validate
func next() -> TokenLoginStage {
switch self {
case .serverAddress: return .userName
case .userName: return .appToken
case .appToken: return .validate
case .validate: return .validate
}
}
func previous() -> TokenLoginStage {
switch self {
case .serverAddress: return .serverAddress
case .userName: return .serverAddress
case .appToken: return .userName
case .validate: return .appToken
}
}
}
struct LoginLabel: View {
let text: LocalizedStringKey
var body: some View {
Text(text)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
struct BorderedLoginTextField: View {
var example: LocalizedStringKey
@Binding var text: String
var body: some View {
TextField(example, text: $text)
.textFieldStyle(.plain)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.padding()
.background(Color(uiColor: .secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
struct LoginTextField: View {
var example: LocalizedStringKey
@Binding var text: String
var body: some View {
TextField(example, text: $text)
.textFieldStyle(.plain)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.padding()
.background(Color(uiColor: .secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
struct ServerAddressField: View {
@ObservedObject var userSettings = UserSettings.shared
@State var serverProtocol: ServerProtocol = UserSettings.shared.serverProtocol == ServerProtocol.http.rawValue ? ServerProtocol.http : ServerProtocol.https
enum ServerProtocol: String {
case https="https://", http="http://"
static let all = [https, http]
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
LoginLabel(text: "Server address")
VStack(alignment: .leading, spacing: 10) {
HStack {
Picker(ServerProtocol.https.rawValue, selection: $serverProtocol) {
ForEach(ServerProtocol.all, id: \.self) {
Text($0.rawValue)
}
}
.pickerStyle(.menu)
.tint(.accentColor)
.onChange(of: serverProtocol) { value in
Logger.view.debug("\(value.rawValue)")
userSettings.serverProtocol = value.rawValue
}
TextField("e.g.: example.com", text: $userSettings.serverAddress)
.textFieldStyle(.plain)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.padding(10)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
Text(userSettings.serverProtocol + userSettings.serverAddress)
.font(.footnote)
.foregroundStyle(.secondary)
}
.padding()
.background(Color(uiColor: .secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
struct ServerAddressField_Preview: PreviewProvider {
static var previews: some View {
ServerAddressField()
.previewLayout(.sizeThatFits)
.padding()
}
}