Merge pull request #832 from omnivore-app/google-ios-signin-update

Google ios signin update
This commit is contained in:
Satindar Dhillon
2022-06-21 18:29:59 -07:00
committed by GitHub
27 changed files with 256 additions and 627 deletions

View File

@ -28,6 +28,14 @@
<string>omnivore</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.267918240109-bdghlau7nsq2480c4l8gdgh6mrarokta</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>

View File

@ -32,6 +32,14 @@
<string>omnivore</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.267918240109-bdghlau7nsq2480c4l8gdgh6mrarokta</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>

View File

@ -1244,6 +1244,7 @@
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
@ -1274,6 +1275,7 @@
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
@ -1560,6 +1562,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.Omnivore-Extension";
PRODUCT_NAME = SafariExtension;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
@ -1597,6 +1600,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.Omnivore-Extension";
PRODUCT_NAME = SafariExtension;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
@ -1664,7 +1668,8 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = Entitlements/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QJF2XZ86HB;
INFOPLIST_FILE = InfoPlists/ShareExtension.plist;
@ -1677,6 +1682,7 @@
MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension";
PRODUCT_NAME = ShareExtension;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_COMPILATION_MODE = wholemodule;
@ -1746,7 +1752,8 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = Entitlements/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QJF2XZ86HB;
INFOPLIST_FILE = InfoPlists/ShareExtension.plist;
@ -1759,6 +1766,7 @@
MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension";
PRODUCT_NAME = ShareExtension;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;

View File

