Skip to content

Instantly share code, notes, and snippets.

@SylarRuby
Last active November 12, 2022 10:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SylarRuby/2529775157f5f4e298ba65557110ec88 to your computer and use it in GitHub Desktop.
Save SylarRuby/2529775157f5f4e298ba65557110ec88 to your computer and use it in GitHub Desktop.
Turbo-iOS Native Authentication View, not modally presented
//
// 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