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>
214 lines
7.3 KiB
Swift
214 lines
7.3 KiB
Swift
//
|
|
// V2LoginView.swift
|
|
// Nextcloud Cookbook iOS Client
|
|
//
|
|
// Created by Vincent Meilinger on 21.11.23.
|
|
//
|
|
|
|
import Foundation
|
|
import OSLog
|
|
import SwiftUI
|
|
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
|
|
case .validate: return .login
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
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.")
|
|
}
|
|
.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 {
|
|
let (response, error) = await fetchLoginV2Response()
|
|
checkLogin(response: response, error: error)
|
|
}
|
|
}) {
|
|
if let loginRequest = loginRequest {
|
|
WebViewSheet(url: loginRequest.login)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = String(localized: "Login failed. Please login via the browser and try again.")
|
|
showAlert = true
|
|
return
|
|
}
|
|
guard let response = response else {
|
|
alertMessage = String(localized: "Login failed. Please login via the browser and try again.")
|
|
showAlert = true
|
|
return
|
|
}
|
|
Logger.network.debug("Login successful for user \(response.loginName)!")
|
|
UserSettings.shared.username = response.loginName
|
|
UserSettings.shared.token = response.appPassword
|
|
let loginString = "\(UserSettings.shared.username):\(UserSettings.shared.token)"
|
|
let loginData = loginString.data(using: String.Encoding.utf8)!
|
|
DispatchQueue.main.async {
|
|
UserSettings.shared.authString = loginData.base64EncodedString()
|
|
}
|
|
UserSettings.shared.onboarding = false
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Login WebView logic
|
|
|
|
struct WebViewSheet: View {
|
|
@Environment(\.dismiss) var dismiss
|
|
@State var url: String
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
WebView(url: URL(string: url)!)
|
|
.navigationTitle("Nextcloud Login")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("Done") {
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WebView: UIViewRepresentable {
|
|
let url: URL
|
|
|
|
func makeUIView(context: Context) -> WKWebView {
|
|
return WKWebView()
|
|
}
|
|
|
|
func updateUIView(_ uiView: WKWebView, context: Context) {
|
|
let request = URLRequest(url: url)
|
|
uiView.load(request)
|
|
}
|
|
}
|