SwiftUI version of google oauth signin controller

This commit is contained in:
Selim Mustafaev 2025-04-14 23:00:32 +03:00
parent 7831b7e615
commit 41578579ef
14 changed files with 266 additions and 175 deletions

View File

@ -136,7 +136,6 @@
7A9519842D80B72B00E69883 /* RecordsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9519832D80B72B00E69883 /* RecordsCoordinator.swift */; };
7A961C6C2C4C3C8600CE2211 /* TextRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A961C6B2C4C3C8600CE2211 /* TextRowView.swift */; };
7A961C6E2C4C3C9E00CE2211 /* LinkRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A961C6D2C4C3C9E00CE2211 /* LinkRowView.swift */; };
7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */; };
7A96AE2F246B2BCD00297C33 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A96AE2E246B2BCD00297C33 /* WebKit.framework */; };
7AA514E02D0B75B3001CAC50 /* StorageService+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */; };
7AA515D02D9ABCC800EB3418 /* RecordPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA515CF2D9ABCC800EB3418 /* RecordPlayerService.swift */; };
@ -194,8 +193,12 @@
7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C96250F41B000F237B2 /* PNKeyboard.swift */; };
7ADF6C99250F872C00F237B2 /* RoadNumbers.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */; };
7ADF6CA12512244400F237B2 /* MapExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6CA02512244400F237B2 /* MapExt.swift */; };
7ADFC9572DAD0288001A43E3 /* GoogleAuthScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADFC9562DAD0288001A43E3 /* GoogleAuthScreen.swift */; };
7ADFC9592DAD1C3D001A43E3 /* GoogleAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADFC9582DAD1C3D001A43E3 /* GoogleAuthViewModel.swift */; };
7ADFC95B2DAD1F45001A43E3 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADFC95A2DAD1F45001A43E3 /* WebView.swift */; };
7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE24C5E251F1B4E00758E39 /* Buttons.swift */; };
7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */; };
7AEAA2A12DAD9C00009954F0 /* TokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEAA2A02DAD9C00009954F0 /* TokenResponse.swift */; };
7AF231932DA1C28100AE5EB3 /* AuthScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF231922DA1C28100AE5EB3 /* AuthScreen.swift */; };
7AF231952DA1C29300AE5EB3 /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF231942DA1C29300AE5EB3 /* AuthViewModel.swift */; };
7AF231972DA1C30000AE5EB3 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF231962DA1C30000AE5EB3 /* AuthCoordinator.swift */; };
@ -422,7 +425,6 @@
7A9519832D80B72B00E69883 /* RecordsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsCoordinator.swift; sourceTree = "<group>"; };
7A961C6B2C4C3C8600CE2211 /* TextRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRowView.swift; sourceTree = "<group>"; };
7A961C6D2C4C3C9E00CE2211 /* LinkRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkRowView.swift; sourceTree = "<group>"; };
7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSignInController.swift; sourceTree = "<group>"; };
7A96AE2E246B2BCD00297C33 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
7A96AE30246B2FE400297C33 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
7A96AE32246C095700297C33 /* Base64FS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64FS.swift; sourceTree = "<group>"; };
@ -478,9 +480,13 @@
7ADF6C96250F41B000F237B2 /* PNKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNKeyboard.swift; sourceTree = "<group>"; };
7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers.otf; sourceTree = "<group>"; };
7ADF6CA02512244400F237B2 /* MapExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapExt.swift; sourceTree = "<group>"; };
7ADFC9562DAD0288001A43E3 /* GoogleAuthScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthScreen.swift; sourceTree = "<group>"; };
7ADFC9582DAD1C3D001A43E3 /* GoogleAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthViewModel.swift; sourceTree = "<group>"; };
7ADFC95A2DAD1F45001A43E3 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
7AE24C5E251F1B4E00758E39 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = "<group>"; };
7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = "<group>"; };
7AE8424D26109F78002F6B31 /* Exportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exportable.swift; sourceTree = "<group>"; };
7AEAA2A02DAD9C00009954F0 /* TokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenResponse.swift; sourceTree = "<group>"; };
7AF231922DA1C28100AE5EB3 /* AuthScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthScreen.swift; sourceTree = "<group>"; };
7AF231942DA1C29300AE5EB3 /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = "<group>"; };
7AF231962DA1C30000AE5EB3 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = "<group>"; };
@ -655,7 +661,6 @@
7A11471423FDEAF800B424AF /* Controllers */ = {
isa = PBXGroup;
children = (
7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */,
7A11471523FDEB2A00B424AF /* MainSplitController.swift */,
7AC3554B29696A1C00889457 /* MainTabController.swift */,
7AC3554D29696C4500889457 /* DummyNewController.swift */,
@ -692,7 +697,6 @@
7A333813249A532400D878F1 /* Filter.swift */,
6841A913FABBB0AB20DEF4FC /* PagedResponse.swift */,
7A6DD90D24337930009DE740 /* PlateNumber.swift */,
7A11474823FF2B2D00B424AF /* Response.swift */,
7A11474623FF2AA500B424AF /* User.swift */,
7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */,
7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.swift */,
@ -725,6 +729,7 @@
7A1441632C297E9800E79018 /* Screens */ = {
isa = PBXGroup;
children = (
7ADFC9552DAD026C001A43E3 /* GoogleAuthScreen */,
7A386A3E2DABDBFF0051676A /* MapScreen */,
7AF231912DA1C26C00AE5EB3 /* AuthScreen */,
7A95197E2D80B69800E69883 /* RecordsScreen */,
@ -1060,6 +1065,8 @@
7AB587402C42FFE200FA7B66 /* ApiServiceProtocol.swift */,
7A11474323FF06CA00B424AF /* ApiService.swift */,
7A599C352C18AC7F00D47C18 /* ApiError.swift */,
7A11474823FF2B2D00B424AF /* Response.swift */,
7AEAA2A02DAD9C00009954F0 /* TokenResponse.swift */,
);
path = ApiService;
sourceTree = "<group>";
@ -1127,6 +1134,16 @@
path = Extensions;
sourceTree = "<group>";
};
7ADFC9552DAD026C001A43E3 /* GoogleAuthScreen */ = {
isa = PBXGroup;
children = (
7ADFC9562DAD0288001A43E3 /* GoogleAuthScreen.swift */,
7ADFC9582DAD1C3D001A43E3 /* GoogleAuthViewModel.swift */,
7ADFC95A2DAD1F45001A43E3 /* WebView.swift */,
);
path = GoogleAuthScreen;
sourceTree = "<group>";
};
7AF231912DA1C26C00AE5EB3 /* AuthScreen */ = {
isa = PBXGroup;
children = (
@ -1446,6 +1463,7 @@
7AFBE8C02C3024E5003C491D /* ACHud.swift in Sources */,
7A9519842D80B72B00E69883 /* RecordsCoordinator.swift in Sources */,
7AAAFADA2C4D1AFE0050410D /* Zoomable.swift in Sources */,
7ADFC9572DAD0288001A43E3 /* GoogleAuthScreen.swift in Sources */,
7AC8B2762D6A01C700190706 /* UISearchTextField+Dumb.swift in Sources */,
7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */,
7A2E11292CCE395300E5CA17 /* OptionalDatePicker.swift in Sources */,
@ -1483,6 +1501,7 @@
7A7158072C44085600852088 /* OsagoScreen.swift in Sources */,
7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */,
7A386A442DABDC360051676A /* MapViewModel.swift in Sources */,
7ADFC9592DAD1C3D001A43E3 /* GoogleAuthViewModel.swift in Sources */,
7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */,
7A912F372D381B7400002938 /* LicensePlateView.swift in Sources */,
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */,
@ -1494,7 +1513,6 @@
7A7DADAC2D99738300F52F6C /* AudioRecordView.swift in Sources */,
7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */,
7AB9FE2A2D08CF35005DE374 /* EventsScreenMode.swift in Sources */,
7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */,
7A10227B2C557EE900B84627 /* LocationPickerCoordinator.swift in Sources */,
7AB490292D6B1217002F39C6 /* ACKeyboardView.swift in Sources */,
7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */,
@ -1505,6 +1523,7 @@
7AF231932DA1C28100AE5EB3 /* AuthScreen.swift in Sources */,
7A1E78F82CE900440004B740 /* ReportViewModel.swift in Sources */,
7A10226E2C551EE000B84627 /* LocationEditViewModel.swift in Sources */,
7ADFC95B2DAD1F45001A43E3 /* WebView.swift in Sources */,
7AB4902B2D6B1446002F39C6 /* ACKeyboardButton.swift in Sources */,
7AFBE8CE2C308B53003C491D /* ACMessageView.swift in Sources */,
7A14416C2C297F2100E79018 /* NotesCoordinator.swift in Sources */,
@ -1582,6 +1601,7 @@
7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */,
7A7097C22C9EC139007CFDCA /* ServiceContainer.swift in Sources */,
7A7AA2C42DA2A3CB00276D83 /* LocationError.swift in Sources */,
7AEAA2A12DAD9C00009954F0 /* TokenResponse.swift in Sources */,
7A54BFD32D43B95E00176D6D /* DbUpdatePolicy.swift in Sources */,
7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */,
7A64A2262C1A32C800284124 /* AudioRecordDto.swift in Sources */,

View File

@ -3,60 +3,9 @@
<device id="retina4_7" orientation="portrait" appearance="dark"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Google Sign In Controller-->
<scene sceneID="ztj-pr-ty7">
<objects>
<viewController storyboardIdentifier="GoogleSignInController" id="Ptg-6q-3w6" customClass="GoogleSignInController" customModule="AutoCat" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="NtL-RA-Nxs">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<wkWebView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0QS-UT-hbi">
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
<color key="backgroundColor" red="0.36078431370000003" green="0.38823529410000002" blue="0.4039215686" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
</wkWebView>
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="AMg-mc-MMG">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<items>
<navigationItem id="fZb-kM-9an">
<barButtonItem key="rightBarButtonItem" title="Close" id="ZHH-OZ-vHc">
<connections>
<action selector="close:" destination="Ptg-6q-3w6" id="VVY-eV-Yeg"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
</subviews>
<viewLayoutGuide key="safeArea" id="4cf-6q-b5U"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="0QS-UT-hbi" firstAttribute="top" secondItem="AMg-mc-MMG" secondAttribute="bottom" id="1l8-UT-leW"/>
<constraint firstItem="AMg-mc-MMG" firstAttribute="top" secondItem="4cf-6q-b5U" secondAttribute="top" id="50U-IM-aiw"/>
<constraint firstItem="AMg-mc-MMG" firstAttribute="leading" secondItem="4cf-6q-b5U" secondAttribute="leading" id="9Si-sE-9y6"/>
<constraint firstItem="AMg-mc-MMG" firstAttribute="trailing" secondItem="4cf-6q-b5U" secondAttribute="trailing" id="DuZ-iN-C4K"/>
<constraint firstItem="0QS-UT-hbi" firstAttribute="leading" secondItem="4cf-6q-b5U" secondAttribute="leading" id="RKF-L4-hnY"/>
<constraint firstItem="0QS-UT-hbi" firstAttribute="trailing" secondItem="4cf-6q-b5U" secondAttribute="trailing" id="gBv-r4-tWa"/>
<constraint firstItem="0QS-UT-hbi" firstAttribute="bottom" secondItem="4cf-6q-b5U" secondAttribute="bottom" id="sYj-Jg-IMn"/>
</constraints>
</view>
<connections>
<outlet property="webView" destination="0QS-UT-hbi" id="vZb-CE-W3J"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="WxN-oK-U9s" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1823" y="143"/>
</scene>
<!--Main Tab Controller-->
<scene sceneID="YhQ-kn-py3">
<objects>
@ -84,9 +33,4 @@
<point key="canvasLocation" x="199" y="143"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -1,108 +0,0 @@
import UIKit
import WebKit
import CommonCrypto
import PKHUD
import AutoCatCore
struct TokenResponse: Codable {
var id_token: String
var refresh_token: String?
var access_token: String
var expires_in: Int
var token_type: String
var scope: String
}
class GoogleSignInController: UIViewController, WKNavigationDelegate {
@IBOutlet weak var webView: WKWebView!
private var codeVerifier: String = ""
public var completion: (() -> Void)?
let apiService: ApiServiceProtocol = ServiceContainer.shared.resolve(ApiServiceProtocol.self)
override func viewDidLoad() {
super.viewDidLoad()
self.webView.navigationDelegate = self
#if targetEnvironment(macCatalyst)
self.webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15"
#else
self.webView.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1"
#endif
self.codeVerifier = UUID().uuidString
let codeChallenge = self.sha256(string: self.codeVerifier) ?? ""
let authUrlString = Constants.googleAuthURL
+ "?response_type=code"
+ "&code_challenge_method=S256"
+ "&scope=email%20profile"
+ "&redirect_uri=" + Constants.googleRedirectURL
+ "&client_id=" + Constants.fbClientId
+ "&code_challenge=" + codeChallenge
if let url = URL(string: authUrlString) {
let request = URLRequest(url: url)
self.webView.load(request)
}
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
if let url = navigationAction.request.url {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
if let queryItems = components.queryItems {
if let code = queryItems.first(where: { $0.name == "code" })?.value {
Task { @MainActor in
do {
let token = try await self.getToken(code: code)
await apiService.fbVerifyAssertion(provider: "google.com", idToken: token.id_token, accessToken: token.access_token)
self.dismiss(animated: true, completion: self.completion)
} catch {
HUD.flash(.labeledError(title: nil, subtitle: error.localizedDescription))
}
}
return .cancel
}
}
}
}
return .allow
}
@IBAction func close(_ sender: UIBarButtonItem) {
self.dismiss(animated: true, completion: nil)
}
func sha256(string: String) -> String? {
guard let data = string.data(using: .utf8) else { return nil }
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
}
return String(data: Data(Base64FS.encode(data: hash)), encoding: .utf8)?.trimmingCharacters(in: CharacterSet(charactersIn: "="))
}
func getToken(code: String) async throws -> TokenResponse {
let tokenUrlString = Constants.googleTokenURL
+ "?grant_type=authorization_code"
+ "&code=" + code
+ "&redirect_uri=" + Constants.googleRedirectURL
+ "&client_id=" + Constants.fbClientId
+ "&code_verifier=" + self.codeVerifier
if let url = URL(string: tokenUrlString) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(TokenResponse.self, from: data)
} else {
throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Bad URL"])
}
}
}

View File

@ -0,0 +1,36 @@
//
// GoogleAuthScreen.swift
// AutoCat
//
// Created by Selim Mustafaev on 14.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
struct GoogleAuthScreen: View {
@State var viewModel = makeViewModel()
@Environment(\.dismiss) var dismiss
var body: some View {
WebView(url: viewModel.url, userAgent: Constants.userAgent)
.decidePolicy { navAction in
let policy = await viewModel.decidePolicy(for: navAction)
if policy == .cancel {
dismiss()
}
return policy
}
.hud($viewModel.hud)
}
static func makeViewModel() -> GoogleAuthViewModel {
let resolver = ServiceContainer.shared
return GoogleAuthViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self)
)
}
}