@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/openid/AppAuth-iOS.git",
"state" : {
"revision" : "01131d68346c8ae552961c768d583c715fbe1410",
"version" : "1.4.0"
"revision" : "33660c271c961f8ce1084cc13f2ea8195e864f7d",
"version" : "1.5.0"
}
},
{
@ -72,6 +72,15 @@
"version" : "9.1.2"
}
},
{
"identity" : "googlesignin-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleSignIn-iOS",
"state" : {
"revision" : "9450e779619fc184d360c9f7ce61023587f7e1f4",
"version" : "6.2.2"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
@ -99,6 +108,15 @@
"version" : "1.7.0"
}
},
{
"identity" : "gtmappauth",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GTMAppAuth.git",
"state" : {
"revision" : "b9d1683be336ba8c8d1c6867bafeb056a5399700",
"version" : "1.3.0"
}
},
{
"identity" : "intercom-ios",
"kind" : "remoteSourceControl",

View File

@ -32,7 +32,7 @@ let package = Package(
.target(
name: "Services",
dependencies: [
.product(name: "AppAuth", package: "AppAuth-iOS"),
.product(name: "GoogleSignIn", package: "GoogleSignIn-iOS"),
"Valet",
.product(name: "SwiftGraphQL", package: "swift-graphql"),
"Models",
@ -63,11 +63,11 @@ var appPackageDependencies: [Target.Dependency] {
var dependencies: [Package.Dependency] {
var deps: [Package.Dependency] = [
.package(url: "https://github.com/openid/AppAuth-iOS.git", .upToNextMajor(from: "1.4.0")),
.package(url: "https://github.com/Square/Valet", from: "4.1.2"),
.package(url: "https://github.com/maticzav/swift-graphql", from: "2.3.1"),
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"),
.package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0"))
.package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2")
]
// #if canImport(UIKit)
deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master"))

View File

@ -1,4 +1,3 @@
import Combine
import Foundation
import Models
import Services

View File

@ -1,4 +1,3 @@
import Combine
import CoreData
import Models
import Services
@ -11,8 +10,6 @@ import Views
let item: LinkedItem?
@Published var webAppWrapperViewModel: WebAppWrapperViewModel?
var subscriptions = Set<AnyCancellable>()
init(linkedItemObjectID: NSManagedObjectID, dataService: DataService) {
if let linkedItem = dataService.viewContext.object(with: linkedItemObjectID) as? LinkedItem {
self.pdfItem = PDFItem.make(item: linkedItem)

View File

@ -5,7 +5,7 @@ import SwiftUI
import Utils
import Views
final class CreateProfileViewModel: ObservableObject {
@MainActor final class CreateProfileViewModel: ObservableObject {
private(set) var initialUserProfile = UserProfile(username: "", name: "", bio: nil)
var isConfigured = false
@ -39,7 +39,9 @@ final class CreateProfileViewModel: ObservableObject {
switch profileOrError {
case let .left(userProfile):
submitProfile(userProfile: userProfile, authenticator: authenticator)
Task {
await submitProfile(userProfile: userProfile, authenticator: authenticator)
}
case let .right(errorMessage):
validationErrorMessage = errorMessage
}
@ -51,41 +53,37 @@ final class CreateProfileViewModel: ObservableObject {
return
}
dataService.validateUsernamePublisher(username: username).sink(
receiveCompletion: { [weak self] completion in
guard case let .failure(usernameError) = completion else { return }
Task {
do {
try await dataService.validateUsernamePublisher(username: username)
} catch {
let usernameError = (error as? UsernameAvailabilityError) ?? .unknown
switch usernameError {
case .tooShort:
self?.potentialUsernameStatus = .tooShort
potentialUsernameStatus = .tooShort
case .tooLong:
self?.potentialUsernameStatus = .tooLong
potentialUsernameStatus = .tooLong
case .invalidPattern:
self?.potentialUsernameStatus = .invalidPattern
potentialUsernameStatus = .invalidPattern
case .nameUnavailable:
self?.potentialUsernameStatus = .unavailable
potentialUsernameStatus = .unavailable
case .internalServer, .unknown:
self?.loginError = .unknown
loginError = .unknown
case .network:
self?.loginError = .network
loginError = .network
}
},
receiveValue: { [weak self] in
self?.potentialUsernameStatus = .available
}
)
.store(in: &subscriptions)
}
}
func submitProfile(userProfile: UserProfile, authenticator: Authenticator) {
authenticator
.createAccount(userProfile: userProfile).sink(
receiveCompletion: { [weak self] completion in
guard case let .failure(loginError) = completion else { return }
self?.loginError = loginError
},
receiveValue: { _ in }
)
.store(in: &subscriptions)
func submitProfile(userProfile: UserProfile, authenticator: Authenticator) async {
do {
try await authenticator.createAccount(userProfile: userProfile)
} catch {
if let error = error as? LoginError {
loginError = error
}
}
}
func configure(profile: UserProfile, dataService: DataService) {

View File

@ -1,27 +1,22 @@
import Combine
import Models
import Services
import SwiftUI
import Utils
import Views
final class NewAppleSignupViewModel: ObservableObject {
@MainActor final class NewAppleSignupViewModel: ObservableObject {
@Published var loginError: LoginError?
var subscriptions = Set<AnyCancellable>()
init() {}
func submitProfile(userProfile: UserProfile, authenticator: Authenticator) {
authenticator
.createAccount(userProfile: userProfile).sink(
receiveCompletion: { [weak self] completion in
guard case let .failure(loginError) = completion else { return }
self?.loginError = loginError
},
receiveValue: { _ in }
)
.store(in: &subscriptions)
func submitProfile(userProfile: UserProfile, authenticator: Authenticator) async {
do {
try await authenticator.createAccount(userProfile: userProfile)
} catch {
if let error = error as? LoginError {
loginError = error
}
}
}
}
@ -48,7 +43,11 @@ struct NewAppleSignupView: View {
VStack {
Button(
action: { viewModel.submitProfile(userProfile: userProfile, authenticator: authenticator) },
action: {
Task {
await viewModel.submitProfile(userProfile: userProfile, authenticator: authenticator)
}
},
label: { Text("Continue") }
)
.buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300))

View File

@ -1,12 +1,11 @@
import AuthenticationServices
import Combine
import Models
import Services
import SwiftUI
import Utils
import Views
final class RegistrationViewModel: ObservableObject {
@MainActor final class RegistrationViewModel: ObservableObject {
enum RegistrationState {
case createProfile(userProfile: UserProfile)
case newAppleSignUp(userProfile: UserProfile)
@ -15,12 +14,10 @@ final class RegistrationViewModel: ObservableObject {
@Published var loginError: LoginError?
@Published var registrationState: RegistrationState?
var subscriptions = Set<AnyCancellable>()
func handleAppleSignInCompletion(result: Result<ASAuthorization, Error>, authenticator: Authenticator) {
switch AppleSigninPayload.parse(authResult: result) {
case let .success(payload):
handleAppleToken(payload: payload, authenticator: authenticator)
Task { await handleAppleToken(payload: payload, authenticator: authenticator) }
case let .failure(error):
switch error {
case .unauthorized, .unknown:
@ -31,56 +28,48 @@ final class RegistrationViewModel: ObservableObject {
}
}
private func handleAppleToken(payload: AppleSigninPayload, authenticator: Authenticator) {
authenticator.submitAppleToken(token: payload.token).sink(
receiveCompletion: { [weak self] completion in
guard case let .failure(loginError) = completion else { return }
switch loginError {
case .unauthorized, .unknown:
self?.handleAppleSignUp(authenticator: authenticator, payload: payload)
case .network:
self?.loginError = loginError
}
},
receiveValue: { _ in }
)
.store(in: &subscriptions)
private func handleAppleToken(payload: AppleSigninPayload, authenticator: Authenticator) async {
do {
try await authenticator.submitAppleToken(token: payload.token)
} catch {
let submitTokenError = (error as? LoginError) ?? .unknown
switch submitTokenError {
case .unauthorized, .unknown:
await handleAppleSignUp(authenticator: authenticator, payload: payload)
case .network:
loginError = submitTokenError
}
}
}
private func handleAppleSignUp(authenticator: Authenticator, payload: AppleSigninPayload) {
authenticator
.createPendingAccountUsingApple(token: payload.token, name: payload.fullName)
.sink(
receiveCompletion: { [weak self] completion in
guard case let .failure(loginError) = completion else { return }
self?.loginError = loginError
},
receiveValue: { [weak self] userProfile in
if userProfile.name.isEmpty {
self?.registrationState = .createProfile(userProfile: userProfile)
} else {
self?.registrationState = .newAppleSignUp(userProfile: userProfile)
}
}
private func handleAppleSignUp(authenticator: Authenticator, payload: AppleSigninPayload) async {
do {
let pendingUserProfile = try await authenticator.createPendingAccountUsingApple(
token: payload.token,
name: payload.fullName
)
.store(in: &subscriptions)
if pendingUserProfile.name.isEmpty {
registrationState = .createProfile(userProfile: pendingUserProfile)
} else {
registrationState = .newAppleSignUp(userProfile: pendingUserProfile)
}
} catch {
loginError = (error as? LoginError) ?? .unknown
}
}
func handleGoogleAuth(authenticator: Authenticator) {
authenticator
.handleGoogleAuth(presentingViewController: presentingViewController())
.sink(
receiveCompletion: { [weak self] completion in
guard case let .failure(loginError) = completion else { return }
self?.loginError = loginError
},
receiveValue: { [weak self] isNewAccount in
if isNewAccount {
self?.registrationState = .createProfile(userProfile: UserProfile(username: "", name: ""))
}
}
)
.store(in: &subscriptions)
func handleGoogleAuth(authenticator: Authenticator) async {
guard let presentingViewController = presentingViewController() else { return }
let googleAuthResponse = await authenticator.handleGoogleAuth(presenting: presentingViewController)
switch googleAuthResponse {
case let .loginError(error):
loginError = error
case .newOmnivoreUser:
registrationState = .createProfile(userProfile: UserProfile(username: "", name: ""))
case .existingOmnivoreUser:
break
}
}
}

View File

@ -97,6 +97,7 @@ struct InnerRootView: View {
}
}
#endif
.onOpenURL { Authenticator.handleGoogleURL(url: $0) }
}
#if os(iOS)

View File

@ -1,4 +1,3 @@
import Combine
import Models
import Services
import SwiftUI
@ -139,7 +138,9 @@ struct WelcomeView: View {
if AppKeys.sharedInstance?.iosClientGoogleId != nil {
GoogleAuthButton {
viewModel.handleGoogleAuth(authenticator: authenticator)
Task {
await viewModel.handleGoogleAuth(authenticator: authenticator)
}
}
}
}

View File

@ -1,4 +1,3 @@
import Combine
import Foundation
import Models
@ -6,12 +5,12 @@ public extension Authenticator {
func createPendingAccountUsingApple(
token: String,
name: PersonNameComponents?
) -> AnyPublisher<UserProfile, LoginError> {
) async throws -> UserProfile {
let params = CreatePendingAccountParams(token: token, provider: .apple, fullName: name)
return createPendingAccount(params: params)
return try await createPendingAccount(params: params)
}
func createAccount(userProfile: UserProfile) -> AnyPublisher<Void, LoginError> {
func createAccount(userProfile: UserProfile) async throws {
let params = CreateAccountParams(
pendingUserToken: pendingUserToken ?? "",
userProfile: userProfile
@ -19,36 +18,29 @@ public extension Authenticator {
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
return networker
.createAccount(params: encodedParams)
.tryMap { [weak self] in
try ValetKey.authCookieString.setValue($0.commentedAuthCookieString)
try ValetKey.authToken.setValue($0.authToken)
self?.pendingUserToken = nil
self?.isLoggedIn = true
do {
let authPayload = try await networker.createAccount(params: encodedParams)
try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString)
try ValetKey.authToken.setValue(authPayload.authToken)
DispatchQueue.main.async {
self.pendingUserToken = nil
self.isLoggedIn = true
}
.mapError { error in
let serverError = (error as? ServerError) ?? ServerError.unknown
return LoginError.make(serverError: serverError)
}
.eraseToAnyPublisher()
} catch {
let serverError = (error as? ServerError) ?? ServerError.unknown
throw LoginError.make(serverError: serverError)
}
}
}
extension Authenticator {
func createPendingAccount(params: CreatePendingAccountParams) -> AnyPublisher<UserProfile, LoginError> {
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
return networker
.createPendingUser(params: encodedParams)
.tryMap { [weak self] in
self?.pendingUserToken = $0.pendingUserToken
return $0.pendingUserProfile
}
.mapError { error in
let serverError = (error as? ServerError) ?? ServerError.unknown
return LoginError.make(serverError: serverError)
}
.eraseToAnyPublisher()
func createPendingAccount(params: CreatePendingAccountParams) async throws -> UserProfile {
do {
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
let pendingUserAuthPayload = try await networker.createPendingUser(params: encodedParams)
return pendingUserAuthPayload.pendingUserProfile
} catch {
throw LoginError.make(serverError: (error as? ServerError) ?? .unknown)
}
}
}

View File

@ -1,20 +1,18 @@
import Combine
import Foundation
import Models
public extension Authenticator {
func submitAppleToken(token: String) -> AnyPublisher<Void, LoginError> {
networker
.submitAppleToken(token: token)
.tryMap { [weak self] in
try ValetKey.authCookieString.setValue($0.commentedAuthCookieString)
try ValetKey.authToken.setValue($0.authToken)
self?.isLoggedIn = true
func submitAppleToken(token: String) async throws {
do {
let authPayload = try await networker.submitAppleToken(token: token)
try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString)
try ValetKey.authToken.setValue(authPayload.authToken)
DispatchQueue.main.async {
self.isLoggedIn = true
}
.mapError { error in
let serverError = (error as? ServerError) ?? ServerError.unknown
return LoginError.make(serverError: serverError)
}
.eraseToAnyPublisher()
} catch {
let serverError = (error as? ServerError) ?? ServerError.unknown
throw LoginError.make(serverError: serverError)
}
}
}

View File

@ -1,6 +1,5 @@
import AppAuth
import Combine
import Foundation
import GoogleSignIn
import Models
import Utils
import WebKit
@ -8,6 +7,10 @@ import WebKit
public final class Authenticator: ObservableObject {
public static var unregisterIntercomUser: (() -> Void)?
public static func handleGoogleURL(url: URL) {
GIDSignIn.sharedInstance.handle(url)
}
public enum AuthStatus {
case loggedOut
case pendingUser
@ -15,18 +18,11 @@ public final class Authenticator: ObservableObject {
}
@Published public internal(set) var isLoggedIn: Bool
@Published public var pendinguserProfile = UserProfile(username: "", name: "", bio: nil)
let networker: Networker
var subscriptions = Set<AnyCancellable>()
var currentAuthorizationFlow: OIDExternalUserAgentSession?
var pendingUserToken: String?
#if os(macOS)
var authRedirectHandler: OIDRedirectHTTPHandler?
#endif
public init(networker: Networker) {
self.networker = networker
self.isLoggedIn = ValetKey.authToken.exists

View File

@ -1,123 +1,73 @@
import AppAuth
import Combine
import Foundation
import GoogleSignIn
import Models
import Utils
#if os(iOS)
import UIKit
public enum GoogleAuthResponse {
case loginError(error: LoginError)
case newOmnivoreUser
case existingOmnivoreUser
}
public extension Authenticator {
func handleGoogleAuth(presentingViewController: PlatformViewController?) -> AnyPublisher<Bool, LoginError> {
Future { [weak self] promise in
guard let self = self, let presenting = presentingViewController else { return }
extension Authenticator {
public func handleGoogleAuth(presenting: PlatformViewController) async -> GoogleAuthResponse {
let idToken = try? await googleSignIn(presenting: presenting)
guard let idToken = idToken else { return .loginError(error: .unauthorized) }
// swiftlint:disable:next line_length
self.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: self.googleAuthRequest(redirectURL: nil), presenting: presenting) { authState, authError in
self.resolveAuthResponse(promise: promise, authState: authState, authError: authError)
}
do {
let authPayload = try await networker.submitGoogleToken(idToken: idToken)
try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString)
try ValetKey.authToken.setValue(authPayload.authToken)
DispatchQueue.main.async {
self.isLoggedIn = true
}
.eraseToAnyPublisher()
}
}
#endif
return .existingOmnivoreUser
} catch {
let loginError = (error as? LoginError) ?? .unknown
#if os(macOS)
import AppKit
public extension Authenticator {
func handleGoogleAuth(presentingViewController _: PlatformViewController?) -> AnyPublisher<Bool, LoginError> {
authRedirectHandler = OIDRedirectHTTPHandler(
successURL: URL(string: "https://omnivore.app")!
)
let redirectURL = authRedirectHandler?.startHTTPListener(nil)
let authRequest = googleAuthRequest(redirectURL: redirectURL)
return Future { [weak self] promise in
guard let self = self else { return }
// swiftlint:disable:next line_length
self.authRedirectHandler?.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: authRequest) { authState, authError in
NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
self.resolveAuthResponse(promise: promise, authState: authState, authError: authError)
}
switch loginError {
case .unauthorized, .unknown:
return await createPendingUser(idToken: idToken)
case .network:
return .loginError(error: .network)
}
.eraseToAnyPublisher()
}
}
#endif
private extension Authenticator {
func resolveAuthResponse(
promise: @escaping (Result<Bool, LoginError>) -> Void,
authState: OIDAuthState?,
authError: Error?
) {
if let idToken = authState?.lastTokenResponse?.idToken {
Task {
do {
let authPayload = try await networker.submitGoogleToken(idToken: idToken)
try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString)
try ValetKey.authToken.setValue(authPayload.authToken)
DispatchQueue.main.async {
self.isLoggedIn = true
}
} catch {
if let error = error as? LoginError {
switch error {
case .unauthorized, .unknown:
self.resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError)
case .network:
promise(.failure(error))
}
self.resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError)
}
}
}
} else {
resolveAuthResponseForAccountCreation(promise: promise, authState: authState, authError: authError)
}
}
func resolveAuthResponseForAccountCreation(
promise: @escaping (Result<Bool, LoginError>) -> Void,
authState: OIDAuthState?,
authError _: Error?
) {
if let idToken = authState?.lastTokenResponse?.idToken {
func createPendingUser(idToken: String) async -> GoogleAuthResponse {
do {
let params = CreatePendingAccountParams(token: idToken, provider: .google, fullName: nil)
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
networker
.createPendingUser(params: encodedParams)
.sink { completion in
guard case let .failure(serverError) = completion else { return }
promise(.failure(LoginError.make(serverError: serverError)))
} receiveValue: { [weak self] in
self?.pendingUserToken = $0.pendingUserToken
promise(.success(true))
}
.store(in: &subscriptions)
} else {
promise(.failure(.unauthorized))
let pendingUserAuthPayload = try await networker.createPendingUser(params: encodedParams)
pendingUserToken = pendingUserAuthPayload.pendingUserToken
return .newOmnivoreUser
} catch {
let loginError = LoginError.make(serverError: (error as? ServerError) ?? .unknown)
return .loginError(error: loginError)
}
}
func googleAuthRequest(redirectURL: URL?) -> OIDAuthorizationRequest {
let authEndpoint = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")!
let tokenEndpoint = URL(string: "https://www.googleapis.com/oauth2/v4/token")!
let iosClientGoogleId = AppKeys.sharedInstance?.iosClientGoogleId ?? ""
let scopes = ["profile", "email"]
func googleSignIn(presenting: PlatformViewController) async throws -> String {
try await withCheckedThrowingContinuation { continuation in
let clientID = "\(AppKeys.sharedInstance?.iosClientGoogleId ?? "").apps.googleusercontent.com"
GIDSignIn.sharedInstance.signIn(
with: GIDConfiguration(clientID: clientID),
presenting: presenting
) { user, error in
guard let user = user, error == nil else {
continuation.resume(throwing: LoginError.unauthorized)
return
}
return OIDAuthorizationRequest(
configuration: OIDServiceConfiguration(authorizationEndpoint: authEndpoint, tokenEndpoint: tokenEndpoint),
clientId: "\(iosClientGoogleId).apps.googleusercontent.com",
scopes: scopes,
redirectURL: redirectURL ?? URL(
string: "com.googleusercontent.apps.\(iosClientGoogleId):/oauth2redirect/google"
)!,
responseType: "code",
additionalParameters: nil
)
user.authentication.do { authentication, error in
guard let idToken = authentication?.idToken, error == nil else {
continuation.resume(throwing: LoginError.unauthorized)
return
}
continuation.resume(returning: idToken)
}
}
}
}
}

View File

@ -1,4 +1,3 @@
import Combine
import CoreData
import CoreImage
import Foundation
@ -19,7 +18,6 @@ public final class DataService: ObservableObject {
var persistentContainer: PersistentContainer
public var backgroundContext: NSManagedObjectContext
var subscriptions = Set<AnyCancellable>()
public var viewContext: NSManagedObjectContext {
persistentContainer.viewContext

View File

@ -1,224 +0,0 @@
import Combine
import Foundation
import Models
import SwiftGraphQL
public enum SaveArticleStatus {
case succeeeded
case processing(jobId: String)
case failed
static func make(jobId: String, savingStatus: Enums.ArticleSavingRequestStatus) -> SaveArticleStatus {
switch savingStatus {
case .processing:
return .processing(jobId: jobId)
case .succeeded:
return .succeeeded
case .failed:
return .failed
}
}
}
public extension Networker {
func articleSaveStatus(jobId: String) -> AnyPublisher<SaveArticleStatus, SaveArticleError> {
enum QueryResult {
case saved(status: SaveArticleStatus)
case error(errorCode: Enums.ArticleSavingRequestErrorCode)
}
let selection = Selection<QueryResult, Unions.ArticleSavingRequestResult> {
try $0.on(
articleSavingRequestError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .notFound) },
articleSavingRequestSuccess: .init {
.saved(
status: try $0.articleSavingRequest(
selection: .init {
SaveArticleStatus.make(
jobId: try $0.id(),
savingStatus: try $0.status()
)
}
)
)
}
)
}
let query = Selection.Query {
try $0.articleSavingRequest(id: jobId, selection: selection)
}
let path = appEnvironment.graphqlPath
let headers = defaultHeaders
return Deferred {
Future { promise in
send(query, to: path, headers: headers) { result in
switch result {
case let .success(payload):
if let graphqlError = payload.errors {
promise(.failure(.unknown(description: graphqlError.first.debugDescription)))
}
switch payload.data {
case let .saved(status):
promise(.success(status))
case let .error(errorCode: errorCode):
switch errorCode {
case .unauthorized:
promise(.failure(.unauthorized))
case .notFound:
promise(.failure(.badData))
}
}
case let .failure(error):
promise(.failure(SaveArticleError.make(from: error)))
}
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
public extension DataService {
// swiftlint:disable:next function_body_length
func saveArticlePublisher(
pageScrapePayload: PageScrapePayload,
uploadFileId: String?
) -> AnyPublisher<Void, SaveArticleError> {
enum MutationResult {
case saved(created: Bool)
case error(errorCode: Enums.CreateArticleErrorCode)
}
let preparedDocument: InputObjects.PreparedDocumentInput? = {
if case let .html(html, title, _) = pageScrapePayload.contentType {
return InputObjects.PreparedDocumentInput(
document: html,
pageInfo: InputObjects.PageInfoInput(title: OptionalArgument(title))
)
}
return nil
}()
let input = InputObjects.CreateArticleInput(
preparedDocument: OptionalArgument(preparedDocument),
uploadFileId: uploadFileId != nil ? .present(uploadFileId!) : .null(),
url: pageScrapePayload.url
)
let selection = Selection<MutationResult, Unions.CreateArticleResult> {
try $0.on(
createArticleError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .unableToParse) },
createArticleSuccess: .init { .saved(created: try $0.created()) }
)
}
let mutation = Selection.Mutation {
try $0.createArticle(input: input, selection: selection)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return Deferred {
Future { promise in
send(mutation, to: path, headers: headers) { result in
switch result {
case let .success(payload):
if let graphqlError = payload.errors {
promise(.failure(.unknown(description: graphqlError.first.debugDescription)))
}
switch payload.data {
case .saved:
promise(.success(()))
case let .error(errorCode: errorCode):
switch errorCode {
case .unauthorized:
promise(.failure(.unauthorized))
default:
promise(.failure(.unknown(description: errorCode.rawValue)))
}
}
case let .failure(error):
promise(.failure(SaveArticleError.make(from: error)))
}
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
func saveArticlePublisher(articleURL: URL) -> AnyPublisher<SaveArticleStatus, SaveArticleError> {
saveArticlePublisher(articleURLString: articleURL.absoluteString)
}
func saveArticlePublisher(articleURLString: String) -> AnyPublisher<SaveArticleStatus, SaveArticleError> {
enum MutationResult {
case saved(status: SaveArticleStatus)
case error(errorCode: Enums.CreateArticleSavingRequestErrorCode)
}
let selection = Selection<MutationResult, Unions.CreateArticleSavingRequestResult> {
try $0.on(
createArticleSavingRequestError: .init { .error(errorCode: (try? $0.errorCodes().first) ?? .badData) },
createArticleSavingRequestSuccess: .init {
.saved(
status: try $0.articleSavingRequest(
selection: .init {
SaveArticleStatus.make(
jobId: try $0.id(),
savingStatus: try $0.status()
)
}
)
)
}
)
}
let mutation = Selection.Mutation {
try $0.createArticleSavingRequest(
input: InputObjects.CreateArticleSavingRequestInput(url: articleURLString),
selection: selection
)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return Deferred {
Future { promise in
send(mutation, to: path, headers: headers) { result in
switch result {
case let .success(payload):
if let graphqlError = payload.errors {
promise(.failure(.unknown(description: graphqlError.first.debugDescription)))
}
switch payload.data {
case let .saved(status):
promise(.success(status))
case let .error(errorCode: errorCode):
switch errorCode {
case .unauthorized:
promise(.failure(.unauthorized))
case .badData:
promise(.failure(.badData))
}
}
case let .failure(error):
promise(.failure(SaveArticleError.make(from: error)))
}
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}

View File

@ -1,4 +1,3 @@
import Combine
import Foundation
import Models
import SwiftGraphQL

View File

@ -39,7 +39,7 @@ extension Networker {
)
do {
let authVerification = try await urlSession.performReq(resource: resource)
let authVerification = try await urlSession.performRequest(resource: resource)
return authVerification.authStatus.isAuthenticated
} catch {
return false

View File

@ -1,4 +1,3 @@
import Combine
import Foundation
import Models
import Utils
@ -37,7 +36,7 @@ extension ServerResponse {
struct EmptyResponse: Decodable {}
extension URLSession {
func performReq<ResponseModel>(
func performRequest<ResponseModel>(
resource: ServerResource<ResponseModel>
) async throws -> ResponseModel {
do {
@ -61,30 +60,6 @@ extension URLSession {
throw ServerError(serverResponse: serverResponse)
}
}
// TODO: remove performRequest
// swiftlint:disable:next line_length
func performRequest<ResponseModel>(resource: ServerResource<ResponseModel>) -> AnyPublisher<ResponseModel, ServerError> {
let request = resource.urlRequest
return dataTaskPublisher(for: resource.urlRequest)
.tryMap { data, response -> ResponseModel in
let serverResponse = ServerResponse(data: data, response: response)
NetworkRequestLogger.log(request: request, serverResponse: serverResponse)
if let decodedValue = resource.decode(serverResponse) {
return decodedValue
}
throw ServerError(serverResponse: serverResponse)
}
.mapError { error -> ServerError in
let serverResponse = ServerResponse(error: error)
NetworkRequestLogger.log(request: request, serverResponse: serverResponse)
return ServerError(serverResponse: serverResponse)
}
.eraseToAnyPublisher()
}
}
extension URLRequest {

View File

@ -1,8 +1,7 @@
import Combine
import Foundation
extension Networker {
func createAccount(params: Data) -> AnyPublisher<AuthPayload, ServerError> {
func createAccount(params: Data) async throws -> AuthPayload {
let urlRequest = URLRequest.create(
baseURL: appEnvironment.serverBaseURL,
urlPath: "/api/mobile-auth/create-account",
@ -14,9 +13,10 @@ extension Networker {
decode: AuthPayload.decode
)
return urlSession
.performRequest(resource: resource)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
do {
return try await urlSession.performRequest(resource: resource)
} catch {
throw (error as? ServerError) ?? .unknown
}
}
}

View File

@ -1,9 +1,8 @@
import Combine
import Foundation
import Models
extension Networker {
func createPendingUser(params: Data) -> AnyPublisher<PendingUserAuthPayload, ServerError> {
func createPendingUser(params: Data) async throws -> PendingUserAuthPayload {
let urlRequest = URLRequest.create(
baseURL: appEnvironment.serverBaseURL,
urlPath: "/api/mobile-auth/sign-up",
@ -15,9 +14,10 @@ extension Networker {
decode: PendingUserAuthPayload.decode
)
return urlSession
.performRequest(resource: resource)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
do {
return try await urlSession.performRequest(resource: resource)
} catch {
throw (error as? ServerError) ?? .unknown
}
}
}

View File

@ -1,26 +0,0 @@
import Combine
import Foundation
import Models
extension Networker {
func submitAppleToken(token: String) -> AnyPublisher<AuthPayload, ServerError> {
let params = SignInParams(token: token, provider: .apple)
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
let urlRequest = URLRequest.create(
baseURL: appEnvironment.serverBaseURL,
urlPath: "/api/mobile-auth/sign-in",
requestMethod: .post(params: encodedParams)
)
let resource = ServerResource<AuthPayload>(
urlRequest: urlRequest,
decode: AuthPayload.decode
)
return urlSession
.performRequest(resource: resource)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}

View File

@ -2,8 +2,17 @@ import Foundation
import Models
extension Networker {
func submitAppleToken(token: String) async throws -> AuthPayload {
let params = SignInParams(token: token, provider: .apple)
return try await submitSignInParams(params: params)
}
func submitGoogleToken(idToken: String) async throws -> AuthPayload {
let params = SignInParams(token: idToken, provider: .google)
return try await submitSignInParams(params: params)
}
func submitSignInParams(params: SignInParams) async throws -> AuthPayload {
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
let urlRequest = URLRequest.create(
@ -18,7 +27,7 @@ extension Networker {
)
do {
return try await urlSession.performReq(resource: resource)
return try await urlSession.performRequest(resource: resource)
} catch {
if let error = error as? ServerError {
throw LoginError.make(serverError: error)

View File

@ -1,30 +1,29 @@
import Combine
import Foundation
import Models
import SwiftGraphQL
public extension DataService {
func validateUsernamePublisher(username: String) -> AnyPublisher<Void, UsernameAvailabilityError> {
func validateUsernamePublisher(username: String) async throws {
let query = Selection.Query {
try $0.validateUsername(username: username)
}
let path = appEnvironment.graphqlPath
return Deferred {
Future { promise in
send(query, to: path) { result in
switch result {
case let .success(payload):
promise(payload.data ? .success(()) : .failure(.nameUnavailable))
case let .failure(error):
promise(.failure(UsernameAvailabilityError.make(from: error)))
return try await withCheckedThrowingContinuation { continuation in
send(query, to: path) { result in
switch result {
case let .success(payload):
if payload.data {
continuation.resume()
} else {
continuation.resume(throwing: UsernameAvailabilityError.nameUnavailable)
}
case let .failure(error):
continuation.resume(throwing: UsernameAvailabilityError.make(from: error))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}

View File

@ -1,63 +0,0 @@
import Combine
import SwiftUI
#if !os(macOS)
import UIKit
#endif
public extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
#if os(iOS)
let willShow = NotificationCenter.default
.publisher(for: UIApplication.keyboardWillShowNotification)
.map(\.keyboardHeight)
.eraseToAnyPublisher()
let willHide = NotificationCenter.default
.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
.eraseToAnyPublisher()
return Merge(willShow, willHide)
.eraseToAnyPublisher()
#elseif os(macOS)
Future { $0(.success(CGFloat.zero)) }
.eraseToAnyPublisher()
#endif
}
}
extension Notification {
var keyboardHeight: CGFloat {
#if os(iOS)
(userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
#elseif os(macOS)
0
#endif
}
}
struct KeyboardAdaptive: ViewModifier {
@State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
content
.padding(.bottom, keyboardHeight)
.onReceive(Publishers.keyboardHeight) { self.keyboardHeight = $0 }
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}
extension View {
func hideKeyboard() {
#if os(iOS)
UIApplication.shared
.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
#endif
}
}