From 41578579ef4fb78b59b470a4f37ae9b88a450932 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Mon, 14 Apr 2025 23:00:32 +0300 Subject: [PATCH] SwiftUI version of google oauth signin controller --- AutoCat.xcodeproj/project.pbxproj | 30 ++++- AutoCat/Base.lproj/Main.storyboard | 56 --------- .../Controllers/GoogleSignInController.swift | 108 ------------------ .../GoogleAuthScreen/GoogleAuthScreen.swift | 36 ++++++ .../GoogleAuthViewModel.swift | 97 ++++++++++++++++ .../Screens/GoogleAuthScreen/WebView.swift | 61 ++++++++++ .../SettingsScreen/SettingsScreen.swift | 8 +- .../SettingsScreen/SettingsViewModel.swift | 4 - .../Services/ApiService/ApiError.swift | 2 + .../Services/ApiService/ApiService.swift | 19 +++ .../ApiService/ApiServiceProtocol.swift | 1 + .../ApiService}/Response.swift | 0 .../Services/ApiService/TokenResponse.swift | 17 +++ AutoCatCore/Utils/Constants.swift | 2 + 14 files changed, 266 insertions(+), 175 deletions(-) delete mode 100644 AutoCat/Controllers/GoogleSignInController.swift create mode 100644 AutoCat/Screens/GoogleAuthScreen/GoogleAuthScreen.swift create mode 100644 AutoCat/Screens/GoogleAuthScreen/GoogleAuthViewModel.swift create mode 100644 AutoCat/Screens/GoogleAuthScreen/WebView.swift rename AutoCatCore/{Models => Services/ApiService}/Response.swift (100%) create mode 100644 AutoCatCore/Services/ApiService/TokenResponse.swift diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index fba88ac..c6f43de 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -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 = ""; }; 7A961C6B2C4C3C8600CE2211 /* TextRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRowView.swift; sourceTree = ""; }; 7A961C6D2C4C3C9E00CE2211 /* LinkRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkRowView.swift; sourceTree = ""; }; - 7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSignInController.swift; sourceTree = ""; }; 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 = ""; }; 7A96AE32246C095700297C33 /* Base64FS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64FS.swift; sourceTree = ""; }; @@ -478,9 +480,13 @@ 7ADF6C96250F41B000F237B2 /* PNKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNKeyboard.swift; sourceTree = ""; }; 7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers.otf; sourceTree = ""; }; 7ADF6CA02512244400F237B2 /* MapExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapExt.swift; sourceTree = ""; }; + 7ADFC9562DAD0288001A43E3 /* GoogleAuthScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthScreen.swift; sourceTree = ""; }; + 7ADFC9582DAD1C3D001A43E3 /* GoogleAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthViewModel.swift; sourceTree = ""; }; + 7ADFC95A2DAD1F45001A43E3 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; 7AE24C5E251F1B4E00758E39 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = ""; }; 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = ""; }; 7AE8424D26109F78002F6B31 /* Exportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exportable.swift; sourceTree = ""; }; + 7AEAA2A02DAD9C00009954F0 /* TokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenResponse.swift; sourceTree = ""; }; 7AF231922DA1C28100AE5EB3 /* AuthScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthScreen.swift; sourceTree = ""; }; 7AF231942DA1C29300AE5EB3 /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = ""; }; 7AF231962DA1C30000AE5EB3 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; @@ -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 = ""; @@ -1127,6 +1134,16 @@ path = Extensions; sourceTree = ""; }; + 7ADFC9552DAD026C001A43E3 /* GoogleAuthScreen */ = { + isa = PBXGroup; + children = ( + 7ADFC9562DAD0288001A43E3 /* GoogleAuthScreen.swift */, + 7ADFC9582DAD1C3D001A43E3 /* GoogleAuthViewModel.swift */, + 7ADFC95A2DAD1F45001A43E3 /* WebView.swift */, + ); + path = GoogleAuthScreen; + sourceTree = ""; + }; 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 */, diff --git a/AutoCat/Base.lproj/Main.storyboard b/AutoCat/Base.lproj/Main.storyboard index e0948fe..3789fe4 100644 --- a/AutoCat/Base.lproj/Main.storyboard +++ b/AutoCat/Base.lproj/Main.storyboard @@ -3,60 +3,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -84,9 +33,4 @@ - - - - - diff --git a/AutoCat/Controllers/GoogleSignInController.swift b/AutoCat/Controllers/GoogleSignInController.swift deleted file mode 100644 index d843d1b..0000000 --- a/AutoCat/Controllers/GoogleSignInController.swift +++ /dev/null @@ -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"]) - } - } -} diff --git a/AutoCat/Screens/GoogleAuthScreen/GoogleAuthScreen.swift b/AutoCat/Screens/GoogleAuthScreen/GoogleAuthScreen.swift new file mode 100644 index 0000000..9c880ed --- /dev/null +++ b/AutoCat/Screens/GoogleAuthScreen/GoogleAuthScreen.swift @@ -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) + ) + } +} diff --git a/AutoCat/Screens/GoogleAuthScreen/GoogleAuthViewModel.swift b/AutoCat/Screens/GoogleAuthScreen/GoogleAuthViewModel.swift new file mode 100644 index 0000000..7a7e3c1 --- /dev/null +++ b/AutoCat/Screens/GoogleAuthScreen/GoogleAuthViewModel.swift @@ -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 + } +} diff --git a/AutoCat/Screens/GoogleAuthScreen/WebView.swift b/AutoCat/Screens/GoogleAuthScreen/WebView.swift new file mode 100644 index 0000000..fb9f9a9 --- /dev/null +++ b/AutoCat/Screens/GoogleAuthScreen/WebView.swift @@ -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 + } +} diff --git a/AutoCat/Screens/SettingsScreen/SettingsScreen.swift b/AutoCat/Screens/SettingsScreen/SettingsScreen.swift index 37e1c43..76e2c0d 100644 --- a/AutoCat/Screens/SettingsScreen/SettingsScreen.swift +++ b/AutoCat/Screens/SettingsScreen/SettingsScreen.swift @@ -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() + } } } diff --git a/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift b/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift index cace17b..17cf031 100644 --- a/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift +++ b/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift @@ -82,10 +82,6 @@ class SettingsViewModel { coordinator?.openAuthScreen() } - func googleSignIn() { - coordinator?.openGoogleOauthPage() - } - func googleSignout() { settingService.user.firebaseIdToken = nil settingService.user.firebaseRefreshToken = nil diff --git a/AutoCatCore/Services/ApiService/ApiError.swift b/AutoCatCore/Services/ApiService/ApiError.swift index 6766378..6c4c851 100644 --- a/AutoCatCore/Services/ApiService/ApiError.swift +++ b/AutoCatCore/Services/ApiService/ApiError.swift @@ -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" } } } diff --git a/AutoCatCore/Services/ApiService/ApiService.swift b/AutoCatCore/Services/ApiService/ApiService.swift index 0dca813..22d5471 100644 --- a/AutoCatCore/Services/ApiService/ApiService.swift +++ b/AutoCatCore/Services/ApiService/ApiService.swift @@ -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 { diff --git a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift index a744589..b029414 100644 --- a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift +++ b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift @@ -34,5 +34,6 @@ public protocol ApiServiceProtocol: Sendable { func getVehicles(with filter: Filter, pageToken: String?, pageSize: Int) async throws -> PagedResponse 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 } diff --git a/AutoCatCore/Models/Response.swift b/AutoCatCore/Services/ApiService/Response.swift similarity index 100% rename from AutoCatCore/Models/Response.swift rename to AutoCatCore/Services/ApiService/Response.swift diff --git a/AutoCatCore/Services/ApiService/TokenResponse.swift b/AutoCatCore/Services/ApiService/TokenResponse.swift new file mode 100644 index 0000000..90847a1 --- /dev/null +++ b/AutoCatCore/Services/ApiService/TokenResponse.swift @@ -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 +} diff --git a/AutoCatCore/Utils/Constants.swift b/AutoCatCore/Utils/Constants.swift index 5f54947..8051d74 100644 --- a/AutoCatCore/Utils/Constants.swift +++ b/AutoCatCore/Utils/Constants.swift @@ -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" }