Bubble/Threaded/Views/AddInstanceView.swift

267 lines
9.1 KiB
Swift

//Made by Lumaa
import SwiftUI
import SwiftData
import AuthenticationServices
struct AddInstanceView: View {
@Environment(\.webAuthenticationSession) private var webAuthenticationSession
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Query private var loggedAccounts: [LoggedAccount]
// Instance URL and verify
@State private var instanceUrl: String = ""
@State private var verifying: Bool = false
@State private var verified: Bool = false
@State private var verifyError: Bool = false
@State private var blockList: [String] = []
@State private var responsability: Bool = false
@State private var showingResponsability: Bool = false
@State private var agreedResponsability: Bool = false
@State private var instanceInfo: Instance?
@State private var signInClient: Client?
@Binding public var logged: Bool
var body: some View {
Form {
Section {
TextField("login.mastodon.instance", text: $instanceUrl)
.keyboardType(.URL)
.textContentType(.URL)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.submitLabel(.continue)
.disabled(verifying)
.onSubmit {
if !verified {
verify()
}
}
if !verifying {
if !verified {
Button {
verify()
} label: {
Text("login.mastodon.verify")
.disabled(instanceUrl.isEmpty)
}
.buttonStyle(.bordered)
if verifyError == true {
Text("login.mastodon.verify-error")
.foregroundStyle(.red)
}
} else {
Button {
Task {
await signIn()
}
} label: {
Text("login.mastodon.login")
.disabled(instanceUrl.isEmpty)
}
.buttonStyle(.bordered)
}
} else {
ProgressView()
.progressViewStyle(.circular)
.tint(Color.white)
.foregroundStyle(Color.white)
}
}
if verified && instanceInfo != nil {
Section {
VStack(alignment: .leading) {
Text(instanceInfo!.title)
.font(.headline)
Text(instanceInfo!.shortDescription)
}
VStack(alignment: .leading, spacing: 15) {
Text("instance.rules")
.font(.headline)
if !(instanceInfo!.rules?.isEmpty ?? true) {
ForEach(instanceInfo!.rules!) { rule in
Text(rule.text)
}
}
}
}
}
}
.task {
withAnimation {
verifying = true
}
blockList = Instance.getBlocklist()
withAnimation {
verifying = false
}
}
.alert("login.instance.unsafe", isPresented: $showingResponsability, actions: {
Button(role: .destructive) {
responsability = true
agreedResponsability = true
showingResponsability.toggle()
} label: {
Text("login.instance.unsafe.agree")
}
Button(role: .cancel) {
responsability = true
agreedResponsability = false
showingResponsability.toggle()
} label: {
Text("login.instance.unsafe.disagree")
}
}, message: {
Text("login.instance.unsafe.description")
})
.scrollContentBackground(.hidden)
.background(Color.appBackground)
.onChange(of: instanceUrl) { _, newValue in
guard !self.verifying else { return }
verified = false
}
}
func verify() {
withAnimation {
verifying = true
verified = false
verifyError = false
}
let cleanInstance = instanceUrl
.replacingOccurrences(of: "http://", with: "")
.replacingOccurrences(of: "https://", with: "")
if !isInstanceSafe() {
if responsability == false && agreedResponsability == false {
responsability = true
agreedResponsability = false
showingResponsability = true
withAnimation {
verifying = false
verified = false
verifyError = false
}
return
} else if responsability == true && agreedResponsability == true {
showingResponsability = false
} else if responsability == true && agreedResponsability == false {
showingResponsability = true
withAnimation {
verifying = false
verified = false
verifyError = false
}
return
}
} else {
responsability = false
agreedResponsability = false
UserDefaults.standard.removeObject(forKey: "unsafe")
}
let client = Client(server: cleanInstance)
Task {
do {
let instance: Instance = try await client.get(endpoint: Instances.instance)
withAnimation {
instanceInfo = instance
verifying = false
verified = true
verifyError = false
}
} catch {
print(error.localizedDescription)
withAnimation {
verifying = false
verified = false
verifyError = true
}
}
}
}
private func signIn() async {
let cleanInstance = instanceUrl
.replacingOccurrences(of: "http://", with: "")
.replacingOccurrences(of: "https://", with: "")
signInClient = .init(server: cleanInstance)
if let oauthURL = try? await signInClient?.oauthURL(),
let url = try? await webAuthenticationSession.authenticate(using: oauthURL, callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: "")) {
await continueSignIn(url: url)
}
}
private func continueSignIn(url: URL) async {
guard let client = signInClient else {
return
}
if agreedResponsability && responsability && loggedAccounts.count <= 0 {
UserDefaults.standard.setValue(true, forKey: "unsafe")
} else {
UserDefaults.standard.removeObject(forKey: "unsafe")
}
do {
let oauthToken = try await client.continueOauthFlow(url: url)
let client = Client(server: client.server, oauthToken: oauthToken)
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
let appAcc = AppAccount(server: client.server, accountName: "\(account.acct)@\(client.server)", oauthToken: oauthToken)
let connections: [String] = try await client.get(endpoint: Instances.peers)
client.addConnections(connections)
appAcc.saveAsCurrent()
AccountManager.shared.setClient(client)
AccountManager.shared.setAccount(account)
let newLog: LoggedAccount = .init(appAccount: appAcc)
modelContext.insert(newLog)
HapticManager.playHaptics(haptics: Haptic.success)
signInClient = client
logged = true
dismiss()
} catch {
HapticManager.playHaptics(haptics: Haptic.error)
print(error)
}
}
/// Is the user input instance URL a safe instance
/// - returns: True, if the instance isn't consider as dangerous
private func isInstanceSafe() -> Bool {
let unsafe = blockList.contains(instanceUrl.trimmingCharacters(in: .whitespacesAndNewlines))
return !unsafe
}
}
#Preview {
AddInstanceView(logged: .constant(false))
}