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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user