-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add social authentication options including - Apple, Microsoft, Facebook, and Google #176
Changes from 34 commits
49e4674
87a826b
2a44cfc
9d068d1
ada811f
bf8ac9b
155455d
e51e380
3be7334
616612b
42a2ffa
b5189f9
1f07709
5324265
38d547e
4d675a9
5b36b8d
6e8f767
42b48a0
4f1c544
57a6c60
abeeaf5
2761c84
49189c7
98f5b99
a69d9b8
f2c960f
3381774
ab752e1
9085988
1f1d4de
9397651
d7dfc24
05cd91b
c7a01c6
10bf647
19bc1d0
05b5c80
c25bcf7
39ff6b3
9f000cb
a9816ef
5a0e838
c6f7f04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import Foundation | ||
|
||
public enum LoginMethod: String { | ||
case apple = "Apple" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The enum name is LoginMethod; I guess in this case, the methods are Password and SocialAuth. How about keeping two values and passing the social auth provider(Apple, Google, Facebook, etc) as a parameter to social auth? |
||
case password = "Password" | ||
case facebook = "Facebook" | ||
case google = "Google" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,9 +9,13 @@ import Foundation | |
import Core | ||
import SwiftUI | ||
import Alamofire | ||
import AuthenticationServices | ||
import FacebookLogin | ||
import GoogleSignIn | ||
import MSAL | ||
|
||
public class SignInViewModel: ObservableObject { | ||
|
||
@Published private(set) var isShowProgress = false | ||
@Published private(set) var showError: Bool = false | ||
@Published private(set) var showAlert: Bool = false | ||
|
@@ -35,7 +39,7 @@ public class SignInViewModel: ObservableObject { | |
private let interactor: AuthInteractorProtocol | ||
private let analytics: AuthorizationAnalytics | ||
private let validator: Validator | ||
|
||
public init( | ||
interactor: AuthInteractorProtocol, | ||
router: AuthorizationRouter, | ||
|
@@ -49,15 +53,19 @@ public class SignInViewModel: ObservableObject { | |
self.analytics = analytics | ||
self.validator = validator | ||
} | ||
|
||
|
||
var socialLoginEnabled: Bool { | ||
config.socialLoginEnabled | ||
} | ||
|
||
@MainActor | ||
func login(username: String, password: String) async { | ||
guard validator.isValidUsername(username) else { | ||
errorMessage = AuthLocalization.Error.invalidEmailAddressOrUsername | ||
return | ||
} | ||
guard !password.isEmpty else { | ||
errorMessage = AuthLocalization.Error.invalidPasswordLength | ||
errorMessage = AuthLocalization.Error.invalidPasswordLenght | ||
return | ||
} | ||
|
||
|
@@ -68,27 +76,114 @@ public class SignInViewModel: ObservableObject { | |
analytics.userLogin(method: .password) | ||
router.showMainOrWhatsNewScreen() | ||
} catch let error { | ||
isShowProgress = false | ||
if error.isUpdateRequeiredError { | ||
router.showUpdateRequiredView(showAccountLink: false) | ||
} else if let validationError = error.validationError, | ||
let value = validationError.data?["error_description"] as? String { | ||
errorMessage = value | ||
} else if case APIError.invalidGrant = error { | ||
errorMessage = CoreLocalization.Error.invalidCredentials | ||
} else if error.isInternetError { | ||
errorMessage = CoreLocalization.Error.slowOrNoInternetConnection | ||
failure(error) | ||
} | ||
} | ||
|
||
@MainActor | ||
func sign(with result: Result<SocialResult, Error>) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please give it a descriptive name? it's hard to understand the functionality of the function by looking at the name. |
||
result.success { social(result: $0) } | ||
result.failure { error in | ||
errorMessage = error.localizedDescription | ||
} | ||
} | ||
|
||
@MainActor | ||
private func social(result: SocialResult) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about renaming it to |
||
switch result { | ||
case .apple(let appleCredentials): | ||
appleLogin(appleCredentials, backend: result.backend) | ||
case .facebook: | ||
facebookLogin(backend: result.backend) | ||
case .google(let gIDSignInResult): | ||
googleLogin(gIDSignInResult, backend: result.backend) | ||
case .microsoft(_, let token): | ||
microsoftLogin(token, backend: result.backend) | ||
} | ||
} | ||
|
||
@MainActor | ||
private func appleLogin(_ credentials: AppleCredentials, backend: String) { | ||
socialLogin( | ||
externalToken: credentials.token, | ||
backend: backend, | ||
loginMethod: .apple | ||
) | ||
} | ||
|
||
@MainActor | ||
private func facebookLogin(backend: String) { | ||
guard let currentAccessToken = AccessToken.current?.tokenString else { | ||
return | ||
} | ||
socialLogin( | ||
externalToken: currentAccessToken, | ||
backend: backend, | ||
loginMethod: .facebook | ||
) | ||
} | ||
|
||
@MainActor | ||
private func googleLogin(_ result: GIDSignInResult, backend: String) { | ||
socialLogin( | ||
externalToken: result.user.accessToken.tokenString, | ||
backend: backend, | ||
loginMethod: .google | ||
) | ||
} | ||
|
||
@MainActor | ||
private func microsoftLogin(_ token: String, backend: String) { | ||
socialLogin( | ||
externalToken: token, | ||
backend: backend, | ||
loginMethod: .microsoft | ||
) | ||
} | ||
|
||
@MainActor | ||
private func socialLogin(externalToken: String, backend: String, loginMethod: LoginMethod) { | ||
Task { | ||
isShowProgress = true | ||
do { | ||
let user = try await interactor.login(externalToken: externalToken, backend: backend) | ||
analytics.setUserID("\(user.id)") | ||
analytics.userLogin(method: loginMethod) | ||
router.showMainOrWhatsNewScreen() | ||
} catch let error { | ||
failure(error, loginMethod: loginMethod, isExternalToken: true) | ||
} | ||
} | ||
} | ||
|
||
@MainActor | ||
private func failure(_ error: Error, loginMethod: LoginMethod? = nil, isExternalToken: Bool = false) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you populate |
||
isShowProgress = false | ||
if let validationError = error.validationError, | ||
let value = validationError.data?["error_description"] as? String { | ||
if isExternalToken, validationError.statusCode == 400, let loginMethod = loginMethod { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please handle |
||
errorMessage = AuthLocalization.Error.authProvider( | ||
loginMethod.rawValue, | ||
config.platformName | ||
) | ||
} else { | ||
errorMessage = CoreLocalization.Error.unknownError | ||
errorMessage = value | ||
} | ||
} else if case APIError.invalidGrant = error { | ||
errorMessage = CoreLocalization.Error.invalidCredentials | ||
} else if error.isInternetError { | ||
errorMessage = CoreLocalization.Error.slowOrNoInternetConnection | ||
} else { | ||
errorMessage = CoreLocalization.Error.unknownError | ||
} | ||
} | ||
|
||
func trackSignUpClicked() { | ||
analytics.signUpClicked() | ||
} | ||
|
||
func trackForgotPasswordClicked() { | ||
analytics.forgotPasswordClicked() | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,7 +67,17 @@ public struct SignUpView: View { | |
.font(Theme.Fonts.titleSmall) | ||
.foregroundColor(Theme.Colors.textPrimary) | ||
.padding(.bottom, 20) | ||
|
||
|
||
if viewModel.isThirdPartyAuthSuccess { | ||
Text(AuthLocalization.SignUp.successSignedinLabel) | ||
.font(Theme.Fonts.titleMedium) | ||
.foregroundColor(Theme.Colors.textPrimary) | ||
Text(AuthLocalization.SignUp.successSignedinSublabel) | ||
.font(Theme.Fonts.titleSmall) | ||
.foregroundColor(Theme.Colors.textSecondary) | ||
.padding(.bottom, 20) | ||
} | ||
|
||
let requiredFields = viewModel.fields.filter {$0.field.required} | ||
let nonRequiredFields = viewModel.fields.filter {!$0.field.required} | ||
|
||
|
@@ -98,15 +108,26 @@ public struct SignUpView: View { | |
}.frame(maxWidth: .infinity) | ||
} else { | ||
StyledButton(AuthLocalization.SignUp.createAccountBtn) { | ||
viewModel.isThirdPartyAuthSuccess = false | ||
Task { | ||
await viewModel.registerUser() | ||
} | ||
viewModel.trackCreateAccountClicked() | ||
} | ||
.padding(.top, 40) | ||
.padding(.bottom, 80) | ||
.frame(maxWidth: .infinity) | ||
} | ||
if viewModel.socialLoginEnabled, | ||
!requiredFields.isEmpty { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are we achieving with this where clause? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when we open sign up screen, make request to get fields and I show only when requiredFields come |
||
SocialSignView( | ||
signType: .register, | ||
viewModel: .init( | ||
config: viewModel.config, | ||
completion: { viewModel.register(with: $0) } | ||
) | ||
) | ||
.padding(.bottom, 30) | ||
} | ||
Spacer() | ||
} | ||
.padding(.horizontal, 24) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about renaming it to
SocialAuth
because it's being used for bothSignIn
andRegister
? Thoughts?