Last active
November 12, 2022 10:56
-
-
Save SylarRuby/2529775157f5f4e298ba65557110ec88 to your computer and use it in GitHub Desktop.
Turbo-iOS Native Authentication View, not modally presented
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SignInViewModel.swift | |
// | |
// Created by @SylarRuby on 11/11/2022. | |
// | |
// This a continuation of https://masilotti.com/turbo-ios/native-authentication/. | |
// Instead of showing a modal for the form to sign in, we replace the all views | |
// with the sign in screen/view. | |
import Foundation | |
import UIKit | |
import Turbo | |
import KeychainAccess | |
import WebKit | |
class SignInViewModel: ObservableObject { | |
let session: Session! | |
let navigationController: UINavigationController! | |
@Published var email: String = "" | |
@Published var password: String = "" | |
init( | |
session: Session!, | |
email: String = "", | |
password: String = "", | |
navigationController: UINavigationController! | |
) { | |
self.session = session | |
self.email = email | |
self.password = password | |
self.navigationController = navigationController | |
} | |
private var request: URLRequest { | |
let url = URL(string: "...")! | |
var request = URLRequest(url: url) | |
request.httpMethod = "POST" | |
request.setValue("application/json", forHTTPHeaderField: "Content-Type") | |
request.setValue("My App (Turbo Native)", forHTTPHeaderField: "User-Agent") | |
let credentials = Credentials(email: email, password: password) | |
request.httpBody = try? JSONEncoder().encode(credentials) | |
return request | |
} | |
private struct Credentials: Encodable { | |
let email: String | |
let password: String | |
} | |
private struct AccessToken: Decodable { | |
let token: String | |
} | |
public func signIn() { | |
URLSession.shared.dataTask(with: request) { data, response, error in | |
guard | |
error == nil, | |
let response = response as? HTTPURLResponse, | |
// Ensure the response was successful | |
(200 ..< 300).contains(response.statusCode), | |
let headers = response.allHeaderFields as? [String: String], | |
let data = data, | |
let token = try? JSONDecoder().decode(AccessToken.self, from: data) | |
else { return /* TODO: Handle errors */ } | |
let keychain = Keychain(service: "Turbo-Credentials") | |
keychain["access-token"] = token.token | |
let url = URL(string: "...")! | |
// Copy the "Set-Cookie" headers to the shared web view storage | |
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: url) | |
HTTPCookieStorage.shared.setCookies(cookies, for: url, mainDocumentURL: nil) | |
DispatchQueue.main.async { | |
let cookieStore = WKWebsiteDataStore.default().httpCookieStore | |
cookies.forEach { cookie in | |
cookieStore.setCookie(cookie, completionHandler: nil) | |
} | |
self.successAuth() | |
} | |
}.resume() | |
} | |
// Show the aunthenticated view... | |
private func successAuth() -> Void { | |
let url = URL(string: "...")! | |
let viewController = VisitableViewController(url: url) | |
guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { | |
fatalError("could not get scene delegate ") | |
} | |
sceneDelegate.window?.rootViewController = navigationController | |
navigationController.viewControllers = [viewController] | |
session.visit(viewController, reload: true) | |
} | |
} | |
// SceneDelegate | |
func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: Error) { | |
if error.isUnauthorized { | |
// Render native sign-in flow | |
let viewModel = SignInViewModel(session: session, navigationController: navigationController) | |
let view = NewSessionView(viewModel: viewModel) | |
let controller = UIHostingController(rootView: view) | |
// We do not want to present a modal, replace the view with the NewSessionView | |
navigationController.viewControllers = [controller] | |
// navigationController.present(controller, animated: true) | |
} else { | |
// Handle actual errors | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment