Files
Nextcloud-Cookbook-iOS/Nextcloud Cookbook iOS Client/Views/Onboarding/V2LoginView.swift
2023-12-10 11:22:17 +01:00

251 lines
9.2 KiB
Swift

//
// V2LoginView.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 21.11.23.
//
import Foundation
import SwiftUI
enum V2LoginStage: LoginStage {
case serverAddress, login, validate
func next() -> V2LoginStage {
switch self {
case .serverAddress: return .login
case .login: return .validate
case .validate: return .validate
}
}
func previous() -> V2LoginStage {
switch self {
case .serverAddress: return .serverAddress
case .login: return .serverAddress
case .validate: return .login
}
}
}
struct CollapsibleView<T: View>: View {
@State var titleColor: Color = .white
@State var content: () -> T
@State var title: () -> Text
@State var isCollapsed: Bool = true
@State var rotationAngle: Double = -90
var body: some View {
VStack(alignment: .leading) {
Button {
withAnimation(.easeInOut(duration: 0.2)) {
isCollapsed.toggle()
if isCollapsed {
rotationAngle += 90
} else {
rotationAngle -= 90
}
}
rotationAngle = isCollapsed ? -90 : 0
} label: {
HStack {
Image(systemName: "chevron.down")
.bold()
.rotationEffect(Angle(degrees: rotationAngle))
title()
}.foregroundStyle(titleColor)
}
if !isCollapsed {
content()
.padding(.top, 1)
}
}
}
}
struct V2LoginView: View {
@Binding var showAlert: Bool
@Binding var alertMessage: String
@State var loginStage: V2LoginStage = .serverAddress
@State var loginRequest: LoginV2Request? = nil
@FocusState private var focusedField: Field?
@AppStorage("serverAddress") var serverAddress = ""
@AppStorage("username") var userName = ""
@AppStorage("token") var token = ""
@AppStorage("onboarding") var onboarding = true
// TextField handling
enum Field {
case server
case username
case token
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
LoginLabel(text: "Server address")
.padding()
LoginTextField(example: "e.g.: example.com", text: $serverAddress, color: loginStage == .serverAddress ? .white : .secondary)
.focused($focusedField, equals: .server)
.textContentType(.URL)
.submitLabel(.done)
.padding([.bottom, .horizontal])
.onSubmit {
withAnimation(.easeInOut) {
loginStage = loginStage.next()
}
}
CollapsibleView {
VStack(alignment: .leading) {
Text("Make sure to enter the server address in the form 'example.com'. Currently, only servers using the 'https' protocol are supported.")
if let loginRequest = loginRequest {
Text("If the login button does not open your browser, copy the following link and paste it in your browser manually:")
Text(loginRequest.login)
}
}
} title: {
Text("Show help")
.foregroundColor(.white)
.font(.headline)
}.padding()
if loginStage == .login || loginStage == .validate {
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'.")
.font(.subheadline)
.foregroundStyle(.white)
.padding()
}
HStack {
if loginStage == .login || loginStage == .validate {
Button {
if serverAddress == "" {
alertMessage = "Please enter a valid server address."
showAlert = true
return
}
Task {
await sendLoginV2Request()
if let loginRequest = loginRequest {
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 {
guard let res = await fetchLoginV2Response() else {
alertMessage = "Login failed. Please login via the browser and try again."
showAlert = true
return
}
print("Login successfull for user \(res.loginName)!")
self.userName = res.loginName
self.token = res.appPassword
self.onboarding = false
}
} 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()
}
}
}
}
}
func sendLoginV2Request() async {
let hostPath = "https://\(serverAddress)"
let headerFields: [HeaderField] = [
//HeaderField.ocsRequest(value: true),
//HeaderField.accept(value: .JSON)
]
let request = RequestWrapper.customRequest(
method: .POST,
path: .LOGINV2REQ,
headerFields: headerFields
)
do {
let (data, _): (Data?, Error?) = try await NetworkHandler.sendHTTPRequest(
request,
hostPath: hostPath,
authString: nil
)
guard let data = data else { return }
print("Data: \(data)")
let loginReq: LoginV2Request? = JSONDecoder.safeDecode(data)
self.loginRequest = loginReq
} catch {
print("Could not establish communication with the server.")
}
}
func fetchLoginV2Response() async -> LoginV2Response? {
guard let loginRequest = loginRequest else { return nil }
let headerFields = [
HeaderField.ocsRequest(value: true),
HeaderField.accept(value: .JSON),
HeaderField.contentType(value: .FORM)
]
let request = RequestWrapper.customRequest(
method: .POST,
path: .NONE,
headerFields: headerFields,
body: "token=\(loginRequest.poll.token)".data(using: .utf8),
authenticate: false
)
var (data, error): (Data?, Error?) = (nil, nil)
do {
(data, error) = try await NetworkHandler.sendHTTPRequest(
request,
hostPath: loginRequest.poll.endpoint,
authString: nil
)
} catch {
print("Error: ", error)
}
guard let data = data else { return nil }
if let loginRes: LoginV2Response = JSONDecoder.safeDecode(data) {
return loginRes
}
print("Could not decode.")
return nil
}
}