View File

@ -0,0 +1,97 @@
//
// GoogleAuthViewModel.swift
// AutoCat
//
// Created by Selim Mustafaev on 14.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import AutoCatCore
import SwiftUI
import CommonCrypto
import WebKit
@MainActor
@Observable
final class GoogleAuthViewModel: ACHudContainer {
let apiService: ApiServiceProtocol
var hud: ACHud?
let codeVerifier = UUID().uuidString
var url = URL(fileURLWithPath: "")
init(
apiService: ApiServiceProtocol
) {
self.apiService = apiService
do {
url = try makeAuthURL()
} catch {
hud = .error(error)
}
}
func makeAuthURL() throws -> URL {
guard let codeChallenge = sha256(string: codeVerifier) else {
throw GenericError.somethingWentWrong
}
let authUrlString = Constants.googleAuthURL
+ "?response_type=code"
+ "&code_challenge_method=S256"
+ "&scope=email%20profile"
+ "&redirect_uri=" + Constants.googleRedirectURL
+ "&client_id=" + Constants.fbClientId
+ "&code_challenge=" + codeChallenge
if let url = URL(string: authUrlString) {
return url
} else {
throw GenericError.somethingWentWrong
}
}
func sha256(string: String) -> String? {
guard let data = string.data(using: .utf8) else {
return nil
}
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
}
let hashData = Data(Base64FS.encode(data: hash))
return String(data: hashData, encoding: .utf8)?
.trimmingCharacters(in: CharacterSet(charactersIn: "="))
}
func decidePolicy(for navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
guard let url = navigationAction.request.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems,
let code = queryItems.first(where: { $0.name == "code" })?.value
else {
return .allow
}
await wrapWithToast { [weak self] in
guard let self else { return }
let token = try await apiService.getToken(
code: code,
codeVerifier: codeVerifier
)
await apiService.fbVerifyAssertion(
provider: "google.com",
idToken: token.id_token,
accessToken: token.access_token
)
}
return .cancel
}
}

