Skip to content
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

Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
49e4674
feat: add login with
eyatsenkoperpetio Oct 12, 2023
87a826b
chore: remove spacing
Oct 19, 2023
2a44cfc
Merge branch 'develop' into feat/social_sign
Oct 19, 2023
9d068d1
fix: padding bottom social view
eyatsenkoperpetio Oct 26, 2023
ada811f
chore: result social
eyatsenkoperpetio Oct 31, 2023
bf8ac9b
chore: add new struct for credentials
eyatsenkoperpetio Nov 1, 2023
155455d
chore: add request social login
eyatsenkoperpetio Nov 1, 2023
e51e380
chore: add request and change design buttons
eyatsenkoperpetio Nov 6, 2023
3be7334
chore: add handle MSAL Response
eyatsenkoperpetio Nov 7, 2023
616612b
Merge branch 'develop' into 103-social-login-and-authentication-mobil…
eyatsenkoperpetio Nov 21, 2023
42a2ffa
chore: add ocial login enabled feature flag
eyatsenkoperpetio Nov 21, 2023
b5189f9
chore: add configs and flags
eyatsenkoperpetio Nov 22, 2023
1f07709
chore: rename file
eyatsenkoperpetio Nov 23, 2023
5324265
Merge branch 'develop' into 103-social-login-and-authentication-mobil…
eyatsenkoperpetio Nov 23, 2023
38d547e
chore: add accessibility, localizable strings uk, and flags for socials
eyatsenkoperpetio Nov 23, 2023
4d675a9
chore: remove extra code
eyatsenkoperpetio Nov 23, 2023
5b36b8d
Merge branch 'develop' into 103-social-login-and-authentication-mobil…
eyatsenkoperpetio Nov 24, 2023
6e8f767
chore: add google config
eyatsenkoperpetio Nov 27, 2023
42b48a0
chore: move enabled google sign in to google config
eyatsenkoperpetio Nov 27, 2023
4f1c544
chore: add new dict social login
eyatsenkoperpetio Nov 27, 2023
57a6c60
chore: remove extra code, change names, add colors
eyatsenkoperpetio Nov 28, 2023
abeeaf5
refactor: open URL options key method
eyatsenkoperpetio Nov 28, 2023
2761c84
refactor: add SocialSignView previews
eyatsenkoperpetio Nov 28, 2023
49189c7
chore: add apple sign in config, pr issues
eyatsenkoperpetio Nov 28, 2023
98f5b99
chore: improvement sign up with third party auth, new error if third…
eyatsenkoperpetio Nov 30, 2023
a69d9b8
chore: add not linked acc error when login by third party auth
eyatsenkoperpetio Dec 1, 2023
f2c960f
chore: add same canceled error
eyatsenkoperpetio Dec 4, 2023
3381774
Merge branch 'develop' into 103-social-login-and-authentication-mobil…
eyatsenkoperpetio Dec 4, 2023
ab752e1
chore: add error if not register by social auth
eyatsenkoperpetio Dec 4, 2023
9085988
fix: fix msal lib
eyatsenkoperpetio Dec 5, 2023
1f1d4de
Merge branch 'develop' into 103-social-login-and-authentication-mobil…
eyatsenkoperpetio Dec 5, 2023
9397651
chore: login if can when register if social provider and add name ema…
eyatsenkoperpetio Dec 5, 2023
d7dfc24
chore: save full name after login with apple
eyatsenkoperpetio Dec 5, 2023
05cd91b
chore: add microsoft to package
eyatsenkoperpetio Dec 5, 2023
c7a01c6
chore: changes from PR feedback
eyatsenkoperpetio Dec 6, 2023
10bf647
chore: changes from PR feedback
eyatsenkoperpetio Dec 6, 2023
19bc1d0
chore: add tests
eyatsenkoperpetio Dec 6, 2023
05b5c80
chore: changes from PR feedback
eyatsenkoperpetio Dec 7, 2023
c25bcf7
chore: remove extra code
eyatsenkoperpetio Dec 7, 2023
39ff6b3
chore: remove extra code and add tests
eyatsenkoperpetio Dec 7, 2023
9f000cb
chore: changes for saving apple info to appstorage
eyatsenkoperpetio Dec 7, 2023
a9816ef
chore: remove extra code and change social auth details
eyatsenkoperpetio Dec 7, 2023
5a0e838
chore: change LoginMethod -> AuthMethod and remove extra code
eyatsenkoperpetio Dec 7, 2023
c6f7f04
chore: change names
eyatsenkoperpetio Dec 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Authorization/Authorization.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
0770DE6B28D0C035006D8A5D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE6D28D0C035006D8A5D /* Localizable.strings */; };
0770DE7128D0C0E7006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE7028D0C0E7006D8A5D /* Strings.swift */; };
5FB79D2802949372CDAF08D6 /* Pods_App_Authorization_AuthorizationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FAE9B7FD61FF88C9C4FE1E8 /* Pods_App_Authorization_AuthorizationTests.framework */; };
BA8B3A322AD5487300D25EF5 /* SocialSignView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A312AD5487300D25EF5 /* SocialSignView.swift */; };
BADB3F552AD6DFC3004D5CFA /* SocialSignViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F542AD6DFC3004D5CFA /* SocialSignViewModel.swift */; };
DE843D6BB1B9DDA398494890 /* Pods_App_Authorization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47BCFB7C19382EECF15131B6 /* Pods_App_Authorization.framework */; };
E03261642AE64676002CA7EB /* StartupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261632AE64676002CA7EB /* StartupViewModel.swift */; };
E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261652AE64AF4002CA7EB /* StartupView.swift */; };
Expand Down Expand Up @@ -78,6 +80,8 @@
96C85172770225EB81A6D2DA /* Pods-App-Authorization.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.releasedev.xcconfig"; sourceTree = "<group>"; };
9BF6A1004A955E24527FCF0F /* Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig"; sourceTree = "<group>"; };
A99D45203C981893C104053A /* Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig"; sourceTree = "<group>"; };
BA8B3A312AD5487300D25EF5 /* SocialSignView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialSignView.swift; sourceTree = "<group>"; };
BADB3F542AD6DFC3004D5CFA /* SocialSignViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialSignViewModel.swift; sourceTree = "<group>"; };
E03261632AE64676002CA7EB /* StartupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupViewModel.swift; sourceTree = "<group>"; };
E03261652AE64AF4002CA7EB /* StartupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupView.swift; sourceTree = "<group>"; };
E03261672AE9F156002CA7EB /* LogistrationBottomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogistrationBottomView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -145,6 +149,7 @@
071009CC28D1E24000344290 /* Presentation */ = {
isa = PBXGroup;
children = (
BA8B3A302AD5485100D25EF5 /* SocialSign */,
Copy link
Contributor

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 both SignIn and Register? Thoughts?

E03261622AE6464A002CA7EB /* Startup */,
020C31BD290AADA700D6DEA2 /* Base */,
071009C528D1D9FA00344290 /* Login */,
Expand Down Expand Up @@ -265,6 +270,15 @@
path = ../Pods;
sourceTree = "<group>";
};
BA8B3A302AD5485100D25EF5 /* SocialSign */ = {
isa = PBXGroup;
children = (
BA8B3A312AD5487300D25EF5 /* SocialSignView.swift */,
BADB3F542AD6DFC3004D5CFA /* SocialSignViewModel.swift */,
);
path = SocialSign;
sourceTree = "<group>";
};
E03261622AE6464A002CA7EB /* Startup */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -484,6 +498,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BADB3F552AD6DFC3004D5CFA /* SocialSignViewModel.swift in Sources */,
02066B442906D72400F4307E /* SignUpView.swift in Sources */,
0770DE7128D0C0E7006D8A5D /* Strings.swift in Sources */,
025F40E229D360E20064C183 /* ResetPasswordViewModel.swift in Sources */,
Expand All @@ -497,6 +512,7 @@
02F3BFE5292533720051930C /* AuthorizationRouter.swift in Sources */,
E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */,
071009C728D1DA4F00344290 /* SignInViewModel.swift in Sources */,
BA8B3A322AD5487300D25EF5 /* SocialSignView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

public enum LoginMethod: String {
case apple = "Apple"
Copy link
Contributor

Choose a reason for hiding this comment

The 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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ public struct SignInView: View {
.padding(.top, 40)
}
}
if viewModel.socialLoginEnabled {
SocialSignView(
viewModel: .init(
config: viewModel.config,
completion: { viewModel.sign(with: $0) }
)
)
}
Spacer()
}
.padding(.horizontal, 24)
Expand Down
129 changes: 112 additions & 17 deletions Authorization/Authorization/Presentation/Login/SignInViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
}

Expand All @@ -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>) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

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 socailAuth(....)?

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you populate isExternalToken locally? like if the LoginMethid isn't a password, that means it's external?

isShowProgress = false
if let validationError = error.validationError,
let value = validationError.data?["error_description"] as? String {
if isExternalToken, validationError.statusCode == 400, let loginMethod = loginMethod {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please handle 403 as well in the case of social auths with the message Your account is disabled. Please contact customer support for assistance.

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
Expand Up @@ -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}

Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we achieving with this where clause?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Expand Down
Loading
Loading