From 7bc4688240f6153495995ef80a5f789407b866f3 Mon Sep 17 00:00:00 2001 From: Tayyab Akram Date: Tue, 11 Feb 2025 13:45:02 +0500 Subject: [PATCH 1/2] chore: improve analytics for sign up and sign in flows --- .../Presentation/AuthorizationAnalytics.swift | 16 ++- .../Presentation/Login/SignInView.swift | 7 +- .../Presentation/Login/SignInViewModel.swift | 35 ++++- .../Registration/SignUpView.swift | 8 +- .../Registration/SignUpViewModel.swift | 29 +++- .../SocialAuth/SocialAuthView.swift | 27 ++-- .../SocialAuth/SocialAuthViewModel.swift | 58 +++++--- .../AuthorizationMock.generated.swift | 128 +++++++++++++++--- .../Login/SignInViewModelTests.swift | 8 +- .../Register/SignUpViewModelTests.swift | 2 +- Core/Core/Analytics/CoreAnalytics.swift | 10 ++ .../SocialAuth/AppleAuthProvider.swift | 22 +-- .../SocialAuth/Error/SocialAuthError.swift | 19 ++- .../SocialAuth/FacebookAuthProvider.swift | 23 ++-- .../SocialAuth/GoogleAuthProvider.swift | 10 +- .../SocialAuth/MicrosoftAuthProvider.swift | 40 +++--- .../AnalyticsManager/AnalyticsManager.swift | 66 ++++++++- 17 files changed, 378 insertions(+), 130 deletions(-) diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift index 9fcd13b6..2bf9b854 100644 --- a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift +++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift @@ -35,9 +35,13 @@ public protocol AuthorizationAnalytics { func userLogin(method: AuthMethod) func registerClicked() func signInClicked() - func userSignInClicked() - func createAccountClicked() + func userSignInClicked(method: String) + func socialRegisterClicked(method: String) + func createAccountClicked(method: String) func registrationSuccess(method: String) + func socialAuthFailure(method: String, errorCode: String?, errorMessage: String?) + func registerFailure(method: String, errorCode: String?, errorMessage: String?) + func signInFailure(method: String, errorCode: String?, errorMessage: String?) func forgotPasswordClicked() func resetPasswordClicked() func resetPassword(success: Bool) @@ -50,9 +54,13 @@ class AuthorizationAnalyticsMock: AuthorizationAnalytics { public func userLogin(method: AuthMethod) {} public func registerClicked() {} public func signInClicked() {} - public func userSignInClicked() {} - public func createAccountClicked() {} + public func userSignInClicked(method: String) {} + public func socialRegisterClicked(method: String) {} + public func createAccountClicked(method: String) {} public func registrationSuccess(method: String) {} + public func socialAuthFailure(method: String, errorCode: String?, errorMessage: String?) {} + public func registerFailure(method: String, errorCode: String?, errorMessage: String?) {} + public func signInFailure(method: String, errorCode: String?, errorMessage: String?) {} public func forgotPasswordClicked() {} public func resetPasswordClicked() {} public func resetPassword(success: Bool) {} diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index e6aec453..10c2c567 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -74,12 +74,7 @@ public struct SignInView: View { if viewModel.socialAuthEnabled { SocialAuthView( - viewModel: .init( - config: viewModel.config, - lastUsedOption: viewModel.storage.lastUsedSocialAuth - ) { result in - Task { await viewModel.login(with: result) } - } + viewModel: viewModel.socialAuthViewModel ) .padding(.top, 22) .padding(.bottom, 16) diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index 13f84e67..5c2ef087 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -67,6 +67,20 @@ public class SignInViewModel: ObservableObject { config.microsoft.enabled || config.google.enabled } + + lazy var socialAuthViewModel = SocialAuthViewModel( + config: config, + analytics: analytics, + authType: .signIn, + lastUsedOption: storage.lastUsedSocialAuth, + completion: { [weak self] method, result in + guard let self else { return } + + Task { + await self.login(with: method, result: result) + } + } + ) @MainActor func login(username: String, password: String) async { @@ -78,7 +92,7 @@ public class SignInViewModel: ObservableObject { errorMessage = AuthLocalization.Error.invalidPasswordLenght return } - analytics.userSignInClicked() + analytics.userSignInClicked(method: AuthMethod.password.analyticsValue) isShowProgress = true do { let user = try await interactor.login(username: username, password: password) @@ -87,12 +101,12 @@ public class SignInViewModel: ObservableObject { router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen, postLoginData: nil) NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch let error { - failure(error) + failure(error, authMethod: .password) } } @MainActor - func login(with result: Result) async { + func login(with method: SocialAuthMethod, result: Result) async { switch result { case .success(let result): await socialLogin( @@ -101,6 +115,11 @@ public class SignInViewModel: ObservableObject { authMethod: result.authMethod ) case .failure(let error): + analytics.signInFailure( + method: AuthMethod.socailAuth(method).analyticsValue, + errorCode: error.errorCode.flatMap { String($0) }, + errorMessage: error.errorDescription + ) errorMessage = error.localizedDescription } } @@ -128,11 +147,11 @@ public class SignInViewModel: ObservableObject { } @MainActor - private func failure(_ error: Error, authMethod: AuthMethod? = nil) { + private func failure(_ error: Error, authMethod: AuthMethod) { isShowProgress = false if let validationError = error.validationError, let value = validationError.data?["error_description"] as? String { - if authMethod != .password, validationError.statusCode == 400, let authMethod = authMethod { + if authMethod != .password, validationError.statusCode == 400 { errorMessage = AuthLocalization.Error.accountNotRegistered( authMethod.analyticsValue, config.platformName @@ -149,6 +168,12 @@ public class SignInViewModel: ObservableObject { } else { errorMessage = CoreLocalization.Error.unknownError } + + analytics.signInFailure( + method: authMethod.analyticsValue, + errorCode: nil, + errorMessage: errorMessage + ) } func trackForgotPasswordClicked() { diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 9232ec3e..9366588c 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -93,13 +93,7 @@ public struct SignUpView: View { if viewModel.socialAuthEnabled, !requiredFields.isEmpty { SocialAuthView( - authType: .register, - viewModel: .init( - config: viewModel.config, - lastUsedOption: viewModel.storage.lastUsedSocialAuth - ) { result in - Task { await viewModel.register(with: result) } - } + viewModel: viewModel.socialAuthViewModel ) .padding(.top, 22) .padding(.bottom, -2) diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index 8ec43645..c1a55800 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -84,6 +84,20 @@ public class SignUpViewModel: ObservableObject { config.google.enabled return socialLoginEnabled && !thirdPartyAuthSuccess && !isShowProgress } + + lazy var socialAuthViewModel = SocialAuthViewModel( + config: config, + analytics: analytics, + authType: .register, + lastUsedOption: storage.lastUsedSocialAuth, + completion: { [weak self] method, result in + guard let self else { return } + + Task { + await self.register(with: method, result: result) + } + } + ) private func showErrors(errors: [String: String]) -> Bool { if thirdPartyAuthSuccess, !errors.map({ $0.value }).filter({ !$0.isEmpty }).isEmpty { @@ -153,6 +167,12 @@ public class SignUpViewModel: ObservableObject { } else { errorMessage = CoreLocalization.Error.unknownError } + + analytics.registerFailure( + method: authMetod.analyticsValue, + errorCode: nil, + errorMessage: errorMessage + ) } } @@ -174,7 +194,7 @@ public class SignUpViewModel: ObservableObject { } @MainActor - func register(with result: Result) async { + func register(with method: SocialAuthMethod, result: Result) async { switch result { case .success(let result): await loginOrRegister( @@ -183,6 +203,11 @@ public class SignUpViewModel: ObservableObject { authMethod: result.authMethod ) case .failure(let error): + analytics.socialAuthFailure( + method: AuthMethod.socailAuth(method).analyticsValue, + errorCode: error.errorCode.flatMap { String($0) }, + errorMessage: error.errorDescription + ) errorMessage = error.localizedDescription } } @@ -222,7 +247,7 @@ public class SignUpViewModel: ObservableObject { } func trackCreateAccountClicked() { - analytics.createAccountClicked() + analytics.createAccountClicked(method: authMethod.analyticsValue) } func trackScreenEvent() { diff --git a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift index 6fda03cb..f61bb029 100644 --- a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift +++ b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift @@ -9,31 +9,26 @@ import SwiftUI import Core import Theme +enum SocialAuthType { + case signIn + case register +} + struct SocialAuthView: View { // MARK: - Properties @StateObject var viewModel: SocialAuthViewModel - init( - authType: SocialAuthType = .signIn, - viewModel: SocialAuthViewModel - ) { + init(viewModel: SocialAuthViewModel) { self._viewModel = .init(wrappedValue: viewModel) - self.authType = authType - } - - enum SocialAuthType { - case signIn - case register } - var authType: SocialAuthType = .signIn private var title: String { AuthLocalization.continueWith } private var bottomViewText: String { - switch authType { + switch viewModel.authType { case .signIn: AuthLocalization.orSignInWith case .register: @@ -64,7 +59,7 @@ struct SocialAuthView: View { private var buttonsView: some View { HStack { if let lastOption = viewModel.lastUsedOption, - authType == .signIn { + viewModel.authType == .signIn { Text(AuthLocalization.lastSignIn) .font(Theme.Fonts.bodySmall) .foregroundStyle(Theme.Colors.textPrimary) @@ -83,7 +78,7 @@ struct SocialAuthView: View { HStack { ForEach(viewModel.enabledOptions, id: \.self) { option in - if option != viewModel.lastUsedOption || authType != .signIn { + if option != viewModel.lastUsedOption || viewModel.authType != .signIn { socialAuthButton(option) .padding(.trailing, option == viewModel.enabledOptions.last ? 0 : 12) } @@ -145,8 +140,10 @@ struct SocialSignView_Previews: PreviewProvider { static var previews: some View { let vm = SocialAuthViewModel( config: ConfigMock(), + analytics: AuthorizationAnalyticsMock(), + authType: .signIn, lastUsedOption: nil, - completion: { _ in } + completion: { _, _ in } ) SocialAuthView(viewModel: vm).padding() } diff --git a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift index 625e5361..d41c62d1 100644 --- a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift +++ b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift @@ -56,22 +56,28 @@ enum SocialAuthDetails { } } -final public class SocialAuthViewModel: ObservableObject { +final class SocialAuthViewModel: ObservableObject { // MARK: - Properties - private var completion: ((Result) -> Void) + private var completion: ((SocialAuthMethod, Result) -> Void) private let config: ConfigProtocol - + private let analytics: AuthorizationAnalytics + + let authType: SocialAuthType @Published var lastUsedOption: SocialAuthMethod? var enabledOptions: [SocialAuthMethod] = [] init( config: ConfigProtocol, + analytics: AuthorizationAnalytics, + authType: SocialAuthType, lastUsedOption: String?, - completion: @escaping (Result) -> Void + completion: @escaping (SocialAuthMethod, Result) -> Void ) { self.config = config + self.analytics = analytics + self.authType = authType self.completion = completion if let lastUsedOption { self.lastUsedOption = SocialAuthMethod(rawValue: lastUsedOption) @@ -136,10 +142,12 @@ final public class SocialAuthViewModel: ObservableObject { // MARK: - Public Intens func signInWithApple() { + trackClickEvent(for: .apple) + appleAuthProvider.request { [weak self] result in guard let self else { return } - result.success { self.success(with: .apple($0)) } - result.failure(self.failure) + result.success { self.success(with: .apple, details: .apple($0)) } + result.failure { self.failure(with: .apple, error: $0) } } } @@ -148,9 +156,11 @@ final public class SocialAuthViewModel: ObservableObject { guard let vc = topViewController else { return } + trackClickEvent(for: .google) + let result = await googleAuthProvider.signIn(withPresenting: vc) - result.success { success(with: .google($0)) } - result.failure(failure) + result.success { success(with: .google, details: .google($0)) } + result.failure { failure(with: .google, error: $0) } } @MainActor @@ -158,9 +168,11 @@ final public class SocialAuthViewModel: ObservableObject { guard let vc = topViewController else { return } + trackClickEvent(for: .facebook) + let result = await facebookAuthProvider.signIn(withPresenting: vc) - result.success { success(with: .facebook($0)) } - result.failure(failure) + result.success { success(with: .facebook, details: .facebook($0)) } + result.failure { failure(with: .facebook, error: $0) } } @MainActor @@ -168,17 +180,31 @@ final public class SocialAuthViewModel: ObservableObject { guard let vc = topViewController else { return } + trackClickEvent(for: .microsoft) + let result = await microsoftAuthProvider.signIn(withPresenting: vc) - result.success { success(with: .microsoft($0)) } - result.failure(failure) + result.success { success(with: .microsoft, details: .microsoft($0)) } + result.failure { failure(with: .microsoft, error: $0) } } - private func success(with social: SocialAuthDetails) { - completion(.success(social)) + private func success(with method: SocialAuthMethod, details: SocialAuthDetails) { + completion(method, .success(details)) } - private func failure(_ error: Error) { - completion(.failure(error)) + private func failure(with method: SocialAuthMethod, error: SocialAuthError) { + completion(method, .failure(error)) } + // MARK: - Analytics + + private func trackClickEvent(for method: SocialAuthMethod) { + let analyticsValue = AuthMethod.socailAuth(method).analyticsValue + + switch authType { + case .signIn: + analytics.userSignInClicked(method: analyticsValue) + case .register: + analytics.socialRegisterClicked(method: analyticsValue) + } + } } diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index 6b4c4742..a0714d84 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -533,16 +533,22 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { perform?() } - open func userSignInClicked() { - addInvocation(.m_userSignInClicked) - let perform = methodPerformValue(.m_userSignInClicked) as? () -> Void - perform?() + open func userSignInClicked(method: String) { + addInvocation(.m_userSignInClicked__method_method(Parameter.value(`method`))) + let perform = methodPerformValue(.m_userSignInClicked__method_method(Parameter.value(`method`))) as? (String) -> Void + perform?(`method`) } - open func createAccountClicked() { - addInvocation(.m_createAccountClicked) - let perform = methodPerformValue(.m_createAccountClicked) as? () -> Void - perform?() + open func socialRegisterClicked(method: String) { + addInvocation(.m_socialRegisterClicked__method_method(Parameter.value(`method`))) + let perform = methodPerformValue(.m_socialRegisterClicked__method_method(Parameter.value(`method`))) as? (String) -> Void + perform?(`method`) + } + + open func createAccountClicked(method: String) { + addInvocation(.m_createAccountClicked__method_method(Parameter.value(`method`))) + let perform = methodPerformValue(.m_createAccountClicked__method_method(Parameter.value(`method`))) as? (String) -> Void + perform?(`method`) } open func registrationSuccess(method: String) { @@ -551,6 +557,24 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { perform?(`method`) } + open func socialAuthFailure(method: String, errorCode: String?, errorMessage: String?) { + addInvocation(.m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter.value(`method`), Parameter.value(`errorCode`), Parameter.value(`errorMessage`))) + let perform = methodPerformValue(.m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter.value(`method`), Parameter.value(`errorCode`), Parameter.value(`errorMessage`))) as? (String, String?, String?) -> Void + perform?(`method`, `errorCode`, `errorMessage`) + } + + open func registerFailure(method: String, errorCode: String?, errorMessage: String?) { + addInvocation(.m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter.value(`method`), Parameter.value(`errorCode`), Parameter.value(`errorMessage`))) + let perform = methodPerformValue(.m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter.value(`method`), Parameter.value(`errorCode`), Parameter.value(`errorMessage`))) as? (String, String?, String?) -> Void + perform?(`method`, `errorCode`, `errorMessage`) + } + + open func signInFailure(method: String, errorCode: String?, errorMessage: String?) { + addInvocation(.m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter.value(`method`), Parameter.value(`errorCode`), Parameter.value(`errorMessage`))) + let perform = methodPerformValue(.m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter.value(`method`), Parameter.value(`errorCode`), Parameter.value(`errorMessage`))) as? (String, String?, String?) -> Void + perform?(`method`, `errorCode`, `errorMessage`) + } + open func forgotPasswordClicked() { addInvocation(.m_forgotPasswordClicked) let perform = methodPerformValue(.m_forgotPasswordClicked) as? () -> Void @@ -581,9 +605,13 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case m_userLogin__method_method(Parameter) case m_registerClicked case m_signInClicked - case m_userSignInClicked - case m_createAccountClicked + case m_userSignInClicked__method_method(Parameter) + case m_socialRegisterClicked__method_method(Parameter) + case m_createAccountClicked__method_method(Parameter) case m_registrationSuccess__method_method(Parameter) + case m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter, Parameter, Parameter) + case m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter, Parameter, Parameter) + case m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(Parameter, Parameter, Parameter) case m_forgotPasswordClicked case m_resetPasswordClicked case m_resetPassword__success_success(Parameter) @@ -607,15 +635,47 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case (.m_signInClicked, .m_signInClicked): return .match - case (.m_userSignInClicked, .m_userSignInClicked): return .match + case (.m_userSignInClicked__method_method(let lhsMethod), .m_userSignInClicked__method_method(let rhsMethod)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsMethod, rhs: rhsMethod, with: matcher), lhsMethod, rhsMethod, "method")) + return Matcher.ComparisonResult(results) - case (.m_createAccountClicked, .m_createAccountClicked): return .match + case (.m_socialRegisterClicked__method_method(let lhsMethod), .m_socialRegisterClicked__method_method(let rhsMethod)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsMethod, rhs: rhsMethod, with: matcher), lhsMethod, rhsMethod, "method")) + return Matcher.ComparisonResult(results) + + case (.m_createAccountClicked__method_method(let lhsMethod), .m_createAccountClicked__method_method(let rhsMethod)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsMethod, rhs: rhsMethod, with: matcher), lhsMethod, rhsMethod, "method")) + return Matcher.ComparisonResult(results) case (.m_registrationSuccess__method_method(let lhsMethod), .m_registrationSuccess__method_method(let rhsMethod)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsMethod, rhs: rhsMethod, with: matcher), lhsMethod, rhsMethod, "method")) return Matcher.ComparisonResult(results) + case (.m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(let lhsMethod, let lhsErrorcode, let lhsErrormessage), .m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(let rhsMethod, let rhsErrorcode, let rhsErrormessage)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsMethod, rhs: rhsMethod, with: matcher), lhsMethod, rhsMethod, "method")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsErrorcode, rhs: rhsErrorcode, with: matcher), lhsErrorcode, rhsErrorcode, "errorCode")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsErrormessage, rhs: rhsErrormessage, with: matcher), lhsErrormessage, rhsErrormessage, "errorMessage")) + return Matcher.ComparisonResult(results) + + case (.m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(let lhsMethod, let lhsErrorcode, let lhsErrormessage), .m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(let rhsMethod, let rhsErrorcode, let rhsErrormessage)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsMethod, rhs: rhsMethod, with: matcher), lhsMethod, rhsMethod, "method")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsErrorcode, rhs: rhsErrorcode, with: matcher), lhsErrorcode, rhsErrorcode, "errorCode")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsErrormessage, rhs: rhsErrormessage, with: matcher), lhsErrormessage, rhsErrormessage, "errorMessage")) + return Matcher.ComparisonResult(results) + + case (.m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(let lhsMethod, let lhsErrorcode, let lhsErrormessage), .m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(let rhsMethod, let rhsErrorcode, let rhsErrormessage)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsMethod, rhs: rhsMethod, with: matcher), lhsMethod, rhsMethod, "method")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsErrorcode, rhs: rhsErrorcode, with: matcher), lhsErrorcode, rhsErrorcode, "errorCode")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsErrormessage, rhs: rhsErrormessage, with: matcher), lhsErrormessage, rhsErrormessage, "errorMessage")) + return Matcher.ComparisonResult(results) + case (.m_forgotPasswordClicked, .m_forgotPasswordClicked): return .match case (.m_resetPasswordClicked, .m_resetPasswordClicked): return .match @@ -640,9 +700,13 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case let .m_userLogin__method_method(p0): return p0.intValue case .m_registerClicked: return 0 case .m_signInClicked: return 0 - case .m_userSignInClicked: return 0 - case .m_createAccountClicked: return 0 + case let .m_userSignInClicked__method_method(p0): return p0.intValue + case let .m_socialRegisterClicked__method_method(p0): return p0.intValue + case let .m_createAccountClicked__method_method(p0): return p0.intValue case let .m_registrationSuccess__method_method(p0): return p0.intValue + case let .m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case .m_forgotPasswordClicked: return 0 case .m_resetPasswordClicked: return 0 case let .m_resetPassword__success_success(p0): return p0.intValue @@ -655,9 +719,13 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case .m_userLogin__method_method: return ".userLogin(method:)" case .m_registerClicked: return ".registerClicked()" case .m_signInClicked: return ".signInClicked()" - case .m_userSignInClicked: return ".userSignInClicked()" - case .m_createAccountClicked: return ".createAccountClicked()" + case .m_userSignInClicked__method_method: return ".userSignInClicked(method:)" + case .m_socialRegisterClicked__method_method: return ".socialRegisterClicked(method:)" + case .m_createAccountClicked__method_method: return ".createAccountClicked(method:)" case .m_registrationSuccess__method_method: return ".registrationSuccess(method:)" + case .m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage: return ".socialAuthFailure(method:errorCode:errorMessage:)" + case .m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage: return ".registerFailure(method:errorCode:errorMessage:)" + case .m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage: return ".signInFailure(method:errorCode:errorMessage:)" case .m_forgotPasswordClicked: return ".forgotPasswordClicked()" case .m_resetPasswordClicked: return ".resetPasswordClicked()" case .m_resetPassword__success_success: return ".resetPassword(success:)" @@ -684,9 +752,13 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func userLogin(method: Parameter) -> Verify { return Verify(method: .m_userLogin__method_method(`method`))} public static func registerClicked() -> Verify { return Verify(method: .m_registerClicked)} public static func signInClicked() -> Verify { return Verify(method: .m_signInClicked)} - public static func userSignInClicked() -> Verify { return Verify(method: .m_userSignInClicked)} - public static func createAccountClicked() -> Verify { return Verify(method: .m_createAccountClicked)} + public static func userSignInClicked(method: Parameter) -> Verify { return Verify(method: .m_userSignInClicked__method_method(`method`))} + public static func socialRegisterClicked(method: Parameter) -> Verify { return Verify(method: .m_socialRegisterClicked__method_method(`method`))} + public static func createAccountClicked(method: Parameter) -> Verify { return Verify(method: .m_createAccountClicked__method_method(`method`))} public static func registrationSuccess(method: Parameter) -> Verify { return Verify(method: .m_registrationSuccess__method_method(`method`))} + public static func socialAuthFailure(method: Parameter, errorCode: Parameter, errorMessage: Parameter) -> Verify { return Verify(method: .m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(`method`, `errorCode`, `errorMessage`))} + public static func registerFailure(method: Parameter, errorCode: Parameter, errorMessage: Parameter) -> Verify { return Verify(method: .m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(`method`, `errorCode`, `errorMessage`))} + public static func signInFailure(method: Parameter, errorCode: Parameter, errorMessage: Parameter) -> Verify { return Verify(method: .m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(`method`, `errorCode`, `errorMessage`))} public static func forgotPasswordClicked() -> Verify { return Verify(method: .m_forgotPasswordClicked)} public static func resetPasswordClicked() -> Verify { return Verify(method: .m_resetPasswordClicked)} public static func resetPassword(success: Parameter) -> Verify { return Verify(method: .m_resetPassword__success_success(`success`))} @@ -709,15 +781,27 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func signInClicked(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_signInClicked, performs: perform) } - public static func userSignInClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_userSignInClicked, performs: perform) + public static func userSignInClicked(method: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_userSignInClicked__method_method(`method`), performs: perform) } - public static func createAccountClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_createAccountClicked, performs: perform) + public static func socialRegisterClicked(method: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_socialRegisterClicked__method_method(`method`), performs: perform) + } + public static func createAccountClicked(method: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_createAccountClicked__method_method(`method`), performs: perform) } public static func registrationSuccess(method: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_registrationSuccess__method_method(`method`), performs: perform) } + public static func socialAuthFailure(method: Parameter, errorCode: Parameter, errorMessage: Parameter, perform: @escaping (String, String?, String?) -> Void) -> Perform { + return Perform(method: .m_socialAuthFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(`method`, `errorCode`, `errorMessage`), performs: perform) + } + public static func registerFailure(method: Parameter, errorCode: Parameter, errorMessage: Parameter, perform: @escaping (String, String?, String?) -> Void) -> Perform { + return Perform(method: .m_registerFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(`method`, `errorCode`, `errorMessage`), performs: perform) + } + public static func signInFailure(method: Parameter, errorCode: Parameter, errorMessage: Parameter, perform: @escaping (String, String?, String?) -> Void) -> Perform { + return Perform(method: .m_signInFailure__method_methoderrorCode_errorCodeerrorMessage_errorMessage(`method`, `errorCode`, `errorMessage`), performs: perform) + } public static func forgotPasswordClicked(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_forgotPasswordClicked, performs: perform) } diff --git a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift index a5784f8a..77f2d3ea 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift @@ -112,14 +112,14 @@ final class SignInViewModelTests: XCTestCase { sourceScreen: .default ) - let result: Result = .success(.apple( + let result: Result = .success(.apple( .init(name: "name", email: "email", token: "239i2oi3jrf2jflkj23lf2f")) ) let user = User(id: 1, username: "username", email: "edxUser@edx.com", name: "Name", userAvatar: "") Given(interactor, .login(externalToken: .any, backend: .any, willReturn: user)) - await viewModel.login(with: result) + await viewModel.login(with: .apple, result: result) Verify(interactor, 1, .login(externalToken: .any, backend: .any)) Verify(analytics, .userLogin(method: .any)) @@ -144,7 +144,7 @@ final class SignInViewModelTests: XCTestCase { sourceScreen: .default ) - let result: Result = .success( + let result: Result = .success( .apple(.init(name: "name", email: "email", token: "239i2oi3jrf2jflkj23lf2f")) ) let validationErrorMessage = AuthLocalization.Error.accountNotRegistered( @@ -156,7 +156,7 @@ final class SignInViewModelTests: XCTestCase { Given(interactor, .login(externalToken: .any, backend: .any, willThrow: error)) - await viewModel.login(with: result) + await viewModel.login(with: .apple, result: result) Verify(interactor, 1, .login(externalToken: .any, backend: .any)) Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any, postLoginData: .any)) diff --git a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift index 90144715..c927db1a 100644 --- a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift @@ -291,6 +291,6 @@ final class SignUpViewModelTests: XCTestCase { viewModel.trackCreateAccountClicked() - Verify(analytics, 1, .createAccountClicked()) + Verify(analytics, 1, .createAccountClicked(method: .any)) } } diff --git a/Core/Core/Analytics/CoreAnalytics.swift b/Core/Core/Analytics/CoreAnalytics.swift index 0a2760ce..c1465ba0 100644 --- a/Core/Core/Analytics/CoreAnalytics.swift +++ b/Core/Core/Analytics/CoreAnalytics.swift @@ -231,7 +231,11 @@ public enum AnalyticsEvent: String { case signInClicked = "Logistration:Sign In Clicked" case userSignInClicked = "Logistration:User Sign In Clicked" case createAccountClicked = "Logistration:Create Account Clicked" + case socialRegisterClicked = "Logistration:Social Register Clicked" case registrationSuccess = "Logistration:Register Success" + case socialAuthFailure = "Logistration:Social Auth Failure" + case registerFailure = "Logistration:Register Failure" + case signInFailure = "Logistration:Sign In Failure" case userLogout = "Profile:Logged Out" case userLogoutClicked = "Profile:Logout Clicked" case forgotPasswordClicked = "Logistration:Forgot Password Clicked" @@ -350,6 +354,10 @@ public enum EventBIValue: String { case registrationSuccess = "edx.bi.app.user.register.success" case userSignInClicked = "edx.bi.app.logistration.user.signin.clicked" case createAccountClicked = "edx.bi.app.logistration.user.create_account.clicked" + case socialRegisterClicked = "edx.bi.app.logistration.social.register.clicked" + case socialAuthFailure = "edx.bi.app.logistration.social.auth.failure" + case registerFailure = "edx.bi.app.user.register.failure" + case signInFailure = "edx.bi.app.user.signin.failure" case forgotPasswordClicked = "edx.bi.app.logistration.forgot_password.clicked" case resetPasswordClicked = "edx.bi.app.user.reset_password.clicked" case resetPasswordSuccess = "edx.bi.app.user.reset_password.success" @@ -495,6 +503,8 @@ public struct EventParamKey { public static let localizedCurrencyCode = "localized_currency_code" public static let lmsPrice = "lms_usd_price" public static let error = "error" + public static let errorCode = "error_code" + public static let errorMessage = "error_message" public static let errorAction = "error_action" public static let flowType = "flow_type" public static let alertType = "alert_type" diff --git a/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift b/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift index 1ed3d4c0..21ce777a 100644 --- a/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift +++ b/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift @@ -18,10 +18,10 @@ public final class AppleAuthProvider: NSObject, ASAuthorizationControllerDelegat super.init() } - private var completion: ((Result) -> Void)? + private var completion: ((Result) -> Void)? private let appleIDProvider = ASAuthorizationAppleIDProvider() - public func request(completion: ((Result) -> Void)?) { + public func request(completion: ((Result) -> Void)?) { let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] @@ -37,7 +37,7 @@ public final class AppleAuthProvider: NSObject, ASAuthorizationControllerDelegat didCompleteWithAuthorization authorization: ASAuthorization ) { guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential else { - completion?(.failure(SocialAuthError.unknownError)) + completion?(.failure(SocialAuthError.unknownError())) return } @@ -58,7 +58,7 @@ public final class AppleAuthProvider: NSObject, ASAuthorizationControllerDelegat guard let data = credentials.identityToken, let code = String(data: data, encoding: .utf8) else { - completion?(.failure(SocialAuthError.unknownError)) + completion?(.failure(SocialAuthError.unknownError())) return } @@ -81,14 +81,20 @@ public final class AppleAuthProvider: NSObject, ASAuthorizationControllerDelegat completion?(.failure(failure(ASAuthorizationError(_nsError: error as NSError)))) } - private func failure(_ error: ASAuthorizationError) -> Error { + private func failure(_ error: ASAuthorizationError) -> SocialAuthError { switch error.code { case .canceled: - return SocialAuthError.socialAuthCanceled + return SocialAuthError.socialAuthCanceled(code: error.code.rawValue) case .failed: - return SocialAuthError.error(text: CoreLocalization.Error.authorizationFailed) + return SocialAuthError.error( + code: error.code.rawValue, + text: CoreLocalization.Error.authorizationFailed + ) default: - return error + return SocialAuthError.error( + code: error.code.rawValue, + text: error.localizedDescription + ) } } } diff --git a/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift b/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift index a9167451..592eaf2e 100644 --- a/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift +++ b/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift @@ -8,15 +8,26 @@ import Foundation public enum SocialAuthError: Error { - case error(text: String) - case socialAuthCanceled - case unknownError + case error(code: Int? = nil, text: String) + case socialAuthCanceled(code: Int? = nil) + case unknownError(code: Int? = nil) } extension SocialAuthError: LocalizedError { + public var errorCode: Int? { + switch self { + case .error(let code, _): + return code + case .socialAuthCanceled(let code): + return code + case .unknownError(let code): + return code + } + } + public var errorDescription: String? { switch self { - case .error(let text): + case .error(_, let text): return text case .socialAuthCanceled: return CoreLocalization.socialSignCanceled diff --git a/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift b/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift index 66ae46b6..405caa18 100644 --- a/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift +++ b/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift @@ -17,27 +17,34 @@ public final class FacebookAuthProvider { @MainActor public func signIn( withPresenting: UIViewController - ) async -> Result { + ) async -> Result { await withCheckedContinuation { continuation in loginManager.logIn( permissions: [], from: withPresenting ) { result, error in if let error = error { - continuation.resume(returning: .failure(error)) + continuation.resume( + returning: .failure( + SocialAuthError.error( + code: (error as NSError).code, + text: error.localizedDescription + ) + ) + ) return } guard let result = result, let tokenString = AccessToken.current?.tokenString else { continuation.resume( - returning: .failure(SocialAuthError.unknownError) + returning: .failure(SocialAuthError.unknownError()) ) return } if result.isCancelled { - continuation.resume(returning: .failure(SocialAuthError.socialAuthCanceled)) + continuation.resume(returning: .failure(SocialAuthError.socialAuthCanceled())) return } @@ -75,12 +82,4 @@ public final class FacebookAuthProvider { public func signOut() { loginManager.logOut() } - - private func failure(_ error: Error?) -> Error { - if let error = error as? NSError, - let description = error.userInfo[ErrorLocalizedDescriptionKey] as? String { - return SocialAuthError.error(text: description) - } - return error ?? SocialAuthError.unknownError - } } diff --git a/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift b/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift index c600c173..04ebdff9 100644 --- a/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift +++ b/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift @@ -15,19 +15,23 @@ public final class GoogleAuthProvider { @MainActor public func signIn( withPresenting: UIViewController - ) async -> Result { + ) async -> Result { await withCheckedContinuation { continuation in GIDSignIn.sharedInstance.signIn( withPresenting: withPresenting, completion: { result, error in if let error = error as? NSError, error.code == GIDSignInError.canceled.rawValue { - continuation.resume(returning: .failure(SocialAuthError.socialAuthCanceled)) + continuation.resume( + returning: .failure( + SocialAuthError.socialAuthCanceled(code: error.code) + ) + ) return } guard let result = result else { continuation.resume( returning: .failure( - SocialAuthError.error(text: CoreLocalization.Error.unknownError) + SocialAuthError.unknownError(code: (error as? NSError)?.code) ) ) return diff --git a/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift b/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift index 878eb427..0e7d2895 100644 --- a/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift +++ b/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift @@ -21,7 +21,7 @@ public final class MicrosoftAuthProvider { @MainActor public func signIn( withPresenting: UIViewController - ) async -> Result { + ) async -> Result { await withCheckedContinuation { continuation in do { let clientApplication = try createClientApplication() @@ -30,14 +30,21 @@ public final class MicrosoftAuthProvider { let parameters = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webParameters) clientApplication.acquireToken(with: parameters) { result, error in if let error = error { - continuation.resume(returning: .failure(error)) + continuation.resume( + returning: .failure( + SocialAuthError.error( + code: (error as NSError).code, + text: error.localizedDescription + ) + ) + ) return } guard let result = result else { continuation.resume( returning: .failure( - SocialAuthError.error(text: CoreLocalization.Error.unknownError) + SocialAuthError.unknownError() ) ) return @@ -57,7 +64,14 @@ public final class MicrosoftAuthProvider { ) } } catch let error { - continuation.resume(returning: .failure(error)) + continuation.resume( + returning: .failure( + SocialAuthError.error( + code: (error as NSError).code, + text: error.localizedDescription + ) + ) + ) } } } @@ -85,7 +99,10 @@ public final class MicrosoftAuthProvider { do { return try MSALPublicClientApplication(configuration: configuration) } catch { - throw SocialAuthError.error(text: error.localizedDescription) + throw SocialAuthError.error( + code: (error as NSError).code, + text: error.localizedDescription + ) } } @@ -99,18 +116,7 @@ public final class MicrosoftAuthProvider { return account } - - private func failure(_ error: Error?) -> Error { - if let error = error as? NSError, - let description = error.userInfo[MSALErrorDescriptionKey] as? String { - if let errorCode = MSALError(rawValue: error.code), case .userCanceled = errorCode { - return SocialAuthError.socialAuthCanceled - } - return SocialAuthError.error(text: description) - } - return error ?? SocialAuthError.error(text: CoreLocalization.Error.unknownError) - } - + func getUser(completion: (MSALAccount) -> Void) { guard let user = result?.account else { return } completion(user) diff --git a/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift b/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift index cd609b00..e188d9bd 100644 --- a/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift +++ b/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift @@ -152,12 +152,28 @@ class AnalyticsManager: AuthorizationAnalytics, trackEvent(.signInClicked, biValue: .signInClicked) } - public func userSignInClicked() { - trackEvent(.userSignInClicked, biValue: .userSignInClicked) + public func userSignInClicked(method: String) { + trackEvent( + .userSignInClicked, + biValue: .userSignInClicked, + parameters: [EventParamKey.method: method] + ) + } + + public func socialRegisterClicked(method: String) { + trackEvent( + .socialRegisterClicked, + biValue: .socialRegisterClicked, + parameters: [EventParamKey.method: method] + ) } - public func createAccountClicked() { - trackEvent(.createAccountClicked, biValue: .createAccountClicked) + public func createAccountClicked(method: String) { + trackEvent( + .createAccountClicked, + biValue: .createAccountClicked, + parameters: [EventParamKey.method: method] + ) } public func registrationSuccess(method: String) { @@ -168,6 +184,48 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(.registrationSuccess, parameters: parameters) } + public func socialAuthFailure(method: String, errorCode: String?, errorMessage: String?) { + var parameters: [String: Any] = [ + EventParamKey.method: method + ] + parameters.setObjectOrNil(errorCode, forKey: EventParamKey.errorCode) + parameters.setObjectOrNil(errorMessage, forKey: EventParamKey.errorMessage) + + trackEvent( + .socialAuthFailure, + biValue: .socialAuthFailure, + parameters: parameters + ) + } + + public func registerFailure(method: String, errorCode: String?, errorMessage: String?) { + var parameters: [String: Any] = [ + EventParamKey.method: method + ] + parameters.setObjectOrNil(errorCode, forKey: EventParamKey.errorCode) + parameters.setObjectOrNil(errorMessage, forKey: EventParamKey.errorMessage) + + trackEvent( + .registerFailure, + biValue: .registerFailure, + parameters: parameters + ) + } + + public func signInFailure(method: String, errorCode: String?, errorMessage: String?) { + var parameters: [String: Any] = [ + EventParamKey.method: method + ] + parameters.setObjectOrNil(errorCode, forKey: EventParamKey.errorCode) + parameters.setObjectOrNil(errorMessage, forKey: EventParamKey.errorMessage) + + trackEvent( + .signInFailure, + biValue: .signInFailure, + parameters: parameters + ) + } + public func forgotPasswordClicked() { trackEvent(.forgotPasswordClicked, biValue: .forgotPasswordClicked) } From 4e1b31f4ba70cc4cdd5a682d94d48fa80936db3d Mon Sep 17 00:00:00 2001 From: Tayyab Akram Date: Tue, 4 Feb 2025 16:03:01 +0500 Subject: [PATCH 2/2] build: update action versions in unit tests workflow --- .github/workflows/unit_tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index fa683726..9ba8d0c2 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -24,7 +24,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} @@ -43,7 +43,7 @@ jobs: run: bundle exec fastlane unit_tests - name: Archive artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: test-output @@ -52,6 +52,6 @@ jobs: if-no-files-found: ignore - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: flags: unittests