View File

@ -0,0 +1,61 @@
//
// WebView.swift
// AutoCat
//
// Created by Selim Mustafaev on 14.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import WebKit
import SwiftUI
struct WebView: UIViewRepresentable {
class Coordinator: NSObject, WKNavigationDelegate {
var decidePolicyClosure: ((WKNavigationAction) async -> WKNavigationActionPolicy)?
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
if let decidePolicyClosure {
return await decidePolicyClosure(navigationAction)
} else {
return .allow
}
}
}
let url: URL
let userAgent: String?
@State private var delegate = Coordinator()
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
webView.customUserAgent = userAgent
webView.navigationDelegate = context.coordinator
let request = URLRequest(url: url)
webView.load(request)
}
func makeCoordinator() -> Coordinator {
delegate
}
}
extension WebView {
func decidePolicy(closure: @escaping (WKNavigationAction) async -> WKNavigationActionPolicy) -> Self {
delegate.decidePolicyClosure = closure
return self
}
}

View File

@ -13,7 +13,8 @@ struct SettingsScreen: View {
@State var viewModel: SettingsViewModel
@State var googleSheetOpened = false
@State var googleLoginSheetOpened = false
var body: some View {
Form {
Section {
@ -29,7 +30,7 @@ struct SettingsScreen: View {
if viewModel.googleAuthorized {
googleSheetOpened = true
} else {
viewModel.googleSignIn()
googleLoginSheetOpened = true
}
}
@ -83,6 +84,9 @@ struct SettingsScreen: View {
Text("You are currently signed in with email \(email). It will help to gather more data about vehicles.")
}
}
.sheet(isPresented: $googleLoginSheetOpened) {
GoogleAuthScreen()
}
}
}

View File

@ -82,10 +82,6 @@ class SettingsViewModel {
coordinator?.openAuthScreen()
}
func googleSignIn() {
coordinator?.openGoogleOauthPage()
}
func googleSignout() {
settingService.user.firebaseIdToken = nil
settingService.user.firebaseRefreshToken = nil

View File

@ -14,6 +14,7 @@ public enum ApiError: LocalizedError, Equatable {
case emptyResponse
case unauthorized
case threadSafety
case badUrl
case message(String)
case httpError(Int)
@ -25,6 +26,7 @@ public enum ApiError: LocalizedError, Equatable {
case .threadSafety: "Thread safety error"
case .message(let message): message
case .httpError(let status): "General http error (status \(status))"
case .badUrl: "Bad url"
}
}
}

View File

@ -193,6 +193,25 @@ public actor ApiService: ApiServiceProtocol {
}
}
public func getToken(code: String, codeVerifier: String) async throws -> TokenResponse {
let tokenUrlString = Constants.googleTokenURL
+ "?grant_type=authorization_code"
+ "&code=" + code
+ "&redirect_uri=" + Constants.googleRedirectURL
+ "&client_id=" + Constants.fbClientId
+ "&code_verifier=" + codeVerifier
if let url = URL(string: tokenUrlString) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(TokenResponse.self, from: data)
} else {
throw ApiError.badUrl
}
}
// MARK: - AutoCat public API
public func login(email: String, password: String) async throws -> User {

View File

@ -34,5 +34,6 @@ public protocol ApiServiceProtocol: Sendable {
func getVehicles(with filter: Filter, pageToken: String?, pageSize: Int) async throws -> PagedResponse<VehicleDto>
func fbVerifyAssertion(provider: String, idToken: String, accessToken: String?) async
func getToken(code: String, codeVerifier: String) async throws -> TokenResponse
func getReport(for number: String) async throws -> VehicleDto
}

View File

@ -0,0 +1,17 @@
//
// TokenResponse.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 14.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
public struct TokenResponse: Codable, Sendable {
public var id_token: String
public var refresh_token: String?
public var access_token: String
public var expires_in: Int
public var token_type: String
public var scope: String
}

View File

@ -52,4 +52,6 @@ public enum Constants {
public static let reportLinkBaseURL = "https://auto.aliencat.pro/report.html"
public static let audioRecordsFolder = "recordings"
public static let userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1"
}