Merge pull request #832 from omnivore-app/google-ios-signin-update
Google ios signin update
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import Models
|
||||
import Services
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -97,6 +97,7 @@ struct InnerRootView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.onOpenURL { Authenticator.handleGoogleURL(url: $0) }
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user